import * as plugins from '../../plugins.js';
import { SimpleLocalClient, SimpleLocalMapper, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import type { IOnkyoConfig, IOnkyoSnapshot } from './onkyo.types.js';
import { onkyoDefaultName, onkyoDefaultPort, onkyoDefaultTimeoutMs, onkyoProfile } from './onkyo.types.js';

interface IOnkyoReceiverState {
  power: 'on' | 'off' | 'unknown';
  volumeRaw?: number;
  volumeLevel?: number;
  muted?: boolean;
  sourceCode?: string;
  source?: string;
  listeningModeCode?: string;
  listeningMode?: string;
}

const eiscpHeaderSize = 16;
const onkyoSourceCodes: Record<string, string> = {
  '00': 'VCR/DVR',
  '01': 'CBL/SAT',
  '02': 'Game',
  '03': 'Aux',
  '04': 'Game 2',
  '05': 'PC',
  '10': 'BD/DVD',
  '11': 'Stream Box',
  '12': 'TV',
  '22': 'Phono',
  '23': 'CD',
  '24': 'FM',
  '25': 'AM',
  '26': 'Tuner',
  '27': 'Music Server',
  '28': 'Internet Radio',
  '29': 'USB',
  '2B': 'NET',
  '2E': 'Bluetooth',
  '2F': 'USB DAC',
  '40': 'Universal Port',
  '44': 'Optical',
  '45': 'Coaxial',
};

const onkyoListeningModes: Record<string, string> = {
  '00': 'Stereo',
  '01': 'Direct',
  '02': 'Surround',
  '0C': 'All Channel Stereo',
  '0F': 'Mono',
  '11': 'Pure Audio',
  '40': 'Straight Decode',
  FF: 'Auto Surround',
};

const hdmiOutputCodes: Record<string, string> = {
  no: '00',
  analog: '00',
  yes: '01',
  out: '01',
  main: '01',
  'out-sub': '02',
  sub: '03',
  hdbaset: '02',
  both: '03',
  up: 'UP',
};

export class OnkyoClient extends SimpleLocalClient<IOnkyoConfig> {
  private currentOnkyoSnapshot?: IOnkyoSnapshot;

  constructor(private readonly configArg: IOnkyoConfig) {
    super(onkyoProfile, configArg);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<IOnkyoSnapshot> {
    if (!forceRefreshArg && this.currentOnkyoSnapshot) {
      return SimpleLocalMapper.clone(this.currentOnkyoSnapshot) as IOnkyoSnapshot;
    }

    if (!forceRefreshArg && this.configArg.snapshot) {
      this.currentOnkyoSnapshot = await super.getSnapshot(false) as IOnkyoSnapshot;
      return SimpleLocalMapper.clone(this.currentOnkyoSnapshot) as IOnkyoSnapshot;
    }

    if (this.configArg.client || this.configArg.snapshotProvider) {
      this.currentOnkyoSnapshot = await super.getSnapshot(forceRefreshArg) as IOnkyoSnapshot;
      return SimpleLocalMapper.clone(this.currentOnkyoSnapshot) as IOnkyoSnapshot;
    }

    if (this.configArg.host && this.configArg.transport !== 'snapshot') {
      try {
        this.currentOnkyoSnapshot = await this.readEiscpSnapshot();
      } catch (errorArg) {
        this.currentOnkyoSnapshot = this.createOfflineSnapshot(errorMessage(errorArg));
      }
      return SimpleLocalMapper.clone(this.currentOnkyoSnapshot) as IOnkyoSnapshot;
    }

    this.currentOnkyoSnapshot = await super.getSnapshot(forceRefreshArg) as IOnkyoSnapshot;
    return SimpleLocalMapper.clone(this.currentOnkyoSnapshot) as IOnkyoSnapshot;
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.currentOnkyoSnapshot = undefined;
    const snapshot = await this.getSnapshot(true);
    const success = snapshot.online && !snapshot.error;
    return { success, error: success ? undefined : snapshot.error, data: snapshot };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (!this.configArg.host || this.configArg.transport === 'snapshot') {
      return super.execute(requestArg);
    }

    try {
      const data = await this.executeEiscpCommand(requestArg);
      this.currentOnkyoSnapshot = undefined;
      return { success: true, data };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  private async readEiscpSnapshot(): Promise<IOnkyoSnapshot> {
    const state = await this.queryReceiverState();
    const name = this.configArg.name || stringConfig(this.configArg, 'modelName') || onkyoDefaultName;
    const uniqueBase = this.configArg.uniqueId || this.configArg.deviceId || this.configArg.macAddress || `${this.host()}:${this.port()}`;
    const rawData = {
      state,
      sourceList: sourceList(this.configArg),
      volumeResolution: this.volumeResolution(),
      fetchedAt: new Date().toISOString(),
    };

    return {
      device: {
        id: uniqueBase,
        name,
        manufacturer: 'Onkyo / Pioneer',
        model: stringConfig(this.configArg, 'modelName') || stringConfig(this.configArg, 'model') || 'eISCP receiver',
        serialNumber: this.configArg.macAddress || this.configArg.deviceId,
        host: this.host(),
        port: this.port(),
        protocol: 'tcp',
        configurationUrl: `tcp://${this.host()}:${this.port()}`,
      },
      entities: [{
        id: 'media_player',
        uniqueId: `${onkyoProfile.domain}_${SimpleLocalMapper.slug(uniqueBase)}_media_player`,
        name,
        platform: 'media_player',
        state: state.power,
        available: state.power !== 'unknown',
        writable: true,
        attributes: {
          source: state.source,
          source_code: state.sourceCode,
          source_list: sourceList(this.configArg),
          volume_level: state.volumeLevel,
          volume_raw: state.volumeRaw,
          is_volume_muted: state.muted,
          sound_mode: state.listeningMode,
          sound_mode_code: state.listeningModeCode,
        },
      }],
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'tcp',
      rawData,
      capabilities: {
        localRead: true,
        localControl: true,
        liveControlRequiresExecutor: false,
      },
    };
  }

  private async queryReceiverState(): Promise<IOnkyoReceiverState> {
    const powerValue = await this.queryValue('PWR');
    if (!powerValue) {
      throw new Error('Onkyo eISCP power query returned no usable PWR status.');
    }
    const [volumeValue, muteValue, sourceValue, listeningModeValue] = await Promise.all([
      optional(() => this.queryValue('MVL')),
      optional(() => this.queryValue('AMT')),
      optional(() => this.queryValue('SLI')),
      optional(() => this.queryValue('LMD')),
    ]);
    const volumeRaw = volumeValue ? Number.parseInt(volumeValue, 16) : undefined;
    const sourceCode = sourceValue?.toUpperCase();
    const listeningModeCode = listeningModeValue?.toUpperCase();

    return {
      power: powerValue === '01' ? 'on' : powerValue === '00' ? 'off' : 'unknown',
      volumeRaw: Number.isFinite(volumeRaw) ? volumeRaw : undefined,
      volumeLevel: Number.isFinite(volumeRaw) ? clamp(Number(((volumeRaw as number) / (this.volumeResolution() * (this.maxVolume() / 100))).toFixed(3)), 0, 1) : undefined,
      muted: muteValue === undefined ? undefined : muteValue === '01',
      sourceCode,
      source: sourceCode ? sourceName(sourceCode, this.configArg) : undefined,
      listeningModeCode,
      listeningMode: listeningModeCode ? onkyoListeningModes[listeningModeCode] || listeningModeCode : undefined,
    };
  }

  private async executeEiscpCommand(requestArg: IServiceCallRequest): Promise<unknown> {
    const service = requestArg.service;
    if (service === 'turn_on') {
      return { response: await this.sendCommand('PWR01') };
    }
    if (service === 'turn_off') {
      return { response: await this.sendCommand('PWR00') };
    }
    if (service === 'toggle') {
      const power = await this.queryValue('PWR');
      return { response: await this.sendCommand(power === '01' ? 'PWR00' : 'PWR01') };
    }
    if (service === 'volume_up') {
      return { response: await this.sendCommand('MVLUP') };
    }
    if (service === 'volume_down') {
      return { response: await this.sendCommand('MVLDOWN') };
    }
    if (service === 'volume_set' || service === 'set_volume' || service === 'set_volume_level') {
      const level = numberData(requestArg, 'volume_level') ?? numberData(requestArg, 'volumeLevel') ?? numberData(requestArg, 'level');
      if (level === undefined) {
        throw new Error('Onkyo volume_set requires data.volume_level.');
      }
      const raw = clamp(Math.round(level * (this.maxVolume() / 100) * this.volumeResolution()), 0, this.volumeResolution());
      return { response: await this.sendCommand(`MVL${hex(raw)}`), volumeRaw: raw };
    }
    if (service === 'volume_mute' || service === 'mute') {
      const muted = booleanData(requestArg, 'is_volume_muted') ?? booleanData(requestArg, 'mute') ?? booleanData(requestArg, 'muted');
      if (muted === undefined) {
        throw new Error('Onkyo volume_mute requires data.is_volume_muted or data.mute.');
      }
      return { response: await this.sendCommand(`AMT${muted ? '01' : '00'}`) };
    }
    if (service === 'media_play') {
      return { response: await this.sendCommand('NTCPLAY') };
    }
    if (service === 'media_pause') {
      return { response: await this.sendCommand('NTCPAUSE') };
    }
    if (service === 'media_stop') {
      return { response: await this.sendCommand('NTCSTOP') };
    }
    if (service === 'select_source' || service === 'set_source') {
      const source = stringData(requestArg, 'source');
      if (!source) {
        throw new Error('Onkyo select_source requires data.source.');
      }
      const code = sourceCode(source, this.configArg);
      if (!code) {
        throw new Error(`Unsupported Onkyo input source: ${source}.`);
      }
      return { response: await this.sendCommand(`SLI${code}`), source, sourceCode: code };
    }
    if (service === 'onkyo_select_hdmi_output' || service === 'select_hdmi_output') {
      const output = stringData(requestArg, 'hdmi_output') || stringData(requestArg, 'output');
      const code = output ? hdmiOutputCodes[normalizeKey(output)] : undefined;
      if (!code) {
        throw new Error('Onkyo HDMI output selection requires data.hdmi_output with a supported value.');
      }
      return { response: await this.sendCommand(`HDO${code}`), hdmiOutput: output };
    }
    throw new Error(`Unsupported Onkyo eISCP service: ${requestArg.domain}.${service}`);
  }

  private async queryValue(prefixArg: string): Promise<string | undefined> {
    const response = await this.sendCommand(`${prefixArg}QSTN`, prefixArg);
    return response.startsWith(prefixArg) ? response.slice(prefixArg.length) : undefined;
  }

  private async sendCommand(commandArg: string, responsePrefixArg = commandArg.slice(0, 3)): Promise<string> {
    const frame = buildEiscpFrame(commandArg);
    const host = this.host();
    const port = this.port();
    const timeoutMs = this.timeoutMs();

    return new Promise<string>((resolve, reject) => {
      const socket = plugins.net.createConnection({ host, port });
      let buffer = Buffer.alloc(0);
      let settled = false;

      const finish = (errorArg?: Error, responseArg?: string) => {
        if (settled) {
          return;
        }
        settled = true;
        socket.removeAllListeners();
        socket.destroy();
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve(responseArg || '');
      };

      socket.setTimeout(timeoutMs, () => finish(new Error(`Onkyo eISCP request to ${host}:${port} timed out after ${timeoutMs}ms.`)));
      socket.on('connect', () => socket.write(frame));
      socket.on('data', (chunkArg) => {
        buffer = Buffer.concat([buffer, Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg)]);
        const parsed = parseEiscpResponses(buffer);
        buffer = Buffer.from(parsed.remaining);
        const response = parsed.messages.find((messageArg) => messageArg.startsWith(responsePrefixArg));
        if (response) {
          finish(undefined, response);
        }
      });
      socket.on('end', () => finish(new Error(`Onkyo eISCP connection to ${host}:${port} closed before a response was received.`)));
      socket.on('close', () => finish(new Error(`Onkyo eISCP connection to ${host}:${port} closed before a response was received.`)));
      socket.on('error', (errorArg) => finish(errorArg));
    });
  }

  private host(): string {
    if (!this.configArg.host) {
      throw new Error('Onkyo eISCP requires config.host.');
    }
    return this.configArg.host;
  }

  private port(): number {
    return this.configArg.port || onkyoDefaultPort;
  }

  private timeoutMs(): number {
    return this.configArg.timeoutMs || onkyoDefaultTimeoutMs;
  }

  private volumeResolution(): number {
    return numberConfig(this.configArg, 'volumeResolution') || numberConfig(this.configArg, 'volume_resolution') || 50;
  }

  private maxVolume(): number {
    return numberConfig(this.configArg, 'maxVolume') || numberConfig(this.configArg, 'max_volume') || 100;
  }

  private createOfflineSnapshot(errorArg: string): IOnkyoSnapshot {
    return SimpleLocalMapper.toSnapshot({ profile: onkyoProfile, config: this.configArg, rawData: this.configArg.rawData, online: false, source: 'runtime', error: errorArg }) as IOnkyoSnapshot;
  }
}

function buildEiscpFrame(commandArg: string): Buffer {
  const data = Buffer.from(`!1${commandArg}\r`, 'ascii');
  const header = Buffer.alloc(eiscpHeaderSize);
  header.write('ISCP', 0, 'ascii');
  header.writeUInt32BE(eiscpHeaderSize, 4);
  header.writeUInt32BE(data.length, 8);
  header.writeUInt8(1, 12);
  return Buffer.concat([header, data]);
}

function parseEiscpResponses(bufferArg: Buffer): { messages: string[]; remaining: Buffer } {
  const messages: string[] = [];
  let offset = 0;
  while (bufferArg.length - offset >= eiscpHeaderSize) {
    if (bufferArg.subarray(offset, offset + 4).toString('ascii') !== 'ISCP') {
      offset++;
      continue;
    }
    const headerSize = bufferArg.readUInt32BE(offset + 4);
    const dataSize = bufferArg.readUInt32BE(offset + 8);
    const frameSize = headerSize + dataSize;
    if (bufferArg.length - offset < frameSize) {
      break;
    }
    const body = bufferArg.subarray(offset + headerSize, offset + frameSize).toString('ascii').replace(/[\u0000\u001a\r\n]+$/g, '');
    if (body.startsWith('!') && body.length >= 5) {
      messages.push(body.slice(2));
    }
    offset += frameSize;
  }
  return { messages, remaining: Buffer.from(bufferArg.subarray(offset)) };
}

async function optional<TValue>(callbackArg: () => Promise<TValue>): Promise<TValue | undefined> {
  try {
    return await callbackArg();
  } catch {
    return undefined;
  }
}

function sourceList(configArg: IOnkyoConfig): string[] {
  const configured = recordConfig(configArg, 'sourceMap') || recordConfig(configArg, 'inputSources') || recordConfig(configArg, 'input_sources');
  if (configured) {
    return Object.values(configured).filter((valueArg): valueArg is string => typeof valueArg === 'string' && valueArg.trim().length > 0);
  }
  return Object.values(onkyoSourceCodes);
}

function sourceName(codeArg: string, configArg: IOnkyoConfig): string {
  const configured = recordConfig(configArg, 'sourceMap') || recordConfig(configArg, 'inputSources') || recordConfig(configArg, 'input_sources');
  const configuredName = configured?.[codeArg];
  return typeof configuredName === 'string' && configuredName.trim() ? configuredName : onkyoSourceCodes[codeArg] || codeArg;
}

function sourceCode(sourceArg: string, configArg: IOnkyoConfig): string | undefined {
  const normalized = normalizeKey(sourceArg);
  const configured = recordConfig(configArg, 'sourceMap') || recordConfig(configArg, 'inputSources') || recordConfig(configArg, 'input_sources');
  const configuredMatch = configured ? Object.entries(configured).find(([codeArg, nameArg]) => normalizeKey(codeArg) === normalized || normalizeKey(String(nameArg)) === normalized) : undefined;
  if (configuredMatch) {
    return configuredMatch[0].toUpperCase();
  }
  return Object.entries(onkyoSourceCodes).find(([codeArg, nameArg]) => normalizeKey(codeArg) === normalized || normalizeKey(nameArg) === normalized)?.[0];
}

function normalizeKey(valueArg: string): string {
  return valueArg.toLowerCase().replace(/[^a-z0-9]+/g, '');
}

function hex(valueArg: number): string {
  return Math.round(valueArg).toString(16).toUpperCase().padStart(2, '0');
}

function clamp(valueArg: number, minArg: number, maxArg: number): number {
  return Math.max(minArg, Math.min(maxArg, valueArg));
}

function numberData(requestArg: IServiceCallRequest, keyArg: string): number | undefined {
  const value = requestArg.data?.[keyArg];
  if (typeof value === 'number' && Number.isFinite(value)) {
    return value;
  }
  if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {
    return Number(value);
  }
  return undefined;
}

function booleanData(requestArg: IServiceCallRequest, keyArg: string): boolean | undefined {
  const value = requestArg.data?.[keyArg];
  return typeof value === 'boolean' ? value : undefined;
}

function stringData(requestArg: IServiceCallRequest, keyArg: string): string | undefined {
  const value = requestArg.data?.[keyArg];
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
}

function numberConfig(configArg: IOnkyoConfig, keyArg: string): number | undefined {
  const value = configArg[keyArg];
  return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}

function stringConfig(configArg: IOnkyoConfig, keyArg: string): string | undefined {
  const value = configArg[keyArg];
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
}

function recordConfig(configArg: IOnkyoConfig, keyArg: string): Record<string, string> | undefined {
  const value = configArg[keyArg];
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return undefined;
  }
  return Object.fromEntries(Object.entries(value as Record<string, unknown>).filter(([, valueArg]) => typeof valueArg === 'string')) as Record<string, string>;
}

function errorMessage(errorArg: unknown): string {
  return errorArg instanceof Error ? errorArg.message : String(errorArg);
}
