import * as plugins from '../../plugins.js';
import { SimpleLocalClient, SimpleLocalMapper, type ISimpleLocalEntitySnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import type { IPioneerConfig, IPioneerSnapshot } from './pioneer.types.js';
import { pioneerDefaultName, pioneerDefaultPort, pioneerMaxSourceNumbers, pioneerMaxVolume, pioneerProfile } from './pioneer.types.js';

export class PioneerClient extends SimpleLocalClient<IPioneerConfig> {
  private liveSnapshot?: IPioneerSnapshot;

  constructor(private readonly configArg: IPioneerConfig) {
    super(pioneerProfile, configArg);
  }

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

    if (this.shouldUseNativeTelnet(forceRefreshArg)) {
      try {
        this.liveSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.liveSnapshot = this.createOfflineSnapshot(errorMessage(errorArg));
      }
      return SimpleLocalMapper.clone(this.liveSnapshot);
    }

    this.liveSnapshot = await super.getSnapshot(forceRefreshArg) as IPioneerSnapshot;
    return SimpleLocalMapper.clone(this.liveSnapshot);
  }

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

  public override async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.client?.execute || this.configArg.commandExecutor || !this.endpoint()) {
      return super.execute(requestArg);
    }

    try {
      const command = await this.commandForService(requestArg);
      const endpoint = this.requireEndpoint();
      await sendPioneerCommand(endpoint, command, this.configArg.timeoutMs || 3000);
      this.liveSnapshot = undefined;
      return { success: true, data: { command } };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  private shouldUseNativeTelnet(forceRefreshArg: boolean): boolean {
    if (!this.endpoint() || this.configArg.client || this.configArg.snapshotProvider) {
      return false;
    }
    if (!forceRefreshArg && this.configArg.snapshot) {
      return false;
    }
    if (!forceRefreshArg && (this.configArg.rawData !== undefined || this.configArg.rawText !== undefined || this.configArg.entities?.length)) {
      return false;
    }
    return true;
  }

  private async fetchSnapshot(): Promise<IPioneerSnapshot> {
    const endpoint = this.requireEndpoint();
    const connection = await PioneerTelnetConnection.connect(endpoint, this.configArg.timeoutMs || 3000);
    try {
      const commandTimeoutMs = this.configArg.commandTimeoutMs || 250;
      const powerRaw = await connection.request('?P', 'PWR', commandTimeoutMs);
      const volumeRaw = await connection.request('?V', 'VOL', commandTimeoutMs);
      const muteRaw = await connection.request('?M', 'MUT', commandTimeoutMs);
      const sourceNameToNumber = await this.sourceNameToNumber(connection, commandTimeoutMs);
      const sourceNumberToName = reverseSourceMap(sourceNameToNumber);
      const sourceRaw = await connection.request('?F', 'FN', commandTimeoutMs);
      const sourceNumber = sourceRaw?.slice(2);
      const source = sourceNumber ? sourceNumberToName[sourceNumber] || sourceNumber : undefined;
      const volume = volumeRaw ? numberValue(volumeRaw.slice(3)) : undefined;
      const state = pioneerPowerState(powerRaw);
      const name = this.configArg.name || pioneerDefaultName;
      const entity: ISimpleLocalEntitySnapshot = {
        id: 'media_player',
        uniqueId: `${pioneerProfile.domain}_${SimpleLocalMapper.slug(this.configArg.uniqueId || this.configArg.deviceId || endpoint.host)}_media_player`,
        name,
        platform: 'media_player',
        state,
        available: true,
        writable: true,
        attributes: cleanAttributes({
          powerRaw,
          volumeRaw,
          volume,
          volumeLevel: volume === undefined ? undefined : volume / pioneerMaxVolume,
          isVolumeMuted: muteRaw === undefined ? undefined : muteRaw === 'MUT0',
          muteRaw,
          source,
          sourceNumber,
          sourceList: Object.keys(sourceNameToNumber).length ? Object.keys(sourceNameToNumber).sort() : undefined,
          sources: Object.keys(sourceNameToNumber).length ? sourceNameToNumber : undefined,
        }),
      };

      return {
        device: {
          id: this.configArg.uniqueId || this.configArg.deviceId || `${endpoint.host}:${endpoint.port}`,
          name,
          manufacturer: 'Pioneer',
          model: stringValue(this.configArg.model) || 'Pioneer AVR',
          serialNumber: this.configArg.macAddress,
          host: endpoint.host,
          port: endpoint.port,
          protocol: 'telnet',
        },
        entities: [entity],
        online: true,
        updatedAt: new Date().toISOString(),
        source: 'tcp',
        rawData: {
          powerRaw,
          volumeRaw,
          muteRaw,
          sourceRaw,
          sources: sourceNameToNumber,
        },
        capabilities: {
          localRead: true,
          localControl: true,
          telnet: true,
        },
      };
    } finally {
      connection.close();
    }
  }

  private async sourceNameToNumber(connectionArg: PioneerTelnetConnection, commandTimeoutMsArg: number): Promise<Record<string, string>> {
    const configured = recordValue(this.configArg.sources) || recordValue(this.configArg.metadata?.sources);
    if (configured) {
      return configured;
    }

    const sources: Record<string, string> = {};
    const limit = Math.max(0, Math.min(pioneerMaxSourceNumbers, this.configArg.sourceScanLimit ?? pioneerMaxSourceNumbers));
    for (let index = 0; index < limit; index++) {
      const response = await connectionArg.request(`?RGB${String(index).padStart(2, '0')}`, 'RGB', commandTimeoutMsArg);
      const source = parseSourceResponse(response);
      if (source) {
        sources[source.name] = source.number;
      }
    }
    return sources;
  }

  private async commandForService(requestArg: IServiceCallRequest): Promise<string> {
    const data = requestArg.data || {};
    if (typeof data.command === 'string' && data.command.trim()) {
      return data.command.trim();
    }
    if (requestArg.service === 'turn_on') {
      return 'PO';
    }
    if (requestArg.service === 'turn_off') {
      return 'PF';
    }
    if (requestArg.service === 'volume_up') {
      return 'VU';
    }
    if (requestArg.service === 'volume_down') {
      return 'VD';
    }
    if (requestArg.service === 'volume_mute' || requestArg.service === 'mute_volume') {
      return boolValue(data.mute ?? data.is_volume_muted ?? data.muted ?? true) ? 'MO' : 'MF';
    }
    if (requestArg.service === 'set_volume_level' || requestArg.service === 'volume_set' || requestArg.service === 'set_level') {
      const requested = numberValue(data.volume_level ?? data.volume ?? data.level);
      const percentage = numberValue(data.percentage);
      const value = percentage !== undefined ? (percentage / 100) * pioneerMaxVolume : requested !== undefined && requested <= 1 ? requested * pioneerMaxVolume : requested;
      if (value === undefined) {
        throw new Error('Pioneer set_volume_level requires data.volume_level, data.volume, data.level, or data.percentage.');
      }
      return `${String(Math.max(0, Math.min(pioneerMaxVolume, Math.round(value)))).padStart(3, '0')}VL`;
    }
    if (requestArg.service === 'select_source') {
      const sourceNumber = stringValue(data.sourceNumber) || stringValue(data.source_number) || stringValue(data.input);
      if (sourceNumber && /^\d{2}$/.test(sourceNumber)) {
        return `${sourceNumber}FN`;
      }
      const source = stringValue(data.source) || stringValue(data.source_name);
      const sourceMap = recordValue(this.configArg.sources) || recordValue(this.configArg.metadata?.sources) || recordValue((await this.getSnapshot()).entities[0]?.attributes?.sources);
      const mapped = source ? sourceMap?.[source] : undefined;
      if (!mapped) {
        throw new Error('Pioneer select_source requires data.sourceNumber or data.source matching configured/discovered sources.');
      }
      return `${mapped}FN`;
    }
    throw new Error(`Unsupported Pioneer native service: ${requestArg.domain}.${requestArg.service}`);
  }

  private requireEndpoint(): IPioneerEndpoint {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('Pioneer Telnet requires config.host or config.url.');
    }
    return endpoint;
  }

  private endpoint(): IPioneerEndpoint | undefined {
    const rawUrl = stringValue(this.configArg.url) || stringValue(this.configArg.metadata?.url);
    if (rawUrl) {
      const parsed = parseUrl(rawUrl);
      if (parsed) {
        return { host: parsed.hostname, port: this.configArg.port || numberValue(parsed.port) || pioneerDefaultPort };
      }
    }
    const host = stringValue(this.configArg.host);
    if (!host) {
      return undefined;
    }
    const parsedHost = parseUrl(host);
    if (parsedHost) {
      return { host: parsedHost.hostname, port: this.configArg.port || numberValue(parsedHost.port) || pioneerDefaultPort };
    }
    const hostPort = splitHostPort(host);
    return { host: hostPort.host, port: this.configArg.port || hostPort.port || pioneerDefaultPort };
  }

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

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

