import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { SimpleLocalMapper, type IIntegrationEntity, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot, type ISimpleLocalSnapshotOptions } from '../../core/index.js';
import type { IMusicAssistantConfig, IMusicAssistantNativeRawData, IMusicAssistantPlayer, IMusicAssistantPlayerQueue, TMusicAssistantRawData } from './music_assistant.types.js';
import { musicAssistantDefaultName, musicAssistantDefaultPort, musicAssistantProfile } from './music_assistant.types.js';

export class MusicAssistantMapper {
  public static toSnapshot(optionsArg: Omit<ISimpleLocalSnapshotOptions<IMusicAssistantConfig>, 'profile'>): ISimpleLocalSnapshot {
    const rawData = optionsArg.rawData ?? optionsArg.config.rawData;
    if (this.isNativeRawData(rawData)) {
      return this.toSnapshotFromNativeRaw(optionsArg.config, rawData, optionsArg.online ?? true, optionsArg.source || 'http', optionsArg.error);
    }
    return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: musicAssistantProfile });
  }

  public static toSnapshotFromRaw(configArg: IMusicAssistantConfig, rawDataArg: TMusicAssistantRawData): ISimpleLocalSnapshot {
    if (this.isNativeRawData(rawDataArg)) {
      return this.toSnapshotFromNativeRaw(configArg, rawDataArg, true, 'manual');
    }
    return SimpleLocalMapper.toSnapshot({ profile: musicAssistantProfile, config: configArg, rawData: rawDataArg, online: true, source: 'manual' });
  }

  public static toSnapshotFromNativeRaw(configArg: IMusicAssistantConfig, rawDataArg: IMusicAssistantNativeRawData, onlineArg = true, sourceArg: ISimpleLocalSnapshot['source'] = 'http', errorArg?: string): ISimpleLocalSnapshot {
    const endpoint = endpointFromConfig(configArg, rawDataArg.info.base_url);
    const serverId = configArg.uniqueId || configArg.deviceId || rawDataArg.info.server_id || endpoint.host || musicAssistantProfile.domain;
    const name = configArg.name || stringValue(rawDataArg.info.name) || musicAssistantDefaultName;
    const queuesById = new Map(rawDataArg.queues.map((queueArg) => [queueArg.queue_id, queueArg]));
    const canControl = hasCommandTransport(configArg);
    const entities: ISimpleLocalEntitySnapshot[] = [
      {
        id: 'server_status',
        uniqueId: `${musicAssistantProfile.domain}_${this.slug(serverId)}_server_status`,
        name: 'Server status',
        platform: 'sensor',
        state: stringValue(rawDataArg.info.status) || 'running',
        available: onlineArg,
        writable: false,
        attributes: cleanAttributes({
          serverVersion: rawDataArg.info.server_version,
          schemaVersion: rawDataArg.info.schema_version,
          minSupportedSchemaVersion: rawDataArg.info.min_supported_schema_version,
        }),
      },
    ];

    for (const player of rawDataArg.players.filter((playerArg) => playerArg.expose_to_ha !== false)) {
      const playerId = player.player_id;
      const playerName = stringValue(player.name) || stringValue(player.display_name) || playerId;
      const activeQueue = stringValue(player.active_source) ? queuesById.get(String(player.active_source)) : queuesById.get(playerId);
      const currentMedia = objectValue(player.current_media);
      const currentItem = objectValue(activeQueue?.current_item);
      const mediaItem = objectValue(currentItem?.media_item);
      entities.push({
        id: `player_${this.slug(playerId)}`,
        uniqueId: `${musicAssistantProfile.domain}_${this.slug(playerId)}`,
        name: playerName,
        platform: 'media_player',
        state: player.powered === false ? 'off' : stringValue(player.playback_state) || stringValue(player.state) || stringValue(activeQueue?.state) || 'idle',
        available: onlineArg && (player.available ?? true),
        writable: canControl,
        attributes: cleanAttributes({
          playerId,
          provider: player.provider,
          playerType: player.type,
          powered: player.powered,
          volumeLevel: numberValue(player.volume_level),
          volumeMuted: player.volume_muted,
          activeSource: player.active_source,
          groupMembers: player.group_members || player.group_childs,
          queueId: activeQueue?.queue_id,
          queueItems: activeQueue?.items,
          shuffleEnabled: activeQueue?.shuffle_enabled,
          repeatMode: activeQueue?.repeat_mode,
          currentIndex: activeQueue?.current_index,
          mediaContentId: stringValue(currentMedia?.uri) || stringValue(currentItem?.uri),
          mediaTitle: stringValue(currentMedia?.title) || stringValue(mediaItem?.name) || stringValue(currentItem?.name),
          mediaArtist: stringValue(currentMedia?.artist),
          mediaAlbum: stringValue(currentMedia?.album),
          mediaDuration: numberValue(currentMedia?.duration) ?? numberValue(currentItem?.duration),
          mediaPosition: numberValue(currentMedia?.elapsed_time) ?? numberValue(activeQueue?.elapsed_time),
        }),
      });

      for (const option of Array.isArray(player.options) ? player.options : []) {
        const optionEntity = optionEntityFromPlayer(playerId, playerName, option, canControl, onlineArg);
        if (optionEntity) {
          entities.push(optionEntity);
        }
      }
    }

    return {
      device: {
        id: serverId,
        name,
        manufacturer: 'Music Assistant',
        model: `Music Assistant Server ${rawDataArg.info.server_version || ''}`.trim(),
        host: endpoint.host,
        port: endpoint.port,
        protocol: endpoint.protocol,
        configurationUrl: rawDataArg.info.base_url || (endpoint.host ? `${endpoint.protocol}://${endpoint.host}${endpoint.port && endpoint.port !== musicAssistantDefaultPort ? `:${endpoint.port}` : ''}` : undefined),
        attributes: cleanAttributes({
          homeassistantAddon: rawDataArg.info.homeassistant_addon,
          onboardDone: rawDataArg.info.onboard_done,
          baseUrl: rawDataArg.info.base_url,
        }),
      },
      entities,
      online: onlineArg,
      updatedAt: rawDataArg.fetchedAt || new Date().toISOString(),
      source: sourceArg,
      rawData: rawDataArg,
      error: errorArg,
      capabilities: {
        localRead: true,
        localControl: canControl,
        protocol: 'music-assistant-websocket',
      },
    };
  }

  public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] {
    return SimpleLocalMapper.toDevices(musicAssistantProfile, snapshotArg);
  }

  public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] {
    return SimpleLocalMapper.toEntities(musicAssistantProfile, snapshotArg);
  }

  public static slug(valueArg: unknown): string {
    return SimpleLocalMapper.slug(valueArg);
  }

  public static isNativeRawData(valueArg: unknown): valueArg is IMusicAssistantNativeRawData {
    return Boolean(valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) && objectValue((valueArg as Record<string, unknown>).info) && Array.isArray((valueArg as Record<string, unknown>).players) && Array.isArray((valueArg as Record<string, unknown>).queues));
  }
}

