import * as plugins from '../../plugins.js';
import type { IIntegrationEntity } from '../../core/types.js';
import { normalizedProductType } from './blebox.classes.client.js';
import type {
  IBleboxBinarySensorFeature,
  IBleboxCoverFeature,
  IBleboxDeviceInfo,
  IBleboxLightFeature,
  IBleboxSensorFeature,
  IBleboxSnapshot,
  IBleboxSwitchFeature,
  TBleboxCoverStateName,
  TBleboxFeature,
  TBleboxJsonValue,
} from './blebox.types.js';

const sensorMetadata: Record<string, { unit?: string; deviceClass?: string; scale?: number }> = {
  activePower: { unit: 'W', deviceClass: 'power' },
  apparentPower: { unit: 'VA', deviceClass: 'apparent_power' },
  current: { unit: 'mA', deviceClass: 'current' },
  forwardActiveEnergy: { unit: 'kWh', deviceClass: 'energy', scale: 1000 },
  frequency: { unit: 'Hz', deviceClass: 'frequency', scale: 1000 },
  humidity: { unit: '%', deviceClass: 'humidity', scale: 100 },
  illuminance: { unit: 'lx', deviceClass: 'illuminance', scale: 100 },
  pm1: { unit: 'ug/m3', deviceClass: 'pm1' },
  pm2_5: { unit: 'ug/m3', deviceClass: 'pm25' },
  pm10: { unit: 'ug/m3', deviceClass: 'pm10' },
  powerConsumption: { unit: 'kWh', deviceClass: 'energy' },
  reactivePower: { unit: 'var', deviceClass: 'power' },
  reverseActiveEnergy: { unit: 'kWh', deviceClass: 'energy', scale: 1000 },
  temperature: { unit: 'C', deviceClass: 'temperature', scale: 100 },
  voltage: { unit: 'V', deviceClass: 'voltage', scale: 10 },
  wind: { unit: 'm/s', deviceClass: 'wind_speed', scale: 10 },
};

export class BleboxMapper {
  public static toDevices(snapshotArg: IBleboxSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = new Date().toISOString();
    const deviceId = this.deviceId(snapshotArg);
    const features: plugins.shxInterfaces.data.IDeviceFeature[] = [
      { id: 'connectivity', capability: 'sensor', name: 'Connectivity', readable: true, writable: false },
    ];
    const state: plugins.shxInterfaces.data.IDeviceState[] = [
      { featureId: 'connectivity', value: 'online', updatedAt },
    ];

    for (const feature of this.features(snapshotArg)) {
      if (feature.platform === 'switch') {
        features.push({ id: feature.id, capability: 'switch', name: feature.name, readable: true, writable: true });
        state.push({ featureId: feature.id, value: feature.isOn, updatedAt });
      } else if (feature.platform === 'light') {
        features.push({ id: feature.id, capability: 'light', name: feature.name, readable: true, writable: true });
        state.push({ featureId: feature.id, value: feature.isOn, updatedAt });
        if (typeof feature.brightness === 'number') {
          features.push({ id: `${feature.id}_brightness`, capability: 'light', name: `${feature.name} brightness`, readable: true, writable: true, unit: '%' });
          state.push({ featureId: `${feature.id}_brightness`, value: Math.round(feature.brightness / 255 * 100), updatedAt });
        }
      } else if (feature.platform === 'cover') {
        features.push({ id: feature.id, capability: 'cover', name: feature.name, readable: true, writable: true, unit: '%' });
        state.push({ featureId: feature.id, value: feature.stateName, updatedAt });
        if (feature.position !== null) {
          features.push({ id: `${feature.id}_position`, capability: 'cover', name: `${feature.name} position`, readable: true, writable: feature.supportsPosition, unit: '%' });
          state.push({ featureId: `${feature.id}_position`, value: feature.position, updatedAt });
        }
      } else if (feature.platform === 'sensor') {
        features.push({ id: feature.id, capability: 'sensor', name: feature.name, readable: true, writable: false, unit: feature.unit });
        state.push({ featureId: feature.id, value: feature.nativeValue, updatedAt });
      } else if (feature.platform === 'binary_sensor') {
        features.push({ id: feature.id, capability: 'sensor', name: feature.name, readable: true, writable: false });
        state.push({ featureId: feature.id, value: feature.isOn, updatedAt });
      }
    }

    return [{
      id: deviceId,
      integrationDomain: 'blebox',
      name: snapshotArg.device.deviceName || 'BleBox device',
      protocol: 'http',
      manufacturer: 'BleBox',
      model: this.model(snapshotArg.device),
      online: true,
      features,
      state,
      metadata: {
        firmwareVersion: snapshotArg.device.fv,
        hardwareVersion: snapshotArg.device.hv,
        apiLevel: snapshotArg.device.apiLevel,
        host: snapshotArg.host || snapshotArg.device.ip,
        port: snapshotArg.port,
      },
    }];
  }

