import * as plugins from '../../plugins.js';
import type { IIntegrationEntity, IIntegrationEvent, IServiceCallRequest, TEntityPlatform } from '../../core/types.js';
import type {
  ITplinkClientCommand,
  ITplinkConfig,
  ITplinkDevice,
  ITplinkEntityDescriptor,
  ITplinkEvent,
  ITplinkFeature,
  ITplinkManualEntry,
  ITplinkSnapshot,
  ITplinkStateRecord,
  TTplinkDeviceKind,
} from './tplink.types.js';
import { tplinkDefaultHttpPort } from './tplink.types.js';

const primaryControlKeys = new Set(['state', 'is_on', 'on', 'power', 'relay_state', 'device_on', 'light_on', 'brightness', 'dimmer', 'dimming', 'color_temperature', 'color_temp', 'color_temp_kelvin', 'hsv', 'rgb', 'rgb_color']);
const binarySensorKeys = new Set(['overheated', 'overloaded', 'battery_low', 'cloud_connection', 'temperature_warning', 'humidity_warning', 'is_open', 'water_alert', 'motion_detected', 'occupancy', 'tamper_detection', 'person_detection', 'baby_cry_detection']);
const switchFeatureKeys = new Set(['led', 'auto_update_enabled', 'auto_off_enabled', 'smooth_transitions', 'fan_sleep_mode', 'child_lock', 'pir_enabled', 'motion_detection', 'person_detection', 'tamper_detection', 'baby_cry_detection', 'carpet_boost']);
const numberControlKeys = new Set(['smooth_transition_on', 'smooth_transition_off', 'auto_off_minutes', 'temperature_offset', 'pan_step', 'tilt_step', 'power_protection_threshold', 'clean_count', 'fan_speed_level', 'target_temperature']);
const sensorUnits: Record<string, string> = {
  current_consumption: 'W',
  current_power_w: 'W',
  power: 'W',
  voltage: 'V',
  current: 'A',
  consumption_today: 'kWh',
  consumption_total: 'kWh',
  consumption_this_month: 'kWh',
  today_energy_kwh: 'kWh',
  total_energy_kwh: 'kWh',
  temperature: 'C',
  humidity: '%',
  rssi: 'dBm',
  signal_level: 'dBm',
  battery_level: '%',
};

export class TplinkMapper {
  public static toSnapshot(configArg: ITplinkConfig, connectedArg?: boolean, eventsArg: ITplinkEvent[] = []): ITplinkSnapshot {
    const source = configArg.snapshot;
    const primaryDevice = this.primaryDevice(configArg, source);
    const devices = this.uniqueDevices([
      ...(source?.devices || []),
      ...(configArg.devices || []),
      ...(configArg.device ? [configArg.device] : []),
      ...(primaryDevice ? [primaryDevice] : []),
      ...this.devicesFromManualEntries(configArg.manualEntries || []),
    ]);
    const host = configArg.host || source?.host;
    const port = configArg.port || source?.port || tplinkDefaultHttpPort;

    return {
      connected: connectedArg ?? source?.connected ?? Boolean(source || devices.some((deviceArg) => this.hasState(deviceArg))),
      configured: Boolean(host || source || devices.length),
      host,
      port,
      alias: configArg.alias || configArg.name || source?.alias,
      model: configArg.model || source?.model,
      macAddress: configArg.macAddress || source?.macAddress,
      devices,
      entities: [...(source?.entities || []), ...(configArg.entities || [])],
      events: [...(source?.events || []), ...(configArg.events || []), ...eventsArg],
      transport: {
        protocol: source?.transport?.protocol || (host ? 'manual' : 'snapshot'),
        host,
        port,
        credentialsConfigured: Boolean(configArg.credentials || configArg.username || configArg.password || configArg.credentialsHash || source?.transport?.credentialsConfigured),
        connectionParameters: configArg.connectionParameters || source?.transport?.connectionParameters,
        legacyXorImplemented: false,
        encryptedLocalProtocolImplemented: false,
      },
      metadata: {
        ...source?.metadata,
        ...configArg.metadata,
        liveLocalWritesImplemented: false,
      },
    };
  }

  public static toDevices(snapshotArg: ITplinkSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    return this.allDevices(snapshotArg).map((deviceArg) => this.toDevice(deviceArg, snapshotArg));
  }

  public static toEntities(snapshotArg: ITplinkSnapshot): IIntegrationEntity[] {
    const entities: IIntegrationEntity[] = [];
    const usedIds = new Map<string, number>();
    const seen = new Set<string>();
    const addEntity = (entityArg: IIntegrationEntity | undefined) => {
      if (!entityArg || seen.has(entityArg.id)) {
        return;
      }
      seen.add(entityArg.id);
      entities.push(entityArg);
    };

    for (const descriptor of snapshotArg.entities) {
      addEntity(this.entityFromDescriptor(snapshotArg, descriptor, usedIds));
    }

    for (const device of this.allDevices(snapshotArg)) {
      const kind = this.deviceKind(device);
      const control = this.controlState(device);
      if (this.isLightKind(kind, device)) {
        addEntity(this.primaryLightEntity(device, control, usedIds));
      } else if (this.isSwitchKind(kind, device)) {
        addEntity(this.primarySwitchEntity(device, control, usedIds));
      }

      for (const property of this.propertiesForDevice(device)) {
        addEntity(this.entityForProperty(device, property, usedIds));
      }
    }

    return entities;
  }

