import type {
  ISamsungtvApp,
  ISamsungtvConfig,
  ISamsungtvDeviceInfoResponse,
  ISamsungtvEvent,
  ISamsungtvSnapshot,
  ISamsungtvState,
  ISamsungtvWebsocketCommand,
  TSamsungtvCommand,
  TSamsungtvCommandAction,
  TSamsungtvRemoteKey,
} from './samsungtv.types.js';

type TWebSocketMessage = { data: unknown };
type TWebSocketHandler = (eventArg: any) => void;
type TWebSocketLike = {
  send(dataArg: string): void;
  close(codeArg?: number, reasonArg?: string): void;
  addEventListener?: (eventArg: 'open' | 'message' | 'error' | 'close', handlerArg: TWebSocketHandler) => void;
  removeEventListener?: (eventArg: 'open' | 'message' | 'error' | 'close', handlerArg: TWebSocketHandler) => void;
  onopen?: TWebSocketHandler | null;
  onmessage?: TWebSocketHandler | null;
  onerror?: TWebSocketHandler | null;
  onclose?: TWebSocketHandler | null;
};
type TWebSocketConstructor = new (urlArg: string) => TWebSocketLike;

const defaultWebsocketPort = 8001;
const encryptedWebsocketPort = 8000;
const legacyPort = 55000;
const defaultKeyPressDelayMs = 250;

export class SamsungtvClient {
  private readonly sockets = new Set<TWebSocketLike>();
  private token?: string;

  constructor(private readonly config: ISamsungtvConfig) {
    this.token = config.token;
  }

  public async getSnapshot(): Promise<ISamsungtvSnapshot> {
    if (this.config.snapshot) {
      return this.config.snapshot;
    }

    const deviceInfo = await this.getDeviceInfo();
    const apps = await this.getApps();
    const activeApp = this.config.activeApp;
    return {
      deviceInfo,
      apps,
      activeApp,
      state: this.getState(deviceInfo, activeApp),
    };
  }

  public async getDeviceInfo(): Promise<ISamsungtvDeviceInfoResponse> {
    if (this.config.snapshot?.deviceInfo) {
      return this.config.snapshot.deviceInfo;
    }
    if (this.config.deviceInfo) {
      return this.config.deviceInfo;
    }

    if (this.config.host) {
      try {
        return await this.requestRestDeviceInfo();
      } catch {
        return this.manualDeviceInfo();
      }
    }

    return this.manualDeviceInfo();
  }

  public async getApps(): Promise<ISamsungtvApp[]> {
    return this.config.snapshot?.apps ?? this.config.apps ?? [];
  }

  public async sendKeys(keysArg: Array<TSamsungtvRemoteKey | string>, actionArg: TSamsungtvCommandAction = 'Click'): Promise<void> {
    const keys = keysArg.map((keyArg) => this.normalizeKey(String(keyArg))).filter(Boolean);
    if (!keys.length) {
      return;
    }

    await this.sendWebsocketCommands(keys.map((keyArg) => this.createRemoteKeyCommand(keyArg, actionArg)));
  }

  public async sendCommand(commandArg: TSamsungtvCommand): Promise<void> {
    if ('type' in commandArg) {
      if (commandArg.type === 'key') {
        await this.sendKeys([commandArg.key], commandArg.action);
        return;
      }
      await this.launchApp(commandArg.appId, commandArg.appType, commandArg.metaTag);
      return;
    }

    await this.sendWebsocketCommands([commandArg]);
  }

  public async turnOn(): Promise<void> {
    await this.sendKeys(['KEY_POWERON']);
    this.updateLocalState({ power: 'on' });
  }

  public async turnOff(): Promise<void> {
    await this.sendKeys(['KEY_POWEROFF']);
    this.updateLocalState({ power: 'off', playback: 'off' });
  }

  public async play(): Promise<void> {
    await this.sendKeys(['KEY_PLAY']);
    this.updateLocalState({ playback: 'playing' });
  }

  public async pause(): Promise<void> {
    await this.sendKeys(['KEY_PAUSE']);
    this.updateLocalState({ playback: 'paused' });
  }

  public async playPause(): Promise<void> {
    await this.sendKeys(['KEY_PLAYPAUSE']);
  }

  public async setVolumeLevel(volumeLevelArg: number): Promise<void> {
    void volumeLevelArg;
    throw new Error('Samsung TV absolute volume_set requires UPnP RenderingControl and is not implemented by this native TypeScript port.');
  }

