import * as plugins from '../../plugins.js';
import { SimpleLocalClient, SimpleLocalMapper, type ISimpleLocalEntitySnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { NikoHomeControlMapper } from './niko_home_control.mapper.js';
import type { INikoHomeControlConfig, INikoHomeControlSnapshot } from './niko_home_control.types.js';
import { nikoHomeControlProfile } from './niko_home_control.types.js';

export class NikoHomeControlClient extends SimpleLocalClient<INikoHomeControlConfig> {
  private liveSnapshot?: INikoHomeControlSnapshot;

  constructor(private readonly configArg: INikoHomeControlConfig) {
    super(nikoHomeControlProfile, configArg);
  }

  public override async getSnapshot(forceRefreshArg = false): Promise<INikoHomeControlSnapshot> {
    if (!forceRefreshArg && this.liveSnapshot) {
      return SimpleLocalMapper.clone(this.liveSnapshot);
    }

    if (this.shouldUseNativeTcp(forceRefreshArg)) {
      try {
        this.liveSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.liveSnapshot = NikoHomeControlMapper.toSnapshot({ config: this.configArg, online: false, source: 'runtime', error: errorMessage(errorArg) });
      }
      return SimpleLocalMapper.clone(this.liveSnapshot);
    }

    const snapshot = await super.getSnapshot(forceRefreshArg);
    this.liveSnapshot = NikoHomeControlMapper.toSnapshot({ config: this.configArg, rawData: snapshot.rawData ?? snapshot, online: snapshot.online, source: snapshot.source, error: snapshot.error });
    return SimpleLocalMapper.clone(this.liveSnapshot);
  }

  public override async refresh(): Promise<IServiceCallResult> {
    this.liveSnapshot = undefined;
    const snapshot = await this.getSnapshot(true);
    const success = snapshot.online && !snapshot.error;
    return { success, error: success ? undefined : snapshot.error, data: snapshot };
  }

  public override async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.client?.execute || this.configArg.commandExecutor || !this.endpoint()) {
      return super.execute(requestArg);
    }

    try {
      const command = await this.commandForService(requestArg);
      await this.writeCommand(command);
      this.liveSnapshot = undefined;
      return { success: true, data: command };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  private shouldUseNativeTcp(forceRefreshArg: boolean): boolean {
    if (!this.endpoint() || this.configArg.client || this.configArg.snapshotProvider) {
      return false;
    }
    if (!forceRefreshArg && this.configArg.snapshot) {
      return false;
    }
    return true;
  }

  private async fetchSnapshot(): Promise<INikoHomeControlSnapshot> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('Niko Home Control TCP snapshots require config.host or config.url.');
    }

    const connection = new NikoHomeControlTcpConnection(endpoint.host, endpoint.port, this.configArg.timeoutMs || 5000);
    await connection.connect();
    try {
      const locations = recordsValue(await connection.send({ cmd: 'listlocations' }));
      const thermostats = recordsValue(await connection.send({ cmd: 'listthermostat' }));
      const energy = recordsValue(await connection.send({ cmd: 'listenergy' }));
      const systemInfo = recordValue(await connection.send({ cmd: 'systeminfo' })) || {};
      const actions = recordsValue(await connection.send({ cmd: 'listactions' }));
      return this.snapshotFromData(endpoint, locations, thermostats, energy, systemInfo, actions);
    } finally {
      connection.close();
    }
  }

  private snapshotFromData(endpointArg: INikoHomeControlEndpoint, locationsArg: Record<string, unknown>[], thermostatsArg: Record<string, unknown>[], energyArg: Record<string, unknown>[], systemInfoArg: Record<string, unknown>, actionsArg: Record<string, unknown>[]): INikoHomeControlSnapshot {
    const locationsById = Object.fromEntries(locationsArg.map((locationArg) => [String(locationArg.id), stringValue(locationArg.name)]));
    const serialNumber = stringValue(systemInfoArg.serial) || stringValue(systemInfoArg.serialNumber) || stringValue(systemInfoArg.uuid);
    const name = this.configArg.name || stringValue(systemInfoArg.name) || nikoHomeControlProfile.defaultName || nikoHomeControlProfile.displayName;
    const deviceId = this.configArg.uniqueId || this.configArg.deviceId || serialNumber || `${endpointArg.host}:${endpointArg.port}`;
    const entities: ISimpleLocalEntitySnapshot[] = [];

    for (const action of actionsArg) {
      const actionId = numberValue(action.id ?? action.channel);
      const actionType = numberValue(action.type);
      if (actionId === undefined || actionType === undefined) {
        continue;
      }
      const actionName = stringValue(action.name) || `Niko action ${actionId}`;
      const value = numberValue(action.value1) || 0;
      const location = locationsById[String(action.location)];
      const commonAttributes = cleanAttributes({ actionId, actionType, location, suggestedArea: location });

      if (actionType === 0) {
        entities.push({
          id: `scene_${actionId}`,
          uniqueId: `${nikoHomeControlProfile.domain}_${SimpleLocalMapper.slug(deviceId)}_scene_${actionId}`,
          name: actionName,
          platform: 'button',
          state: 'idle',
          available: true,
          writable: true,
          attributes: commonAttributes,
        });
        continue;
      }

      if (actionType === 1 || actionType === 2) {
        const brightness = actionType === 2 ? Math.round(value * 2.55) : undefined;
        entities.push({
          id: `light_${actionId}`,
          uniqueId: `${nikoHomeControlProfile.domain}_${SimpleLocalMapper.slug(deviceId)}_light_${actionId}`,
          name: actionName,
          platform: 'light',
          state: value > 0,
          available: true,
          writable: true,
          attributes: cleanAttributes({ ...commonAttributes, brightness, isDimmable: actionType === 2, rawValue: value }),
        });
        continue;
      }

      if (actionType === 4 || actionType === 5) {
        entities.push({
          id: `cover_${actionId}`,
          uniqueId: `${nikoHomeControlProfile.domain}_${SimpleLocalMapper.slug(deviceId)}_cover_${actionId}`,
          name: actionName,
          platform: 'cover',
          state: value > 0 ? 'open' : 'closed',
          available: true,
          writable: true,
          attributes: cleanAttributes({ ...commonAttributes, rawValue: value }),
        });
      }
    }

    for (const thermostat of thermostatsArg) {
      const thermostatId = numberValue(thermostat.id ?? thermostat.channel);
      if (thermostatId === undefined) {
        continue;
      }
      const mode = numberValue(thermostat.mode) ?? 3;
      const location = locationsById[String(thermostat.location)];
      entities.push({
        id: `thermostat_${thermostatId}`,
        uniqueId: `${nikoHomeControlProfile.domain}_${SimpleLocalMapper.slug(deviceId)}_thermostat_${thermostatId}`,
        name: stringValue(thermostat.name) || `Niko thermostat ${thermostatId}`,
        platform: 'climate',
        state: hvacMode(mode),
        available: true,
        writable: true,
        unit: 'C',
        attributes: cleanAttributes({
          actionId: thermostatId,
          mode,
          presetMode: presetMode(mode),
          currentTemperature: temperatureValue(thermostat.measured),
          targetTemperature: temperatureValue(thermostat.setpoint),
          overrule: thermostat.overrule,
          overruleTime: thermostat.overruletime,
          ecoSave: thermostat.ecosave,
          location,
          suggestedArea: location,
        }),
      });
    }

    return {
      device: {
        id: deviceId,
        name,
        manufacturer: 'Niko',
        model: stringValue(systemInfoArg.model) || 'Niko Home Control controller',
        serialNumber,
        host: endpointArg.host,
        port: endpointArg.port,
        protocol: 'tcp',
        attributes: cleanAttributes({ systemInfo: systemInfoArg, locations: locationsArg }),
      },
      entities,
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'tcp',
      rawData: {
        locations: locationsArg,
        thermostats: thermostatsArg,
        energy: energyArg,
        systemInfo: systemInfoArg,
        actions: actionsArg,
      },
      capabilities: {
        localRead: true,
        localControl: true,
        localPush: false,
      },
    };
  }

  private async commandForService(requestArg: IServiceCallRequest): Promise<Record<string, unknown>> {
    const data = requestArg.data || {};
    if (recordValue(data.command)) {
      return data.command as Record<string, unknown>;
    }

    if (requestArg.service === 'turn_on' || requestArg.service === 'turn_off' || requestArg.service === 'toggle') {
      const actionId = await this.actionIdForRequest(requestArg);
      const value = requestArg.service === 'turn_off'
        ? 0
        : requestArg.service === 'toggle'
          ? await this.toggleValue(actionId)
          : lightValue(data.brightness ?? data.value ?? data.level);
      return { cmd: 'executeactions', id: actionId, value1: value };
    }

    if (requestArg.service === 'open_cover' || requestArg.service === 'close_cover' || requestArg.service === 'stop_cover') {
      const actionId = await this.actionIdForRequest(requestArg);
      return { cmd: 'executeactions', id: actionId, value1: coverValue(requestArg.service) };
    }

    if (requestArg.service === 'press' || requestArg.service === 'activate_scene') {
      const actionId = await this.actionIdForRequest(requestArg);
      return { cmd: 'executeactions', id: actionId, value1: 255 };
    }

    if (requestArg.service === 'set_hvac_mode') {
      const actionId = await this.actionIdForRequest(requestArg);
      const mode = thermostatModeValue(data.hvac_mode ?? data.hvacMode ?? data.mode);
      return { cmd: 'executethermostat', id: actionId, mode };
    }

    if (requestArg.service === 'set_temperature') {
      const actionId = await this.actionIdForRequest(requestArg);
      const temperature = numberValue(data.temperature ?? data.value ?? data.setpoint);
      if (temperature === undefined) {
        throw new Error('Niko Home Control set_temperature requires data.temperature.');
      }
      return { cmd: 'executethermostat', id: actionId, overrule: Math.round(temperature * 10), overruletime: stringValue(data.overruletime) || stringValue(data.overruleTime) || '23:59' };
    }

    throw new Error(`Unsupported Niko Home Control service: ${requestArg.domain}.${requestArg.service}`);
  }

  private async actionIdForRequest(requestArg: IServiceCallRequest): Promise<number> {
    const data = requestArg.data || {};
    const explicitId = numberValue(data.id ?? data.actionId ?? data.thermostatId);
    if (explicitId !== undefined) {
      return explicitId;
    }

    const entityId = stringValue(requestArg.target.entityId) || stringValue(data.entityId);
    if (entityId) {
      const snapshot = this.liveSnapshot || await this.getSnapshot();
      const entity = snapshot.entities.find((entityArg) => entityArg.id === entityId || entityArg.uniqueId === entityId || entityId.endsWith(`.${SimpleLocalMapper.slug(entityArg.name)}`));
      const actionId = numberValue(entity?.attributes?.actionId);
      if (actionId !== undefined) {
        return actionId;
      }
    }

    throw new Error('Niko Home Control command requires data.id/actionId or a target.entityId from the current snapshot.');
  }

  private async toggleValue(actionIdArg: number): Promise<number> {
    const snapshot = this.liveSnapshot || await this.getSnapshot();
    const entity = snapshot.entities.find((entityArg) => numberValue(entityArg.attributes?.actionId) === actionIdArg);
    return entity?.state === true ? 0 : 100;
  }

  private async writeCommand(commandArg: Record<string, unknown>): Promise<void> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('Niko Home Control TCP commands require config.host or config.url.');
    }
    const connection = new NikoHomeControlTcpConnection(endpoint.host, endpoint.port, this.configArg.timeoutMs || 5000);
    await connection.connect();
    try {
      await connection.write(commandArg);
    } finally {
      connection.close();
    }
  }

  private endpoint(): INikoHomeControlEndpoint | undefined {
    const url = safeUrl(this.configArg.url || (this.configArg.host?.includes('://') ? this.configArg.host : undefined));
    if (url) {
      return { host: url.hostname, port: url.port ? Number(url.port) : nikoHomeControlProfile.defaultPort || 8000 };
    }
    if (!this.configArg.host) {
      return undefined;
    }
    return { host: this.configArg.host, port: this.configArg.port || nikoHomeControlProfile.defaultPort || 8000 };
  }
}

