import { SimpleLocalClient, SimpleLocalMapper, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import type { IItunesConfig, TItunesRawData } from './itunes.types.js';
import { itunesProfile } from './itunes.types.js';

export class ItunesClient extends SimpleLocalClient<IItunesConfig> {
  private snapshotCache?: ISimpleLocalSnapshot;

  constructor(configArg: IItunesConfig) {
    super(itunesProfile, configArg);
    this.localConfig = configArg;
  }

  private readonly localConfig: IItunesConfig;

  public async getSnapshot(forceRefreshArg = false): Promise<ISimpleLocalSnapshot> {
    if (!forceRefreshArg && this.snapshotCache) {
      return SimpleLocalMapper.clone(this.snapshotCache);
    }
    if (!this.canUseItunesApi()) {
      return super.getSnapshot(forceRefreshArg);
    }

    try {
      this.snapshotCache = await this.fetchSnapshot();
    } catch (errorArg) {
      this.snapshotCache = this.localOfflineSnapshot(errorMessage(errorArg));
    }
    return SimpleLocalMapper.clone(this.snapshotCache);
  }

  public async refresh(): Promise<IServiceCallResult> {
    if (!this.canUseItunesApi()) {
      return super.refresh();
    }
    this.snapshotCache = undefined;
    const snapshot = await this.getSnapshot(true);
    const success = snapshot.online && !snapshot.error;
    return { success, error: success ? undefined : snapshot.error, data: snapshot };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (!this.canUseItunesApi()) {
      return super.execute(requestArg);
    }

    try {
      const data = requestArg.data || {};
      const response = await this.executeItunesApiCommand(requestArg.service, data, requestArg.target?.deviceId);
      this.snapshotCache = undefined;
      return { success: true, data: response };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  public async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    const nowPlaying = await this.requestJson<Record<string, unknown>>('/now_playing');
    const airplayDevices = await this.requestJson<Record<string, unknown>>('/airplay_devices').catch((errorArg) => ({
      airplay_devices: [],
      error: errorMessage(errorArg),
    }));
    const rawData: TItunesRawData = this.toRawData(nowPlaying, airplayDevices);
    return SimpleLocalMapper.toSnapshot({ profile: itunesProfile, config: this.localConfig, rawData, online: true, source: 'http' });
  }

  private async executeItunesApiCommand(serviceArg: string, dataArg: Record<string, unknown>, targetDeviceIdArg?: string): Promise<unknown> {
    const service = serviceArg.replace(/^itunes\./, '');
    if (service === 'snapshot' || service === 'status') {
      return this.getSnapshot(true);
    }
    if (service === 'refresh' || service === 'reload') {
      return this.refresh();
    }

    const airplayDeviceId = stringValue(dataArg.deviceId) || stringValue(dataArg.airplayDeviceId) || targetDeviceIdArg;
    if (airplayDeviceId && (service === 'turn_on' || service === 'turn_off')) {
      return this.requestJson(`/airplay_devices/${encodeURIComponent(airplayDeviceId)}/${service === 'turn_on' ? 'on' : 'off'}`, { method: 'PUT' });
    }
    if (airplayDeviceId && (service === 'set_volume_level' || service === 'volume_set')) {
      return this.requestJson(`/airplay_devices/${encodeURIComponent(airplayDeviceId)}/volume`, { method: 'PUT', params: { level: this.volumePercent(dataArg) } });
    }

    if (service === 'media_play') {
      return this.requestJson('/play', { method: 'PUT' });
    }
    if (service === 'media_pause') {
      return this.requestJson('/pause', { method: 'PUT' });
    }
    if (service === 'media_next_track') {
      return this.requestJson('/next', { method: 'PUT' });
    }
    if (service === 'media_previous_track') {
      return this.requestJson('/previous', { method: 'PUT' });
    }
    if (service === 'media_stop' || service === 'turn_off') {
      return this.requestJson('/stop', { method: 'PUT' });
    }
    if (service === 'set_volume_level' || service === 'volume_set') {
      return this.requestJson('/volume', { method: 'PUT', params: { level: this.volumePercent(dataArg) } });
    }
    if (service === 'volume_mute') {
      return this.requestJson('/mute', { method: 'PUT', params: { muted: Boolean(dataArg.muted ?? dataArg.isVolumeMuted ?? true) } });
    }
    if (service === 'volume_up' || service === 'volume_down') {
      const snapshot = await this.getSnapshot();
      const currentVolume = numberValue((snapshot.rawData as Record<string, unknown> | undefined)?.nowPlaying && ((snapshot.rawData as Record<string, unknown>).nowPlaying as Record<string, unknown>).volume) ?? 0;
      const step = numberValue(dataArg.step) ?? 5;
      const level = Math.max(0, Math.min(100, currentVolume + (service === 'volume_up' ? step : -step)));
      return this.requestJson('/volume', { method: 'PUT', params: { level } });
    }
    if (service === 'set_shuffle') {
      return this.requestJson('/shuffle', { method: 'PUT', params: { mode: Boolean(dataArg.shuffle ?? dataArg.enabled) ? 'songs' : 'off' } });
    }
    if (service === 'play_media') {
      const mediaId = stringValue(dataArg.mediaId) || stringValue(dataArg.media_id) || stringValue(dataArg.playlist);
      if (!mediaId) {
        throw new Error('Apple iTunes play_media requires data.mediaId or data.playlist.');
      }
      return this.playPlaylist(mediaId);
    }
    throw new Error(`Apple iTunes service ${serviceArg} is not implemented by the itunes-api HTTP client.`);
  }

  private async playPlaylist(playlistIdOrNameArg: string): Promise<unknown> {
    const response = await this.requestJson<Record<string, unknown>>('/playlists');
    const playlists = Array.isArray(response.playlists) ? response.playlists as Array<Record<string, unknown>> : [];
    const playlist = playlists.find((playlistArg) => playlistArg.id === playlistIdOrNameArg || playlistArg.name === playlistIdOrNameArg);
    if (!playlist?.id) {
      throw new Error(`Apple iTunes playlist ${playlistIdOrNameArg} was not found.`);
    }
    return this.requestJson(`/playlists/${encodeURIComponent(String(playlist.id))}/play`, { method: 'PUT' });
  }

  private volumePercent(dataArg: Record<string, unknown>): number {
    const rawVolume = numberValue(dataArg.level) ?? numberValue(dataArg.volume) ?? numberValue(dataArg.volumeLevel) ?? 0;
    return Math.max(0, Math.min(100, rawVolume <= 1 ? Math.round(rawVolume * 100) : Math.round(rawVolume)));
  }

  private toRawData(nowPlayingArg: Record<string, unknown>, airplayDevicesArg: Record<string, unknown>): TItunesRawData {
    const airplayDevices = Array.isArray(airplayDevicesArg.airplay_devices) ? airplayDevicesArg.airplay_devices as Array<Record<string, unknown>> : [];
    const playerName = stringValue(nowPlayingArg.name) || this.localConfig.name || itunesProfile.defaultName || itunesProfile.displayName;
    const entities: ISimpleLocalEntitySnapshot[] = [
      {
        id: 'player',
        name: playerName,
        platform: 'media_player',
        state: stringValue(nowPlayingArg.player_state) || 'unknown',
        available: nowPlayingArg.player_state !== 'offline' && nowPlayingArg.player_state !== 'error',
        writable: true,
        attributes: cleanAttributes({
          volume: numberValue(nowPlayingArg.volume),
          muted: booleanValue(nowPlayingArg.muted),
          shuffle: nowPlayingArg.shuffle,
          title: nowPlayingArg.name,
          album: nowPlayingArg.album,
          artist: nowPlayingArg.artist,
          playlist: nowPlayingArg.playlist,
          contentId: nowPlayingArg.id,
          artworkUrl: `${this.baseUrl()}/artwork${nowPlayingArg.id ? `?id=${encodeURIComponent(String(nowPlayingArg.id))}` : ''}`,
        }),
      },
      ...airplayDevices.map((deviceArg) => ({
        id: `airplay_${SimpleLocalMapper.slug(deviceArg.id || deviceArg.name)}`,
        name: `${stringValue(deviceArg.name) || 'AirPlay'} AirTunes Speaker`.trim(),
        platform: 'media_player' as const,
        state: Boolean(deviceArg.selected) ? 'on' : 'off',
        available: true,
        writable: true,
        attributes: cleanAttributes({
          airplayDeviceId: deviceArg.id,
          kind: deviceArg.kind,
          active: booleanValue(deviceArg.active),
          selected: booleanValue(deviceArg.selected),
          volume: numberValue(deviceArg.sound_volume),
          supportsAudio: booleanValue(deviceArg.supports_audio),
          supportsVideo: booleanValue(deviceArg.supports_video),
        }),
      })),
    ];

    return {
      device: {
        id: this.localConfig.uniqueId || endpointId(this.localConfig),
        name: this.localConfig.name || itunesProfile.defaultName || itunesProfile.displayName,
        manufacturer: 'Apple',
        model: 'itunes-api HTTP bridge',
        host: endpointHost(this.localConfig),
        port: endpointPort(this.localConfig),
        protocol: endpointProtocol(this.localConfig),
        configurationUrl: this.baseUrl(),
      },
      entities,
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'http',
      rawData: { nowPlaying: nowPlayingArg, airplayDevices: airplayDevicesArg },
    };
  }

  private async requestJson<TValue = unknown>(pathArg: string, optionsArg: { method?: 'GET' | 'PUT' | 'POST' | 'DELETE'; params?: Record<string, unknown> } = {}): Promise<TValue> {
    const url = `${this.baseUrl()}${pathArg.startsWith('/') ? pathArg : `/${pathArg}`}`;
    const timeoutMs = this.localConfig.timeoutMs || 10000;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      const body = optionsArg.params ? new URLSearchParams(Object.entries(optionsArg.params).map(([keyArg, valueArg]) => [keyArg, String(valueArg)])) : undefined;
      const response = await globalThis.fetch(url, {
        method: optionsArg.method || 'GET',
        body,
        headers: body ? { 'content-type': 'application/x-www-form-urlencoded' } : undefined,
        signal: controller.signal,
      });
      const text = await response.text();
      if (!response.ok) {
        throw new Error(`Apple iTunes HTTP request ${pathArg} failed with ${response.status}: ${text.slice(0, 160)}`);
      }
      return parseJson<TValue>(text, pathArg);
    } finally {
      clearTimeout(timer);
    }
  }

  private baseUrl(): string {
    const parsed = endpointUrl(this.localConfig);
    const portPart = parsed.port && !((parsed.protocol === 'https' && parsed.port === 443) || (parsed.protocol === 'http' && parsed.port === 80)) ? `:${parsed.port}` : '';
    return `${parsed.protocol}://${parsed.host}${portPart}`;
  }

  private canUseItunesApi(): boolean {
    return Boolean(endpointHost(this.localConfig));
  }

  private localOfflineSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return SimpleLocalMapper.toSnapshot({ profile: itunesProfile, config: this.localConfig, online: false, source: 'runtime', error: errorArg });
  }
}