  public async launchApp(appIdArg: string, appTypeArg = 'DEEP_LINK', metaTagArg = ''): Promise<void> {
    if (!appIdArg) {
      throw new Error('Samsung TV launch_app requires an app id.');
    }

    await this.sendWebsocketCommands([{
      method: 'ms.channel.emit',
      params: {
        event: 'ed.apps.launch',
        to: 'host',
        data: {
          action_type: appTypeArg,
          appId: appIdArg,
          metaTag: metaTagArg,
        },
      },
    }]);

    const app = (await this.getApps()).find((appArg) => appArg.id === appIdArg) ?? { id: appIdArg, name: appIdArg };
    this.setActiveApp(app);
  }

  public async selectSource(sourceArg: string): Promise<void> {
    const source = sourceArg.trim();
    if (!source) {
      throw new Error('Samsung TV select_source requires a source name.');
    }

    const sourceKeys: Record<string, string> = {
      tv: 'KEY_TV',
      hdmi: 'KEY_HDMI',
      source: 'KEY_SOURCE',
    };
    const sourceKey = sourceKeys[source.toLowerCase()];
    if (sourceKey) {
      await this.sendKeys([sourceKey]);
      this.updateLocalState({ source });
      return;
    }

    const app = (await this.getApps()).find((appArg) => appArg.id === source || appArg.name === source);
    if (app) {
      await this.launchApp(app.id, app.appType || app.type || 'DEEP_LINK');
      return;
    }

    if (source.toUpperCase().startsWith('KEY_')) {
      await this.sendKeys([source]);
      this.updateLocalState({ source });
      return;
    }

    throw new Error(`Samsung TV source is not known: ${source}`);
  }

  public async destroy(): Promise<void> {
    for (const socket of this.sockets) {
      socket.close();
    }
    this.sockets.clear();
  }

  private async sendWebsocketCommands(commandsArg: ISamsungtvWebsocketCommand[]): Promise<void> {
    this.assertLiveWebsocketSupported();
    await this.withWebSocket(async (socketArg) => {
      for (const command of commandsArg) {
        socketArg.send(JSON.stringify(command));
        await this.delay(this.config.keyPressDelayMs ?? defaultKeyPressDelayMs);
      }
    });
  }

  private async withWebSocket(runArg: (socketArg: TWebSocketLike) => Promise<void>): Promise<void> {
    const WebSocketCtor = (globalThis as typeof globalThis & { WebSocket?: TWebSocketConstructor }).WebSocket;
    if (!WebSocketCtor) {
      throw new Error('Global WebSocket is not available for Samsung TV websocket control.');
    }

    const socket = new WebSocketCtor(this.websocketUrl());
    this.sockets.add(socket);
    try {
      await this.waitForChannelConnect(socket);
      await runArg(socket);
    } finally {
      this.sockets.delete(socket);
      socket.close();
    }
  }

  private waitForChannelConnect(socketArg: TWebSocketLike): Promise<void> {
    return new Promise((resolve, reject) => {
      let settled = false;
      const cleanups: Array<() => void> = [];
      const timeout = setTimeout(() => finish(new Error('Samsung TV websocket did not complete channel connect.')), this.config.connectTimeoutMs ?? 5000);

      const finish = (errorArg?: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        clearTimeout(timeout);
        for (const cleanup of cleanups) {
          cleanup();
        }
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve();
      };

      cleanups.push(this.addSocketListener(socketArg, 'message', (messageArg) => {
        const event = this.parseWebSocketMessage((messageArg as TWebSocketMessage).data);
        if (!event) {
          return;
        }
        if (event.token) {
          this.token = event.token;
        }
        if (event.event === 'ms.channel.unauthorized') {
          finish(new Error('Samsung TV websocket access was denied or the token is invalid.'));
          return;
        }
        if (event.event === 'ms.error') {
          finish(new Error(`Samsung TV websocket returned an error event: ${JSON.stringify(event.data)}`));
          return;
        }
        if (event.event === 'ms.channel.connect') {
          finish();
        }
      }));
      cleanups.push(this.addSocketListener(socketArg, 'error', (errorArg) => finish(new Error(`Samsung TV websocket failed: ${this.errorMessage(errorArg)}`))));
      cleanups.push(this.addSocketListener(socketArg, 'close', () => finish(new Error('Samsung TV websocket closed before channel connect.'))));
    });
  }