  public static toEntities(snapshotArg: IBleboxSnapshot): IIntegrationEntity[] {
    const deviceId = this.deviceId(snapshotArg);
    const deviceSlug = this.slug(snapshotArg.device.deviceName || snapshotArg.device.id || 'blebox');
    const entities: IIntegrationEntity[] = [];

    for (const feature of this.features(snapshotArg)) {
      const featureSlug = this.slug(feature.alias);
      if (feature.platform === 'switch') {
        entities.push({
          id: `switch.${deviceSlug}_${featureSlug}`,
          uniqueId: feature.uniqueId,
          integrationDomain: 'blebox',
          deviceId,
          platform: 'switch',
          name: feature.name,
          state: feature.isOn ? 'on' : 'off',
          attributes: { unitId: feature.unitId, deviceClass: feature.deviceClass },
          available: feature.isOn !== null,
        });
      } else if (feature.platform === 'light') {
        entities.push({
          id: `light.${deviceSlug}_${featureSlug}`,
          uniqueId: feature.uniqueId,
          integrationDomain: 'blebox',
          deviceId,
          platform: 'light',
          name: feature.name,
          state: feature.isOn ? 'on' : 'off',
          attributes: {
            brightness: feature.brightness,
            colorMode: feature.colorModeName,
            desiredColor: feature.desiredColor,
            effect: feature.effect,
            effectList: feature.effectList,
            supportsColor: feature.supportsColor,
            supportsWhite: feature.supportsWhite,
          },
          available: feature.isOn !== null,
        });
      } else if (feature.platform === 'cover') {
        entities.push({
          id: `cover.${deviceSlug}_${featureSlug}`,
          uniqueId: feature.uniqueId,
          integrationDomain: 'blebox',
          deviceId,
          platform: 'cover',
          name: feature.name,
          state: feature.stateName,
          attributes: {
            currentPosition: feature.position,
            currentTiltPosition: feature.tiltPosition,
            deviceClass: feature.deviceClass,
            supportsStop: feature.supportsStop,
            supportsPosition: feature.supportsPosition,
            supportsTilt: feature.supportsTilt,
          },
          available: feature.stateName !== 'unknown',
        });
      } else if (feature.platform === 'sensor') {
        entities.push({
          id: `sensor.${deviceSlug}_${featureSlug}`,
          uniqueId: feature.uniqueId,
          integrationDomain: 'blebox',
          deviceId,
          platform: 'sensor',
          name: feature.name,
          state: feature.nativeValue,
          attributes: {
            unit: feature.unit,
            deviceClass: feature.deviceClass,
            sensorType: feature.sensorType,
            sensorId: feature.sensorId,
          },
          available: feature.nativeValue !== null,
        });
      } else if (feature.platform === 'binary_sensor') {
        entities.push({
          id: `binary_sensor.${deviceSlug}_${featureSlug}`,
          uniqueId: feature.uniqueId,
          integrationDomain: 'blebox',
          deviceId,
          platform: 'binary_sensor',
          name: feature.name,
          state: feature.isOn ? 'on' : 'off',
          attributes: {
            deviceClass: feature.deviceClass,
            sensorType: feature.sensorType,
            sensorId: feature.sensorId,
          },
          available: true,
        });
      }
    }

    return entities;
  }

