import * as plugins from '../../plugins.js';
import type {
  IYeelightBulbInfo,
  IYeelightBulbProperties,
  IYeelightCommand,
  IYeelightCommandResponse,
  IYeelightCommandResult,
  IYeelightConfig,
  IYeelightEvent,
  IYeelightLightStatePatch,
  IYeelightManualDeviceConfig,
  IYeelightSnapshot,
  TYeelightEffect,
} from './yeelight.types.js';

type TYeelightEventHandler = (eventArg: IYeelightEvent) => void;

const defaultPort = 55443;
const defaultEffect: TYeelightEffect = 'smooth';
const defaultTransition = 350;
const defaultPropertyRequest = [
  'power',
  'main_power',
  'bright',
  'ct',
  'rgb',
  'hue',
  'sat',
  'color_mode',
  'flowing',
  'delayoff',
  'music_on',
  'name',
  'bg_power',
  'bg_flowing',
  'bg_ct',
  'bg_bright',
  'bg_hue',
  'bg_sat',
  'bg_rgb',
  'bg_lmode',
  'nl_br',
  'active_mode',
];

export class YeelightClient {
  private commandId = 1;
  private cachedSnapshot?: IYeelightSnapshot;
  private readonly events: IYeelightEvent[] = [];
  private readonly eventHandlers = new Set<TYeelightEventHandler>();

  constructor(private readonly config: IYeelightConfig) {}

  public async getSnapshot(): Promise<IYeelightSnapshot> {
    if (this.cachedSnapshot) {
      return this.withEvents(this.cachedSnapshot);
    }
    if (this.config.snapshot) {
      this.cachedSnapshot = this.withEvents({ ...this.config.snapshot, source: this.config.snapshot.source || 'snapshot' });
      return this.cachedSnapshot;
    }
    const manualBulbs = this.manualBulbs();
    if (manualBulbs.length) {
      this.cachedSnapshot = this.withEvents({ connected: false, bulbs: manualBulbs, events: [], source: 'manual', updatedAt: new Date().toISOString() });
      return this.cachedSnapshot;
    }
    if (!this.config.host) {
      throw new Error('Yeelight snapshot requires config.snapshot, config.bulb/config.bulbs/config.devices, or a host for local TCP get_prop.');
    }
    const properties = await this.getProperties(defaultPropertyRequest);
    this.cachedSnapshot = this.withEvents({
      connected: true,
      bulbs: [this.bulbFromConfig(properties)],
      events: [],
      source: 'tcp',
      updatedAt: new Date().toISOString(),
    });
    return this.cachedSnapshot;
  }

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

  public async getProperties(propertiesArg: string[] = defaultPropertyRequest): Promise<IYeelightBulbProperties> {
    const response = await this.sendCommand({ method: 'get_prop', params: propertiesArg });
    const result = response.result || [];
    const properties: IYeelightBulbProperties = {};
    for (const [index, property] of propertiesArg.entries()) {
      const value = result[index];
      properties[property] = value === '' || value === undefined ? null : String(value);
    }
    this.emit({ type: 'properties', host: this.config.host, data: properties, timestamp: Date.now() });
    return properties;
  }

  public async setLightState(patchArg: IYeelightLightStatePatch): Promise<void> {
    const effect = patchArg.effect || this.config.effect || defaultEffect;
    const transition = this.transitionValue(patchArg.transition);
    if (patchArg.on !== undefined) {
      await this.setPower(patchArg.on, effect, transition);
    }
    if (patchArg.colorTempKelvin !== undefined) {
      await this.setColorTemp(patchArg.colorTempKelvin, effect, transition);
    }
    if (patchArg.rgbColor !== undefined) {
      await this.setRgb(patchArg.rgbColor, effect, transition);
    }
    const brightness = patchArg.brightness ?? patchArg.percentage;
    if (brightness !== undefined) {
      await this.setBrightness(brightness, effect, transition);
    }
  }

