import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import * as plugins from '../../plugins.js';
import { MediaroomMapper } from './mediaroom.mapper.js';
import type { IMediaroomConfig } from './mediaroom.types.js';
import { mediaroomDefaultPort, mediaroomProfile } from './mediaroom.types.js';

export class MediaroomTransportError extends Error {}
export class MediaroomUnsupportedCommandError extends Error {}

interface IMediaroomEndpoint {
  host: string;
  port: number;
}

const mediaroomCommands = {
  Number0: 48,
  Number1: 49,
  Number2: 50,
  Number3: 51,
  Number4: 52,
  Number5: 53,
  Number6: 54,
  Number7: 55,
  Number8: 56,
  Number9: 57,
  Back: 8,
  OK: 13,
  Menu: 36,
  Left: 37,
  Right: 39,
  Up: 38,
  Down: 40,
  ProgUp: 33,
  ProgDown: 34,
  VolUp: 175,
  VolDown: 174,
  Mute: 173,
  Exit: 27,
  Guide: 112,
  Switch: 156,
  Rec: 225,
  PlayPause: 119,
  Forward: 121,
  Rewind: 118,
  Power: 233,
  Info: 159,
  Search: 106,
  Enter: 43,
  VoD: 114,
  Stop: 123,
  Red: 140,
  Green: 141,
  Yellow: 142,
  Blue: 143,
  Delete: 46,
  Options: 111,
  Rose: 237,
  Open: 115,
  AV: 0,
  Prev: 117,
  Next: 122,
  OptionsAlt: 157,
  Favourite: 113,
} as const;

type TMediaroomCommandName = keyof typeof mediaroomCommands;

