import * as plugins from '../../plugins.js';
import type { IIntegrationEntity, IServiceCallRequest, TEntityPlatform } from '../../core/types.js';
import type { IEsphomeAreaInfo, IEsphomeClientCommand, IEsphomeConfig, IEsphomeDeviceInfo, IEsphomeEntityDescriptor, IEsphomeEntityState, IEsphomeManualEntry, IEsphomeSnapshot } from './esphome.types.js';
import { esphomeDefaultNativeApiPort } from './esphome.types.js';

export class EsphomeMapper {
  public static toSnapshot(configArg: IEsphomeConfig, connectedArg?: boolean, eventsArg: IEsphomeSnapshot['events'] = []): IEsphomeSnapshot {
    const source = configArg.snapshot;
    const manualEntry = configArg.manualEntries?.[0];
    const deviceInfo = this.deviceInfoFromConfig(configArg, source?.deviceInfo, manualEntry);
    return {
      host: configArg.host || source?.host || manualEntry?.host || deviceInfo.host,
      port: configArg.port || source?.port || manualEntry?.port || deviceInfo.port || esphomeDefaultNativeApiPort,
      connected: connectedArg ?? source?.connected ?? false,
      deviceInfo,
      apiVersion: source?.apiVersion,
      entities: [...(source?.entities || []), ...(configArg.entities || [])],
      states: [
        ...this.normalizeStates(source?.states),
        ...this.normalizeStates(configArg.states),
      ],
      services: [...(source?.services || []), ...(configArg.services || [])],
      events: [...(source?.events || []), ...eventsArg],
    };
  }

  public static toDevices(snapshotArg: IEsphomeSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = new Date().toISOString();
    const deviceInfo = snapshotArg.deviceInfo;
    const devices = new Map<string, plugins.shxInterfaces.data.IDeviceDefinition>();
    const mainDeviceId = this.mainDeviceId(snapshotArg);
    devices.set(mainDeviceId, {
      id: mainDeviceId,
      integrationDomain: 'esphome',
      name: this.deviceName(deviceInfo, snapshotArg.host),
      room: this.areaName(deviceInfo.area) || deviceInfo.suggestedArea,
      protocol: 'esphome',
      manufacturer: this.manufacturer(deviceInfo),
      model: this.model(deviceInfo),
      online: snapshotArg.connected || Boolean(snapshotArg.entities.length || snapshotArg.states.length),
      features: [
        { id: 'connection', capability: 'sensor', name: 'Connection', readable: true, writable: false },
        { id: 'entity_count', capability: 'sensor', name: 'Entity count', readable: true, writable: false },
      ],
      state: [
        { featureId: 'connection', value: snapshotArg.connected ? 'connected' : 'configured', updatedAt },
        { featureId: 'entity_count', value: snapshotArg.entities.length, updatedAt },
      ],
      metadata: {
        macAddress: this.normalizeMac(deviceInfo.macAddress),
        bluetoothMacAddress: this.normalizeMac(deviceInfo.bluetoothMacAddress),
        host: snapshotArg.host,
        port: snapshotArg.port,
        esphomeVersion: deviceInfo.esphomeVersion,
        compilationTime: deviceInfo.compilationTime,
        projectName: deviceInfo.projectName,
        projectVersion: deviceInfo.projectVersion,
        apiEncryptionSupported: deviceInfo.apiEncryptionSupported,
        usesPassword: deviceInfo.usesPassword,
        webserverPort: deviceInfo.webserverPort,
      },
    });

    for (const subDevice of deviceInfo.devices || []) {
      const subDeviceId = this.subDeviceIdValue(subDevice);
      if (subDeviceId === undefined || subDeviceId === null || String(subDeviceId) === '0') {
        continue;
      }
      devices.set(this.subDeviceId(snapshotArg, subDeviceId), {
        id: this.subDeviceId(snapshotArg, subDeviceId),
        integrationDomain: 'esphome',
        name: subDevice.name || this.deviceName(deviceInfo, snapshotArg.host),
        room: this.areaForSubDevice(deviceInfo, subDevice.areaId ?? subDevice.area_id),
        protocol: 'esphome',
        manufacturer: this.manufacturer(deviceInfo),
        model: this.model(deviceInfo),
        online: snapshotArg.connected || Boolean(snapshotArg.entities.length || snapshotArg.states.length),
        features: [],
        state: [],
        metadata: {
          parentDeviceId: mainDeviceId,
          esphomeDeviceId: subDeviceId,
        },
      });
    }

    for (const entity of snapshotArg.entities) {
      const deviceId = this.deviceIdForEntity(snapshotArg, entity);
      const device = devices.get(deviceId) || devices.get(mainDeviceId);
      if (!device) {
        continue;
      }
      const feature = this.featureForEntity(entity);
      device.features.push(feature);
      device.state.push({ featureId: feature.id, value: this.deviceStateValue(this.entityState(entity, this.stateForEntity(snapshotArg, entity))), updatedAt });
    }

    return [...devices.values()];
  }