const endpointUrl = (configArg: IItunesConfig): { protocol: 'http' | 'https'; host: string; port?: number } => {
  const rawUrl = stringValue(configArg.url) || (stringValue(configArg.host)?.includes('://') ? stringValue(configArg.host) : undefined);
  if (rawUrl) {
    const url = new URL(rawUrl);
    return { protocol: url.protocol === 'https:' ? 'https' : 'http', host: url.hostname, port: url.port ? Number(url.port) : undefined };
  }
  return {
    protocol: (configArg.ssl || configArg.useTls || configArg.useSsl) ? 'https' : 'http',
    host: stringValue(configArg.host) || '',
    port: configArg.port || itunesProfile.defaultPort,
  };
};

const endpointHost = (configArg: IItunesConfig): string | undefined => endpointUrl(configArg).host || undefined;
const endpointPort = (configArg: IItunesConfig): number | undefined => endpointUrl(configArg).port;
const endpointProtocol = (configArg: IItunesConfig): 'http' | 'https' => endpointUrl(configArg).protocol;
const endpointId = (configArg: IItunesConfig): string => `${endpointHost(configArg) || 'itunes'}:${endpointPort(configArg) || itunesProfile.defaultPort || 8181}`;

const parseJson = <TValue>(textArg: string, pathArg: string): TValue => {
  try {
    return JSON.parse(textArg || '{}') as TValue;
  } catch (errorArg) {
    throw new Error(`Apple iTunes HTTP request ${pathArg} returned invalid JSON: ${errorMessage(errorArg)}`);
  }
};

const stringValue = (valueArg: unknown): string | undefined => 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() && Number.isFinite(Number(valueArg))) {
    return Number(valueArg);
  }
  return undefined;
};
const booleanValue = (valueArg: unknown): boolean | undefined => typeof valueArg === 'boolean' ? valueArg : undefined;
const cleanAttributes = (valueArg: Record<string, unknown>): Record<string, unknown> => Object.fromEntries(Object.entries(valueArg).filter(([, entryValueArg]) => entryValueArg !== undefined));
const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
