import { WakeOnLanMapper } from './wake_on_lan.mapper.js';
import { createWakeOnLanMagicPacket, createWakeOnLanMagicPacketBuffer } from './wake_on_lan.packet.js';
import type { IWakeOnLanCommand, IWakeOnLanCommandResult, IWakeOnLanConfig, IWakeOnLanEvent, IWakeOnLanMagicPacket, IWakeOnLanSnapshot } from './wake_on_lan.types.js';

type TWakeOnLanEventHandler = (eventArg: IWakeOnLanEvent) => void;

export class WakeOnLanClient {
  private snapshot?: IWakeOnLanSnapshot;
  private readonly events: IWakeOnLanEvent[] = [];
  private readonly eventHandlers = new Set<TWakeOnLanEventHandler>();

  constructor(private readonly config: IWakeOnLanConfig) {}

  public async getSnapshot(): Promise<IWakeOnLanSnapshot> {
    if (!this.snapshot) {
      this.snapshot = WakeOnLanMapper.toSnapshot(this.config, undefined, this.events);
    } else {
      this.snapshot.events = [...(this.config.snapshot?.events || []), ...this.events];
    }
    return this.cloneSnapshot(this.snapshot);
  }

  public onEvent(handlerArg: TWakeOnLanEventHandler): () => void {
    this.eventHandlers.add(handlerArg);
    return () => this.eventHandlers.delete(handlerArg);
  }

  public async refresh(): Promise<IWakeOnLanCommandResult> {
    const snapshot = WakeOnLanMapper.toSnapshot(this.config, undefined, this.events);
    if (this.config.pingExecutor) {
      for (const target of snapshot.targets) {
        if (!target.host) {
          continue;
        }
        try {
          target.online = await this.config.pingExecutor(target.host, target);
          target.state = target.online;
          target.available = target.available ?? Boolean(target.normalizedMac);
        } catch (errorArg) {
          target.online = false;
          target.state = false;
          target.available = false;
          target.metadata = { ...target.metadata, lastPingError: errorArg instanceof Error ? errorArg.message : String(errorArg) };
        }
      }
    }
    this.snapshot = snapshot;
    this.emit({ type: 'snapshot_refreshed', data: this.cloneSnapshot(snapshot), timestamp: Date.now() });
    return { success: true, transmitted: false, data: this.cloneSnapshot(snapshot) };
  }

  public async sendCommand(commandArg: IWakeOnLanCommand): Promise<IWakeOnLanCommandResult> {
    this.emit({ type: 'command_mapped', command: commandArg, deviceId: commandArg.deviceId, entityId: commandArg.entityId, targetId: commandArg.targetId, timestamp: Date.now() });

    try {
      const result = commandArg.type === 'switch_turn_off'
        ? await this.executeTurnOff(commandArg)
        : await this.sendMagicPacket(commandArg);
      if (result.success) {
        this.patchSnapshot(commandArg);
      }
      this.emit({ type: result.success ? 'packet_sent' : 'command_failed', command: commandArg, data: result, error: result.error, deviceId: commandArg.deviceId, entityId: commandArg.entityId, targetId: commandArg.targetId, timestamp: Date.now() });
      return result;
    } catch (errorArg) {
      const result: IWakeOnLanCommandResult = { success: false, transmitted: false, error: errorArg instanceof Error ? errorArg.message : String(errorArg), data: { command: commandArg } };
      this.emit({ type: 'command_failed', command: commandArg, data: result, error: result.error, deviceId: commandArg.deviceId, entityId: commandArg.entityId, targetId: commandArg.targetId, timestamp: Date.now() });
      return result;
    }
  }

  public async destroy(): Promise<void> {
    this.eventHandlers.clear();
  }

  public static createMagicPacket(macAddressArg: string, optionsArg: { broadcastAddress?: string; broadcastPort?: number } = {}): IWakeOnLanMagicPacket {
    return createWakeOnLanMagicPacket(macAddressArg, optionsArg);
  }

