import * as plugins from '../../plugins.js';
import type {
  IDenonClientLike,
  IDenonCommandRequest,
  IDenonConfig,
  IDenonRawCommandRequest,
  IDenonSnapshot,
  TDenonSnapshotSource,
} from './denon.types.js';
import {
  denonDefaultName,
  denonDefaultPort,
  denonDefaultSourceMap,
  denonDefaultTimeoutMs,
  denonMediaModeCodes,
} from './denon.types.js';

export class DenonClientError extends Error {
  constructor(messageArg: string) {
    super(messageArg);
    this.name = 'DenonClientError';
  }
}

export class DenonTransportError extends DenonClientError {
  constructor(messageArg: string) {
    super(messageArg);
    this.name = 'DenonTransportError';
  }
}

export class DenonClient {
  private currentSnapshot?: IDenonSnapshot;

  constructor(private readonly config: IDenonConfig) {
    this.currentSnapshot = config.snapshot ? this.normalizeSnapshot(this.cloneValue(config.snapshot), 'snapshot') : undefined;
  }

  public async getSnapshot(): Promise<IDenonSnapshot> {
    if (this.config.snapshot) {
      this.currentSnapshot = this.normalizeSnapshot(this.cloneValue(this.config.snapshot), 'snapshot');
      return this.cloneValue(this.currentSnapshot);
    }

    if (this.config.client?.getSnapshot || this.config.client?.snapshot) {
      this.currentSnapshot = this.normalizeSnapshot(await this.snapshotFromClient(this.config.client), 'client');
      return this.cloneValue(this.currentSnapshot);
    }

    if (this.config.host || this.config.commandExecutor) {
      try {
        this.currentSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.currentSnapshot = this.offlineSnapshot(this.errorMessage(errorArg));
      }
      return this.cloneValue(this.currentSnapshot);
    }

    this.currentSnapshot = this.offlineSnapshot('Denon refresh requires config.host, config.snapshot, injected client, or commandExecutor.');
    return this.cloneValue(this.currentSnapshot);
  }

  public async refresh(): Promise<IDenonSnapshot> {
    return this.getSnapshot();
  }

  public async validateConnection(): Promise<IDenonSnapshot> {
    const snapshot = await this.fetchSnapshot();
    if (!snapshot.online) {
      throw new DenonTransportError(snapshot.error || 'Denon receiver did not return live telnet state.');
    }
    return snapshot;
  }

  public async execute(requestArg: IDenonCommandRequest): Promise<unknown> {
    if (this.config.client?.execute) {
      return this.config.client.execute(requestArg);
    }

    const rawCommand = this.rawCommandFromRequest(requestArg);
    if (!rawCommand) {
      throw new DenonClientError(`Unsupported Denon command: ${requestArg.command}`);
    }

    if (this.config.commandExecutor) {
      return this.config.commandExecutor.execute(this.rawRequest(rawCommand));
    }
    if (!this.config.host) {
      throw new DenonTransportError('Denon telnet commands require config.host, injected client.execute, or commandExecutor. Static snapshots/manual data are read-only.');
    }

    return this.sendRawCommand(rawCommand);
  }

  public async destroy(): Promise<void> {}

  private async fetchSnapshot(): Promise<IDenonSnapshot> {
    const source = this.config.commandExecutor && !this.config.host ? 'executor' : 'telnet';
    const session = this.config.commandExecutor ? undefined : await DenonTelnetSession.connect({
      host: this.requireHost(),
      port: this.port(),
      timeoutMs: this.timeoutMs(),
      responseIdleMs: this.responseIdleMs(),
    });

    try {
      const request = async (commandArg: string): Promise<string[]> => {
        if (this.config.commandExecutor) {
          return this.executorResultToLines(await this.config.commandExecutor.execute(this.rawRequest(commandArg)));
        }
        return session!.request(commandArg);
      };

      const setup = await this.readSources(request);
      const power = firstLine(await request('PW?'));
      const volumeLines = await request('MV?');
      const volumeMax = this.volumeMax(volumeLines) || 60;
      const volume = this.volume(volumeLines);
      const muted = firstLine(await request('MU?')) === 'MUON';
      const sourceCode = this.stripPrefix(firstLine(await request('SI?')), 'SI');
      const sourceName = this.sourceName(setup.sourceMap, sourceCode);
      const mediaMode = denonMediaModeCodes.has(sourceCode || '');
      const mediaInfo = mediaMode
        ? this.mediaInfo(await request('NSE'))
        : sourceName;

      return this.normalizeSnapshot({
        receiverInfo: {
          host: this.config.host,
          port: this.port(),
          name: setup.name || this.config.name || denonDefaultName,
          manufacturer: this.config.manufacturer || 'Denon',
          modelName: this.config.model,
          serialNumber: this.config.serialNumber,
        },
        state: {
          power,
          state: this.mediaState(power, mediaMode),
          volume,
          volumeMax,
          volumeLevel: typeof volume === 'number' ? Math.max(0, Math.min(1, volume / volumeMax)) : undefined,
          muted,
          source: sourceCode,
          sourceName,
          sourceList: Object.keys(setup.sourceMap).sort(),
          mediaTitle: mediaInfo,
          mediaInfo,
          mediaMode,
          supportedFeatures: this.supportedFeatures(mediaMode),
        },
        sourceMap: setup.sourceMap,
        online: true,
        available: true,
        source: source,
      }, source);
    } finally {
      await session?.close();
    }
  }

