import * as plugins from '../../plugins.js';
import type { IIntegrationEntity, IIntegrationEvent, IServiceCallRequest } from '../../core/types.js';
import type {
  IVelbusChannel,
  IVelbusCommand,
  IVelbusEntity,
  IVelbusEvent,
  IVelbusModule,
  IVelbusSnapshot,
} from './velbus.types.js';

const velbusDomain = 'velbus';

export class VelbusMapper {
  public static toDevices(snapshotArg: IVelbusSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
    const gatewayDeviceId = this.gatewayDeviceId(snapshotArg);
    const devices: plugins.shxInterfaces.data.IDeviceDefinition[] = [{
      id: gatewayDeviceId,
      integrationDomain: velbusDomain,
      name: snapshotArg.gateway.name || 'Velbus Gateway',
      protocol: 'unknown',
      manufacturer: snapshotArg.gateway.manufacturer || 'Velleman',
      model: snapshotArg.gateway.model || (snapshotArg.gateway.connection === 'tcp' ? 'Velbus TCP/IP interface' : snapshotArg.gateway.connection === 'serial' ? 'Velbus serial interface' : 'Velbus manual snapshot'),
      online: snapshotArg.connected,
      features: [
        { id: 'connectivity', capability: 'sensor', name: 'Connectivity', readable: true, writable: false },
        { id: 'module_count', capability: 'sensor', name: 'Module count', readable: true, writable: false },
      ],
      state: [
        { featureId: 'connectivity', value: snapshotArg.connected ? 'online' : 'offline', updatedAt },
        { featureId: 'module_count', value: snapshotArg.modules.length, updatedAt },
      ],
      metadata: this.cleanAttributes({
        connection: snapshotArg.gateway.connection,
        dsn: snapshotArg.gateway.dsn,
        serialPath: snapshotArg.gateway.serialPath,
        host: snapshotArg.gateway.host,
        port: snapshotArg.gateway.port,
        tls: snapshotArg.gateway.tls,
        serialNumber: snapshotArg.gateway.serialNumber,
        vlpFile: snapshotArg.gateway.vlpFile,
        limitations: snapshotArg.limitations,
        ...snapshotArg.gateway.metadata,
      }),
    }];

    for (const module of snapshotArg.modules) {
      devices.push({
        id: this.moduleDeviceId(module),
        integrationDomain: velbusDomain,
        name: module.name || `Velbus Module ${module.address}`,
        room: module.room,
        protocol: 'unknown',
        manufacturer: module.manufacturer || 'Velleman',
        model: module.typeName || (module.type !== undefined ? String(module.type) : undefined),
        online: snapshotArg.connected && module.connected !== false,
        features: module.channels.map((channelArg) => ({
          id: this.channelFeatureId(channelArg),
          capability: this.capabilityForChannel(channelArg),
          name: channelArg.name || `Channel ${channelArg.channelNumber ?? channelArg.id}`,
          readable: true,
          writable: this.isWritable(channelArg),
          unit: channelArg.unit,
        })),
        state: module.channels.map((channelArg) => ({
          featureId: this.channelFeatureId(channelArg),
          value: this.deviceStateValue(channelArg),
          updatedAt,
        })),
        metadata: this.cleanAttributes({
          gatewayDeviceId,
          address: module.address,
          type: module.type,
          typeName: module.typeName,
          swVersion: module.swVersion,
          serialNumber: module.serialNumber,
          ...module.attributes,
        }),
      });
    }

    return devices;
  }

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

