import * as plugins from '../../plugins.js';
import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult, type TSimpleLocalRawData } from '../../core/index.js';
import { KwbMapper } from './kwb.mapper.js';
import type { IKwbConfig, IKwbRawData, IKwbSensorSnapshot, TKwbSnapshotSource } from './kwb.types.js';
import { kwbDefaultPort, kwbDefaultTimeoutMs, kwbProfile } from './kwb.types.js';

type TKwbPacketMode = 'sense' | 'control';

interface IKwbPacket {
  mode: TKwbPacketMode;
  version: number;
  data: number[];
}

interface IKwbSensorDefinition {
  id: string;
  packet: TKwbPacketMode;
  index: number;
  name: string;
  kind: IKwbSensorSnapshot['kind'];
}

const senseSensorDefinitions: IKwbSensorDefinition[] = [
  { id: 'raw_sense', packet: 'sense', index: 0, name: 'RAW SENSE', kind: 'raw' },
  { id: 'supply', packet: 'sense', index: 0, name: 'Supply', kind: 'temperature' },
  { id: 'return', packet: 'sense', index: 1, name: 'Return', kind: 'temperature' },
  { id: 'boiler_0', packet: 'sense', index: 2, name: 'Boiler 0', kind: 'temperature' },
  { id: 'furnace', packet: 'sense', index: 3, name: 'Furnace', kind: 'temperature' },
  { id: 'buffer_tank_2', packet: 'sense', index: 4, name: 'Buffer Tank 2', kind: 'temperature' },
  { id: 'buffer_tank_1', packet: 'sense', index: 5, name: 'Buffer Tank 1', kind: 'temperature' },
  { id: 'outside', packet: 'sense', index: 6, name: 'Outside', kind: 'temperature' },
  { id: 'exhaust', packet: 'sense', index: 7, name: 'Exhaust', kind: 'temperature' },
  { id: 'unknown', packet: 'sense', index: 8, name: 'Unknown', kind: 'temperature' },
  { id: 'stoker_channel', packet: 'sense', index: 12, name: 'Stoker Channel', kind: 'temperature' },
];

const controlSensorDefinitions: IKwbSensorDefinition[] = [
  { id: 'raw_ctrl', packet: 'control', index: 0, name: 'RAW CTRL', kind: 'raw' },
  { id: 'return_mixer', packet: 'control', index: 17, name: 'Return Mixer', kind: 'flag' },
  { id: 'unknown_resupply', packet: 'control', index: 25, name: 'Unknown Resupply', kind: 'flag' },
];

