import type {
  IYamahaMusiccastCommandRequest,
  IYamahaMusiccastConfig,
  IYamahaMusiccastDeviceInfo,
  IYamahaMusiccastDistributionInfo,
  IYamahaMusiccastFeatures,
  IYamahaMusiccastFuncStatus,
  IYamahaMusiccastNetusbPlayInfo,
  IYamahaMusiccastNetworkStatus,
  IYamahaMusiccastRangeStep,
  IYamahaMusiccastSnapshot,
  IYamahaMusiccastTunerPlayInfo,
  IYamahaMusiccastZoneFeatures,
  IYamahaMusiccastZoneState,
  IYamahaMusiccastZoneStatus,
  TYamahaMusiccastZoneId,
} from './yamaha_musiccast.types.js';

const defaultZone = 'main';
const defaultPort = 80;
const zoneOrder = ['main', 'zone2', 'zone3', 'zone4'];

export class YamahaMusiccastClient {
  constructor(private readonly config: IYamahaMusiccastConfig) {}

  public async getSnapshot(): Promise<IYamahaMusiccastSnapshot> {
    if (this.config.snapshot) {
      return this.config.snapshot;
    }

    if (!this.config.host) {
      return this.manualSnapshot();
    }

    const deviceInfo = await this.getDeviceInfo();
    const [networkStatus, funcStatus, features, distribution] = await Promise.all([
      this.optionalJson<IYamahaMusiccastNetworkStatus>('/system/getNetworkStatus'),
      this.optionalJson<IYamahaMusiccastFuncStatus>('/system/getFuncStatus'),
      this.optionalJson<IYamahaMusiccastFeatures>('/system/getFeatures'),
      this.optionalJson<IYamahaMusiccastDistributionInfo>('/dist/getDistributionInfo'),
    ]);
    const zoneIds = this.zoneIds(features);
    const [netusb, tuner] = await Promise.all([
      this.optionalJson<IYamahaMusiccastNetusbPlayInfo>('/netusb/getPlayInfo'),
      this.optionalJson<IYamahaMusiccastTunerPlayInfo>('/tuner/getPlayInfo'),
    ]);
    const zones = await Promise.all(zoneIds.map((zoneArg) => this.getZoneState(zoneArg, features)));

    return {
      deviceInfo: { ...this.manualDeviceInfo(), ...deviceInfo },
      networkStatus,
      funcStatus,
      features,
      zones,
      inputNames: this.config.inputNames,
      netusb,
      tuner,
      distribution,
      capabilities: this.config.capabilities,
      lastUpdated: new Date().toISOString(),
    };
  }

  public async getDeviceInfo(): Promise<IYamahaMusiccastDeviceInfo> {
    if (this.config.deviceInfo && !this.config.host) {
      return this.config.deviceInfo;
    }
    if (!this.config.host) {
      return this.manualDeviceInfo();
    }
    return { ...this.manualDeviceInfo(), ...(await this.fetchJson<IYamahaMusiccastDeviceInfo>('/system/getDeviceInfo')) };
  }

