import type { IDenonavrConfig, IDenonavrReceiverInfo, IDenonavrSnapshot, IDenonavrZoneState, TDenonavrCommand, TDenonavrZone } from './denonavr.types.js';

const zoneNumbers: Record<TDenonavrZone, number> = { Main: 1, Zone2: 2, Zone3: 3 };
const statusPaths: Record<TDenonavrZone, string> = {
  Main: '/goform/formMainZone_MainZoneXmlStatus.xml',
  Zone2: '/goform/formZone2_Zone2XmlStatus.xml',
  Zone3: '/goform/formZone3_Zone3XmlStatus.xml',
};
const sourcePrefixes: Record<TDenonavrZone, string> = { Main: 'SI', Zone2: 'Z2', Zone3: 'Z3' };
const volumeUpCommands: Record<TDenonavrZone, string> = { Main: 'MVUP', Zone2: 'Z2UP', Zone3: 'Z3UP' };
const volumeDownCommands: Record<TDenonavrZone, string> = { Main: 'MVDOWN', Zone2: 'Z2DOWN', Zone3: 'Z3DOWN' };

const defaultSourceMap: Record<string, string> = {
  'TV AUDIO': 'TV',
  TV: 'TV',
  'Blu-ray': 'BD',
  'BLU-RAY': 'BD',
  BD: 'BD',
  'CBL/SAT': 'SAT/CBL',
  'SAT/CBL': 'SAT/CBL',
  DVD: 'DVD',
  'Media Player': 'MPLAY',
  'MEDIA PLAYER': 'MPLAY',
  MPLAY: 'MPLAY',
  GAME: 'GAME',
  AUX: 'AUX1',
  AUX1: 'AUX1',
  CD: 'CD',
  PHONO: 'PHONO',
  Tuner: 'TUNER',
  TUNER: 'TUNER',
  NETWORK: 'NET',
  NET: 'NET',
  Bluetooth: 'BT',
  BT: 'BT',
  'iPod/USB': 'USB/IPOD',
  USB: 'USB/IPOD',
  'USB/IPOD': 'USB/IPOD',
  'Internet Radio': 'IRADIO',
  IRADIO: 'IRADIO',
  'Media Server': 'SERVER',
  SERVER: 'SERVER',
  Favorites: 'FAVORITES',
  FAVORITES: 'FAVORITES',
  Spotify: 'SPOTIFY',
  SpotifyConnect: 'SPOTIFY',
  'Spotify Connect': 'SPOTIFY',
};

export class DenonavrClient {
  constructor(private readonly config: IDenonavrConfig) {}

  public async getSnapshot(): Promise<IDenonavrSnapshot> {
    if (this.config.snapshot) {
      return this.config.snapshot;
    }
    const receiverInfo = await this.getReceiverInfo();
    const zones = await this.getZones();
    return { receiverInfo, zones, lastUpdated: new Date().toISOString() };
  }

  public async getReceiverInfo(): Promise<IDenonavrReceiverInfo> {
    if (this.config.receiverInfo) {
      return { ...this.manualReceiverInfo(), ...this.config.receiverInfo };
    }
    if (!this.config.host) {
      return this.manualReceiverInfo();
    }

    const descriptions = await this.tryDescriptionXml();
    if (descriptions) {
      return { ...this.manualReceiverInfo(), ...descriptions };
    }

    const deviceInfo = await this.tryDeviceInfoXml();
    return { ...this.manualReceiverInfo(), ...deviceInfo };
  }

  public async getZones(): Promise<IDenonavrZoneState[]> {
    if (this.config.zones) {
      return this.config.zones;
    }
    if (!this.config.host) {
      return [{ zone: 'Main', name: this.config.name || 'Main Zone', power: 'OFF', state: 'off', available: false }];
    }

    const zones: TDenonavrZone[] = ['Main'];
    if (this.config.zone2) {
      zones.push('Zone2');
    }
    if (this.config.zone3) {
      zones.push('Zone3');
    }

    return Promise.all(zones.map((zoneArg) => this.getZoneState(zoneArg)));
  }

