import * as plugins from '../../plugins.js';
import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { MaxcubeMapper } from './maxcube.mapper.js';
import type { IMaxcubeConfig, IMaxcubeDevice, IMaxcubeRawState, IMaxcubeRoom, IMaxcubeSnapshot } from './maxcube.types.js';
import { maxcubeDefaultPort, maxcubeDefaultTimeoutMs, maxcubeProfile } from './maxcube.types.js';

export class MaxcubeApiError extends Error {}
export class MaxcubeConnectionError extends MaxcubeApiError {}
export class MaxcubeInvalidResponseError extends MaxcubeApiError {}
export class MaxcubeUnsupportedCommandError extends MaxcubeApiError {}

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

interface IMaxcubeMessage {
  cmd: string;
  arg: string;
}

const maxDeviceModeAutomatic = 0;
const maxDeviceModeManual = 1;
const maxDeviceModeVacation = 2;
const maxDeviceModeBoost = 3;
const offTemperature = 4.5;
const onTemperature = 30.5;
const minTemperature = 5.0;
const modeNames: Record<number, string> = {
  [maxDeviceModeAutomatic]: 'auto',
  [maxDeviceModeManual]: 'manual',
  [maxDeviceModeVacation]: 'away',
  [maxDeviceModeBoost]: 'boost',
};

export class MaxcubeClient extends SimpleLocalClient<IMaxcubeConfig> {
  private cachedSnapshot?: IMaxcubeSnapshot;

  constructor(private readonly maxcubeConfig: IMaxcubeConfig) {
    super(maxcubeProfile, maxcubeConfig);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<IMaxcubeSnapshot> {
    if (!forceRefreshArg && this.cachedSnapshot) {
      return MaxcubeMapper.clone(this.cachedSnapshot) as IMaxcubeSnapshot;
    }

    if (this.endpoint() && this.maxcubeConfig.transport !== 'snapshot') {
      try {
        this.cachedSnapshot = await this.fetchSnapshot();
        return MaxcubeMapper.clone(this.cachedSnapshot) as IMaxcubeSnapshot;
      } catch (errorArg) {
        this.cachedSnapshot = this.maxcubeOfflineSnapshot(this.errorMessage(errorArg));
        return MaxcubeMapper.clone(this.cachedSnapshot) as IMaxcubeSnapshot;
      }
    }

    return await super.getSnapshot(forceRefreshArg) as IMaxcubeSnapshot;
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.cachedSnapshot = undefined;
    const snapshot = await this.getSnapshot(true);
    const success = Boolean(this.endpoint()) && snapshot.online && !snapshot.error && snapshot.source === 'tcp';
    return { success, error: success ? undefined : snapshot.error || 'MAX! Cube refresh requires a reachable TCP gateway endpoint.', data: snapshot };
  }

  public async fetchSnapshot(): Promise<IMaxcubeSnapshot> {
    const endpoint = this.requireEndpoint();
    const messages = await this.collectMessages(endpoint);
    const rawData = parseMaxcubeMessages(messages, endpoint);
    if (!rawData.cube.serial && !rawData.devices.length) {
      throw new MaxcubeInvalidResponseError('MAX! Cube did not return H/M/C/L state messages.');
    }
    return MaxcubeMapper.toSnapshot({ config: this.maxcubeConfig, rawData, online: true, source: 'tcp' }) as IMaxcubeSnapshot;
  }

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

    if (!['set_temperature', 'set_hvac_mode', 'set_preset_mode'].includes(requestArg.service)) {
      return super.execute(requestArg);
    }

    try {
      const endpoint = this.requireEndpoint();
      const snapshot = await this.getSnapshot();
      const rawData = maxcubeRawState(snapshot.rawData);
      const device = this.resolveTargetDevice(rawData, requestArg);
      const command = this.commandForService(device, requestArg);
      const acknowledged = await this.sendRadioMessage(endpoint, command.hex);
      this.cachedSnapshot = undefined;
      return {
        success: acknowledged,
        error: acknowledged ? undefined : 'MAX! Cube did not acknowledge the thermostat radio command.',
        data: { command, rfAddress: device.rfAddress, serial: device.serial },
      };
    } catch (errorArg) {
      return { success: false, error: this.errorMessage(errorArg) };
    }
  }

  public async destroy(): Promise<void> {
    await super.destroy();
  }

  private async collectMessages(endpointArg: IMaxcubeEndpoint): Promise<IMaxcubeMessage[]> {
    return this.withSocket(endpointArg, undefined, 'L');
  }