  public static toIntegrationEvent(eventArg: ITplinkEvent): IIntegrationEvent {
    return {
      type: eventArg.type === 'command_failed' || eventArg.type === 'error' ? 'error' : 'state_changed',
      integrationDomain: 'tplink',
      deviceId: eventArg.deviceId,
      entityId: eventArg.entityId,
      data: eventArg,
      timestamp: eventArg.timestamp || Date.now(),
    };
  }

  public static commandForService(snapshotArg: ITplinkSnapshot, requestArg: IServiceCallRequest): ITplinkClientCommand | undefined {
    if (requestArg.domain === 'tplink' && requestArg.service === 'raw_command' && this.isRecord(requestArg.data?.payload)) {
      const targetEntity = this.findTargetEntity(snapshotArg, requestArg);
      return this.command(requestArg, targetEntity, this.findTargetDevice(snapshotArg, requestArg, targetEntity), 'raw_command', requestArg.data.payload as Record<string, unknown>);
    }

    const targetEntity = this.findTargetEntity(snapshotArg, requestArg);
    const targetDevice = this.findTargetDevice(snapshotArg, requestArg, targetEntity);
    if (!targetEntity && !targetDevice) {
      return undefined;
    }

    if (requestArg.service === 'turn_on' || requestArg.service === 'turn_off') {
      const payload: Record<string, unknown> = { state: requestArg.service === 'turn_on' };
      if (requestArg.service === 'turn_on' && (targetEntity?.platform === 'light' || requestArg.domain === 'light')) {
        this.applyLightServiceData(payload, requestArg);
      }
      return this.command(requestArg, targetEntity, targetDevice, 'set_state', payload, 'state', payload.state);
    }

    if (requestArg.service === 'set_brightness' || requestArg.service === 'set_percentage') {
      const percentage = this.percentageFromData(requestArg.data, requestArg.service);
      if (percentage === undefined) {
        return undefined;
      }
      const featureId = targetEntity?.platform === 'fan' || requestArg.domain === 'fan' ? 'fan_speed_level' : 'brightness';
      const payload = featureId === 'fan_speed_level'
        ? { [featureId]: percentage }
        : { state: percentage > 0, brightness: percentage };
      return this.command(requestArg, targetEntity, targetDevice, 'set_feature_value', payload, featureId, percentage);
    }

    if (requestArg.service === 'set_color_temp' || requestArg.service === 'set_color_temperature') {
      const kelvin = this.kelvinFromData(requestArg.data);
      if (kelvin === undefined) {
        return undefined;
      }
      return this.command(requestArg, targetEntity, targetDevice, 'set_feature_value', { color_temperature: kelvin }, 'color_temperature', kelvin);
    }

    if (requestArg.service === 'set_rgb_color' || requestArg.service === 'set_color') {
      const rgb = this.rgbFromData(requestArg.data, 'rgb_color') || this.rgbFromData(requestArg.data, 'rgb');
      if (!rgb) {
        return undefined;
      }
      const value = { r: rgb[0], g: rgb[1], b: rgb[2] };
      return this.command(requestArg, targetEntity, targetDevice, 'set_feature_value', { rgb: value }, 'rgb', value);
    }

    if (requestArg.service === 'set_value') {
      const value = requestArg.data?.value;
      const featureId = this.stringValue(requestArg.data?.featureId || requestArg.data?.feature_id || requestArg.data?.field || requestArg.data?.key || targetEntity?.attributes?.tplinkFeatureId);
      if (featureId === undefined || value === undefined) {
        return undefined;
      }
      return this.command(requestArg, targetEntity, targetDevice, 'set_feature_value', { [featureId]: value }, featureId, value);
    }

    if (requestArg.service === 'select_option') {
      const option = this.stringValue(requestArg.data?.option || requestArg.data?.value);
      const featureId = this.stringValue(targetEntity?.attributes?.tplinkFeatureId) || 'light_preset';
      if (!option) {
        return undefined;
      }
      return this.command(requestArg, targetEntity, targetDevice, 'set_feature_value', { [featureId]: option }, featureId, option);
    }

    if (requestArg.service === 'press') {
      const featureId = this.stringValue(targetEntity?.attributes?.tplinkFeatureId);
      return featureId ? this.command(requestArg, targetEntity, targetDevice, 'action', { [featureId]: true }, featureId, true) : undefined;
    }

    return undefined;
  }