  public static features(snapshotArg: IBleboxSnapshot): TBleboxFeature[] {
    return [
      ...this.switchFeatures(snapshotArg),
      ...this.lightFeatures(snapshotArg),
      ...this.coverFeatures(snapshotArg),
      ...this.sensorFeatures(snapshotArg),
      ...this.binarySensorFeatures(snapshotArg),
    ];
  }

  public static deviceId(snapshotArg: IBleboxSnapshot): string {
    return `blebox.device.${this.slug(snapshotArg.device.id || snapshotArg.device.deviceName || 'unknown')}`;
  }

  private static switchFeatures(snapshotArg: IBleboxSnapshot): IBleboxSwitchFeature[] {
    const productType = normalizedProductType(snapshotArg.device);
    if (!['switchBox', 'switchBoxD'].includes(productType)) {
      return [];
    }
    const root = this.mergedRecord(snapshotArg);
    const relays = this.relayStates(root);
    const stateRelays = relays.length > 0 ? relays : this.relayStates(asArray(snapshotArg.state) || []);
    return stateRelays.map((relayArg) => {
      const alias = `relay_${relayArg.unitId}`;
      return {
        platform: 'switch',
        id: `switch_${relayArg.unitId}`,
        alias,
        uniqueId: this.uniqueId(snapshotArg.device, alias),
        name: relayArg.name || `${snapshotArg.device.deviceName || 'BleBox'} relay ${relayArg.unitId}`,
        unitId: relayArg.unitId,
        isOn: relayArg.state === undefined ? null : relayArg.state === 1 || relayArg.state === true,
        deviceClass: 'switch',
      };
    });
  }

  private static lightFeatures(snapshotArg: IBleboxSnapshot): IBleboxLightFeature[] {
    const productType = normalizedProductType(snapshotArg.device);
    const root = this.mergedRecord(snapshotArg);
    if (productType === 'dimmerBox') {
      const dimmer = asRecord(root.dimmer);
      const brightness = toNumber(dimmer?.desiredBrightness ?? dimmer?.currentBrightness);
      const alias = 'brightness';
      return [{
        platform: 'light',
        id: 'light_brightness',
        alias,
        uniqueId: this.uniqueId(snapshotArg.device, alias),
        name: `${snapshotArg.device.deviceName || 'BleBox'} brightness`,
        isOn: brightness === undefined ? null : brightness > 0,
        brightness: brightness ?? null,
        colorModeName: 'brightness',
        supportsColor: false,
        supportsWhite: false,
      }];
    }
    if (!['wLightBox', 'wLightBoxS'].includes(productType)) {
      return [];
    }

    const lightState = asRecord(root.rgbw) || asRecord(root.light);
    if (!lightState) {
      return [];
    }
    const desiredColor = typeof lightState.desiredColor === 'string' ? lightState.desiredColor.replace(/-/g, '0') : undefined;
    const colorMode = toNumber(lightState.colorMode);
    const effectId = toNumber(lightState.effectID);
    const effectList = this.effectList(lightState.effectsNames);
    const effect = effectId === undefined ? undefined : effectList[effectId] ?? effectId;
    const alias = 'color';
    return [{
      platform: 'light',
      id: 'light_color',
      alias,
      uniqueId: this.uniqueId(snapshotArg.device, alias),
      name: `${snapshotArg.device.deviceName || 'BleBox'} color`,
      isOn: desiredColor === undefined ? null : Number.parseInt(desiredColor || '0', 16) > 0 || Boolean(effectId && effectId > 0),
      brightness: desiredColor ? this.brightnessFromHex(desiredColor, colorMode) : null,
      desiredColor: desiredColor ?? null,
      colorMode: colorMode ?? null,
      colorModeName: this.colorModeName(colorMode, productType),
      effect: effect ?? null,
      effectList,
      supportsColor: productType === 'wLightBox' && colorMode !== 3,
      supportsWhite: productType === 'wLightBox' && [1, 4, 7].includes(colorMode ?? 0),
    }];
  }

