import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { MusicAssistantMapper } from './music_assistant.mapper.js';
import type { IMusicAssistantConfig, IMusicAssistantNativeRawData, IMusicAssistantPlayer, IMusicAssistantPlayerQueue, IMusicAssistantServerInfo } from './music_assistant.types.js';
import { musicAssistantAuthSchemaVersion, musicAssistantDefaultPort, musicAssistantDefaultTimeoutMs, musicAssistantProfile } from './music_assistant.types.js';

type TWebSocketEvent = { data?: unknown; error?: unknown; message?: unknown };
type TWebSocketHandler = (eventArg: TWebSocketEvent) => void;
type TWebSocketLike = {
  send(dataArg: string): void;
  close(): void;
  addEventListener?: (eventArg: 'open' | 'message' | 'error' | 'close', handlerArg: TWebSocketHandler) => void;
  removeEventListener?: (eventArg: 'open' | 'message' | 'error' | 'close', handlerArg: TWebSocketHandler) => void;
  onopen?: TWebSocketHandler | null;
  onmessage?: TWebSocketHandler | null;
  onerror?: TWebSocketHandler | null;
  onclose?: TWebSocketHandler | null;
};
type TWebSocketConstructor = new (urlArg: string) => TWebSocketLike;

interface IMusicAssistantCommandRequest {
  command: string;
  args?: Record<string, unknown>;
  mutates?: boolean;
}

interface IMusicAssistantCommandState extends IMusicAssistantCommandRequest {
  messageId: string;
}

export class MusicAssistantApiError extends Error {}
export class MusicAssistantApiConnectionError extends MusicAssistantApiError {}
export class MusicAssistantApiAuthError extends MusicAssistantApiError {}
export class MusicAssistantApiResponseError extends MusicAssistantApiError {}
export class MusicAssistantApiUnsupportedError extends MusicAssistantApiError {}

export class MusicAssistantClient extends SimpleLocalClient<IMusicAssistantConfig> {
  private liveSnapshot?: ISimpleLocalSnapshot;
  private cachedToken?: string;

  constructor(private readonly configArg: IMusicAssistantConfig) {
    super(musicAssistantProfile, configArg);
  }

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

