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 { IGoveeBleConfig } from './govee_ble.types.js';
import { goveeBleDefaultName, goveeBleProfile } from './govee_ble.types.js';

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

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

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

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

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

  private static normalizeRawData(configArg: IGoveeBleConfig, rawDataArg: unknown): unknown {
    if (!isRecord(rawDataArg) || ('device' in rawDataArg && 'entities' in rawDataArg)) {
      return rawDataArg;
    }

    const rawDevice = recordValue(rawDataArg.device) || recordValue(rawDataArg.deviceInfo) || recordValue(rawDataArg.device_info);
    const address = configArg.address || stringValue(rawDataArg.address) || stringValue(rawDataArg.macAddress) || stringValue(rawDataArg.mac) || stringValue(rawDataArg.bleAddress) || stringValue(rawDevice?.address) || stringValue(rawDevice?.mac) || configArg.uniqueId;
    const deviceType = configArg.deviceType || stringValue(rawDataArg.deviceType) || stringValue(rawDataArg.device_type) || stringValue(rawDataArg.model) || stringValue(rawDataArg.sku) || stringValue(rawDevice?.model) || stringValue(rawDevice?.deviceType);
    const name = configArg.name || stringValue(rawDataArg.title) || stringValue(rawDataArg.name) || stringValue(rawDevice?.name) || (deviceType ? `Govee ${deviceType}` : goveeBleDefaultName);
    const uniqueBase = address || deviceType || name;
    const entities: ISimpleLocalEntitySnapshot[] = [];

    addSensorRecord(entities, uniqueBase, rawDataArg);
    for (const key of ['sensors', 'sensorValues', 'sensor_values', 'entityValues', 'entity_values', 'measurements', 'values']) {
      addSensorRecord(entities, uniqueBase, recordValue(rawDataArg[key]));
    }
    for (const key of ['binarySensors', 'binary_sensors', 'binaryEntityValues', 'binary_entity_values']) {
      addBinaryRecord(entities, uniqueBase, recordValue(rawDataArg[key]));
    }
    addBinaryRecord(entities, uniqueBase, rawDataArg);
    addEventRecord(entities, uniqueBase, recordValue(rawDataArg.events) || recordValue(rawDataArg.eventValues) || recordValue(rawDataArg.event_values));

    const hasHints = Boolean(address || deviceType || entities.length || stringValue(rawDataArg.localName) || stringValue(rawDataArg.local_name));
    if (!hasHints) {
      return rawDataArg;
    }

    return {
      device: {
        id: configArg.uniqueId || address || uniqueBase,
        name,
        manufacturer: goveeBleProfile.manufacturer,
        model: deviceType || goveeBleProfile.model,
        serialNumber: address,
        protocol: 'local',
        attributes: {
          address,
          deviceType,
          localName: stringValue(rawDataArg.localName) || stringValue(rawDataArg.local_name),
          rssi: numberValue(rawDataArg.rssi),
        },
      },
      entities: entities.length ? entities : [statusEntity(uniqueBase, true)],
      online: configArg.online ?? true,
      updatedAt: new Date().toISOString(),
      source: 'manual',
      rawData: rawDataArg,
    } satisfies ISimpleLocalSnapshot;
  }
}

const addSensorRecord = (entitiesArg: ISimpleLocalEntitySnapshot[], uniqueBaseArg: string, recordArg: Record<string, unknown> | undefined): void => {
  if (!recordArg) {
    return;
  }
  for (const [key, value] of Object.entries(recordArg)) {
    if (ignoredKeys.has(key)) {
      continue;
    }
    const valueRecord = recordValue(value);
    const nativeValue = primitiveValue(valueRecord?.native_value ?? valueRecord?.nativeValue ?? valueRecord?.value ?? valueRecord?.state ?? value);
    if (nativeValue === undefined || typeof nativeValue === 'boolean') {
      continue;
    }
    const name = stringValue(valueRecord?.name) || titleCase(key);
    const descriptor = sensorDescriptor(key, name, valueRecord);
    if (!descriptor) {
      continue;
    }
    entitiesArg.push(sensorEntity(uniqueBaseArg, descriptor.id, descriptor.name, nativeValue, descriptor.unit, descriptor.deviceClass, descriptor.stateClass));
  }
};

const addBinaryRecord = (entitiesArg: ISimpleLocalEntitySnapshot[], uniqueBaseArg: string, recordArg: Record<string, unknown> | undefined): void => {
  if (!recordArg) {
    return;
  }
  for (const [key, value] of Object.entries(recordArg)) {
    if (ignoredKeys.has(key)) {
      continue;
    }
    const valueRecord = recordValue(value);
    const nativeValue = booleanValue(valueRecord?.native_value ?? valueRecord?.nativeValue ?? valueRecord?.value ?? valueRecord?.state ?? value);
    const name = stringValue(valueRecord?.name) || titleCase(key);
    const id = binaryId(key, name);
    if (nativeValue === undefined || !id) {
      continue;
    }
    entitiesArg.push({
      id,
      uniqueId: `${goveeBleProfile.domain}_${SimpleLocalMapper.slug(uniqueBaseArg)}_${SimpleLocalMapper.slug(id)}`,
      name,
      platform: 'binary_sensor',
      state: nativeValue,
      available: true,
      writable: false,
      deviceClass: binaryDeviceClass(id),
    });
  }
};

const addEventRecord = (entitiesArg: ISimpleLocalEntitySnapshot[], uniqueBaseArg: string, recordArg: Record<string, unknown> | undefined): void => {
  if (!recordArg) {
    return;
  }
  for (const [key, value] of Object.entries(recordArg)) {
    const eventType = stringValue(recordValue(value)?.event_type) || stringValue(recordValue(value)?.eventType) || stringValue(value) || key;
    entitiesArg.push(sensorEntity(uniqueBaseArg, `event_${key}`, `${titleCase(key)} Event`, eventType, undefined, undefined, undefined, { eventTypes: [eventType] }));
  }
};