export class KwbClient extends SimpleLocalClient<IKwbConfig> {
  private currentKwbSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly kwbConfig: IKwbConfig) {
    super(kwbProfile, kwbConfig);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<ISimpleLocalSnapshot> {
    if (!forceRefreshArg && this.currentKwbSnapshot) {
      return clone(this.currentKwbSnapshot);
    }

    if (!forceRefreshArg && this.kwbConfig.snapshot) {
      this.currentKwbSnapshot = KwbMapper.toSnapshot({ config: this.kwbConfig, source: 'snapshot', online: this.kwbConfig.snapshot.online });
      return clone(this.currentKwbSnapshot);
    }

    if (this.kwbConfig.client) {
      try {
        this.currentKwbSnapshot = await this.snapshotFromInjectedClient();
      } catch (errorArg) {
        this.currentKwbSnapshot = KwbMapper.offlineSnapshot(this.kwbConfig, errorMessage(errorArg));
      }
      return clone(this.currentKwbSnapshot);
    }

    if (this.kwbConfig.type === 'serial') {
      this.currentKwbSnapshot = KwbMapper.offlineSnapshot(this.kwbConfig, 'KWB serial polling requires a serial-port dependency; only TCP and file capture decoding are implemented natively.');
      return clone(this.currentKwbSnapshot);
    }

    if (this.filePath()) {
      try {
        this.currentKwbSnapshot = await this.readFileSnapshot();
      } catch (errorArg) {
        this.currentKwbSnapshot = KwbMapper.offlineSnapshot(this.kwbConfig, errorMessage(errorArg));
      }
      return clone(this.currentKwbSnapshot);
    }

    if (this.kwbConfig.host) {
      try {
        this.currentKwbSnapshot = await this.readTcpSnapshot();
      } catch (errorArg) {
        this.currentKwbSnapshot = KwbMapper.offlineSnapshot(this.kwbConfig, errorMessage(errorArg));
      }
      return clone(this.currentKwbSnapshot);
    }

    if (this.hasManualData()) {
      this.currentKwbSnapshot = KwbMapper.toSnapshot({ config: this.kwbConfig, rawData: this.kwbConfig.rawData ?? this.kwbConfig.rawText, online: this.kwbConfig.online ?? true, source: 'manual' });
      return clone(this.currentKwbSnapshot);
    }

    this.currentKwbSnapshot = KwbMapper.offlineSnapshot(this.kwbConfig, 'KWB Easyfire snapshots require config.host/config.port for TCP, filePath/packetFile, injected client, snapshotProvider, snapshot, entities, or rawData.');
    return clone(this.currentKwbSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.currentKwbSnapshot = undefined;
    const liveCapable = Boolean(this.kwbConfig.host || this.filePath() || this.kwbConfig.client);
    const snapshot = await this.getSnapshot(liveCapable);
    const success = snapshot.online && !snapshot.error && (liveCapable || this.hasManualData() || Boolean(this.kwbConfig.snapshot));
    return { success, error: success ? undefined : snapshot.error || 'KWB Easyfire refresh requires a TCP host, file capture, or injected client.', data: snapshot };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.kwbConfig.client?.execute) {
      return normalizeServiceResult(await this.kwbConfig.client.execute(requestArg));
    }
    if (this.kwbConfig.commandExecutor) {
      return normalizeServiceResult(await this.kwbConfig.commandExecutor.execute(requestArg));
    }
    return { success: false, error: `KWB Easyfire is read-only; service ${requestArg.domain}.${requestArg.service} requires an injected client.execute() or commandExecutor for non-sensor control.` };
  }

  public async destroy(): Promise<void> {
    await this.kwbConfig.client?.destroy?.();
  }

  public static parsePackets(dataArg: Uint8Array, packetLimitArg = 16): IKwbPacket[] {
    return parseKwbPackets(dataArg, packetLimitArg);
  }

  private async snapshotFromInjectedClient(): Promise<ISimpleLocalSnapshot> {
    const client = this.kwbConfig.client;
    if (!client) {
      throw new Error('KWB injected client is not configured.');
    }
    const value = client.getSnapshot ? await client.getSnapshot() : client.getRawData ? await client.getRawData() : client.getState ? await client.getState() : undefined;
    if (value === undefined) {
      throw new Error('KWB injected client must expose getSnapshot(), getRawData(), or getState().');
    }
    if (isSimpleLocalSnapshot(value)) {
      return KwbMapper.toSnapshot({ config: { ...this.kwbConfig, snapshot: value }, source: 'client', online: value.online });
    }
    return KwbMapper.toSnapshot({ config: this.kwbConfig, rawData: value as TSimpleLocalRawData, source: 'client', online: true });
  }

  private async readTcpSnapshot(): Promise<ISimpleLocalSnapshot> {
    const bytes = await this.readTcpBytes();
    return this.snapshotFromBytes(bytes, 'tcp');
  }

  private async readFileSnapshot(): Promise<ISimpleLocalSnapshot> {
    const bytes = await this.readFileBytes();
    return this.snapshotFromBytes(bytes, 'file');
  }

  private snapshotFromBytes(bytesArg: Uint8Array, sourceArg: TKwbSnapshotSource): ISimpleLocalSnapshot {
    const packets = parseKwbPackets(bytesArg, this.kwbConfig.readPacketLimit || 16);
    if (!packets.length) {
      throw new Error('KWB byte stream did not contain a complete Easyfire sense or control packet.');
    }
    const sensors = sensorsFromPackets(packets);
    const rawData: IKwbRawData = {
      sensors,
      packets,
      source: sourceArg,
      fetchedAt: new Date().toISOString(),
    };
    const online = sensors.some((sensorArg) => sensorArg.available);
    return KwbMapper.toSnapshot({ config: this.kwbConfig, rawData, online, source: sourceArg === 'tcp' ? 'tcp' : 'manual' });
  }

  private async readTcpBytes(): Promise<Buffer> {
    const host = this.kwbConfig.host;
    if (!host) {
      throw new Error('KWB TCP polling requires config.host.');
    }
    const port = this.kwbConfig.port || kwbDefaultPort;
    const timeoutMs = this.kwbConfig.timeoutMs || kwbDefaultTimeoutMs;

    return new Promise<Buffer>((resolve, reject) => {
      const chunks: Buffer[] = [];
      let settled = false;
      const socket = plugins.net.createConnection({ host, port });

      const finish = (errorArg?: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        socket.removeAllListeners();
        socket.destroy();
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve(Buffer.concat(chunks));
      };

      socket.setTimeout(timeoutMs, () => chunks.length ? finish() : finish(new Error(`KWB TCP stream ${host}:${port} timed out after ${timeoutMs}ms without data.`)));
      socket.on('data', (chunkArg) => chunks.push(Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg)));
      socket.on('end', () => finish());
      socket.on('close', () => finish());
      socket.on('error', (errorArg) => finish(errorArg));
    });
  }

  private async readFileBytes(): Promise<Buffer> {
    const filePath = this.filePath();
    if (!filePath) {
      throw new Error('KWB file capture decoding requires config.filePath or config.packetFile.');
    }
    const data = await plugins.fs.readFile(filePath);
    const text = data.toString('utf8');
    const decimalLines = text.split(/\r?\n/).map((lineArg) => lineArg.trim()).filter(Boolean);
    if (decimalLines.length && decimalLines.every((lineArg) => /^\d+$/.test(lineArg) && Number(lineArg) >= 0 && Number(lineArg) <= 255)) {
      return Buffer.from(decimalLines.map((lineArg) => Number(lineArg)));
    }
    return data;
  }

  private filePath(): string | undefined {
    return stringValue(this.kwbConfig.filePath) || stringValue(this.kwbConfig.packetFile);
  }

  private hasManualData(): boolean {
    return this.kwbConfig.rawData !== undefined || this.kwbConfig.rawText !== undefined || Boolean(this.kwbConfig.entities?.length) || Boolean(this.kwbConfig.snapshotProvider);
  }
}

