import { SimpleLocalClient } from '../../core/index.js';
import { IskraMapper } from './iskra.mapper.js';
import type { IIskraConfig, IIskraRestDeviceRaw, IIskraRestRawData, IIskraSnapshot } from './iskra.types.js';
import { iskraDefaultHttpPort, iskraDefaultTimeoutMs, iskraProfile } from './iskra.types.js';

interface IIskraEndpoint {
  baseUrl: string;
  host: string;
  port?: number;
  protocol: 'http' | 'https';
}

export class IskraClient extends SimpleLocalClient<IIskraConfig> {
  private cachedSnapshot?: IIskraSnapshot;

  constructor(private readonly configArg: IIskraConfig) {
    super(iskraProfile, configArg);
  }

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

    if (this.shouldFetchRestApi(forceRefreshArg)) {
      try {
        this.cachedSnapshot = await this.fetchRestApiSnapshot();
      } catch (errorArg) {
        this.cachedSnapshot = IskraMapper.toSnapshot({ config: this.configArg, online: false, source: 'runtime', error: errorMessage(errorArg) });
      }
      return clone(this.cachedSnapshot);
    }

    const snapshot = await new SimpleLocalClient<IIskraConfig>(iskraProfile, safeSnapshotConfig(this.configArg)).getSnapshot(forceRefreshArg);
    if (snapshot.rawData === undefined && snapshot.entities.length) {
      this.cachedSnapshot = IskraMapper.toSnapshot({ config: { ...this.configArg, snapshot }, online: snapshot.online, source: snapshot.source, error: snapshot.error });
    } else {
      this.cachedSnapshot = IskraMapper.toSnapshot({ config: this.configArg, rawData: snapshot.rawData, online: snapshot.online, source: snapshot.source, error: snapshot.error });
    }
    return clone(this.cachedSnapshot);
  }

  private async fetchRestApiSnapshot(): Promise<IIskraSnapshot> {
    const rawData = await this.fetchRestApiRawData();
    return IskraMapper.toSnapshot({ config: this.configArg, rawData, online: true, source: 'http' });
  }

  private async fetchRestApiRawData(): Promise<IIskraRestRawData> {
    const gatewayResponse = objectValue(await this.requestJson('api'));
    const gateway = objectValue(gatewayResponse?.device) || gatewayResponse || {};
    const rawData: IIskraRestRawData = {
      gateway: normalizeBasicInfo(gateway),
      devices: [],
      fetchedAt: new Date().toISOString(),
    };

    const devicesResponse = objectValue(await this.requestJson('api/devices'));
    const availableDevices = arrayValue(devicesResponse?.devices)
      .filter((deviceArg) => isEnabledChildDevice(deviceArg))
      .map((deviceArg, indexArg) => ({ index: indexArg, basicInfo: normalizeBasicInfo(deviceArg) }));
    const configuredDeviceIndex = numberValue(this.configArg.deviceIndex) ?? numberValue(this.configArg.device_index);
    const devices = configuredDeviceIndex === undefined ? availableDevices : availableDevices.filter((deviceArg) => deviceArg.index === configuredDeviceIndex);

    for (const device of devices) {
      const entry: IIskraRestDeviceRaw = {
        index: device.index,
        basicInfo: device.basicInfo,
      };
      const errors: Record<string, string> = {};

      try {
        const measurementResponse = objectValue(await this.requestJson(`api/measurement/${device.index}`));
        entry.measurements = objectValue(measurementResponse?.measurements) || measurementResponse;
      } catch (errorArg) {
        errors.measurements = errorMessage(errorArg);
      }

      try {
        const counterResponse = objectValue(await this.requestJson(`api/counter/${device.index}`));
        entry.counters = objectValue(counterResponse?.counters) || counterResponse;
      } catch (errorArg) {
        errors.counters = errorMessage(errorArg);
      }

      if (Object.keys(errors).length) {
        entry.errors = errors;
      }
      rawData.devices.push(entry);
    }

    if (!rawData.devices.length) {
      rawData.errors = { devices: 'No enabled Iskra Smart Gateway child devices were returned by /api/devices.' };
    }

    return rawData;
  }

  private async requestJson(resourceArg: string): Promise<unknown> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('iskra REST API snapshot requires config.host or config.url.');
    }
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), this.timeoutMs());
    const url = new URL(resourceArg.replace(/^\/+/, ''), `${endpoint.baseUrl}/`);
    const headers: Record<string, string> = { accept: 'application/json' };

    if (this.configArg.username && this.configArg.password !== undefined) {
      headers.cookie = `Authorization=Basic ${Buffer.from(`${this.configArg.username}:${this.configArg.password}`).toString('base64')}`;
    }

    try {
      const response = await globalThis.fetch(url, { headers, signal: controller.signal });
      const text = await response.text();
      if (response.status === 403) {
        throw new Error('Not authorised to access the iskra Smart Gateway REST API.');
      }
      if (response.status === 404) {
        throw new Error(`iskra Smart Gateway REST resource not found: /${resourceArg}`);
      }
      if (!response.ok) {
        throw new Error(`iskra Smart Gateway REST request failed with ${response.status}: ${text.slice(0, 160)}`);
      }
      return text ? JSON.parse(text) : {};
    } finally {
      clearTimeout(timer);
    }
  }

  private shouldFetchRestApi(forceRefreshArg: boolean): boolean {
    if (!this.endpoint() || this.configArg.client || this.configArg.snapshotProvider || this.configArg.protocol === 'modbus_tcp') {
      return false;
    }
    if (!forceRefreshArg && (this.configArg.snapshot || this.configArg.rawData !== undefined || this.configArg.rawText !== undefined || this.configArg.entities?.length)) {
      return false;
    }
    const protocol = stringValue(this.configArg.protocol) || stringValue(this.configArg.metadata?.protocol);
    return !protocol || ['rest_api', 'http', 'https'].includes(protocol);
  }

  private endpoint(): IIskraEndpoint | undefined {
    const rawUrl = stringValue(this.configArg.url) || stringValue(this.configArg.baseUrl) || stringValue(this.configArg.metadata?.url);
    if (rawUrl) {
      const parsed = new URL(rawUrl);
      return {
        baseUrl: parsed.origin,
        host: parsed.hostname,
        port: parsed.port ? Number(parsed.port) : undefined,
        protocol: parsed.protocol === 'https:' ? 'https' : 'http',
      };
    }

    const host = stringValue(this.configArg.host);
    if (!host) {
      return undefined;
    }
    const useTls = Boolean(this.configArg.useTls || this.configArg.metadata?.protocol === 'https');
    const port = this.configArg.port || (useTls ? 443 : iskraDefaultHttpPort);
    const portPart = port && !((useTls && port === 443) || (!useTls && port === 80)) ? `:${port}` : '';
    return {
      baseUrl: `${useTls ? 'https' : 'http'}://${host}${portPart}`,
      host,
      port,
      protocol: useTls ? 'https' : 'http',
    };
  }

  private timeoutMs(): number {
    return numberValue(this.configArg.timeoutMs) || numberValue(this.configArg.metadata?.timeoutMs) || iskraDefaultTimeoutMs;
  }
}