  private async sendRadioMessage(endpointArg: IMaxcubeEndpoint, hexRadioMessageArg: string): Promise<boolean> {
    const payload = Buffer.from(hexRadioMessageArg, 'hex').toString('base64');
    const messages = await this.withSocket(endpointArg, `s:${payload}\r\n`, 'S');
    const response = messages.find((messageArg) => messageArg.cmd.toUpperCase() === 'S');
    if (!response) {
      throw new MaxcubeInvalidResponseError('MAX! Cube did not return an S: command response.');
    }
    const [, statusCode] = response.arg.split(',', 3);
    return statusCode === '0';
  }

  private async withSocket(endpointArg: IMaxcubeEndpoint, commandAfterStateArg: string | undefined, expectedReplyArg: string): Promise<IMaxcubeMessage[]> {
    const timeoutMs = this.maxcubeConfig.timeoutMs || maxcubeDefaultTimeoutMs;
    return await new Promise<IMaxcubeMessage[]>((resolve, reject) => {
      const messages: IMaxcubeMessage[] = [];
      let buffer = '';
      let settled = false;
      let commandSent = false;
      const socket = plugins.net.createConnection({ host: endpointArg.host, port: endpointArg.port });

      const finish = (errorArg?: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        clearTimeout(timer);
        socket.removeAllListeners();
        if (!socket.destroyed) {
          if (!errorArg) {
            socket.write('q:\r\n');
          }
          socket.destroy();
        }
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve(messages);
      };

      const timer = setTimeout(() => finish(new MaxcubeConnectionError(`MAX! Cube TCP request to ${endpointArg.host}:${endpointArg.port} timed out after ${timeoutMs}ms.`)), timeoutMs);

      socket.on('data', (chunkArg) => {
        buffer += Buffer.isBuffer(chunkArg) ? chunkArg.toString('utf8') : String(chunkArg);
        let delimiter = buffer.indexOf('\r\n');
        while (delimiter >= 0) {
          const line = buffer.slice(0, delimiter);
          buffer = buffer.slice(delimiter + 2);
          const message = decodeMessage(line);
          if (message) {
            messages.push(message);
            const cmd = message.cmd.toUpperCase();
            if (commandAfterStateArg && !commandSent && cmd === 'L') {
              socket.write(commandAfterStateArg);
              commandSent = true;
            } else if (cmd === expectedReplyArg && (!commandAfterStateArg || commandSent)) {
              finish();
              return;
            }
          }
          delimiter = buffer.indexOf('\r\n');
        }
      });
      socket.on('connect', () => undefined);
      socket.on('error', (errorArg) => finish(new MaxcubeConnectionError(this.errorMessage(errorArg))));
      socket.on('close', () => {
        if (!settled) {
          finish(new MaxcubeConnectionError(`MAX! Cube TCP connection to ${endpointArg.host}:${endpointArg.port} closed before ${expectedReplyArg}: response.`));
        }
      });
    });
  }

  private commandForService(deviceArg: IMaxcubeDevice, requestArg: IServiceCallRequest): { hex: string; mode: number; temperature: number; service: string } {
    if (deviceArg.kind !== 'thermostat' && deviceArg.kind !== 'wall_thermostat') {
      throw new MaxcubeUnsupportedCommandError(`MAX! Cube device ${deviceArg.rfAddress} is not a thermostat.`);
    }

    let mode = deviceArg.mode ?? maxDeviceModeManual;
    let temperature = deviceArg.targetTemperature ?? minTemperature;

    if (requestArg.service === 'set_temperature') {
      temperature = requiredNumber(requestArg.data?.temperature, 'temperature');
    } else if (requestArg.service === 'set_hvac_mode') {
      const hvacMode = stringValue(requestArg.data?.hvac_mode ?? requestArg.data?.hvacMode);
      if (hvacMode === 'off') {
        mode = maxDeviceModeManual;
        temperature = offTemperature;
      } else if (hvacMode === 'auto') {
        mode = maxDeviceModeAutomatic;
        temperature = 0;
      } else if (hvacMode === 'heat') {
        mode = maxDeviceModeManual;
        temperature = Math.max(deviceArg.targetTemperature ?? deviceArg.minTemperature ?? minTemperature, deviceArg.minTemperature ?? minTemperature);
      } else {
        throw new MaxcubeUnsupportedCommandError(`Unsupported MAX! Cube hvac mode: ${hvacMode || 'missing'}.`);
      }
    } else if (requestArg.service === 'set_preset_mode') {
      const presetMode = stringValue(requestArg.data?.preset_mode ?? requestArg.data?.presetMode);
      if (presetMode === 'comfort') {
        mode = maxDeviceModeManual;
        temperature = deviceArg.comfortTemperature ?? temperature;
      } else if (presetMode === 'eco') {
        mode = maxDeviceModeManual;
        temperature = deviceArg.ecoTemperature ?? temperature;
      } else if (presetMode === 'on') {
        mode = maxDeviceModeManual;
        temperature = onTemperature;
      } else if (presetMode === 'away') {
        mode = maxDeviceModeVacation;
        temperature = 0;
      } else if (presetMode === 'boost') {
        mode = maxDeviceModeBoost;
        temperature = 0;
      } else if (presetMode === 'none') {
        mode = maxDeviceModeAutomatic;
        temperature = 0;
      } else {
        throw new MaxcubeUnsupportedCommandError(`Unsupported MAX! Cube preset mode: ${presetMode || 'missing'}.`);
      }
    }

    const roomId = toHex(deviceArg.roomId ?? 0);
    const targetTemperature = toHex(Math.round(temperature * 2) + (mode << 6));
    return { hex: `000440000000${deviceArg.rfAddress}${roomId}${targetTemperature}`, mode, temperature, service: requestArg.service };
  }

