import * as plugins from '../../plugins.js';
import type { IIntegrationEntity, IServiceCallRequest, TEntityPlatform } from '../../core/types.js';
import type {
  IFreeboxCommandRequest,
  IFreeboxConfig,
  IFreeboxDiskSnapshot,
  IFreeboxHomeNodeSnapshot,
  IFreeboxLanHostSnapshot,
  IFreeboxPartitionSnapshot,
  IFreeboxRaidSnapshot,
  IFreeboxRawData,
  IFreeboxRouterSnapshot,
  IFreeboxSensorSnapshot,
  IFreeboxSnapshot,
  TFreeboxProtocol,
  TFreeboxSnapshotSource,
} from './freebox.types.js';
import { freeboxDefaultApiBaseUrl, freeboxDefaultApiVersion, freeboxDefaultPort, freeboxDisplayName, freeboxDomain, freeboxHomeCategories, freeboxHomeCategoryModels, freeboxManufacturer } from './freebox.types.js';

interface IFreeboxSnapshotOptions {
  config: IFreeboxConfig;
  rawData?: Partial<IFreeboxRawData>;
  online?: boolean;
  source?: TFreeboxSnapshotSource;
  error?: string;
}

interface IFreeboxEntityOptions {
  platform: TEntityPlatform;
  key: string;
  name: string;
  state: unknown;
  available: boolean;
  deviceId: string;
  attributes?: Record<string, unknown>;
}

interface IEndpointInfo {
  protocol?: TFreeboxProtocol;
  host?: string;
  port?: number;
}

const sensorNames: Record<string, string> = {
  rate_down: 'Download Speed',
  rate_up: 'Upload Speed',
  bandwidth_down: 'Download Bandwidth',
  bandwidth_up: 'Upload Bandwidth',
  bytes_down: 'Downloaded Bytes',
  bytes_up: 'Uploaded Bytes',
  temp_cpum: 'CPU-M Temperature',
  temp_cpub: 'CPU-B Temperature',
  temp_sw: 'Switch Temperature',
  fan_rpm: 'Fan Speed',
  uptime_val: 'Uptime',
  missed_calls: 'Missed Calls',
  partition_free_space: 'Free Space',
  battery: 'Battery',
};

const alarmStatus: Record<string, string> = {
  alarm1_arming: 'arming',
  alarm2_arming: 'arming',
  alarm1_armed: 'armed_away',
  alarm2_armed: 'armed_home',
  alarm1_alert_timer: 'triggered',
  alarm2_alert_timer: 'triggered',
  alert: 'triggered',
  idle: 'disarmed',
};

export class FreeboxMapper {
  public static toSnapshot(optionsArg: IFreeboxSnapshotOptions): IFreeboxSnapshot {
    if (optionsArg.config.snapshot && !optionsArg.rawData) {
      return this.normalizeSnapshot(this.clone(optionsArg.config.snapshot), optionsArg.config, optionsArg.source || 'snapshot', optionsArg.error);
    }

    const rawData = this.rawData(optionsArg.config, optionsArg.rawData);
    const endpoint = parseEndpoint(optionsArg.config.url || optionsArg.config.host, optionsArg.config.port, optionsArg.config.protocol);
    const router = this.router(optionsArg.config, rawData, endpoint);
    const online = optionsArg.online ?? optionsArg.config.online ?? this.hasUsableRawData(rawData);
    const source = optionsArg.source || (this.hasUsableRawData(rawData) ? 'http' : 'runtime');
    const sensors = this.sensors(router, rawData, online);
    const lanHosts = this.lanHosts(rawData, router);
    const disks = this.disks(rawData);
    const raids = this.raids(rawData);
    const homeNodes = this.homeNodes(rawData);

    return this.normalizeSnapshot({
      router,
      sensors,
      lanHosts,
      disks,
      raids,
      homeNodes,
      wifiConfig: recordValue(rawData.wifiConfig),
      callLog: arrayRecords(rawData.callLog),
      rawData: this.hasUsableRawData(rawData) ? rawData : undefined,
      capabilities: {
        localRead: Boolean(optionsArg.config.client || endpoint.host || optionsArg.config.snapshot || optionsArg.config.rawData || this.hasUsableRawData(rawData)),
        localControl: this.hasLocalControl(optionsArg.config, endpoint),
        authenticated: Boolean(optionsArg.config.sessionToken || optionsArg.config.appToken || optionsArg.config.token),
        wifiControl: this.hasLocalControl(optionsArg.config, endpoint),
        homeControl: this.hasLocalControl(optionsArg.config, endpoint),
        callControl: this.hasLocalControl(optionsArg.config, endpoint),
        reboot: this.hasLocalControl(optionsArg.config, endpoint),
      },
      online,
      updatedAt: rawData.fetchedAt || new Date().toISOString(),
      source,
      error: optionsArg.error,
    }, optionsArg.config, source, optionsArg.error);
  }