  public static toEntities(snapshotArg: IEsphomeSnapshot): IIntegrationEntity[] {
    return snapshotArg.entities.map((entityArg) => {
      const state = this.stateForEntity(snapshotArg, entityArg);
      const platform = this.corePlatform(entityArg.platform);
      const name = this.entityName(entityArg);
      return {
        id: entityArg.entityId || entityArg.entity_id || `${platform}.${this.slug(`${this.deviceName(snapshotArg.deviceInfo, snapshotArg.host)} ${name}`)}`,
        uniqueId: this.uniqueIdForEntity(snapshotArg, entityArg),
        integrationDomain: 'esphome',
        deviceId: this.deviceIdForEntity(snapshotArg, entityArg),
        platform,
        name,
        state: this.entityState(entityArg, state),
        attributes: {
          key: entityArg.key,
          deviceId: entityArg.deviceId ?? entityArg.device_id,
          esphomePlatform: entityArg.platform,
          deviceClass: entityArg.deviceClass || entityArg.device_class,
          unit: entityArg.unitOfMeasurement || entityArg.unit_of_measurement,
          accuracyDecimals: entityArg.accuracyDecimals ?? entityArg.accuracy_decimals,
          stateClass: entityArg.stateClass ?? entityArg.state_class,
          entityCategory: entityArg.entityCategory || entityArg.entity_category,
          icon: entityArg.icon,
          options: entityArg.options,
          effects: entityArg.effects,
          rawState: state,
          rawInfo: entityArg.raw,
        },
        available: !this.missingState(state),
      };
    });
  }

  public static commandForService(snapshotArg: IEsphomeSnapshot, requestArg: IServiceCallRequest): IEsphomeClientCommand | undefined {
    const target = this.findTargetEntity(snapshotArg, requestArg);
    if (!target) {
      return undefined;
    }
    const platform = this.corePlatform(target.platform);
    const payload = this.payloadForService(target, requestArg);
    if (!payload) {
      return undefined;
    }
    return {
      type: `${platform}.command`,
      service: requestArg.service,
      platform: target.platform,
      key: target.key,
      deviceId: target.deviceId ?? target.device_id,
      entityId: target.entityId || target.entity_id,
      uniqueId: this.uniqueIdForEntity(snapshotArg, target),
      payload,
      target: requestArg.target,
    };
  }

  public static corePlatform(platformArg: string): TEntityPlatform {
    const platform = platformArg.toLowerCase();
    if (platform === 'text_sensor') {
      return 'sensor';
    }
    const allowed: TEntityPlatform[] = ['light', 'switch', 'sensor', 'binary_sensor', 'button', 'climate', 'cover', 'fan', 'media_player', 'number', 'select', 'text', 'update'];
    return allowed.includes(platform as TEntityPlatform) ? platform as TEntityPlatform : 'sensor';
  }

  public static uniqueIdForEntity(snapshotArg: IEsphomeSnapshot, entityArg: IEsphomeEntityDescriptor): string {
    const explicit = entityArg.uniqueId || entityArg.unique_id;
    if (explicit) {
      return explicit;
    }
    const deviceIdentifier = this.compactDeviceIdentifier(snapshotArg.deviceInfo) || this.slug(snapshotArg.host || 'configured');
    const objectId = entityArg.objectId || entityArg.object_id || this.slug(this.entityName(entityArg));
    const subDevice = entityArg.deviceId ?? entityArg.device_id;
    return `esphome_${this.slug(`${deviceIdentifier}_${entityArg.platform}_${objectId}_${entityArg.key}_${subDevice ?? 0}`)}`;
  }