  public async execute(requestArg: { command: TDenonavrCommand; zone?: TDenonavrZone; source?: string; volumeLevel?: number; volumeDb?: number; muted?: boolean; path?: string }): Promise<string | undefined> {
    const zone = requestArg.zone || 'Main';
    if (requestArg.command === 'turn_on') {
      return this.command(`/goform/formiPhoneAppPower.xml?${zoneNumbers[zone]}+PowerOn`);
    }
    if (requestArg.command === 'turn_off') {
      return this.command(`/goform/formiPhoneAppPower.xml?${zoneNumbers[zone]}+PowerStandby`);
    }
    if (requestArg.command === 'volume_up') {
      return this.direct(volumeUpCommands[zone]);
    }
    if (requestArg.command === 'volume_down') {
      return this.direct(volumeDownCommands[zone]);
    }
    if (requestArg.command === 'set_volume') {
      const volumeDb = typeof requestArg.volumeDb === 'number' ? requestArg.volumeDb : this.volumeLevelToDb(requestArg.volumeLevel ?? 0);
      return this.command(`/goform/formiPhoneAppVolume.xml?${zoneNumbers[zone]}+${volumeDb.toFixed(1)}`);
    }
    if (requestArg.command === 'mute') {
      return this.command(`/goform/formiPhoneAppMute.xml?${zoneNumbers[zone]}+${requestArg.muted ? 'MuteOn' : 'MuteOff'}`);
    }
    if (requestArg.command === 'select_source') {
      if (!requestArg.source) {
        throw new Error('Denon AVR select_source requires a source.');
      }
      return this.direct(`${sourcePrefixes[zone]}${this.toSourceCode(requestArg.source, zone)}`);
    }
    if (requestArg.command === 'play') {
      return this.direct('NS9A');
    }
    if (requestArg.command === 'pause') {
      return this.direct('NS9B');
    }
    if (requestArg.command === 'stop') {
      return this.direct('NS9C');
    }
    if (requestArg.command === 'play_pause') {
      const snapshot = await this.getSnapshot();
      const zoneState = snapshot.zones.find((zoneArg) => zoneArg.zone === zone);
      return this.direct(zoneState?.state === 'playing' ? 'NS9B' : 'NS9A');
    }
    if (requestArg.command === 'previous_track') {
      await this.netaudioCommand('CurUp');
      return undefined;
    }
    if (requestArg.command === 'next_track') {
      await this.netaudioCommand('CurDown');
      return undefined;
    }
    if (requestArg.command === 'get_command') {
      if (!requestArg.path) {
        throw new Error('Denon AVR get_command requires a path.');
      }
      return this.command(requestArg.path);
    }
    throw new Error(`Unsupported Denon AVR command: ${requestArg.command}`);
  }

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

  private async getZoneState(zoneArg: TDenonavrZone): Promise<IDenonavrZoneState> {
    const [statusXml, mainXml] = await Promise.all([
      this.fetchText(statusPaths[zoneArg]).catch(() => ''),
      zoneArg === 'Main' ? this.fetchText('/goform/formMainZone_MainZoneXml.xml').catch(() => '') : Promise.resolve(''),
    ]);
    const xml = `${statusXml}\n${mainXml}`;
    const source = this.firstContainerValue(xml, ['InputFuncSelect', 'InputFunc']) || undefined;
    const volumeDb = this.numberValue(this.firstContainerValue(xml, ['MasterVolume', 'Volume']));
    const muted = this.boolOnOff(this.firstContainerValue(xml, ['Mute']));
    const media = await this.getMediaInfo(source);
    const power = this.firstContainerValue(xml, ['ZonePower', 'Power']) || undefined;
    return {
      zone: zoneArg,
      name: zoneArg === 'Main' ? (this.config.name || this.firstContainerValue(mainXml, ['FriendlyName']) || 'Main Zone') : zoneArg,
      power,
      state: this.stateFromPower(power, source, media),
      volumeDb,
      volumeLevel: typeof volumeDb === 'number' ? this.volumeDbToLevel(volumeDb) : undefined,
      muted,
      source,
      sourceList: this.readInputList(mainXml),
      sourceMap: this.config.sourceMap,
      soundModeRaw: this.firstContainerValue(xml, ['selectSurround', 'SurrMode']) || undefined,
      soundMode: this.firstContainerValue(xml, ['selectSurround', 'SurrMode']) || undefined,
      ecoMode: this.firstContainerValue(xml, ['ECOMode']) || undefined,
      media,
      available: Boolean(statusXml || mainXml),
    };
  }