    try {
      const rawData = await this.getRawData();
      this.liveSnapshot = MusicAssistantMapper.toSnapshot({ config: this.configArg, rawData, online: true, source: 'http' });
    } catch (errorArg) {
      this.liveSnapshot = MusicAssistantMapper.toSnapshot({ config: this.configArg, online: false, source: 'runtime', error: errorMessage(errorArg) });
    }
    return clone(this.liveSnapshot);
  }

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

  public async getRawData(): Promise<IMusicAssistantNativeRawData> {
    if (!this.hasEndpoint()) {
      throw new MusicAssistantApiConnectionError('Music Assistant local API requires config.host or config.url.');
    }
    const info = await this.getServerInfo();
    const results = await this.sendCommands([
      { command: 'players/all' },
      { command: 'player_queues/all' },
    ], info);
    return {
      info,
      players: Array.isArray(results[0]) ? results[0] as IMusicAssistantPlayer[] : [],
      queues: Array.isArray(results[1]) ? results[1] as IMusicAssistantPlayerQueue[] : [],
      fetchedAt: new Date().toISOString(),
    };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (!this.hasEndpoint()) {
      return super.execute(requestArg);
    }
    try {
      if (requestArg.service === 'refresh' || requestArg.service === 'reload') {
        return this.refresh();
      }
      const command = this.commandForRequest(requestArg);
      if (!command) {
        return { success: false, error: `Unsupported Music Assistant service: ${requestArg.domain}.${requestArg.service}` };
      }
      const [result] = await this.sendCommands([command]);
      if (command.mutates) {
        this.liveSnapshot = undefined;
      }
      return { success: true, data: result };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  public async getServerInfo(): Promise<IMusicAssistantServerInfo> {
    const response = await this.fetchHttp('info');
    const text = await response.text();
    if (!response.ok) {
      throw new MusicAssistantApiConnectionError(`Music Assistant /info failed with ${response.status}: ${text.slice(0, 160)}`);
    }
    try {
      return JSON.parse(text) as IMusicAssistantServerInfo;
    } catch (errorArg) {
      throw new MusicAssistantApiResponseError(`Music Assistant /info response was not JSON: ${errorMessage(errorArg)}`);
    }
  }

  private async sendCommands(commandsArg: IMusicAssistantCommandRequest[], knownInfoArg?: IMusicAssistantServerInfo): Promise<unknown[]> {
    const WebSocketCtor = (globalThis as unknown as { WebSocket?: TWebSocketConstructor }).WebSocket;
    if (!WebSocketCtor) {
      throw new MusicAssistantApiConnectionError('Global WebSocket is not available for Music Assistant local API commands.');
    }
    const timeoutMs = this.configArg.timeoutMs || musicAssistantDefaultTimeoutMs;
    const url = this.websocketUrl();

    return new Promise<unknown[]>((resolve, reject) => {
      const socket = new WebSocketCtor(url);
      const results: unknown[] = [];
      let activeCommand: IMusicAssistantCommandState | undefined;
      let pendingCommands = [...commandsArg];
      let settled = false;
      let serverInfo: IMusicAssistantServerInfo | undefined;
      const timer = setTimeout(() => fail(new MusicAssistantApiConnectionError(`Music Assistant WebSocket request to ${url} timed out.`)), timeoutMs);

      const cleanup = () => {
        clearTimeout(timer);
        this.removeSocketListener(socket, 'open');
        this.removeSocketListener(socket, 'message');
        this.removeSocketListener(socket, 'error');
        this.removeSocketListener(socket, 'close');
        try {
          socket.close();
        } catch {
          // Ignore close errors from mock or already-closed sockets.
        }
      };
      const finish = () => {
        if (settled) {
          return;
        }
        settled = true;
        cleanup();
        resolve(results);
      };
      const fail = (errorArg: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        cleanup();
        reject(errorArg);
      };
      const sendNext = () => {
        if (activeCommand) {
          return;
        }
        const command = pendingCommands.shift();
        if (!command) {
          finish();
          return;
        }
        const messageId = randomMessageId();
        activeCommand = { ...command, messageId };
        socket.send(JSON.stringify({ message_id: messageId, command: command.command, args: command.args || {} }));
      };
      const sendAuthThenNext = (infoArg: IMusicAssistantServerInfo) => {
        if (infoArg.schema_version < musicAssistantAuthSchemaVersion) {
          sendNext();
          return;
        }
        void this.authToken(infoArg).then((tokenArg) => {
          pendingCommands = [{ command: 'auth', args: { token: tokenArg } }, ...pendingCommands];
          sendNext();
        }, fail);
      };
      const onMessage = (eventArg: TWebSocketEvent) => {
        let message: Record<string, unknown>;
        try {
          message = JSON.parse(messageDataToString(eventArg.data)) as Record<string, unknown>;
        } catch (errorArg) {
          fail(new MusicAssistantApiResponseError(`Music Assistant WebSocket response was not JSON: ${errorMessage(errorArg)}`));
          return;
        }

        if (!serverInfo && isServerInfoMessage(message)) {
          serverInfo = knownInfoArg || message as unknown as IMusicAssistantServerInfo;
          sendAuthThenNext(serverInfo);
          return;
        }

        if (!activeCommand || message.message_id !== activeCommand.messageId) {
          return;
        }
        if (typeof message.error_code === 'number') {
          fail(new MusicAssistantApiResponseError(`Music Assistant command ${activeCommand.command} failed with ${message.error_code}: ${stringValue(message.details) || 'unknown error'}`));
          return;
        }
        if (!('result' in message)) {
          fail(new MusicAssistantApiResponseError(`Music Assistant command ${activeCommand.command} returned no result.`));
          return;
        }
        if (activeCommand.command === 'auth' && message.result !== true) {
          fail(new MusicAssistantApiAuthError('Music Assistant authentication failed.'));
          return;
        }
        if (activeCommand.command !== 'auth') {
          results.push(message.result);
        }
        activeCommand = undefined;
        sendNext();
      };

      this.addSocketListener(socket, 'message', onMessage);
      this.addSocketListener(socket, 'error', (eventArg) => fail(new MusicAssistantApiConnectionError(`Music Assistant WebSocket error: ${errorMessage(eventArg.error || eventArg.message || eventArg)}`)));
      this.addSocketListener(socket, 'close', () => {
        if (!settled) {
          fail(new MusicAssistantApiConnectionError('Music Assistant WebSocket closed before all command responses were received.'));
        }
      });
    });
  }

  private async authToken(infoArg: IMusicAssistantServerInfo): Promise<string> {
    if (infoArg.schema_version < musicAssistantAuthSchemaVersion) {
      return '';
    }
    if (this.configArg.token) {
      return this.configArg.token;
    }
    if (this.cachedToken) {
      return this.cachedToken;
    }
    if (!this.configArg.username || !this.configArg.password) {
      throw new MusicAssistantApiAuthError(`Music Assistant schema version ${infoArg.schema_version} requires a local auth token or username/password.`);
    }
    const response = await this.fetchHttp('auth/login', {
      method: 'POST',
      headers: { 'content-type': 'application/json', accept: 'application/json' },
      body: JSON.stringify({ username: this.configArg.username, password: this.configArg.password }),
    });
    const text = await response.text();
    if (response.status === 401) {
      throw new MusicAssistantApiAuthError('Music Assistant local username/password authentication failed.');
    }
    if (!response.ok) {
      throw new MusicAssistantApiConnectionError(`Music Assistant /auth/login failed with ${response.status}: ${text.slice(0, 160)}`);
    }
    const data = JSON.parse(text) as { access_token?: string };
    if (!data.access_token) {
      throw new MusicAssistantApiAuthError('Music Assistant /auth/login did not return an access_token.');
    }
    this.cachedToken = data.access_token;
    return data.access_token;
  }

  private commandForRequest(requestArg: IServiceCallRequest): IMusicAssistantCommandRequest | undefined {
    const data = requestArg.data || {};
    const playerId = playerIdFromRequest(requestArg);
    const queueId = stringValue(data.queue_id) || stringValue(data.queueId) || playerId;
    const mutates = true;

    switch (requestArg.service) {
      case 'search':
        return { command: 'music/search', args: cleanArgs({ search_query: data.name || data.search_query || data.search, media_types: ensureArray(data.media_type || data.media_types), limit: data.limit, library_only: data.library_only }) };
      case 'get_library':
        return { command: libraryCommand(data.media_type), args: cleanArgs({ favorite: data.favorite, search: data.search, limit: data.limit, offset: data.offset, order_by: data.order_by, album_types: data.album_type, album_artists_only: data.album_artists_only }) };
      case 'get_queue':
        return { command: 'player_queues/get_active_queue', args: requireArgs({ player_id: playerId }, 'Music Assistant get_queue requires data.player_id.') };
      case 'media_play':
        return { command: 'players/cmd/play', args: requireArgs({ player_id: playerId }, 'Music Assistant media_play requires data.player_id.'), mutates };
      case 'media_pause':
        return { command: 'players/cmd/pause', args: requireArgs({ player_id: playerId }, 'Music Assistant media_pause requires data.player_id.'), mutates };
      case 'media_stop':
        return { command: 'players/cmd/stop', args: requireArgs({ player_id: playerId }, 'Music Assistant media_stop requires data.player_id.'), mutates };
      case 'media_next_track':
      case 'next_track':
        return { command: 'players/cmd/next', args: requireArgs({ player_id: playerId }, 'Music Assistant next track requires data.player_id.'), mutates };
      case 'media_previous_track':
      case 'previous_track':
        return { command: 'players/cmd/previous', args: requireArgs({ player_id: playerId }, 'Music Assistant previous track requires data.player_id.'), mutates };
      case 'media_seek':
        return { command: 'players/cmd/seek', args: requireArgs({ player_id: playerId, position: numberValue(data.position) }, 'Music Assistant media_seek requires data.player_id and data.position.'), mutates };
      case 'turn_on':
        return { command: 'players/cmd/power', args: requireArgs({ player_id: playerId, powered: true }, 'Music Assistant turn_on requires data.player_id.'), mutates };
      case 'turn_off':
        return { command: 'players/cmd/power', args: requireArgs({ player_id: playerId, powered: false }, 'Music Assistant turn_off requires data.player_id.'), mutates };
      case 'volume_up':
        return { command: 'players/cmd/volume_up', args: requireArgs({ player_id: playerId }, 'Music Assistant volume_up requires data.player_id.'), mutates };
      case 'volume_down':
        return { command: 'players/cmd/volume_down', args: requireArgs({ player_id: playerId }, 'Music Assistant volume_down requires data.player_id.'), mutates };
      case 'volume_mute':
        return { command: 'players/cmd/volume_mute', args: requireArgs({ player_id: playerId, muted: booleanValue(data.is_volume_muted ?? data.muted ?? data.mute) }, 'Music Assistant volume_mute requires data.player_id and mute state.'), mutates };
      case 'set_volume_level':
        return { command: 'players/cmd/volume_set', args: requireArgs({ player_id: playerId, volume_level: volumeLevel(data.volume_level ?? data.volumeLevel) }, 'Music Assistant set_volume_level requires data.player_id and data.volume_level.'), mutates };
      case 'play_announcement':
        return { command: 'players/cmd/play_announcement', args: requireArgs(cleanArgs({ player_id: playerId, url: data.url, pre_announce: data.use_pre_announce, volume_level: data.announce_volume, pre_announce_url: data.pre_announce_url }), 'Music Assistant play_announcement requires data.player_id and data.url.'), mutates };
      case 'play_media':
        return { command: 'player_queues/play_media', args: requireArgs(cleanArgs({ queue_id: queueId, media: data.media_id || data.media, option: data.enqueue || data.media_enqueue, radio_mode: data.radio_mode, start_item: data.start_item }), 'Music Assistant play_media requires data.queue_id or data.player_id and data.media_id.'), mutates };
      case 'transfer_queue':
        return { command: 'player_queues/transfer', args: requireArgs(cleanArgs({ source_queue_id: data.source_queue_id || data.source_player, target_queue_id: data.target_queue_id || queueId, auto_play: data.auto_play }), 'Music Assistant transfer_queue requires source_queue_id/source_player and target_queue_id/player_id.'), mutates };
      case 'press':
        return { command: 'players/add_currently_playing_to_favorites', args: requireArgs({ player_id: playerId }, 'Music Assistant press requires data.player_id.'), mutates };
      case 'toggle':
        return { command: 'player_queues/play_pause', args: requireArgs({ queue_id: queueId }, 'Music Assistant toggle requires data.queue_id or data.player_id.'), mutates };
      case 'select_option':
      case 'set_value':
        return { command: 'players/cmd/set_option', args: requireArgs(cleanArgs({ player_id: playerId, option_key: data.option_key || data.key, option_value: data.option_value ?? data.value ?? data.option }), 'Music Assistant set option requires data.player_id, data.option_key, and data.value.'), mutates };
      default:
        return undefined;
    }
  }

  private async fetchHttp(pathArg: string, initArg: RequestInit = {}): Promise<Response> {
    const timeoutMs = this.configArg.timeoutMs || musicAssistantDefaultTimeoutMs;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      return await globalThis.fetch(this.httpUrl(pathArg), { ...initArg, signal: controller.signal, headers: { accept: 'application/json', ...headersRecord(initArg.headers) } });
    } finally {
      clearTimeout(timer);
    }
  }

  private httpUrl(pathArg: string): string {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new MusicAssistantApiConnectionError('Music Assistant local API requires config.host or config.url.');
    }
    return new URL(pathArg.replace(/^\/+/, ''), `${endpoint.origin}/`).toString();
  }

  private websocketUrl(): string {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new MusicAssistantApiConnectionError('Music Assistant local API requires config.host or config.url.');
    }
    return `${endpoint.protocol === 'https:' ? 'wss' : 'ws'}://${endpoint.hostname}${endpoint.port ? `:${endpoint.port}` : ''}/ws`;
  }

  private hasEndpoint(): boolean {
    return Boolean(this.endpoint());
  }

  private endpoint(): URL | undefined {
    const configured = stringValue(this.configArg.url) || stringValue(this.configArg.host);
    if (!configured) {
      return undefined;
    }
    const withProtocol = /^https?:\/\//i.test(configured) ? configured : `${this.configArg.useTls ? 'https' : 'http'}://${configured}`;
    try {
      const url = new URL(withProtocol);
      if (!url.port && this.configArg.port) {
        url.port = String(this.configArg.port);
      } else if (!url.port) {
        url.port = String(musicAssistantDefaultPort);
      }
      return url;
    } catch (errorArg) {
      throw new MusicAssistantApiConnectionError(`Invalid Music Assistant endpoint ${configured}: ${errorMessage(errorArg)}`);
    }
  }

  private addSocketListener(socketArg: TWebSocketLike, eventArg: 'open' | 'message' | 'error' | 'close', handlerArg: TWebSocketHandler): void {
    if (socketArg.addEventListener) {
      socketArg.addEventListener(eventArg, handlerArg);
      return;
    }
    socketArg[`on${eventArg}` as keyof TWebSocketLike] = handlerArg as never;
  }

  private removeSocketListener(socketArg: TWebSocketLike, eventArg: 'open' | 'message' | 'error' | 'close'): void {
    if (socketArg.removeEventListener) {
      socketArg.removeEventListener(eventArg, () => undefined);
    }
    socketArg[`on${eventArg}` as keyof TWebSocketLike] = null as never;
  }
}