const normalizeBasicInfo = (valueArg: Record<string, unknown>): Record<string, unknown> => {
  const model = stringValue(valueArg.model) || stringValue(valueArg.model_type);
  const serial = stringValue(valueArg.serial) || stringValue(valueArg.serial_number);
  return cleanObject({
    ...valueArg,
    model: model?.trim(),
    serial: serial?.trim(),
    serialNumber: serial?.trim(),
    model_type: undefined,
    serial_number: undefined,
  });
};

const isEnabledChildDevice = (valueArg: Record<string, unknown>): boolean => {
  const model = stringValue(valueArg.model);
  const iface = stringValue(valueArg.interface);
  return Boolean(model && model !== 'Disabled' && model !== 'Not Detected' && !iface?.includes('Right IR'));
};

const objectValue = (valueArg: unknown): Record<string, unknown> | undefined => valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Record<string, unknown> : undefined;
const arrayValue = (valueArg: unknown): Array<Record<string, unknown>> => Array.isArray(valueArg) ? valueArg.filter((itemArg): itemArg is Record<string, unknown> => Boolean(objectValue(itemArg))) : [];
const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
const numberValue = (valueArg: unknown): number | undefined => typeof valueArg === 'number' && Number.isFinite(valueArg) ? valueArg : typeof valueArg === 'string' && valueArg.trim() && Number.isFinite(Number(valueArg)) ? Number(valueArg) : undefined;
const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);

const cleanObject = (valueArg: Record<string, unknown>): Record<string, unknown> => Object.fromEntries(Object.entries(valueArg).filter(([, value]) => value !== undefined));
const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;

const safeSnapshotConfig = (configArg: IIskraConfig): IIskraConfig => {
  const { host, port, path, transport, tcpCommand, useTls, url, baseUrl, ...safeConfig } = configArg;
  void host;
  void port;
  void path;
  void transport;
  void tcpCommand;
  void useTls;
  void url;
  void baseUrl;
  return safeConfig;
};
