import { SimpleLocalClient, SimpleLocalMapper, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult, type TSimpleLocalRawData } from '../../core/index.js';
import { NamMapper } from './nam.mapper.js';
import type { INamConfig, INamRawApiData } from './nam.types.js';
import { namDefaultPort, namDefaultTimeoutMs, namProfile } from './nam.types.js';

export class NamClient extends SimpleLocalClient<INamConfig> {
  private cachedSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configArg: INamConfig) {
    super(namProfile, configArg);
  }

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

    if (this.hasEndpoint() && !this.configArg.client && !this.configArg.snapshotProvider && (forceRefreshArg || !this.configArg.snapshot)) {
      try {
        this.cachedSnapshot = NamMapper.toSnapshot({ config: this.configArg, rawData: await this.fetchRawData(), online: true, source: 'http' });
      } catch (errorArg) {
        this.cachedSnapshot = NamMapper.offlineSnapshot(this.configArg, errorMessage(errorArg));
      }
      return SimpleLocalMapper.clone(this.cachedSnapshot);
    }

    const snapshot = await super.getSnapshot(forceRefreshArg);
    this.cachedSnapshot = NamMapper.toSnapshot({ config: this.configArg, rawData: snapshot.rawData as TSimpleLocalRawData | undefined ?? snapshot, online: snapshot.online, source: snapshot.source, error: snapshot.error });
    return SimpleLocalMapper.clone(this.cachedSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.cachedSnapshot = undefined;
    const liveCapable = this.hasEndpoint() || Boolean(this.configArg.client || this.configArg.snapshotProvider);
    const snapshot = await this.getSnapshot(liveCapable);
    const success = snapshot.online && !snapshot.error && (liveCapable || this.hasManualData());
    return { success, error: success ? undefined : snapshot.error || 'Nettigo Air Monitor refresh requires a local HTTP host, injected client, snapshotProvider, snapshot, entities, or rawData.', data: snapshot };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.client?.execute || this.configArg.commandExecutor || !this.hasEndpoint()) {
      return super.execute(requestArg);
    }
    if (requestArg.service !== 'press' && requestArg.service !== 'restart') {
      return { success: false, error: `Unsupported Nettigo Air Monitor service: ${requestArg.domain}.${requestArg.service}` };
    }

    try {
      await this.requestText('/reset', 'POST');
      return { success: true };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

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

  private async fetchRawData(): Promise<INamRawApiData> {
    const [configText, deviceData] = await Promise.all([
      this.requestText('/config', 'GET'),
      this.requestJson<Record<string, unknown>>('/data.json', 'GET'),
    ]);
    const macAddress = extractMacAddress(configText);
    if (!macAddress) {
      throw new Error('Nettigo Air Monitor /config did not contain a MAC address.');
    }
    const sensors = parseSensors(deviceData);
    return {
      macAddress,
      softwareVersion: stringValue(deviceData.software_version),
      uptime: numberValue(deviceData.uptime),
      sensors,
      rawDeviceData: deviceData,
      fetchedAt: new Date().toISOString(),
    };
  }

  private async requestJson<TValue>(pathArg: string, methodArg: string): Promise<TValue> {
    const text = await this.requestText(pathArg, methodArg);
    try {
      return JSON.parse(text) as TValue;
    } catch (errorArg) {
      throw new Error(`Nettigo Air Monitor JSON parse failed for ${pathArg}: ${errorMessage(errorArg)}`);
    }
  }

  private async requestText(pathArg: string, methodArg: string): Promise<string> {
    const response = await this.fetch(pathArg, methodArg);
    const text = await response.text();
    if (response.status === 401) {
      throw new Error('Nettigo Air Monitor local authentication failed.');
    }
    if (!response.ok) {
      throw new Error(`Nettigo Air Monitor request ${pathArg} failed with ${response.status}: ${text.slice(0, 160)}`);
    }
    return text;
  }

  private async fetch(pathArg: string, methodArg: string): Promise<Response> {
    const timeoutMs = this.configArg.timeoutMs || namDefaultTimeoutMs;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      const headers: Record<string, string> = { accept: 'application/json, text/plain;q=0.9, */*;q=0.8' };
      if (this.configArg.username) {
        headers.authorization = `Basic ${Buffer.from(`${this.configArg.username}:${this.configArg.password || ''}`).toString('base64')}`;
      }
      return await globalThis.fetch(this.url(pathArg), { method: methodArg, headers, signal: controller.signal });
    } finally {
      clearTimeout(timer);
    }
  }

  private url(pathArg: string): string {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('Nettigo Air Monitor local HTTP API requires config.host or config.url.');
    }
    const path = pathArg.startsWith('/') ? pathArg : `/${pathArg}`;
    return new URL(path, endpoint.origin).toString();
  }

  private endpoint(): URL | undefined {
    const rawUrl = stringValue(this.configArg.url) || stringValue(this.configArg.host);
    if (!rawUrl) {
      return undefined;
    }
    try {
      const url = new URL(rawUrl.includes('://') ? rawUrl : `http://${rawUrl}`);
      if (!url.port && this.configArg.port && this.configArg.port !== namDefaultPort) {
        url.port = String(this.configArg.port);
      }
      return url;
    } catch {
      return undefined;
    }
  }

  private hasEndpoint(): boolean {
    return Boolean(this.endpoint());
  }

  private hasManualData(): boolean {
    return this.configArg.rawData !== undefined || this.configArg.rawText !== undefined || Boolean(this.configArg.entities?.length) || Boolean(this.configArg.snapshotProvider) || Boolean(this.configArg.snapshot);
  }
}