const parseKwbPackets = (dataArg: Uint8Array, packetLimitArg: number): IKwbPacket[] => {
  const packets: IKwbPacket[] = [];
  let offset = 0;
  while (offset < dataArg.length && packets.length < packetLimitArg) {
    const parsed = readPacket(dataArg, offset);
    if (!parsed) {
      break;
    }
    packets.push(parsed.packet);
    offset = parsed.offset;
  }
  return packets;
};

const readPacket = (dataArg: Uint8Array, startOffsetArg: number): { packet: IKwbPacket; offset: number } | undefined => {
  let status = 0;
  let mode: TKwbPacketMode = 'sense';
  let length = 0;
  let version = 0;
  let index = 0;
  const packet: number[] = [];

  for (let offset = startOffsetArg; offset < dataArg.length; offset++) {
    const byte = dataArg[offset];
    if (status === 0) {
      status = byte === 2 ? 1 : 0;
    } else if (status === 1) {
      if (byte === 2) {
        mode = 'sense';
        status = 2;
      } else if (byte === 0) {
        status = 0;
      } else {
        mode = 'control';
        status = 10;
      }
    } else if (status === 2) {
      length = byte;
      status = 6;
    } else if (status === 6) {
      version = byte;
      status = 3;
    } else if (status === 3) {
      index = 0;
      status = 8;
    } else if (status === 8) {
      packet.push(byte);
      index++;
      if (index === length) {
        status = 9;
      }
    } else if (status === 9) {
      void byte;
      return { packet: { mode, version, data: packet }, offset: offset + 1 };
    } else if (status === 10) {
      version = byte;
      status = 11;
    } else if (status === 11) {
      void byte;
      index = 0;
      length = 16;
      status = 12;
    } else if (status === 12) {
      packet.push(byte);
      index++;
      if (index === length) {
        status = 19;
      }
    } else if (status === 19) {
      void byte;
      return { packet: { mode, version, data: packet }, offset: offset + 1 };
    } else {
      status = 0;
    }
  }
  return undefined;
};

