import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { SimpleLocalMapper, type IIntegrationEntity, type IServiceCallRequest, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot, type ISimpleLocalSnapshotOptions, type TSimpleLocalRawData } from '../../core/index.js';
import type { ILametricConfig, ILametricDeviceData, ILametricSnapshot } from './lametric.types.js';
import { lametricDefaultHttpsPort, lametricDefaultName, lametricDomain, lametricProfile } from './lametric.types.js';

export type TLametricCommandAction =
  | 'app_next'
  | 'app_previous'
  | 'dismiss_current'
  | 'dismiss_all'
  | 'message'
  | 'chart'
  | 'set_brightness'
  | 'set_brightness_mode'
  | 'set_volume'
  | 'set_bluetooth';

export interface ILametricCommandRequest {
  action: TLametricCommandAction;
  service?: string;
  target?: IServiceCallRequest['target'];
  data?: Record<string, unknown>;
  value?: number | string | boolean;
}

const buttonEntities: Array<{ key: TLametricCommandAction; name: string }> = [
  { key: 'app_next', name: 'Next app' },
  { key: 'app_previous', name: 'Previous app' },
  { key: 'dismiss_current', name: 'Dismiss current notification' },
  { key: 'dismiss_all', name: 'Dismiss all notifications' },
];