const renameKeyMap: Array<[string, string]> = [
  ['bmp_pressure', 'bmp180_pressure'],
  ['bmp_temperature', 'bmp180_temperature'],
  ['conc_co2_ppm', 'mhz14a_carbon_dioxide'],
  ['humidity', 'dht22_humidity'],
  ['sds_p1', 'sds011_p1'],
  ['sds_p2', 'sds011_p2'],
  ['temperature', 'dht22_temperature'],
  ['ambient_light', 'bh1750_illuminance'],
];

const ignoredSensorKeys = new Set(['GPS_date', 'GPS_time']);

const parseSensors = (dataArg: Record<string, unknown>): Record<string, number> => {
  const values = Array.isArray(dataArg.sensordatavalues) ? dataArg.sensordatavalues : [];
  const sensors: Record<string, number> = {};
  for (const item of values) {
    if (!item || typeof item !== 'object' || Array.isArray(item)) {
      continue;
    }
    const record = item as Record<string, unknown>;
    const rawKey = stringValue(record.value_type);
    const value = numberValue(record.value);
    if (!rawKey || value === undefined || ignoredSensorKeys.has(rawKey)) {
      continue;
    }
    const key = rawKey.toLowerCase();
    sensors[key] = key.includes('pressure') ? value / 100 : value;
  }

  if (sensors.ambient_light === -1) {
    delete sensors.ambient_light;
  }
  for (const [oldKey, newKey] of renameKeyMap) {
    if (sensors[oldKey] !== undefined) {
      sensors[newKey] = sensors[oldKey];
      delete sensors[oldKey];
    }
  }
  const uptime = numberValue(dataArg.uptime);
  if (uptime !== undefined) {
    sensors.uptime = Math.trunc(uptime);
  }
  delete sensors.gps_lat;
  delete sensors.gps_lon;
  delete sensors.gps_height;
  return sensors;
};

const extractMacAddress = (valueArg: string): string | undefined => {
  const match = valueArg.match(/(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}|\([0-9a-fA-F]{12}\)/);
  if (!match) {
    return undefined;
  }
  const normalized = match[0].replace(/[():]/g, '').toLowerCase();
  return normalized.match(/.{1,2}/g)?.join(':');
};

const stringValue = (valueArg: unknown): string | undefined => {
  return 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()) {
    const parsed = Number(valueArg.trim());
    return Number.isFinite(parsed) ? parsed : undefined;
  }
  return undefined;
};

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