  public async execute(requestArg: IYamahaMusiccastCommandRequest): Promise<unknown> {
    if (requestArg.command === 'raw_get') {
      if (!requestArg.path) {
        throw new Error('Yamaha MusicCast raw_get requires a path.');
      }
      return this.command(requestArg.path);
    }

    const zone = this.normalizeZone(requestArg.zone);
    if (requestArg.command === 'turn_on') {
      return this.command(this.path(`/${zone}/setPower`, { power: 'on' }));
    }
    if (requestArg.command === 'turn_off') {
      return this.command(this.path(`/${zone}/setPower`, { power: 'standby' }));
    }
    if (requestArg.command === 'volume_up') {
      return this.command(this.path(`/${zone}/setVolume`, { volume: 'up' }));
    }
    if (requestArg.command === 'volume_down') {
      return this.command(this.path(`/${zone}/setVolume`, { volume: 'down' }));
    }
    if (requestArg.command === 'set_volume') {
      const volume = typeof requestArg.volume === 'number'
        ? requestArg.volume
        : await this.volumeLevelToRaw(zone, requestArg.volumeLevel ?? 0);
      return this.command(this.path(`/${zone}/setVolume`, { volume }));
    }
    if (requestArg.command === 'mute') {
      return this.command(this.path(`/${zone}/setMute`, { enable: this.boolString(Boolean(requestArg.muted)) }));
    }
    if (requestArg.command === 'select_source') {
      if (!requestArg.source) {
        throw new Error('Yamaha MusicCast select_source requires a source.');
      }
      return this.command(this.path(`/${zone}/setInput`, { input: requestArg.source }));
    }
    if (requestArg.command === 'select_sound_mode') {
      if (!requestArg.soundMode) {
        throw new Error('Yamaha MusicCast select_sound_mode requires soundMode.');
      }
      return this.command(this.path(`/${zone}/setSoundProgram`, { program: requestArg.soundMode }));
    }
    if (requestArg.command === 'play' || requestArg.command === 'pause' || requestArg.command === 'stop' || requestArg.command === 'play_pause') {
      const playback = requestArg.command === 'play' ? 'play' : requestArg.command === 'pause' ? 'pause' : requestArg.command === 'stop' ? 'stop' : 'play_pause';
      return this.command(this.path('/netusb/setPlayback', { playback }));
    }
    if (requestArg.command === 'previous_track' || requestArg.command === 'next_track') {
      const playback = requestArg.command === 'previous_track' ? 'previous' : 'next';
      const snapshot = await this.getSnapshot().catch(() => undefined);
      const source = snapshot?.zones.find((zoneArg) => zoneArg.zone === zone)?.input;
      if (source === 'tuner') {
        return this.command(this.path('/tuner/switchPreset', { dir: playback }));
      }
      return this.command(this.path('/netusb/setPlayback', { playback }));
    }
    if (requestArg.command === 'set_repeat') {
      return this.command(this.path('/netusb/setRepeat', { mode: requestArg.repeat || requestArg.option || 'off' }));
    }
    if (requestArg.command === 'set_shuffle') {
      const mode = typeof requestArg.shuffle === 'string' ? requestArg.shuffle : requestArg.shuffle ? 'on' : 'off';
      return this.command(this.path('/netusb/setShuffle', { mode }));
    }
    if (requestArg.command === 'set_switch' || requestArg.command === 'select_option' || requestArg.command === 'set_number') {
      return this.executeCapability(requestArg, zone);
    }

    throw new Error(`Unsupported Yamaha MusicCast command: ${requestArg.command}`);
  }

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

  private async getZoneState(zoneArg: TYamahaMusiccastZoneId, featuresArg: IYamahaMusiccastFeatures | undefined): Promise<IYamahaMusiccastZoneState> {
    const [status, soundPrograms, signalInfo, nameText] = await Promise.all([
      this.optionalJson<IYamahaMusiccastZoneStatus>(`/${zoneArg}/getStatus`),
      this.optionalJson<{ sound_program_list?: string[] }>(`/${zoneArg}/getSoundProgramList`),
      this.optionalJson<Record<string, unknown>>(`/${zoneArg}/getSignalInfo`),
      this.optionalJson<{ text?: string }>(this.path('/system/getNameText', { id: zoneArg })),
    ]);
    return this.normalizeZoneState(zoneArg, status, featuresArg, soundPrograms?.sound_program_list, signalInfo, nameText?.text, status !== undefined);
  }

