import { SimpleLocalClient, SimpleLocalMapper, type ISimpleLocalEntitySnapshot, type IServiceCallResult } from '../../core/index.js';
import { LibreHardwareMonitorMapper } from './libre_hardware_monitor.mapper.js';
import type { ILibreHardwareMonitorConfig, ILibreHardwareMonitorSnapshot } from './libre_hardware_monitor.types.js';
import { libreHardwareMonitorProfile } from './libre_hardware_monitor.types.js';

export class LibreHardwareMonitorClient extends SimpleLocalClient<ILibreHardwareMonitorConfig> {
  private liveSnapshot?: ILibreHardwareMonitorSnapshot;

  constructor(private readonly configArg: ILibreHardwareMonitorConfig) {
    super(libreHardwareMonitorProfile, configArg);
  }

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

    if (this.shouldUseNativeHttp(forceRefreshArg)) {
      try {
        this.liveSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.liveSnapshot = LibreHardwareMonitorMapper.toSnapshot({ config: this.configArg, online: false, source: 'runtime', error: errorMessage(errorArg) });
      }
      return SimpleLocalMapper.clone(this.liveSnapshot);
    }

    const snapshot = await super.getSnapshot(forceRefreshArg);
    this.liveSnapshot = LibreHardwareMonitorMapper.toSnapshot({ config: this.configArg, rawData: snapshot.rawData ?? snapshot, online: snapshot.online, source: snapshot.source, error: snapshot.error });
    return SimpleLocalMapper.clone(this.liveSnapshot);
  }

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

  private shouldUseNativeHttp(forceRefreshArg: boolean): boolean {
    if (!this.endpoint() || this.configArg.client || this.configArg.snapshotProvider) {
      return false;
    }
    if (!forceRefreshArg && this.configArg.snapshot) {
      return false;
    }
    return true;
  }

  private async fetchSnapshot(): Promise<ILibreHardwareMonitorSnapshot> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('Libre Hardware Monitor live snapshots require config.host or config.url.');
    }

    const rawJson = await this.requestData(endpoint);
    const parsed = parseLibreHardwareMonitorData(rawJson);
    const entities = parsed.sensors.map((sensorArg): ISimpleLocalEntitySnapshot => {
      const values = finalSensorValues(sensorArg, parsed.isDeprecatedVersion);
      return {
        id: sensorArg.sensorId,
        uniqueId: `${libreHardwareMonitorProfile.domain}_${sensorArg.sensorId}`,
        name: sensorArg.name,
        platform: 'sensor',
        state: values.value,
        available: true,
        writable: false,
        unit: values.unit,
        stateClass: 'measurement',
        attributes: cleanAttributes({
          min_value: values.min,
          max_value: values.max,
          type: sensorArg.type,
          deviceId: sensorArg.deviceId,
          deviceName: sensorArg.deviceName,
          deviceType: sensorArg.deviceType,
        }),
      };
    });

    return {
      device: {
        id: this.configArg.uniqueId || parsed.computerName || `${endpoint.host}:${endpoint.port}`,
        name: this.configArg.name || parsed.computerName || libreHardwareMonitorProfile.defaultName || libreHardwareMonitorProfile.displayName,
        manufacturer: 'Libre Hardware Monitor',
        model: 'Libre Hardware Monitor Web Server',
        host: endpoint.host,
        port: endpoint.port,
        protocol: endpoint.protocol,
        configurationUrl: endpoint.origin,
        attributes: cleanAttributes({
          version: parsed.version,
          isDeprecatedVersion: parsed.isDeprecatedVersion,
          mainDeviceCount: Object.keys(parsed.mainDeviceIdsAndNames).length,
        }),
      },
      entities,
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'http',
      rawData: {
        computerName: parsed.computerName,
        mainDeviceIdsAndNames: parsed.mainDeviceIdsAndNames,
        sensorData: Object.fromEntries(parsed.sensors.map((sensorArg) => [sensorArg.sensorId, sensorArg])),
        version: parsed.version,
        isDeprecatedVersion: parsed.isDeprecatedVersion,
      },
      capabilities: {
        localRead: true,
        localControl: false,
        dataPath: '/data.json',
      },
    };
  }

  private async requestData(endpointArg: ILibreHardwareMonitorEndpoint): Promise<Record<string, unknown>> {
    const headers: Record<string, string> = { accept: 'application/json' };
    if (this.configArg.username !== undefined && this.configArg.password !== undefined) {
      headers.authorization = `Basic ${Buffer.from(`${this.configArg.username}:${this.configArg.password}`).toString('base64')}`;
    }

    const response = await globalThis.fetch(endpointArg.dataUrl, {
      headers,
      signal: AbortSignal.timeout(this.configArg.timeoutMs || 5000),
    });
    const text = await response.text();
    if (response.status === 401) {
      throw new Error('Libre Hardware Monitor authentication failed with HTTP 401.');
    }
    if (!response.ok) {
      throw new Error(`Libre Hardware Monitor /data.json failed with HTTP ${response.status}: ${text.slice(0, 160)}`);
    }
    try {
      const parsed = JSON.parse(text) as unknown;
      if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
        throw new Error('response was not a JSON object');
      }
      return parsed as Record<string, unknown>;
    } catch (errorArg) {
      throw new Error(`Libre Hardware Monitor /data.json returned invalid JSON: ${errorMessage(errorArg)}`);
    }
  }

  private endpoint(): ILibreHardwareMonitorEndpoint | undefined {
    const rawUrl = stringValue(this.configArg.url) || stringValue(this.configArg.metadata?.url);
    if (rawUrl) {
      const parsed = parseUrl(rawUrl);
      if (parsed) {
        const protocol = parsed.protocol === 'https:' ? 'https' : 'http';
        const port = this.configArg.port || numberValue(parsed.port) || (protocol === 'https' ? 443 : libreHardwareMonitorDefaultPort);
        const origin = `${protocol}://${parsed.hostname}${portPart(protocol, port)}`;
        const dataPath = parsed.pathname && parsed.pathname !== '/' ? parsed.pathname : '/data.json';
        return { host: parsed.hostname, port, protocol, origin, dataUrl: `${origin}${dataPath}` };
      }
    }

    const host = stringValue(this.configArg.host);
    if (!host) {
      return undefined;
    }
    const parsedHost = parseUrl(host);
    if (parsedHost) {
      const protocol = parsedHost.protocol === 'https:' ? 'https' : 'http';
      const port = this.configArg.port || numberValue(parsedHost.port) || (protocol === 'https' ? 443 : libreHardwareMonitorDefaultPort);
      const origin = `${protocol}://${parsedHost.hostname}${portPart(protocol, port)}`;
      return { host: parsedHost.hostname, port, protocol, origin, dataUrl: `${origin}/data.json` };
    }

    const protocol = this.configArg.useTls ? 'https' : 'http';
    const port = this.configArg.port || (protocol === 'https' ? 443 : libreHardwareMonitorDefaultPort);
    const origin = `${protocol}://${host}${portPart(protocol, port)}`;
    return { host, port, protocol, origin, dataUrl: `${origin}/data.json` };
  }
}

