import * as plugins from '../../plugins.js';
import { SimpleLocalClient, SimpleLocalMapper, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult, type TSimpleLocalRawData } from '../../core/index.js';
import { NadMapper } from './nad.mapper.js';
import type { INadConfig, INadLiveStatus } from './nad.types.js';
import { nadDefaultTcpPort, nadDefaultTelnetPort, nadDefaultTimeoutMs, nadProfile } from './nad.types.js';

export class NadClient extends SimpleLocalClient<INadConfig> {
  private cachedSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configArg: INadConfig) {
    super(nadProfile, configArg);
  }

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

    if (this.isRs232()) {
      this.cachedSnapshot = NadMapper.offlineSnapshot(this.configArg, 'NAD RS-232 requires a serial-port dependency and physical serial adapter; native TypeScript supports NAD TCP and Telnet only.');
      return SimpleLocalMapper.clone(this.cachedSnapshot);
    }

    if (this.shouldUseNativeProtocol(forceRefreshArg)) {
      try {
        this.cachedSnapshot = await this.fetchLiveSnapshot();
      } catch (errorArg) {
        this.cachedSnapshot = NadMapper.offlineSnapshot(this.configArg, errorMessage(errorArg));
      }
      return SimpleLocalMapper.clone(this.cachedSnapshot);
    }

    const snapshot = await super.getSnapshot(forceRefreshArg);
    this.cachedSnapshot = NadMapper.toSnapshot({ config: this.configArg, rawData: snapshot.rawData as TSimpleLocalRawData | undefined ?? snapshot, online: snapshot.online, source: snapshot.source, error: snapshot.error });
    return SimpleLocalMapper.clone(this.cachedSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.cachedSnapshot = undefined;
    const liveCapable = Boolean(this.endpoint()) && !this.isRs232();
    const snapshot = await this.getSnapshot(liveCapable);
    const success = snapshot.online && !snapshot.error && (liveCapable || this.hasManualData());
    return { success, error: success ? undefined : snapshot.error || 'NAD refresh requires a TCP/Telnet host, injected client, snapshotProvider, snapshot, entities, or rawData.', data: snapshot };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.client?.execute || this.configArg.commandExecutor || !this.endpoint()) {
      return super.execute(requestArg);
    }
    if (this.isRs232()) {
      return { success: false, error: 'NAD RS-232 control requires a serial-port dependency and physical serial adapter; native TypeScript supports NAD TCP and Telnet only.' };
    }

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

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

  private shouldUseNativeProtocol(forceRefreshArg: boolean): boolean {
    if (!this.endpoint() || this.configArg.client || this.configArg.snapshotProvider) {
      return false;
    }
    if (!forceRefreshArg && this.configArg.snapshot) {
      return false;
    }
    return true;
  }

  private async fetchLiveSnapshot(): Promise<ISimpleLocalSnapshot> {
    const status = this.isD7050Tcp() ? await this.readD7050Status() : await this.readTelnetStatus();
    const availableSources = this.isD7050Tcp() ? Object.keys(d7050Sources) : Object.values(this.configArg.sources || {});
    return NadMapper.toSnapshot({
      config: this.configArg,
      rawData: { status, availableSources, fetchedAt: new Date().toISOString() },
      online: true,
      source: 'tcp',
    });
  }

  private async executeNative(requestArg: IServiceCallRequest): Promise<unknown> {
    return this.isD7050Tcp() ? await this.executeD7050(requestArg) : await this.executeTelnet(requestArg);
  }

  private async readD7050Status(): Promise<INadLiveStatus> {
    const reply = await this.sendD7050Hex(`${d7050Commands.pollVolume}${d7050Commands.pollPower}${d7050Commands.pollMuted}${d7050Commands.pollSource}`, true);
    const chunks = hexChunks(reply, 10);
    if (chunks.length < 4) {
      throw new Error(`NAD D7050 status reply was incomplete: ${reply}`);
    }
    const sourceCode = chunks[3].slice(-2);
    return {
      mode: 'tcp',
      volume: Number.parseInt(chunks[0].slice(-2), 16),
      power: chunks[1].slice(-2) === '01',
      muted: chunks[2].slice(-2) === '01',
      source: d7050SourcesByCode[sourceCode] || sourceCode,
      model: 'NAD D7050',
    };
  }

  private async executeD7050(requestArg: IServiceCallRequest): Promise<unknown> {
    const data = requestArg.data || {};
    if (requestArg.service === 'turn_on') {
      return await this.sendD7050Hex(d7050Commands.powerOn, true);
    }
    if (requestArg.service === 'turn_off') {
      return await this.sendD7050Hex(`${d7050Commands.powersave}${d7050Commands.powerOff}`, false);
    }
    if (requestArg.service === 'volume_mute') {
      const mute = data.mute ?? data.is_volume_muted ?? true;
      return await this.sendD7050Hex(mute ? d7050Commands.mute : d7050Commands.unmute, true);
    }
    if (requestArg.service === 'set_volume_level' || requestArg.service === 'volume_set' || requestArg.service === 'set_level') {
      const volume = this.nadVolumeFromServiceData(data);
      return await this.sendD7050Hex(`${d7050Commands.volume}${volume.toString(16).padStart(2, '0')}`, false);
    }
    if (requestArg.service === 'volume_up' || requestArg.service === 'volume_down') {
      const status = await this.readD7050Status();
      const current = typeof status.volume === 'number' ? status.volume : 0;
      const step = this.configArg.volumeStep ?? 4;
      const next = clamp(current + (requestArg.service === 'volume_up' ? step : -step), 0, 200);
      return await this.sendD7050Hex(`${d7050Commands.volume}${next.toString(16).padStart(2, '0')}`, false);
    }
    if (requestArg.service === 'select_source') {
      const source = stringValue(data.source ?? data.sourceName ?? data.input);
      const sourceCode = source ? d7050Sources[source] : stringValue(data.sourceCode);
      if (!sourceCode) {
        throw new Error('NAD D7050 select_source requires data.source matching a documented D7050 source.');
      }
      return await this.sendD7050Hex(`${d7050Commands.source}${sourceCode}`, true);
    }
    throw new Error(`Unsupported NAD D7050 service: ${requestArg.domain}.${requestArg.service}`);
  }

  private async readTelnetStatus(): Promise<INadLiveStatus> {
    const power = await this.sendRequiredTelnetCommand('Main.Power?');
    const status: INadLiveStatus = { mode: 'telnet', power };
    if (power === 'On') {
      const [mute, volume, source, model, version] = await Promise.all([
        this.sendTelnetCommand('Main.Mute?'),
        this.sendTelnetCommand('Main.Volume?'),
        this.sendTelnetCommand('Main.Source?'),
        this.sendTelnetCommand('Main.Model?').catch(() => undefined),
        this.sendTelnetCommand('Main.Version?').catch(() => undefined),
      ]);
      status.muted = mute === 'On';
      status.volume = numberValue(volume);
      status.source = numberValue(source) ?? source;
      status.model = model;
      status.version = version;
    }
    return status;
  }

  private async executeTelnet(requestArg: IServiceCallRequest): Promise<unknown> {
    const data = requestArg.data || {};
    if (requestArg.service === 'turn_on') {
      return await this.sendRequiredTelnetCommand('Main.Power=On');
    }
    if (requestArg.service === 'turn_off') {
      return await this.sendRequiredTelnetCommand('Main.Power=Off');
    }
    if (requestArg.service === 'volume_mute') {
      const mute = data.mute ?? data.is_volume_muted ?? true;
      return await this.sendRequiredTelnetCommand(`Main.Mute=${mute ? 'On' : 'Off'}`);
    }
    if (requestArg.service === 'volume_up') {
      return await this.sendRequiredTelnetCommand('Main.Volume+');
    }
    if (requestArg.service === 'volume_down') {
      return await this.sendRequiredTelnetCommand('Main.Volume-');
    }
    if (requestArg.service === 'set_volume_level' || requestArg.service === 'volume_set' || requestArg.service === 'set_level') {
      return await this.sendRequiredTelnetCommand(`Main.Volume=${this.dbVolumeFromServiceData(data)}`);
    }
    if (requestArg.service === 'select_source') {
      const source = data.source ?? data.sourceName ?? data.input;
      const sourceValue = source === undefined ? data.sourceRaw : reverseSourceMap(this.configArg.sources || {})[String(source)] ?? source;
      if (sourceValue === undefined || sourceValue === null || sourceValue === '') {
        throw new Error('NAD Telnet select_source requires data.source or data.sourceRaw.');
      }
      return await this.sendRequiredTelnetCommand(`Main.Source=${sourceValue}`);
    }
    throw new Error(`Unsupported NAD Telnet service: ${requestArg.domain}.${requestArg.service}`);
  }

  private nadVolumeFromServiceData(dataArg: Record<string, unknown>): number {
    const value = numberValue(dataArg.volume_level ?? dataArg.volume ?? dataArg.level ?? dataArg.percentage);
    if (value === undefined) {
      throw new Error('NAD D7050 volume service requires data.volume_level, data.volume, data.level, or data.percentage.');
    }
    if (value > 1) {
      return clamp(Math.round(value), 0, 200);
    }
    const min = ((this.configArg.minVolume ?? -92) + 90) * 2;
    const max = ((this.configArg.maxVolume ?? -20) + 90) * 2;
    return clamp(Math.round(value * (max - min) + min), 0, 200);
  }

  private dbVolumeFromServiceData(dataArg: Record<string, unknown>): number {
    const value = numberValue(dataArg.volume_level ?? dataArg.volume ?? dataArg.level ?? dataArg.percentage);
    if (value === undefined) {
      throw new Error('NAD Telnet volume service requires data.volume_level, data.volume, data.level, or data.percentage.');
    }
    if (value <= 1) {
      const min = this.configArg.minVolume ?? -92;
      const max = this.configArg.maxVolume ?? -20;
      return min + Math.round(Math.abs(min - max) * value);
    }
    return Math.round(value);
  }

  private async sendD7050Hex(hexArg: string, readReplyArg: boolean): Promise<string> {
    const endpoint = this.endpoint(nadDefaultTcpPort);
    if (!endpoint) {
      throw new Error('NAD D7050 TCP requires config.host or config.url.');
    }
    const timeoutMs = this.configArg.timeoutMs || nadDefaultTimeoutMs;
    const payload = Buffer.from(hexArg, 'hex');

    return new Promise<string>((resolve, reject) => {
      const chunks: Buffer[] = [];
      let settled = false;
      const socket = plugins.net.createConnection({ host: endpoint.host, port: endpoint.port });

      const finish = (errorArg?: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        socket.removeAllListeners();
        socket.destroy();
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve(Buffer.concat(chunks).toString('hex'));
      };

      socket.setTimeout(timeoutMs, () => readReplyArg && !chunks.length ? finish(new Error(`NAD D7050 TCP request to ${endpoint.host}:${endpoint.port} timed out after ${timeoutMs}ms.`)) : finish());
      socket.on('connect', () => {
        socket.write(payload);
        if (!readReplyArg) {
          setTimeout(() => finish(), 20);
        }
      });
      socket.on('data', (chunkArg) => {
        chunks.push(Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg));
        if (Buffer.concat(chunks).length >= payload.length) {
          finish();
        }
      });
      socket.on('end', () => finish());
      socket.on('close', () => finish());
      socket.on('error', (errorArg) => finish(errorArg));
    });
  }

  private async sendTelnetCommand(commandArg: string): Promise<string | undefined> {
    const endpoint = this.endpoint(nadDefaultTelnetPort);
    if (!endpoint) {
      throw new Error('NAD Telnet requires config.host or config.url.');
    }
    const timeoutMs = this.configArg.timeoutMs || nadDefaultTimeoutMs;
    const commandName = commandArg.replace(/[?+=-].*$/, '');

    return new Promise<string | undefined>((resolve, reject) => {
      const chunks: Buffer[] = [];
      let settled = false;
      const socket = plugins.net.createConnection({ host: endpoint.host, port: endpoint.port });

      const finish = (errorArg?: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        socket.removeAllListeners();
        socket.destroy();
        if (errorArg) {
          reject(errorArg);
          return;
        }
        const text = Buffer.concat(chunks).toString('utf8');
        resolve(parseTelnetValue(text, commandName));
      };

      socket.setTimeout(timeoutMs, () => finish(new Error(`NAD Telnet command ${commandArg} to ${endpoint.host}:${endpoint.port} timed out after ${timeoutMs}ms.`)));
      socket.on('connect', () => socket.write(`\n${commandArg}\r`));
      socket.on('data', (chunkArg) => {
        chunks.push(Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg));
        const value = parseTelnetValue(Buffer.concat(chunks).toString('utf8'), commandName);
        if (value !== undefined) {
          finish();
        }
      });
      socket.on('end', () => finish());
      socket.on('close', () => finish());
      socket.on('error', (errorArg) => finish(errorArg));
    });
  }

  private async sendRequiredTelnetCommand(commandArg: string): Promise<string> {
    const value = await this.sendTelnetCommand(commandArg);
    if (value === undefined) {
      throw new Error(`NAD Telnet command ${commandArg} did not return a ${commandArg.replace(/[?+=-].*$/, '')}=... response.`);
    }
    return value;
  }

  private endpoint(defaultPortArg?: number): { host: string; port: number } | undefined {
    const fallbackPort = defaultPortArg || (this.isD7050Tcp() ? nadDefaultTcpPort : nadDefaultTelnetPort);
    const rawUrl = stringValue(this.configArg.url) || stringValue(this.configArg.host);
    if (!rawUrl) {
      return undefined;
    }
    try {
      const url = new URL(rawUrl.includes('://') ? rawUrl : `tcp://${rawUrl}`);
      return { host: url.hostname, port: this.configArg.port || numberValue(url.port) || fallbackPort };
    } catch {
      return { host: rawUrl, port: this.configArg.port || fallbackPort };
    }
  }

  private isD7050Tcp(): boolean {
    return normalizedType(this.configArg.type) === 'tcp' || this.configArg.transport === 'tcp' && normalizedType(this.configArg.type) !== 'telnet';
  }

  private isRs232(): boolean {
    return normalizedType(this.configArg.type) === 'rs232';
  }

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

