import * as plugins from '../../plugins.js';
import type { IIntegrationEntity, IIntegrationEvent, IServiceCallRequest } from '../../core/types.js';
import { wakeOnLanDefaultBroadcastAddress, wakeOnLanDefaultBroadcastPort, wakeOnLanDefaultName, wakeOnLanDomain } from './wake_on_lan.constants.js';
import { createWakeOnLanMagicPacket, normalizeWakeOnLanMac } from './wake_on_lan.packet.js';
import type {
  IWakeOnLanCommand,
  IWakeOnLanConfig,
  IWakeOnLanEntityDescriptor,
  IWakeOnLanEvent,
  IWakeOnLanManualEntry,
  IWakeOnLanSnapshot,
  IWakeOnLanTarget,
  TWakeOnLanEntityKind,
} from './wake_on_lan.types.js';

interface INormalizedWakeOnLanTarget extends IWakeOnLanTarget {
  normalizedMac?: string;
  entityTypes: TWakeOnLanEntityKind[];
}

export class WakeOnLanMapper {
  public static toSnapshot(configArg: IWakeOnLanConfig, connectedArg?: boolean, eventsArg: IWakeOnLanEvent[] = []): IWakeOnLanSnapshot {
    const source = configArg.snapshot;
    const targets = this.uniqueTargets([
      ...(source?.targets || []),
      ...(configArg.targets || []),
      ...(configArg.devices || []),
      ...(configArg.entries || []),
      ...(configArg.buttons || []).map((targetArg) => ({ ...targetArg, platform: targetArg.platform || 'button' as const })),
      ...(configArg.switches || []).map((targetArg) => ({ ...targetArg, platform: targetArg.platform || 'switch' as const })),
      ...this.targetsFromManualEntries(configArg.manualEntries || []),
      ...this.primaryTargets(configArg),
    ]);
    const connected = connectedArg ?? configArg.connected ?? source?.connected ?? targets.some((targetArg) => targetArg.available === true || targetArg.online === true || Boolean(targetArg.normalizedMac));

    return {
      connected,
      targets,
      entities: [...(source?.entities || []), ...(configArg.entities || [])],
      events: [...(source?.events || []), ...eventsArg],
      updatedAt: source?.updatedAt || new Date().toISOString(),
      metadata: {
        ...source?.metadata,
        ...configArg.metadata,
        nativeUdpImplemented: true,
        nativeUdpEnabled: configArg.nativeUdpEnabled !== false,
        commandExecutorConfigured: Boolean(configArg.commandExecutor || configArg.transport?.execute),
      },
    };
  }

  public static toDevices(snapshotArg: IWakeOnLanSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
    return this.normalizedTargets(snapshotArg.targets).map((targetArg) => {
      const features: plugins.shxInterfaces.data.IDeviceFeature[] = [{ id: 'magic_packet', capability: 'switch', name: 'Magic Packet', readable: false, writable: true }];
      const state: plugins.shxInterfaces.data.IDeviceState[] = [{ featureId: 'magic_packet', value: false, updatedAt }];
      if (targetArg.entityTypes.includes('switch')) {
        features.push({ id: 'power', capability: 'switch', name: 'Power', readable: true, writable: true });
        state.push({ featureId: 'power', value: this.switchIsOn(targetArg), updatedAt });
      }
      if (targetArg.host) {
        features.push({ id: 'host', capability: 'sensor', name: 'Host', readable: true, writable: false });
        state.push({ featureId: 'host', value: targetArg.host, updatedAt });
      }
      return {
        id: this.deviceId(targetArg),
        integrationDomain: wakeOnLanDomain,
        name: this.targetName(targetArg),
        protocol: 'unknown',
        manufacturer: 'Wake on LAN',
        model: targetArg.host ? 'Magic Packet Host Target' : 'Magic Packet Target',
        online: this.available(snapshotArg, targetArg),
        features: this.uniqueFeatures(features),
        state,
        metadata: this.cleanAttributes({
          macAddress: targetArg.normalizedMac,
          host: targetArg.host,
          broadcastAddress: this.broadcastAddress(targetArg),
          broadcastPort: this.broadcastPort(targetArg),
          entityTypes: targetArg.entityTypes,
          nativeUdpImplemented: true,
          ...targetArg.metadata,
        }),
      };
    });
  }