  private static toDevice(deviceArg: ITplinkDevice, snapshotArg: ITplinkSnapshot): plugins.shxInterfaces.data.IDeviceDefinition {
    const updatedAt = deviceArg.updatedAt || new Date().toISOString();
    const kind = this.deviceKind(deviceArg);
    const control = this.controlState(deviceArg);
    const properties = this.propertiesForDevice(deviceArg);
    const features: plugins.shxInterfaces.data.IDeviceFeature[] = [
      { id: 'availability', capability: 'sensor', name: 'Availability', readable: true, writable: false },
    ];
    const state: plugins.shxInterfaces.data.IDeviceState[] = [
      { featureId: 'availability', value: deviceArg.available === false || deviceArg.online === false ? 'offline' : 'online', updatedAt },
    ];

    if (this.isLightKind(kind, deviceArg)) {
      features.push({ id: 'state', capability: 'light', name: 'Power', readable: true, writable: true });
      this.pushDeviceState(state, 'state', control.on, updatedAt);
      if (control.brightness !== undefined || this.hasFeature(deviceArg, 'brightness')) {
        features.push({ id: 'brightness', capability: 'light', name: 'Brightness', readable: true, writable: true, unit: '%' });
        this.pushDeviceState(state, 'brightness', control.brightness, updatedAt);
      }
      if (control.colorTemperature !== undefined || this.hasFeature(deviceArg, 'color_temperature')) {
        features.push({ id: 'color_temperature', capability: 'light', name: 'Color Temperature', readable: true, writable: true, unit: 'K' });
        this.pushDeviceState(state, 'color_temperature', control.colorTemperature, updatedAt);
      }
      if (control.rgb || this.hasFeature(deviceArg, 'hsv')) {
        features.push({ id: 'rgb', capability: 'light', name: 'RGB Color', readable: true, writable: true });
        this.pushDeviceState(state, 'rgb', control.rgb, updatedAt);
      }
    } else if (this.isSwitchKind(kind, deviceArg)) {
      features.push({ id: 'state', capability: 'switch', name: 'Power', readable: true, writable: true });
      this.pushDeviceState(state, 'state', control.on, updatedAt);
    }

    for (const property of properties) {
      if (this.shouldSkipDeviceProperty(property, kind)) {
        continue;
      }
      features.push(this.featureForProperty(property));
      this.pushDeviceState(state, property.key, property.value, updatedAt);
    }

    return {
      id: this.deviceId(deviceArg),
      integrationDomain: 'tplink',
      name: this.deviceName(deviceArg),
      protocol: 'unknown',
      manufacturer: deviceArg.manufacturer || 'TP-Link',
      model: deviceArg.model,
      online: deviceArg.available !== false && deviceArg.online !== false && (snapshotArg.connected || this.hasState(deviceArg) || Boolean(deviceArg.host)),
      features: this.uniqueFeatures(features),
      state,
      metadata: {
        ...deviceArg.metadata,
        host: deviceArg.host || snapshotArg.host,
        port: deviceArg.port || snapshotArg.port || tplinkDefaultHttpPort,
        macAddress: this.mac(deviceArg),
        deviceId: deviceArg.deviceId || deviceArg.device_id || deviceArg.id,
        kind,
        hwVersion: deviceArg.hwVersion || deviceArg.hardwareVersion,
        swVersion: deviceArg.swVersion || deviceArg.firmwareVersion,
        liveLocalWritesImplemented: false,
      },
    };
  }

  private static primaryLightEntity(deviceArg: ITplinkDevice, controlArg: ReturnType<typeof TplinkMapper.controlState>, usedIdsArg: Map<string, number>): IIntegrationEntity {
    const name = this.deviceName(deviceArg);
    return this.entity('light', name, this.deviceId(deviceArg), this.uniqueId('light', deviceArg), controlArg.on ? 'on' : 'off', usedIdsArg, {
      ...this.baseAttributes(deviceArg),
      brightness: controlArg.brightness,
      brightness255: controlArg.brightness === undefined ? undefined : this.clamp(Math.round(controlArg.brightness / 100 * 255), 0, 255),
      colorTemperatureKelvin: controlArg.colorTemperature,
      rgbColor: controlArg.rgb ? [controlArg.rgb.r, controlArg.rgb.g, controlArg.rgb.b] : undefined,
      effect: controlArg.effect,
      writable: true,
    }, deviceArg.available !== false && deviceArg.online !== false);
  }

  private static primarySwitchEntity(deviceArg: ITplinkDevice, controlArg: ReturnType<typeof TplinkMapper.controlState>, usedIdsArg: Map<string, number>): IIntegrationEntity {
    const name = this.deviceName(deviceArg);
    return this.entity('switch', name, this.deviceId(deviceArg), this.uniqueId('switch', deviceArg), controlArg.on ? 'on' : 'off', usedIdsArg, {
      ...this.baseAttributes(deviceArg),
      writable: true,
    }, deviceArg.available !== false && deviceArg.online !== false);
  }

  private static entityForProperty(deviceArg: ITplinkDevice, propertyArg: ITplinkFeature & { key: string }, usedIdsArg: Map<string, number>): IIntegrationEntity | undefined {
    const kind = this.deviceKind(deviceArg);
    if (this.shouldSkipEntityProperty(propertyArg, kind)) {
      return undefined;
    }
    const platform = this.platformForProperty(propertyArg, kind);
    const name = platform === 'button'
      ? `${this.deviceName(deviceArg)} ${this.title(propertyArg.name || propertyArg.key)}`
      : `${this.deviceName(deviceArg)} ${this.title(propertyArg.name || propertyArg.key)}`;
    const state = this.entityState(propertyArg.value, platform);
    return this.entity(platform, name, this.deviceId(deviceArg), `${this.uniqueId(platform, deviceArg)}_${this.slug(propertyArg.key)}`, state, usedIdsArg, {
      ...this.baseAttributes(deviceArg),
      tplinkFeatureId: propertyArg.key,
      deviceClass: propertyArg.deviceClass,
      unit: propertyArg.unit || sensorUnits[propertyArg.key],
      writable: propertyArg.writable === true,
      min: propertyArg.minimumValue ?? propertyArg.min,
      max: propertyArg.maximumValue ?? propertyArg.max,
      options: propertyArg.choices,
    }, propertyArg.available !== false && deviceArg.available !== false && deviceArg.online !== false);
  }

