import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { SimpleLocalMapper, type IIntegrationEntity, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot, type ISimpleLocalSnapshotOptions, type TSimpleLocalRawData } from '../../core/index.js';
import type { INadConfig, INadRawApiData, TNadRawData } from './nad.types.js';
import { nadDefaultName, nadDefaultTcpPort, nadDefaultTelnetPort, nadProfile } from './nad.types.js';

export class NadMapper {
  public static toSnapshot(optionsArg: Omit<ISimpleLocalSnapshotOptions<INadConfig>, 'profile'>): ISimpleLocalSnapshot {
    return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: nadProfile, rawData: this.normalizeRawData(optionsArg.rawData, optionsArg.config) });
  }

  public static toSnapshotFromRaw(configArg: INadConfig, rawDataArg: TSimpleLocalRawData | TNadRawData): ISimpleLocalSnapshot {
    return this.toSnapshot({ config: configArg, rawData: rawDataArg, online: true, source: 'manual' });
  }

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

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

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

  public static offlineSnapshot(configArg: INadConfig, errorArg: string): ISimpleLocalSnapshot {
    return this.toSnapshot({ config: configArg, online: false, source: 'runtime', error: errorArg });
  }

  public static normalizeRawData(rawDataArg: unknown, configArg: INadConfig): unknown {
    const parsedRawData = parseMaybeJson(rawDataArg);
    const rawObject = objectValue(parsedRawData);
    if (!rawObject || Array.isArray(rawObject.entities) || rawObject.device) {
      return parsedRawData;
    }

    const status = objectValue(rawObject.status);
    if (!status) {
      return parsedRawData;
    }

    const mode = stringValue(status.mode) || stringValue(rawObject.mode) || normalizedType(configArg.type);
    const endpoint = endpointFromConfig(configArg, mode === 'tcp' ? nadDefaultTcpPort : nadDefaultTelnetPort);
    const powerValue = status.power;
    const power = typeof powerValue === 'boolean' ? powerValue : stringValue(powerValue)?.toLowerCase() === 'on';
    const volume = numberValue(status.volume);
    const muted = boolValue(status.muted);
    const source = status.source as string | number | undefined;
    const sourceName = source === undefined ? undefined : configArg.sources?.[String(source)] || String(source);
    const availableSources = stringArray(rawObject.availableSources);
    const minVolume = configArg.minVolume ?? -92;
    const maxVolume = configArg.maxVolume ?? -20;
    const isTcp = mode === 'tcp';

    const entity: ISimpleLocalEntitySnapshot = {
      id: 'media_player',
      name: configArg.name || nadDefaultName,
      platform: 'media_player',
      state: power ? 'on' : 'off',
      writable: true,
      attributes: cleanAttributes({
        mode,
        power,
        volume,
        volumeLevel: volume === undefined ? undefined : isTcp ? tcpVolumeLevel(volume, minVolume, maxVolume) : dbVolumeLevel(volume, minVolume, maxVolume),
        isVolumeMuted: muted,
        source: sourceName,
        sourceRaw: source,
        sourceList: availableSources.length ? availableSources : Object.values(configArg.sources || {}).sort(),
        model: stringValue(status.model),
        version: stringValue(status.version),
      }),
    };

    const normalizedStatus = {
      ...status,
      mode: mode === 'tcp' ? 'tcp' : 'telnet',
    } as INadRawApiData['status'];

    return {
      device: {
        id: configArg.uniqueId || configArg.deviceId || (endpoint ? `${endpoint.host}:${endpoint.port}` : undefined) || nadDefaultName,
        name: configArg.name || nadDefaultName,
        manufacturer: 'NAD',
        model: stringValue(status.model) || (isTcp ? 'NAD D7050' : 'NAD Receiver'),
        host: endpoint?.host,
        port: endpoint?.port,
        protocol: isTcp ? 'tcp' : 'telnet',
      },
      entities: [entity],
      status: normalizedStatus,
      availableSources,
      fetchedAt: stringValue(rawObject.fetchedAt) || new Date().toISOString(),
    } satisfies INadRawApiData & { device: Record<string, unknown>; entities: ISimpleLocalEntitySnapshot[] };
  }
}

const normalizedType = (valueArg: unknown): 'tcp' | 'telnet' | 'rs232' => {
  const value = stringValue(valueArg)?.toLowerCase();
  if (value === 'tcp') {
    return 'tcp';
  }
  if (value === 'rs232') {
    return 'rs232';
  }
  return 'telnet';
};

const endpointFromConfig = (configArg: INadConfig, defaultPortArg: number): { host: string; port: number } | undefined => {
  const rawUrl = stringValue(configArg.url) || stringValue(configArg.host);
  if (!rawUrl) {
    return undefined;
  }
  try {
    const url = new URL(rawUrl.includes('://') ? rawUrl : `tcp://${rawUrl}`);
    return { host: url.hostname, port: configArg.port || numberValue(url.port) || defaultPortArg };
  } catch {
    return { host: rawUrl, port: configArg.port || defaultPortArg };
  }
};

const tcpVolumeLevel = (volumeArg: number, minVolumeArg: number, maxVolumeArg: number): number => {
  const min = (minVolumeArg + 90) * 2;
  const max = (maxVolumeArg + 90) * 2;
  if (max === min) {
    return 0;
  }
  if (volumeArg < min) {
    return 0;
  }
  if (volumeArg > max) {
    return 1;
  }
  return (volumeArg - min) / (max - min);
};

const dbVolumeLevel = (volumeArg: number, minVolumeArg: number, maxVolumeArg: number): number => {
  if (maxVolumeArg === minVolumeArg) {
    return 0;
  }
  return Math.max(0, Math.min(1, Math.abs(minVolumeArg - volumeArg) / Math.abs(minVolumeArg - maxVolumeArg)));
};

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

const parseMaybeJson = (valueArg: unknown): unknown => {
  if (typeof valueArg !== 'string') {
    return valueArg;
  }
  try {
    return JSON.parse(valueArg) as unknown;
  } catch {
    return valueArg;
  }
};

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.trim());
    return Number.isFinite(parsed) ? parsed : undefined;
  }
  return undefined;
};

const boolValue = (valueArg: unknown): boolean | undefined => {
  if (typeof valueArg === 'boolean') {
    return valueArg;
  }
  const value = stringValue(valueArg)?.toLowerCase();
  if (value === 'true' || value === 'on' || value === '1') {
    return true;
  }
  if (value === 'false' || value === 'off' || value === '0') {
    return false;
  }
  return undefined;
};

const stringArray = (valueArg: unknown): string[] => {
  return Array.isArray(valueArg) ? valueArg.map((itemArg) => stringValue(itemArg)).filter((itemArg): itemArg is string => Boolean(itemArg)) : [];
};

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