import * as plugins from '../../plugins.js';
import type { IIntegrationEntity, IServiceCallRequest, TEntityPlatform } from '../../core/types.js';
import type { IZwaveJsConfig, IZwaveJsNode, IZwaveJsServerCommand, IZwaveJsSnapshot, IZwaveJsValue, IZwaveJsValueId } from './zwave_js.types.js';

export class ZwaveJsMapper {
  public static toSnapshot(configArg: IZwaveJsConfig, connectedArg = false, eventsArg: IZwaveJsSnapshot['events'] = []): IZwaveJsSnapshot {
    return {
      version: configArg.version,
      controller: configArg.controller || configArg.state?.controller,
      nodes: configArg.nodes || configArg.state?.nodes || [],
      events: eventsArg,
      connected: connectedArg,
      url: configArg.url,
    };
  }

  public static toDevices(snapshotArg: IZwaveJsSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = new Date().toISOString();
    const devices: plugins.shxInterfaces.data.IDeviceDefinition[] = [];
    devices.push({
      id: this.controllerDeviceId(snapshotArg),
      integrationDomain: 'zwave_js',
      name: 'Z-Wave controller',
      protocol: 'zwave',
      manufacturer: 'Z-Wave JS',
      model: 'Controller',
      online: snapshotArg.connected || Boolean(snapshotArg.nodes.length),
      features: [
        { id: 'connection', capability: 'sensor', name: 'Connection', readable: true, writable: false },
        { id: 'node_count', capability: 'sensor', name: 'Node count', readable: true, writable: false },
      ],
      state: [
        { featureId: 'connection', value: snapshotArg.connected ? 'connected' : 'configured', updatedAt },
        { featureId: 'node_count', value: snapshotArg.nodes.length, updatedAt },
      ],
      metadata: {
        homeId: snapshotArg.controller?.homeId ?? snapshotArg.version?.homeId,
        serverVersion: snapshotArg.version?.serverVersion,
        driverVersion: snapshotArg.version?.driverVersion,
        url: snapshotArg.url,
      },
    });

    for (const node of snapshotArg.nodes) {
      const features: plugins.shxInterfaces.data.IDeviceFeature[] = [
        { id: 'status', capability: 'sensor', name: 'Node status', readable: true, writable: false },
      ];
      const state: plugins.shxInterfaces.data.IDeviceState[] = [
        { featureId: 'status', value: node.status || (node.ready ? 'ready' : 'unknown'), updatedAt },
      ];
      for (const value of this.nodeValues(node)) {
        const feature = this.featureForValue(value);
        features.push(feature);
        state.push({ featureId: feature.id, value: this.stateValue(value.value), updatedAt });
      }
      devices.push({
        id: this.nodeDeviceId(node),
        integrationDomain: 'zwave_js',
        name: this.nodeName(node),
        room: node.location,
        protocol: 'zwave',
        manufacturer: node.manufacturer,
        model: node.productLabel || node.productDescription,
        online: node.status ? !['dead', 'asleep'].includes(node.status.toLowerCase()) : node.ready !== false,
        features,
        state,
        metadata: {
          nodeId: node.nodeId,
          manufacturerId: node.manufacturerId,
          productId: node.productId,
          productType: node.productType,
          firmwareVersion: node.firmwareVersion,
          isControllerNode: node.isControllerNode,
        },
      });
    }
    return devices;
  }

  public static toEntities(snapshotArg: IZwaveJsSnapshot): IIntegrationEntity[] {
    const entities: IIntegrationEntity[] = [];
    for (const node of snapshotArg.nodes) {
      const deviceId = this.nodeDeviceId(node);
      for (const value of this.nodeValues(node)) {
        const platform = this.platformForValue(value);
        entities.push({
          id: `${platform}.${this.slug(`${this.nodeName(node)} ${this.valueLabel(value)}`)}`,
          uniqueId: `zwave_js_${this.slug(`${node.nodeId}_${this.valueIdKey(value)}`)}`,
          integrationDomain: 'zwave_js',
          deviceId,
          platform,
          name: this.valueLabel(value),
          state: this.entityState(value, platform),
          attributes: {
            nodeId: node.nodeId,
            valueId: this.valueId(value),
            commandClass: value.commandClass,
            commandClassName: value.commandClassName,
            endpoint: value.endpoint,
            property: value.property,
            propertyKey: value.propertyKey,
            unit: value.metadata?.unit,
            zwaveType: value.metadata?.type,
            states: value.metadata?.states,
          },
          available: node.ready !== false && node.status !== 'dead',
        });
      }
    }
    return entities;
  }

  public static commandForService(snapshotArg: IZwaveJsSnapshot, requestArg: IServiceCallRequest): IZwaveJsServerCommand | undefined {
    const target = this.findTargetValue(snapshotArg, requestArg);
    if (!target) {
      return undefined;
    }
    let value: unknown;
    if (requestArg.service === 'turn_on') {
      value = this.onValue(target.value);
    } else if (requestArg.service === 'turn_off') {
      value = this.offValue(target.value);
    } else if (requestArg.service === 'set_value') {
      value = requestArg.data?.value;
    } else if (requestArg.service === 'set_position') {
      value = requestArg.data?.position;
    } else if (requestArg.service === 'select_option') {
      value = requestArg.data?.option;
    } else if (requestArg.service === 'set_percentage') {
      value = requestArg.data?.percentage;
    } else if (requestArg.service === 'open_cover') {
      value = 99;
    } else if (requestArg.service === 'close_cover') {
      value = 0;
    } else if (requestArg.service === 'press') {
      value = true;
    }
    if (value === undefined) {
      return undefined;
    }
    return {
      command: 'node.set_value',
      nodeId: target.node.nodeId,
      valueId: this.valueId(target.value),
      value,
    };
  }

