import type {
  IWizClientCommand,
  IWizCommandResult,
  IWizConfig,
  IWizDeviceInfo,
  IWizEvent,
  IWizPilotPatch,
  IWizPilotState,
  IWizSnapshot,
  IWizSnapshotDevice,
  IWizUdpCommand,
  IWizUdpResponse,
} from './wiz.types.js';
import { wizDefaultPort } from './wiz.types.js';
import { WizMapper } from './wiz.mapper.js';

type TWizEventHandler = (eventArg: IWizEvent) => void;

export class WizClient {
  private readonly events: IWizEvent[] = [];
  private readonly eventHandlers = new Set<TWizEventHandler>();

  constructor(private readonly config: IWizConfig) {}

  public async getSnapshot(): Promise<IWizSnapshot> {
    const host = this.host();
    if (!host) {
      return WizMapper.toSnapshot(this.config, false, this.events);
    }

    try {
      const pilot = await this.getPilot();
      const deviceInfo = await this.liveDeviceInfo(pilot).catch(() => this.staticDeviceInfo(pilot));
      const device: IWizSnapshotDevice = {
        host,
        port: this.port(),
        mac: pilot.mac || deviceInfo.mac || this.config.mac,
        name: this.config.name || deviceInfo.name,
        deviceInfo,
        pilot,
        available: true,
      };
      return WizMapper.toSnapshot({ ...this.config, snapshot: undefined, devices: [device], manualEntries: undefined }, true, this.events);
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      this.emit({ type: 'error', data: { message }, timestamp: Date.now() });
      return WizMapper.toSnapshot(this.config, false, this.events);
    }
  }

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

  public async getPilot(): Promise<IWizPilotState> {
    const response = await this.sendUdp<IWizPilotState>({ method: 'getPilot', params: {} });
    const result = this.record(response.result) ? response.result as IWizPilotState : {};
    this.emit({ type: 'pilot', data: result, timestamp: Date.now() });
    return result;
  }

  public async setPilot(payloadArg: IWizPilotPatch): Promise<IWizUdpResponse<Record<string, unknown>>> {
    return this.sendUdp<Record<string, unknown>>({ method: 'setPilot', params: payloadArg as Record<string, unknown> });
  }

  public async getSystemConfig(): Promise<IWizUdpResponse<Record<string, unknown>>> {
    return this.sendUdp<Record<string, unknown>>({ method: 'getSystemConfig', params: {} });
  }

  public async sendCommand(commandArg: IWizClientCommand): Promise<IWizCommandResult> {
    let result: IWizCommandResult;
    if (this.config.commandExecutor) {
      result = this.commandResult(await this.config.commandExecutor(commandArg), commandArg);
    } else if (!this.host()) {
      result = {
        success: false,
        error: this.unsupportedLiveControlMessage(),
        data: { command: commandArg },
      };
    } else {
      try {
        const response = await this.setPilot(commandArg.payload);
        result = { success: true, data: response.result || response };
      } catch (error) {
        result = {
          success: false,
          error: error instanceof Error ? error.message : String(error),
          data: { command: commandArg },
        };
      }
    }

    this.emit({
      type: result.success ? 'command_mapped' : 'command_failed',
      command: commandArg,
      data: result,
      timestamp: Date.now(),
      deviceId: commandArg.deviceId,
      entityId: commandArg.entityId,
    });
    return result;
  }

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

  private async liveDeviceInfo(pilotArg: IWizPilotState): Promise<IWizDeviceInfo> {
    const systemConfig = await this.getSystemConfig();
    const result = this.record(systemConfig.result) ? systemConfig.result : {};
    return this.staticDeviceInfo(pilotArg, result);
  }

  private staticDeviceInfo(pilotArg?: IWizPilotState, systemArg: Record<string, unknown> = {}): IWizDeviceInfo {
    const moduleName = this.stringValue(systemArg.moduleName) || this.config.deviceInfo?.moduleName;
    const model = this.config.deviceInfo?.model || moduleName;
    const isSocket = this.config.deviceInfo?.isSocket ?? this.textContainsSocket(model, moduleName, systemArg.typeId);
    return {
      ...this.config.deviceInfo,
      host: this.host(),
      port: this.port(),
      mac: this.stringValue(systemArg.mac) || pilotArg?.mac || this.config.mac || this.config.deviceInfo?.mac,
      name: this.config.name || this.config.deviceInfo?.name,
      manufacturer: this.config.deviceInfo?.manufacturer || 'WiZ',
      model,
      moduleName,
      fwVersion: this.stringValue(systemArg.fwVersion) || this.config.deviceInfo?.fwVersion,
      typeId: this.stringValue(systemArg.typeId) || this.numberValue(systemArg.typeId) || this.config.deviceInfo?.typeId,
      isSocket,
      features: {
        light: !isSocket,
        switch: isSocket,
        brightness: !isSocket || typeof pilotArg?.dimming === 'number',
        color: ['r', 'g', 'b'].every((keyArg) => typeof pilotArg?.[keyArg] === 'number') || this.textContains(moduleName, 'rgb'),
        colorTemp: typeof pilotArg?.temp === 'number' || this.textContains(moduleName, 'tw'),
        effect: typeof pilotArg?.sceneId === 'number' || typeof pilotArg?.schdPsetId === 'number',
        fan: typeof pilotArg?.fanState === 'number',
        power: typeof pilotArg?.pc === 'number',
        occupancy: pilotArg?.src === 'pir',
        ...this.config.deviceInfo?.features,
      },
    };
  }