  private async readSources(requestArg: (commandArg: string) => Promise<string[]>): Promise<{ name?: string; sourceMap: Record<string, string> }> {
    const configuredSourceMap = { ...denonDefaultSourceMap, ...this.config.sourceMap };
    let sourceMap = configuredSourceMap;
    const name = this.stripPrefix(firstLine(await requestArg('NSFRN ?')), 'NSFRN ') || this.config.name;
    const sourceLines = await requestArg('SSFUN ?');

    if (sourceLines.length) {
      const parsedSourceMap: Record<string, string> = {};
      for (const line of sourceLines) {
        const sourceLine = this.stripPrefix(line, 'SSFUN');
        if (!sourceLine) {
          continue;
        }
        const [source, configuredName] = splitFirst(sourceLine.trim(), ' ');
        parsedSourceMap[configuredName || source] = source;
      }
      if (Object.keys(parsedSourceMap).length) {
        sourceMap = { ...parsedSourceMap, ...this.config.sourceMap };
      }
    }

    for (const line of await requestArg('SSSOD ?')) {
      const deletedLine = this.stripPrefix(line, 'SSSOD') || '';
      const [source, status] = splitFirst(deletedLine.trim(), ' ');
      if (status !== 'DEL') {
        continue;
      }
      for (const [prettyName, sourceCode] of Object.entries(sourceMap)) {
        if (sourceCode === source) {
          delete sourceMap[prettyName];
          break;
        }
      }
    }

    return { name, sourceMap };
  }

  private rawCommandFromRequest(requestArg: IDenonCommandRequest): string | undefined {
    if (requestArg.command === 'raw') {
      return requestArg.rawCommand;
    }
    if (requestArg.command === 'turn_on') {
      return 'PWON';
    }
    if (requestArg.command === 'turn_off') {
      return 'PWSTANDBY';
    }
    if (requestArg.command === 'volume_up') {
      return 'MVUP';
    }
    if (requestArg.command === 'volume_down') {
      return 'MVDOWN';
    }
    if (requestArg.command === 'set_volume') {
      return this.setVolumeCommand(requestArg.volumeLevel);
    }
    if (requestArg.command === 'mute') {
      return `MU${requestArg.muted ? 'ON' : 'OFF'}`;
    }
    if (requestArg.command === 'select_source') {
      return this.selectSourceCommand(requestArg.source);
    }
    if (requestArg.command === 'play') {
      return 'NS9A';
    }
    if (requestArg.command === 'pause') {
      return 'NS9B';
    }
    if (requestArg.command === 'stop') {
      return 'NS9C';
    }
    if (requestArg.command === 'next_track') {
      return 'NS9D';
    }
    if (requestArg.command === 'previous_track') {
      return 'NS9E';
    }
    return undefined;
  }

  private setVolumeCommand(volumeLevelArg: number | undefined): string {
    if (typeof volumeLevelArg !== 'number' || !Number.isFinite(volumeLevelArg)) {
      throw new DenonClientError('Denon set_volume requires data.volume_level.');
    }
    const volumeMax = this.currentSnapshot?.state.volumeMax || this.config.snapshot?.state.volumeMax || 60;
    const volume = Math.max(0, Math.min(volumeMax, Math.round(volumeLevelArg * volumeMax)));
    return `MV${String(volume).padStart(2, '0')}`;
  }