  private static coverFeatures(snapshotArg: IBleboxSnapshot): IBleboxCoverFeature[] {
    const productType = normalizedProductType(snapshotArg.device);
    const root = this.mergedRecord(snapshotArg);
    const alias = 'position';
    const base = {
      platform: 'cover' as const,
      id: 'cover_position',
      alias,
      uniqueId: this.uniqueId(snapshotArg.device, alias),
      name: `${snapshotArg.device.deviceName || 'BleBox'} position`,
      supportsOpen: true,
      supportsClose: true,
    };

    if (productType === 'shutterBox') {
      const shutter = asRecord(root.shutter);
      if (!shutter) return [];
      const desiredPos = asRecord(shutter.desiredPos);
      const currentPos = asRecord(shutter.currentPos);
      const rawPosition = toNumber(desiredPos?.position ?? currentPos?.position);
      const rawTilt = toNumber(desiredPos?.tilt ?? currentPos?.tilt);
      const state = toNumber(shutter.state);
      return [{
        ...base,
        deviceClass: 'shutter',
        state: state ?? null,
        stateName: this.coverStateName(state),
        position: this.invertPosition(rawPosition),
        tiltPosition: this.invertPosition(rawTilt),
        supportsStop: true,
        supportsPosition: true,
        supportsTilt: toNumber(shutter.controlType) === 3 || rawTilt !== undefined,
        isPositionInverted: true,
      }];
    }

    if (productType === 'gateController') {
      const gateController = asRecord(root.gateController);
      if (!gateController) return [];
      const desiredPos = asRecord(gateController.desiredPos);
      const positions = asArray(desiredPos?.positions);
      const rawPosition = toNumber(positions?.[0]);
      const state = toNumber(gateController.state);
      return [{
        ...base,
        deviceClass: 'gate',
        state: state ?? null,
        stateName: this.coverStateName(state),
        position: this.invertPosition(rawPosition),
        supportsStop: true,
        supportsPosition: true,
        supportsTilt: false,
        isPositionInverted: true,
      }];
    }

    if (productType === 'gateBox') {
      const nestedGate = asRecord(root.gate);
      const gate = nestedGate || root;
      const rawPosition = toNumber(gate.currentPos ?? gate.desiredPos);
      const rawDesired = toNumber(gate.desiredPos ?? gate.currentPos);
      const mappedPosition = nestedGate ? rawPosition : rawDesired;
      const openCloseMode = toNumber(gate.openCloseMode);
      const state = this.gateBoxState(rawPosition, rawDesired);
      return [{
        ...base,
        deviceClass: 'door',
        state,
        stateName: this.coverStateName(state),
        position: mappedPosition === -1 || mappedPosition === undefined ? null : mappedPosition,
        supportsStop: openCloseMode === undefined ? toNumber(gate.extraButtonType) === 1 : openCloseMode !== 2,
        supportsPosition: false,
        supportsTilt: false,
        isPositionInverted: false,
      }];
    }

    return [];
  }

  private static sensorFeatures(snapshotArg: IBleboxSnapshot): IBleboxSensorFeature[] {
    const root = this.mergedRecord(snapshotArg);
    const sensors = this.sensorStates(root).filter((sensorArg) => !['rain', 'flood'].includes(sensorArg.type));
    return sensors.map((sensorArg) => {
      const metadata = sensorMetadata[sensorArg.type] || {};
      const scale = metadata.scale || 1;
      const value = typeof sensorArg.value === 'number' ? sensorArg.value / scale : sensorArg.value ?? null;
      const alias = sensorArg.id === undefined ? sensorArg.type : `${sensorArg.type}_${sensorArg.id}`;
      return {
        platform: 'sensor',
        id: `sensor_${this.slug(alias)}`,
        alias,
        uniqueId: this.uniqueId(snapshotArg.device, alias),
        name: `${snapshotArg.device.deviceName || 'BleBox'} ${this.humanize(sensorArg.type)}`,
        sensorType: sensorArg.type,
        sensorId: sensorArg.id,
        nativeValue: typeof value === 'number' && Number.isFinite(value) ? Number(value.toFixed(3)) : value,
        unit: metadata.unit,
        deviceClass: metadata.deviceClass || sensorArg.type,
      };
    });
  }