  private manualSnapshot(): IYamahaMusiccastSnapshot {
    const zones = this.config.zones?.length
      ? this.config.zones.map((zoneArg) => this.normalizeManualZone(zoneArg))
      : [this.normalizeManualZone({ zone: defaultZone, name: this.config.name || 'Main Zone', power: 'standby', available: false })];

    return {
      deviceInfo: this.config.deviceInfo || this.manualDeviceInfo(),
      networkStatus: this.config.networkStatus,
      funcStatus: this.config.funcStatus,
      features: this.config.features,
      zones,
      inputNames: this.config.inputNames,
      netusb: this.config.netusb,
      tuner: this.config.tuner,
      distribution: this.config.distribution,
      capabilities: this.config.capabilities,
      lastUpdated: new Date().toISOString(),
    };
  }

  private manualDeviceInfo(): IYamahaMusiccastDeviceInfo {
    return {
      model_name: this.config.model,
      serial_number: this.config.serialNumber,
      system_id: this.config.systemId || this.config.serialNumber,
      device_id: this.config.deviceId || this.config.systemId || this.config.serialNumber,
    };
  }

  private normalizeManualZone(zoneArg: IYamahaMusiccastZoneState): IYamahaMusiccastZoneState {
    const feature = this.config.features?.zone?.find((featureArg) => featureArg.id === zoneArg.zone);
    const status = zoneArg.status || this.zoneStateToStatus(zoneArg);
    return {
      ...this.normalizeZoneState(zoneArg.zone, status, this.config.features, zoneArg.soundProgramList, zoneArg.signalInfo, zoneArg.name, zoneArg.available !== false),
      ...zoneArg,
      inputList: zoneArg.inputList || feature?.input_list,
      features: zoneArg.features || feature?.func_list,
      rangeStep: zoneArg.rangeStep || feature?.range_step,
    };
  }

  private normalizeZoneState(
    zoneArg: TYamahaMusiccastZoneId,
    statusArg: IYamahaMusiccastZoneStatus | undefined,
    featuresArg: IYamahaMusiccastFeatures | undefined,
    soundProgramListArg: string[] | undefined,
    signalInfoArg: Record<string, unknown> | undefined,
    nameArg: string | undefined,
    availableArg: boolean
  ): IYamahaMusiccastZoneState {
    const feature = featuresArg?.zone?.find((featureArg) => featureArg.id === zoneArg);
    const volumeRange = this.range(feature, 'volume');
    const minVolume = statusArg?.min_volume ?? volumeRange?.min ?? 0;
    const maxVolume = statusArg?.max_volume ?? volumeRange?.max ?? 100;
    const volume = statusArg?.volume;

    return {
      zone: zoneArg,
      name: nameArg || this.config.zoneNames?.[zoneArg] || this.defaultZoneName(zoneArg),
      power: statusArg?.power,
      available: availableArg,
      sleep: statusArg?.sleep,
      volume,
      minVolume,
      maxVolume,
      volumeLevel: typeof volume === 'number' && maxVolume > minVolume ? (volume - minVolume) / (maxVolume - minVolume) : undefined,
      muted: statusArg?.mute,
      input: statusArg?.input,
      inputText: statusArg?.input_text,
      inputList: feature?.input_list,
      soundProgram: statusArg?.sound_program,
      soundProgramList: soundProgramListArg || feature?.sound_program_list,
      surrDecoderType: statusArg?.surr_decoder_type,
      surrDecoderTypeList: feature?.surr_decoder_type_list,
      toneControlModeList: feature?.tone_control_mode_list,
      equalizerModeList: feature?.equalizer_mode_list,
      linkControlList: feature?.link_control_list,
      linkAudioDelayList: feature?.link_audio_delay_list,
      linkAudioQualityList: feature?.link_audio_quality_list,
      rangeStep: feature?.range_step,
      features: feature?.func_list,
      status: statusArg,
      signalInfo: signalInfoArg,
      actualVolume: statusArg?.actual_volume,
      toneControl: statusArg?.tone_control,
      equalizer: statusArg?.equalizer,
      balance: this.numberValue(statusArg?.balance),
      dialogueLevel: this.numberValue(statusArg?.dialogue_level),
      dialogueLift: this.numberValue(statusArg?.dialogue_lift),
      dtsDialogueControl: this.numberValue(statusArg?.dts_dialogue_control),
      subwooferVolume: this.numberValue(statusArg?.subwoofer_volume),
      linkControl: statusArg?.link_control,
      linkAudioDelay: statusArg?.link_audio_delay,
      linkAudioQuality: statusArg?.link_audio_quality,
      extraBass: this.booleanValue(statusArg?.extra_bass),
      bassExtension: this.booleanValue(statusArg?.bass_extension),
      enhancer: this.booleanValue(statusArg?.enhancer),
      pureDirect: this.booleanValue(statusArg?.pure_direct),
      adaptiveDrc: this.booleanValue(statusArg?.adaptive_drc),
      clearVoice: this.booleanValue(statusArg?.clear_voice),
      surround3d: this.booleanValue(statusArg?.surround_3d ?? statusArg?.['3d_surround']),
      mono: this.booleanValue(statusArg?.mono),
    };
  }