  private resolveTargetDevice(rawDataArg: IMaxcubeRawState | undefined, requestArg: IServiceCallRequest): IMaxcubeDevice {
    if (!rawDataArg) {
      throw new MaxcubeInvalidResponseError('MAX! Cube snapshot does not contain parsed device data.');
    }
    const data = requestArg.data || {};
    const target = requestArg.target || {};
    const wanted = [data.rfAddress, data.rf_address, data.serial, data.deviceSerial, target.entityId, target.deviceId]
      .filter((valueArg): valueArg is string => typeof valueArg === 'string' && valueArg.trim().length > 0)
      .map((valueArg) => valueArg.toLowerCase());
    const device = rawDataArg.devices.find((deviceArg) => wanted.some((wantedArg) => [deviceArg.rfAddress, deviceArg.serial, deviceArg.name].filter(Boolean).some((valueArg) => String(valueArg).toLowerCase() === wantedArg || wantedArg.includes(String(valueArg).toLowerCase()))));
    if (device) {
      return device;
    }
    const firstThermostat = rawDataArg.devices.find((deviceArg) => deviceArg.kind === 'thermostat' || deviceArg.kind === 'wall_thermostat');
    if (firstThermostat && !wanted.length) {
      return firstThermostat;
    }
    throw new MaxcubeUnsupportedCommandError(`No MAX! Cube thermostat matched target ${wanted.join(', ') || '<empty>'}.`);
  }

  private endpoint(): IMaxcubeEndpoint | undefined {
    const value = this.maxcubeConfig.url || this.maxcubeConfig.host;
    if (!value) {
      return undefined;
    }
    try {
      const url = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `tcp://${value}`);
      return { host: url.hostname, port: this.maxcubeConfig.port || (url.port ? Number(url.port) : maxcubeDefaultPort) };
    } catch {
      const [host, port] = value.split(':', 2);
      return { host, port: this.maxcubeConfig.port || (port ? Number(port) : maxcubeDefaultPort) };
    }
  }

  private requireEndpoint(): IMaxcubeEndpoint {
    const endpoint = this.endpoint();
    if (!endpoint?.host || !endpoint.port) {
      throw new MaxcubeConnectionError('MAX! Cube TCP access requires config.host/config.url and port 62910.');
    }
    return endpoint;
  }

  private maxcubeOfflineSnapshot(errorArg: string): IMaxcubeSnapshot {
    return MaxcubeMapper.toSnapshot({ config: this.maxcubeConfig, rawData: this.maxcubeConfig.rawData, online: false, source: 'runtime', error: errorArg }) as IMaxcubeSnapshot;
  }

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