  private async getMediaInfo(sourceArg: string | undefined) {
    const source = sourceArg?.toLowerCase() || '';
    if (['internet radio', 'media server', 'network', 'net', 'bluetooth', 'bt', 'spotify', 'spotifyconnect', 'ipod/usb', 'usb/ipod'].includes(source)) {
      const xml = await this.fetchText('/goform/formNetAudio_StatusXml.xml').catch(() => '');
      const lines = this.readSzLines(xml);
      return {
        title: lines[1],
        artist: lines[2],
        album: lines[4],
        imageUrl: this.config.host ? `http://${this.config.host}:${this.port()}/img/album%20art_S.png` : undefined,
        contentType: 'music',
      };
    }
    if (source === 'tuner') {
      const xml = await this.fetchText('/goform/formTuner_TunerXml.xml').catch(() => '');
      return {
        band: this.firstContainerValue(xml, ['Band']) || undefined,
        frequency: this.firstContainerValue(xml, ['Frequency']) || undefined,
        contentType: 'channel',
      };
    }
    if (source === 'hd radio' || source === 'hdradio') {
      const xml = await this.fetchText('/goform/formTuner_HdXml.xml').catch(() => '');
      return {
        title: this.firstContainerValue(xml, ['Title']) || undefined,
        artist: this.firstContainerValue(xml, ['Artist']) || undefined,
        album: this.firstContainerValue(xml, ['Album']) || undefined,
        band: this.firstContainerValue(xml, ['Band']) || undefined,
        frequency: this.firstContainerValue(xml, ['Frequency']) || undefined,
        station: this.firstContainerValue(xml, ['StationNameSh']) || undefined,
        contentType: 'music',
      };
    }
    return undefined;
  }

  private async tryDescriptionXml(): Promise<IDenonavrReceiverInfo | undefined> {
    const attempts = [
      { port: this.config.descriptionPort || this.config.port || 8080, path: '/description.xml' },
      { port: 60006, path: '/upnp/desc/aios_device/aios_device.xml' },
      { port: 8080, path: '/description.xml' },
      { port: 80, path: '/description.xml' },
    ];
    const seen = new Set<string>();
    for (const attempt of attempts) {
      const key = `${attempt.port}${attempt.path}`;
      if (seen.has(key)) {
        continue;
      }
      seen.add(key);
      const xml = await this.fetchText(attempt.path, attempt.port).catch(() => '');
      if (!xml) {
        continue;
      }
      const manufacturer = this.readXmlTag(xml, 'manufacturer');
      const modelName = this.readXmlTag(xml, 'modelName');
      const serialNumber = this.readXmlTag(xml, 'serialNumber');
      if (manufacturer || modelName || serialNumber) {
        return {
          host: this.config.host,
          port: this.port(),
          friendlyName: this.readXmlTag(xml, 'friendlyName'),
          name: this.readXmlTag(xml, 'friendlyName'),
          manufacturer,
          modelName,
          modelNumber: this.readXmlTag(xml, 'modelNumber'),
          serialNumber,
          presentationUrl: this.readXmlTag(xml, 'presentationURL'),
        };
      }
    }
    return undefined;
  }

  private async tryDeviceInfoXml(): Promise<IDenonavrReceiverInfo | undefined> {
    const xml = await this.fetchText('/goform/Deviceinfo.xml').catch(() => '');
    if (!xml) {
      return undefined;
    }
    return {
      host: this.config.host,
      port: this.port(),
      manufacturer: this.readXmlTag(xml, 'Manufacturer') || this.config.manufacturer,
      modelName: this.readXmlTag(xml, 'ModelName') || this.config.model,
      serialNumber: this.readXmlTag(xml, 'SerialNumber') || this.config.serialNumber,
      receiverType: this.readXmlTag(xml, 'CommApiVers') ? 'avr-x' : this.config.receiverType,
    };
  }

  private manualReceiverInfo(): IDenonavrReceiverInfo {
    return {
      host: this.config.host,
      port: this.config.port || 80,
      name: this.config.name,
      friendlyName: this.config.name,
      manufacturer: this.config.manufacturer || 'Denon',
      modelName: this.config.model,
      serialNumber: this.config.serialNumber,
      receiverType: this.config.receiverType,
      receiverPort: this.config.port || 80,
    };
  }

  private async command(pathArg: string): Promise<string> {
    return this.fetchText(pathArg.startsWith('/') ? pathArg : `/${pathArg}`);
  }

  private async direct(commandArg: string): Promise<string> {
    return this.command(`/goform/formiPhoneAppDirect.xml?${encodeURIComponent(commandArg).replace(/%2F/g, '/').replace(/%20/g, '%20')}`);
  }

  private async netaudioCommand(commandArg: 'CurUp' | 'CurDown'): Promise<void> {
    if (!this.config.host) {
      throw new Error('Denon AVR host is required for local HTTP commands.');
    }
    const body = new URLSearchParams({
      cmd0: `PutNetAudioCommand/${commandArg}`,
      cmd1: 'aspMainZone_WebUpdateStatus/',
      ZoneName: 'MAIN ZONE',
    });
    const response = await globalThis.fetch(`${this.baseUrl()}/NetAudio/index.put.asp`, {
      method: 'POST',
      headers: { 'content-type': 'application/x-www-form-urlencoded' },
      body,
    });
    if (!response.ok) {
      throw new Error(`Denon AVR NetAudio command failed with HTTP ${response.status}: ${await response.text()}`);
    }
  }