  private zoneStateToStatus(zoneArg: IYamahaMusiccastZoneState): IYamahaMusiccastZoneStatus {
    return {
      power: zoneArg.power,
      sleep: zoneArg.sleep,
      volume: zoneArg.volume,
      max_volume: zoneArg.maxVolume,
      min_volume: zoneArg.minVolume,
      mute: zoneArg.muted,
      input: zoneArg.input,
      input_text: zoneArg.inputText,
      sound_program: zoneArg.soundProgram,
      surr_decoder_type: zoneArg.surrDecoderType,
      tone_control: zoneArg.toneControl,
      equalizer: zoneArg.equalizer,
      balance: zoneArg.balance,
      dialogue_level: zoneArg.dialogueLevel,
      dialogue_lift: zoneArg.dialogueLift,
      dts_dialogue_control: zoneArg.dtsDialogueControl,
      subwoofer_volume: zoneArg.subwooferVolume,
      link_control: zoneArg.linkControl,
      link_audio_delay: zoneArg.linkAudioDelay,
      link_audio_quality: zoneArg.linkAudioQuality,
      extra_bass: zoneArg.extraBass,
      bass_extension: zoneArg.bassExtension,
      enhancer: zoneArg.enhancer,
      pure_direct: zoneArg.pureDirect,
      adaptive_drc: zoneArg.adaptiveDrc,
      clear_voice: zoneArg.clearVoice,
      surround_3d: zoneArg.surround3d,
      mono: zoneArg.mono,
    };
  }