  private static entityFromDescriptor(snapshotArg: ITplinkSnapshot, entityArg: ITplinkEntityDescriptor, usedIdsArg: Map<string, number>): IIntegrationEntity {
    const platform = this.corePlatform(entityArg.platform || 'sensor');
    const name = entityArg.name || entityArg.entityId || entityArg.id || 'TP-Link entity';
    return this.entity(platform, name, this.entityDeviceId(snapshotArg, entityArg), entityArg.uniqueId || entityArg.unique_id || `tplink_${this.slug(entityArg.id || entityArg.entityId || name)}`, this.entityState(entityArg.state ?? entityArg.value, platform), usedIdsArg, {
      ...entityArg.attributes,
      tplinkFeatureId: entityArg.key,
      deviceClass: entityArg.deviceClass || entityArg.device_class,
      unit: entityArg.unit,
      writable: entityArg.writable === true,
    }, entityArg.available !== false, entityArg.entityId || entityArg.id);
  }

  private static command(requestArg: IServiceCallRequest, entityArg: IIntegrationEntity | undefined, deviceArg: ITplinkDevice | undefined, methodArg: string, payloadArg: Record<string, unknown>, featureIdArg?: string, valueArg?: unknown): ITplinkClientCommand {
    return {
      type: `tplink.${methodArg}`,
      service: requestArg.service,
      method: methodArg,
      platform: entityArg?.platform || requestArg.domain,
      protocol: 'snapshot',
      deviceId: entityArg?.deviceId || (deviceArg ? this.deviceId(deviceArg) : requestArg.target.deviceId),
      entityId: entityArg?.id || requestArg.target.entityId,
      uniqueId: entityArg?.uniqueId,
      featureId: featureIdArg,
      value: valueArg,
      target: requestArg.target,
      payload: { ...payloadArg, data: requestArg.data || {} },
    };
  }

  private static applyLightServiceData(payloadArg: Record<string, unknown>, requestArg: IServiceCallRequest): void {
    const brightness = this.percentageFromData(requestArg.data, 'turn_on');
    if (brightness !== undefined) {
      payloadArg.brightness = brightness;
    }
    const kelvin = this.kelvinFromData(requestArg.data);
    if (kelvin !== undefined) {
      payloadArg.color_temperature = kelvin;
    }
    const rgb = this.rgbFromData(requestArg.data, 'rgb_color') || this.rgbFromData(requestArg.data, 'rgb');
    if (rgb) {
      payloadArg.rgb = { r: rgb[0], g: rgb[1], b: rgb[2] };
    }
  }

  private static primaryDevice(configArg: ITplinkConfig, sourceArg?: ITplinkSnapshot): ITplinkDevice | undefined {
    if (!configArg.host && !configArg.model && !configArg.alias && !configArg.name && !configArg.state && !configArg.features && !configArg.modules && !configArg.children?.length) {
      return undefined;
    }
    return {
      id: configArg.deviceId || configArg.macAddress || configArg.host || 'configured',
      host: configArg.host || sourceArg?.host,
      port: configArg.port || sourceArg?.port || tplinkDefaultHttpPort,
      alias: configArg.alias || configArg.name,
      model: configArg.model,
      macAddress: configArg.macAddress,
      type: configArg.deviceType,
      brand: configArg.brand,
      state: configArg.state,
      features: configArg.features,
      modules: configArg.modules,
      children: configArg.children,
      metadata: configArg.metadata,
    };
  }

  private static devicesFromManualEntries(entriesArg: ITplinkManualEntry[]): ITplinkDevice[] {
    const devices: ITplinkDevice[] = [];
    for (const entry of entriesArg) {
      if (entry.snapshot) {
        devices.push(...entry.snapshot.devices);
      }
      if (entry.devices) {
        devices.push(...entry.devices);
      }
      if (entry.device) {
        devices.push(entry.device);
      }
      if (!entry.snapshot && !entry.devices?.length && !entry.device && (entry.host || entry.model || entry.alias || entry.name || entry.state || entry.features)) {
        devices.push({
          id: entry.deviceId || entry.id || entry.macAddress || entry.mac || entry.host,
          host: entry.host,
          port: entry.port || tplinkDefaultHttpPort,
          macAddress: entry.macAddress || entry.mac,
          alias: entry.alias || entry.name,
          model: entry.model,
          manufacturer: entry.manufacturer,
          brand: entry.brand,
          type: entry.deviceType,
          state: entry.state,
          features: entry.features,
          metadata: entry.metadata,
        });
      }
    }
    return devices;
  }

  private static allDevices(snapshotArg: ITplinkSnapshot): ITplinkDevice[] {
    const devices: ITplinkDevice[] = [];
    const visit = (deviceArg: ITplinkDevice, parentArg?: ITplinkDevice) => {
      devices.push(parentArg && !deviceArg.parentId ? { ...deviceArg, parentId: this.deviceId(parentArg), host: deviceArg.host || parentArg.host, port: deviceArg.port || parentArg.port } : deviceArg);
      for (const child of deviceArg.children || []) {
        visit(child, deviceArg);
      }
    };
    for (const device of snapshotArg.devices) {
      visit(device);
    }
    return this.uniqueDevices(devices);
  }