const d7050Commands = {
  pollVolume: '0001020204',
  pollPower: '0001020209',
  pollMuted: '000102020a',
  pollSource: '0001020203',
  powersave: '00010207000001020207',
  powerOff: '0001020900',
  powerOn: '0001020901',
  volume: '00010204',
  mute: '0001020a01',
  unmute: '0001020a00',
  source: '00010203',
};

const d7050Sources: Record<string, string> = {
  'Coaxial 1': '00',
  'Coaxial 2': '01',
  'Optical 1': '02',
  'Optical 2': '03',
  Computer: '04',
  Airplay: '05',
  Dock: '06',
  Bluetooth: '07',
};

const d7050SourcesByCode = Object.fromEntries(Object.entries(d7050Sources).map(([keyArg, valueArg]) => [valueArg, keyArg]));

const parseTelnetValue = (textArg: string, commandNameArg: string): string | undefined => {
  const lines = textArg.split(/[\r\n]+/).map((lineArg) => lineArg.trim()).filter(Boolean);
  const prefix = `${commandNameArg}=`;
  const line = lines.find((lineArg) => lineArg.startsWith(prefix));
  return line ? line.slice(prefix.length) : undefined;
};

const hexChunks = (valueArg: string, sizeArg: number): string[] => {
  const chunks: string[] = [];
  for (let index = 0; index < valueArg.length; index += sizeArg) {
    chunks.push(valueArg.slice(index, index + sizeArg));
  }
  return chunks.filter((chunkArg) => chunkArg.length === sizeArg);
};

const normalizedType = (valueArg: unknown): 'tcp' | 'telnet' | 'rs232' => {
  const value = stringValue(valueArg)?.toLowerCase();
  if (value === 'tcp') {
    return 'tcp';
  }
  if (value === 'rs232') {
    return 'rs232';
  }
  return 'telnet';
};

const reverseSourceMap = (sourcesArg: Record<string, string>): Record<string, string> => {
  return Object.fromEntries(Object.entries(sourcesArg).map(([keyArg, valueArg]) => [valueArg, keyArg]));
};

const stringValue = (valueArg: unknown): string | undefined => {
  return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
};

const numberValue = (valueArg: unknown): number | undefined => {
  if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
    return valueArg;
  }
  if (typeof valueArg === 'string' && valueArg.trim()) {
    const parsed = Number(valueArg.trim());
    return Number.isFinite(parsed) ? parsed : undefined;
  }
  return undefined;
};

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

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