import type {
  IVolumioConfig,
  IVolumioDeviceInfo,
  IVolumioPlaylistCollection,
  IVolumioQueue,
  IVolumioSnapshot,
  IVolumioState,
  IVolumioSystemInfo,
  IVolumioSystemVersion,
} from './volumio.types.js';
import { volumioDefaultPort } from './volumio.types.js';

const defaultTimeoutMs = 5000;

export class VolumioHttpError extends Error {
  constructor(public readonly status: number, messageArg: string) {
    super(messageArg);
    this.name = 'VolumioHttpError';
  }
}

export class VolumioClient {
  private currentSnapshot?: IVolumioSnapshot;

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

  public async getSnapshot(): Promise<IVolumioSnapshot> {
    if (!this.config.host && this.currentSnapshot) {
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (this.config.snapshot) {
      this.currentSnapshot = this.normalizeSnapshot(this.cloneSnapshot(this.config.snapshot));
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (this.hasManualSnapshotData()) {
      this.currentSnapshot = this.normalizeSnapshot(this.snapshotFromConfig(this.config.connected ?? true));
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (!this.config.host) {
      this.currentSnapshot = this.normalizeSnapshot(this.snapshotFromConfig(false));
      return this.cloneSnapshot(this.currentSnapshot);
    }

    this.currentSnapshot = this.normalizeSnapshot(await this.fetchSnapshot());
    return this.cloneSnapshot(this.currentSnapshot);
  }

  public async ping(): Promise<boolean> {
    const response = await this.requestText('ping');
    return response.trim().toLowerCase() === 'pong';
  }

  public async getSystemVersion(): Promise<IVolumioSystemVersion> {
    return this.getJson<IVolumioSystemVersion>('getSystemVersion');
  }

  public async getSystemInfo(): Promise<IVolumioSystemInfo> {
    return this.getJson<IVolumioSystemInfo>('getSystemInfo');
  }

  public async getState(): Promise<IVolumioState> {
    return this.getJson<IVolumioState>('getState');
  }

  public async getPlaylists(): Promise<IVolumioPlaylistCollection> {
    return this.getJson<IVolumioPlaylistCollection>('listplaylists');
  }

  public async getQueue(): Promise<IVolumioQueue> {
    return this.getJson<IVolumioQueue>('getQueue');
  }

  public async sendCommand(paramsArg: Record<string, unknown>): Promise<unknown> {
    const command = paramsArg.cmd;
    if (typeof command !== 'string' || !command) {
      throw new Error('Volumio command requires params.cmd.');
    }
    if (!this.config.host) {
      this.applyCommandToCachedSnapshot(paramsArg);
      return { response: `${command} Success` };
    }
    return this.getJson('commands', this.params(paramsArg));
  }

  public async play(): Promise<void> {
    await this.command('play');
  }

  public async pause(): Promise<void> {
    await this.command('pause');
  }

  public async toggle(): Promise<void> {
    await this.command('toggle');
  }

  public async stop(): Promise<void> {
    await this.command('stop');
  }

  public async next(): Promise<void> {
    await this.command('next');
  }

  public async previous(): Promise<void> {
    await this.command('prev');
  }

  public async setVolumeLevel(volumeLevelArg: number): Promise<void> {
    await this.command('volume', { volume: this.volumePercent(volumeLevelArg) });
  }

  public async volumeUp(): Promise<void> {
    await this.command('volume', { volume: 'plus' });
  }

  public async volumeDown(): Promise<void> {
    await this.command('volume', { volume: 'minus' });
  }

  public async setMuted(mutedArg: boolean): Promise<void> {
    await this.command('volume', { volume: mutedArg ? 'mute' : 'unmute' });
  }

  public async setShuffle(shuffleArg: boolean): Promise<void> {
    await this.command('random', { value: String(shuffleArg) });
  }

  public async repeatAll(repeatArg: boolean): Promise<void> {
    await this.command('repeat', { value: String(repeatArg) });
  }

  public async playPlaylist(playlistArg: string): Promise<void> {
    await this.command('playplaylist', { name: playlistArg });
  }

  public async clearPlaylist(): Promise<void> {
    await this.command('clearQueue');
  }

  public async seek(positionSecondsArg: number): Promise<void> {
    await this.command('seek', { position: Math.max(0, Math.round(positionSecondsArg)) });
  }

  public async replaceAndPlay(itemArg: unknown): Promise<void> {
    if (!this.config.host) {
      this.patchCachedState({ status: 'play' });
      return;
    }
    await this.postJson('replaceAndPlay', itemArg);
  }

  public canonicUrl(urlArg: string | undefined): string | undefined {
    if (!urlArg) {
      return undefined;
    }
    if (/^https?:\/\//i.test(urlArg)) {
      return urlArg;
    }
    const host = this.config.host || this.currentSnapshot?.deviceInfo.host;
    if (!host) {
      return urlArg;
    }
    return new URL(urlArg, `${this.protocol()}://${host}:${this.config.port || this.currentSnapshot?.deviceInfo.port || volumioDefaultPort}`).toString();
  }

  public async watchSocketIoPush(): Promise<never> {
    throw new Error('Volumio socket.io push is not implemented in this TypeScript port; use devices()/entities() polling and REST service calls.');
  }

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

  private async fetchSnapshot(): Promise<IVolumioSnapshot> {
    const state = await this.getState();
    const [systemInfo, systemVersion, playlists, queue] = await Promise.all([
      this.getSystemInfo().catch(() => this.config.systemInfo),
      this.getSystemVersion().catch(() => this.config.systemVersion),
      this.getPlaylists().catch(() => this.config.playlists),
      this.getQueue().catch(() => this.config.queue),
    ]);

    return {
      deviceInfo: this.deviceInfoFromPayload(systemInfo, systemVersion),
      systemInfo,
      systemVersion,
      state,
      playlists,
      queue,
      online: true,
      updatedAt: new Date().toISOString(),
    };
  }

  private snapshotFromConfig(onlineArg: boolean): IVolumioSnapshot {
    return {
      deviceInfo: this.deviceInfoFromPayload(this.config.systemInfo, this.config.systemVersion),
      systemInfo: this.config.systemInfo,
      systemVersion: this.config.systemVersion,
      state: this.config.state || { status: onlineArg ? 'stop' : 'offline' },
      playlists: this.config.playlists,
      queue: this.config.queue,
      online: onlineArg,
      updatedAt: new Date().toISOString(),
    };
  }

  private normalizeSnapshot(snapshotArg: IVolumioSnapshot): IVolumioSnapshot {
    const deviceInfo = {
      ...this.deviceInfoFromPayload(snapshotArg.systemInfo || this.config.systemInfo, snapshotArg.systemVersion || this.config.systemVersion),
      ...snapshotArg.deviceInfo,
    };
    const state = { ...(snapshotArg.state || {}) };
    const albumart = typeof state.albumart === 'string' ? this.canonicUrl(state.albumart) : undefined;
    if (albumart) {
      state.albumart = albumart;
    }
    return {
      ...snapshotArg,
      deviceInfo,
      state,
      online: Boolean(snapshotArg.online),
      updatedAt: snapshotArg.updatedAt || new Date().toISOString(),
    };
  }

  private deviceInfoFromPayload(systemInfoArg: IVolumioSystemInfo | undefined, systemVersionArg: IVolumioSystemVersion | undefined): IVolumioDeviceInfo {
    const version = stringValue(systemVersionArg?.systemversion)
      || stringValue(systemVersionArg?.systemVersion)
      || stringValue(systemVersionArg?.volumioVersion)
      || stringValue(systemVersionArg?.version)
      || stringValue(systemInfoArg?.systemversion)
      || stringValue(systemInfoArg?.systemVersion)
      || stringValue(systemInfoArg?.volumioVersion)
      || stringValue(systemInfoArg?.version);
    const hardware = stringValue(systemVersionArg?.hardware) || stringValue(systemInfoArg?.hardware);
    return {
      ...this.config.deviceInfo,
      id: this.config.deviceInfo?.id || this.config.uniqueId || stringValue(systemInfoArg?.id) || stringValue(systemInfoArg?.uuid),
      uuid: this.config.deviceInfo?.uuid || this.config.uniqueId || stringValue(systemInfoArg?.id) || stringValue(systemInfoArg?.uuid),
      name: this.config.deviceInfo?.name || this.config.name || stringValue(systemInfoArg?.name) || this.config.host || 'Volumio',
      host: this.config.deviceInfo?.host || this.config.host,
      port: this.config.deviceInfo?.port || this.config.port || volumioDefaultPort,
      manufacturer: this.config.deviceInfo?.manufacturer || 'Volumio',
      model: this.config.deviceInfo?.model || hardware,
      hardware: this.config.deviceInfo?.hardware || hardware,
      systemVersion: this.config.deviceInfo?.systemVersion || version,
      softwareVersion: this.config.deviceInfo?.softwareVersion || version,
    };
  }

  private async command(commandArg: string, paramsArg: Record<string, unknown> = {}): Promise<void> {
    await this.sendCommand({ cmd: commandArg, ...paramsArg });
  }

  private async getJson<T = unknown>(pathArg: string, paramsArg?: URLSearchParams): Promise<T> {
    return this.requestJson<T>('GET', pathArg, paramsArg);
  }

  private async postJson<T = unknown>(pathArg: string, bodyArg: unknown): Promise<T> {
    return this.requestJson<T>('POST', pathArg, undefined, bodyArg);
  }

  private async requestJson<T>(methodArg: 'GET' | 'POST', pathArg: string, paramsArg?: URLSearchParams, bodyArg?: unknown): Promise<T> {
    const response = await this.request(pathArg, {
      method: methodArg,
      headers: methodArg === 'POST' ? { 'content-type': 'application/json' } : undefined,
      body: methodArg === 'POST' ? JSON.stringify(bodyArg) : undefined,
    }, paramsArg);
    const text = await response.text();
    if (!response.ok) {
      throw new VolumioHttpError(response.status, `Volumio request ${pathArg} failed with HTTP ${response.status}: ${text}`);
    }
    if (!text.trim()) {
      return {} as T;
    }
    try {
      return JSON.parse(text) as T;
    } catch {
      return {} as T;
    }
  }

  private async requestText(pathArg: string, paramsArg?: URLSearchParams): Promise<string> {
    const response = await this.request(pathArg, { method: 'GET' }, paramsArg);
    const text = await response.text();
    if (!response.ok) {
      throw new VolumioHttpError(response.status, `Volumio request ${pathArg} failed with HTTP ${response.status}: ${text}`);
    }
    return text;
  }

  private async request(pathArg: string, initArg: RequestInit, paramsArg?: URLSearchParams): Promise<Response> {
    if (!this.config.host) {
      throw new Error('Volumio host is required when snapshot or manual config data is not provided.');
    }
    const abortController = new AbortController();
    const timeout = globalThis.setTimeout(() => abortController.abort(), this.config.timeoutMs || defaultTimeoutMs);
    try {
      return await globalThis.fetch(this.url(pathArg, paramsArg), {
        ...initArg,
        signal: abortController.signal,
      });
    } finally {
      globalThis.clearTimeout(timeout);
    }
  }

  private url(pathArg: string, paramsArg?: URLSearchParams): string {
    const normalizedPath = pathArg.replace(/^\/+/, '');
    const query = paramsArg && Array.from(paramsArg.keys()).length ? `?${paramsArg.toString()}` : '';
    return `${this.protocol()}://${this.config.host}:${this.config.port || volumioDefaultPort}/api/v1/${normalizedPath}${query}`;
  }

  private protocol(): 'http' | 'https' {
    return this.config.ssl ? 'https' : 'http';
  }

  private params(recordArg: Record<string, unknown>): URLSearchParams {
    const params = new URLSearchParams();
    for (const [key, value] of Object.entries(recordArg)) {
      if (value !== undefined && value !== null) {
        params.set(key, String(value));
      }
    }
    return params;
  }

  private volumePercent(valueArg: number): number {
    const percent = valueArg <= 1 ? valueArg * 100 : valueArg;
    return Math.max(0, Math.min(100, Math.round(percent)));
  }

  private hasManualSnapshotData(): boolean {
    return Boolean(this.config.state || this.config.systemInfo || this.config.systemVersion || this.config.playlists || this.config.queue || this.config.deviceInfo);
  }

  private applyCommandToCachedSnapshot(paramsArg: Record<string, unknown>): void {
    const command = paramsArg.cmd;
    if (typeof command !== 'string') {
      return;
    }
    if (!this.currentSnapshot) {
      this.currentSnapshot = this.normalizeSnapshot(this.snapshotFromConfig(this.config.connected ?? true));
    }
    if (command === 'play' || command === 'toggle') {
      this.patchCachedState({ status: 'play' });
    } else if (command === 'pause') {
      this.patchCachedState({ status: 'pause' });
    } else if (command === 'stop') {
      this.patchCachedState({ status: 'stop', seek: 0 });
    } else if (command === 'volume') {
      this.applyVolumeCommand(paramsArg.volume);
    } else if (command === 'random') {
      this.patchCachedState({ random: paramsArg.value === true || paramsArg.value === 'true' });
    } else if (command === 'repeat') {
      this.patchCachedState({ repeat: paramsArg.value === true || paramsArg.value === 'true' });
    } else if (command === 'seek') {
      this.patchCachedState({ seek: Number(paramsArg.position || 0) * 1000 });
    }
  }

  private applyVolumeCommand(valueArg: unknown): void {
    const state = this.currentSnapshot?.state || {};
    const current = numberValue(state.volume) || 0;
    if (valueArg === 'mute') {
      this.patchCachedState({ mute: true });
      return;
    }
    if (valueArg === 'unmute') {
      this.patchCachedState({ mute: false });
      return;
    }
    if (valueArg === 'plus') {
      this.patchCachedState({ volume: Math.min(100, current + 5), mute: false });
      return;
    }
    if (valueArg === 'minus') {
      this.patchCachedState({ volume: Math.max(0, current - 5), mute: false });
      return;
    }
    const volume = numberValue(valueArg);
    if (typeof volume === 'number') {
      this.patchCachedState({ volume: this.volumePercent(volume), mute: false });
    }
  }

  private patchCachedState(stateArg: Partial<IVolumioState>): void {
    if (!this.currentSnapshot) {
      this.currentSnapshot = this.normalizeSnapshot(this.snapshotFromConfig(this.config.connected ?? true));
    }
    this.currentSnapshot.state = { ...this.currentSnapshot.state, ...stateArg };
    this.currentSnapshot.updatedAt = new Date().toISOString();
  }

  private cloneSnapshot(snapshotArg: IVolumioSnapshot): IVolumioSnapshot {
    return JSON.parse(JSON.stringify(snapshotArg)) as IVolumioSnapshot;
  }
}

const stringValue = (valueArg: unknown): string | undefined => {
  return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
};

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