  private addSocketListener(socketArg: TWebSocketLike, eventArg: 'open' | 'message' | 'error' | 'close', handlerArg: TWebSocketHandler): () => void {
    if (socketArg.addEventListener) {
      socketArg.addEventListener(eventArg, handlerArg);
      return () => socketArg.removeEventListener?.(eventArg, handlerArg);
    }

    const key = `on${eventArg}` as 'onopen' | 'onmessage' | 'onerror' | 'onclose';
    const previous = socketArg[key];
    socketArg[key] = handlerArg;
    return () => {
      if (socketArg[key] === handlerArg) {
        socketArg[key] = previous;
      }
    };
  }

  private createRemoteKeyCommand(keyArg: string, actionArg: TSamsungtvCommandAction): ISamsungtvWebsocketCommand {
    return {
      method: 'ms.remote.control',
      params: {
        Cmd: actionArg,
        DataOfCmd: keyArg,
        Option: 'false',
        TypeOfRemote: 'SendRemoteKey',
      },
    };
  }

  private assertLiveWebsocketSupported(): void {
    if (!this.config.host) {
      throw new Error('Samsung TV host is required for live websocket control.');
    }

    const port = this.config.port ?? defaultWebsocketPort;
    if (this.config.method === 'encrypted' || this.config.sessionId || port === encryptedWebsocketPort) {
      throw new Error('Samsung TV encrypted websocket/PIN protocol is not supported by this native TypeScript port.');
    }

    if (this.config.method === 'legacy' || port === legacyPort) {
      throw new Error('Samsung TV legacy TCP remote protocol is not supported by this native TypeScript port.');
    }
  }

  private websocketUrl(): string {
    const host = this.config.host;
    if (!host) {
      throw new Error('Samsung TV host is required for websocket control.');
    }
    const port = this.config.port ?? defaultWebsocketPort;
    const protocol = port === 8002 ? 'wss' : 'ws';
    const params = new URLSearchParams({
      name: Buffer.from(this.config.websocketName || 'smarthome.exchange').toString('base64'),
    });
    if (this.token) {
      params.set('token', this.token);
    }
    return `${protocol}://${host}:${port}/api/v2/channels/samsung.remote.control?${params.toString()}`;
  }

  private async requestRestDeviceInfo(): Promise<ISamsungtvDeviceInfoResponse> {
    return this.requestJson<ISamsungtvDeviceInfoResponse>('');
  }

  private async requestJson<TResult>(routeArg: string): Promise<TResult> {
    if (!this.config.host) {
      throw new Error('Samsung TV host is required for REST device information.');
    }
    const response = await globalThis.fetch(`${this.restBaseUrl()}${routeArg}`);
    const text = await response.text();
    if (!response.ok) {
      throw new Error(`Samsung TV REST request failed with HTTP ${response.status}: ${text}`);
    }
    return (text ? JSON.parse(text) : {}) as TResult;
  }

  private restBaseUrl(): string {
    const port = this.config.port ?? defaultWebsocketPort;
    const protocol = port === 8002 ? 'https' : 'http';
    return `${protocol}://${this.config.host}:${port}/api/v2/`;
  }

  private manualDeviceInfo(): ISamsungtvDeviceInfoResponse {
    const name = this.config.name || this.config.model || this.config.host || 'Samsung Smart TV';
    return {
      id: this.config.macAddress || this.config.host || name,
      device: {
        type: 'Samsung SmartTV',
        name,
        modelName: this.config.model,
        wifiMac: this.config.macAddress,
        manufacturer: this.config.manufacturer || 'Samsung',
        PowerState: this.config.state?.power === 'on' ? 'on' : this.config.state?.power === 'off' ? 'standby' : undefined,
      },
    };
  }

  private getState(deviceInfoArg: ISamsungtvDeviceInfoResponse, activeAppArg?: ISamsungtvApp): ISamsungtvState {
    const configuredState = this.config.snapshot?.state ?? this.config.state ?? {};
    const power = configuredState.power ?? this.powerFromDeviceInfo(deviceInfoArg);
    const appId = configuredState.appId ?? activeAppArg?.id;
    const appName = configuredState.appName ?? activeAppArg?.name;
    const source = configuredState.source ?? appName;
    const playback = configuredState.playback ?? (power === 'off' ? 'off' : 'idle');
    return {
      ...configuredState,
      power,
      playback,
      appId,
      appName,
      source,
    };
  }