  private selectSourceCommand(sourceArg: string | undefined): string {
    if (!sourceArg) {
      throw new DenonClientError('Denon select_source requires data.source.');
    }
    const sourceMap = {
      ...denonDefaultSourceMap,
      ...this.currentSnapshot?.sourceMap,
      ...this.config.snapshot?.sourceMap,
      ...this.config.sourceMap,
    };
    const sourceCode = sourceMap[sourceArg] || sourceMap[sourceArg.toUpperCase()] || sourceArg;
    if (!sourceCode) {
      throw new DenonClientError(`Denon source is not available: ${sourceArg}`);
    }
    return `SI${sourceCode}`;
  }

  private async sendRawCommand(commandArg: string): Promise<string[]> {
    const session = await DenonTelnetSession.connect({
      host: this.requireHost(),
      port: this.port(),
      timeoutMs: this.timeoutMs(),
      responseIdleMs: this.responseIdleMs(),
    });
    try {
      return await session.request(commandArg);
    } finally {
      await session.close();
    }
  }

  private rawRequest(commandArg: string): IDenonRawCommandRequest {
    return {
      command: 'raw',
      rawCommand: commandArg,
      host: this.config.host,
      port: this.port(),
      timeoutMs: this.timeoutMs(),
    };
  }

  private async snapshotFromClient(clientArg: IDenonClientLike): Promise<IDenonSnapshot> {
    const result = clientArg.getSnapshot ? await clientArg.getSnapshot() : clientArg.snapshot ? await clientArg.snapshot() : undefined;
    if (!result) {
      throw new DenonClientError('Denon client must expose getSnapshot() or snapshot().');
    }
    return result;
  }

  private normalizeSnapshot(snapshotArg: IDenonSnapshot, sourceArg: TDenonSnapshotSource): IDenonSnapshot {
    const receiverInfo = {
      host: snapshotArg.receiverInfo.host || this.config.host,
      port: snapshotArg.receiverInfo.port || this.port(),
      name: snapshotArg.receiverInfo.name || this.config.name || denonDefaultName,
      manufacturer: snapshotArg.receiverInfo.manufacturer || this.config.manufacturer || 'Denon',
      modelName: snapshotArg.receiverInfo.modelName || this.config.model,
      serialNumber: snapshotArg.receiverInfo.serialNumber || this.config.serialNumber,
    };
    const sourceMap = { ...denonDefaultSourceMap, ...snapshotArg.sourceMap, ...this.config.sourceMap };
    const sourceName = snapshotArg.state.sourceName || this.sourceName(sourceMap, snapshotArg.state.source);
    const volumeMax = snapshotArg.state.volumeMax || 60;
    const volumeLevel = typeof snapshotArg.state.volumeLevel === 'number'
      ? Math.max(0, Math.min(1, snapshotArg.state.volumeLevel))
      : typeof snapshotArg.state.volume === 'number'
        ? Math.max(0, Math.min(1, snapshotArg.state.volume / volumeMax))
        : undefined;

    return {
      ...snapshotArg,
      receiverInfo,
      state: {
        ...snapshotArg.state,
        sourceName,
        sourceList: snapshotArg.state.sourceList || Object.keys(sourceMap).sort(),
        volumeMax,
        volumeLevel,
        state: snapshotArg.state.state || this.mediaState(snapshotArg.state.power, snapshotArg.state.mediaMode),
        supportedFeatures: snapshotArg.state.supportedFeatures || this.supportedFeatures(Boolean(snapshotArg.state.mediaMode)),
      },
      sourceMap,
      online: snapshotArg.online,
      available: snapshotArg.available,
      source: sourceArg,
      lastUpdated: snapshotArg.lastUpdated || new Date().toISOString(),
    };
  }

  private offlineSnapshot(errorArg: string): IDenonSnapshot {
    return this.normalizeSnapshot({
      receiverInfo: {
        host: this.config.host,
        port: this.port(),
        name: this.config.name || denonDefaultName,
        manufacturer: this.config.manufacturer || 'Denon',
        modelName: this.config.model,
        serialNumber: this.config.serialNumber,
      },
      state: {
        power: 'PWSTANDBY',
        state: 'unavailable',
        volumeMax: 60,
        sourceList: Object.keys({ ...denonDefaultSourceMap, ...this.config.sourceMap }).sort(),
        supportedFeatures: this.supportedFeatures(false),
      },
      sourceMap: { ...denonDefaultSourceMap, ...this.config.sourceMap },
      online: false,
      available: false,
      source: 'runtime',
      error: errorArg,
    }, 'runtime');
  }