  public static toDevices(snapshotArg: IFreeboxSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
    const routerSensors = snapshotArg.sensors.filter((sensorArg) => sensorArg.deviceId === snapshotArg.router.id);
    const devices: plugins.shxInterfaces.data.IDeviceDefinition[] = [{
      id: snapshotArg.router.id,
      integrationDomain: freeboxDomain,
      name: snapshotArg.router.name,
      protocol: snapshotArg.router.host ? 'http' : 'unknown',
      manufacturer: snapshotArg.router.manufacturer,
      model: snapshotArg.router.model || snapshotArg.router.modelId || freeboxDisplayName,
      online: snapshotArg.online,
      features: [
        { id: 'online', capability: 'sensor', name: 'Online', readable: true, writable: false },
        ...routerSensors.map((sensorArg) => ({ id: sensorArg.id, capability: this.capabilityForSensor(sensorArg), name: sensorArg.name, readable: true, writable: false, unit: sensorArg.unit })),
        { id: 'wifi_enabled', capability: 'switch', name: 'WiFi', readable: true, writable: snapshotArg.capabilities.wifiControl },
      ],
      state: [
        { featureId: 'online', value: snapshotArg.online, updatedAt },
        ...routerSensors.map((sensorArg) => ({ featureId: sensorArg.id, value: deviceStateValue(sensorArg.value), updatedAt })),
        { featureId: 'wifi_enabled', value: booleanValue(snapshotArg.wifiConfig?.enabled) ?? null, updatedAt },
      ],
      metadata: cleanAttributes({
        macAddress: snapshotArg.router.macAddress,
        serialNumber: snapshotArg.router.serialNumber,
        firmwareVersion: snapshotArg.router.firmwareVersion,
        hardwareVersion: snapshotArg.router.hardwareVersion,
        modelId: snapshotArg.router.modelId,
        host: snapshotArg.router.host,
        port: snapshotArg.router.port,
        apiVersion: snapshotArg.router.apiVersion,
        apiBaseUrl: snapshotArg.router.apiBaseUrl,
        source: snapshotArg.source,
        error: snapshotArg.error,
      }),
    }];

    for (const host of snapshotArg.lanHosts) {
      devices.push({
        id: host.deviceId,
        integrationDomain: freeboxDomain,
        name: host.name,
        protocol: 'unknown',
        manufacturer: host.manufacturer,
        model: host.hostType,
        online: host.active,
        features: [{ id: 'connected', capability: 'sensor', name: 'Connected', readable: true, writable: false }],
        state: [{ featureId: 'connected', value: host.active, updatedAt }],
        metadata: cleanAttributes({ ...host.attributes, macAddress: host.macAddress, addresses: host.addresses, reachable: host.reachable, viaDevice: snapshotArg.router.id }),
      });
    }

    for (const disk of snapshotArg.disks) {
      const features: plugins.shxInterfaces.data.IDeviceFeature[] = [
        { id: 'online', capability: 'sensor', name: 'Online', readable: true, writable: false },
        ...disk.partitions.map((partitionArg) => ({ id: `partition_${slug(partitionArg.id)}_free_space`, capability: 'sensor' as const, name: `${partitionArg.label} Free Space`, readable: true, writable: false, unit: '%' })),
      ];
      if (disk.temperature !== undefined) {
        features.push({ id: 'temperature', capability: 'sensor', name: 'Temperature', readable: true, writable: false, unit: 'C' });
      }
      devices.push({
        id: disk.id,
        integrationDomain: freeboxDomain,
        name: disk.name,
        protocol: 'unknown',
        manufacturer: freeboxManufacturer,
        model: disk.model || disk.type || 'Storage Disk',
        online: snapshotArg.online && disk.state !== 'disabled' && disk.state !== 'error',
        features,
        state: [
          { featureId: 'online', value: snapshotArg.online && disk.state !== 'disabled' && disk.state !== 'error', updatedAt },
          ...disk.partitions.map((partitionArg) => ({ featureId: `partition_${slug(partitionArg.id)}_free_space`, value: partitionArg.freePercent ?? null, updatedAt })),
          ...(disk.temperature === undefined ? [] : [{ featureId: 'temperature', value: disk.temperature, updatedAt }]),
        ],
        metadata: cleanAttributes({ ...disk.attributes, diskId: disk.diskId, serialNumber: disk.serialNumber, firmwareVersion: disk.firmwareVersion, state: disk.state, type: disk.type, totalBytes: disk.totalBytes, viaDevice: snapshotArg.router.id }),
      });
    }

    for (const node of snapshotArg.homeNodes) {
      const nodeSensors = snapshotArg.sensors.filter((sensorArg) => sensorArg.deviceId === node.id);
      devices.push({
        id: node.id,
        integrationDomain: freeboxDomain,
        name: node.name,
        protocol: 'unknown',
        manufacturer: node.manufacturer,
        model: node.model || node.category,
        online: snapshotArg.online && node.status !== 'inactive',
        features: [
          { id: 'online', capability: 'sensor', name: 'Online', readable: true, writable: false },
          ...nodeSensors.map((sensorArg) => ({ id: sensorArg.id, capability: this.capabilityForSensor(sensorArg), name: sensorArg.name, readable: true, writable: false, unit: sensorArg.unit })),
          ...(node.category === freeboxHomeCategories.camera ? [{ id: 'stream', capability: 'camera' as const, name: 'Stream', readable: true, writable: false }] : []),
        ],
        state: [
          { featureId: 'online', value: snapshotArg.online && node.status !== 'inactive', updatedAt },
          ...nodeSensors.map((sensorArg) => ({ featureId: sensorArg.id, value: deviceStateValue(sensorArg.value), updatedAt })),
          ...(node.category === freeboxHomeCategories.camera ? [{ featureId: 'stream', value: node.streamUrl || null, updatedAt }] : []),
        ],
        metadata: cleanAttributes({ ...node.attributes, category: node.category, firmwareVersion: node.firmwareVersion, status: node.status, streamUrl: node.streamUrl, viaDevice: snapshotArg.router.id }),
      });
    }

    return devices;
  }