export class LametricMapper {
  public static toSnapshot(optionsArg: Omit<ISimpleLocalSnapshotOptions<ILametricConfig>, 'profile'>): ILametricSnapshot {
    const rawSnapshot = this.snapshotValue(optionsArg.rawData) || (optionsArg.rawData === undefined ? optionsArg.config.snapshot : undefined);
    if (rawSnapshot) {
      return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: lametricProfile }) as ILametricSnapshot;
    }

    const rawData = this.rawDeviceData(optionsArg.config, optionsArg.rawData);
    const endpoint = parseEndpoint(optionsArg.config.url || optionsArg.config.host, optionsArg.config.port, optionsArg.config.protocol, optionsArg.config.useTls);
    const serialNumber = stringValue(rawData.serial_number ?? rawData.serialNumber) || optionsArg.config.serialNumber || optionsArg.config.uniqueId;
    const wifi = objectValue(rawData.wifi);
    const wifiMac = normalizedMac(stringValue(wifi?.mac ?? wifi?.address) || optionsArg.config.macAddress);
    const deviceName = stringValue(rawData.name) || optionsArg.config.name || lametricDefaultName;
    const device: ILametricSnapshot['device'] = {
      id: stringValue(rawData.id) || serialNumber || optionsArg.config.deviceId || optionsArg.config.uniqueId || endpoint?.host || lametricDomain,
      name: deviceName,
      manufacturer: 'LaMetric',
      model: stringValue(rawData.model) || optionsArg.config.model || 'LaMetric TIME',
      serialNumber,
      host: endpoint?.host,
      port: endpoint?.port,
      protocol: endpoint?.protocol,
      configurationUrl: endpoint ? `${endpoint.protocol}://${endpoint.host}${endpoint.port ? `:${endpoint.port}` : ''}/` : undefined,
      attributes: cleanAttributes({
        apiVersion: stringValue(rawData.api_version),
        osVersion: stringValue(rawData.os_version ?? rawData.osVersion),
        mode: stringValue(rawData.mode),
        macAddress: wifiMac,
        wifiIp: stringValue(wifi?.ip ?? wifi?.ipv4),
      }),
    };
    const entities = this.entitiesFromRaw(rawData, Boolean(optionsArg.online ?? optionsArg.config.online ?? rawData.name ?? rawData.serial_number ?? rawData.serialNumber));
    const online = optionsArg.online ?? optionsArg.config.online ?? (entities.some((entityArg) => entityArg.state !== undefined && entityArg.state !== null) || Boolean(rawData.name || rawData.serial_number || rawData.serialNumber));

    return {
      device,
      entities,
      online,
      updatedAt: new Date().toISOString(),
      source: optionsArg.source || (Object.keys(rawData).length ? 'http' : 'runtime'),
      rawData: Object.keys(rawData).length ? rawData : undefined,
      error: optionsArg.error,
      capabilities: {
        localRead: true,
        localControl: Boolean(endpoint?.host),
        liveControlRequiresExecutor: !endpoint?.host,
      },
    };
  }

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

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

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

  public static commandForService(snapshotArg: ISimpleLocalSnapshot, requestArg: IServiceCallRequest): ILametricCommandRequest | undefined {
    const service = `${requestArg.domain}.${requestArg.service}`;
    if (requestArg.domain === lametricDomain) {
      if (requestArg.service === 'message') {
        return { action: 'message', service, target: requestArg.target, data: requestArg.data };
      }
      if (requestArg.service === 'chart') {
        return { action: 'chart', service, target: requestArg.target, data: requestArg.data };
      }
      if (requestArg.service === 'app_next' || requestArg.service === 'next_app') {
        return { action: 'app_next', service, target: requestArg.target, data: requestArg.data };
      }
      if (requestArg.service === 'app_previous' || requestArg.service === 'previous_app') {
        return { action: 'app_previous', service, target: requestArg.target, data: requestArg.data };
      }
      if (requestArg.service === 'dismiss_current') {
        return { action: 'dismiss_current', service, target: requestArg.target, data: requestArg.data };
      }
      if (requestArg.service === 'dismiss_all') {
        return { action: 'dismiss_all', service, target: requestArg.target, data: requestArg.data };
      }
      if (requestArg.service === 'set_brightness') {
        return { action: 'set_brightness', service, target: requestArg.target, data: requestArg.data, value: numberValue(requestArg.data?.brightness ?? requestArg.data?.value) };
      }
      if (requestArg.service === 'set_volume') {
        return { action: 'set_volume', service, target: requestArg.target, data: requestArg.data, value: numberValue(requestArg.data?.volume ?? requestArg.data?.value) };
      }
      if (requestArg.service === 'set_brightness_mode') {
        return { action: 'set_brightness_mode', service, target: requestArg.target, data: requestArg.data, value: stringValue(requestArg.data?.brightness_mode ?? requestArg.data?.option) };
      }
      if (requestArg.service === 'set_bluetooth') {
        return { action: 'set_bluetooth', service, target: requestArg.target, data: requestArg.data, value: booleanValue(requestArg.data?.active ?? requestArg.data?.enabled) };
      }
    }

    const entity = this.targetEntity(snapshotArg, requestArg);
    const key = stringValue(entity?.attributes?.key);
    if (!key) {
      return undefined;
    }

    if (requestArg.domain === 'button' && requestArg.service === 'press') {
      return buttonEntities.some((entityArg) => entityArg.key === key) ? { action: key as TLametricCommandAction, service, target: requestArg.target, data: requestArg.data } : undefined;
    }
    if (requestArg.domain === 'number' && ['set_value', 'set_native_value'].includes(requestArg.service)) {
      const value = numberValue(requestArg.data?.value ?? requestArg.data?.native_value);
      if (key === 'brightness') {
        return { action: 'set_brightness', service, target: requestArg.target, data: requestArg.data, value };
      }
      if (key === 'volume') {
        return { action: 'set_volume', service, target: requestArg.target, data: requestArg.data, value };
      }
    }
    if (requestArg.domain === 'select' && requestArg.service === 'select_option' && key === 'brightness_mode') {
      return { action: 'set_brightness_mode', service, target: requestArg.target, data: requestArg.data, value: stringValue(requestArg.data?.option) };
    }
    if (requestArg.domain === 'switch' && ['turn_on', 'turn_off', 'toggle'].includes(requestArg.service) && key === 'bluetooth') {
      const current = booleanValue(entity?.state) ?? false;
      const active = requestArg.service === 'toggle' ? !current : requestArg.service === 'turn_on';
      return { action: 'set_bluetooth', service, target: requestArg.target, data: requestArg.data, value: active };
    }
    return undefined;
  }

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

  public static clone<TValue>(valueArg: TValue): TValue {
    return SimpleLocalMapper.clone(valueArg);
  }

  private static entitiesFromRaw(rawDataArg: ILametricDeviceData, onlineArg: boolean): ISimpleLocalEntitySnapshot[] {
    const entities: ISimpleLocalEntitySnapshot[] = [];
    const wifi = objectValue(rawDataArg.wifi);
    const display = objectValue(rawDataArg.display);
    const audio = objectValue(rawDataArg.audio);
    const bluetooth = objectValue(rawDataArg.bluetooth);
    const rssi = numberValue(wifi?.rssi ?? wifi?.strength ?? wifi?.signal_strength);
    const brightness = numberValue(display?.brightness);
    const brightnessMode = stringValue(display?.brightness_mode);
    const volume = numberValue(audio?.volume);
    const bluetoothAvailable = bluetooth ? booleanValue(bluetooth.available) ?? true : false;
    const updateVersion = stringValue((objectValue(rawDataArg.update_available) || objectValue(rawDataArg.update))?.version);
    const osVersion = stringValue(rawDataArg.os_version ?? rawDataArg.osVersion);

    if (rssi !== undefined) {
      entities.push({ id: 'rssi', name: 'Wi-Fi signal', platform: 'sensor', state: rssi, unit: '%', stateClass: 'measurement', available: onlineArg, attributes: { key: 'rssi', entityCategory: 'diagnostic' } });
    }
    for (const button of buttonEntities) {
      entities.push({ id: button.key, name: button.name, platform: 'button', state: 'available', writable: true, available: onlineArg, attributes: { key: button.key, entityCategory: 'config' } });
    }
    if (brightness !== undefined) {
      entities.push({
        id: 'brightness',
        name: 'Brightness',
        platform: 'number',
        state: brightness,
        unit: '%',
        writable: true,
        available: onlineArg,
        attributes: cleanAttributes({ key: 'brightness', entityCategory: 'config', nativeMinValue: rangeValue(display?.brightness_limit, 'min') ?? rangeValue(display?.brightness_range, 'min') ?? 0, nativeMaxValue: rangeValue(display?.brightness_limit, 'max') ?? rangeValue(display?.brightness_range, 'max') ?? 100, nativeStep: 1 }),
      });
    }
    if (volume !== undefined && (audio === undefined || (booleanValue(audio.available) ?? true))) {
      entities.push({
        id: 'volume',
        name: 'Volume',
        platform: 'number',
        state: volume,
        unit: '%',
        writable: true,
        available: onlineArg,
        attributes: cleanAttributes({ key: 'volume', entityCategory: 'config', nativeMinValue: rangeValue(audio?.volume_limit, 'min') ?? rangeValue(audio?.volume_range, 'min') ?? 0, nativeMaxValue: rangeValue(audio?.volume_limit, 'max') ?? rangeValue(audio?.volume_range, 'max') ?? 100, nativeStep: 1 }),
      });
    }
    if (brightnessMode) {
      entities.push({ id: 'brightness_mode', name: 'Brightness mode', platform: 'select', state: brightnessMode, writable: true, available: onlineArg, attributes: { key: 'brightness_mode', entityCategory: 'config', options: ['auto', 'manual'] } });
    }
    if (bluetooth) {
      entities.push({ id: 'bluetooth', name: 'Bluetooth', platform: 'switch', state: booleanValue(bluetooth.active) ?? false, writable: true, available: onlineArg && bluetoothAvailable, attributes: cleanAttributes({ key: 'bluetooth', entityCategory: 'config', address: stringValue(bluetooth.address ?? bluetooth.mac), discoverable: booleanValue(bluetooth.discoverable), pairable: booleanValue(bluetooth.pairable) }) });
    }
    if (osVersion && (updateVersion || compareVersions(osVersion, '2.3.0') >= 0)) {
      entities.push({ id: 'update', name: 'Firmware', platform: 'update', state: updateVersion || osVersion, available: onlineArg, attributes: cleanAttributes({ key: 'update', deviceClass: 'firmware', installedVersion: osVersion, latestVersion: updateVersion || osVersion }) });
    }
    return entities;
  }

  private static rawDeviceData(configArg: ILametricConfig, rawDataArg?: unknown): ILametricDeviceData {
    const raw = objectValue(rawDataArg ?? configArg.rawData) as ILametricDeviceData | undefined;
    if (raw) {
      return this.clone(raw);
    }
    return cleanAttributes({
      id: configArg.deviceId || configArg.uniqueId,
      name: configArg.name,
      serial_number: configArg.serialNumber,
      model: configArg.model,
      wifi: configArg.macAddress ? { mac: configArg.macAddress } : undefined,
    }) as ILametricDeviceData;
  }

  private static targetEntity(snapshotArg: ISimpleLocalSnapshot, requestArg: IServiceCallRequest): IIntegrationEntity | undefined {
    const entities = this.toEntities(snapshotArg);
    return entities.find((entityArg) => entityArg.id === requestArg.target.entityId || entityArg.uniqueId === requestArg.target.entityId)
      || (requestArg.target.deviceId ? entities.find((entityArg) => entityArg.deviceId === requestArg.target.deviceId) : undefined);
  }

  private static snapshotValue(valueArg: unknown): ISimpleLocalSnapshot | undefined {
    return valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'entities' in valueArg ? valueArg as ISimpleLocalSnapshot : undefined;
  }
}