  private static binarySensorFeatures(snapshotArg: IBleboxSnapshot): IBleboxBinarySensorFeature[] {
    const root = this.mergedRecord(snapshotArg);
    return this.sensorStates(root)
      .filter((sensorArg) => ['rain', 'flood'].includes(sensorArg.type))
      .map((sensorArg) => {
        const alias = sensorArg.id === undefined ? sensorArg.type : `${sensorArg.type}_${sensorArg.id}`;
        return {
          platform: 'binary_sensor',
          id: `binary_sensor_${this.slug(alias)}`,
          alias,
          uniqueId: this.uniqueId(snapshotArg.device, alias),
          name: `${snapshotArg.device.deviceName || 'BleBox'} ${this.humanize(sensorArg.type)}`,
          sensorType: sensorArg.type,
          sensorId: sensorArg.id,
          isOn: Number(sensorArg.value || 0) > 0,
          deviceClass: 'moisture',
        };
      });
  }

  private static mergedRecord(snapshotArg: IBleboxSnapshot): Record<string, TBleboxJsonValue | undefined> {
    const state = asRecord(snapshotArg.state) || {};
    const extendedState = asRecord(snapshotArg.extendedState) || {};
    const rawInfo = asRecord(snapshotArg.rawInfo) || {};
    return { ...rawInfo, ...state, ...extendedState };
  }

  private static relayStates(rootArg: Record<string, TBleboxJsonValue | undefined> | TBleboxJsonValue[]): Array<{ unitId: number; state?: number | boolean; name?: string }> {
    if (Array.isArray(rootArg)) {
      const oldRelays = rootArg
        .map((relayArg) => asRecord(relayArg))
        .filter((relayArg): relayArg is Record<string, TBleboxJsonValue | undefined> => Boolean(relayArg));
      return oldRelays.map((relayArg, indexArg) => ({
        unitId: toNumber(relayArg.relay) ?? indexArg,
        state: toNumber(relayArg.state) ?? (typeof relayArg.state === 'boolean' ? relayArg.state : undefined),
        name: typeof relayArg.name === 'string' ? relayArg.name : undefined,
      }));
    }

    const relays = asArray(rootArg.relays)
      ?.map((relayArg) => asRecord(relayArg))
      .filter((relayArg): relayArg is Record<string, TBleboxJsonValue | undefined> => Boolean(relayArg)) || [];
    if (relays.length > 0) {
      return relays.map((relayArg, indexArg) => ({
        unitId: toNumber(relayArg.relay) ?? indexArg,
        state: toNumber(relayArg.state) ?? (typeof relayArg.state === 'boolean' ? relayArg.state : undefined),
        name: typeof relayArg.name === 'string' ? relayArg.name : undefined,
      }));
    }

    return [];
  }

  private static sensorStates(rootArg: Record<string, TBleboxJsonValue | undefined>): Array<{ type: string; id?: string | number; value?: number | string }> {
    const sensors: Array<{ type: string; id?: string | number; value?: number | string }> = [];
    const multiSensor = asRecord(rootArg.multiSensor);
    this.pushSensorArray(sensors, asArray(multiSensor?.sensors));
    this.pushSensorArray(sensors, asArray(rootArg.sensors));

    const air = asRecord(rootArg.air);
    for (const airSensor of asArray(air?.sensors) || []) {
      const record = asRecord(airSensor);
      const rawType = typeof record?.type === 'string' ? record.type : undefined;
      if (!rawType) continue;
      sensors.push({ type: this.normalizeSensorType(rawType), value: primitiveNumberOrString(record?.value) });
    }

    const tempSensor = asRecord(rootArg.tempSensor);
    for (const temperatureSensor of asArray(tempSensor?.sensors) || []) {
      const record = asRecord(temperatureSensor);
      sensors.push({ type: 'temperature', id: primitiveId(record?.id), value: primitiveNumberOrString(record?.value) });
    }

    const powerMeasuring = asRecord(rootArg.powerMeasuring);
    for (const power of asArray(powerMeasuring?.powerConsumption) || []) {
      const record = asRecord(power);
      sensors.push({ type: 'powerConsumption', value: primitiveNumberOrString(record?.value) });
    }

    return sensors.filter((sensorArg) => sensorArg.value !== undefined);
  }