interface ILibreHardwareMonitorEndpoint {
  host: string;
  port: number;
  protocol: 'http' | 'https';
  origin: string;
  dataUrl: string;
}

interface ILibreHardwareMonitorParsedData {
  computerName: string;
  mainDeviceIdsAndNames: Record<string, string>;
  sensors: ILibreHardwareMonitorSensorData[];
  version: string;
  isDeprecatedVersion: boolean;
}

interface ILibreHardwareMonitorSensorData {
  name: string;
  value: string | null;
  type?: string;
  min: string | null;
  max: string | null;
  unit?: string;
  deviceId: string;
  deviceName: string;
  deviceType: string;
  sensorId: string;
}

interface IDisplayValue {
  value: string | null;
  unit?: string;
}

const libreHardwareMonitorDefaultPort = 8085;

const parseLibreHardwareMonitorData = (dataArg: Record<string, unknown>): ILibreHardwareMonitorParsedData => {
  const rootChildren = arrayValue(dataArg.Children);
  const computer = recordValue(rootChildren[0]);
  if (!computer) {
    throw new Error('Libre Hardware Monitor data.json did not include a computer node.');
  }
  const computerName = stringValue(computer.Text) || 'Computer';
  const version = stringValue(dataArg.Version) || '0.0.0';
  const mainDeviceIdsAndNames: Record<string, string> = {};
  const sensors: ILibreHardwareMonitorSensorData[] = [];
  let isDeprecatedVersion = false;

  for (const mainDeviceValue of arrayValue(computer.Children)) {
    const mainDevice = recordValue(mainDeviceValue);
    if (!mainDevice) {
      continue;
    }
    const deviceName = stringValue(mainDevice.Text) || 'Device';
    const deviceType = parseDeviceType(mainDevice);
    const configuredDeviceId = formatId(stringValue(mainDevice.HardwareId));

    for (const sensor of flattenSensors(mainDevice)) {
      const rawSensorId = stringValue(sensor.SensorId);
      if (!rawSensorId) {
        continue;
      }
      const sensorId = formatSensorId(rawSensorId);
      let deviceId = configuredDeviceId;
      if (!deviceId) {
        deviceId = sensorId.split('-').slice(0, -2).join('-') || sensorId;
        isDeprecatedVersion = true;
      }
      const type = stringValue(sensor.Type);
      const parsedValue = parseSensorValue(sensor, type);
      const name = sensorNameWithTypeSuffix(stringValue(sensor.Text) || sensorId, type);
      sensors.push({
        name,
        value: ensureNumerical(parsedValue.value),
        type,
        min: ensureNumerical(parsedValue.min),
        max: ensureNumerical(parsedValue.max),
        unit: parsedValue.unit,
        deviceId,
        deviceName,
        deviceType,
        sensorId,
      });
      mainDeviceIdsAndNames[deviceId] = deviceName;
    }
  }

  if (!sensors.length) {
    throw new Error('Libre Hardware Monitor reported no sensor data.');
  }

  return { computerName, mainDeviceIdsAndNames, sensors, version, isDeprecatedVersion };
};