    for (const module of snapshotArg.modules) {
      for (const channel of module.channels) {
        const available = snapshotArg.connected && module.connected !== false && channel.connected !== false;
        if (channel.kind === 'dimmer' || channel.kind === 'led') {
          const name = channel.kind === 'led' && !(channel.name || '').toLowerCase().startsWith('led') ? `LED ${channel.name || this.channelName(channel)}` : this.channelName(channel);
          entities.push(this.entity('light', name, module, channel, usedIds, this.boolState(channel) ? 'on' : 'off', {
            brightness: this.brightness(channel),
            colorMode: channel.kind === 'dimmer' ? 'brightness' : 'onoff',
            transition: channel.kind === 'dimmer',
            flash: channel.kind === 'led',
          }, available));
          continue;
        }

        if (channel.kind === 'relay') {
          entities.push(this.entity('switch', this.channelName(channel), module, channel, usedIds, this.boolState(channel) ? 'on' : 'off', {}, available));
          continue;
        }

        if (channel.kind === 'button' || channel.kind === 'binary_sensor') {
          entities.push(this.entity('binary_sensor', this.channelName(channel), module, channel, usedIds, this.boolState(channel) ? 'on' : 'off', {
            deviceClass: channel.deviceClass,
          }, available));
          continue;
        }

        if (channel.kind === 'blind') {
          entities.push(this.entity('cover', this.channelName(channel), module, channel, usedIds, this.coverState(channel), {
            currentPosition: channel.position,
            supportedFeatures: channel.position === undefined ? ['open', 'close', 'stop'] : ['open', 'close', 'stop', 'set_position'],
          }, available));
          continue;
        }

        if (channel.kind === 'climate') {
          entities.push(this.entity('climate', this.channelName(channel), module, channel, usedIds, channel.hvacMode || 'heat', {
            currentTemperature: channel.currentTemperature,
            targetTemperature: channel.targetTemperature,
            presetMode: channel.presetMode,
            hvacModes: ['heat', 'cool'],
            presetModes: ['away', 'comfort', 'eco', 'home'],
            temperatureUnit: channel.unit || 'C',
          }, available));
          continue;
        }

        entities.push(this.entity('sensor', this.channelName(channel), module, channel, usedIds, this.sensorValue(channel), {
          unit: channel.unit,
          deviceClass: channel.deviceClass || (channel.kind === 'temperature' ? 'temperature' : undefined),
          stateClass: channel.stateClass,
        }, available));

        if (channel.kind === 'counter' && channel.counterValue !== undefined) {
          entities.push(this.entity('sensor', `${this.channelName(channel)} counter`, module, channel, usedIds, channel.counterValue, {
            unit: channel.counterUnit,
            deviceClass: 'energy',
            stateClass: 'total_increasing',
            counter: true,
          }, available, '-counter'));
        }
      }
    }