const sensorsFromPackets = (packetsArg: IKwbPacket[]): IKwbSensorSnapshot[] => {
  const sensors = [...senseSensorDefinitions, ...controlSensorDefinitions].map((definitionArg) => sensorFromDefinition(definitionArg));
  const sensorById = new Map(sensors.map((sensorArg) => [sensorArg.id, sensorArg]));

  for (const packet of packetsArg) {
    if (packet.mode === 'sense') {
      sensorById.get('raw_sense')!.value = packet.data;
      sensorById.get('raw_sense')!.available = true;
      const temperatures = decodeSenseTemperatures(packet.data);
      for (const definition of senseSensorDefinitions.filter((definitionArg) => definitionArg.kind === 'temperature')) {
        const value = temperatures[definition.index];
        if (value !== undefined) {
          const sensor = sensorById.get(definition.id)!;
          sensor.value = value;
          sensor.available = true;
        }
      }
    } else {
      sensorById.get('raw_ctrl')!.value = packet.data;
      sensorById.get('raw_ctrl')!.available = true;
      for (const definition of controlSensorDefinitions.filter((definitionArg) => definitionArg.kind === 'flag')) {
        const sensor = sensorById.get(definition.id)!;
        sensor.value = flagValue(packet.data, definition.index);
        sensor.available = true;
      }
    }
  }

  return sensors;
};

const sensorFromDefinition = (definitionArg: IKwbSensorDefinition): IKwbSensorSnapshot => ({
  id: definitionArg.id,
  name: definitionArg.name,
  packet: definitionArg.packet,
  index: definitionArg.index,
  kind: definitionArg.kind,
  value: null,
  available: false,
  unit: definitionArg.kind === 'temperature' ? '°C' : undefined,
});

const decodeSenseTemperatures = (packetArg: number[]): number[] => {
  const data = unescapeSensePacket(packetArg);
  const offset = 4;
  const dataLength = data.length - offset - 6;
  const temperatures: number[] = [];
  for (let index = 0; index < Math.floor(dataLength / 2); index++) {
    const byteIndex = index * 2 + offset;
    temperatures.push(decodeTemperature(data[byteIndex], data[byteIndex + 1]));
  }
  return temperatures;
};

const unescapeSensePacket = (packetArg: number[]): number[] => {
  const data: number[] = [];
  let last = 0;
  for (let index = 1; index < packetArg.length; index++) {
    const byte = packetArg[index];
    if (!(last === 2 && byte === 0)) {
      data.push(byte);
    }
    last = byte;
  }
  return data;
};

const decodeTemperature = (byte1Arg: number, byte2Arg: number): number => {
  let value = (byte1Arg << 8) + byte2Arg;
  if (value > 32767) {
    value -= 65536;
  }
  return value / 10;
};

const flagValue = (packetArg: number[], bitIndexArg: number): number => {
  const byte = packetArg[Math.floor(bitIndexArg / 8)] || 0;
  return (byte >> (bitIndexArg % 8)) & 1;
};

const isSimpleLocalSnapshot = (valueArg: unknown): valueArg is ISimpleLocalSnapshot => {
  return Boolean(valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'entities' in valueArg);
};

const normalizeServiceResult = (valueArg: unknown): IServiceCallResult => {
  if (valueArg && typeof valueArg === 'object' && 'success' in valueArg && typeof (valueArg as IServiceCallResult).success === 'boolean') {
    return valueArg as IServiceCallResult;
  }
  return { success: true, data: valueArg };
};

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

const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;

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