  private supportedFeatures(mediaModeArg: boolean): string[] {
    const features = ['volume_set', 'volume_mute', 'turn_on', 'turn_off', 'select_source'];
    return mediaModeArg ? [...features, 'pause', 'stop', 'previous_track', 'next_track', 'play'] : features;
  }

  private mediaState(powerArg: string | undefined, mediaModeArg: boolean | undefined): string {
    if (powerArg === 'PWSTANDBY') {
      return 'off';
    }
    if (powerArg === 'PWON') {
      return mediaModeArg ? 'playing' : 'on';
    }
    return 'unavailable';
  }

  private volumeMax(linesArg: string[]): number | undefined {
    for (const line of linesArg) {
      if (!line.startsWith('MVMAX')) {
        continue;
      }
      const value = this.numberValue(line.replace(/^MVMAX\s*/i, '').slice(0, 2));
      if (typeof value === 'number') {
        return value;
      }
    }
    return undefined;
  }

  private volume(linesArg: string[]): number | undefined {
    for (const line of linesArg) {
      if (line.startsWith('MVMAX') || !line.startsWith('MV')) {
        continue;
      }
      return this.parseVolumeValue(line.replace(/^MV/i, ''));
    }
    return undefined;
  }

  private parseVolumeValue(valueArg: string): number | undefined {
    const normalized = valueArg.trim();
    if (!normalized) {
      return undefined;
    }
    const value = this.numberValue(normalized);
    if (typeof value !== 'number') {
      return undefined;
    }
    return normalized.length >= 3 && !normalized.includes('.') ? value / 10 : value;
  }

  private numberValue(valueArg: string): number | undefined {
    const value = Number(valueArg);
    return Number.isFinite(value) ? value : undefined;
  }

  private mediaInfo(linesArg: string[]): string | undefined {
    const answerCodes = ['NSE0', 'NSE1X', 'NSE2X', 'NSE3X', 'NSE4', 'NSE5', 'NSE6', 'NSE7', 'NSE8'];
    const lines = linesArg.map((lineArg, indexArg) => this.stripPrefix(lineArg, answerCodes[indexArg] || '')).filter(Boolean);
    return lines.length ? lines.join('\n') : undefined;
  }

  private sourceName(sourceMapArg: Record<string, string>, sourceArg: string | undefined): string | undefined {
    if (!sourceArg) {
      return undefined;
    }
    for (const [prettyName, sourceCode] of Object.entries(sourceMapArg)) {
      if (sourceCode === sourceArg) {
        return prettyName;
      }
    }
    return undefined;
  }

  private stripPrefix(valueArg: string | undefined, prefixArg: string): string | undefined {
    if (!valueArg) {
      return undefined;
    }
    return valueArg.startsWith(prefixArg) ? valueArg.slice(prefixArg.length).trim() || undefined : valueArg.trim() || undefined;
  }

  private executorResultToLines(valueArg: unknown): string[] {
    if (Array.isArray(valueArg)) {
      return valueArg.map((itemArg) => String(itemArg).trim()).filter(Boolean);
    }
    if (typeof valueArg === 'string') {
      return valueArg.split(/\r?\n|\r/).map((lineArg) => lineArg.trim()).filter(Boolean);
    }
    if (valueArg && typeof valueArg === 'object') {
      const result = valueArg as { lines?: unknown; response?: unknown; data?: unknown };
      if (Array.isArray(result.lines)) {
        return result.lines.map((itemArg) => String(itemArg).trim()).filter(Boolean);
      }
      if (typeof result.response === 'string') {
        return this.executorResultToLines(result.response);
      }
      if (typeof result.data === 'string') {
        return this.executorResultToLines(result.data);
      }
    }
    return [];
  }

  private port(): number {
    return this.config.port || denonDefaultPort;
  }

  private timeoutMs(): number {
    return this.config.timeoutMs || denonDefaultTimeoutMs;
  }

  private responseIdleMs(): number {
    return this.config.responseIdleMs || Math.min(200, Math.max(20, Math.floor(this.timeoutMs() / 5)));
  }

  private requireHost(): string {
    if (!this.config.host) {
      throw new DenonTransportError('Denon telnet transport requires config.host.');
    }
    return this.config.host;
  }

