import type {
  IDunehdCommandRequest,
  IDunehdConfig,
  IDunehdDeviceInfo,
  IDunehdRawCommandRequest,
  IDunehdSnapshot,
  IDunehdState,
} from './dunehd.types.js';
import { dunehdDefaultName, dunehdDefaultPort } from './dunehd.types.js';

const playbackSpeedPlay = 256;
const playbackSpeedPause = 0;
const playbackSpeedFastForward = 512;
const playbackSpeedRewind = -512;
const defaultTimeoutMs = 5000;

const irCodes = {
  turnOn: 'A05FBF00',
  turnOff: 'A15EBF00',
  previousTrack: 'B649BF00',
  nextTrack: 'E21DBF00',
} as const;

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

export class DunehdClient {
  private currentSnapshot?: IDunehdSnapshot;

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

  public async getSnapshot(): Promise<IDunehdSnapshot> {
    if (this.currentSnapshot) {
      return this.normalizeSnapshot(this.cloneValue(this.currentSnapshot));
    }

    if (!this.config.host && !this.config.commandExecutor) {
      return this.normalizeSnapshot(this.snapshotFromConfig());
    }

    const state = await this.updateState().catch(() => ({}));
    return this.normalizeSnapshot({
      deviceInfo: this.deviceInfoFromConfig(),
      state,
      online: Object.keys(state).length > 0,
      updatedAt: new Date().toISOString(),
    });
  }

  public async validateConnection(): Promise<IDunehdSnapshot> {
    const state = await this.updateState();
    if (!Object.keys(state).length) {
      throw new DunehdTransportError('Dune HD did not return status data.');
    }
    return this.normalizeSnapshot({
      deviceInfo: this.deviceInfoFromConfig(),
      state,
      online: true,
      updatedAt: new Date().toISOString(),
    });
  }

  public async updateState(): Promise<IDunehdState> {
    return this.sendCommand('status');
  }

  public async execute(requestArg: IDunehdCommandRequest): Promise<IDunehdState> {
    if (requestArg.command === 'turn_on') {
      return this.turnOn();
    }
    if (requestArg.command === 'turn_off') {
      return this.turnOff();
    }
    if (requestArg.command === 'play') {
      return this.play();
    }
    if (requestArg.command === 'pause') {
      return this.pause();
    }
    if (requestArg.command === 'stop') {
      return this.stop();
    }
    if (requestArg.command === 'previous_track') {
      return this.previousTrack();
    }
    if (requestArg.command === 'next_track') {
      return this.nextTrack();
    }
    if (requestArg.command === 'volume_up') {
      return this.volumeUp();
    }
    if (requestArg.command === 'volume_down') {
      return this.volumeDown();
    }
    if (requestArg.command === 'mute') {
      if (typeof requestArg.muted !== 'boolean') {
        throw new Error('Dune HD mute command requires muted.');
      }
      return this.mute(requestArg.muted);
    }
    if (requestArg.command === 'play_media') {
      if (!requestArg.mediaUrl) {
        throw new Error('Dune HD play_media requires mediaUrl.');
      }
      return this.launchMediaUrl(requestArg.mediaUrl);
    }
    if (requestArg.command === 'remote') {
      if (!requestArg.remoteCommand) {
        throw new Error('Dune HD remote command requires remoteCommand.');
      }
      return this.sendRemoteCommand(requestArg.remoteCommand);
    }
    throw new Error(`Unsupported Dune HD command: ${requestArg.command}`);
  }

  public async launchMediaUrl(mediaUrlArg: string): Promise<IDunehdState> {
    return this.sendCommand('launch_media_url', { media_url: mediaUrlArg });
  }

  public async play(): Promise<IDunehdState> {
    return this.changePlaybackSpeed(playbackSpeedPlay);
  }

  public async pause(): Promise<IDunehdState> {
    return this.changePlaybackSpeed(playbackSpeedPause);
  }

  public async fastForward(): Promise<IDunehdState> {
    return this.changePlaybackSpeed(playbackSpeedFastForward);
  }

  public async rewind(): Promise<IDunehdState> {
    return this.changePlaybackSpeed(playbackSpeedRewind);
  }

  public async stop(): Promise<IDunehdState> {
    return this.sendCommand('standby');
  }

  public async turnOn(): Promise<IDunehdState> {
    return this.sendIrCode(irCodes.turnOn);
  }

  public async turnOff(): Promise<IDunehdState> {
    return this.sendIrCode(irCodes.turnOff);
  }

  public async previousTrack(): Promise<IDunehdState> {
    return this.sendIrCode(irCodes.previousTrack);
  }

  public async nextTrack(): Promise<IDunehdState> {
    return this.sendIrCode(irCodes.nextTrack);
  }

  public async volumeUp(): Promise<IDunehdState> {
    const state = await this.updateState();
    return this.setVolume(Math.min(100, this.numberValue(state.playback_volume, 0) + 10));
  }

  public async volumeDown(): Promise<IDunehdState> {
    const state = await this.updateState();
    return this.setVolume(Math.max(0, this.numberValue(state.playback_volume, 0) - 10));
  }

