import type { IEsphomeClientCommand, IEsphomeCommandResult, IEsphomeConfig, IEsphomeEvent, IEsphomeSnapshot } from './esphome.types.js';
import { EsphomeMapper } from './esphome.mapper.js';

type TEsphomeEventHandler = (eventArg: IEsphomeEvent) => void;

export class EsphomeClient {
  private readonly events: IEsphomeEvent[] = [];
  private readonly eventHandlers = new Set<TEsphomeEventHandler>();

  constructor(private readonly config: IEsphomeConfig) {}

  public async getSnapshot(): Promise<IEsphomeSnapshot> {
    return EsphomeMapper.toSnapshot(this.config, undefined, this.events);
  }

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

  public async sendCommand(commandArg: IEsphomeClientCommand): Promise<IEsphomeCommandResult> {
    let result: IEsphomeCommandResult;
    if (this.config.commandExecutor) {
      result = this.commandResult(await this.config.commandExecutor(commandArg), commandArg);
    } else {
      result = {
        success: false,
        error: this.unsupportedLiveControlMessage(),
        data: { command: commandArg },
      };
    }
    this.emit({
      type: result.success ? 'command_mapped' : 'command_failed',
      command: commandArg,
      data: result,
      timestamp: Date.now(),
      deviceId: this.stringValue(commandArg.deviceId),
      entityId: commandArg.entityId,
    });
    return result;
  }

  public async connectLive(): Promise<void> {
    await new EsphomeNativeApiConnection(this.config).connect();
  }

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

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

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

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

  private unsupportedLiveControlMessage(): string {
    if (this.hasEncryptionKey()) {
      return 'ESPHome live native API writes require protobuf framing plus Noise encryption support, which is not implemented. The mapped command was not sent.';
    }
    if (this.hasPassword()) {
      return 'ESPHome live native API writes require protobuf framing plus legacy password login support, which is not implemented. The mapped command was not sent.';
    }
    return 'ESPHome live native API writes require protobuf framing, which is not implemented. The mapped command was not sent.';
  }

  private hasEncryptionKey(): boolean {
    return Boolean(this.config.encryptionKey || this.config.noisePsk || this.config.manualEntries?.some((entryArg) => entryArg.encryptionKey || entryArg.noisePsk));
  }

  private hasPassword(): boolean {
    return Boolean(this.config.password || this.config.manualEntries?.some((entryArg) => entryArg.password));
  }

  private stringValue(valueArg: unknown): string | undefined {
    if (typeof valueArg === 'string') {
      return valueArg;
    }
    if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
      return String(valueArg);
    }
    return undefined;
  }
}

export class EsphomeNativeApiConnection {
  constructor(private readonly config: IEsphomeConfig) {}

  public async connect(): Promise<void> {
    throw new Error(this.unsupportedMessage());
  }

  public async sendCommand(commandArg: IEsphomeClientCommand): Promise<void> {
    void commandArg;
    throw new Error(this.unsupportedMessage());
  }

  private unsupportedMessage(): string {
    if (this.config.encryptionKey || this.config.noisePsk || this.config.manualEntries?.some((entryArg) => entryArg.encryptionKey || entryArg.noisePsk)) {
      return 'Encrypted ESPHome native API uses Noise plus protobuf over TCP; live TCP support is intentionally not implemented in this dependency-free port.';
    }
    return 'ESPHome native API uses protobuf over TCP; live TCP support is intentionally not implemented in this dependency-free port.';
  }
}