const sensorDescriptor = (keyArg: string, nameArg: string, valueRecordArg?: Record<string, unknown>): { id: string; name: string; unit?: string; deviceClass?: string; stateClass?: string } | undefined => {
  const haystack = `${keyArg} ${nameArg} ${stringValue(valueRecordArg?.device_class) || ''} ${stringValue(valueRecordArg?.deviceClass) || ''}`.toLowerCase();
  if (haystack.includes('temperature') || /\btemp\b/.test(haystack)) {
    return { id: 'temperature', name: nameArg === titleCase(keyArg) ? 'Temperature' : nameArg, unit: 'C', deviceClass: 'temperature', stateClass: 'measurement' };
  }
  if (haystack.includes('humidity')) {
    return { id: 'humidity', name: nameArg === titleCase(keyArg) ? 'Humidity' : nameArg, unit: '%', deviceClass: 'humidity', stateClass: 'measurement' };
  }
  if (haystack.includes('battery')) {
    return { id: 'battery', name: nameArg === titleCase(keyArg) ? 'Battery' : nameArg, unit: '%', deviceClass: 'battery', stateClass: 'measurement' };
  }
  if (haystack.includes('signal') || haystack.includes('rssi')) {
    return { id: 'signal_strength', name: nameArg === titleCase(keyArg) ? 'Signal Strength' : nameArg, unit: 'dBm', deviceClass: 'signal_strength', stateClass: 'measurement' };
  }
  if (haystack.includes('pm25') || haystack.includes('pm2_5') || haystack.includes('pm2.5')) {
    return { id: 'pm25', name: nameArg === titleCase(keyArg) ? 'PM2.5' : nameArg, unit: 'ug/m3', deviceClass: 'pm25', stateClass: 'measurement' };
  }
  if (haystack.includes('co2')) {
    return { id: 'co2', name: nameArg === titleCase(keyArg) ? 'Carbon Dioxide' : nameArg, unit: 'ppm', deviceClass: 'carbon_dioxide', stateClass: 'measurement' };
  }
  return undefined;
};

const sensorEntity = (uniqueBaseArg: string, idArg: string, nameArg: string, stateArg: unknown, unitArg?: string, deviceClassArg?: string, stateClassArg?: string, attributesArg?: Record<string, unknown>): ISimpleLocalEntitySnapshot => ({
  id: SimpleLocalMapper.slug(idArg),
  uniqueId: `${goveeBleProfile.domain}_${SimpleLocalMapper.slug(uniqueBaseArg)}_${SimpleLocalMapper.slug(idArg)}`,
  name: nameArg,
  platform: 'sensor',
  state: stateArg,
  available: true,
  writable: false,
  unit: unitArg,
  deviceClass: deviceClassArg,
  stateClass: stateClassArg,
  attributes: attributesArg,
});

const statusEntity = (uniqueBaseArg: string, stateArg: boolean): ISimpleLocalEntitySnapshot => ({
  id: 'online',
  uniqueId: `${goveeBleProfile.domain}_${SimpleLocalMapper.slug(uniqueBaseArg)}_online`,
  name: 'Online',
  platform: 'binary_sensor',
  state: stateArg,
  available: true,
  writable: false,
  deviceClass: 'connectivity',
});

const binaryId = (keyArg: string, nameArg: string): string | undefined => {
  const haystack = `${keyArg} ${nameArg}`.toLowerCase();
  if (haystack.includes('window')) return 'window';
  if (haystack.includes('motion')) return 'motion';
  if (haystack.includes('occupancy')) return 'occupancy';
  if (haystack.includes('presence')) return 'presence';
  if (haystack.includes('opening')) return 'opening';
  return undefined;
};

const binaryDeviceClass = (idArg: string): string | undefined => idArg === 'opening' ? 'opening' : idArg;

const ignoredKeys = new Set(['address', 'bleAddress', 'device', 'deviceInfo', 'device_info', 'deviceType', 'device_type', 'events', 'localName', 'local_name', 'mac', 'macAddress', 'manufacturer', 'model', 'name', 'rssi', 'sku', 'title']);

const primitiveValue = (valueArg: unknown): string | number | boolean | null | undefined => valueArg === null || ['string', 'number', 'boolean'].includes(typeof valueArg) ? valueArg as string | number | boolean | null : undefined;
const recordValue = (valueArg: unknown): Record<string, unknown> | undefined => isRecord(valueArg) ? valueArg : undefined;
const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> => Boolean(valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg));
const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
const numberValue = (valueArg: unknown): number | undefined => {
  const value = typeof valueArg === 'number' ? valueArg : typeof valueArg === 'string' && valueArg.trim() ? Number(valueArg) : undefined;
  return value !== undefined && Number.isFinite(value) ? value : undefined;
};
const booleanValue = (valueArg: unknown): boolean | undefined => {
  if (typeof valueArg === 'boolean') return valueArg;
  if (typeof valueArg === 'number') return valueArg !== 0;
  const value = stringValue(valueArg)?.toLowerCase();
  if (!value) return undefined;
  if (['1', 'true', 'yes', 'on', 'open', 'detected'].includes(value)) return true;
  if (['0', 'false', 'no', 'off', 'closed', 'clear'].includes(value)) return false;
  return undefined;
};
const titleCase = (valueArg: string): string => valueArg.replace(/[_-]+/g, ' ').replace(/\b\w/g, (charArg) => charArg.toUpperCase());