  private async sendMagicPacket(commandArg: IWakeOnLanCommand): Promise<IWakeOnLanCommandResult> {
    if (!commandArg.normalizedMac || !commandArg.packet || !commandArg.broadcastAddress || commandArg.broadcastPort === undefined) {
      return { success: false, transmitted: false, error: 'Wake on LAN send_magic_packet requires a valid MAC address and broadcast target.', data: { command: commandArg } };
    }

    const executor = this.config.commandExecutor || this.config.transport?.execute.bind(this.config.transport);
    if (executor) {
      return this.commandResult(await executor(commandArg), commandArg, true);
    }

    if (this.config.nativeUdpEnabled === false) {
      return {
        success: false,
        transmitted: false,
        error: 'Wake on LAN UDP send path is unavailable. Provide commandExecutor or transport.execute, or enable native UDP sending.',
        data: { command: commandArg },
      };
    }

    await this.sendNativeUdp(commandArg);
    return { success: true, transmitted: true, packet: commandArg.packet, data: { packet: commandArg.packet, broadcastAddress: commandArg.broadcastAddress, broadcastPort: commandArg.broadcastPort } };
  }

  private async executeTurnOff(commandArg: IWakeOnLanCommand): Promise<IWakeOnLanCommandResult> {
    const executor = this.config.commandExecutor || this.config.transport?.execute.bind(this.config.transport);
    if (!executor) {
      return {
        success: false,
        transmitted: false,
        error: 'Wake on LAN switch turn_off actions require an injected commandExecutor or transport.execute; Home Assistant scripts are not executed by this native port.',
        data: { command: commandArg },
      };
    }
    return this.commandResult(await executor(commandArg), commandArg, false);
  }

  private async sendNativeUdp(commandArg: IWakeOnLanCommand): Promise<void> {
    const packet = createWakeOnLanMagicPacketBuffer(commandArg.normalizedMac as string);
    const { createSocket } = await import('node:dgram');
    const timeoutMs = this.config.udpTimeoutMs ?? 5000;

    await new Promise<void>((resolve, reject) => {
      const socket = createSocket('udp4');
      let settled = false;
      const cleanup = () => {
        clearTimeout(timer);
        socket.removeAllListeners();
        try {
          socket.close();
        } catch {
          // The socket may not be bound if setup failed early.
        }
      };
      const finish = (errorArg?: Error | null) => {
        if (settled) {
          return;
        }
        settled = true;
        cleanup();
        if (errorArg) {
          reject(errorArg);
        } else {
          resolve();
        }
      };
      const timer = setTimeout(() => finish(new Error(`Wake on LAN UDP send to ${commandArg.broadcastAddress}:${commandArg.broadcastPort} timed out.`)), timeoutMs);

      socket.once('error', (errorArg) => finish(errorArg));
      socket.bind(() => {
        try {
          socket.setBroadcast(true);
        } catch (errorArg) {
          finish(errorArg instanceof Error ? errorArg : new Error(String(errorArg)));
          return;
        }
        socket.send(packet, commandArg.broadcastPort as number, commandArg.broadcastAddress as string, (errorArg) => finish(errorArg));
      });
    });
  }

  private commandResult(resultArg: unknown, commandArg: IWakeOnLanCommand, transmittedDefaultArg: boolean): IWakeOnLanCommandResult {
    if (this.isCommandResult(resultArg)) {
      return { transmitted: transmittedDefaultArg, ...resultArg };
    }
    return { success: true, transmitted: transmittedDefaultArg, data: resultArg ?? { command: commandArg } };
  }

  private isCommandResult(valueArg: unknown): valueArg is IWakeOnLanCommandResult {
    return Boolean(valueArg && typeof valueArg === 'object' && 'success' in valueArg);
  }

  private patchSnapshot(commandArg: IWakeOnLanCommand): void {
    const snapshot = this.snapshot;
    if (!snapshot) {
      return;
    }
    const target = snapshot.targets.find((targetArg) => targetArg.id === commandArg.targetId || targetArg.normalizedMac === commandArg.normalizedMac);
    if (!target) {
      return;
    }
    if (commandArg.type === 'switch_turn_off') {
      target.state = false;
      target.online = false;
    } else {
      target.state = true;
      target.online = true;
    }
    target.available = true;
    target.metadata = { ...target.metadata, updatedAt: new Date().toISOString() };
    this.emit({ type: 'state_changed', command: commandArg, deviceId: commandArg.deviceId, entityId: commandArg.entityId, targetId: commandArg.targetId, data: this.cloneSnapshot(snapshot), timestamp: Date.now() });
  }

  private emit(eventArg: IWakeOnLanEvent): void {
    this.events.push(eventArg);
    for (const handler of this.eventHandlers) {
      handler(eventArg);
    }
  }

  private cloneSnapshot(snapshotArg: IWakeOnLanSnapshot): IWakeOnLanSnapshot {
    return JSON.parse(JSON.stringify(snapshotArg)) as IWakeOnLanSnapshot;
  }
}