  private static deviceInfoFromConfig(configArg: IEsphomeConfig, sourceArg?: IEsphomeDeviceInfo, manualEntryArg?: IEsphomeManualEntry): IEsphomeDeviceInfo {
    return {
      ...sourceArg,
      ...configArg.deviceInfo,
      name: configArg.deviceInfo?.name || sourceArg?.name || configArg.deviceName || manualEntryArg?.deviceName || manualEntryArg?.name || configArg.name || configArg.host || manualEntryArg?.host || 'ESPHome',
      friendlyName: configArg.deviceInfo?.friendlyName || sourceArg?.friendlyName || configArg.name || manualEntryArg?.name,
      manufacturer: configArg.deviceInfo?.manufacturer || sourceArg?.manufacturer || manualEntryArg?.manufacturer,
      model: configArg.deviceInfo?.model || sourceArg?.model || manualEntryArg?.model,
      host: configArg.host || sourceArg?.host || manualEntryArg?.host || configArg.deviceInfo?.host,
      port: configArg.port || sourceArg?.port || manualEntryArg?.port || configArg.deviceInfo?.port || esphomeDefaultNativeApiPort,
      usesPassword: configArg.deviceInfo?.usesPassword ?? sourceArg?.usesPassword ?? Boolean(configArg.password || manualEntryArg?.password),
      apiEncryptionSupported: configArg.deviceInfo?.apiEncryptionSupported ?? sourceArg?.apiEncryptionSupported ?? Boolean(configArg.encryptionKey || configArg.noisePsk || manualEntryArg?.encryptionKey || manualEntryArg?.noisePsk),
    };
  }

  private static normalizeStates(statesArg: IEsphomeConfig['states'] | IEsphomeSnapshot['states'] | undefined): IEsphomeEntityState[] {
    if (!statesArg) {
      return [];
    }
    if (Array.isArray(statesArg)) {
      return statesArg.map((stateArg) => ({ ...stateArg }));
    }
    return Object.entries(statesArg).map(([key, value]) => {
      if (this.isRecord(value)) {
        return { key, ...value } as IEsphomeEntityState;
      }
      return { key, state: value };
    });
  }

  private static stateForEntity(snapshotArg: IEsphomeSnapshot, entityArg: IEsphomeEntityDescriptor): IEsphomeEntityState | undefined {
    const uniqueId = this.uniqueIdForEntity(snapshotArg, entityArg);
    const entityId = entityArg.entityId || entityArg.entity_id;
    return snapshotArg.states.find((stateArg) => {
      if ((stateArg.uniqueId || stateArg.unique_id) && (stateArg.uniqueId || stateArg.unique_id) === uniqueId) {
        return true;
      }
      if (entityId && (stateArg.entityId || stateArg.entity_id) === entityId) {
        return true;
      }
      const stateKey = stateArg.key;
      if (stateKey === undefined || String(stateKey) !== String(entityArg.key)) {
        return false;
      }
      const statePlatform = stateArg.platform;
      if (statePlatform && this.corePlatform(String(statePlatform)) !== this.corePlatform(entityArg.platform)) {
        return false;
      }
      const entityDeviceId = entityArg.deviceId ?? entityArg.device_id;
      const stateDeviceId = stateArg.deviceId ?? stateArg.device_id;
      return stateDeviceId === undefined || entityDeviceId === undefined || String(stateDeviceId) === String(entityDeviceId);
    });
  }