  private powerFromDeviceInfo(deviceInfoArg: ISamsungtvDeviceInfoResponse): 'on' | 'off' | 'unknown' {
    const value = String(deviceInfoArg.device?.PowerState || '').toLowerCase();
    if (value === 'on') {
      return 'on';
    }
    if (value.includes('off') || value.includes('standby') || value.includes('sleep')) {
      return 'off';
    }
    return 'unknown';
  }

  private normalizeKey(keyArg: string): string {
    const trimmed = keyArg.trim();
    if (!trimmed) {
      return '';
    }

    const normalized = trimmed.replace(/[_\s-]+/g, '').toLowerCase();
    const aliases: Record<string, string> = {
      power: 'KEY_POWER',
      poweron: 'KEY_POWERON',
      poweroff: 'KEY_POWEROFF',
      play: 'KEY_PLAY',
      pause: 'KEY_PAUSE',
      playpause: 'KEY_PLAYPAUSE',
      stop: 'KEY_STOP',
      volumeup: 'KEY_VOLUP',
      volup: 'KEY_VOLUP',
      volumedown: 'KEY_VOLDOWN',
      voldown: 'KEY_VOLDOWN',
      mute: 'KEY_MUTE',
      home: 'KEY_HOME',
      menu: 'KEY_MENU',
      source: 'KEY_SOURCE',
      tv: 'KEY_TV',
      hdmi: 'KEY_HDMI',
      up: 'KEY_UP',
      down: 'KEY_DOWN',
      left: 'KEY_LEFT',
      right: 'KEY_RIGHT',
      enter: 'KEY_ENTER',
      select: 'KEY_ENTER',
      back: 'KEY_RETURN',
      return: 'KEY_RETURN',
      channelup: 'KEY_CHUP',
      channeldown: 'KEY_CHDOWN',
      chup: 'KEY_CHUP',
      chdown: 'KEY_CHDOWN',
      next: 'KEY_CHUP',
      previous: 'KEY_CHDOWN',
    };
    if (aliases[normalized]) {
      return aliases[normalized];
    }
    if (trimmed.toUpperCase().startsWith('KEY_')) {
      return trimmed.toUpperCase();
    }
    return `KEY_${trimmed.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}`;
  }

  private parseWebSocketMessage(dataArg: unknown): ISamsungtvEvent | undefined {
    const text = this.messageText(dataArg);
    if (!text) {
      return undefined;
    }

    try {
      const parsed = JSON.parse(text) as Record<string, unknown>;
      const data = typeof parsed.data === 'object' && parsed.data !== null ? parsed.data as Record<string, unknown> : undefined;
      return {
        type: 'websocket',
        event: typeof parsed.event === 'string' ? parsed.event : undefined,
        data: parsed.data,
        token: typeof data?.token === 'string' ? data.token : undefined,
        timestamp: Date.now(),
      };
    } catch {
      return undefined;
    }
  }

  private messageText(dataArg: unknown): string | undefined {
    if (typeof dataArg === 'string') {
      return dataArg;
    }
    if (Buffer.isBuffer(dataArg)) {
      return dataArg.toString('utf8');
    }
    if (dataArg instanceof ArrayBuffer) {
      return Buffer.from(dataArg).toString('utf8');
    }
    return undefined;
  }

  private updateLocalState(stateArg: Partial<ISamsungtvState>): void {
    const state = this.config.snapshot?.state ?? this.config.state ?? {};
    Object.assign(state, stateArg);
    if (this.config.snapshot) {
      this.config.snapshot.state = state;
      return;
    }
    this.config.state = state;
  }

  private setActiveApp(appArg: ISamsungtvApp): void {
    this.updateLocalState({ power: 'on', appId: appArg.id, appName: appArg.name, source: appArg.name });
    if (this.config.snapshot) {
      this.config.snapshot.activeApp = appArg;
      return;
    }
    this.config.activeApp = appArg;
  }

  private errorMessage(errorArg: unknown): string {
    if (errorArg instanceof Error) {
      return errorArg.message;
    }
    if (typeof errorArg === 'object' && errorArg !== null && 'message' in errorArg) {
      return String((errorArg as { message?: unknown }).message);
    }
    return String(errorArg);
  }

  private async delay(msArg: number): Promise<void> {
    await new Promise((resolve) => setTimeout(resolve, msArg));
  }
}