  private async executeCapability(requestArg: IYamahaMusiccastCommandRequest, zoneArg: TYamahaMusiccastZoneId): Promise<unknown> {
    if (!requestArg.capabilityId) {
      throw new Error('Yamaha MusicCast capability service calls require capabilityId.');
    }
    const value = requestArg.value ?? requestArg.option;
    const capability = requestArg.capabilityId;

    if (capability === 'enhancer') {
      return this.command(this.path(`/${zoneArg}/setEnhancer`, { enable: this.boolString(Boolean(value)) }));
    }
    if (capability === 'pure_direct') {
      return this.command(this.path(`/${zoneArg}/setPureDirect`, { enable: this.boolString(Boolean(value)) }));
    }
    if (capability === 'extra_bass') {
      return this.command(this.path(`/${zoneArg}/setExtraBass`, { enable: this.boolString(Boolean(value)) }));
    }
    if (capability === 'bass_extension') {
      return this.command(this.path(`/${zoneArg}/setBassExtension`, { enable: this.boolString(Boolean(value)) }));
    }
    if (capability === 'adaptive_drc') {
      return this.command(this.path(`/${zoneArg}/setAdaptiveDrc`, { enable: this.boolString(Boolean(value)) }));
    }
    if (capability === 'clear_voice') {
      return this.command(this.path(`/${zoneArg}/setClearVoice`, { enable: this.boolString(Boolean(value)) }));
    }
    if (capability === 'surround_3d') {
      return this.command(this.path(`/${zoneArg}/set3dSurround`, { enable: this.boolString(Boolean(value)) }));
    }
    if (capability === 'sleep') {
      return this.command(this.path(`/${zoneArg}/setSleep`, { sleep: this.sleepValue(value) }));
    }
    if (capability === 'tone_control_mode') {
      return this.command(this.path(`/${zoneArg}/setToneControl`, { mode: value }));
    }
    if (capability === 'tone_control_bass') {
      return this.command(this.path(`/${zoneArg}/setToneControl`, { bass: value }));
    }
    if (capability === 'tone_control_treble') {
      return this.command(this.path(`/${zoneArg}/setToneControl`, { treble: value }));
    }
    if (capability === 'equalizer_mode') {
      return this.command(this.path(`/${zoneArg}/setEqualizer`, { mode: value }));
    }
    if (capability === 'equalizer_low') {
      return this.command(this.path(`/${zoneArg}/setEqualizer`, { low: value }));
    }
    if (capability === 'equalizer_mid') {
      return this.command(this.path(`/${zoneArg}/setEqualizer`, { mid: value }));
    }
    if (capability === 'equalizer_high') {
      return this.command(this.path(`/${zoneArg}/setEqualizer`, { high: value }));
    }
    if (capability === 'balance') {
      return this.command(this.path(`/${zoneArg}/setBalance`, { value }));
    }
    if (capability === 'dialogue_level') {
      return this.command(this.path(`/${zoneArg}/setDialogueLevel`, { value }));
    }
    if (capability === 'dialogue_lift') {
      return this.command(this.path(`/${zoneArg}/setDialogueLift`, { value }));
    }
    if (capability === 'dts_dialogue_control') {
      return this.command(this.path(`/${zoneArg}/setDtsDialogueControl`, { num: value }));
    }
    if (capability === 'subwoofer_volume') {
      return this.command(this.path(`/${zoneArg}/setSubwooferVolume`, { volume: value }));
    }
    if (capability === 'surr_decoder_type') {
      return this.command(this.path(`/${zoneArg}/setSurroundDecoderType`, { type: value }));
    }
    if (capability === 'link_control') {
      return this.command(this.path(`/${zoneArg}/setLinkControl`, { control: value }));
    }
    if (capability === 'link_audio_delay') {
      return this.command(this.path(`/${zoneArg}/setLinkAudioDelay`, { delay: value }));
    }
    if (capability === 'link_audio_quality') {
      return this.command(this.path(`/${zoneArg}/setLinkAudioQuality`, { mode: value }));
    }

    throw new Error(`Unsupported Yamaha MusicCast capability: ${capability}`);
  }

  private async volumeLevelToRaw(zoneArg: TYamahaMusiccastZoneId, volumeLevelArg: number): Promise<number> {
    const snapshot = await this.getSnapshot();
    const zone = snapshot.zones.find((zoneItemArg) => zoneItemArg.zone === zoneArg);
    const minVolume = zone?.minVolume ?? 0;
    const maxVolume = zone?.maxVolume ?? 100;
    return Math.round(minVolume + Math.max(0, Math.min(1, volumeLevelArg)) * (maxVolume - minVolume));
  }

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

  private async optionalJson<T>(pathArg: string): Promise<T | undefined> {
    try {
      return await this.fetchJson<T>(pathArg);
    } catch {
      return undefined;
    }
  }