  private static propertiesForDevice(deviceArg: ITplinkDevice): Array<ITplinkFeature & { key: string }> {
    const properties: Array<ITplinkFeature & { key: string }> = [];
    const state = this.normalizedState(deviceArg);
    const existing = new Set<string>();
    for (const feature of this.featureList(deviceArg)) {
      const key = feature.key || feature.id;
      if (!key) {
        continue;
      }
      existing.add(key);
      properties.push({ ...feature, key, value: feature.value ?? state[key] });
    }
    for (const [key, value] of Object.entries(state)) {
      if (existing.has(key) || value === undefined || this.isRecord(value) && !['rgb', 'rgb_color', 'hsv'].includes(key)) {
        continue;
      }
      properties.push({ key, id: key, name: this.title(key), value, readable: true, writable: this.isWritableStateKey(key), unit: sensorUnits[key] });
    }
    return properties;
  }

  private static featureList(deviceArg: ITplinkDevice): ITplinkFeature[] {
    const features: ITplinkFeature[] = [];
    if (Array.isArray(deviceArg.features)) {
      features.push(...deviceArg.features);
    } else if (this.isRecord(deviceArg.features)) {
      for (const [key, value] of Object.entries(deviceArg.features)) {
        if (this.isRecord(value)) {
          features.push({ ...value, key: this.stringValue(value.key) || this.stringValue(value.id) || key } as ITplinkFeature);
        }
      }
    }
    for (const module of Object.values(deviceArg.modules || {})) {
      const moduleFeatures = module.features;
      if (Array.isArray(moduleFeatures)) {
        features.push(...moduleFeatures);
      } else if (this.isRecord(moduleFeatures)) {
        for (const [key, value] of Object.entries(moduleFeatures)) {
          if (this.isRecord(value)) {
            features.push({ ...value, key: this.stringValue(value.key) || this.stringValue(value.id) || key } as ITplinkFeature);
          }
        }
      }
    }
    if (deviceArg.sensors) {
      features.push(...deviceArg.sensors);
    }
    return features;
  }

  private static normalizedState(deviceArg: ITplinkDevice): ITplinkStateRecord {
    const sysInfo = this.asRecord(deviceArg.sysInfo || deviceArg.systemInfo);
    const lightState = this.asRecord(deviceArg.lightState || sysInfo.light_state);
    const emeter = this.asRecord(deviceArg.emeter || deviceArg.emeter_realtime || deviceArg.emeterRealtime);
    const state: ITplinkStateRecord = {
      ...sysInfo,
      ...emeter,
      ...lightState,
      ...this.asRecord(deviceArg.state),
    };
    if (state.state === undefined && state.relay_state !== undefined) {
      state.state = this.boolish(state.relay_state);
    }
    if (state.state === undefined && state.on_off !== undefined) {
      state.state = this.boolish(state.on_off);
    }
    if (state.brightness === undefined && state.dimmer !== undefined) {
      state.brightness = state.dimmer;
    }
    if (state.current_consumption === undefined && state.current_power_w !== undefined) {
      state.current_consumption = state.current_power_w;
    }
    return state;
  }

  private static controlState(deviceArg: ITplinkDevice): { on?: boolean; brightness?: number; colorTemperature?: number; rgb?: { r: number; g: number; b: number }; effect?: string } {
    const state = this.normalizedState(deviceArg);
    const on = this.boolish(state.state ?? state.is_on ?? state.on ?? state.device_on ?? state.light_on ?? state.relay_state ?? state.power);
    const brightness = this.numberValue(state.brightness ?? state.brightness_pct ?? state.dimmer ?? state.dimming);
    const colorTemperature = this.numberValue(state.color_temperature ?? state.colorTemperature ?? state.color_temp_kelvin ?? state.color_temp);
    return {
      on,
      brightness: brightness === undefined ? undefined : this.clamp(Math.round(brightness), 0, 100),
      colorTemperature: colorTemperature === undefined ? undefined : Math.round(colorTemperature),
      rgb: this.rgbValue(state.rgb ?? state.rgb_color ?? state.rgbColor ?? state.hsv),
      effect: this.stringValue(state.light_effect ?? state.effect),
    };
  }

  private static featureForProperty(propertyArg: ITplinkFeature & { key: string }): plugins.shxInterfaces.data.IDeviceFeature {
    const platform = this.platformForProperty(propertyArg, 'unknown');
    return {
      id: this.slug(propertyArg.key),
      capability: platform === 'light' ? 'light' : platform === 'switch' || platform === 'button' ? 'switch' : 'sensor',
      name: propertyArg.name || this.title(propertyArg.key),
      readable: propertyArg.readable !== false,
      writable: propertyArg.writable === true,
      unit: propertyArg.unit || sensorUnits[propertyArg.key],
    };
  }