const parseSensorValue = (sensorArg: Record<string, unknown>, typeArg?: string): { value: string | null; min: string | null; max: string | null; unit?: string } => {
  const parsed = splitDisplayValue(stringValue(sensorArg.Value));
  let value = parsed.value;
  let min = splitDisplayValue(stringValue(sensorArg.Min)).value;
  let max = splitDisplayValue(stringValue(sensorArg.Max)).value;
  let unit = parsed.unit;

  if (typeArg === 'Throughput' && sensorArg.RawValue !== undefined) {
    unit = 'B/s';
    if (typeof sensorArg.RawValue === 'number') {
      value = sensorArg.RawValue.toFixed(1);
      min = typeof sensorArg.RawMin === 'number' ? sensorArg.RawMin.toFixed(1) : min;
      max = typeof sensorArg.RawMax === 'number' ? sensorArg.RawMax.toFixed(1) : max;
    } else {
      value = splitDisplayValue(stringValue(sensorArg.RawValue)).value;
      min = splitDisplayValue(stringValue(sensorArg.RawMin)).value;
      max = splitDisplayValue(stringValue(sensorArg.RawMax)).value;
    }
  } else if (typeArg === 'TimeSpan') {
    unit = 's';
    if (sensorArg.RawValue !== undefined) {
      value = String(sensorArg.RawValue);
      min = sensorArg.RawMin === undefined ? min : String(sensorArg.RawMin);
      max = sensorArg.RawMax === undefined ? max : String(sensorArg.RawMax);
    } else {
      value = toSeconds(value);
      min = toSeconds(min);
      max = toSeconds(max);
    }
  }

  return { value, min, max, unit };
};