interface INikoHomeControlEndpoint {
  host: string;
  port: number;
}

class NikoHomeControlTcpConnection {
  private socket?: plugins.net.Socket;
  private buffer = '';
  private lines: string[] = [];
  private waiters: Array<{ resolve: (lineArg: string) => void; reject: (errorArg: Error) => void; timer: NodeJS.Timeout }> = [];

  constructor(private readonly host: string, private readonly port: number, private readonly timeoutMs: number) {}

  public async connect(): Promise<void> {
    await new Promise<void>((resolve, reject) => {
      const socket = plugins.net.createConnection({ host: this.host, port: this.port });
      const timer = setTimeout(() => {
        socket.destroy();
        reject(new Error(`Niko Home Control TCP connection to ${this.host}:${this.port} timed out after ${this.timeoutMs}ms.`));
      }, this.timeoutMs);
      const onError = (errorArg: Error) => {
        clearTimeout(timer);
        socket.removeListener('connect', onConnect);
        reject(errorArg);
      };
      const onConnect = () => {
        clearTimeout(timer);
        socket.removeListener('error', onError);
        this.socket = socket;
        socket.on('data', (chunkArg) => this.onData(chunkArg));
        socket.on('error', (errorArg) => this.failWaiters(errorArg));
        socket.on('close', () => this.failWaiters(new Error('Niko Home Control TCP connection closed.')));
        resolve();
      };
      socket.once('error', onError);
      socket.once('connect', onConnect);
    });
  }