  private static entityState(entityArg: IEsphomeEntityDescriptor, stateArg?: IEsphomeEntityState): unknown {
    if (this.missingState(stateArg)) {
      return 'unknown';
    }
    const rawValue = this.rawStateValue(stateArg);
    const platform = this.corePlatform(entityArg.platform);
    if (platform === 'light' || platform === 'switch' || platform === 'fan') {
      if (typeof rawValue === 'boolean') {
        return rawValue ? 'on' : 'off';
      }
      if (this.isRecord(stateArg) && typeof stateArg.state === 'boolean') {
        return stateArg.state ? 'on' : 'off';
      }
    }
    if (platform === 'cover') {
      const position = this.numberValue(stateArg?.position);
      if (position !== undefined) {
        return Math.round(position * 100);
      }
      if (typeof rawValue === 'boolean') {
        return rawValue ? 'open' : 'closed';
      }
    }
    if (platform === 'climate') {
      return stateArg?.mode ?? stateArg?.hvacMode ?? stateArg?.hvac_mode ?? rawValue ?? 'unknown';
    }
    if (platform === 'button') {
      return rawValue ?? 'idle';
    }
    return rawValue ?? 'unknown';
  }

  private static payloadForService(entityArg: IEsphomeEntityDescriptor, requestArg: IServiceCallRequest): Record<string, unknown> | undefined {
    const platform = this.corePlatform(entityArg.platform);
    if (requestArg.service === 'turn_on') {
      if (!['light', 'switch', 'fan', 'climate'].includes(platform)) {
        return undefined;
      }
      return {
        state: true,
        ...this.lightPayload(requestArg),
        ...this.fanPayload(requestArg),
      };
    }
    if (requestArg.service === 'turn_off') {
      if (!['light', 'switch', 'fan', 'climate'].includes(platform)) {
        return undefined;
      }
      return { state: false };
    }
    if (requestArg.service === 'set_value') {
      if (!['number', 'text'].includes(platform) || requestArg.data?.value === undefined) {
        return undefined;
      }
      return { value: requestArg.data.value };
    }
    if (requestArg.service === 'press') {
      return platform === 'button' ? { press: true } : undefined;
    }
    if (requestArg.service === 'set_position') {
      const position = this.numberValue(requestArg.data?.position ?? requestArg.data?.positionPercentage ?? requestArg.data?.position_percentage);
      return platform === 'cover' && position !== undefined ? { position: this.percentToFraction(position), positionPercentage: this.clampPercent(position) } : undefined;
    }
    if (requestArg.service === 'set_percentage') {
      const percentage = this.numberValue(requestArg.data?.percentage);
      return platform === 'fan' && percentage !== undefined ? { state: percentage > 0, percentage: this.clampPercent(percentage) } : undefined;
    }
    if (requestArg.service === 'select_option') {
      const option = requestArg.data?.option;
      return platform === 'select' && typeof option === 'string' ? { option } : undefined;
    }
    if (requestArg.service === 'open_cover') {
      return platform === 'cover' ? { position: 1, positionPercentage: 100 } : undefined;
    }
    if (requestArg.service === 'close_cover') {
      return platform === 'cover' ? { position: 0, positionPercentage: 0 } : undefined;
    }
    if (requestArg.service === 'stop_cover') {
      return platform === 'cover' ? { stop: true } : undefined;
    }
    if (requestArg.service === 'set_temperature') {
      const temperature = this.numberValue(requestArg.data?.temperature);
      return platform === 'climate' && temperature !== undefined ? { targetTemperature: temperature } : undefined;
    }
    return undefined;
  }

  private static findTargetEntity(snapshotArg: IEsphomeSnapshot, requestArg: IServiceCallRequest): IEsphomeEntityDescriptor | undefined {
    if (requestArg.target.entityId) {
      const entity = this.toEntities(snapshotArg).find((entityArg) => entityArg.id === requestArg.target.entityId || entityArg.uniqueId === requestArg.target.entityId);
      if (!entity) {
        return undefined;
      }
      const target = snapshotArg.entities.find((entityArg) => this.uniqueIdForEntity(snapshotArg, entityArg) === entity.uniqueId);
      return target && this.serviceMatchesEntity(target, requestArg) ? target : undefined;
    }
    const candidates = requestArg.target.deviceId
      ? snapshotArg.entities.filter((entityArg) => this.deviceIdForEntity(snapshotArg, entityArg) === requestArg.target.deviceId)
      : snapshotArg.entities;
    return candidates.find((entityArg) => this.serviceMatchesEntity(entityArg, requestArg));
  }

