import * as plugins from '../../plugins.js';
import { SimpleLocalClient, SimpleLocalMapper, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import type { IIzoneConfig, TIzoneRawData } from './izone.types.js';
import { izoneProfile } from './izone.types.js';

export class IzoneClient extends SimpleLocalClient<IIzoneConfig> {
  private snapshotCache?: ISimpleLocalSnapshot;

  constructor(configArg: IIzoneConfig) {
    super(izoneProfile, configArg);
    this.localConfig = configArg;
  }

  private readonly localConfig: IIzoneConfig;

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

    try {
      this.snapshotCache = await this.fetchSnapshot();
    } catch (errorArg) {
      this.snapshotCache = this.localOfflineSnapshot(errorMessage(errorArg));
    }
    return SimpleLocalMapper.clone(this.snapshotCache);
  }

  public async refresh(): Promise<IServiceCallResult> {
    if (!this.canUseIzoneHttp()) {
      return super.refresh();
    }
    this.snapshotCache = 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.canUseIzoneHttp()) {
      return super.execute(requestArg);
    }

    try {
      const data = requestArg.data || {};
      const response = await this.executeIzoneCommand(requestArg.service, data, requestArg.target?.deviceId || requestArg.target?.entityId);
      this.snapshotCache = undefined;
      return { success: true, data: response };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  public async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    const systemSettings = await this.requestJson<Record<string, unknown>>('/SystemSettings');
    const zoneCount = numberValue(systemSettings.NoOfZones) ?? 0;
    const zones: Array<Record<string, unknown>> = [];
    for (let groupStart = 1; groupStart <= Math.min(zoneCount || 0, 12); groupStart += 4) {
      const zoneGroup = await this.requestJson<unknown>(`/Zones${groupStart}_${groupStart + 3}`);
      if (Array.isArray(zoneGroup)) {
        zones.push(...zoneGroup.filter((zoneArg): zoneArg is Record<string, unknown> => Boolean(zoneArg && typeof zoneArg === 'object' && !Array.isArray(zoneArg))));
      }
    }

    const rawData: TIzoneRawData = this.toRawData(systemSettings, zones);
    return SimpleLocalMapper.toSnapshot({ profile: izoneProfile, config: this.localConfig, rawData, online: true, source: 'http' });
  }

  private async executeIzoneCommand(serviceArg: string, dataArg: Record<string, unknown>, targetArg?: string): Promise<unknown> {
    const service = serviceArg.replace(/^izone\./, '');
    if (service === 'snapshot' || service === 'status') {
      return this.getSnapshot(true);
    }
    if (service === 'refresh' || service === 'reload') {
      return this.refresh();
    }
    if (service === 'turn_on') {
      const zoneNo = this.zoneNo(dataArg, targetArg);
      if (zoneNo !== undefined) {
        return this.postJson('/ZoneCommand', { ZoneCommand: { ZoneNo: String(zoneNo), Command: 'open' } });
      }
      return this.postJson('/SystemON', { SystemON: 'on' });
    }
    if (service === 'turn_off') {
      const zoneNo = this.zoneNo(dataArg, targetArg);
      if (zoneNo !== undefined) {
        return this.postJson('/ZoneCommand', { ZoneCommand: { ZoneNo: String(zoneNo), Command: 'close' } });
      }
      return this.postJson('/SystemON', { SystemON: 'off' });
    }
    if (service === 'set_hvac_mode') {
      const mode = stringValue(dataArg.hvacMode) || stringValue(dataArg.hvac_mode) || stringValue(dataArg.mode);
      if (!mode) {
        throw new Error('iZone set_hvac_mode requires data.hvacMode.');
      }
      const zoneNo = this.zoneNo(dataArg, targetArg);
      if (zoneNo !== undefined) {
        return this.postJson('/ZoneCommand', { ZoneCommand: { ZoneNo: String(zoneNo), Command: izoneZoneMode(mode) } });
      }
      if (mode === 'off') {
        return this.postJson('/SystemON', { SystemON: 'off' });
      }
      await this.postJson('/SystemON', { SystemON: 'on' });
      return this.postJson('/SystemMODE', { SystemMODE: izoneSystemMode(mode) });
    }
    if (service === 'set_temperature') {
      const temperature = numberValue(dataArg.temperature) ?? numberValue(dataArg.targetTemperature) ?? numberValue(dataArg.target_temperature);
      if (temperature === undefined) {
        throw new Error('iZone set_temperature requires data.temperature.');
      }
      const zoneNo = this.zoneNo(dataArg, targetArg);
      if (zoneNo !== undefined) {
        return this.postJson('/ZoneCommand', { ZoneCommand: { ZoneNo: String(zoneNo), Command: String(temperature) } });
      }
      return this.postJson('/UnitSetpoint', { UnitSetpoint: String(temperature) });
    }
    if (service === 'set_fan_mode') {
      const fanMode = stringValue(dataArg.fanMode) || stringValue(dataArg.fan_mode) || stringValue(dataArg.mode);
      if (!fanMode) {
        throw new Error('iZone set_fan_mode requires data.fanMode.');
      }
      return this.postJson('/SystemFAN', { SystemFAN: fanMode === 'medium' ? 'medium' : fanMode });
    }
    if (service === 'set_preset_mode') {
      const presetMode = stringValue(dataArg.presetMode) || stringValue(dataArg.preset_mode) || stringValue(dataArg.mode);
      return this.postJson('/FreeAir', { FreeAir: presetMode === 'eco' ? 'on' : 'off' });
    }
    if (service === 'airflow_min' || service === 'airflow_max') {
      const airflow = numberValue(dataArg.airflow);
      const zoneNo = this.zoneNo(dataArg, targetArg);
      if (airflow === undefined || zoneNo === undefined) {
        throw new Error(`iZone ${service} requires data.airflow and data.zoneNo.`);
      }
      const command = service === 'airflow_min' ? 'AirMinCommand' : 'AirMaxCommand';
      return this.postJson(`/${command}`, { [command]: { ZoneNo: String(zoneNo), Command: String(airflow) } });
    }
    throw new Error(`iZone service ${serviceArg} is not implemented by the local HTTP client.`);
  }

  private zoneNo(dataArg: Record<string, unknown>, targetArg?: string): number | undefined {
    const explicit = numberValue(dataArg.zoneNo) ?? numberValue(dataArg.zone_no) ?? numberValue(dataArg.zone) ?? numberValue(dataArg.zoneIndex);
    if (explicit !== undefined) {
      return explicit;
    }
    const targetMatch = targetArg?.match(/(?:zone|_z)(\d+)/i);
    return targetMatch ? Number(targetMatch[1]) : undefined;
  }

  private toRawData(systemSettingsArg: Record<string, unknown>, zonesArg: Array<Record<string, unknown>>): TIzoneRawData {
    const deviceUid = stringValue(systemSettingsArg.AirStreamDeviceUId) || this.localConfig.uniqueId || endpointId(this.localConfig);
    const controllerName = this.localConfig.name || `iZone Controller ${deviceUid}`;
    const controllerEntity: ISimpleLocalEntitySnapshot = {
      id: 'controller',
      name: controllerName,
      platform: 'climate',
      state: this.controllerState(systemSettingsArg),
      available: true,
      writable: true,
      unit: '°C',
      attributes: cleanAttributes({
        currentTemperature: numberValue(systemSettingsArg.Temp),
        supplyTemperature: numberValue(systemSettingsArg.Supply),
        targetTemperature: numberValue(systemSettingsArg.Setpoint),
        fanMode: systemSettingsArg.SysFan,
        fanModes: fanModes(systemSettingsArg.FanAuto),
        controlZone: numberValue(systemSettingsArg.CtrlZone),
        rasMode: systemSettingsArg.RAS,
        freeAir: systemSettingsArg.FreeAir,
        minTemp: numberValue(systemSettingsArg.EcoMin) ?? 15,
        maxTemp: numberValue(systemSettingsArg.EcoMax) ?? 30,
        systemType: systemSettingsArg.SysType,
      }),
    };

    const zoneEntities = zonesArg.map((zoneArg) => ({
      id: `zone_${numberValue(zoneArg.Index) !== undefined ? (numberValue(zoneArg.Index) as number) + 1 : SimpleLocalMapper.slug(zoneArg.Name)}`,
      name: stringValue(zoneArg.Name) || `Zone ${(numberValue(zoneArg.Index) ?? 0) + 1}`,
      platform: 'climate' as const,
      state: zoneState(zoneArg),
      available: true,
      writable: true,
      unit: '°C',
      attributes: cleanAttributes({
        zoneNo: (numberValue(zoneArg.Index) ?? 0) + 1,
        zoneIndex: numberValue(zoneArg.Index),
        type: zoneArg.Type,
        currentTemperature: numberValue(zoneArg.Temp),
        targetTemperature: numberValue(zoneArg.SetPoint),
        airflowMin: numberValue(zoneArg.MinAir),
        airflowMax: numberValue(zoneArg.MaxAir),
      }),
    }));

    return {
      device: {
        id: deviceUid,
        name: controllerName,
        manufacturer: 'iZone',
        model: stringValue(systemSettingsArg.SysType) || izoneProfile.model,
        host: endpointHost(this.localConfig),
        port: endpointPort(this.localConfig),
        protocol: 'http',
        configurationUrl: this.baseUrl(),
        attributes: cleanAttributes({ deviceUid, isV2: this.localConfig.isV2, isIPower: this.localConfig.isIPower }),
      },
      entities: [controllerEntity, ...zoneEntities],
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'http',
      rawData: { systemSettings: systemSettingsArg, zones: zonesArg },
    };
  }

  private controllerState(systemSettingsArg: Record<string, unknown>): string {
    if (systemSettingsArg.SysOn === 'off') {
      return 'off';
    }
    if (systemSettingsArg.FreeAir === 'on') {
      return 'eco';
    }
    return stringValue(systemSettingsArg.SysMode) || 'unknown';
  }

  private async requestJson<TValue = unknown>(pathArg: string): Promise<TValue> {
    const url = `${this.baseUrl()}${pathArg.startsWith('/') ? pathArg : `/${pathArg}`}`;
    const timeoutMs = this.localConfig.timeoutMs || 3000;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      const response = await globalThis.fetch(url, { headers: { accept: 'application/json' }, signal: controller.signal });
      const text = await response.text();
      if (!response.ok) {
        throw new Error(`iZone HTTP request ${pathArg} failed with ${response.status}: ${text.slice(0, 160)}`);
      }
      return parseIzoneJson<TValue>(text, pathArg);
    } finally {
      clearTimeout(timer);
    }
  }

  private async postJson(pathArg: string, bodyArg: Record<string, unknown>): Promise<unknown> {
    const endpoint = endpointUrl(this.localConfig);
    const path = pathArg.startsWith('/') ? pathArg : `/${pathArg}`;
    const body = JSON.stringify(bodyArg);
    const timeoutMs = this.localConfig.timeoutMs || 3000;

    return new Promise<unknown>((resolve, reject) => {
      const socket = plugins.net.createConnection({ host: endpoint.host, port: endpoint.port || 80 });
      const chunks: Buffer[] = [];
      let settled = false;
      const finish = (errorArg?: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        socket.removeAllListeners();
        socket.destroy();
        if (errorArg) {
          reject(errorArg);
          return;
        }
        try {
          resolve(parseRawHttpResponse(Buffer.concat(chunks).toString('latin1'), path));
        } catch (parseErrorArg) {
          reject(parseErrorArg);
        }
      };

      socket.setTimeout(timeoutMs, () => finish(new Error(`iZone POST ${path} timed out after ${timeoutMs}ms.`)));
      socket.on('connect', () => {
        socket.write([
          `POST ${path} HTTP/1.1`,
          `Host: ${endpoint.host}`,
          'Content-Type: application/json',
          `Content-Length: ${Buffer.byteLength(body)}`,
          'Connection: close',
          '',
          body,
        ].join('\r\n'));
      });
      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 baseUrl(): string {
    const endpoint = endpointUrl(this.localConfig);
    const portPart = endpoint.port && endpoint.port !== 80 ? `:${endpoint.port}` : '';
    return `http://${endpoint.host}${portPart}`;
  }

  private canUseIzoneHttp(): boolean {
    return Boolean(endpointHost(this.localConfig));
  }

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

const endpointUrl = (configArg: IIzoneConfig): { host: string; port?: number } => {
  const rawUrl = stringValue(configArg.url) || (stringValue(configArg.host)?.includes('://') ? stringValue(configArg.host) : undefined);
  if (rawUrl) {
    const url = new URL(rawUrl);
    return { host: url.hostname, port: url.port ? Number(url.port) : undefined };
  }
  return { host: stringValue(configArg.host) || '', port: configArg.port || izoneProfile.defaultPort };
};
const endpointHost = (configArg: IIzoneConfig): string | undefined => endpointUrl(configArg).host || undefined;
const endpointPort = (configArg: IIzoneConfig): number | undefined => endpointUrl(configArg).port;
const endpointId = (configArg: IIzoneConfig): string => `${endpointHost(configArg) || 'izone'}:${endpointPort(configArg) || izoneProfile.defaultPort || 80}`;

const parseIzoneJson = <TValue>(textArg: string, pathArg: string): TValue => {
  const trimmed = textArg.trim().endsWith('{OK}') ? textArg.trim().slice(0, -4) : textArg.trim();
  try {
    return JSON.parse(trimmed || '{}') as TValue;
  } catch (errorArg) {
    throw new Error(`iZone HTTP request ${pathArg} returned invalid JSON: ${errorMessage(errorArg)}`);
  }
};

const parseRawHttpResponse = (responseArg: string, pathArg: string): unknown => {
  const [head, ...bodyParts] = responseArg.split('\r\n\r\n');
  const status = Number(head?.split(' ')[1]);
  const body = bodyParts.join('\r\n\r\n');
  if (!status || status < 200 || status >= 300) {
    throw new Error(`iZone POST ${pathArg} failed with HTTP ${status || 'unknown'}: ${body.slice(0, 160)}`);
  }
  const trimmed = body.trim().endsWith('{OK}') ? body.trim().slice(0, -4) : body.trim();
  if (!trimmed) {
    return { ok: true };
  }
  try {
    return JSON.parse(trimmed) as unknown;
  } catch {
    return trimmed;
  }
};

const fanModes = (fanAutoArg: unknown): string[] => {
  const fanAuto = stringValue(fanAutoArg) || 'unknown';
  if (fanAuto === '2-speed') {
    return ['low', 'high', 'auto'];
  }
  if (fanAuto === '3-speed') {
    return ['low', 'medium', 'high', 'auto'];
  }
  return ['low', 'medium', 'high', 'top', 'auto'];
};

const zoneState = (zoneArg: Record<string, unknown>): string => {
  const mode = stringValue(zoneArg.Mode) || 'unknown';
  if (mode === 'close') {
    return 'off';
  }
  if (mode === 'open') {
    return 'fan_only';
  }
  return 'heat_cool';
};

const izoneSystemMode = (modeArg: string): string => {
  const normalized = modeArg.toLowerCase();
  if (normalized === 'heat_cool' || normalized === 'auto') {
    return 'auto';
  }
  if (normalized === 'fan_only' || normalized === 'vent') {
    return 'vent';
  }
  if (normalized === 'dry') {
    return 'dry';
  }
  if (normalized === 'heat' || normalized === 'cool') {
    return normalized;
  }
  throw new Error(`Unsupported iZone HVAC mode: ${modeArg}`);
};

const izoneZoneMode = (modeArg: string): string => {
  const normalized = modeArg.toLowerCase();
  if (normalized === 'off') {
    return 'close';
  }
  if (normalized === 'fan_only' || normalized === 'on') {
    return 'open';
  }
  if (normalized === 'heat_cool' || normalized === 'auto') {
    return 'auto';
  }
  throw new Error(`Unsupported iZone zone HVAC mode: ${modeArg}`);
};

const stringValue = (valueArg: unknown): string | undefined => 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 cleanAttributes = (valueArg: Record<string, unknown>): Record<string, unknown> => Object.fromEntries(Object.entries(valueArg).filter(([, entryValueArg]) => entryValueArg !== undefined));
const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