  public async send(commandArg: Record<string, unknown>): Promise<unknown> {
    await this.write(commandArg);
    const line = await this.readLine();
    const response = JSON.parse(line) as unknown;
    const responseData = recordValue(response)?.data;
    const error = recordValue(responseData)?.error;
    if (error) {
      throw new Error(`Niko Home Control controller returned error ${String(error)} for ${String(commandArg.cmd)}.`);
    }
    return responseData;
  }

  public async write(commandArg: Record<string, unknown>): Promise<void> {
    const socket = this.socket;
    if (!socket) {
      throw new Error('Niko Home Control TCP socket is not connected.');
    }
    await new Promise<void>((resolve, reject) => {
      socket.write(JSON.stringify(commandArg), (errorArg?: Error | null) => {
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve();
      });
    });
  }

  public close(): void {
    this.failWaiters(new Error('Niko Home Control TCP connection closed.'));
    this.socket?.destroy();
    this.socket = undefined;
  }

  private readLine(): Promise<string> {
    const line = this.lines.shift();
    if (line !== undefined) {
      return Promise.resolve(line);
    }
    return new Promise<string>((resolve, reject) => {
      const timer = setTimeout(() => {
        this.waiters = this.waiters.filter((waiterArg) => waiterArg.resolve !== resolve);
        reject(new Error(`Niko Home Control TCP response timed out after ${this.timeoutMs}ms.`));
      }, this.timeoutMs);
      this.waiters.push({ resolve, reject, timer });
    });
  }