    return entities;
  }

  public static commandForService(snapshotArg: IVelbusSnapshot, requestArg: IServiceCallRequest): IVelbusCommand | undefined {
    if (requestArg.domain === 'light' && requestArg.service === 'turn_on') {
      const target = this.findTarget(snapshotArg, requestArg, ['light']);
      return target ? this.command('turn_on', target, requestArg, this.brightnessValue(requestArg)) : undefined;
    }

    if (requestArg.domain === 'light' && requestArg.service === 'turn_off') {
      const target = this.findTarget(snapshotArg, requestArg, ['light']);
      return target ? this.command('turn_off', target, requestArg) : undefined;
    }

    if (requestArg.domain === 'switch' && requestArg.service === 'turn_on') {
      const target = this.findTarget(snapshotArg, requestArg, ['switch']);
      return target ? this.command('turn_on', target, requestArg) : undefined;
    }

    if (requestArg.domain === 'switch' && requestArg.service === 'turn_off') {
      const target = this.findTarget(snapshotArg, requestArg, ['switch']);
      return target ? this.command('turn_off', target, requestArg) : undefined;
    }

    if (requestArg.domain === velbusDomain && requestArg.service === 'turn_on') {
      const target = this.findTarget(snapshotArg, requestArg, ['light', 'switch']);
      return target ? this.command('turn_on', target, requestArg, target.entity.platform === 'light' ? this.brightnessValue(requestArg) : undefined) : undefined;
    }

    if (requestArg.domain === velbusDomain && requestArg.service === 'turn_off') {
      const target = this.findTarget(snapshotArg, requestArg, ['light', 'switch']);
      return target ? this.command('turn_off', target, requestArg) : undefined;
    }

    if ((requestArg.domain === 'cover' && ['open_cover', 'open'].includes(requestArg.service)) || (requestArg.domain === velbusDomain && requestArg.service === 'open')) {
      const target = this.findTarget(snapshotArg, requestArg, ['cover']);
      return target ? this.command('open', target, requestArg) : undefined;
    }

    if ((requestArg.domain === 'cover' && ['close_cover', 'close'].includes(requestArg.service)) || (requestArg.domain === velbusDomain && requestArg.service === 'close')) {
      const target = this.findTarget(snapshotArg, requestArg, ['cover']);
      return target ? this.command('close', target, requestArg) : undefined;
    }

    if ((requestArg.domain === 'cover' && requestArg.service === 'stop_cover') || (requestArg.domain === velbusDomain && requestArg.service === 'stop')) {
      const target = this.findTarget(snapshotArg, requestArg, ['cover']);
      return target ? this.command('stop', target, requestArg) : undefined;
    }

    if (requestArg.service === 'set_value' || (requestArg.domain === 'cover' && requestArg.service === 'set_cover_position') || (requestArg.domain === 'climate' && requestArg.service === 'set_temperature')) {
      const target = this.findTarget(snapshotArg, requestArg, ['light', 'cover', 'climate', 'sensor']);
      const value = this.setValue(requestArg, target?.entity.platform);
      return target && value !== undefined ? this.command('set_value', target, requestArg, value) : undefined;
    }

    return undefined;
  }

  public static toIntegrationEvent(eventArg: IVelbusEvent): IIntegrationEvent {
    return {
      type: eventArg.type === 'command_failed' ? 'error' : 'state_changed',
      integrationDomain: velbusDomain,
      deviceId: eventArg.deviceId,
      entityId: eventArg.entityId,
      data: eventArg,
      timestamp: eventArg.timestamp,
    };
  }

  public static gatewayDeviceId(snapshotArg: IVelbusSnapshot): string {
    return `velbus.gateway.${this.slug(snapshotArg.gateway.id || snapshotArg.gateway.serialNumber || snapshotArg.gateway.dsn || snapshotArg.gateway.host || snapshotArg.gateway.serialPath || 'manual')}`;
  }

  public static moduleDeviceId(moduleArg: IVelbusModule): string {
    return `velbus.module.${this.slug(moduleArg.serialNumber || moduleArg.id || String(moduleArg.address))}`;
  }

  private static entity(platformArg: IIntegrationEntity['platform'], nameArg: string, moduleArg: IVelbusModule, channelArg: IVelbusChannel, usedIdsArg: Map<string, number>, stateArg: unknown, attributesArg: Record<string, unknown>, availableArg: boolean, uniqueSuffixArg = ''): IVelbusEntity {
    const baseId = `${platformArg}.${this.slug(nameArg) || this.slug(`${moduleArg.address}_${channelArg.id}`)}`;
    const seen = usedIdsArg.get(baseId) || 0;
    usedIdsArg.set(baseId, seen + 1);
    return {
      id: seen ? `${baseId}_${seen + 1}` : baseId,
      uniqueId: `velbus_${this.slug(moduleArg.serialNumber || String(moduleArg.address))}_${this.slug(String(channelArg.channelNumber ?? channelArg.id))}${uniqueSuffixArg}`,
      integrationDomain: velbusDomain,
      deviceId: this.moduleDeviceId(moduleArg),
      platform: platformArg,
      name: nameArg,
      state: stateArg,
      attributes: this.cleanAttributes({
        moduleAddress: moduleArg.address,
        channelId: channelArg.id,
        channelNumber: channelArg.channelNumber,
        kind: channelArg.kind,
        moduleType: moduleArg.type,
        moduleTypeName: moduleArg.typeName,
        moduleSerialNumber: moduleArg.serialNumber,
        ...attributesArg,
        ...channelArg.attributes,
      }),
      available: availableArg,
    };
  }

  private static command(typeArg: IVelbusCommand['type'], targetArg: { entity: IIntegrationEntity; module: IVelbusModule; channel: IVelbusChannel }, requestArg: IServiceCallRequest, valueArg?: number | string | boolean): IVelbusCommand {
    return this.cleanAttributes({
      type: typeArg,
      moduleAddress: targetArg.module.address,
      channelId: targetArg.channel.id,
      channelNumber: targetArg.channel.channelNumber,
      platform: targetArg.entity.platform,
      entityId: requestArg.target.entityId,
      deviceId: requestArg.target.deviceId,
      value: valueArg,
      transition: this.numberData(requestArg, 'transition'),
    }) as unknown as IVelbusCommand;
  }

  private static findTarget(snapshotArg: IVelbusSnapshot, requestArg: IServiceCallRequest, platformsArg: IIntegrationEntity['platform'][]): { entity: IIntegrationEntity; module: IVelbusModule; channel: IVelbusChannel } | undefined {
    const entities = this.toEntities(snapshotArg).filter((entityArg) => platformsArg.includes(entityArg.platform));
    const targetId = requestArg.target.entityId || requestArg.target.deviceId;
    const entity = targetId ? entities.find((entityArg) => entityArg.id === targetId || entityArg.uniqueId === targetId || entityArg.deviceId === targetId) : entities.length === 1 ? entities[0] : undefined;
    if (entity) {
      const moduleAddress = typeof entity.attributes?.moduleAddress === 'number' ? entity.attributes.moduleAddress : undefined;
      const channelId = typeof entity.attributes?.channelId === 'string' ? entity.attributes.channelId : undefined;
      const found = this.findChannel(snapshotArg, moduleAddress, channelId, undefined);
      return found ? { entity, ...found } : undefined;
    }

    const explicitModuleAddress = this.numberData(requestArg, 'moduleAddress') ?? this.numberData(requestArg, 'address');
    const explicitChannelNumber = this.numberData(requestArg, 'channelNumber') ?? this.numberData(requestArg, 'channel');
    const explicitChannelId = this.stringData(requestArg, 'channelId');
    const found = this.findChannel(snapshotArg, explicitModuleAddress, explicitChannelId, explicitChannelNumber);
    if (!found) {
      return undefined;
    }
    const foundEntity = entities.find((entityArg) => entityArg.attributes?.moduleAddress === found.module.address && entityArg.attributes?.channelId === found.channel.id);
    return foundEntity ? { entity: foundEntity, ...found } : undefined;
  }

  private static findChannel(snapshotArg: IVelbusSnapshot, moduleAddressArg?: number, channelIdArg?: string, channelNumberArg?: number): { module: IVelbusModule; channel: IVelbusChannel } | undefined {
    for (const module of snapshotArg.modules) {
      if (moduleAddressArg !== undefined && module.address !== moduleAddressArg) {
        continue;
      }
      for (const channel of module.channels) {
        if (channelIdArg !== undefined && channel.id === channelIdArg) {
          return { module, channel };
        }
        if (channelNumberArg !== undefined && channel.channelNumber === channelNumberArg) {
          return { module, channel };
        }
      }
    }
    return undefined;
  }

  private static channelFeatureId(channelArg: IVelbusChannel): string {
    return `channel_${this.slug(String(channelArg.channelNumber ?? channelArg.id))}`;
  }

  private static capabilityForChannel(channelArg: IVelbusChannel): plugins.shxInterfaces.data.TDeviceCapability {
    if (channelArg.kind === 'relay') {
      return 'switch';
    }
    if (channelArg.kind === 'dimmer' || channelArg.kind === 'led') {
      return 'light';
    }
    if (channelArg.kind === 'blind') {
      return 'cover';
    }
    if (channelArg.kind === 'climate') {
      return 'climate';
    }
    return 'sensor';
  }

  private static deviceStateValue(channelArg: IVelbusChannel): plugins.shxInterfaces.data.TDeviceStateValue {
    if (channelArg.kind === 'blind') {
      return this.cleanAttributes({ state: this.coverState(channelArg), position: channelArg.position });
    }
    if (channelArg.kind === 'climate') {
      return this.cleanAttributes({ currentTemperature: channelArg.currentTemperature, targetTemperature: channelArg.targetTemperature, hvacMode: channelArg.hvacMode, presetMode: channelArg.presetMode });
    }
    if (channelArg.kind === 'dimmer' || channelArg.kind === 'led') {
      return this.cleanAttributes({ on: this.boolState(channelArg), brightness: this.brightness(channelArg) });
    }
    return channelArg.state ?? this.sensorValue(channelArg) ?? null;
  }

  private static channelName(channelArg: IVelbusChannel): string {
    return channelArg.name || `Channel ${channelArg.channelNumber ?? channelArg.id}`;
  }

  private static isWritable(channelArg: IVelbusChannel): boolean {
    if (channelArg.writable !== undefined) {
      return channelArg.writable;
    }
    return ['relay', 'dimmer', 'led', 'blind', 'climate'].includes(channelArg.kind);
  }

  private static boolState(channelArg: IVelbusChannel): boolean {
    if (typeof channelArg.state === 'boolean') {
      return channelArg.state;
    }
    if (typeof channelArg.state === 'number') {
      return channelArg.state > 0;
    }
    if (typeof channelArg.state === 'string') {
      return ['on', 'true', 'active', 'closed', 'pressed', 'slow', 'fast'].includes(channelArg.state.toLowerCase());
    }
    return Boolean(channelArg.brightness && channelArg.brightness > 0);
  }

  private static brightness(channelArg: IVelbusChannel): number | undefined {
    if (typeof channelArg.brightness === 'number') {
      return Math.min(100, Math.max(0, channelArg.brightness));
    }
    if (channelArg.kind === 'dimmer' && typeof channelArg.state === 'number') {
      return Math.min(100, Math.max(0, channelArg.state));
    }
    return channelArg.kind === 'dimmer' ? (this.boolState(channelArg) ? 100 : 0) : undefined;
  }

  private static coverState(channelArg: IVelbusChannel): string {
    if (typeof channelArg.state === 'string' && ['open', 'closed', 'opening', 'closing'].includes(channelArg.state.toLowerCase())) {
      return channelArg.state.toLowerCase();
    }
    if (typeof channelArg.position === 'number') {
      return channelArg.position <= 0 ? 'closed' : 'open';
    }
    return 'unknown';
  }

  private static sensorValue(channelArg: IVelbusChannel): number | string | boolean | null {
    if (channelArg.kind === 'temperature' && typeof channelArg.currentTemperature === 'number') {
      return channelArg.currentTemperature;
    }
    return channelArg.state ?? null;
  }

  private static brightnessValue(requestArg: IServiceCallRequest): number | undefined {
    const percent = this.numberData(requestArg, 'brightnessPercent') ?? this.numberData(requestArg, 'brightness_pct');
    if (percent !== undefined) {
      return Math.min(100, Math.max(0, percent));
    }
    const brightness = this.numberData(requestArg, 'brightness');
    if (brightness !== undefined) {
      return Math.round(Math.min(255, Math.max(0, brightness)) / 255 * 100);
    }
    return this.numberData(requestArg, 'value');
  }

  private static setValue(requestArg: IServiceCallRequest, platformArg?: IIntegrationEntity['platform']): number | string | boolean | undefined {
    if (platformArg === 'cover') {
      return this.numberData(requestArg, 'position') ?? this.numberData(requestArg, 'value');
    }
    if (platformArg === 'light') {
      return this.brightnessValue(requestArg);
    }
    if (platformArg === 'climate') {
      return this.numberData(requestArg, 'temperature') ?? this.numberData(requestArg, 'value');
    }
    const value = requestArg.data?.value;
    return typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean' ? value : undefined;
  }

  private static numberData(requestArg: IServiceCallRequest, keyArg: string): number | undefined {
    const value = requestArg.data?.[keyArg];
    return typeof value === 'number' && Number.isFinite(value) ? value : typeof value === 'string' && value.trim() && Number.isFinite(Number(value)) ? Number(value) : undefined;
  }

  private static stringData(requestArg: IServiceCallRequest, keyArg: string): string | undefined {
    const value = requestArg.data?.[keyArg];
    return typeof value === 'string' && value.trim() ? value.trim() : undefined;
  }

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

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