const parseMaxcubeMessages = (messagesArg: IMaxcubeMessage[], endpointArg: IMaxcubeEndpoint): IMaxcubeRawState => {
  const rawData: IMaxcubeRawState = {
    cube: { host: endpointArg.host, port: endpointArg.port },
    rooms: [],
    devices: [],
    messages: messagesArg.map((messageArg) => `${messageArg.cmd}:${messageArg.arg}`),
    fetchedAt: new Date().toISOString(),
  };

  for (const message of messagesArg) {
    if (message.cmd.toUpperCase() === 'H') {
      parseHMessage(rawData, message.arg);
    }
    if (message.cmd.toUpperCase() === 'M') {
      parseMMessage(rawData, message.arg);
    }
  }
  for (const message of messagesArg) {
    if (message.cmd.toUpperCase() === 'C') {
      parseCMessage(rawData, message.arg);
    }
    if (message.cmd.toUpperCase() === 'L') {
      parseLMessage(rawData, message.arg);
    }
  }

  for (const device of rawData.devices) {
    device.roomName = rawData.rooms.find((roomArg) => roomArg.id === device.roomId)?.name;
  }
  return rawData;
};

const parseHMessage = (rawDataArg: IMaxcubeRawState, messageArg: string): void => {
  const [serial, rfAddress, firmware] = messageArg.split(',');
  rawDataArg.cube.serial = serial;
  rawDataArg.cube.rfAddress = rfAddress;
  rawDataArg.cube.firmwareVersion = firmware && firmware.length >= 4 ? `${firmware.slice(0, 2)}.${firmware.slice(2, 4)}` : firmware;
};

const parseMMessage = (rawDataArg: IMaxcubeRawState, messageArg: string): void => {
  const data = decodeBase64Payload(messageArg);
  if (data.length < 3) {
    return;
  }
  const numRooms = data[2] || 0;
  let pos = 3;
  for (let index = 0; index < numRooms && pos < data.length; index++) {
    const roomId = data[pos];
    const nameLength = data[pos + 1] || 0;
    pos += 2;
    const name = data.slice(pos, pos + nameLength).toString('utf8');
    pos += nameLength;
    const rfAddress = rfAddressFromBuffer(data.slice(pos, pos + 3));
    pos += 3;
    if (roomId !== undefined) {
      upsertRoom(rawDataArg.rooms, { id: roomId, name, rfAddress });
    }
  }

  const numDevices = data[pos] || 0;
  pos += 1;
  for (let index = 0; index < numDevices && pos < data.length; index++) {
    const type = data[pos] || 0;
    const rfAddress = rfAddressFromBuffer(data.slice(pos + 1, pos + 4));
    const serial = data.slice(pos + 4, pos + 14).toString('utf8').replace(/\0+$/g, '');
    const nameLength = data[pos + 14] || 0;
    const name = data.slice(pos + 15, pos + 15 + nameLength).toString('utf8');
    const roomId = data[pos + 15 + nameLength];
    upsertDevice(rawDataArg.devices, {
      type,
      kind: deviceKind(type),
      rfAddress,
      roomId,
      name,
      serial,
    });
    pos += 1 + 3 + 10 + nameLength + 2;
  }
};

const parseCMessage = (rawDataArg: IMaxcubeRawState, messageArg: string): void => {
  const [rfAddressRaw] = messageArg.split(',', 1);
  const data = decodeBase64Payload(messageArg);
  const device = rawDataArg.devices.find((deviceArg) => deviceArg.rfAddress === rfAddressRaw?.toUpperCase());
  if (!device || data.length < 22) {
    return;
  }
  if (device.kind === 'thermostat' || device.kind === 'wall_thermostat') {
    device.comfortTemperature = data[18] / 2.0;
    device.ecoTemperature = data[19] / 2.0;
    device.maxTemperature = data[20] / 2.0;
    device.minTemperature = data[21] / 2.0;
    if (device.kind === 'thermostat' && data.length > 29) {
      device.programme = parseProgramme(data.slice(29));
    }
  }
  if (device.kind === 'window_shutter') {
    device.initialized = data[5] === 1;
  }
};

const parseLMessage = (rawDataArg: IMaxcubeRawState, messageArg: string): void => {
  const data = decodeBase64Payload(messageArg);
  let pos = 0;
  while (pos < data.length) {
    const length = data[pos];
    if (!length || pos + length >= data.length + 1) {
      break;
    }
    const rfAddress = rfAddressFromBuffer(data.slice(pos + 1, pos + 4));
    const device = rawDataArg.devices.find((deviceArg) => deviceArg.rfAddress === rfAddress);
    if (device) {
      const bits2 = data[pos + 6] || 0;
      device.batteryLow = (bits2 >> 7) === 1;
      if (device.kind === 'thermostat' || device.kind === 'wall_thermostat') {
        device.targetTemperature = ((data[pos + 8] || 0) & 0x7f) / 2.0;
        device.mode = bits2 & 3;
        device.modeName = modeNames[device.mode] || String(device.mode);
      }
      if (device.kind === 'thermostat') {
        device.valvePosition = data[pos + 7];
        if (device.mode === maxDeviceModeManual || device.mode === maxDeviceModeAutomatic) {
          const actual = (((data[pos + 9] || 0) & 0xff) * 256 + ((data[pos + 10] || 0) & 0xff)) / 10.0;
          device.actualTemperature = actual || undefined;
        }
      }
      if (device.kind === 'wall_thermostat') {
        device.actualTemperature = ((((data[pos + 8] || 0) & 0x80) << 1) + (data[pos + 12] || 0)) / 10.0;
      }
      if (device.kind === 'window_shutter') {
        device.isOpen = ((data[pos + 6] || 0) & 0x03) > 0;
      }
    }
    pos += length + 1;
  }
};