  public static toEntities(snapshotArg: IFreeboxSnapshot): IIntegrationEntity[] {
    const seen = new Map<string, number>();
    const entities: IIntegrationEntity[] = [];

    for (const sensor of snapshotArg.sensors) {
      const device = this.deviceName(snapshotArg, sensor.deviceId);
      entities.push(this.entity(snapshotArg, {
        platform: 'sensor',
        key: sensor.id,
        name: `${device} ${sensor.name}`,
        state: sensor.value ?? null,
        available: sensor.available && snapshotArg.online,
        deviceId: sensor.deviceId,
        attributes: cleanAttributes({
          ...sensor.attributes,
          key: sensor.id,
          kind: sensor.kind,
          nativeUnitOfMeasurement: sensor.unit,
          deviceClass: sensor.deviceClass,
          stateClass: sensor.stateClass,
          entityCategory: sensor.entityCategory,
        }),
      }, seen));
    }

    entities.push(this.entity(snapshotArg, {
      platform: 'switch',
      key: 'wifi',
      name: 'Freebox WiFi',
      state: onOffState(snapshotArg.wifiConfig?.enabled),
      available: snapshotArg.online && Boolean(snapshotArg.wifiConfig),
      deviceId: snapshotArg.router.id,
      attributes: cleanAttributes({ entityCategory: 'config', action: 'set_wifi_enabled', enabled: booleanValue(snapshotArg.wifiConfig?.enabled) }),
    }, seen));

    entities.push(this.entity(snapshotArg, {
      platform: 'button',
      key: 'reboot',
      name: 'Reboot Freebox',
      state: 'available',
      available: snapshotArg.online && snapshotArg.capabilities.reboot,
      deviceId: snapshotArg.router.id,
      attributes: { entityCategory: 'config', action: 'reboot' },
    }, seen));

    entities.push(this.entity(snapshotArg, {
      platform: 'button',
      key: 'mark_calls_as_read',
      name: 'Mark calls as read',
      state: 'available',
      available: snapshotArg.online && snapshotArg.capabilities.callControl,
      deviceId: snapshotArg.router.id,
      attributes: { entityCategory: 'diagnostic', action: 'mark_calls_as_read' },
    }, seen));

    for (const host of snapshotArg.lanHosts) {
      entities.push(this.entity(snapshotArg, {
        platform: 'device_tracker' as TEntityPlatform,
        key: `device_tracker_${slug(host.id)}`,
        name: host.name,
        state: host.active ? 'home' : 'not_home',
        available: snapshotArg.online,
        deviceId: host.deviceId,
        attributes: cleanAttributes({ ...host.attributes, macAddress: host.macAddress, addresses: host.addresses, hostType: host.hostType, reachable: host.reachable, homeAssistantPlatform: 'device_tracker' }),
      }, seen));
    }

    for (const raid of snapshotArg.raids) {
      entities.push(this.entity(snapshotArg, {
        platform: 'binary_sensor',
        key: `raid_${slug(raid.id)}_degraded`,
        name: `Raid array ${raid.name} degraded`,
        state: raid.degraded,
        available: snapshotArg.online,
        deviceId: snapshotArg.router.id,
        attributes: cleanAttributes({ ...raid.attributes, deviceClass: 'problem', entityCategory: 'diagnostic' }),
      }, seen));
    }

    for (const node of snapshotArg.homeNodes) {
      entities.push(...this.homeEntities(snapshotArg, node, seen));
    }

    return entities;
  }