  private onData(chunkArg: Buffer | string): void {
    this.buffer += chunkArg.toString('utf8');
    const parts = this.buffer.split(/\r?\n/);
    this.buffer = parts.pop() || '';
    for (const part of parts) {
      const line = part.trim();
      if (!line) {
        continue;
      }
      const waiter = this.waiters.shift();
      if (waiter) {
        clearTimeout(waiter.timer);
        waiter.resolve(line);
      } else {
        this.lines.push(line);
      }
    }
  }

  private failWaiters(errorArg: Error): void {
    const waiters = this.waiters.splice(0);
    for (const waiter of waiters) {
      clearTimeout(waiter.timer);
      waiter.reject(errorArg);
    }
  }
}

const thermostatModes: Record<number, string> = {
  0: 'day',
  1: 'night',
  2: 'eco',
  3: 'off',
  4: 'cool',
  5: 'prog1',
  6: 'prog2',
  7: 'prog3',
};

const recordValue = (valueArg: unknown): Record<string, unknown> | undefined => {
  return valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Record<string, unknown> : undefined;
};

const recordsValue = (valueArg: unknown): Record<string, unknown>[] => {
  return Array.isArray(valueArg) ? valueArg.filter((itemArg): itemArg is Record<string, unknown> => Boolean(recordValue(itemArg))) : [];
};

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

