import type { IRokuApp, IRokuConfig, IRokuDeviceInfo, IRokuSnapshot, TRokuKeypress } from './roku.types.js';

export class RokuClient {
  constructor(private readonly config: IRokuConfig) {}

  public async getSnapshot(): Promise<IRokuSnapshot> {
    const [deviceInfo, apps, activeApp] = await Promise.all([
      this.getDeviceInfo(),
      this.getApps(),
      this.getActiveApp(),
    ]);
    return { deviceInfo, apps, activeApp };
  }

  public async getDeviceInfo(): Promise<IRokuDeviceInfo> {
    if (this.config.deviceInfo) {
      return this.config.deviceInfo;
    }
    const xml = await this.requestText('/query/device-info');
    return {
      serialNumber: this.readXmlTag(xml, 'serial-number'),
      deviceId: this.readXmlTag(xml, 'device-id'),
      vendorName: this.readXmlTag(xml, 'vendor-name'),
      modelName: this.readXmlTag(xml, 'model-name'),
      modelNumber: this.readXmlTag(xml, 'model-number'),
      friendlyDeviceName: this.readXmlTag(xml, 'friendly-device-name'),
      userDeviceName: this.readXmlTag(xml, 'user-device-name'),
      deviceType: this.readXmlTag(xml, 'device-type'),
      softwareVersion: this.readXmlTag(xml, 'software-version'),
      powerMode: this.readXmlTag(xml, 'power-mode'),
      supportsSuspend: this.readXmlTag(xml, 'supports-suspend') === 'true',
    };
  }

  public async getApps(): Promise<IRokuApp[]> {
    if (this.config.apps) {
      return this.config.apps;
    }
    const xml = await this.requestText('/query/apps');
    return this.readApps(xml);
  }

  public async getActiveApp(): Promise<IRokuApp | undefined> {
    if (this.config.activeApp) {
      return this.config.activeApp;
    }
    const xml = await this.requestText('/query/active-app');
    return this.readApps(xml)[0];
  }

  public async keypress(keyArg: TRokuKeypress | string): Promise<void> {
    await this.requestText(`/keypress/${encodeURIComponent(this.toEcpKey(keyArg))}`, 'POST');
  }

  public async launch(appIdArg: string, paramsArg: Record<string, string | number | boolean> = {}): Promise<void> {
    const searchParams = new URLSearchParams();
    for (const [key, value] of Object.entries(paramsArg)) {
      searchParams.set(key, String(value));
    }
    const suffix = searchParams.size ? `?${searchParams.toString()}` : '';
    await this.requestText(`/launch/${encodeURIComponent(appIdArg)}${suffix}`, 'POST');
  }

  public async playUrl(urlArg: string, mediaTypeArg: 'video' | 'audio' = 'video', titleArg?: string): Promise<void> {
    const params: Record<string, string> = {
      u: urlArg,
      t: mediaTypeArg === 'audio' ? 'a' : 'v',
    };
    if (titleArg) {
      params[mediaTypeArg === 'audio' ? 'songName' : 'videoName'] = titleArg;
    }
    await this.launch(this.config.playMediaAppId || '15985', params);
  }

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

  private async requestText(pathArg: string, methodArg: 'GET' | 'POST' = 'GET'): Promise<string> {
    if (!this.config.host) {
      throw new Error('Roku host is required when fixture data is not provided.');
    }
    const response = await globalThis.fetch(`${this.baseUrl()}${pathArg}`, { method: methodArg });
    const text = await response.text();
    if (!response.ok) {
      throw new Error(`Roku request ${pathArg} failed with HTTP ${response.status}: ${text}`);
    }
    return text;
  }

  private readApps(xmlArg: string): IRokuApp[] {
    const apps: IRokuApp[] = [];
    const regex = /<app\s+([^>]*)>([\s\S]*?)<\/app>/g;
    let match: RegExpExecArray | null;
    while ((match = regex.exec(xmlArg))) {
      const attrs = this.readAttributes(match[1]);
      const id = attrs.id;
      if (!id) {
        continue;
      }
      apps.push({
        id,
        name: this.unescapeXml(match[2].trim()),
        type: attrs.type,
        version: attrs.version,
      });
    }
    return apps;
  }

  private readAttributes(valueArg: string): Record<string, string> {
    const attrs: Record<string, string> = {};
    const regex = /([a-zA-Z0-9_-]+)="([^"]*)"/g;
    let match: RegExpExecArray | null;
    while ((match = regex.exec(valueArg))) {
      attrs[match[1]] = this.unescapeXml(match[2]);
    }
    return attrs;
  }

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

  private toEcpKey(keyArg: string): string {
    const normalized = keyArg.replace(/[_\s-]+/g, '').toLowerCase();
    const keys: Record<string, TRokuKeypress> = {
      home: 'Home',
      rev: 'Rev',
      reverse: 'Rev',
      fwd: 'Fwd',
      forward: 'Fwd',
      play: 'Play',
      pause: 'Play',
      select: 'Select',
      left: 'Left',
      right: 'Right',
      down: 'Down',
      up: 'Up',
      back: 'Back',
      instantreplay: 'InstantReplay',
      info: 'Info',
      backspace: 'Backspace',
      search: 'Search',
      enter: 'Enter',
      poweron: 'PowerOn',
      poweroff: 'PowerOff',
      power: 'Power',
      volumeup: 'VolumeUp',
      volumedown: 'VolumeDown',
      volumemute: 'VolumeMute',
      mute: 'VolumeMute',
    };
    return keys[normalized] || keyArg;
  }

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

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