  private async fetchText(pathArg: string, portArg = this.port()): Promise<string> {
    if (!this.config.host) {
      throw new Error('Denon AVR host is required when snapshot data is not provided.');
    }
    const response = await globalThis.fetch(`${this.baseUrl(portArg)}${pathArg}`);
    const text = await response.text();
    if (!response.ok) {
      throw new Error(`Denon AVR request ${pathArg} failed with HTTP ${response.status}: ${text}`);
    }
    return text;
  }

  private stateFromPower(powerArg: string | undefined, sourceArg: string | undefined, mediaArg: unknown): string {
    if (!powerArg || powerArg === 'OFF' || powerArg === 'STANDBY') {
      return 'off';
    }
    if (mediaArg && sourceArg) {
      return 'playing';
    }
    return 'on';
  }

  private toSourceCode(sourceArg: string, zoneArg: TDenonavrZone): string {
    const zone = this.config.zones?.find((itemArg) => itemArg.zone === zoneArg);
    const sourceMap = { ...defaultSourceMap, ...this.config.sourceMap, ...zone?.sourceMap };
    return sourceMap[sourceArg] || sourceMap[sourceArg.toUpperCase()] || sourceArg.toUpperCase().replace(/\s+/g, '');
  }

  private volumeLevelToDb(valueArg: number): number {
    return Math.max(-80, Math.min(18, valueArg * 100 - 80));
  }

  private volumeDbToLevel(valueArg: number): number {
    return Math.max(0, Math.min(1, (valueArg + 80) / 100));
  }

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

  private boolOnOff(valueArg: string | undefined): boolean | undefined {
    if (!valueArg) {
      return undefined;
    }
    const value = valueArg.toLowerCase();
    if (value === 'on') {
      return true;
    }
    if (value === 'off') {
      return false;
    }
    return undefined;
  }

  private readInputList(xmlArg: string): string[] | undefined {
    const container = this.readContainer(xmlArg, 'InputFuncList');
    if (!container) {
      return undefined;
    }
    const values = this.readAllTags(container, 'value');
    return values.length ? values : undefined;
  }

  private readSzLines(xmlArg: string): Record<number, string> {
    const lines: Record<number, string> = {};
    const container = this.readContainer(xmlArg, 'szLine') || '';
    const values = this.readAllTags(container, 'value');
    values.forEach((valueArg, indexArg) => {
      lines[indexArg] = valueArg;
    });
    return lines;
  }

  private firstContainerValue(xmlArg: string, tagsArg: string[]): string | undefined {
    for (const tag of tagsArg) {
      const container = this.readContainer(xmlArg, tag);
      if (container) {
        return this.readXmlTag(container, 'value') || this.stripTags(container).trim() || undefined;
      }
      const value = this.readXmlTag(xmlArg, tag);
      if (value) {
        return value;
      }
    }
    return undefined;
  }

  private readContainer(xmlArg: string, tagArg: string): string | undefined {
    const escapedTag = tagArg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const regex = new RegExp(`<(?:[A-Za-z0-9_]+:)?${escapedTag}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/(?:[A-Za-z0-9_]+:)?${escapedTag}>`, 'i');
    return regex.exec(xmlArg)?.[1]?.trim();
  }

  private readXmlTag(xmlArg: string, tagArg: string): string | undefined {
    const escapedTag = tagArg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const regex = new RegExp(`<(?:[A-Za-z0-9_]+:)?${escapedTag}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/(?:[A-Za-z0-9_]+:)?${escapedTag}>`, 'i');
    const match = regex.exec(xmlArg);
    return match?.[1] ? this.unescapeXml(this.stripTags(match[1]).trim()) : undefined;
  }

  private readAllTags(xmlArg: string, tagArg: string): string[] {
    const escapedTag = tagArg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const regex = new RegExp(`<(?:[A-Za-z0-9_]+:)?${escapedTag}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/(?:[A-Za-z0-9_]+:)?${escapedTag}>`, 'gi');
    const values: string[] = [];
    let match: RegExpExecArray | null;
    while ((match = regex.exec(xmlArg))) {
      values.push(this.unescapeXml(this.stripTags(match[1]).trim()));
    }
    return values.filter(Boolean);
  }

  private stripTags(valueArg: string): string {
    return valueArg.replace(/<[^>]+>/g, '');
  }

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

  private baseUrl(portArg = this.port()): string {
    return `http://${this.config.host}:${portArg}`;
  }

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