  public static commandForService(snapshotArg: IFreeboxSnapshot, requestArg: IServiceCallRequest): IFreeboxCommandRequest | undefined {
    if (requestArg.domain === freeboxDomain) {
      if (requestArg.service === 'snapshot' || requestArg.service === 'status') {
        return { action: 'snapshot' };
      }
      if (requestArg.service === 'refresh') {
        return { action: 'refresh' };
      }
      if (requestArg.service === 'reboot' || requestArg.service === 'restart') {
        return { action: 'reboot', endpoint: 'system/reboot/', method: 'POST', service: `${requestArg.domain}.${requestArg.service}`, target: requestArg.target, data: requestArg.data };
      }
      if (requestArg.service === 'mark_calls_as_read') {
        return { action: 'mark_calls_as_read', endpoint: 'call/log/mark_all_as_read/', method: 'POST', service: `${requestArg.domain}.${requestArg.service}`, target: requestArg.target, data: requestArg.data };
      }
      if (requestArg.service === 'set_wifi_enabled') {
        const enabled = booleanValue(requestArg.data?.enabled);
        return enabled === undefined ? undefined : this.wifiCommand(enabled, requestArg);
      }
      if (requestArg.service === 'set_home_endpoint_value') {
        const nodeId = stringValue(requestArg.data?.nodeId) || stringValue(requestArg.data?.node_id);
        const endpointId = stringValue(requestArg.data?.endpointId) || stringValue(requestArg.data?.endpoint_id);
        if (!nodeId || !endpointId) {
          return undefined;
        }
        return this.homeEndpointCommand(nodeId, endpointId, requestArg.data?.value, requestArg);
      }
    }

    const entity = this.entityForTarget(snapshotArg, requestArg);
    const action = stringValue(entity?.attributes?.action);
    if (requestArg.domain === 'button' && requestArg.service === 'press') {
      if (action === 'reboot') {
        return { action: 'reboot', endpoint: 'system/reboot/', method: 'POST', service: 'button.press', target: requestArg.target, data: requestArg.data };
      }
      if (action === 'mark_calls_as_read') {
        return { action: 'mark_calls_as_read', endpoint: 'call/log/mark_all_as_read/', method: 'POST', service: 'button.press', target: requestArg.target, data: requestArg.data };
      }
    }

    if ((requestArg.domain === 'switch' || requestArg.domain === freeboxDomain) && (requestArg.service === 'turn_on' || requestArg.service === 'turn_off')) {
      if (action === 'set_wifi_enabled' || entity?.id.includes('freebox_wifi')) {
        return this.wifiCommand(requestArg.service === 'turn_on', requestArg);
      }
    }

    if (requestArg.domain === 'alarm_control_panel') {
      const nodeId = stringValue(entity?.attributes?.nodeId);
      const commandName = requestArg.service === 'alarm_arm_away' ? 'alarm1'
        : requestArg.service === 'alarm_arm_home' ? 'alarm2'
          : requestArg.service === 'alarm_disarm' ? 'off'
            : requestArg.service === 'alarm_trigger' ? 'trigger'
              : undefined;
      if (nodeId && commandName) {
        const node = snapshotArg.homeNodes.find((nodeArg) => nodeArg.id === nodeId);
        const endpointId = node ? endpointIdFor(node.endpoints, 'slot', commandName) : undefined;
        return endpointId === undefined ? undefined : this.homeEndpointCommand(nodeId, String(endpointId), undefined, requestArg);
      }
    }

    if (requestArg.domain === 'camera' && (requestArg.service === 'enable_motion_detection' || requestArg.service === 'disable_motion_detection')) {
      const nodeId = stringValue(entity?.attributes?.nodeId);
      const node = nodeId ? snapshotArg.homeNodes.find((nodeArg) => nodeArg.id === nodeId) : undefined;
      const endpointId = node ? endpointIdFor(node.endpoints, 'slot', 'detection') : undefined;
      return nodeId && endpointId !== undefined ? this.homeEndpointCommand(nodeId, String(endpointId), requestArg.service === 'enable_motion_detection', requestArg) : undefined;
    }

    return undefined;
  }

  public static hasUsableRawData(rawDataArg: Partial<IFreeboxRawData> | undefined): boolean {
    return Boolean(rawDataArg && (rawDataArg.systemConfig || rawDataArg.connectionStatus || rawDataArg.callLog?.length || rawDataArg.disks?.length || rawDataArg.raids?.length || rawDataArg.lanHosts?.length || rawDataArg.wifiConfig || rawDataArg.homeNodes?.length || rawDataArg.apiVersionInfo));
  }

  public static offlineSnapshot(configArg: IFreeboxConfig, errorArg: string): IFreeboxSnapshot {
    return this.toSnapshot({ config: configArg, rawData: configArg.rawData, online: false, source: 'runtime', error: errorArg });
  }

  public static clone<TValue>(valueArg: TValue): TValue {
    return JSON.parse(JSON.stringify(valueArg)) as TValue;
  }

  private static rawData(configArg: IFreeboxConfig, rawDataArg?: Partial<IFreeboxRawData>): Partial<IFreeboxRawData> {
    return {
      ...configArg.rawData,
      ...rawDataArg,
      errors: {
        ...(configArg.rawData?.errors || {}),
        ...(rawDataArg?.errors || {}),
      },
    };
  }

  private static normalizeSnapshot(snapshotArg: IFreeboxSnapshot, configArg: IFreeboxConfig, sourceArg: TFreeboxSnapshotSource, errorArg?: string): IFreeboxSnapshot {
    const endpoint = parseEndpoint(configArg.url || configArg.host, configArg.port, configArg.protocol);
    snapshotArg.router.host = snapshotArg.router.host || endpoint.host;
    snapshotArg.router.port = snapshotArg.router.port || endpoint.port;
    snapshotArg.router.protocol = snapshotArg.router.protocol || endpoint.protocol;
    snapshotArg.router.apiVersion = normalizeApiVersion(snapshotArg.router.apiVersion || configArg.apiVersion || freeboxDefaultApiVersion);
    snapshotArg.router.apiBaseUrl = snapshotArg.router.apiBaseUrl || configArg.apiBaseUrl || freeboxDefaultApiBaseUrl;
    snapshotArg.source = sourceArg;
    snapshotArg.error = errorArg || snapshotArg.error;
    snapshotArg.capabilities = {
      ...snapshotArg.capabilities,
      localControl: this.hasLocalControl(configArg, endpoint) || snapshotArg.capabilities.localControl,
      authenticated: Boolean(configArg.sessionToken || configArg.appToken || configArg.token || snapshotArg.capabilities.authenticated),
    };
    snapshotArg.capabilities.wifiControl = snapshotArg.capabilities.localControl;
    snapshotArg.capabilities.homeControl = snapshotArg.capabilities.localControl;
    snapshotArg.capabilities.callControl = snapshotArg.capabilities.localControl;
    snapshotArg.capabilities.reboot = snapshotArg.capabilities.localControl;
    return snapshotArg;
  }