class PioneerTelnetConnection {
  private buffer = '';
  private lines: string[] = [];
  private waiters: Array<() => void> = [];
  private closed = false;
  private lastError?: Error;

  private constructor(private readonly socket: plugins.net.Socket) {
    socket.on('data', (chunkArg) => this.pushData(Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg)));
    socket.on('close', () => {
      this.closed = true;
      this.notify();
    });
    socket.on('end', () => {
      this.closed = true;
      this.notify();
    });
    socket.on('error', (errorArg) => {
      this.lastError = errorArg;
      this.closed = true;
      this.notify();
    });
  }

  public static async connect(endpointArg: IPioneerEndpoint, timeoutMsArg: number): Promise<PioneerTelnetConnection> {
    return new Promise<PioneerTelnetConnection>((resolve, reject) => {
      const socket = plugins.net.createConnection({ host: endpointArg.host, port: endpointArg.port });
      let settled = false;
      const finish = (connectionArg?: PioneerTelnetConnection, errorArg?: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        socket.off('connect', onConnect);
        socket.off('error', onError);
        socket.off('timeout', onTimeout);
        if (errorArg) {
          socket.destroy();
          reject(errorArg);
          return;
        }
        resolve(connectionArg!);
      };
      const onConnect = () => finish(new PioneerTelnetConnection(socket));
      const onError = (errorArg: Error) => finish(undefined, errorArg);
      const onTimeout = () => finish(undefined, new Error(`Pioneer Telnet connection to ${endpointArg.host}:${endpointArg.port} timed out after ${timeoutMsArg}ms.`));
      socket.setTimeout(timeoutMsArg);
      socket.once('connect', onConnect);
      socket.once('error', onError);
      socket.once('timeout', onTimeout);
    });
  }

  public async request(commandArg: string, expectedPrefixArg: string, timeoutMsArg: number): Promise<string | undefined> {
    await this.write(`${commandArg}\r`);
    for (let index = 0; index < 3; index++) {
      const line = await this.readLine(timeoutMsArg);
      if (!line) {
        return undefined;
      }
      if (line.startsWith(expectedPrefixArg)) {
        return line;
      }
    }
    return undefined;
  }

  public async write(dataArg: string): Promise<void> {
    if (this.lastError) {
      throw this.lastError;
    }
    await new Promise<void>((resolve, reject) => {
      this.socket.write(dataArg, 'ascii', (errorArg) => errorArg ? reject(errorArg) : resolve());
    });
  }

  public close(): void {
    this.closed = true;
    this.socket.destroy();
    this.notify();
  }

  private async readLine(timeoutMsArg: number): Promise<string | undefined> {
    if (this.lastError) {
      throw this.lastError;
    }
    if (this.lines.length) {
      return this.lines.shift();
    }
    if (this.closed) {
      return undefined;
    }
    return new Promise<string | undefined>((resolve, reject) => {
      let settled = false;
      const cleanup = () => {
        clearTimeout(timer);
        this.waiters = this.waiters.filter((waiterArg) => waiterArg !== waiter);
      };
      const done = (valueArg?: string, errorArg?: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        cleanup();
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve(valueArg);
      };
      const waiter = () => {
        if (this.lastError) {
          done(undefined, this.lastError);
          return;
        }
        if (this.lines.length) {
          done(this.lines.shift());
          return;
        }
        if (this.closed) {
          done(undefined);
        }
      };
      const timer = setTimeout(() => done(undefined), timeoutMsArg);
      this.waiters.push(waiter);
    });
  }

  private pushData(chunkArg: Buffer): void {
    this.buffer += chunkArg.toString('ascii');
    let match = /\r\n|\r|\n/.exec(this.buffer);
    while (match?.index !== undefined) {
      const line = this.buffer.slice(0, match.index).trim();
      this.buffer = this.buffer.slice(match.index + match[0].length);
      if (line) {
        this.lines.push(line);
      }
      match = /\r\n|\r|\n/.exec(this.buffer);
    }
    this.notify();
  }

  private notify(): void {
    const waiters = this.waiters.splice(0);
    for (const waiter of waiters) {
      waiter();
    }
  }
}