  private static pushSensorArray(targetArg: Array<{ type: string; id?: string | number; value?: number | string }>, arrayArg?: TBleboxJsonValue[]): void {
    for (const sensor of arrayArg || []) {
      const record = asRecord(sensor);
      const rawType = typeof record?.type === 'string' ? record.type : undefined;
      if (!rawType) continue;
      targetArg.push({
        type: this.normalizeSensorType(rawType),
        id: primitiveId(record?.id),
        value: primitiveNumberOrString(record?.value),
      });
    }
  }

  private static gateBoxState(currentArg?: number, desiredArg?: number): number | null {
    if (currentArg === undefined || desiredArg === undefined || currentArg === -1) return null;
    if (desiredArg < currentArg) return 0;
    if (desiredArg > currentArg) return 1;
    if (currentArg === 0) return 3;
    if (currentArg === 100) return 4;
    return 2;
  }

  private static coverStateName(stateArg?: number | null): TBleboxCoverStateName {
    if (stateArg === 0) return 'closing';
    if (stateArg === 1) return 'opening';
    if (stateArg === 3) return 'closed';
    if ([2, 4, 5, 6, 8].includes(stateArg ?? -1)) return 'open';
    return 'unknown';
  }

  private static invertPosition(positionArg?: number): number | null {
    if (positionArg === undefined || positionArg === -1) return null;
    return 100 - positionArg;
  }

  private static brightnessFromHex(valueArg: string, colorModeArg?: number): number {
    const elements = valueArg.match(/../g)?.map((value) => Number.parseInt(value, 16)) || [];
    if (elements.length === 0) return 0;
    if ([5, 6].includes(colorModeArg ?? 0) && elements.length >= 2) {
      return Math.max(elements[0], elements[1]);
    }
    return Math.max(...elements.slice(0, Math.min(elements.length, 4)));
  }

  private static effectList(valueArg: TBleboxJsonValue | undefined): string[] {
    const effects = asRecord(valueArg);
    if (!effects) return [];
    return Object.keys(effects)
      .sort((leftArg, rightArg) => Number(leftArg) - Number(rightArg))
      .map((keyArg) => effects[keyArg])
      .filter((effectArg): effectArg is string => typeof effectArg === 'string');
  }

  private static colorModeName(valueArg: number | undefined, productTypeArg: string): string {
    if (productTypeArg === 'dimmerBox') return 'brightness';
    return ({
      1: 'rgbw',
      2: 'rgb',
      3: 'brightness',
      4: 'rgbw',
      5: 'color_temp',
      6: 'color_temp',
      7: 'rgbww',
    } as Record<number, string>)[valueArg ?? 0] || 'onoff';
  }

  private static normalizeSensorType(valueArg: string): string {
    return valueArg === 'pm2.5' ? 'pm2_5' : valueArg;
  }

  private static model(deviceArg: IBleboxDeviceInfo): string {
    return normalizedProductType(deviceArg);
  }

  private static uniqueId(deviceArg: IBleboxDeviceInfo, aliasArg: string): string {
    return `BleBox-${this.model(deviceArg)}-${deviceArg.id || 'unknown'}-${aliasArg}`;
  }

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

  private static humanize(valueArg: string): string {
    return valueArg.replace(/_/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase();
  }
}

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

const asArray = (valueArg: unknown): TBleboxJsonValue[] | undefined => {
  return Array.isArray(valueArg) ? valueArg as TBleboxJsonValue[] : undefined;
};

const toNumber = (valueArg: unknown): number | undefined => {
  if (typeof valueArg === 'number' && Number.isFinite(valueArg)) return valueArg;
  if (typeof valueArg === 'string' && valueArg.trim()) {
    const value = Number(valueArg);
    return Number.isFinite(value) ? value : undefined;
  }
  return undefined;
};

const primitiveId = (valueArg: unknown): string | number | undefined => {
  return typeof valueArg === 'string' || typeof valueArg === 'number' ? valueArg : undefined;
};

const primitiveNumberOrString = (valueArg: unknown): number | string | undefined => {
  return typeof valueArg === 'number' || typeof valueArg === 'string' ? valueArg : undefined;
};