  private static serviceMatchesEntity(entityArg: IEsphomeEntityDescriptor, requestArg: IServiceCallRequest): boolean {
    if (!this.entityWritable(entityArg)) {
      return false;
    }
    const domainPlatform = this.domainPlatform(requestArg.domain);
    if (domainPlatform && this.corePlatform(entityArg.platform) !== domainPlatform) {
      return false;
    }
    return Boolean(this.payloadForService(entityArg, requestArg));
  }

  private static domainPlatform(domainArg: string): TEntityPlatform | undefined {
    if (domainArg === 'esphome') {
      return undefined;
    }
    const allowed: TEntityPlatform[] = ['light', 'switch', 'sensor', 'binary_sensor', 'button', 'climate', 'cover', 'fan', 'media_player', 'number', 'select', 'text', 'update'];
    return allowed.includes(domainArg as TEntityPlatform) ? domainArg as TEntityPlatform : undefined;
  }

  private static entityWritable(entityArg: IEsphomeEntityDescriptor): boolean {
    if (typeof entityArg.writable === 'boolean') {
      return entityArg.writable;
    }
    return ['light', 'switch', 'fan', 'cover', 'climate', 'button', 'number', 'select', 'text'].includes(this.corePlatform(entityArg.platform));
  }

  private static featureForEntity(entityArg: IEsphomeEntityDescriptor): plugins.shxInterfaces.data.IDeviceFeature {
    const platform = this.corePlatform(entityArg.platform);
    return {
      id: this.slug(`${entityArg.platform}_${entityArg.objectId || entityArg.object_id || entityArg.key}`),
      capability: this.capabilityForPlatform(platform),
      name: this.entityName(entityArg),
      readable: true,
      writable: this.entityWritable(entityArg),
      unit: entityArg.unitOfMeasurement || entityArg.unit_of_measurement,
    };
  }

  private static capabilityForPlatform(platformArg: TEntityPlatform): plugins.shxInterfaces.data.TDeviceCapability {
    if (platformArg === 'light') {
      return 'light';
    }
    if (platformArg === 'cover') {
      return 'cover';
    }
    if (platformArg === 'climate') {
      return 'climate';
    }
    if (platformArg === 'fan') {
      return 'fan';
    }
    if (platformArg === 'switch' || platformArg === 'button' || platformArg === 'number' || platformArg === 'select' || platformArg === 'text') {
      return 'switch';
    }
    return 'sensor';
  }

  private static deviceIdForEntity(snapshotArg: IEsphomeSnapshot, entityArg: IEsphomeEntityDescriptor): string {
    const deviceId = entityArg.deviceId ?? entityArg.device_id;
    if (deviceId === undefined || deviceId === null || String(deviceId) === '0') {
      return this.mainDeviceId(snapshotArg);
    }
    return this.subDeviceId(snapshotArg, deviceId);
  }

  private static mainDeviceId(snapshotArg: IEsphomeSnapshot): string {
    return `esphome.device.${this.slug(this.compactDeviceIdentifier(snapshotArg.deviceInfo) || snapshotArg.deviceInfo.name || snapshotArg.host || 'configured')}`;
  }

  private static subDeviceId(snapshotArg: IEsphomeSnapshot, subDeviceIdArg: string | number): string {
    return `${this.mainDeviceId(snapshotArg)}.${this.slug(String(subDeviceIdArg))}`;
  }

  private static subDeviceIdValue(valueArg: { deviceId?: number | string; device_id?: number | string }): number | string | undefined {
    return valueArg.deviceId ?? valueArg.device_id;
  }

  private static entityName(entityArg: IEsphomeEntityDescriptor): string {
    return entityArg.name || entityArg.objectId || entityArg.object_id || `${entityArg.platform} ${entityArg.key}`;
  }

  private static deviceName(deviceInfoArg: IEsphomeDeviceInfo, hostArg?: string): string {
    return deviceInfoArg.friendlyName || deviceInfoArg.name || hostArg || 'ESPHome';
  }