const optionEntityFromPlayer = (playerIdArg: string, playerNameArg: string, optionArg: Record<string, unknown>, canControlArg: boolean, onlineArg: boolean): ISimpleLocalEntitySnapshot | undefined => {
  const key = stringValue(optionArg.key);
  if (!key) {
    return undefined;
  }
  const optionName = stringValue(optionArg.name) || key;
  const optionType = stringValue(optionArg.type);
  const readOnly = optionArg.read_only === true;
  const writable = canControlArg && !readOnly;
  const optionEntries = Array.isArray(optionArg.options) ? optionArg.options.filter((entryArg): entryArg is Record<string, unknown> => Boolean(entryArg && typeof entryArg === 'object')) : [];
  const platform = optionType === 'boolean' ? 'switch' : optionType === 'integer' || optionType === 'float' ? 'number' : optionEntries.length ? 'select' : 'text';
  return {
    id: `player_${slug(playerIdArg)}_option_${slug(key)}`,
    uniqueId: `${musicAssistantProfile.domain}_${slug(playerIdArg)}_${slug(key)}`,
    name: `${playerNameArg} ${optionName}`,
    platform,
    state: optionArg.value,
    available: onlineArg,
    writable,
    attributes: cleanAttributes({
      playerId: playerIdArg,
      optionKey: key,
      readOnly,
      min: numberValue(optionArg.min_value),
      max: numberValue(optionArg.max_value),
      step: numberValue(optionArg.step),
      options: optionEntries.map((entryArg) => stringValue(entryArg.value) || stringValue(entryArg.key)).filter((valueArg): valueArg is string => Boolean(valueArg)),
    }),
  };
};

const endpointFromConfig = (configArg: IMusicAssistantConfig, baseUrlArg?: string): { host?: string; port: number; protocol: 'http' | 'https' } => {
  const configured = stringValue(configArg.url) || stringValue(baseUrlArg) || stringValue(configArg.host);
  if (configured && /^https?:\/\//i.test(configured)) {
    try {
      const url = new URL(configured);
      return { host: url.hostname, port: configArg.port || (url.port ? Number(url.port) : musicAssistantDefaultPort), protocol: url.protocol === 'https:' ? 'https' : 'http' };
    } catch {
      return { host: configured, port: configArg.port || musicAssistantDefaultPort, protocol: 'http' };
    }
  }
  return { host: configured, port: configArg.port || musicAssistantDefaultPort, protocol: configArg.useTls ? 'https' : 'http' };
};

const hasCommandTransport = (configArg: IMusicAssistantConfig): boolean => {
  return Boolean(configArg.host || configArg.url || configArg.client?.execute || configArg.commandExecutor);
};

const objectValue = (valueArg: unknown): Record<string, unknown> | undefined => {
  return valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Record<string, unknown> : undefined;
};

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 cleanAttributes = (attributesArg: Record<string, unknown | undefined>): Record<string, unknown> | undefined => {
  const clean = Object.fromEntries(Object.entries(attributesArg).filter(([, valueArg]) => valueArg !== undefined));
  return Object.keys(clean).length ? clean : undefined;
};

const slug = (valueArg: unknown): string => SimpleLocalMapper.slug(valueArg);