function parseEndpoint(valueArg: string | undefined, portArg?: number, protocolArg?: string, useTlsArg?: boolean): { protocol: 'http' | 'https'; host: string; port: number } | undefined {
  if (!valueArg) {
    return undefined;
  }
  const defaultProtocol = protocolArg === 'http' || useTlsArg === false ? 'http' : 'https';
  try {
    const url = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg) ? valueArg : `${defaultProtocol}://${valueArg}`);
    const protocol = url.protocol === 'http:' ? 'http' : 'https';
    return { protocol, host: url.hostname, port: portArg || (url.port ? Number(url.port) : protocol === 'http' ? 8080 : lametricDefaultHttpsPort) };
  } catch {
    return { protocol: defaultProtocol, host: valueArg, port: portArg || (defaultProtocol === 'http' ? 8080 : lametricDefaultHttpsPort) };
  }
}

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

function stringValue(valueArg: unknown): string | undefined {
  return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : typeof valueArg === 'number' && Number.isFinite(valueArg) ? String(valueArg) : undefined;
}

function 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;
}

function booleanValue(valueArg: unknown): boolean | undefined {
  if (typeof valueArg === 'boolean') {
    return valueArg;
  }
  if (typeof valueArg === 'string') {
    if (['true', 'on', '1'].includes(valueArg.toLowerCase())) {
      return true;
    }
    if (['false', 'off', '0'].includes(valueArg.toLowerCase())) {
      return false;
    }
  }
  return undefined;
}

function rangeValue(valueArg: unknown, keyArg: 'min' | 'max'): number | undefined {
  return numberValue(objectValue(valueArg)?.[keyArg]);
}

function normalizedMac(valueArg?: string): string | undefined {
  return valueArg?.trim().toLowerCase();
}

function compareVersions(leftArg: string, rightArg: string): number {
  const left = leftArg.split(/[.-]/).map((partArg) => Number(partArg) || 0);
  const right = rightArg.split(/[.-]/).map((partArg) => Number(partArg) || 0);
  const length = Math.max(left.length, right.length);
  for (let index = 0; index < length; index += 1) {
    const delta = (left[index] || 0) - (right[index] || 0);
    if (delta !== 0) {
      return delta;
    }
  }
  return 0;
}

function cleanAttributes<TValue extends Record<string, unknown>>(attributesArg: TValue): TValue {
  return Object.fromEntries(Object.entries(attributesArg).filter(([, valueArg]) => valueArg !== undefined)) as TValue;
}