const isServerInfoMessage = (messageArg: Record<string, unknown>): boolean => {
  return typeof messageArg.server_id === 'string' && typeof messageArg.schema_version === 'number';
};

const playerIdFromRequest = (requestArg: IServiceCallRequest): string | undefined => {
  const data = requestArg.data || {};
  const explicit = stringValue(data.player_id) || stringValue(data.playerId);
  if (explicit) {
    return explicit;
  }
  const entityId = requestArg.target.entityId;
  if (!entityId) {
    return undefined;
  }
  const slugPart = entityId.split('.').pop() || '';
  const marker = '_player_';
  return slugPart.includes(marker) ? slugPart.slice(slugPart.indexOf(marker) + marker.length) : undefined;
};

const libraryCommand = (mediaTypeArg: unknown): string => {
  const mediaType = stringValue(mediaTypeArg) || 'track';
  const plural: Record<string, string> = {
    artist: 'artists',
    album: 'albums',
    track: 'tracks',
    playlist: 'playlists',
    radio: 'radios',
    audiobook: 'audiobooks',
    podcast: 'podcasts',
  };
  const endpoint = plural[mediaType];
  if (!endpoint) {
    throw new MusicAssistantApiUnsupportedError(`Unsupported Music Assistant library media_type: ${mediaType}`);
  }
  return `music/${endpoint}/library_items`;
};