  private async sendUdp<TResult>(commandArg: IWizUdpCommand): Promise<IWizUdpResponse<TResult>> {
    const host = this.host();
    if (!host) {
      throw new Error(this.unsupportedLiveControlMessage());
    }
    const port = this.port();
    const timeoutMs = this.timeoutMs();
    const { createSocket } = await import('node:dgram');
    const payload = Buffer.from(JSON.stringify(commandArg));

    return new Promise<IWizUdpResponse<TResult>>((resolve, reject) => {
      const socket = createSocket('udp4');
      const timers: Array<ReturnType<typeof setTimeout>> = [];
      let settled = false;

      const cleanup = () => {
        for (const timer of timers) {
          clearTimeout(timer);
        }
        socket.removeAllListeners();
        try {
          socket.close();
        } catch {
          // The socket may already be closed after an early UDP error.
        }
      };

      const finish = (errorArg: Error | undefined, responseArg?: IWizUdpResponse<TResult>) => {
        if (settled) {
          return;
        }
        settled = true;
        cleanup();
        if (errorArg) {
          reject(errorArg);
        } else {
          resolve(responseArg || {});
        }
      };

      const send = () => {
        if (settled) {
          return;
        }
        socket.send(payload, port, host, (errorArg) => {
          if (errorArg) {
            finish(errorArg);
          }
        });
      };

      socket.on('error', (errorArg) => finish(errorArg));
      socket.on('message', (messageArg) => {
        let response: IWizUdpResponse<TResult>;
        try {
          response = JSON.parse(messageArg.toString('utf8')) as IWizUdpResponse<TResult>;
        } catch (error) {
          finish(error instanceof Error ? error : new Error(String(error)));
          return;
        }
        if (response.method && response.method !== commandArg.method) {
          return;
        }
        if (response.error) {
          finish(new Error(response.error.message || `WiZ UDP error ${response.error.code ?? 'unknown'}`));
          return;
        }
        this.emit({ type: 'udp_response', data: response, timestamp: Date.now() });
        finish(undefined, response);
      });

      send();
      timers.push(setTimeout(send, 750));
      timers.push(setTimeout(send, 2250));
      timers.push(setTimeout(() => finish(new Error(`Timed out waiting for WiZ ${commandArg.method} response from ${host}:${port}.`)), timeoutMs));
    });
  }

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

  private commandResult(resultArg: unknown, commandArg: IWizClientCommand): IWizCommandResult {
    if (this.isCommandResult(resultArg)) {
      return resultArg;
    }
    return { success: true, data: resultArg ?? { command: commandArg } };
  }

  private isCommandResult(valueArg: unknown): valueArg is IWizCommandResult {
    return typeof valueArg === 'object' && valueArg !== null && 'success' in valueArg;
  }

  private host(): string | undefined {
    return this.config.host || this.config.manualEntries?.find((entryArg) => entryArg.host)?.host;
  }

  private port(): number {
    const manualPort = this.config.manualEntries?.find((entryArg) => entryArg.host)?.port;
    return this.config.port || manualPort || wizDefaultPort;
  }

  private timeoutMs(): number {
    return typeof this.config.timeoutMs === 'number' && this.config.timeoutMs > 0 ? this.config.timeoutMs : 5000;
  }

  private unsupportedLiveControlMessage(): string {
    return 'WiZ live UDP control requires a configured host. Snapshot-only WiZ configs are read-only unless commandExecutor is provided.';
  }

  private textContainsSocket(...valuesArg: unknown[]): boolean {
    return valuesArg.some((valueArg) => this.textContains(valueArg, 'socket') || this.textContains(valueArg, 'plug'));
  }

  private textContains(valueArg: unknown, fragmentArg: string): boolean {
    return typeof valueArg === 'string' && valueArg.toLowerCase().includes(fragmentArg);
  }

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

  private numberValue(valueArg: unknown): number | undefined {
    return typeof valueArg === 'number' && Number.isFinite(valueArg) ? valueArg : undefined;
  }

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