import {
  SimpleLocalClient,
  SimpleLocalMapper,
  type IIntegrationEntity,
  type IServiceCallRequest,
  type IServiceCallResult,
  type ISimpleLocalEntitySnapshot,
  type ISimpleLocalSnapshot,
} from '../../core/index.js';
import type { IIncomfortConfig } from './incomfort.types.js';
import { incomfortProfile } from './incomfort.types.js';

const incomfortDefaultPort = 80;
const incomfortDefaultTimeoutMs = 20000;
const nullSerialNumber = '000W00000';
const invalidValue = 327.67;
const overrideMinTemp = 5;
const overrideMaxTemp = 30;

const bitmaskFail = 0x01;
const bitmaskPump = 0x02;
const bitmaskTap = 0x04;
const bitmaskBurner = 0x08;

const displayCodes: Record<number, string> = {
  0: 'opentherm',
  15: 'boiler_ext',
  24: 'frost',
  37: 'central_heating_rf',
  51: 'tapwater_int',
  85: 'sensor_test',
  102: 'central_heating',
  103: 'central_heating_wait',
  104: 'central_heating_low',
  106: 'hp_error_recovery',
  126: 'standby',
  127: 'off',
  153: 'postrun_boyler',
  170: 'service',
  204: 'tapwater',
  230: 'starting_ch',
  231: 'postrun_ch',
  240: 'boiler_int',
  255: 'buffer',
};

const faultCodes: Record<number, string> = {
  0: 'sensor_fault_after_self_check_e0',
  1: 'cv_temperature_too_high_e1',
  2: 's1_and_s2_interchanged_e2',
  4: 'no_flame_signal_e4',
  5: 'poor_flame_signal_e5',
  6: 'flame_detection_fault_e6',
  8: 'incorrect_fan_speed_e8',
  10: 'sensor_fault_s1_e10',
  11: 'sensor_fault_s1_e11',
  12: 'sensor_fault_s1_e12',
  13: 'sensor_fault_s1_e13',
  14: 'sensor_fault_s1_e14',
  20: 'sensor_fault_s2_e20',
  21: 'sensor_fault_s2_e21',
  22: 'sensor_fault_s2_e22',
  23: 'sensor_fault_s2_e23',
  24: 'sensor_fault_s2_e24',
  27: 'shortcut_outside_sensor_temperature_e27',
  29: 'gas_valve_relay_faulty_e29',
  30: 'gas_valve_relay_faulty_e30',
};

interface IIncomfortHeaterListResponse {
  heaterlist?: Array<string | null>;
}

interface IIncomfortRoomStatus {
  roomNo: number;
  roomTemp?: number | null;
  setpoint?: number | null;
  override?: number | null;
}

interface IIncomfortHeaterStatus {
  index: number;
  serialNumber: string;
  displayCode?: number;
  displayText?: string;
  faultCode?: number | null;
  failed: boolean;
  burning: boolean;
  pumping: boolean;
  tapping: boolean;
  heaterTemp?: number | null;
  tapTemp?: number | null;
  pressure?: number | null;
  rfMessageRssi?: number;
  rfStatusCounter?: number;
  nodeNumber?: number;
  rooms: IIncomfortRoomStatus[];
  raw: Record<string, unknown>;
}

export class IncomfortClient {
  private readonly fallbackClient: SimpleLocalClient<IIncomfortConfig>;
  private currentSnapshot?: ISimpleLocalSnapshot;

  constructor(configArg: IIncomfortConfig) {
    this.config = configArg;
    this.fallbackClient = new SimpleLocalClient(incomfortProfile, configArg);
  }

  private readonly config: IIncomfortConfig;

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

    if (this.shouldUseFallbackBeforeHttp(forceRefreshArg)) {
      this.currentSnapshot = await this.fallbackClient.getSnapshot(forceRefreshArg);
      return SimpleLocalMapper.clone(this.currentSnapshot);
    }

    if (this.config.host || this.config.url) {
      try {
        this.currentSnapshot = await this.fetchSnapshot();
        return SimpleLocalMapper.clone(this.currentSnapshot);
      } catch (errorArg) {
        this.currentSnapshot = this.offlineSnapshot(this.errorMessage(errorArg));
        return SimpleLocalMapper.clone(this.currentSnapshot);
      }
    }