const requireArgs = (argsArg: Record<string, unknown>, messageArg: string): Record<string, unknown> => {
  const clean = cleanArgs(argsArg);
  for (const [key, value] of Object.entries(argsArg)) {
    if (value === undefined || value === null || value === '') {
      throw new MusicAssistantApiUnsupportedError(`${messageArg} Missing ${key}.`);
    }
  }
  return clean;
};

const cleanArgs = (argsArg: Record<string, unknown>): Record<string, unknown> => {
  return Object.fromEntries(Object.entries(argsArg).filter(([, valueArg]) => valueArg !== undefined && valueArg !== null && valueArg !== ''));
};

const ensureArray = (valueArg: unknown): unknown[] | undefined => {
  if (valueArg === undefined || valueArg === null || valueArg === '') {
    return undefined;
  }
  return Array.isArray(valueArg) ? valueArg : [valueArg];
};

const volumeLevel = (valueArg: unknown): number | undefined => {
  const value = numberValue(valueArg);
  if (value === undefined) {
    return undefined;
  }
  return value <= 1 ? Math.round(value * 100) : Math.round(value);
};

const booleanValue = (valueArg: unknown): boolean | undefined => {
  if (typeof valueArg === 'boolean') {
    return valueArg;
  }
  if (typeof valueArg === 'string') {
    if (valueArg.toLowerCase() === 'true') {
      return true;
    }
    if (valueArg.toLowerCase() === 'false') {
      return false;
    }
  }
  return undefined;
};