const sendPioneerCommand = async (endpointArg: IPioneerEndpoint, commandArg: string, timeoutMsArg: number): Promise<void> => {
  const connection = await PioneerTelnetConnection.connect(endpointArg, timeoutMsArg);
  try {
    await connection.write(`${commandArg}\r`);
    await delay(40);
  } finally {
    connection.close();
  }
};

const parseSourceResponse = (responseArg: string | undefined): { number: string; name: string } | undefined => {
  const match = responseArg ? /^RGB(\d{2})\s*(.+)$/.exec(responseArg) : undefined;
  const name = match?.[2]?.trim();
  return match && name ? { number: match[1], name } : undefined;
};

const pioneerPowerState = (valueArg: string | undefined): string => {
  if (valueArg === 'PWR0') {
    return 'on';
  }
  if (valueArg === 'PWR1' || valueArg === 'PWR2') {
    return 'off';
  }
  return 'unknown';
};

const reverseSourceMap = (sourcesArg: Record<string, string>): Record<string, string> => Object.fromEntries(Object.entries(sourcesArg).map(([nameArg, numberArg]) => [numberArg, nameArg]));

const recordValue = (valueArg: unknown): Record<string, string> | undefined => {
  if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
    return undefined;
  }
  const entries = Object.entries(valueArg).filter((entryArg): entryArg is [string, string] => typeof entryArg[1] === 'string' && entryArg[1].trim().length > 0);
  return entries.length ? Object.fromEntries(entries) : undefined;
};

const stringValue = (valueArg: unknown): string | undefined => 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() && Number.isFinite(Number(valueArg))) {
    return Number(valueArg);
  }
  return undefined;
};

const boolValue = (valueArg: unknown): boolean => valueArg === true || valueArg === 'true' || valueArg === '1' || valueArg === 1;

const cleanAttributes = (attributesArg: Record<string, unknown>): Record<string, unknown> | undefined => {
  const clean = Object.fromEntries(Object.entries(attributesArg).filter(([, valueArg]) => valueArg !== undefined));
  return Object.keys(clean).length ? clean : undefined;
};

const parseUrl = (valueArg: string): URL | undefined => {
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg)) {
    return undefined;
  }
  try {
    return new URL(valueArg);
  } catch {
    return undefined;
  }
};

const splitHostPort = (valueArg: string): { host: string; port?: number } => {
  const match = /^([^:]+):(\d+)$/.exec(valueArg);
  return match ? { host: match[1], port: Number(match[2]) } : { host: valueArg };
};

const delay = async (msArg: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, msArg));

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