  private static router(configArg: IFreeboxConfig, rawDataArg: Partial<IFreeboxRawData>, endpointArg: IEndpointInfo): IFreeboxRouterSnapshot {
    const system = recordValue(rawDataArg.systemConfig) || {};
    const apiInfo = recordValue(rawDataArg.apiVersionInfo) || {};
    const modelInfo = recordValue(system.model_info) || recordValue(system.modelInfo) || {};
    const connection = recordValue(rawDataArg.connectionStatus);
    const macAddress = normalizeMac(configArg.macAddress || system.mac);
    const uniqueId = configArg.uniqueId || macAddress || stringValue(apiInfo.uid) || configArg.serialNumber || stringValue(system.serial) || endpointArg.host || freeboxDomain;
    const name = configArg.name || stringValue(modelInfo.pretty_name) || stringValue(apiInfo.device_name) || freeboxDisplayName;

    return {
      id: `freebox.router.${slug(uniqueId)}`,
      name,
      host: endpointArg.host || stringValue(apiInfo.api_domain),
      port: endpointArg.port || numberValue(apiInfo.https_port),
      protocol: endpointArg.protocol,
      manufacturer: configArg.manufacturer || freeboxManufacturer,
      model: configArg.model || stringValue(modelInfo.pretty_name) || stringValue(apiInfo.device_type) || freeboxDisplayName,
      modelId: configArg.modelId || stringValue(modelInfo.name) || stringValue(system.board_name) || stringValue(apiInfo.device_type),
      firmwareVersion: stringValue(system.firmware_version),
      hardwareVersion: stringValue(system.board_name),
      serialNumber: configArg.serialNumber || stringValue(system.serial),
      macAddress,
      apiVersion: normalizeApiVersion(configArg.apiVersion || stringValue(apiInfo.api_version) || freeboxDefaultApiVersion),
      apiBaseUrl: configArg.apiBaseUrl || stringValue(apiInfo.api_base_url) || freeboxDefaultApiBaseUrl,
      uptimeSeconds: numberValue(system.uptime_val),
      connection,
      attributes: cleanAttributes({
        IPv4: connection?.ipv4,
        IPv6: connection?.ipv6,
        connectionType: connection?.media,
        connectionState: connection?.state,
        uid: apiInfo.uid,
        boxFlavor: system.box_flavor,
        diskStatus: system.disk_status,
        authenticated: system.box_authenticated,
      }),
    };
  }