  public async setPower(onArg: boolean, effectArg = defaultEffect, durationArg = defaultTransition): Promise<void> {
    await this.sendCommand({ method: 'set_power', params: [onArg ? 'on' : 'off', effectArg, durationArg] });
  }

  public async setBrightness(brightnessArg: number, effectArg = defaultEffect, durationArg = defaultTransition): Promise<void> {
    await this.sendCommand({ method: 'set_bright', params: [this.clamp(Math.round(brightnessArg), 1, 100), effectArg, durationArg] });
  }

  public async setColorTemp(kelvinArg: number, effectArg = defaultEffect, durationArg = defaultTransition): Promise<void> {
    await this.sendCommand({ method: 'set_ct_abx', params: [this.clamp(Math.round(kelvinArg), 1700, 6500), effectArg, durationArg] });
  }

  public async setRgb(rgbArg: [number, number, number], effectArg = defaultEffect, durationArg = defaultTransition): Promise<void> {
    const [red, green, blue] = rgbArg.map((valueArg) => this.clamp(Math.round(valueArg), 0, 255));
    await this.sendCommand({ method: 'set_rgb', params: [red * 65536 + green * 256 + blue, effectArg, durationArg] });
  }

  public async sendCommand(commandArg: IYeelightCommand): Promise<IYeelightCommandResponse> {
    const command = {
      ...commandArg,
      id: commandArg.id ?? this.commandId++,
      host: commandArg.host || this.config.host,
      port: commandArg.port || this.config.port || defaultPort,
      params: commandArg.params || [],
    };
    this.emit({ type: 'command', host: command.host, command, timestamp: Date.now() });
    if (this.config.commandExecutor) {
      return this.commandExecutorResponse(await this.config.commandExecutor(command), command);
    }
    if (!command.host) {
      throw new Error('Yeelight live TCP commands require config.host. Snapshot/manual configs are read-only without commandExecutor.');
    }
    return this.sendTcpCommand({ ...command, host: command.host });
  }

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

  private manualBulbs(): IYeelightBulbInfo[] {
    const bulbs: IYeelightBulbInfo[] = [];
    if (this.config.bulb) {
      bulbs.push(this.config.bulb);
    }
    if (this.config.bulbs) {
      bulbs.push(...this.config.bulbs);
    }
    if (this.config.devices) {
      for (const [host, deviceConfig] of Object.entries(this.config.devices)) {
        bulbs.push(this.bulbFromManualDevice(host, deviceConfig));
      }
    }
    if (!bulbs.length && this.config.host && (this.config.name || this.config.model || this.config.id)) {
      bulbs.push(this.bulbFromConfig());
    }
    return bulbs;
  }

  private bulbFromManualDevice(hostArg: string, configArg: IYeelightManualDeviceConfig): IYeelightBulbInfo {
    return {
      id: configArg.id,
      host: hostArg,
      port: configArg.port || defaultPort,
      name: configArg.name,
      model: configArg.model || configArg.detectedModel || configArg.detected_model,
      capabilities: configArg.capabilities,
      properties: configArg.properties,
      support: configArg.capabilities?.support?.split(' ').filter(Boolean),
      available: false,
    };
  }

  private bulbFromConfig(propertiesArg?: IYeelightBulbProperties): IYeelightBulbInfo {
    return {
      id: this.config.id,
      host: this.config.host,
      port: this.config.port || defaultPort,
      name: this.config.name || propertiesArg?.name || undefined,
      model: this.config.model || this.config.detectedModel || this.config.detected_model,
      properties: propertiesArg,
      available: Boolean(propertiesArg),
    };
  }

