import type { ISonosConfig, ISonosPositionInfo, ISonosSnapshot, ISonosSpeakerInfo, ISonosTransportInfo } from './sonos.types.js';

type TSonosServiceType = 'AVTransport' | 'RenderingControl' | 'DeviceProperties';

export class SonosClient {
  constructor(private readonly config: ISonosConfig) {}

  public async getSnapshot(): Promise<ISonosSnapshot> {
    const speakerInfo = await this.getSpeakerInfo();
    const [transportInfo, positionInfo, volume, muted] = await Promise.all([
      this.getTransportInfo(),
      this.getPositionInfo(),
      this.getVolume(),
      this.getMute(),
    ]);
    return { speakerInfo, transportInfo, positionInfo, volume, muted };
  }

  public async getSpeakerInfo(): Promise<ISonosSpeakerInfo> {
    if (this.config.speakerInfo) {
      return this.config.speakerInfo;
    }
    const xml = await this.fetchText('/xml/device_description.xml');
    const uid = this.stripUuid(this.readXmlTag(xml, 'UDN') || this.config.host || 'unknown');
    const zoneName = this.readXmlTag(xml, 'roomName') || this.readXmlTag(xml, 'friendlyName') || `Sonos ${uid}`;
    return {
      uid,
      zoneName,
      modelName: this.readXmlTag(xml, 'modelName'),
      modelNumber: this.readXmlTag(xml, 'modelNumber'),
      serialNumber: this.readXmlTag(xml, 'serialNum'),
      softwareVersion: this.readXmlTag(xml, 'softwareVersion'),
      manufacturer: this.readXmlTag(xml, 'manufacturer') || 'Sonos',
    };
  }

  public async getTransportInfo(): Promise<ISonosTransportInfo> {
    if (this.config.transportInfo) {
      return this.config.transportInfo;
    }
    const result = await this.soap('AVTransport', 'GetTransportInfo', [
      ['InstanceID', 0],
    ]);
    return {
      currentTransportState: result.CurrentTransportState,
      currentTransportStatus: result.CurrentTransportStatus,
      currentSpeed: result.CurrentSpeed,
    };
  }

  public async getPositionInfo(): Promise<ISonosPositionInfo> {
    if (this.config.positionInfo) {
      return this.config.positionInfo;
    }
    const result = await this.soap('AVTransport', 'GetPositionInfo', [
      ['InstanceID', 0],
    ]);
    const metadata = result.TrackMetaData || '';
    return {
      track: Number(result.Track || 0),
      trackDuration: result.TrackDuration,
      trackMetaData: metadata,
      trackUri: result.TrackURI,
      relativeTime: result.RelTime,
      title: this.readXmlTag(metadata, 'dc:title') || this.readXmlTag(metadata, 'title'),
      artist: this.readXmlTag(metadata, 'dc:creator') || this.readXmlTag(metadata, 'creator'),
      album: this.readXmlTag(metadata, 'upnp:album') || this.readXmlTag(metadata, 'album'),
      albumArtUri: this.readXmlTag(metadata, 'upnp:albumArtURI') || this.readXmlTag(metadata, 'albumArtURI'),
    };
  }

  public async getVolume(): Promise<number | undefined> {
    if (typeof this.config.volume === 'number') {
      return this.config.volume;
    }
    const result = await this.soap('RenderingControl', 'GetVolume', [
      ['InstanceID', 0],
      ['Channel', 'Master'],
    ]);
    return Number(result.CurrentVolume);
  }

  public async getMute(): Promise<boolean | undefined> {
    if (typeof this.config.muted === 'boolean') {
      return this.config.muted;
    }
    const result = await this.soap('RenderingControl', 'GetMute', [
      ['InstanceID', 0],
      ['Channel', 'Master'],
    ]);
    return result.CurrentMute === '1' || result.CurrentMute?.toLowerCase() === 'true';
  }

  public async play(): Promise<void> {
    await this.soap('AVTransport', 'Play', [
      ['InstanceID', 0],
      ['Speed', 1],
    ]);
  }

  public async pause(): Promise<void> {
    await this.soap('AVTransport', 'Pause', [
      ['InstanceID', 0],
      ['Speed', 1],
    ]);
  }

