import { SimpleLocalClient, SimpleLocalMapper, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot } from '../../core/index.js';
import type { INetdataConfig, INetdataResourceConfig } from './netdata.types.js';
import { netdataProfile } from './netdata.types.js';

const netdataAllMetricsEndpoint = 'allmetrics?format=json&help=no&types=no&timestamps=yes&names=yes&data=average';
const netdataAlarmsEndpoint = 'alarms?all&format=json';

export class NetdataClient extends SimpleLocalClient<INetdataConfig> {
  private liveSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configArg: INetdataConfig) {
    super(netdataProfile, configArg);
  }

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

    try {
      const [metrics, alarms] = await Promise.all([
        this.requestJson(netdataAllMetricsEndpoint),
        this.requestJson(netdataAlarmsEndpoint),
      ]);
      this.liveSnapshot = this.snapshotFromApiData(metrics, alarms, true);
      return SimpleLocalMapper.clone(this.liveSnapshot);
    } catch (errorArg) {
      this.liveSnapshot = this.buildOfflineSnapshot(errorMessage(errorArg));
      return SimpleLocalMapper.clone(this.liveSnapshot);
    }
  }

  private async requestJson(endpointArg: string): Promise<Record<string, unknown>> {
    const baseUrl = this.apiBaseUrl();
    if (!baseUrl) {
      throw new Error('Netdata HTTP polling requires config.host or config.url.');
    }
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), this.configArg.timeoutMs || 5000);
    try {
      const headers: Record<string, string> = { accept: 'application/json' };
      if (this.configArg.token) {
        headers.authorization = `Bearer ${this.configArg.token}`;
      }
      const response = await globalThis.fetch(`${baseUrl}/${endpointArg}`, { headers, signal: controller.signal });
      const text = await response.text();
      if (!response.ok) {
        throw new Error(`Netdata request ${endpointArg} failed with HTTP ${response.status}: ${text.slice(0, 160)}`);
      }
      const parsed = JSON.parse(text) as unknown;
      if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
        return parsed as Record<string, unknown>;
      }
      throw new Error(`Netdata request ${endpointArg} did not return a JSON object.`);
    } finally {
      clearTimeout(timeout);
    }
  }

  private snapshotFromApiData(metricsArg: Record<string, unknown>, alarmsArg: Record<string, unknown>, onlineArg: boolean, errorArg?: string): ISimpleLocalSnapshot {
    const host = this.configArg.host || hostFromUrl(this.configArg.url);
    const port = this.configArg.port || portFromUrl(this.configArg.url) || netdataProfile.defaultPort;
    const entities: ISimpleLocalEntitySnapshot[] = [];

    for (const resource of this.resourceEntries()) {
      const metric = record(metricsArg[resource.dataGroup]);
      const dimensions = record(metric.dimensions);
      const dimension = record(dimensions[resource.element]);
      if (!Object.keys(metric).length || !Object.keys(dimension).length) {
        continue;
      }
      const numericValue = numberValue(dimension.value);
      const state = numericValue === undefined ? dimension.value : round2(numericValue) * (resource.invert ? -1 : 1);
      const unit = stringValue(metric.units) === 'percentage' ? '%' : stringValue(metric.units);
      entities.push({
        id: resource.id,
        name: resource.name,
        platform: 'sensor',
        state,
        unit,
        available: onlineArg,
        attributes: {
          data_group: resource.dataGroup,
          element: resource.element,
          icon: resource.icon,
          source: '/api/v1/allmetrics',
        },
      });
    }

    entities.push(this.alarmsEntity(alarmsArg, onlineArg));

    return {
      device: {
        id: this.configArg.uniqueId || (host ? `${host}:${port || ''}` : netdataProfile.domain),
        name: this.configArg.name || netdataProfile.defaultName || netdataProfile.displayName,
        manufacturer: netdataProfile.manufacturer,
        model: netdataProfile.model,
        host,
        port,
        protocol: this.configArg.useTls ? 'https' : 'http',
        configurationUrl: this.configurationUrl(),
      },
      entities,
      online: onlineArg,
      updatedAt: new Date().toISOString(),
      source: onlineArg ? 'http' : 'runtime',
      rawData: { metrics: metricsArg, alarms: alarmsArg },
      error: errorArg,
      capabilities: { localRead: true, localControl: false },
    };
  }

  private alarmsEntity(alarmsArg: Record<string, unknown>, onlineArg: boolean): ISimpleLocalEntitySnapshot {
    const alarms = record(alarmsArg.alarms);
    const values = Object.values(alarms).map((alarmArg) => record(alarmArg));
    const relevant = values.filter((alarmArg) => {
      const status = stringValue(alarmArg.status)?.toUpperCase();
      return stringValue(alarmArg.recipient) !== 'silent' && !['CLEAR', 'UNDEFINED', 'UNINITIALIZED'].includes(status || '');
    });
    const critical = relevant.filter((alarmArg) => stringValue(alarmArg.status)?.toUpperCase() === 'CRITICAL');
    return {
      id: 'alarms',
      name: `${this.configArg.name || netdataProfile.defaultName || netdataProfile.displayName} Alarms`,
      platform: 'sensor',
      state: critical.length ? 'critical' : relevant.length ? 'warning' : 'ok',
      available: onlineArg,
      attributes: {
        total: values.length,
        relevant: relevant.length,
        critical: critical.length,
        source: '/api/v1/alarms',
      },
    };
  }

  private resourceEntries(): Array<{ id: string; name: string; dataGroup: string; element: string; icon?: string; invert: boolean }> {
    const resources = record(this.configArg.resources);
    return Object.entries(resources).flatMap(([id, valueArg]) => {
      const resource = normalizeResource(id, valueArg);
      return resource ? [resource] : [];
    });
  }

  private buildOfflineSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return this.snapshotFromApiData({}, {}, false, errorArg);
  }

  private apiBaseUrl(): string | undefined {
    if (this.configArg.url) {
      const trimmedUrl = this.configArg.url.replace(/\/+$/, '');
      const parsed = safeUrl(trimmedUrl);
      if (!parsed || parsed.pathname === '/' || parsed.pathname === '') {
        return `${trimmedUrl}/api/v1`;
      }
      return trimmedUrl;
    }
    const host = this.configArg.host;
    if (!host) {
      return undefined;
    }
    const protocol = this.configArg.useTls ? 'https' : 'http';
    const port = this.configArg.port || netdataProfile.defaultPort || 19999;
    const portPart = (protocol === 'http' && port === 80) || (protocol === 'https' && port === 443) ? '' : `:${port}`;
    const apiPath = normalizePath(this.configArg.apiPath || this.configArg.path || '/api/v1');
    return `${protocol}://${host}${portPart}${apiPath}`.replace(/\/+$/, '');
  }

  private configurationUrl(): string | undefined {
    if (this.configArg.url) {
      const parsed = safeUrl(this.configArg.url);
      return parsed ? parsed.origin : this.configArg.url;
    }
    if (!this.configArg.host) {
      return undefined;
    }
    const protocol = this.configArg.useTls ? 'https' : 'http';
    const port = this.configArg.port || netdataProfile.defaultPort || 19999;
    const portPart = (protocol === 'http' && port === 80) || (protocol === 'https' && port === 443) ? '' : `:${port}`;
    return `${protocol}://${this.configArg.host}${portPart}`;
  }
}