  public async setVolume(volumePercentArg: number): Promise<IDunehdState> {
    return this.sendCommand('set_playback_state', { volume: Math.max(0, Math.min(100, Math.round(volumePercentArg))) });
  }

  public async mute(mutedArg: boolean): Promise<IDunehdState> {
    return this.sendCommand('set_playback_state', { mute: mutedArg ? 1 : 0 });
  }

  public async sendRemoteCommand(commandArg: string): Promise<IDunehdState> {
    const command = commandArg.trim();
    if (!command) {
      throw new Error('Dune HD remote command cannot be empty.');
    }

    const normalized = command.replace(/[_\s-]+/g, '').toLowerCase();
    const aliases: Record<string, () => Promise<IDunehdState>> = {
      on: () => this.turnOn(),
      poweron: () => this.turnOn(),
      turnon: () => this.turnOn(),
      off: () => this.turnOff(),
      poweroff: () => this.turnOff(),
      turnoff: () => this.turnOff(),
      play: () => this.play(),
      pause: () => this.pause(),
      stop: () => this.stop(),
      previous: () => this.previousTrack(),
      previoustrack: () => this.previousTrack(),
      prev: () => this.previousTrack(),
      next: () => this.nextTrack(),
      nexttrack: () => this.nextTrack(),
      volumeup: () => this.volumeUp(),
      volup: () => this.volumeUp(),
      volumedown: () => this.volumeDown(),
      voldown: () => this.volumeDown(),
      mute: () => this.mute(true),
      unmute: () => this.mute(false),
    };
    const alias = aliases[normalized];
    if (alias) {
      return alias();
    }

    if (/^[a-f0-9]{8}$/i.test(command)) {
      return this.sendIrCode(command.toUpperCase());
    }

    throw new Error(`Unsupported Dune HD remote command: ${command}`);
  }

  public parseStatus(statusArg: string): IDunehdState {
    const state: IDunehdState = {};
    this.readStatusAttributes(statusArg).forEach((attrsArg) => {
      const name = attrsArg.name;
      if (name) {
        state[name] = attrsArg.value || '';
      }
    });
    return state;
  }

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

  private async sendIrCode(codeArg: string): Promise<IDunehdState> {
    return this.sendCommand('ir_code', { ir_code: codeArg });
  }

  private async changePlaybackSpeed(speedArg: number): Promise<IDunehdState> {
    return this.sendCommand('set_playback_state', { speed: speedArg });
  }

  private async sendCommand(cmdArg: string, paramsArg: Record<string, string | number | boolean> = {}): Promise<IDunehdState> {
    const request = this.rawRequest(cmdArg, paramsArg);
    if (this.config.commandExecutor) {
      const result = await this.config.commandExecutor.execute(request);
      const state = this.resultToState(result);
      this.updateCurrentSnapshot(state);
      return state;
    }
    const state = await this.requestHttp(request);
    this.updateCurrentSnapshot(state);
    return state;
  }

  private async requestHttp(requestArg: IDunehdRawCommandRequest): Promise<IDunehdState> {
    if (!this.config.host) {
      throw new DunehdTransportError('Dune HD command transport requires config.host or commandExecutor.');
    }

    const abortController = new AbortController();
    const timeout = setTimeout(() => abortController.abort(), this.config.timeoutMs || defaultTimeoutMs);
    try {
      const response = await globalThis.fetch(requestArg.uri, { method: requestArg.method, signal: abortController.signal });
      const text = await response.text();
      if (!response.ok) {
        throw new DunehdTransportError(`Dune HD request ${requestArg.cmd} failed with HTTP ${response.status}: ${text}`);
      }
      return this.parseStatus(text);
    } catch (errorArg) {
      if (errorArg instanceof DunehdTransportError) {
        throw errorArg;
      }
      if (errorArg instanceof Error && errorArg.name === 'AbortError') {
        throw new DunehdTransportError(`Dune HD request ${requestArg.cmd} timed out after ${this.config.timeoutMs || defaultTimeoutMs}ms.`);
      }
      throw new DunehdTransportError(`Dune HD request ${requestArg.cmd} failed: ${errorArg instanceof Error ? errorArg.message : String(errorArg)}`);
    } finally {
      clearTimeout(timeout);
    }
  }

  private rawRequest(cmdArg: string, paramsArg: Record<string, string | number | boolean>): IDunehdRawCommandRequest {
    return {
      cmd: cmdArg,
      params: { ...paramsArg },
      uri: this.commandUri(cmdArg, paramsArg),
      host: this.config.host,
      port: this.config.port || dunehdDefaultPort,
      method: 'GET',
    };
  }

  private commandUri(cmdArg: string, paramsArg: Record<string, string | number | boolean>): string {
    const searchParams = new URLSearchParams();
    searchParams.set('cmd', cmdArg);
    for (const [key, value] of Object.entries(paramsArg)) {
      searchParams.set(key, String(value));
    }
    if (!this.config.host) {
      return `/cgi-bin/do?${searchParams.toString()}`;
    }
    const port = this.config.port && this.config.port !== dunehdDefaultPort ? `:${this.config.port}` : '';
    return `http://${this.config.host}${port}/cgi-bin/do?${searchParams.toString()}`;
  }