const finalSensorValues = (sensorArg: ILibreHardwareMonitorSensorData, isDeprecatedVersionArg: boolean): { value: string | null; min: string | null; max: string | null; unit?: string } => {
  if (!isDeprecatedVersionArg && sensorArg.type === 'Throughput') {
    return {
      value: sensorArg.value === null ? null : (Number(sensorArg.value) / 1024).toFixed(1),
      min: sensorArg.min === null ? null : (Number(sensorArg.min) / 1024).toFixed(1),
      max: sensorArg.max === null ? null : (Number(sensorArg.max) / 1024).toFixed(1),
      unit: 'KB/s',
    };
  }
  return { value: sensorArg.value, min: sensorArg.min, max: sensorArg.max, unit: sensorArg.unit };
};

const flattenSensors = (deviceArg: Record<string, unknown>): Record<string, unknown>[] => {
  const children = arrayValue(deviceArg.Children);
  if (!children.length) {
    return stringValue(deviceArg.SensorId) ? [deviceArg] : [];
  }
  return children.flatMap((childArg) => {
    const child = recordValue(childArg);
    return child ? flattenSensors(child) : [];
  });
};

const splitDisplayValue = (valueArg: string | undefined): IDisplayValue => {
  if (!valueArg) {
    return { value: null };
  }
  const normalized = valueArg.replace(',', '.');
  const parts = normalized.split(' ');
  return { value: parts[0] || null, unit: parts[1] };
};

const ensureNumerical = (valueArg: string | null): string | null => {
  if (valueArg === null || valueArg === 'NaN') {
    return null;
  }
  return Number.isFinite(Number(valueArg)) ? valueArg : null;
};

const toSeconds = (valueArg: string | null): string | null => {
  if (!valueArg) {
    return null;
  }
  const parts = valueArg.split(':').map((partArg) => Number(partArg));
  if (parts.length !== 3 || parts.some((partArg) => !Number.isFinite(partArg))) {
    return null;
  }
  return String(parts[0] * 3600 + parts[1] * 60 + parts[2]);
};

const sensorNameWithTypeSuffix = (nameArg: string, typeArg?: string): string => {
  const suffix = typeSuffix(typeArg);
  if (!suffix || nameArg.includes(suffix)) {
    return nameArg;
  }
  return `${nameArg} ${suffix}`;
};

const typeSuffix = (typeArg?: string): string | undefined => {
  if (!typeArg) {
    return undefined;
  }
  if (typeArg === 'Fan') {
    return 'Speed';
  }
  if (typeArg === 'SmallData') {
    return 'Data';
  }
  if (typeArg === 'TimeSpan') {
    return 'Time';
  }
  if (typeArg === 'Energy') {
    return 'Capacity';
  }
  return typeArg;
};

const parseDeviceType = (deviceArg: Record<string, unknown>): string => {
  const imageUrl = stringValue(deviceArg.ImageURL) || '';
  const type = imageUrl.includes('/') ? imageUrl.split('/')[1].split('.')[0] : '';
  return type && type !== 'transparent' ? type.toUpperCase() : 'UNKNOWN';
};

const formatSensorId = (valueArg: string): string => valueArg.split('/').slice(1).join('-').replace(/[^a-zA-Z0-9_-]/g, '');
const formatId = (valueArg: string | undefined): string | undefined => valueArg ? formatSensorId(valueArg) || undefined : undefined;
const recordValue = (valueArg: unknown): Record<string, unknown> | undefined => valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Record<string, unknown> : undefined;
const arrayValue = (valueArg: unknown): unknown[] => Array.isArray(valueArg) ? valueArg : [];
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 : stringValue(valueArg) && Number.isFinite(Number(valueArg)) ? Number(valueArg) : undefined;
const parseUrl = (valueArg: string): URL | undefined => {
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg)) {
    return undefined;
  }
  try {
    return new URL(valueArg);
  } catch {
    return undefined;
  }
};
const portPart = (protocolArg: 'http' | 'https', portArg: number): string => (protocolArg === 'http' && portArg === 80) || (protocolArg === 'https' && portArg === 443) ? '' : `:${portArg}`;
const cleanAttributes = (attributesArg: Record<string, unknown>): Record<string, unknown> | undefined => {
  const clean = Object.fromEntries(Object.entries(attributesArg).filter(([, valueArg]) => valueArg !== undefined));
  return Object.keys(clean).length ? clean : undefined;
};
const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