const normalizeResource = (idArg: string, valueArg: unknown): { id: string; name: string; dataGroup: string; element: string; icon?: string; invert: boolean } | undefined => {
  if (typeof valueArg === 'string') {
    return { id: idArg, name: titleCase(idArg), dataGroup: idArg, element: valueArg, invert: false };
  }
  const resource = record(valueArg) as INetdataResourceConfig;
  const dataGroup = stringValue(resource.data_group) || stringValue(resource.dataGroup);
  const element = stringValue(resource.element);
  if (!dataGroup || !element) {
    return undefined;
  }
  return {
    id: idArg,
    name: stringValue(resource.name) || titleCase(idArg),
    dataGroup,
    element,
    icon: stringValue(resource.icon),
    invert: resource.invert === true,
  };
};

const record = (valueArg: unknown): Record<string, unknown> => valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Record<string, unknown> : {};

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 round2 = (valueArg: number): number => Math.round(valueArg * 100) / 100;

const titleCase = (valueArg: string): string => valueArg.replace(/[_-]+/g, ' ').replace(/\b\w/g, (charArg) => charArg.toUpperCase());

const normalizePath = (valueArg: string): string => `/${valueArg.replace(/^\/+|\/+$/g, '')}`;

const safeUrl = (valueArg: string | undefined): URL | undefined => {
  if (!valueArg) {
    return undefined;
  }
  try {
    return new URL(valueArg);
  } catch {
    return undefined;
  }
};

const hostFromUrl = (valueArg: string | undefined): string | undefined => safeUrl(valueArg)?.hostname;

const portFromUrl = (valueArg: string | undefined): number | undefined => {
  const parsed = safeUrl(valueArg);
  return parsed?.port ? Number(parsed.port) : undefined;
};

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