  private static sensors(routerArg: IFreeboxRouterSnapshot, rawDataArg: Partial<IFreeboxRawData>, onlineArg: boolean): IFreeboxSensorSnapshot[] {
    const sensors: IFreeboxSensorSnapshot[] = [];
    const system = recordValue(rawDataArg.systemConfig) || {};
    const connection = recordValue(rawDataArg.connectionStatus) || {};

    for (const sensor of arrayRecords(system.sensors)) {
      const key = slug(stringValue(sensor.name) || stringValue(sensor.id) || `temperature_${sensors.length + 1}`);
      sensors.push(this.sensor(routerArg.id, 'temperature', key, sensor.value, onlineArg, { unit: 'C', deviceClass: 'temperature', stateClass: 'measurement', sourceKey: stringValue(sensor.name) }));
    }

    for (const key of ['temp_cpum', 'temp_cpub', 'temp_sw']) {
      if (system[key] !== undefined) {
        sensors.push(this.sensor(routerArg.id, 'temperature', key, system[key], onlineArg, { unit: 'C', deviceClass: 'temperature', stateClass: 'measurement' }));
      }
    }
    if (system.fan_rpm !== undefined) {
      sensors.push(this.sensor(routerArg.id, 'fan', 'fan_rpm', system.fan_rpm, onlineArg, { unit: 'rpm', stateClass: 'measurement' }));
    }
    if (system.uptime_val !== undefined) {
      sensors.push(this.sensor(routerArg.id, 'system', 'uptime_val', system.uptime_val, onlineArg, { unit: 's', deviceClass: 'duration', entityCategory: 'diagnostic' }));
    }

    for (const key of ['rate_down', 'rate_up']) {
      if (connection[key] !== undefined) {
        sensors.push(this.sensor(routerArg.id, 'connection', key, connection[key], onlineArg, { unit: 'B/s', deviceClass: 'data_rate', stateClass: 'measurement' }));
      }
    }
    for (const key of ['bandwidth_down', 'bandwidth_up']) {
      if (connection[key] !== undefined) {
        sensors.push(this.sensor(routerArg.id, 'connection', key, connection[key], onlineArg, { unit: 'bit/s', deviceClass: 'data_rate', stateClass: 'measurement' }));
      }
    }
    for (const key of ['bytes_down', 'bytes_up']) {
      if (connection[key] !== undefined) {
        sensors.push(this.sensor(routerArg.id, 'connection', key, connection[key], onlineArg, { unit: 'B', deviceClass: 'data_size', stateClass: 'total_increasing' }));
      }
    }

    const missedCalls = arrayRecords(rawDataArg.callLog).filter((callArg) => callArg.new === true && callArg.type === 'missed');
    if (rawDataArg.callLog) {
      sensors.push(this.sensor(routerArg.id, 'call', 'missed_calls', missedCalls.length, onlineArg, { attributes: Object.fromEntries(missedCalls.map((callArg) => [String(callArg.datetime || callArg.id), callArg.name || callArg.number || callArg.id])) }));
    }

    for (const disk of this.disks(rawDataArg)) {
      if (disk.temperature !== undefined) {
        sensors.push(this.sensor(disk.id, 'storage', 'temperature', disk.temperature, onlineArg, { unit: 'C', deviceClass: 'temperature', stateClass: 'measurement' }));
      }
      for (const partition of disk.partitions) {
        sensors.push(this.sensor(disk.id, 'storage', `partition_${slug(partition.id)}_free_space`, partition.freePercent, onlineArg, { unit: '%', sourceKey: 'partition_free_space', attributes: { partitionId: partition.partitionId, label: partition.label, totalBytes: partition.totalBytes, freeBytes: partition.freeBytes } }));
      }
    }

    for (const node of this.homeNodes(rawDataArg)) {
      const battery = endpointValue(node.showEndpoints, 'signal', 'battery');
      if (battery !== undefined && battery !== null) {
        sensors.push(this.sensor(node.id, 'home', 'battery', battery, onlineArg, { unit: '%', deviceClass: 'battery', stateClass: 'measurement' }));
      }
    }

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

  private static disks(rawDataArg: Partial<IFreeboxRawData>): IFreeboxDiskSnapshot[] {
    return arrayRecords(rawDataArg.disks).map((diskArg, indexArg) => {
      const diskId = primitiveId(diskArg.id) ?? indexArg + 1;
      const partitions = arrayRecords(diskArg.partitions).map((partitionArg, partitionIndexArg) => this.partition(partitionArg, diskId, partitionIndexArg));
      const serial = stringValue(diskArg.serial);
      return {
        id: `freebox.disk.${slug(serial || diskId)}`,
        name: stringValue(diskArg.model) || `Disk ${diskId}`,
        diskId,
        model: stringValue(diskArg.model),
        serialNumber: serial,
        firmwareVersion: stringValue(diskArg.firmware),
        type: stringValue(diskArg.type),
        state: stringValue(diskArg.state),
        totalBytes: numberValue(diskArg.total_bytes),
        temperature: numberValue(diskArg.temp),
        partitions,
        attributes: cleanAttributes({ connector: diskArg.connector, tableType: diskArg.table_type, idle: diskArg.idle, spinning: diskArg.spinning }),
      };
    });
  }

  private static partition(partitionArg: Record<string, unknown>, diskIdArg: unknown, indexArg: number): IFreeboxPartitionSnapshot {
    const diskId = primitiveId(diskIdArg) ?? 'unknown';
    const partitionId = primitiveId(partitionArg.id) ?? indexArg + 1;
    const totalBytes = numberValue(partitionArg.total_bytes);
    const freeBytes = numberValue(partitionArg.free_bytes);
    return {
      id: `${diskId}_${partitionId}`,
      partitionId,
      diskId: primitiveId(partitionArg.disk_id) ?? diskId,
      label: stringValue(partitionArg.label) || `Partition ${partitionId}`,
      state: stringValue(partitionArg.state),
      filesystem: stringValue(partitionArg.fstype),
      totalBytes,
      freeBytes,
      usedBytes: numberValue(partitionArg.used_bytes),
      freePercent: totalBytes && freeBytes !== undefined ? round((freeBytes * 100) / totalBytes) : undefined,
      attributes: cleanAttributes({ fsckResult: partitionArg.fsck_result, path: partitionArg.path }),
    };
  }

  private static raids(rawDataArg: Partial<IFreeboxRawData>): IFreeboxRaidSnapshot[] {
    return arrayRecords(rawDataArg.raids).map((raidArg, indexArg) => {
      const id = raidArg.id ?? indexArg + 1;
      return {
        id: String(id),
        name: stringValue(raidArg.name) || String(id),
        degraded: Boolean(raidArg.degraded),
        attributes: cleanAttributes({ ...raidArg }),
      };
    });
  }

  private static lanHosts(rawDataArg: Partial<IFreeboxRawData>, routerArg: IFreeboxRouterSnapshot): IFreeboxLanHostSnapshot[] {
    const hosts = arrayRecords(rawDataArg.lanHosts).map((hostArg, indexArg) => this.lanHost(hostArg, indexArg));
    if (routerArg.macAddress && !hosts.some((hostArg) => normalizeMac(hostArg.macAddress) === routerArg.macAddress)) {
      hosts.push({
        id: routerArg.macAddress,
        deviceId: `${routerArg.id}.tracker`,
        name: routerArg.name,
        macAddress: routerArg.macAddress,
        manufacturer: routerArg.manufacturer,
        hostType: 'router',
        active: true,
        reachable: true,
        addresses: routerArg.host ? [routerArg.host] : [],
        attributes: routerArg.attributes,
      });
    }
    return hosts;
  }

  private static lanHost(hostArg: Record<string, unknown>, indexArg: number): IFreeboxLanHostSnapshot {
    const l2ident = recordValue(hostArg.l2ident) || {};
    const macAddress = normalizeMac(l2ident.id || hostArg.mac || hostArg.macAddress);
    const id = stringValue(hostArg.id) || macAddress || `host_${indexArg + 1}`;
    const addresses = arrayRecords(hostArg.l3connectivities).map((connArg) => stringValue(connArg.addr)).filter(Boolean) as string[];
    return {
      id,
      deviceId: `freebox.host.${slug(macAddress || id)}`,
      name: stringValue(hostArg.primary_name)?.trim() || 'Unknown device',
      macAddress,
      manufacturer: stringValue(hostArg.vendor_name),
      hostType: stringValue(hostArg.host_type),
      active: Boolean(hostArg.active),
      reachable: booleanValue(hostArg.reachable),
      addresses,
      attributes: cleanAttributes({ lastTimeReachable: hostArg.last_time_reachable, lastActivity: hostArg.last_activity, persistent: hostArg.persistent, names: hostArg.names }),
    };
  }

  private static homeNodes(rawDataArg: Partial<IFreeboxRawData>): IFreeboxHomeNodeSnapshot[] {
    return arrayRecords(rawDataArg.homeNodes).map((nodeArg, indexArg) => {
      const id = String(nodeArg.id ?? indexArg + 1);
      const type = recordValue(nodeArg.type) || {};
      const props = recordValue(nodeArg.props) || {};
      const category = stringValue(nodeArg.category);
      const inherit = stringValue(type.inherit);
      return {
        id: `freebox.home.${slug(id)}`,
        name: stringValue(nodeArg.label)?.trim() || `Freebox Home ${id}`,
        category,
        model: category ? freeboxHomeCategoryModels[category] : inherit === 'node::rts' ? freeboxHomeCategoryModels[freeboxHomeCategories.rts] : inherit === 'node::ios' ? freeboxHomeCategoryModels[freeboxHomeCategories.iohome] : undefined,
        manufacturer: inherit === 'node::rts' || inherit === 'node::ios' ? 'Somfy' : freeboxManufacturer,
        firmwareVersion: stringValue(props.FwVersion),
        status: stringValue(nodeArg.status),
        streamUrl: stringValue(props.Stream),
        showEndpoints: arrayRecords(nodeArg.show_endpoints),
        endpoints: arrayRecords(type.endpoints),
        attributes: cleanAttributes({ rawNodeId: nodeArg.id, props, type }),
      };
    });
  }

  private static homeEntities(snapshotArg: IFreeboxSnapshot, nodeArg: IFreeboxHomeNodeSnapshot, seenArg: Map<string, number>): IIntegrationEntity[] {
    const entities: IIntegrationEntity[] = [];
    const available = snapshotArg.online && nodeArg.status !== 'inactive';
    if (nodeArg.category === freeboxHomeCategories.pir || nodeArg.category === freeboxHomeCategories.dws) {
      const raw = endpointValue(nodeArg.showEndpoints, 'signal', 'trigger');
      entities.push(this.entity(snapshotArg, {
        platform: 'binary_sensor',
        key: 'trigger',
        name: nodeArg.name,
        state: raw === undefined || raw === null ? null : !Boolean(raw),
        available,
        deviceId: nodeArg.id,
        attributes: cleanAttributes({ nodeId: this.rawHomeNodeId(nodeArg), deviceClass: nodeArg.category === freeboxHomeCategories.pir ? 'motion' : 'door', category: nodeArg.category }),
      }, seenArg));
    }

    const cover = endpointValue(nodeArg.showEndpoints, 'signal', 'cover');
    if (cover !== undefined && cover !== null) {
      entities.push(this.entity(snapshotArg, {
        platform: 'binary_sensor',
        key: 'cover',
        name: `${nodeArg.name} Cover`,
        state: Boolean(cover),
        available,
        deviceId: nodeArg.id,
        attributes: cleanAttributes({ nodeId: this.rawHomeNodeId(nodeArg), deviceClass: 'safety', entityCategory: 'diagnostic', category: nodeArg.category }),
      }, seenArg));
    }

    if (nodeArg.category === freeboxHomeCategories.alarm) {
      const state = stringValue(endpointValue(nodeArg.showEndpoints, 'signal', 'state'));
      entities.push(this.entity(snapshotArg, {
        platform: 'alarm_control_panel' as TEntityPlatform,
        key: 'alarm',
        name: nodeArg.name,
        state: state ? alarmStatus[state] || state : 'unknown',
        available,
        deviceId: nodeArg.id,
        attributes: cleanAttributes({ nodeId: this.rawHomeNodeId(nodeArg), supportedFeatures: ['arm_away', endpointIdFor(nodeArg.endpoints, 'slot', 'alarm2') === undefined ? undefined : 'arm_home', 'trigger'].filter(Boolean), homeAssistantPlatform: 'alarm_control_panel' }),
      }, seenArg));
    }

    if (nodeArg.category === freeboxHomeCategories.camera) {
      const detection = endpointValue(nodeArg.showEndpoints, 'signal', 'detection');
      entities.push(this.entity(snapshotArg, {
        platform: 'camera' as TEntityPlatform,
        key: 'camera',
        name: nodeArg.name,
        state: nodeArg.status === 'active' ? 'streaming' : 'idle',
        available,
        deviceId: nodeArg.id,
        attributes: cleanAttributes({ nodeId: this.rawHomeNodeId(nodeArg), streamUrl: nodeArg.streamUrl, detection, homeAssistantPlatform: 'camera' }),
      }, seenArg));
    }

    return entities;
  }

  private static sensor(deviceIdArg: string, kindArg: string, keyArg: string, valueArg: unknown, onlineArg: boolean, optionsArg: { unit?: string; deviceClass?: string; stateClass?: string; entityCategory?: string; sourceKey?: string; attributes?: Record<string, unknown> } = {}): IFreeboxSensorSnapshot {
    return {
      id: keyArg,
      deviceId: deviceIdArg,
      kind: kindArg,
      name: sensorNames[optionsArg.sourceKey || keyArg] || sensorNames[keyArg] || humanName(keyArg),
      value: valueArg,
      unit: optionsArg.unit,
      deviceClass: optionsArg.deviceClass,
      stateClass: optionsArg.stateClass,
      entityCategory: optionsArg.entityCategory,
      available: onlineArg && valueArg !== null && valueArg !== undefined,
      attributes: cleanAttributes({ ...optionsArg.attributes, sourceKey: optionsArg.sourceKey }),
    };
  }

  private static entity(snapshotArg: IFreeboxSnapshot, optionsArg: IFreeboxEntityOptions, seenArg: Map<string, number>): IIntegrationEntity {
    const baseId = `${optionsArg.platform}.${slug(this.deviceName(snapshotArg, optionsArg.deviceId))}_${slug(optionsArg.key)}`;
    const seenCount = seenArg.get(baseId) || 0;
    seenArg.set(baseId, seenCount + 1);
    const id = seenCount ? `${baseId}_${seenCount + 1}` : baseId;
    return {
      id,
      uniqueId: `${freeboxDomain}_${slug(optionsArg.deviceId)}_${slug(optionsArg.key)}`,
      integrationDomain: freeboxDomain,
      deviceId: optionsArg.deviceId,
      platform: optionsArg.platform,
      name: optionsArg.name,
      state: optionsArg.state,
      available: optionsArg.available,
      attributes: optionsArg.attributes,
    };
  }

  private static entityForTarget(snapshotArg: IFreeboxSnapshot, requestArg: IServiceCallRequest): IIntegrationEntity | undefined {
    const entityId = requestArg.target.entityId;
    if (!entityId) {
      return undefined;
    }
    return this.toEntities(snapshotArg).find((entityArg) => entityArg.id === entityId || entityArg.uniqueId === entityId);
  }

  private static wifiCommand(enabledArg: boolean, requestArg: IServiceCallRequest): IFreeboxCommandRequest {
    return { action: 'set_wifi_enabled', endpoint: 'wifi/config/', method: 'PUT', body: { enabled: enabledArg }, service: `${requestArg.domain}.${requestArg.service}`, target: requestArg.target, data: requestArg.data };
  }

  private static homeEndpointCommand(nodeIdArg: string, endpointIdArg: string, valueArg: unknown, requestArg: IServiceCallRequest): IFreeboxCommandRequest {
    return { action: 'set_home_endpoint_value', endpoint: `home/endpoints/${encodeURIComponent(nodeIdArg)}/${encodeURIComponent(endpointIdArg)}`, method: 'PUT', body: { value: valueArg ?? null }, service: `${requestArg.domain}.${requestArg.service}`, target: requestArg.target, data: requestArg.data };
  }

  private static hasLocalControl(configArg: IFreeboxConfig, endpointArg: IEndpointInfo): boolean {
    return Boolean(configArg.commandExecutor || configArg.client?.execute || (endpointArg.host && (configArg.sessionToken || configArg.appToken || configArg.token)));
  }

  private static capabilityForSensor(sensorArg: IFreeboxSensorSnapshot): plugins.shxInterfaces.data.TDeviceCapability {
    return sensorArg.deviceClass === 'energy' ? 'energy' : 'sensor';
  }

  private static deviceName(snapshotArg: IFreeboxSnapshot, deviceIdArg: string): string {
    if (deviceIdArg === snapshotArg.router.id) {
      return snapshotArg.router.name;
    }
    return snapshotArg.lanHosts.find((hostArg) => hostArg.deviceId === deviceIdArg)?.name
      || snapshotArg.disks.find((diskArg) => diskArg.id === deviceIdArg)?.name
      || snapshotArg.homeNodes.find((nodeArg) => nodeArg.id === deviceIdArg)?.name
      || freeboxDisplayName;
  }

  private static rawHomeNodeId(nodeArg: IFreeboxHomeNodeSnapshot): string | undefined {
    return stringValue(nodeArg.attributes?.rawNodeId) || nodeArg.id.replace(/^freebox\.home\./, '');
  }
}

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

function endpointValue(endpointsArg: Array<Record<string, unknown>>, epTypeArg: string, nameArg: string): unknown {
  return endpointsArg.find((endpointArg) => endpointArg.name === nameArg && endpointArg.ep_type === epTypeArg)?.value;
}

function endpointIdFor(endpointsArg: Array<Record<string, unknown>>, epTypeArg: string, nameArg: string): unknown {
  return endpointsArg.find((endpointArg) => endpointArg.name === nameArg && endpointArg.ep_type === epTypeArg)?.id;
}

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

function arrayRecords(valueArg: unknown): Array<Record<string, unknown>> {
  return Array.isArray(valueArg) ? valueArg.filter((itemArg) => itemArg && typeof itemArg === 'object' && !Array.isArray(itemArg)) as Array<Record<string, unknown>> : [];
}

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

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

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

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

function onOffState(valueArg: unknown): string {
  const value = booleanValue(valueArg);
  return value === undefined ? 'unknown' : value ? 'on' : 'off';
}

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

function normalizeApiVersion(valueArg: string): string {
  return valueArg.startsWith('v') ? valueArg : `v${valueArg.split('.')[0] || '6'}`;
}

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

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

function round(valueArg: number): number {
  return Math.round(valueArg * 100) / 100;
}

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

function deviceStateValue(valueArg: unknown): plugins.shxInterfaces.data.TDeviceStateValue {
  if (valueArg === undefined || valueArg === null) {
    return null;
  }
  if (typeof valueArg === 'string' || typeof valueArg === 'number' || typeof valueArg === 'boolean') {
    return valueArg;
  }
  if (typeof valueArg === 'object' && !Array.isArray(valueArg)) {
    return valueArg as Record<string, unknown>;
  }
  return String(valueArg);
}