  private static manufacturer(deviceInfoArg: IEsphomeDeviceInfo): string {
    if (deviceInfoArg.manufacturer) {
      return deviceInfoArg.manufacturer;
    }
    if (deviceInfoArg.projectName) {
      return deviceInfoArg.projectName.split('.')[0] || 'ESPHome';
    }
    return 'ESPHome';
  }

  private static model(deviceInfoArg: IEsphomeDeviceInfo): string | undefined {
    if (deviceInfoArg.model) {
      return deviceInfoArg.model;
    }
    if (deviceInfoArg.projectName) {
      return deviceInfoArg.projectName.split('.').slice(1).join('.') || deviceInfoArg.projectName;
    }
    return 'ESPHome node';
  }

  private static areaForSubDevice(deviceInfoArg: IEsphomeDeviceInfo, areaIdArg: unknown): string | undefined {
    const area = (deviceInfoArg.areas || []).find((areaArg) => String(areaArg.areaId ?? areaArg.area_id) === String(areaIdArg));
    return this.areaName(area);
  }

  private static areaName(areaArg?: IEsphomeAreaInfo): string | undefined {
    return areaArg?.name || undefined;
  }

  private static rawStateValue(stateArg?: IEsphomeEntityState): unknown {
    if (!stateArg) {
      return undefined;
    }
    if ('state' in stateArg) {
      return stateArg.state;
    }
    if ('value' in stateArg) {
      return stateArg.value;
    }
    return undefined;
  }

  private static missingState(stateArg?: IEsphomeEntityState): boolean {
    return stateArg?.missingState === true || stateArg?.missing_state === true;
  }

  private static lightPayload(requestArg: IServiceCallRequest): Record<string, unknown> {
    const brightness = this.numberValue(requestArg.data?.brightness);
    const transition = this.numberValue(requestArg.data?.transition);
    const effect = requestArg.data?.effect;
    return {
      ...(brightness !== undefined ? { brightness: this.clampPercent(brightness / 255 * 100) / 100 } : {}),
      ...(transition !== undefined ? { transitionLength: transition } : {}),
      ...(typeof effect === 'string' ? { effect } : {}),
    };
  }

  private static fanPayload(requestArg: IServiceCallRequest): Record<string, unknown> {
    const percentage = this.numberValue(requestArg.data?.percentage);
    const presetMode = requestArg.data?.presetMode ?? requestArg.data?.preset_mode;
    return {
      ...(percentage !== undefined ? { percentage: this.clampPercent(percentage) } : {}),
      ...(typeof presetMode === 'string' ? { presetMode } : {}),
    };
  }

  private static percentToFraction(valueArg: number): number {
    return this.clampPercent(valueArg) / 100;
  }

  private static clampPercent(valueArg: number): number {
    return Math.max(0, Math.min(100, valueArg));
  }

  private static numberValue(valueArg: unknown): number | undefined {
    return typeof valueArg === 'number' && Number.isFinite(valueArg) ? valueArg : undefined;
  }

  private static compactDeviceIdentifier(deviceInfoArg: IEsphomeDeviceInfo): string | undefined {
    const mac = this.normalizeMac(deviceInfoArg.macAddress);
    return mac ? mac.replace(/:/g, '') : undefined;
  }

  private static normalizeMac(valueArg: unknown): string | undefined {
    if (typeof valueArg !== 'string') {
      return undefined;
    }
    const compact = valueArg.replace(/[^a-fA-F0-9]/g, '').toLowerCase();
    if (compact.length !== 12) {
      return valueArg.toLowerCase();
    }
    return compact.match(/.{1,2}/g)?.join(':');
  }

  private static deviceStateValue(valueArg: unknown): plugins.shxInterfaces.data.TDeviceStateValue {
    if (Array.isArray(valueArg)) {
      return JSON.stringify(valueArg);
    }
    if (typeof valueArg === 'string' || typeof valueArg === 'number' || typeof valueArg === 'boolean' || valueArg === null || this.isRecord(valueArg)) {
      return valueArg;
    }
    return valueArg === undefined ? null : String(valueArg);
  }

  private static isRecord(valueArg: unknown): valueArg is Record<string, unknown> {
    return typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
  }

  private static slug(valueArg: string): string {
    return valueArg.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'esphome';
  }
}