  private async sendTcpCommand(commandArg: Required<Pick<IYeelightCommand, 'id' | 'method' | 'params' | 'host' | 'port'>>): Promise<IYeelightCommandResponse> {
    return new Promise((resolve, reject) => {
      let settled = false;
      let buffer = '';
      const socket = plugins.net.createConnection({ host: commandArg.host, port: commandArg.port });
      const timeout = setTimeout(() => {
        finish(new Error(`Yeelight TCP command ${commandArg.method} timed out for ${commandArg.host}:${commandArg.port}.`));
      }, 5000);

      const finish = (errorArg?: Error, responseArg?: IYeelightCommandResponse) => {
        if (settled) {
          return;
        }
        settled = true;
        clearTimeout(timeout);
        socket.removeAllListeners();
        socket.destroy();
        if (errorArg) {
          this.emit({ type: 'error', host: commandArg.host, command: commandArg, error: errorArg.message, timestamp: Date.now() });
          reject(errorArg);
        } else {
          resolve(responseArg || { id: commandArg.id, result: [] });
        }
      };

      socket.on('connect', () => {
        socket.write(`${JSON.stringify({ id: commandArg.id, method: commandArg.method, params: commandArg.params })}\r\n`);
      });
      socket.on('data', (chunkArg) => {
        buffer += chunkArg.toString('utf8');
        const lines = buffer.split(/\r?\n/);
        buffer = lines.pop() || '';
        for (const line of lines) {
          if (!line.trim()) {
            continue;
          }
          const response = this.parseResponseLine(line);
          if (!response) {
            continue;
          }
          if (response.method === 'props') {
            this.emit({ type: 'properties', host: commandArg.host, data: response.params, timestamp: Date.now() });
            continue;
          }
          if (response.error) {
            finish(new Error(`Yeelight command ${commandArg.method} failed: ${JSON.stringify(response.error)}`));
            return;
          }
          finish(undefined, response);
          return;
        }
      });
      socket.on('error', (errorArg) => finish(errorArg));
      socket.on('close', () => {
        if (!settled) {
          const response = this.parseResponseLine(buffer);
          if (response && !response.error) {
            finish(undefined, response);
          } else {
            finish(new Error(`Yeelight TCP connection closed before ${commandArg.method} response.`));
          }
        }
      });
    });
  }

  private parseResponseLine(lineArg: string): IYeelightCommandResponse | undefined {
    try {
      return JSON.parse(lineArg) as IYeelightCommandResponse;
    } catch {
      return undefined;
    }
  }

  private commandExecutorResponse(resultArg: unknown, commandArg: IYeelightCommand): IYeelightCommandResponse {
    if (this.isCommandResponse(resultArg)) {
      return resultArg;
    }
    if (this.isCommandResult(resultArg)) {
      if (!resultArg.success) {
        throw new Error(resultArg.error || `Yeelight command ${commandArg.method} failed.`);
      }
      return { id: commandArg.id, result: [resultArg.data ?? 'ok'] };
    }
    return { id: commandArg.id, result: [resultArg ?? 'ok'] };
  }

  private withEvents(snapshotArg: IYeelightSnapshot): IYeelightSnapshot {
    return { ...snapshotArg, events: [...(snapshotArg.events || []), ...this.events] };
  }

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

  private transitionValue(valueArg?: number): number {
    if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
      return Math.round(valueArg >= 0 && valueArg < 100 ? valueArg * 1000 : valueArg);
    }
    if (typeof this.config.transition === 'number' && Number.isFinite(this.config.transition)) {
      return this.config.transition;
    }
    return defaultTransition;
  }

  private clamp(valueArg: number, minArg: number, maxArg: number): number {
    return Math.min(maxArg, Math.max(minArg, valueArg));
  }

  private isCommandResponse(valueArg: unknown): valueArg is IYeelightCommandResponse {
    return this.isRecord(valueArg) && ('result' in valueArg || 'error' in valueArg || 'method' in valueArg);
  }

  private isCommandResult(valueArg: unknown): valueArg is IYeelightCommandResult {
    return this.isRecord(valueArg) && typeof valueArg.success === 'boolean';
  }

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