  private async fetchJson<T>(pathArg: string): Promise<T> {
    if (!this.config.host) {
      throw new Error('Yamaha MusicCast host is required for local HTTP API calls.');
    }
    const controller = this.config.requestTimeoutMs ? new AbortController() : undefined;
    const timeout = controller ? setTimeout(() => controller.abort(), this.config.requestTimeoutMs) : undefined;
    try {
      const response = await globalThis.fetch(`${this.baseUrl()}${pathArg}`, {
        headers: {
          'X-AppName': 'MusicCast/1.0',
          'X-AppPort': '41100',
        },
        signal: controller?.signal,
      });
      const text = await response.text();
      if (!response.ok) {
        throw new Error(`Yamaha MusicCast request ${pathArg} failed with HTTP ${response.status}: ${text}`);
      }
      const data = JSON.parse(text) as T & { response_code?: number };
      if (typeof data.response_code === 'number' && data.response_code !== 0) {
        throw new Error(`Yamaha MusicCast request ${pathArg} failed with response_code ${data.response_code}.`);
      }
      return data;
    } finally {
      if (timeout) {
        clearTimeout(timeout);
      }
    }
  }

  private zoneIds(featuresArg: IYamahaMusiccastFeatures | undefined): TYamahaMusiccastZoneId[] {
    const configured = this.config.zones?.map((zoneArg) => zoneArg.zone) || [];
    const fromFeatures = featuresArg?.zone?.map((zoneArg) => zoneArg.id) || [];
    const values = configured.length ? configured : fromFeatures.length ? fromFeatures : [defaultZone];
    return [...new Set(values)].sort((leftArg, rightArg) => this.zoneSort(leftArg) - this.zoneSort(rightArg));
  }

  private zoneSort(zoneArg: string): number {
    const index = zoneOrder.indexOf(zoneArg);
    return index === -1 ? 999 : index;
  }

  private range(featureArg: IYamahaMusiccastZoneFeatures | undefined, idArg: string): IYamahaMusiccastRangeStep | undefined {
    return featureArg?.range_step?.find((rangeArg) => rangeArg.id === idArg);
  }

  private normalizeZone(zoneArg: TYamahaMusiccastZoneId | undefined): TYamahaMusiccastZoneId {
    if (!zoneArg || zoneArg === '1') {
      return defaultZone;
    }
    if (zoneArg === '2') {
      return 'zone2';
    }
    if (zoneArg === '3') {
      return 'zone3';
    }
    if (zoneArg === '4') {
      return 'zone4';
    }
    return zoneArg;
  }

  private defaultZoneName(zoneArg: string): string {
    if (zoneArg === defaultZone) {
      return this.config.name || 'Main Zone';
    }
    return zoneArg.replace(/^zone/, 'Zone ');
  }

  private baseUrl(): string {
    const rawHost = this.config.host || 'localhost';
    if (/^https?:\/\//i.test(rawHost)) {
      const url = new URL(rawHost);
      if (this.config.port && !url.port) {
        url.port = String(this.config.port);
      }
      return url.origin;
    }
    const host = rawHost.replace(/\/.*$/, '');
    const port = this.config.port || defaultPort;
    return /:\d+$/.test(host) ? `http://${host}` : `http://${host}:${port}`;
  }

  private path(baseArg: string, paramsArg: Record<string, string | number | boolean | undefined>): string {
    const params = new URLSearchParams();
    for (const [key, value] of Object.entries(paramsArg)) {
      if (value !== undefined) {
        params.set(key, String(value));
      }
    }
    const query = params.toString();
    return query ? `${baseArg}?${query}` : baseArg;
  }

  private boolString(valueArg: boolean): string {
    return valueArg ? 'true' : 'false';
  }

  private sleepValue(valueArg: unknown): number {
    if (typeof valueArg === 'number') {
      return valueArg;
    }
    if (valueArg === 'off') {
      return 0;
    }
    const match = String(valueArg || '').match(/\d+/);
    return match ? Number(match[0]) : 0;
  }

  private numberValue(valueArg: unknown): number | undefined {
    return typeof valueArg === 'number' && Number.isFinite(valueArg) ? valueArg : undefined;
  }

  private booleanValue(valueArg: unknown): boolean | undefined {
    return typeof valueArg === 'boolean' ? valueArg : undefined;
  }
}