  private resultToState(resultArg: unknown): IDunehdState {
    if (typeof resultArg === 'string') {
      return this.parseStatus(resultArg);
    }
    if (this.isRecord(resultArg)) {
      if (this.isRecord(resultArg.state)) {
        return this.recordToState(resultArg.state);
      }
      return this.recordToState(resultArg);
    }
    return {};
  }

  private recordToState(recordArg: Record<string, unknown>): IDunehdState {
    const state: IDunehdState = {};
    for (const [key, value] of Object.entries(recordArg)) {
      if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || value === undefined) {
        state[key] = value;
      }
    }
    return state;
  }

  private updateCurrentSnapshot(stateArg: IDunehdState): void {
    if (!this.currentSnapshot) {
      if (this.config.snapshot) {
        this.currentSnapshot = this.normalizeSnapshot(this.config.snapshot);
      } else if (this.config.state) {
        this.currentSnapshot = this.snapshotFromConfig();
      }
    }
    if (!this.currentSnapshot) {
      return;
    }
    this.currentSnapshot.state = { ...this.currentSnapshot.state, ...stateArg };
    this.currentSnapshot.online = Object.keys(this.currentSnapshot.state).length > 0;
    this.currentSnapshot.updatedAt = new Date().toISOString();
  }

  private snapshotFromConfig(): IDunehdSnapshot {
    const state = { ...(this.config.state || {}) };
    return {
      deviceInfo: this.deviceInfoFromConfig(),
      state,
      online: Object.keys(state).length > 0,
      updatedAt: new Date().toISOString(),
    };
  }

  private deviceInfoFromConfig(): IDunehdDeviceInfo {
    return {
      ...this.config.deviceInfo,
      id: this.config.deviceInfo?.id || this.config.uniqueId || this.config.serialNumber || this.config.macAddress || this.config.host,
      host: this.config.deviceInfo?.host || this.config.host,
      port: this.config.deviceInfo?.port || this.config.port || dunehdDefaultPort,
      name: this.config.deviceInfo?.name || this.config.name || this.config.host || dunehdDefaultName,
      manufacturer: this.config.deviceInfo?.manufacturer || this.config.manufacturer || 'Dune',
      model: this.config.deviceInfo?.model || this.config.model,
      serialNumber: this.config.deviceInfo?.serialNumber || this.config.serialNumber,
      macAddress: this.config.deviceInfo?.macAddress || this.config.macAddress,
    };
  }

  private normalizeSnapshot(snapshotArg: IDunehdSnapshot): IDunehdSnapshot {
    const state = { ...(snapshotArg.state || {}) };
    const deviceInfo = {
      ...this.deviceInfoFromConfig(),
      ...snapshotArg.deviceInfo,
    };
    return {
      ...snapshotArg,
      deviceInfo,
      state,
      online: snapshotArg.online ?? Object.keys(state).length > 0,
      updatedAt: snapshotArg.updatedAt || new Date().toISOString(),
    };
  }

  private readStatusAttributes(statusArg: string): Array<Record<string, string>> {
    const result: Array<Record<string, string>> = [];
    const tagRegex = /<[^>]+>/g;
    let tagMatch: RegExpExecArray | null;
    while ((tagMatch = tagRegex.exec(statusArg))) {
      const attrs = this.readAttributes(tagMatch[0]);
      if (attrs.name !== undefined && attrs.value !== undefined) {
        result.push(attrs);
      }
    }
    const lineRegex = /name=(['"])(.*?)\1\s+value=(['"])(.*?)\3/g;
    let lineMatch: RegExpExecArray | null;
    while ((lineMatch = lineRegex.exec(statusArg))) {
      result.push({ name: this.decodeHtml(lineMatch[2]), value: this.decodeHtml(lineMatch[4]) });
    }
    return result;
  }

  private readAttributes(valueArg: string): Record<string, string> {
    const attrs: Record<string, string> = {};
    const attrRegex = /([a-zA-Z0-9_-]+)=(['"])(.*?)\2/g;
    let attrMatch: RegExpExecArray | null;
    while ((attrMatch = attrRegex.exec(valueArg))) {
      attrs[attrMatch[1]] = this.decodeHtml(attrMatch[3]);
    }
    return attrs;
  }

  private decodeHtml(valueArg: string): string {
    return valueArg
      .replace(/&#(\d+);/g, (_matchArg, codeArg: string) => String.fromCharCode(Number(codeArg)))
      .replace(/&apos;/g, "'")
      .replace(/&quot;/g, '"')
      .replace(/&gt;/g, '>')
      .replace(/&lt;/g, '<')
      .replace(/&amp;/g, '&');
  }

  private numberValue(valueArg: unknown, fallbackArg: number): number {
    if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
      return valueArg;
    }
    if (typeof valueArg === 'string' && valueArg.trim()) {
      const parsed = Number(valueArg);
      return Number.isFinite(parsed) ? parsed : fallbackArg;
    }
    return fallbackArg;
  }

  private isRecord(valueArg: unknown): valueArg is Record<string, unknown> {
    return typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
  }

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