  public static toEntities(snapshotArg: IWakeOnLanSnapshot): 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 target of this.normalizedTargets(snapshotArg.targets)) {
      if (target.entityTypes.includes('button')) {
        addEntity(this.buttonEntity(snapshotArg, target, usedIds));
      }
      if (target.entityTypes.includes('switch')) {
        addEntity(this.switchEntity(snapshotArg, target, usedIds));
      }
    }

    return entities;
  }

  public static commandForService(snapshotArg: IWakeOnLanSnapshot, requestArg: IServiceCallRequest): IWakeOnLanCommand | undefined {
    if (requestArg.domain === wakeOnLanDomain && requestArg.service === 'send_magic_packet') {
      return this.magicPacketCommand(snapshotArg, requestArg, 'send_magic_packet');
    }

    const targetEntity = this.findTargetEntity(snapshotArg, requestArg);
    if (requestArg.domain === 'button' && requestArg.service === 'press' && targetEntity?.attributes?.nativeAction === 'send_magic_packet') {
      return this.magicPacketCommand(snapshotArg, requestArg, 'button_press', targetEntity);
    }
    if (requestArg.domain === 'switch' && requestArg.service === 'turn_on') {
      return targetEntity ? this.magicPacketCommand(snapshotArg, requestArg, 'switch_turn_on', targetEntity) : undefined;
    }
    if (requestArg.domain === 'switch' && requestArg.service === 'turn_off' && targetEntity) {
      const target = this.findTargetByEntity(snapshotArg, targetEntity);
      return {
        type: 'switch_turn_off',
        method: 'local_turn_off_action',
        service: requestArg.service,
        target: requestArg.target,
        data: requestArg.data,
        deviceId: targetEntity.deviceId,
        entityId: targetEntity.id,
        targetId: target?.id,
        macAddress: target?.normalizedMac,
        normalizedMac: target?.normalizedMac,
        host: target?.host,
        offAction: target?.offAction ?? target?.turn_off,
        transmitted: false,
      };
    }

    return undefined;
  }

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

  public static deviceId(targetArg: IWakeOnLanTarget): string {
    return `wake_on_lan.device.${this.slug(targetArg.id || targetArg.normalizedMac || targetArg.macAddress || targetArg.mac || targetArg.host || targetArg.name || 'target')}`;
  }

  public static normalizeMac(valueArg?: string): string | undefined {
    return normalizeWakeOnLanMac(valueArg);
  }

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

  private static magicPacketCommand(snapshotArg: IWakeOnLanSnapshot, requestArg: IServiceCallRequest, typeArg: IWakeOnLanCommand['type'], entityArg?: IIntegrationEntity): IWakeOnLanCommand | undefined {
    const target = entityArg ? this.findTargetByEntity(snapshotArg, entityArg) : this.findTargetByRequest(snapshotArg, requestArg);
    const data = requestArg.data || {};
    const macAddress = this.normalizeMac(this.stringValue(data.mac) || this.stringValue(data.macAddress) || this.stringValue(data.mac_address) || this.stringValue(entityArg?.attributes?.macAddress) || target?.normalizedMac || target?.macAddress || target?.mac);
    if (!macAddress) {
      return undefined;
    }
    const broadcastAddress = this.stringValue(data.broadcast_address) || this.stringValue(data.broadcastAddress) || this.stringValue(data.ip_address) || this.stringValue(data.ipAddress) || (target ? this.broadcastAddress(target) : wakeOnLanDefaultBroadcastAddress);
    const broadcastPort = this.portValue(data.broadcast_port ?? data.broadcastPort ?? data.port) ?? (target ? this.broadcastPort(target) : wakeOnLanDefaultBroadcastPort);
    const packet = createWakeOnLanMagicPacket(macAddress, { broadcastAddress, broadcastPort });
    return {
      type: typeArg,
      method: 'wakeonlan.send_magic_packet',
      service: requestArg.service,
      target: requestArg.target,
      data,
      deviceId: entityArg?.deviceId || (target ? this.deviceId(target) : undefined),
      entityId: entityArg?.id || target?.entityId || requestArg.target.entityId,
      targetId: target?.id,
      macAddress,
      normalizedMac: macAddress,
      host: target?.host,
      broadcastAddress,
      broadcastPort,
      packet,
      transmitted: false,
      metadata: this.cleanAttributes({ entityPlatform: entityArg?.platform, assumedState: target?.assumedState }),
    };
  }

  private static entityFromDescriptor(snapshotArg: IWakeOnLanSnapshot, descriptorArg: IWakeOnLanEntityDescriptor, usedIdsArg: Map<string, number>): IIntegrationEntity | undefined {
    const platform = descriptorArg.platform;
    if (platform !== 'button' && platform !== 'switch') {
      return undefined;
    }
    const target = this.normalizedTargets(snapshotArg.targets).find((targetArg) => targetArg.id === descriptorArg.targetId || targetArg.normalizedMac === this.normalizeMac(descriptorArg.macAddress));
    const name = descriptorArg.name || (target ? this.entityName(target, platform) : `${wakeOnLanDefaultName} ${platform}`);
    return this.entity(platform, name, descriptorArg.deviceId || (target ? this.deviceId(target) : `wake_on_lan.device.${this.slug(name)}`), descriptorArg.uniqueId || `wake_on_lan_${this.slug(descriptorArg.id || descriptorArg.entityId || name)}`, descriptorArg.state ?? (platform === 'button' ? 'available' : target ? this.switchState(target) : 'unknown'), usedIdsArg, {
      nativeAction: 'send_magic_packet',
      macAddress: target?.normalizedMac || this.normalizeMac(descriptorArg.macAddress),
      targetId: target?.id || descriptorArg.targetId,
      writable: true,
      ...descriptorArg.attributes,
    }, descriptorArg.available ?? (target ? this.available(snapshotArg, target) : true), descriptorArg.entityId || descriptorArg.id);
  }

  private static buttonEntity(snapshotArg: IWakeOnLanSnapshot, targetArg: INormalizedWakeOnLanTarget, usedIdsArg: Map<string, number>): IIntegrationEntity {
    return this.entity('button', this.entityName(targetArg, 'button'), this.deviceId(targetArg), targetArg.uniqueId ? `${targetArg.uniqueId}_button` : `wake_on_lan_${this.slug(targetArg.normalizedMac || targetArg.id || targetArg.name)}_button`, 'available', usedIdsArg, {
      nativeAction: 'send_magic_packet',
      nativeType: 'magic_packet',
      macAddress: targetArg.normalizedMac,
      host: targetArg.host,
      broadcastAddress: this.broadcastAddress(targetArg),
      broadcastPort: this.broadcastPort(targetArg),
      entityCategory: 'config',
      writable: true,
      ...targetArg.metadata,
    }, this.available(snapshotArg, targetArg), targetArg.buttonEntityId || (targetArg.platform === 'button' ? targetArg.entityId : undefined));
  }

  private static switchEntity(snapshotArg: IWakeOnLanSnapshot, targetArg: INormalizedWakeOnLanTarget, usedIdsArg: Map<string, number>): IIntegrationEntity {
    const assumedState = targetArg.assumedState ?? !targetArg.host;
    return this.entity('switch', this.entityName(targetArg, 'switch'), this.deviceId(targetArg), targetArg.uniqueId ? `${targetArg.uniqueId}_switch` : `wake_on_lan_${this.slug(targetArg.normalizedMac || targetArg.id || targetArg.name)}_switch`, this.switchState(targetArg), usedIdsArg, {
      nativeAction: 'send_magic_packet',
      nativeType: 'wol_switch',
      macAddress: targetArg.normalizedMac,
      host: targetArg.host,
      broadcastAddress: this.broadcastAddress(targetArg),
      broadcastPort: this.broadcastPort(targetArg),
      assumedState,
      shouldPoll: !assumedState,
      turnOffActionConfigured: targetArg.offAction !== undefined || targetArg.turn_off !== undefined,
      writable: true,
      ...targetArg.metadata,
    }, this.available(snapshotArg, targetArg), targetArg.switchEntityId || (targetArg.platform === 'switch' ? targetArg.entityId : undefined));
  }

  private static entity(platformArg: TWakeOnLanEntityKind, nameArg: string, deviceIdArg: string, uniqueIdArg: string, stateArg: unknown, usedIdsArg: Map<string, number>, attributesArg: Record<string, unknown>, availableArg: boolean, entityIdArg?: string): IIntegrationEntity {
    const id = entityIdArg || this.uniqueEntityId(platformArg, nameArg, usedIdsArg);
    return {
      id,
      uniqueId: uniqueIdArg,
      integrationDomain: wakeOnLanDomain,
      deviceId: deviceIdArg,
      platform: platformArg,
      name: nameArg,
      state: stateArg,
      attributes: this.cleanAttributes(attributesArg),
      available: availableArg,
    };
  }

  private static findTargetEntity(snapshotArg: IWakeOnLanSnapshot, 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 && entityArg.attributes?.writable === true);
    }
    return entities.find((entityArg) => entityArg.platform === requestArg.domain && entityArg.attributes?.writable === true);
  }

  private static findTargetByRequest(snapshotArg: IWakeOnLanSnapshot, requestArg: IServiceCallRequest): INormalizedWakeOnLanTarget | undefined {
    const targetEntity = this.findTargetEntity(snapshotArg, requestArg);
    return targetEntity ? this.findTargetByEntity(snapshotArg, targetEntity) : undefined;
  }

  private static findTargetByEntity(snapshotArg: IWakeOnLanSnapshot, entityArg: IIntegrationEntity): INormalizedWakeOnLanTarget | undefined {
    const targetId = this.stringValue(entityArg.attributes?.targetId);
    const mac = this.normalizeMac(this.stringValue(entityArg.attributes?.macAddress));
    return this.normalizedTargets(snapshotArg.targets).find((targetArg) => targetArg.id === targetId || targetArg.normalizedMac === mac || this.deviceId(targetArg) === entityArg.deviceId);
  }

  private static targetsFromManualEntries(entriesArg: IWakeOnLanManualEntry[]): IWakeOnLanTarget[] {
    return entriesArg.flatMap((entryArg) => [
      ...this.primaryTargets(entryArg),
      ...(entryArg.targets || []),
      ...(entryArg.buttons || []).map((targetArg) => ({ ...targetArg, platform: targetArg.platform || 'button' as const })),
      ...(entryArg.switches || []).map((targetArg) => ({ ...targetArg, platform: targetArg.platform || 'switch' as const })),
      ...(entryArg.snapshot?.targets || []),
    ]);
  }

  private static primaryTargets(configArg: IWakeOnLanTarget): IWakeOnLanTarget[] {
    if (!configArg.macAddress && !configArg.mac && !configArg.host && !configArg.name) {
      return [];
    }
    const explicitPlatform = configArg.platform || (configArg.createButton && configArg.createSwitch ? 'both' : configArg.createSwitch ? 'switch' : configArg.createButton ? 'button' : undefined);
    return [{
      id: configArg.id,
      name: configArg.name,
      macAddress: configArg.macAddress,
      mac: configArg.mac,
      host: configArg.host,
      broadcastAddress: configArg.broadcastAddress,
      broadcast_address: configArg.broadcast_address,
      broadcastPort: configArg.broadcastPort,
      broadcast_port: configArg.broadcast_port,
      platform: explicitPlatform || 'button',
      entityTypes: configArg.entityTypes,
      entityId: configArg.entityId,
      switchEntityId: configArg.switchEntityId,
      buttonEntityId: configArg.buttonEntityId,
      uniqueId: configArg.uniqueId,
      state: configArg.state,
      online: configArg.online,
      available: configArg.available,
      assumedState: configArg.assumedState,
      offAction: configArg.offAction,
      turn_off: configArg.turn_off,
      metadata: configArg.metadata,
    }];
  }

  private static uniqueTargets(targetsArg: IWakeOnLanTarget[]): INormalizedWakeOnLanTarget[] {
    const targets: INormalizedWakeOnLanTarget[] = [];
    const seen = new Set<string>();
    for (const target of targetsArg) {
      const normalized = this.normalizeTarget(target);
      const key = normalized.normalizedMac || normalized.id || normalized.host || normalized.name;
      if (!key) {
        continue;
      }
      if (seen.has(key)) {
        const existing = targets.find((targetArg) => (targetArg.normalizedMac || targetArg.id || targetArg.host || targetArg.name) === key);
        if (existing) {
          Object.assign(existing, this.cleanAttributes({ ...normalized, entityTypes: this.uniqueEntityTypes([...existing.entityTypes, ...normalized.entityTypes]) }));
        }
        continue;
      }
      seen.add(key);
      targets.push(normalized);
    }
    return targets;
  }

  private static normalizedTargets(targetsArg: IWakeOnLanTarget[]): INormalizedWakeOnLanTarget[] {
    return targetsArg.map((targetArg) => this.normalizeTarget(targetArg));
  }

  private static normalizeTarget(targetArg: IWakeOnLanTarget): INormalizedWakeOnLanTarget {
    const normalizedMac = this.normalizeMac(targetArg.normalizedMac || targetArg.macAddress || targetArg.mac);
    const id = targetArg.id || normalizedMac || targetArg.host || targetArg.name;
    return {
      ...targetArg,
      id,
      macAddress: normalizedMac || targetArg.macAddress,
      normalizedMac,
      name: targetArg.name || (normalizedMac ? `${wakeOnLanDefaultName} ${normalizedMac}` : targetArg.host || wakeOnLanDefaultName),
      entityTypes: this.entityTypes(targetArg),
    };
  }

  private static entityTypes(targetArg: IWakeOnLanTarget): TWakeOnLanEntityKind[] {
    const explicit = targetArg.entityTypes?.filter((valueArg): valueArg is TWakeOnLanEntityKind => valueArg === 'button' || valueArg === 'switch') || [];
    if (explicit.length) {
      return this.uniqueEntityTypes(explicit);
    }
    if (targetArg.platform === 'both') {
      return ['button', 'switch'];
    }
    if (targetArg.platform === 'button' || targetArg.platform === 'switch') {
      return [targetArg.platform];
    }
    const derived: TWakeOnLanEntityKind[] = [];
    if (targetArg.createButton) derived.push('button');
    if (targetArg.createSwitch) derived.push('switch');
    return derived.length ? this.uniqueEntityTypes(derived) : ['button'];
  }

  private static uniqueEntityTypes(valuesArg: TWakeOnLanEntityKind[]): TWakeOnLanEntityKind[] {
    return [...new Set(valuesArg)];
  }

  private static available(snapshotArg: IWakeOnLanSnapshot, targetArg: IWakeOnLanTarget): boolean {
    return targetArg.available ?? Boolean(targetArg.normalizedMac && snapshotArg.connected);
  }

  private static switchState(targetArg: IWakeOnLanTarget): string {
    const state = targetArg.state;
    if (typeof state === 'boolean') {
      return state ? 'on' : 'off';
    }
    if (typeof state === 'string' && state.trim()) {
      return state.trim().toLowerCase();
    }
    if (typeof targetArg.online === 'boolean') {
      return targetArg.online ? 'on' : 'off';
    }
    return 'off';
  }

  private static switchIsOn(targetArg: IWakeOnLanTarget): boolean {
    return this.switchState(targetArg) === 'on';
  }

  private static targetName(targetArg: IWakeOnLanTarget): string {
    return targetArg.name || (targetArg.normalizedMac ? `${wakeOnLanDefaultName} ${targetArg.normalizedMac}` : targetArg.host || wakeOnLanDefaultName);
  }

  private static entityName(targetArg: IWakeOnLanTarget, platformArg: TWakeOnLanEntityKind): string {
    const name = this.targetName(targetArg);
    return targetArg.entityTypes?.length === 2 ? `${name} ${platformArg === 'button' ? 'Wake' : 'Power'}` : name;
  }

  private static broadcastAddress(targetArg: IWakeOnLanTarget): string {
    return targetArg.broadcastAddress || targetArg.broadcast_address || wakeOnLanDefaultBroadcastAddress;
  }

  private static broadcastPort(targetArg: IWakeOnLanTarget): number {
    return this.portValue(targetArg.broadcastPort ?? targetArg.broadcast_port) ?? wakeOnLanDefaultBroadcastPort;
  }

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

  private static uniqueFeatures(featuresArg: plugins.shxInterfaces.data.IDeviceFeature[]): plugins.shxInterfaces.data.IDeviceFeature[] {
    const seen = new Set<string>();
    return featuresArg.filter((featureArg) => {
      if (seen.has(featureArg.id)) {
        return false;
      }
      seen.add(featureArg.id);
      return true;
    });
  }

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

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

  private static portValue(valueArg: unknown): number | undefined {
    if (typeof valueArg === 'number' && Number.isFinite(valueArg) && valueArg >= 0 && valueArg <= 65535) {
      return Math.round(valueArg);
    }
    if (typeof valueArg === 'string' && valueArg.trim()) {
      const parsed = Number(valueArg);
      return Number.isFinite(parsed) && parsed >= 0 && parsed <= 65535 ? Math.round(parsed) : undefined;
    }
    return undefined;
  }
}