const decodeMessage = (lineArg: string): IMaxcubeMessage | undefined => {
  if (!lineArg.trim()) {
    return undefined;
  }
  const [cmd, arg = ''] = lineArg.split(':', 2);
  return { cmd, arg };
};

const decodeBase64Payload = (messageArg: string): Buffer => {
  const token = messageArg.split(',').pop() || '';
  try {
    return Buffer.from(token, 'base64');
  } catch {
    return Buffer.alloc(0);
  }
};

const upsertRoom = (roomsArg: IMaxcubeRoom[], roomArg: IMaxcubeRoom): void => {
  const existing = roomsArg.find((candidateArg) => candidateArg.id === roomArg.id);
  if (existing) {
    Object.assign(existing, roomArg);
    return;
  }
  roomsArg.push(roomArg);
};

const upsertDevice = (devicesArg: IMaxcubeDevice[], deviceArg: IMaxcubeDevice): void => {
  const existing = devicesArg.find((candidateArg) => candidateArg.rfAddress === deviceArg.rfAddress);
  if (existing) {
    Object.assign(existing, deviceArg);
    return;
  }
  devicesArg.push(deviceArg);
};

const deviceKind = (typeArg: number): IMaxcubeDevice['kind'] => {
  if (typeArg === 1 || typeArg === 2) {
    return 'thermostat';
  }
  if (typeArg === 3) {
    return 'wall_thermostat';
  }
  if (typeArg === 4) {
    return 'window_shutter';
  }
  return 'unknown';
};

const parseProgramme = (bitsArg: Buffer): IMaxcubeDevice['programme'] => {
  const days = ['saturday', 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
  const programme: IMaxcubeDevice['programme'] = {};
  for (let dayIndex = 0; dayIndex < Math.floor(bitsArg.length / 26); dayIndex++) {
    const settings = bitsArg.slice(dayIndex * 26, dayIndex * 26 + 26);
    const dayProgramme: Array<{ temp: number; until: string }> = [];
    for (let pos = 0; pos + 1 < settings.length; pos += 2) {
      const word = `${settings[pos].toString(2).padStart(8, '0')}${settings[pos + 1].toString(2).padStart(8, '0')}`;
      const temp = Math.floor(parseInt(word.slice(0, 7), 2) / 2);
      const timeMins = parseInt(word.slice(7), 2) * 5;
      const mins = timeMins % 60;
      const hours = Math.floor((timeMins - mins) / 60);
      const until = `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
      dayProgramme.push({ temp, until });
      if (until === '24:00') {
        break;
      }
    }
    programme[days[dayIndex]] = dayProgramme;
  }
  return programme;
};

const rfAddressFromBuffer = (bufferArg: Buffer): string => [...bufferArg].map((valueArg) => valueArg.toString(16).padStart(2, '0').toUpperCase()).join('');

const maxcubeRawState = (valueArg: unknown): IMaxcubeRawState | undefined => {
  if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
    return undefined;
  }
  const value = valueArg as Partial<IMaxcubeRawState>;
  if (value.cube && Array.isArray(value.rooms) && Array.isArray(value.devices)) {
    return value as IMaxcubeRawState;
  }
  return undefined;
};

const requiredNumber = (valueArg: unknown, nameArg: string): number => {
  const value = typeof valueArg === 'number' ? valueArg : typeof valueArg === 'string' ? Number(valueArg) : Number.NaN;
  if (!Number.isFinite(value)) {
    throw new MaxcubeUnsupportedCommandError(`MAX! Cube ${nameArg} must be a number.`);
  }
  return value;
};

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

const toHex = (valueArg: number): string => Math.max(0, Math.round(valueArg)).toString(16).padStart(2, '0').toUpperCase();