const numberValue = (valueArg: unknown): number | undefined => {
  if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
    return valueArg;
  }
  if (typeof valueArg === 'string' && valueArg.trim() && Number.isFinite(Number(valueArg))) {
    return Number(valueArg);
  }
  return undefined;
};

const temperatureValue = (valueArg: unknown): number | undefined => {
  const value = numberValue(valueArg);
  return value === undefined ? undefined : value / 10;
};

const hvacMode = (modeArg: number): string => {
  if (modeArg === 3) return 'off';
  if (modeArg === 4) return 'cool';
  return 'auto';
};

const presetMode = (modeArg: number): string | undefined => {
  return modeArg === 3 || modeArg === 4 ? undefined : thermostatModes[modeArg];
};

const lightValue = (valueArg: unknown): number => {
  const value = numberValue(valueArg);
  if (value === undefined) {
    return 100;
  }
  if (value <= 1) {
    return Math.max(0, Math.min(100, Math.round(value * 100)));
  }
  if (value <= 100) {
    return Math.max(0, Math.min(100, Math.round(value)));
  }
  return Math.max(0, Math.min(100, Math.round(value / 2.55)));
};

const coverValue = (serviceArg: string): number => {
  if (serviceArg === 'open_cover') return 255;
  if (serviceArg === 'close_cover') return 254;
  return 253;
};

const thermostatModeValue = (valueArg: unknown): number => {
  const numeric = numberValue(valueArg);
  if (numeric !== undefined) {
    return Math.round(numeric);
  }
  const value = stringValue(valueArg)?.toLowerCase();
  if (value === 'off') return 3;
  if (value === 'cool') return 4;
  if (value === 'auto') return 5;
  for (const [key, preset] of Object.entries(thermostatModes)) {
    if (preset === value) {
      return Number(key);
    }
  }
  throw new Error('Niko Home Control set_hvac_mode requires data.hvac_mode of off, cool, auto, day, night, eco, prog1, prog2, or prog3.');
};

const safeUrl = (valueArg: string | undefined): URL | undefined => {
  if (!valueArg) {
    return undefined;
  }
  try {
    return new URL(valueArg);
  } catch {
    return undefined;
  }
};

const cleanAttributes = (attributesArg: Record<string, unknown | undefined>): Record<string, unknown> | undefined => {
  const clean = Object.fromEntries(Object.entries(attributesArg).filter(([, valueArg]) => valueArg !== undefined));
  return Object.keys(clean).length ? clean : undefined;
};

const errorMessage = (errorArg: unknown): string => {
  return errorArg instanceof Error ? errorArg.message : String(errorArg);
};