  private cloneValue<TValue>(valueArg: TValue): TValue {
    return JSON.parse(JSON.stringify(valueArg)) as TValue;
  }

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

class DenonTelnetSession {
  private buffer = '';
  private pending?: {
    lines: string[];
    resolve: (valueArg: string[]) => void;
    reject: (errorArg: Error) => void;
    idleTimer: ReturnType<typeof setTimeout>;
    timeoutTimer: ReturnType<typeof setTimeout>;
  };
  private closed = false;

  private constructor(
    private readonly socket: plugins.net.Socket,
    private readonly options: { host: string; port: number; timeoutMs: number; responseIdleMs: number },
  ) {}

  public static async connect(optionsArg: { host: string; port: number; timeoutMs: number; responseIdleMs: number }): Promise<DenonTelnetSession> {
    const socket = plugins.net.createConnection({ host: optionsArg.host, port: optionsArg.port });
    const session = new DenonTelnetSession(socket, optionsArg);
    await new Promise<void>((resolve, reject) => {
      let settled = false;
      const finish = (errorArg?: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        socket.removeListener('connect', onConnect);
        socket.removeListener('error', onError);
        socket.removeListener('timeout', onTimeout);
        if (errorArg) {
          socket.destroy();
          reject(errorArg);
          return;
        }
        resolve();
      };
      const onConnect = () => finish();
      const onError = (errorArg: Error) => finish(errorArg);
      const onTimeout = () => finish(new DenonTransportError(`Timed out connecting to Denon receiver at ${optionsArg.host}:${optionsArg.port}.`));
      socket.setEncoding('ascii');
      socket.setTimeout(optionsArg.timeoutMs, onTimeout);
      socket.once('connect', onConnect);
      socket.once('error', onError);
      socket.once('timeout', onTimeout);
    });

    socket.setTimeout(0);
    socket.on('data', (chunkArg) => session.onData(String(chunkArg)));
    socket.on('error', (errorArg) => session.rejectPending(errorArg));
    socket.on('close', () => {
      session.closed = true;
      session.rejectPending(new DenonTransportError('Denon telnet connection closed.'));
    });
    return session;
  }

  public async request(commandArg: string): Promise<string[]> {
    if (this.closed) {
      throw new DenonTransportError('Denon telnet connection is closed.');
    }
    if (this.pending) {
      throw new DenonTransportError('Denon telnet session already has a pending request.');
    }

    return new Promise<string[]>((resolve, reject) => {
      const finish = (errorArg?: Error, linesArg: string[] = []) => {
        const pending = this.pending;
        if (!pending) {
          return;
        }
        clearTimeout(pending.idleTimer);
        clearTimeout(pending.timeoutTimer);
        this.pending = undefined;
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve(linesArg);
      };
      const idleTimer = setTimeout(() => finish(undefined, this.pending?.lines || []), this.options.responseIdleMs);
      const timeoutTimer = setTimeout(() => finish(new DenonTransportError(`Denon telnet command ${commandArg} timed out after ${this.options.timeoutMs}ms.`)), this.options.timeoutMs);
      this.pending = { lines: [], resolve: (valueArg) => finish(undefined, valueArg), reject: (errorArg) => finish(errorArg), idleTimer, timeoutTimer };
      this.socket.write(`${commandArg}\r`, 'ascii');
    });
  }

  public async close(): Promise<void> {
    if (this.closed) {
      return;
    }
    this.closed = true;
    this.socket.removeAllListeners();
    this.socket.destroy();
    this.rejectPending(new DenonTransportError('Denon telnet connection closed.'));
  }

  private onData(chunkArg: string): void {
    this.buffer += chunkArg;
    const lines = this.buffer.split(/\r?\n|\r/);
    this.buffer = lines.pop() || '';
    for (const line of lines) {
      const value = line.trim();
      if (value) {
        this.pending?.lines.push(value);
      }
    }
    if (this.pending) {
      clearTimeout(this.pending.idleTimer);
      this.pending.idleTimer = setTimeout(() => this.pending?.resolve(this.pending.lines), this.options.responseIdleMs);
    }
  }

  private rejectPending(errorArg: Error): void {
    this.pending?.reject(errorArg);
  }
}

const firstLine = (linesArg: string[]): string | undefined => {
  return linesArg[0];
};

const splitFirst = (valueArg: string, separatorArg: string): [string, string | undefined] => {
  const index = valueArg.indexOf(separatorArg);
  if (index < 0) {
    return [valueArg, undefined];
  }
  return [valueArg.slice(0, index), valueArg.slice(index + separatorArg.length).trim() || undefined];
};