  public async stop(): Promise<void> {
    await this.soap('AVTransport', 'Stop', [
      ['InstanceID', 0],
      ['Speed', 1],
    ]);
  }

  public async next(): Promise<void> {
    await this.soap('AVTransport', 'Next', [
      ['InstanceID', 0],
      ['Speed', 1],
    ]);
  }

  public async previous(): Promise<void> {
    await this.soap('AVTransport', 'Previous', [
      ['InstanceID', 0],
      ['Speed', 1],
    ]);
  }

  public async setVolume(volumeArg: number): Promise<void> {
    const volume = Math.max(0, Math.min(100, Math.round(volumeArg)));
    await this.soap('RenderingControl', 'SetVolume', [
      ['InstanceID', 0],
      ['Channel', 'Master'],
      ['DesiredVolume', volume],
    ]);
  }

  public async setMute(mutedArg: boolean): Promise<void> {
    await this.soap('RenderingControl', 'SetMute', [
      ['InstanceID', 0],
      ['Channel', 'Master'],
      ['DesiredMute', mutedArg ? 1 : 0],
    ]);
  }

  public async playUri(uriArg: string, titleArg?: string): Promise<void> {
    await this.soap('AVTransport', 'SetAVTransportURI', [
      ['InstanceID', 0],
      ['CurrentURI', uriArg],
      ['CurrentURIMetaData', titleArg ? this.createMinimalRadioMetadata(titleArg) : ''],
    ]);
    await this.play();
  }

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

  private async soap(serviceTypeArg: TSonosServiceType, actionArg: string, argsArg: Array<[string, string | number | boolean]>): Promise<Record<string, string>> {
    const path = this.servicePath(serviceTypeArg);
    const body = this.createSoapEnvelope(serviceTypeArg, actionArg, argsArg);
    const response = await globalThis.fetch(`${this.baseUrl()}${path}`, {
      method: 'POST',
      headers: {
        'content-type': 'text/xml; charset="utf-8"',
        soapaction: `urn:schemas-upnp-org:service:${serviceTypeArg}:1#${actionArg}`,
      },
      body,
    });
    const text = await response.text();
    if (!response.ok) {
      throw new Error(`Sonos SOAP ${serviceTypeArg}.${actionArg} failed with HTTP ${response.status}: ${text}`);
    }
    return this.unwrapSoapResponse(text);
  }

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

  private createSoapEnvelope(serviceTypeArg: TSonosServiceType, actionArg: string, argsArg: Array<[string, string | number | boolean]>): string {
    const args = argsArg.map(([nameArg, valueArg]) => `<${nameArg}>${this.escapeXml(String(valueArg))}</${nameArg}>`).join('');
    return `<?xml version="1.0"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:${actionArg} xmlns:u="urn:schemas-upnp-org:service:${serviceTypeArg}:1">${args}</u:${actionArg}></s:Body></s:Envelope>`;
  }

  private createMinimalRadioMetadata(titleArg: string): string {
    return `<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="R:0/0/0" parentID="R:0/0" restricted="true"><dc:title>${this.escapeXml(titleArg)}</dc:title><upnp:class>object.item.audioItem.audioBroadcast</upnp:class><desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">SA_RINCON65031_</desc></item></DIDL-Lite>`;
  }

  private unwrapSoapResponse(xmlArg: string): Record<string, string> {
    const response: Record<string, string> = {};
    const regex = /<([A-Za-z0-9_:]+)>([^<>]*)<\/\1>/g;
    let match: RegExpExecArray | null;
    while ((match = regex.exec(xmlArg))) {
      response[this.localName(match[1])] = this.unescapeXml(match[2]);
    }
    return response;
  }

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

  private servicePath(serviceTypeArg: TSonosServiceType): string {
    if (serviceTypeArg === 'RenderingControl') {
      return '/MediaRenderer/RenderingControl/Control';
    }
    if (serviceTypeArg === 'DeviceProperties') {
      return '/DeviceProperties/Control';
    }
    return '/MediaRenderer/AVTransport/Control';
  }

  private stripUuid(valueArg: string): string {
    return valueArg.replace(/^uuid:/i, '');
  }

  private localName(valueArg: string): string {
    return valueArg.includes(':') ? valueArg.split(':').pop() || valueArg : valueArg;
  }

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

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

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