    this.currentSnapshot = await this.fallbackClient.getSnapshot(forceRefreshArg);
    return SimpleLocalMapper.clone(this.currentSnapshot);
  }

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

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.isSetTemperatureRequest(requestArg) && (this.config.host || this.config.url)) {
      return this.setTemperature(requestArg);
    }
    return this.fallbackClient.execute(requestArg);
  }

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

  private shouldUseFallbackBeforeHttp(forceRefreshArg: boolean): boolean {
    if (!forceRefreshArg && this.config.snapshot) {
      return true;
    }
    return Boolean(this.config.client || this.config.snapshotProvider);
  }

  private async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    const heaterList = await this.requestJson<IIncomfortHeaterListResponse>('heaterlist.json');
    const serials = Array.isArray(heaterList.heaterlist) ? heaterList.heaterlist : [];
    const heaterEntries = serials
      .map((serialNumberArg, indexArg) => ({ serialNumber: typeof serialNumberArg === 'string' ? serialNumberArg : undefined, index: indexArg }))
      .filter((entryArg): entryArg is { serialNumber: string; index: number } => Boolean(entryArg.serialNumber && entryArg.serialNumber !== nullSerialNumber));

    if (!heaterEntries.length) {
      throw new Error('Intergas gateway returned no valid heaters in heaterlist.json.');
    }

    const heaters = await Promise.all(heaterEntries.map(async (entryArg) => this.fetchHeater(entryArg.serialNumber, entryArg.index)));
    const snapshot: ISimpleLocalSnapshot = {
      device: {
        id: this.config.uniqueId || this.config.macAddress || this.config.deviceId || this.hostId(),
        name: this.config.name || incomfortProfile.defaultName,
        manufacturer: 'Intergas',
        model: 'InComfort/Intouch Lan2RF gateway',
        host: this.host(),
        port: this.port(),
        protocol: this.config.useTls ? 'https' : 'http',
        configurationUrl: this.configurationUrl(),
        attributes: {
          heaterCount: heaters.length,
        },
      },
      entities: heaters.flatMap((heaterArg) => this.entitiesForHeater(heaterArg)),
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'http',
      rawData: {
        heaterlist: serials,
        heaters,
      },
      capabilities: {
        localRead: true,
        localControl: true,
        protocol: 'incomfort-http',
      },
    };
    return snapshot;
  }

  private async fetchHeater(serialNumberArg: string, indexArg: number): Promise<IIncomfortHeaterStatus> {
    const raw = await this.requestJson<Record<string, unknown>>(`data.json?heater=${indexArg}`);
    return this.heaterStatus(serialNumberArg, indexArg, raw);
  }

  private heaterStatus(serialNumberArg: string, indexArg: number, rawArg: Record<string, unknown>): IIncomfortHeaterStatus {
    const io = this.numberValue(rawArg.IO) || 0;
    const failed = Boolean(io & bitmaskFail);
    const displayCode = this.numberValue(rawArg.displ_code);
    const rooms: IIncomfortRoomStatus[] = [];

    for (const roomNo of [1, 2]) {
      const roomTemp = this.decodedValue(`room_temp_${roomNo}`, rawArg);
      if (roomTemp !== undefined && roomTemp !== null) {
        rooms.push({
          roomNo,
          roomTemp,
          setpoint: this.decodedValue(`room_temp_set_${roomNo}`, rawArg),
          override: this.decodedValue(`room_set_ovr_${roomNo}`, rawArg),
        });
      }
    }

    return {
      index: indexArg,
      serialNumber: serialNumberArg,
      displayCode,
      displayText: displayCode === undefined ? undefined : failed ? faultCodes[displayCode] || 'unknown' : displayCodes[displayCode] || 'unknown',
      faultCode: failed ? displayCode ?? null : null,
      failed,
      burning: Boolean(io & bitmaskBurner),
      pumping: Boolean(io & bitmaskPump),
      tapping: Boolean(io & bitmaskTap),
      heaterTemp: this.decodedValue('ch_temp', rawArg),
      tapTemp: this.decodedValue('tap_temp', rawArg),
      pressure: this.decodedValue('ch_pressure', rawArg),
      rfMessageRssi: this.numberValue(rawArg.rf_message_rssi),
      rfStatusCounter: this.numberValue(rawArg.rfstatus_cntr),
      nodeNumber: this.numberValue(rawArg.nodenr),
      rooms,
      raw: rawArg,
    };
  }

  private entitiesForHeater(heaterArg: IIncomfortHeaterStatus): ISimpleLocalEntitySnapshot[] {
    const baseAttributes = {
      heaterIndex: heaterArg.index,
      serialNumber: heaterArg.serialNumber,
      displayCode: heaterArg.displayCode,
      displayText: heaterArg.displayText,
      faultCode: heaterArg.faultCode,
      nodeNumber: heaterArg.nodeNumber,
    };
    const entities: ISimpleLocalEntitySnapshot[] = [
      this.entity(`heater_${heaterArg.index}_failed`, 'Fault', 'binary_sensor', heaterArg.failed, { ...baseAttributes, fault_code: heaterArg.faultCode, deviceClass: 'problem' }),
      this.entity(`heater_${heaterArg.index}_is_pumping`, 'Is pumping', 'binary_sensor', heaterArg.pumping, { ...baseAttributes, deviceClass: 'running' }),
      this.entity(`heater_${heaterArg.index}_is_burning`, 'Is burning', 'binary_sensor', heaterArg.burning, { ...baseAttributes, deviceClass: 'running' }),
      this.entity(`heater_${heaterArg.index}_is_tapping`, 'Is tapping', 'binary_sensor', heaterArg.tapping, { ...baseAttributes, deviceClass: 'running' }),
      this.entity(`heater_${heaterArg.index}_cv_pressure`, 'CV pressure', 'sensor', heaterArg.pressure ?? null, { ...baseAttributes, unit: 'bar', deviceClass: 'pressure', stateClass: 'measurement' }),
      this.entity(`heater_${heaterArg.index}_cv_temp`, 'CV temperature', 'sensor', heaterArg.heaterTemp ?? null, { ...baseAttributes, is_pumping: heaterArg.pumping, unit: '°C', deviceClass: 'temperature', stateClass: 'measurement' }),
      this.entity(`heater_${heaterArg.index}_tap_temp`, 'Tap temperature', 'sensor', heaterArg.tapTemp ?? null, { ...baseAttributes, is_tapping: heaterArg.tapping, unit: '°C', deviceClass: 'temperature', stateClass: 'measurement' }),
      this.entity(`heater_${heaterArg.index}_rf_message_rssi`, 'RF message RSSI', 'sensor', heaterArg.rfMessageRssi ?? null, { ...baseAttributes, rfstatus_cntr: heaterArg.rfStatusCounter, stateClass: 'measurement' }),
    ];

    for (const room of heaterArg.rooms) {
      const targetTemperature = room.override ?? room.setpoint ?? null;
      entities.push(this.entity(
        `heater_${heaterArg.index}_room_${room.roomNo}`,
        `Thermostat ${room.roomNo}`,
        'climate',
        targetTemperature,
        {
          ...baseAttributes,
          roomNumber: room.roomNo,
          currentTemperature: room.roomTemp,
          targetTemperature,
          setpoint: room.setpoint,
          override: room.override,
          hvacMode: 'heat',
          hvacAction: heaterArg.burning && heaterArg.pumping ? 'heating' : 'idle',
          minTemp: overrideMinTemp,
          maxTemp: overrideMaxTemp,
          unit: '°C',
        },
        true,
      ));
    }

    return entities;
  }

  private entity(idArg: string, nameArg: string, platformArg: ISimpleLocalEntitySnapshot['platform'], stateArg: unknown, attributesArg: Record<string, unknown>, writableArg = false): ISimpleLocalEntitySnapshot {
    return {
      id: idArg,
      name: nameArg,
      platform: platformArg,
      state: stateArg,
      available: true,
      writable: writableArg,
      unit: typeof attributesArg.unit === 'string' ? attributesArg.unit : undefined,
      deviceClass: typeof attributesArg.deviceClass === 'string' ? attributesArg.deviceClass : undefined,
      stateClass: typeof attributesArg.stateClass === 'string' ? attributesArg.stateClass : undefined,
      attributes: attributesArg,
    };
  }

  private async setTemperature(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    const temperature = this.numberValue(requestArg.data?.temperature);
    if (temperature === undefined || temperature < overrideMinTemp || temperature > overrideMaxTemp) {
      return { success: false, error: `InComfort set_temperature requires data.temperature between ${overrideMinTemp} and ${overrideMaxTemp}.` };
    }

    const target = await this.targetRoom(requestArg);
    if (!target) {
      return { success: false, error: 'InComfort set_temperature requires a climate entity target or exactly one discovered thermostat.' };
    }

    const setpoint = Math.round((temperature - overrideMinTemp) * 10);
    const response = await this.requestJson<Record<string, unknown>>(`data.json?heater=${target.heaterIndex}&thermostat=${target.roomNumber - 1}&setpoint=${setpoint}`);
    this.currentSnapshot = undefined;
    return { success: true, data: { heaterIndex: target.heaterIndex, roomNumber: target.roomNumber, temperature, setpoint, response } };
  }

  private async targetRoom(requestArg: IServiceCallRequest): Promise<{ heaterIndex: number; roomNumber: number } | undefined> {
    const snapshot = await this.getSnapshot();
    const climates = SimpleLocalMapper.toEntities(incomfortProfile, snapshot)
      .filter((entityArg): entityArg is IIntegrationEntity => entityArg.platform === 'climate');
    const targetEntity = requestArg.target.entityId
      ? climates.find((entityArg) => entityArg.id === requestArg.target.entityId)
      : climates.length === 1 ? climates[0] : undefined;

    const attributes = targetEntity?.attributes;
    const heaterIndex = this.numberValue(attributes?.heaterIndex);
    const roomNumber = this.numberValue(attributes?.roomNumber);
    if (heaterIndex === undefined || roomNumber === undefined) {
      return undefined;
    }
    return { heaterIndex, roomNumber };
  }

  private isSetTemperatureRequest(requestArg: IServiceCallRequest): boolean {
    return (requestArg.domain === 'incomfort' || requestArg.domain === 'climate') && requestArg.service === 'set_temperature';
  }

  private async requestJson<TResponse>(pathArg: string): Promise<TResponse> {
    const response = await globalThis.fetch(`${this.baseUrl()}/${pathArg.replace(/^\/+/, '')}`, {
      headers: this.headers(),
      signal: AbortSignal.timeout(this.config.timeoutMs || incomfortDefaultTimeoutMs),
    });
    const text = await response.text();
    if (!response.ok) {
      throw new Error(`InComfort request ${pathArg} failed with HTTP ${response.status}: ${text.slice(0, 160)}`);
    }
    return JSON.parse(text.replace(/\\/g, '0')) as TResponse;
  }

  private baseUrl(): string {
    const url = this.config.url ? new URL(this.config.url) : undefined;
    const protocol = this.config.useTls || url?.protocol === 'https:' ? 'https' : 'http';
    const host = url?.hostname || this.config.host;
    if (!host) {
      throw new Error('InComfort HTTP access requires config.host or config.url.');
    }
    const port = this.port(url);
    const defaultPort = protocol === 'https' ? 443 : incomfortDefaultPort;
    const portPart = port && port !== defaultPort ? `:${port}` : '';
    const rawConfiguredPath = url?.pathname && url.pathname !== '/' ? this.trimSlashes(url.pathname) : this.trimSlashes(stringValue(this.config.path));
    const configuredPath = rawConfiguredPath?.endsWith('.json') ? undefined : rawConfiguredPath;
    const protectedPath = this.config.username ? 'protect' : undefined;
    const basePath = protectedPath && configuredPath !== protectedPath ? this.joinPaths(configuredPath, protectedPath) : configuredPath;
    return `${protocol}://${host}${portPart}${basePath ? `/${basePath}` : ''}`;
  }

  private headers(): Record<string, string> {
    const headers: Record<string, string> = { accept: 'application/json, text/plain;q=0.9, */*;q=0.8' };
    if (this.config.username) {
      headers.authorization = `Basic ${Buffer.from(`${this.config.username}:${this.config.password || ''}`).toString('base64')}`;
    }
    return headers;
  }

  private host(): string | undefined {
    return this.config.host || (this.config.url ? new URL(this.config.url).hostname : undefined);
  }

  private port(urlArg?: URL): number | undefined {
    const url = urlArg || (this.config.url ? new URL(this.config.url) : undefined);
    return this.config.port || (url?.port ? Number(url.port) : undefined) || (this.host() ? incomfortDefaultPort : undefined);
  }

  private configurationUrl(): string | undefined {
    try {
      return this.baseUrl();
    } catch {
      return undefined;
    }
  }

  private hostId(): string | undefined {
    const host = this.host();
    return host ? `${host}:${this.port() || incomfortDefaultPort}` : undefined;
  }

  private decodedValue(keyStubArg: string, dataArg: Record<string, unknown>): number | null | undefined {
    const msb = this.numberValue(dataArg[`${keyStubArg}_msb`]);
    const lsb = this.numberValue(dataArg[`${keyStubArg}_lsb`]);
    if (msb === undefined || lsb === undefined) {
      return undefined;
    }
    const value = (msb * 256 + lsb) / 100;
    return value === invalidValue ? null : value;
  }

  private 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;
  }

  private offlineSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return SimpleLocalMapper.toSnapshot({ profile: incomfortProfile, config: this.config, online: false, source: 'runtime', error: errorArg });
  }

  private trimSlashes(valueArg: string | undefined): string | undefined {
    const trimmed = valueArg?.trim().replace(/^\/+|\/+$/g, '');
    return trimmed || undefined;
  }

  private joinPaths(...partsArg: Array<string | undefined>): string | undefined {
    const cleanParts = partsArg.filter((partArg): partArg is string => Boolean(partArg));
    return cleanParts.length ? cleanParts.join('/') : undefined;
  }

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

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