  private static platformForProperty(propertyArg: ITplinkFeature & { key: string }, kindArg: TTplinkDeviceKind): TEntityPlatform {
    const explicit = this.stringValue(propertyArg.platform || propertyArg.type)?.toLowerCase();
    if (explicit === 'binarysensor') {
      return 'binary_sensor';
    }
    if (explicit && ['light', 'switch', 'sensor', 'binary_sensor', 'button', 'number', 'select', 'fan', 'climate'].includes(explicit)) {
      return explicit === 'choice' ? 'select' : explicit as TEntityPlatform;
    }
    if (explicit === 'choice' || propertyArg.choices?.length) {
      return 'select';
    }
    if (explicit === 'action') {
      return 'button';
    }
    if (binarySensorKeys.has(propertyArg.key)) {
      return 'binary_sensor';
    }
    if (switchFeatureKeys.has(propertyArg.key) || typeof propertyArg.value === 'boolean' && propertyArg.writable === true) {
      return 'switch';
    }
    if (numberControlKeys.has(propertyArg.key) || typeof propertyArg.value === 'number' && propertyArg.writable === true) {
      return 'number';
    }
    if (this.isLightKind(kindArg, undefined) && ['brightness', 'color_temperature', 'color_temp', 'hsv', 'rgb'].includes(propertyArg.key)) {
      return 'light';
    }
    return 'sensor';
  }

  private static shouldSkipDeviceProperty(propertyArg: ITplinkFeature & { key: string }, kindArg: TTplinkDeviceKind): boolean {
    return primaryControlKeys.has(propertyArg.key) && (this.isLightKind(kindArg, undefined) || this.isSwitchKind(kindArg, undefined));
  }

  private static shouldSkipEntityProperty(propertyArg: ITplinkFeature & { key: string }, kindArg: TTplinkDeviceKind): boolean {
    if (primaryControlKeys.has(propertyArg.key) && (this.isLightKind(kindArg, undefined) || this.isSwitchKind(kindArg, undefined))) {
      return true;
    }
    if (propertyArg.key === 'current_consumption') {
      return false;
    }
    if (!propertyArg.unit && propertyArg.writable !== true && this.platformForProperty(propertyArg, kindArg) === 'sensor' && !sensorUnits[propertyArg.key]) {
      return !['rssi', 'signal_level', 'ssid', 'battery_level', 'temperature', 'humidity'].includes(propertyArg.key);
    }
    return false;
  }

  private static entity(platformArg: TEntityPlatform, nameArg: string, deviceIdArg: string, uniqueIdArg: string, stateArg: unknown, usedIdsArg: Map<string, number>, attributesArg: Record<string, unknown>, availableArg: boolean, explicitEntityIdArg?: string): IIntegrationEntity {
    return {
      id: explicitEntityIdArg || this.entityId(platformArg, nameArg, usedIdsArg),
      uniqueId: uniqueIdArg,
      integrationDomain: 'tplink',
      deviceId: deviceIdArg,
      platform: platformArg,
      name: nameArg,
      state: stateArg,
      attributes: attributesArg,
      available: availableArg,
    };
  }

  private static findTargetEntity(snapshotArg: ITplinkSnapshot, requestArg: IServiceCallRequest): IIntegrationEntity | undefined {
    const entities = this.toEntities(snapshotArg);
    if (requestArg.target.entityId) {
      return entities.find((entityArg) => entityArg.id === requestArg.target.entityId || entityArg.uniqueId === requestArg.target.entityId);
    }
    if (requestArg.target.deviceId) {
      return entities.find((entityArg) => entityArg.deviceId === requestArg.target.deviceId && entityArg.platform === requestArg.domain)
        || entities.find((entityArg) => entityArg.deviceId === requestArg.target.deviceId && Boolean(entityArg.attributes?.writable))
        || entities.find((entityArg) => entityArg.deviceId === requestArg.target.deviceId);
    }
    return entities.find((entityArg) => entityArg.platform === requestArg.domain)
      || entities.find((entityArg) => Boolean(entityArg.attributes?.writable));
  }

  private static findTargetDevice(snapshotArg: ITplinkSnapshot, requestArg: IServiceCallRequest, entityArg?: IIntegrationEntity): ITplinkDevice | undefined {
    const deviceId = requestArg.target.deviceId || entityArg?.deviceId;
    const devices = this.allDevices(snapshotArg);
    if (deviceId) {
      return devices.find((deviceArg) => this.deviceId(deviceArg) === deviceId || this.rawDeviceKey(deviceArg) === deviceId);
    }
    return devices[0];
  }

  private static entityDeviceId(snapshotArg: ITplinkSnapshot, entityArg: ITplinkEntityDescriptor): string {
    if (entityArg.deviceId || entityArg.device_id) {
      return entityArg.deviceId || entityArg.device_id as string;
    }
    const device = this.allDevices(snapshotArg)[0];
    return device ? this.deviceId(device) : 'tplink.device.unknown';
  }