export class MediaroomClient extends SimpleLocalClient<IMediaroomConfig> {
  private currentMediaroomSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configArg: IMediaroomConfig) {
    super(mediaroomProfile, configArg);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<ISimpleLocalSnapshot> {
    if (!forceRefreshArg && this.currentMediaroomSnapshot) {
      return clone(this.currentMediaroomSnapshot);
    }
    if (!forceRefreshArg && this.configArg.snapshot) {
      this.currentMediaroomSnapshot = MediaroomMapper.toSnapshot({ config: this.configArg, source: 'snapshot', online: this.configArg.snapshot.online });
      return clone(this.currentMediaroomSnapshot);
    }
    if (this.configArg.client) {
      try {
        this.currentMediaroomSnapshot = await this.snapshotFromMediaroomClient();
      } catch (errorArg) {
        this.currentMediaroomSnapshot = this.offlineMediaroomSnapshot(errorMessage(errorArg));
      }
      return clone(this.currentMediaroomSnapshot);
    }
    if (this.configArg.snapshotProvider) {
      try {
        const provided = await this.configArg.snapshotProvider();
        if (provided !== undefined) {
          this.currentMediaroomSnapshot = MediaroomMapper.toSnapshot({ config: this.configArg, rawData: provided, source: 'provider', online: true });
          return clone(this.currentMediaroomSnapshot);
        }
      } catch (errorArg) {
        this.currentMediaroomSnapshot = this.offlineMediaroomSnapshot(errorMessage(errorArg));
        return clone(this.currentMediaroomSnapshot);
      }
    }
    if (!forceRefreshArg && this.hasManualData()) {
      this.currentMediaroomSnapshot = MediaroomMapper.toSnapshot({ config: this.configArg, rawData: this.configArg.rawData ?? this.configArg.rawText, source: 'manual', online: this.configArg.online ?? true });
      return clone(this.currentMediaroomSnapshot);
    }
    if (this.endpoint()) {
      try {
        await this.probe();
        this.currentMediaroomSnapshot = MediaroomMapper.toSnapshot({ config: this.configArg, rawData: { state: this.configArg.optimistic ? 'playing' : 'unknown', fetchedAt: new Date().toISOString() }, source: 'tcp', online: true });
      } catch (errorArg) {
        this.currentMediaroomSnapshot = this.offlineMediaroomSnapshot(errorMessage(errorArg));
      }
      return clone(this.currentMediaroomSnapshot);
    }

    this.currentMediaroomSnapshot = this.offlineMediaroomSnapshot('Mediaroom snapshots require config.host/config.url, an injected client, snapshotProvider, snapshot, entities, or rawData.');
    return clone(this.currentMediaroomSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.currentMediaroomSnapshot = undefined;
    const liveCapable = Boolean(this.configArg.client || this.configArg.snapshotProvider || this.endpoint());
    const snapshot = await this.getSnapshot(liveCapable);
    const success = liveCapable && snapshot.online && !snapshot.error;
    return { success, error: success ? undefined : snapshot.error || 'Mediaroom refresh requires a TCP endpoint, injected client, or snapshotProvider.', data: snapshot };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    try {
      if (this.configArg.commandExecutor) {
        return normalizeServiceResult(await this.configArg.commandExecutor.execute(requestArg));
      }
      if (this.configArg.client?.execute) {
        return normalizeServiceResult(await this.configArg.client.execute(requestArg));
      }
      if (requestArg.service === 'refresh') {
        return this.refresh();
      }
      const keys = this.keysForService(requestArg);
      if (!keys.length) {
        return { success: false, error: `Unsupported Mediaroom service or command: ${requestArg.domain}.${requestArg.service}` };
      }
      await this.sendKeys(keys);
      this.currentMediaroomSnapshot = undefined;
      return { success: true, data: { keys } };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  public async destroy(): Promise<void> {
    await this.configArg.client?.destroy?.();
  }

  private async snapshotFromMediaroomClient(): Promise<ISimpleLocalSnapshot> {
    const client = this.configArg.client;
    if (!client) {
      throw new MediaroomTransportError('No Mediaroom client configured.');
    }
    if (client.getSnapshot) {
      const result = await client.getSnapshot();
      if (isSimpleSnapshot(result)) {
        return MediaroomMapper.toSnapshot({ config: { ...this.configArg, snapshot: result }, source: 'client', online: result.online });
      }
      return MediaroomMapper.toSnapshot({ config: this.configArg, rawData: result, source: 'client', online: true });
    }
    if (client.getRawData) {
      return MediaroomMapper.toSnapshot({ config: this.configArg, rawData: await client.getRawData(), source: 'client', online: true });
    }
    if (client.getState) {
      return MediaroomMapper.toSnapshot({ config: this.configArg, rawData: await client.getState(), source: 'client', online: true });
    }
    throw new MediaroomTransportError('Mediaroom client must expose getSnapshot(), getRawData(), or getState().');
  }

  private async probe(): Promise<void> {
    await this.sendKeys([]);
  }

  private async sendKeys(keysArg: number[]): Promise<void> {
    const endpoint = this.requireEndpoint();
    const timeoutMs = this.configArg.timeoutMs || 5000;
    const keyDelayMs = this.configArg.keyDelayMs ?? 300;

    await new Promise<void>((resolve, reject) => {
      const socket = plugins.net.createConnection({ host: endpoint.host, port: endpoint.port });
      let buffer = Buffer.alloc(0);
      let keyIndex = 0;
      let waitingForHello = true;
      let settled = false;
      let delayTimer: NodeJS.Timeout | undefined;

      const finish = (errorArg?: Error): void => {
        if (settled) {
          return;
        }
        settled = true;
        if (delayTimer) {
          clearTimeout(delayTimer);
        }
        socket.removeAllListeners();
        socket.destroy();
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve();
      };

      const sendNext = (): void => {
        if (keyIndex >= keysArg.length) {
          finish();
          return;
        }
        const key = keysArg[keyIndex++];
        socket.write(`key=${key}\n`);
      };

      socket.setTimeout(timeoutMs, () => finish(new MediaroomTransportError(`Mediaroom TCP request to ${endpoint.host}:${endpoint.port} timed out after ${timeoutMs}ms.`)));
      socket.on('data', (chunkArg) => {
        buffer = Buffer.concat([buffer, Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg)]);
        if (waitingForHello && buffer.length >= 6) {
          waitingForHello = false;
          buffer = Buffer.alloc(0);
          if (!keysArg.length) {
            finish();
            return;
          }
          sendNext();
          return;
        }
        if (!waitingForHello && buffer.length >= 2) {
          buffer = Buffer.alloc(0);
          delayTimer = setTimeout(sendNext, keyDelayMs);
        }
      });
      socket.on('error', (errorArg) => finish(errorArg));
      socket.on('close', () => {
        if (!settled) {
          finish(new MediaroomTransportError(`Mediaroom TCP connection to ${endpoint.host}:${endpoint.port} closed before command acknowledgement.`));
        }
      });
    });
  }

  private keysForService(requestArg: IServiceCallRequest): number[] {
    const service = requestArg.service;
    if (service === 'turn_on' || service === 'turn_off') {
      return [mediaroomCommands.Power];
    }
    if (service === 'media_play' || service === 'media_pause') {
      return [mediaroomCommands.PlayPause];
    }
    if (service === 'media_stop') {
      return [mediaroomCommands.Stop];
    }
    if (service === 'media_next_track') {
      return [mediaroomCommands.ProgUp];
    }
    if (service === 'media_previous_track') {
      return [mediaroomCommands.ProgDown];
    }
    if (service === 'volume_up') {
      return [mediaroomCommands.VolUp];
    }
    if (service === 'volume_down') {
      return [mediaroomCommands.VolDown];
    }
    if (service === 'volume_mute') {
      return [mediaroomCommands.Mute];
    }
    if (service === 'play_media') {
      return this.keysForPlayMedia(requestArg.data || {});
    }
    if (service === 'send_command') {
      return [this.commandKey(requiredCommandName(requestArg.data?.command))];
    }
    return [];
  }

  private keysForPlayMedia(dataArg: Record<string, unknown>): number[] {
    const mediaId = stringValue(dataArg.media_id ?? dataArg.mediaId);
    const mediaType = stringValue(dataArg.media_type ?? dataArg.mediaType);
    if (!mediaId) {
      throw new MediaroomUnsupportedCommandError('Mediaroom play_media requires media_id.');
    }
    if (mediaType === 'channel' || /^\d+$/.test(mediaId)) {
      return this.channelKeys(mediaId);
    }
    if (mediaType === 'mediaroom' || mediaType === undefined) {
      return [this.commandKey(requiredCommandName(mediaId))];
    }
    throw new MediaroomUnsupportedCommandError(`Unsupported Mediaroom media type: ${mediaType}`);
  }

  private channelKeys(channelArg: string): number[] {
    if (!/^\d{1,3}$/.test(channelArg)) {
      throw new MediaroomUnsupportedCommandError('Mediaroom channel media_id must be a 1-3 digit channel number.');
    }
    const keys = [...channelArg].map((digitArg) => this.commandKey(`Number${digitArg}` as TMediaroomCommandName));
    if (keys.length < 3) {
      keys.push(mediaroomCommands.OK);
    }
    return keys;
  }

  private commandKey(commandArg: TMediaroomCommandName): number {
    return mediaroomCommands[commandArg];
  }

  private requireEndpoint(): IMediaroomEndpoint {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new MediaroomTransportError('Mediaroom TCP commands require config.host/config.url, an injected client.execute, or commandExecutor. Static snapshots/manual data are read-only.');
    }
    return endpoint;
  }

  private endpoint(): IMediaroomEndpoint | undefined {
    const value = this.configArg.url || this.configArg.host;
    if (!value) {
      return undefined;
    }
    try {
      const url = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `tcp://${value}`);
      return { host: url.hostname, port: this.configArg.port || (url.port ? Number(url.port) : mediaroomDefaultPort) };
    } catch {
      return { host: value, port: this.configArg.port || mediaroomDefaultPort };
    }
  }

  private hasManualData(): boolean {
    return Boolean(this.configArg.snapshot || this.configArg.rawData !== undefined || this.configArg.rawText !== undefined || this.configArg.entities?.length);
  }

  private offlineMediaroomSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return MediaroomMapper.toSnapshot({ config: this.configArg, rawData: this.configArg.rawData, online: false, source: 'runtime', error: errorArg });
  }
}

const normalizeServiceResult = (valueArg: unknown): IServiceCallResult => {
  if (valueArg && typeof valueArg === 'object' && 'success' in valueArg && typeof (valueArg as IServiceCallResult).success === 'boolean') {
    return valueArg as IServiceCallResult;
  }
  return { success: true, data: valueArg };
};

const requiredCommandName = (valueArg: unknown): TMediaroomCommandName => {
  const value = stringValue(valueArg);
  if (!value || !(value in mediaroomCommands)) {
    throw new MediaroomUnsupportedCommandError(`Unsupported Mediaroom command: ${value || 'missing'}`);
  }
  return value as TMediaroomCommandName;
};

const isSimpleSnapshot = (valueArg: unknown): valueArg is ISimpleLocalSnapshot => Boolean(valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'entities' in valueArg);
const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;
const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