const headersRecord = (headersArg: HeadersInit | undefined): Record<string, string> => {
  if (!headersArg) {
    return {};
  }
  if (headersArg instanceof Headers) {
    return Object.fromEntries(headersArg.entries());
  }
  if (Array.isArray(headersArg)) {
    return Object.fromEntries(headersArg);
  }
  return headersArg;
};

const messageDataToString = (dataArg: unknown): string => {
  if (typeof dataArg === 'string') {
    return dataArg;
  }
  if (dataArg instanceof Buffer) {
    return dataArg.toString('utf8');
  }
  if (dataArg instanceof ArrayBuffer) {
    return Buffer.from(dataArg).toString('utf8');
  }
  if (ArrayBuffer.isView(dataArg)) {
    return Buffer.from(dataArg.buffer, dataArg.byteOffset, dataArg.byteLength).toString('utf8');
  }
  return String(dataArg ?? '');
};

const stringValue = (valueArg: unknown): string | undefined => {
  if (typeof valueArg === 'string' && valueArg.trim()) {
    return valueArg.trim();
  }
  if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
    return String(valueArg);
  }
  return 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;
};

const randomMessageId = (): string => `${Date.now().toString(16)}${Math.random().toString(16).slice(2)}`;

const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;

const errorMessage = (errorArg: unknown): string => {
  return errorArg instanceof Error ? errorArg.message : String(errorArg);
};