  private static deviceKind(deviceArg?: ITplinkDevice): TTplinkDeviceKind {
    if (!deviceArg) {
      return 'unknown';
    }
    const explicit = this.stringValue(deviceArg.kind || deviceArg.type || deviceArg.deviceType || deviceArg.device_type)?.toLowerCase().replace(/\s+/g, '_');
    if (explicit) {
      if (explicit.includes('lightstrip') || explicit.includes('light_strip')) {
        return 'light_strip';
      }
      if (explicit.includes('wallswitch') || explicit.includes('switch')) {
        return 'switch';
      }
      if (explicit.includes('bulb')) {
        return 'bulb';
      }
      if (explicit.includes('plug') || explicit.includes('outlet')) {
        return 'plug';
      }
      return explicit;
    }
    const text = [deviceArg.model, deviceArg.alias, deviceArg.name].filter((valueArg): valueArg is string => typeof valueArg === 'string').join(' ').toLowerCase();
    if (/\b(kl|lb|l5|l6)\d/i.test(deviceArg.model || '') || text.includes('bulb') || text.includes('lamp')) {
      return 'bulb';
    }
    if (/\b(kl4|l9)\d/i.test(deviceArg.model || '') || text.includes('light strip') || text.includes('lightstrip')) {
      return 'light_strip';
    }
    if (/\b(hs3|kp3|ep4|p3|p2|tp25)/i.test(deviceArg.model || '') || text.includes('strip')) {
      return 'strip';
    }
    if (text.includes('switch') || text.includes('dimmer') || /\b(ks|hs2|s5|ts15)/i.test(deviceArg.model || '')) {
      return text.includes('dimmer') ? 'dimmer' : 'switch';
    }
    if (text.includes('sensor') || text.includes('motion') || text.includes('door') || text.includes('water') || /^t(100|110|300|310|315)/i.test(deviceArg.model || '')) {
      return 'sensor';
    }
    if (text.includes('plug') || text.includes('socket') || /\b(hs1|kp1|ep|p1|tp1)/i.test(deviceArg.model || '')) {
      return 'plug';
    }
    return this.controlState(deviceArg).on !== undefined ? 'switch' : 'sensor';
  }

  private static isLightKind(kindArg: TTplinkDeviceKind, deviceArg?: ITplinkDevice): boolean {
    const kind = String(kindArg).toLowerCase();
    return kind === 'bulb' || kind === 'light_strip' || kind === 'dimmer' || kind === 'light' || this.hasModule(deviceArg, 'light');
  }

  private static isSwitchKind(kindArg: TTplinkDeviceKind, deviceArg?: ITplinkDevice): boolean {
    const kind = String(kindArg).toLowerCase();
    return kind === 'plug' || kind === 'strip' || kind === 'switch' || kind === 'outlet' || kind === 'wall_switch';
  }

  private static hasModule(deviceArg: ITplinkDevice | undefined, nameArg: string): boolean {
    return Boolean(deviceArg?.modules && Object.keys(deviceArg.modules).some((keyArg) => keyArg.toLowerCase() === nameArg.toLowerCase()));
  }

  private static hasFeature(deviceArg: ITplinkDevice, keyArg: string): boolean {
    return this.featureList(deviceArg).some((featureArg) => featureArg.key === keyArg || featureArg.id === keyArg);
  }

  private static hasState(deviceArg: ITplinkDevice): boolean {
    return Boolean(Object.keys(this.normalizedState(deviceArg)).length || this.featureList(deviceArg).length || deviceArg.children?.length);
  }

  private static uniqueDevices(devicesArg: ITplinkDevice[]): ITplinkDevice[] {
    const devices = new Map<string, ITplinkDevice>();
    for (const device of devicesArg) {
      devices.set(this.rawDeviceKey(device), this.mergeDefined(devices.get(this.rawDeviceKey(device)) || {}, device));
    }
    return [...devices.values()];
  }

  private static mergeDefined(baseArg: ITplinkDevice, nextArg: ITplinkDevice): ITplinkDevice {
    const merged: ITplinkDevice = { ...baseArg };
    for (const [key, value] of Object.entries(nextArg)) {
      if (value !== undefined) {
        merged[key] = value;
      }
    }
    return merged;
  }

  private static uniqueFeatures(featuresArg: plugins.shxInterfaces.data.IDeviceFeature[]): plugins.shxInterfaces.data.IDeviceFeature[] {
    const features = new Map<string, plugins.shxInterfaces.data.IDeviceFeature>();
    for (const feature of featuresArg) {
      features.set(feature.id, { ...features.get(feature.id), ...feature });
    }
    return [...features.values()];
  }

  private static rawDeviceKey(deviceArg: ITplinkDevice): string {
    return this.mac(deviceArg) || deviceArg.deviceId || deviceArg.device_id || deviceArg.id || deviceArg.host || this.deviceName(deviceArg);
  }

  private static deviceId(deviceArg: ITplinkDevice): string {
    return `tplink.device.${this.slug(this.rawDeviceKey(deviceArg))}`;
  }

  private static uniqueId(platformArg: string, deviceArg: ITplinkDevice): string {
    return `tplink_${platformArg}_${this.slug(this.rawDeviceKey(deviceArg))}`;
  }

  private static deviceName(deviceArg: ITplinkDevice): string {
    return deviceArg.alias || deviceArg.name || deviceArg.model || (this.mac(deviceArg) ? `TP-Link ${this.shortMac(this.mac(deviceArg))}` : 'TP-Link device');
  }

  private static mac(deviceArg: ITplinkDevice): string | undefined {
    return this.normalizeMac(deviceArg.macAddress || deviceArg.mac || this.stringValue(deviceArg.sysInfo?.mac) || this.stringValue(deviceArg.systemInfo?.mac));
  }

  private static baseAttributes(deviceArg: ITplinkDevice): Record<string, unknown> {
    return {
      tplinkDeviceId: this.deviceId(deviceArg),
      tplinkRawDeviceId: deviceArg.deviceId || deviceArg.device_id || deviceArg.id,
      tplinkHost: deviceArg.host,
      tplinkPort: deviceArg.port || tplinkDefaultHttpPort,
      tplinkMac: this.mac(deviceArg),
      model: deviceArg.model,
      liveLocalWritesImplemented: false,
    };
  }

