import * as plugins from '../../plugins.js';
import type {
  IHarmanKardonAvrClientLike,
  IHarmanKardonAvrCommandRequest,
  IHarmanKardonAvrCommandResult,
  IHarmanKardonAvrConfig,
  IHarmanKardonAvrMediaPlayerState,
  IHarmanKardonAvrRawCommandRequest,
  IHarmanKardonAvrSnapshot,
  THarmanKardonAvrSnapshotSource,
} from './harman_kardon_avr.types.js';
import {
  harmanKardonAvrCommandMap,
  harmanKardonAvrDefaultName,
  harmanKardonAvrDefaultPort,
  harmanKardonAvrDefaultSources,
  harmanKardonAvrDefaultTimeoutMs,
  harmanKardonAvrDefaultZone,
} from './harman_kardon_avr.types.js';

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

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

export class HarmanKardonAvrClient {
  private currentSnapshot?: IHarmanKardonAvrSnapshot;

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

  public async getSnapshot(): Promise<IHarmanKardonAvrSnapshot> {
    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);
    }

    if (this.config.snapshot) {
      this.currentSnapshot = this.normalizeSnapshot(this.cloneValue(this.config.snapshot), 'snapshot');
      return this.cloneValue(this.currentSnapshot);
    }

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

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

  public async validateConnection(): Promise<IHarmanKardonAvrSnapshot> {
    const snapshot = await this.fetchSnapshot();
    if (!snapshot.online) {
      throw new HarmanKardonAvrTransportError(snapshot.error || 'Harman Kardon AVR did not return live TCP status.');
    }
    return snapshot;
  }

  public async execute(requestArg: IHarmanKardonAvrCommandRequest): Promise<IHarmanKardonAvrCommandResult> {
    if (this.config.client?.execute) {
      const response = await this.config.client.execute(requestArg);
      this.applyCommandState(requestArg);
      return { command: requestArg.command, response, snapshot: this.currentSnapshot ? this.cloneValue(this.currentSnapshot) : undefined };
    }

    this.assertWritableTransport();
    const command = this.rawCommandFromRequest(requestArg);
    if (command.skipped) {
      return { command: requestArg.command, skipped: true, snapshot: this.currentSnapshot ? this.cloneValue(this.currentSnapshot) : undefined };
    }

    if (!command.rawCommand) {
      throw new HarmanKardonAvrClientError(`Unsupported Harman Kardon AVR command: ${requestArg.command}`);
    }

    const rawRequest = this.rawRequest(command.rawCommand, command.parameter);
    const response = this.config.commandExecutor
      ? await this.config.commandExecutor.execute(rawRequest)
      : await this.sendRawCommand(command.rawCommand, command.parameter);

    this.applyCommandState(requestArg);
    return {
      command: requestArg.command,
      rawCommand: command.rawCommand,
      parameter: command.parameter,
      response,
      snapshot: this.currentSnapshot ? this.cloneValue(this.currentSnapshot) : undefined,
    };
  }

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

  private async fetchSnapshot(): Promise<IHarmanKardonAvrSnapshot> {
    const source = this.config.commandExecutor && !this.config.host ? 'executor' : 'tcp';
    const rawCommand = harmanKardonAvrCommandMap.heartbeat;
    const rawRequest = this.rawRequest(rawCommand);
    const response = this.config.commandExecutor
      ? await this.config.commandExecutor.execute(rawRequest)
      : await this.sendRawCommand(rawCommand);
    const responseLines = this.responseToLines(response);
    return this.normalizeSnapshot({
      deviceInfo: {
        host: this.config.host,
        port: this.port(),
        name: this.config.name || this.currentSnapshot?.deviceInfo.name || harmanKardonAvrDefaultName,
        manufacturer: this.config.manufacturer || this.currentSnapshot?.deviceInfo.manufacturer || 'Harman Kardon',
        model: this.config.model || this.currentSnapshot?.deviceInfo.model,
        serialNumber: this.config.serialNumber || this.currentSnapshot?.deviceInfo.serialNumber,
        zone: this.zone(),
      },
      state: {
        ...this.config.state,
        ...this.currentSnapshot?.state,
        sourceList: this.sourceList(),
        supportedFeatures: this.supportedFeatures(),
      },
      online: true,
      available: true,
      source,
      rawResponse: responseLines,
    }, source);
  }

  private rawCommandFromRequest(requestArg: IHarmanKardonAvrCommandRequest): { rawCommand?: string; parameter?: string; skipped?: boolean } {
    if (requestArg.command === 'raw') {
      if (!requestArg.rawCommand) {
        throw new HarmanKardonAvrClientError('Harman Kardon AVR raw command requires data.command or data.rawCommand.');
      }
      return { rawCommand: requestArg.rawCommand, parameter: requestArg.parameter };
    }
    if (requestArg.command === 'select_source') {
      if (!requestArg.source) {
        throw new HarmanKardonAvrClientError('Harman Kardon AVR select_source requires data.source.');
      }
      return { rawCommand: harmanKardonAvrCommandMap.select_source, parameter: requestArg.source };
    }
    if (requestArg.command === 'mute') {
      if (typeof requestArg.muted !== 'boolean') {
        throw new HarmanKardonAvrClientError('Harman Kardon AVR mute requires data.is_volume_muted or data.mute.');
      }
      const knownMuted = this.currentSnapshot?.state.muted ?? this.config.state?.muted ?? this.config.snapshot?.state.muted;
      if (typeof knownMuted !== 'boolean') {
        throw new HarmanKardonAvrClientError('Harman Kardon AVR mute requires known current mute state because the AVR protocol only exposes mute-toggle.');
      }
      if (knownMuted === requestArg.muted) {
        return { skipped: true };
      }
      return { rawCommand: harmanKardonAvrCommandMap.mute_toggle };
    }
    if (requestArg.command === 'mute_toggle') {
      return { rawCommand: harmanKardonAvrCommandMap.mute_toggle };
    }

    const rawCommand = harmanKardonAvrCommandMap[requestArg.command as keyof typeof harmanKardonAvrCommandMap];
    return rawCommand ? { rawCommand } : {};
  }

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

  private rawRequest(commandArg: string, parameterArg = ''): IHarmanKardonAvrRawCommandRequest {
    const body = this.buildXmlBody(commandArg, parameterArg);
    return {
      command: 'raw',
      rawCommand: commandArg,
      parameter: parameterArg,
      host: this.config.host,
      port: this.port(),
      zone: this.zone(),
      timeoutMs: this.timeoutMs(),
      body,
      httpRequest: this.buildHttpRequest(commandArg, parameterArg),
    };
  }

  private buildHttpRequest(commandArg: string, parameterArg = ''): string {
    const body = this.buildXmlBody(commandArg, parameterArg);
    return [
      'POST AVR HTTP/1.1',
      `Host: :${this.port()}`,
      'User-Agent: Harman Kardon AVR Remote Controller /2.0',
      `Content-Length: ${Buffer.byteLength(body, 'utf8')}`,
      '',
      body,
    ].join('\r\n');
  }

  private buildXmlBody(commandArg: string, parameterArg = ''): string {
    return `<?xml version="1.0" encoding="UTF-8"?>
        <harman>
            <avr>
                <common>
                    <control>
                        <name>${this.escapeXml(commandArg)}</name>
                        <zone>${this.escapeXml(this.zone())}</zone>
                        <para>${this.escapeXml(parameterArg)}</para>
                    </control>
                </common>
            </avr>
        </harman>`;
  }

  private async snapshotFromClient(clientArg: IHarmanKardonAvrClientLike): Promise<IHarmanKardonAvrSnapshot> {
    const result = clientArg.getSnapshot ? await clientArg.getSnapshot() : clientArg.snapshot ? await clientArg.snapshot() : undefined;
    if (!result) {
      throw new HarmanKardonAvrClientError('Harman Kardon AVR client must expose getSnapshot() or snapshot().');
    }
    return result;
  }

  private normalizeSnapshot(snapshotArg: IHarmanKardonAvrSnapshot, sourceArg: THarmanKardonAvrSnapshotSource): IHarmanKardonAvrSnapshot {
    const state = this.normalizeState(snapshotArg.state || {});
    return {
      ...snapshotArg,
      deviceInfo: {
        host: snapshotArg.deviceInfo.host || this.config.host,
        port: snapshotArg.deviceInfo.port || this.port(),
        name: snapshotArg.deviceInfo.name || this.config.name || harmanKardonAvrDefaultName,
        manufacturer: snapshotArg.deviceInfo.manufacturer || this.config.manufacturer || 'Harman Kardon',
        model: snapshotArg.deviceInfo.model || this.config.model,
        serialNumber: snapshotArg.deviceInfo.serialNumber || this.config.serialNumber,
        zone: snapshotArg.deviceInfo.zone || this.zone(),
      },
      state,
      online: snapshotArg.online,
      available: snapshotArg.available,
      source: sourceArg,
      lastUpdated: snapshotArg.lastUpdated || new Date().toISOString(),
    };
  }

  private normalizeState(stateArg: IHarmanKardonAvrMediaPlayerState): IHarmanKardonAvrMediaPlayerState {
    return {
      ...stateArg,
      sourceList: stateArg.sourceList || this.sourceList(),
      supportedFeatures: stateArg.supportedFeatures || this.supportedFeatures(),
    };
  }

  private offlineSnapshot(errorArg: string): IHarmanKardonAvrSnapshot {
    return this.normalizeSnapshot({
      deviceInfo: {
        host: this.config.host,
        port: this.port(),
        name: this.config.name || harmanKardonAvrDefaultName,
        manufacturer: this.config.manufacturer || 'Harman Kardon',
        model: this.config.model,
        serialNumber: this.config.serialNumber,
        zone: this.zone(),
      },
      state: {
        ...this.config.state,
        ...this.currentSnapshot?.state,
        state: 'unavailable',
        sourceList: this.sourceList(),
        supportedFeatures: this.supportedFeatures(),
      },
      online: false,
      available: false,
      source: 'runtime',
      error: errorArg,
    }, 'runtime');
  }

  private applyCommandState(requestArg: IHarmanKardonAvrCommandRequest): void {
    const base = this.currentSnapshot || this.normalizeSnapshot({
      deviceInfo: {
        host: this.config.host,
        port: this.port(),
        name: this.config.name || harmanKardonAvrDefaultName,
        manufacturer: this.config.manufacturer || 'Harman Kardon',
        model: this.config.model,
        serialNumber: this.config.serialNumber,
        zone: this.zone(),
      },
      state: { ...this.config.state, sourceList: this.sourceList(), supportedFeatures: this.supportedFeatures() },
      online: true,
      available: true,
      source: this.config.commandExecutor && !this.config.host ? 'executor' : 'tcp',
    }, this.config.commandExecutor && !this.config.host ? 'executor' : 'tcp');

    const nextState: IHarmanKardonAvrMediaPlayerState = {
      ...base.state,
      lastCommand: requestArg.command,
    };

    if (requestArg.command === 'turn_on') {
      nextState.power = 'ON';
      nextState.state = 'on';
    } else if (requestArg.command === 'turn_off') {
      nextState.power = 'OFF';
      nextState.state = 'off';
    } else if (requestArg.command === 'select_source' && requestArg.source) {
      nextState.source = requestArg.source;
    } else if (requestArg.command === 'mute') {
      nextState.muted = requestArg.muted;
    } else if (requestArg.command === 'mute_toggle') {
      nextState.muted = typeof nextState.muted === 'boolean' ? !nextState.muted : undefined;
    }

    this.currentSnapshot = this.normalizeSnapshot({
      ...base,
      state: nextState,
      online: true,
      available: true,
      error: undefined,
    }, base.source === 'snapshot' || base.source === 'manual' || base.source === 'runtime'
      ? this.config.commandExecutor && !this.config.host ? 'executor' : 'tcp'
      : base.source);
  }

  private assertWritableTransport(): void {
    if (this.config.commandExecutor || this.config.host) {
      return;
    }
    throw new HarmanKardonAvrTransportError('Harman Kardon AVR live commands require config.host, injected client.execute, or commandExecutor. Static snapshots/manual data are read-only.');
  }

  private responseToLines(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.responseToLines(result.response);
      }
      if (typeof result.data === 'string') {
        return this.responseToLines(result.data);
      }
    }
    return [];
  }

  private supportedFeatures(): string[] {
    return ['volume_step', 'volume_mute', 'turn_on', 'turn_off', 'select_source', 'play', 'pause', 'next_track', 'previous_track'];
  }

  private sourceList(): string[] {
    return this.config.sources || this.config.snapshot?.state.sourceList || this.currentSnapshot?.state.sourceList || harmanKardonAvrDefaultSources;
  }

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

  private zone(): string {
    return this.config.zone || this.config.snapshot?.deviceInfo.zone || harmanKardonAvrDefaultZone;
  }

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

  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 HarmanKardonAvrTransportError('Harman Kardon AVR TCP transport requires config.host.');
    }
    return this.config.host;
  }

  private escapeXml(valueArg: string): string {
    return valueArg
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&apos;');
  }

  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 HarmanKardonAvrTcpSession {
  private buffer = '';
  private pending?: {
    lines: string[];
    reject: (errorArg: Error) => void;
    finish: (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<HarmanKardonAvrTcpSession> {
    const socket = plugins.net.createConnection({ host: optionsArg.host, port: optionsArg.port });
    const session = new HarmanKardonAvrTcpSession(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 HarmanKardonAvrTransportError(`Timed out connecting to Harman Kardon AVR at ${optionsArg.host}:${optionsArg.port}.`));
      socket.setEncoding('utf8');
      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.finishPending();
    });
    return session;
  }

  public async request(payloadArg: string): Promise<string[]> {
    if (this.closed) {
      throw new HarmanKardonAvrTransportError('Harman Kardon AVR TCP connection is closed.');
    }
    if (this.pending) {
      throw new HarmanKardonAvrTransportError('Harman Kardon AVR TCP session already has a pending request.');
    }

    return new Promise<string[]>((resolve, reject) => {
      const finish = (errorArg?: Error) => {
        const pending = this.pending;
        if (!pending) {
          return;
        }
        clearTimeout(pending.idleTimer);
        clearTimeout(pending.timeoutTimer);
        this.pending = undefined;
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve([...pending.lines]);
      };
      const idleTimer = setTimeout(() => finish(), this.options.responseIdleMs);
      const timeoutTimer = setTimeout(() => finish(new HarmanKardonAvrTransportError(`Harman Kardon AVR TCP command timed out after ${this.options.timeoutMs}ms.`)), this.options.timeoutMs);
      this.pending = { lines: [], reject, finish, idleTimer, timeoutTimer };
      this.socket.write(payloadArg, 'utf8', (errorArg?: Error | null) => {
        if (errorArg) {
          finish(errorArg);
        }
      });
    });
  }

  public async close(): Promise<void> {
    if (this.closed) {
      return;
    }
    this.closed = true;
    this.socket.removeAllListeners();
    this.socket.destroy();
    this.finishPending();
  }

  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);
      }
    }
  }

  private rejectPending(errorArg: Error): void {
    const pending = this.pending;
    if (!pending) {
      return;
    }
    pending.reject(errorArg);
    clearTimeout(pending.idleTimer);
    clearTimeout(pending.timeoutTimer);
    this.pending = undefined;
  }

  private finishPending(): void {
    this.pending?.finish();
  }
}