  public static valueId(valueArg: IZwaveJsValue): IZwaveJsValueId {
    return {
      commandClass: valueArg.commandClass || 0,
      endpoint: valueArg.endpoint,
      property: valueArg.property ?? valueArg.propertyName ?? 'value',
      propertyKey: valueArg.propertyKey,
    };
  }

  public static nodeValues(nodeArg: IZwaveJsNode): IZwaveJsValue[] {
    if (Array.isArray(nodeArg.values)) {
      return nodeArg.values.map((valueArg) => ({ ...valueArg, nodeId: nodeArg.nodeId }));
    }
    if (nodeArg.values && typeof nodeArg.values === 'object') {
      return Object.entries(nodeArg.values).map(([id, value]) => ({ ...value, id, nodeId: nodeArg.nodeId }));
    }
    return [];
  }

  private static findTargetValue(snapshotArg: IZwaveJsSnapshot, requestArg: IServiceCallRequest): { node: IZwaveJsNode; value: IZwaveJsValue } | undefined {
    const entities = this.toEntities(snapshotArg);
    if (requestArg.target.entityId) {
      const entity = entities.find((entityArg) => entityArg.id === requestArg.target.entityId || entityArg.uniqueId === requestArg.target.entityId);
      if (!entity) {
        return undefined;
      }
      return this.nodeValueByUniqueId(snapshotArg, entity.uniqueId);
    }
    if (requestArg.target.deviceId) {
      const node = snapshotArg.nodes.find((nodeArg) => this.nodeDeviceId(nodeArg) === requestArg.target.deviceId);
      const value = node ? this.nodeValues(node).find((valueArg) => this.valueWritable(valueArg)) : undefined;
      return node && value ? { node, value } : undefined;
    }
    for (const node of snapshotArg.nodes) {
      const value = this.nodeValues(node).find((valueArg) => this.valueWritable(valueArg));
      if (value) {
        return { node, value };
      }
    }
    return undefined;
  }

  private static nodeValueByUniqueId(snapshotArg: IZwaveJsSnapshot, uniqueIdArg: string): { node: IZwaveJsNode; value: IZwaveJsValue } | undefined {
    for (const node of snapshotArg.nodes) {
      for (const value of this.nodeValues(node)) {
        if (`zwave_js_${this.slug(`${node.nodeId}_${this.valueIdKey(value)}`)}` === uniqueIdArg) {
          return { node, value };
        }
      }
    }
    return undefined;
  }

  private static featureForValue(valueArg: IZwaveJsValue): plugins.shxInterfaces.data.IDeviceFeature {
    const platform = this.platformForValue(valueArg);
    return {
      id: this.slug(this.valueIdKey(valueArg)),
      capability: this.capabilityForPlatform(platform),
      name: this.valueLabel(valueArg),
      readable: valueArg.metadata?.readable !== false,
      writable: this.valueWritable(valueArg),
      unit: valueArg.metadata?.unit,
    };
  }

  private static platformForValue(valueArg: IZwaveJsValue): TEntityPlatform {
    const label = this.valueLabel(valueArg).toLowerCase();
    const cc = (valueArg.commandClassName || '').toLowerCase();
    const type = valueArg.metadata?.type;
    const writable = this.valueWritable(valueArg);
    if (cc.includes('switch binary') || (type === 'boolean' && writable)) {
      return 'switch';
    }
    if (cc.includes('switch multilevel') && writable) {
      return 'light';
    }
    if (cc.includes('window covering')) {
      return 'cover';
    }
    if (cc.includes('thermostat')) {
      return 'climate';
    }
    if (valueArg.metadata?.states && writable) {
      return 'select';
    }
    if (type === 'number' && writable) {
      return 'number';
    }
    if (type === 'boolean') {
      return 'binary_sensor';
    }
    if (label.includes('firmware')) {
      return 'update';
    }
    return 'sensor';
  }

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

  private static entityState(valueArg: IZwaveJsValue, platformArg: TEntityPlatform): unknown {
    if (platformArg === 'switch' || platformArg === 'light') {
      return valueArg.value ? 'on' : 'off';
    }
    return valueArg.value ?? 'unknown';
  }

  private static valueWritable(valueArg: IZwaveJsValue): boolean {
    return valueArg.metadata?.writeable === true || valueArg.metadata?.writeable === undefined && valueArg.commandClassName?.toLowerCase().includes('switch') === true;
  }

  private static valueLabel(valueArg: IZwaveJsValue): string {
    return valueArg.metadata?.label || valueArg.propertyName || String(valueArg.property ?? valueArg.id ?? 'Z-Wave value');
  }

  private static valueIdKey(valueArg: IZwaveJsValue): string {
    return [valueArg.commandClass, valueArg.endpoint || 0, valueArg.property ?? valueArg.propertyName ?? 'value', valueArg.propertyKey ?? ''].join('_');
  }

  private static onValue(currentArg: unknown): unknown {
    return typeof currentArg === 'number' ? 99 : true;
  }

  private static offValue(currentArg: unknown): unknown {
    return typeof currentArg === 'number' ? 0 : false;
  }

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

  private static nodeDeviceId(nodeArg: IZwaveJsNode): string {
    return `zwave_js.node.${nodeArg.nodeId}`;
  }

  private static controllerDeviceId(snapshotArg: IZwaveJsSnapshot): string {
    return `zwave_js.controller.${snapshotArg.controller?.homeId ?? snapshotArg.version?.homeId ?? 'network'}`;
  }

  private static nodeName(nodeArg: IZwaveJsNode): string {
    return nodeArg.name || nodeArg.productLabel || nodeArg.productDescription || `Z-Wave node ${nodeArg.nodeId}`;
  }

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

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