  private static entityId(platformArg: TEntityPlatform, nameArg: string, usedIdsArg: Map<string, number>): string {
    const base = `${platformArg}.${this.slug(nameArg)}`;
    const count = usedIdsArg.get(base) || 0;
    usedIdsArg.set(base, count + 1);
    return count ? `${base}_${count + 1}` : base;
  }

  private static corePlatform(platformArg: TEntityPlatform | string): TEntityPlatform {
    const platform = platformArg.toLowerCase();
    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';
  }

  private static entityState(valueArg: unknown, platformArg: TEntityPlatform): unknown {
    if (platformArg === 'light' || platformArg === 'switch' || platformArg === 'fan' || platformArg === 'binary_sensor') {
      const value = this.boolish(valueArg);
      return value === undefined ? valueArg ?? 'unknown' : value ? 'on' : 'off';
    }
    return valueArg ?? 'unknown';
  }

  private static pushDeviceState(stateArg: plugins.shxInterfaces.data.IDeviceState[], featureIdArg: string, valueArg: unknown, updatedAtArg: string): void {
    if (valueArg === undefined) {
      return;
    }
    stateArg.push({ featureId: this.slug(featureIdArg), value: this.deviceStateValue(valueArg), updatedAt: updatedAtArg });
  }

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

  private static isWritableStateKey(keyArg: string): boolean {
    return primaryControlKeys.has(keyArg) || switchFeatureKeys.has(keyArg) || numberControlKeys.has(keyArg);
  }

  private static percentageFromData(dataArg: Record<string, unknown> | undefined, serviceArg: string): number | undefined {
    const pct = this.numberFromData(dataArg, ['percentage', 'brightness_pct']);
    if (pct !== undefined) {
      return this.clamp(Math.round(pct), 0, 100);
    }
    const brightness = this.numberFromData(dataArg, serviceArg === 'set_brightness' ? ['brightness', 'value'] : ['brightness']);
    return brightness === undefined ? undefined : this.clamp(Math.round(brightness / 255 * 100), 0, 100);
  }

  private static kelvinFromData(dataArg: Record<string, unknown> | undefined): number | undefined {
    const direct = this.numberFromData(dataArg, ['color_temp_kelvin', 'kelvin', 'color_temperature']);
    if (direct !== undefined) {
      return Math.round(direct);
    }
    const mired = this.numberFromData(dataArg, ['color_temp', 'color_temp_mired']);
    return mired && mired > 0 ? Math.round(1000000 / mired) : undefined;
  }

  private static rgbFromData(dataArg: Record<string, unknown> | undefined, keyArg: string): number[] | undefined {
    const value = dataArg?.[keyArg];
    if (!Array.isArray(value) || value.length < 3) {
      return undefined;
    }
    const numbers = value.slice(0, 3).map((valueArg) => typeof valueArg === 'number' && Number.isFinite(valueArg) ? this.clamp(Math.round(valueArg), 0, 255) : undefined);
    return numbers.every((valueArg) => valueArg !== undefined) ? numbers as number[] : undefined;
  }

  private static rgbValue(valueArg: unknown): { r: number; g: number; b: number } | undefined {
    if (Array.isArray(valueArg) && valueArg.length >= 3) {
      const [r, g, b] = valueArg;
      return typeof r === 'number' && typeof g === 'number' && typeof b === 'number' ? { r, g, b } : undefined;
    }
    if (this.isRecord(valueArg)) {
      const r = this.numberValue(valueArg.r ?? valueArg.red);
      const g = this.numberValue(valueArg.g ?? valueArg.green);
      const b = this.numberValue(valueArg.b ?? valueArg.blue);
      return r !== undefined && g !== undefined && b !== undefined ? { r, g, b } : undefined;
    }
    return undefined;
  }

  private static boolish(valueArg: unknown): boolean | undefined {
    if (typeof valueArg === 'boolean') {
      return valueArg;
    }
    if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
      return valueArg > 0;
    }
    if (typeof valueArg === 'string') {
      const value = valueArg.toLowerCase();
      if (['on', 'true', '1', 'yes', 'open'].includes(value)) {
        return true;
      }
      if (['off', 'false', '0', 'no', 'closed'].includes(value)) {
        return false;
      }
    }
    return undefined;
  }

  private static valueFromData(dataArg: Record<string, unknown> | undefined, keysArg: string[]): unknown {
    for (const key of keysArg) {
      if (dataArg && key in dataArg) {
        return dataArg[key];
      }
    }
    return undefined;
  }

  private static numberFromData(dataArg: Record<string, unknown> | undefined, keysArg: string[]): number | undefined {
    return this.numberValue(this.valueFromData(dataArg, keysArg));
  }

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

  private static stringValue(valueArg: unknown): string | undefined {
    return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
  }

  private static asRecord(valueArg: unknown): Record<string, unknown> {
    return this.isRecord(valueArg) ? valueArg : {};
  }

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

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

  private static shortMac(valueArg?: string): string {
    return (valueArg || '').replace(/[^0-9a-f]/gi, '').slice(-6).toUpperCase();
  }

  private static title(valueArg: string): string {
    return valueArg.replace(/_/g, ' ').replace(/\b\w/g, (matchArg) => matchArg.toUpperCase());
  }

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

  private static clamp(valueArg: number, minArg: number, maxArg: number): number {
    return Math.max(minArg, Math.min(maxArg, valueArg));
  }
}
