import { SimpleLocalClient } from '../../core/index.js';
import type { IServiceCallResult, ISimpleLocalSnapshot } from '../../core/index.js';
import { LuciMapper } from './luci.mapper.js';
import type { ILuciConfig, ILuciConnectedDevice, ILuciRawData } from './luci.types.js';
import { luciProfile } from './luci.types.js';

const openWrtVersionCommand = 'if [ -f "/etc/os-release" ]; then awk -F= \'$1=="VERSION_ID" { print $2 ;}\' /etc/os-release; else awk -F= \'$1=="DISTRIB_RELEASE" { print $2 ;}\' /etc/openwrt_release; fi | tr -d \'\\"\'';

export class LuciClient extends SimpleLocalClient<ILuciConfig> {
  private snapshot?: ISimpleLocalSnapshot;
  private token?: string;

  constructor(private readonly configData: ILuciConfig) {
    super(luciProfile, configData);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<ISimpleLocalSnapshot> {
    if (!forceRefreshArg && this.snapshot) {
      return clone(this.snapshot);
    }
    if (this.canUseNativeHttp()) {
      try {
        this.snapshot = await this.fetchSnapshot();
        return clone(this.snapshot);
      } catch (errorArg) {
        this.snapshot = LuciMapper.toSnapshotFromApi(this.configData, { router: { host: this.routerHost(), authenticated: false }, devices: [] }, false, 'runtime', errorMessage(errorArg));
        return clone(this.snapshot);
      }
    }
    this.snapshot = await super.getSnapshot(forceRefreshArg);
    return clone(this.snapshot);
  }

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

  private async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    await this.login();
    const version = await this.determineVersion();
    const legacy = isLegacyVersion(version);
    const onlyReachable = this.configData.onlyReachable ?? this.configData.only_reachable ?? true;
    const neighbors = legacy
      ? await this.jsonRpc<unknown[]>('sys', 'net.arptable')
      : await this.jsonRpc<unknown[]>('ip', 'neighbors', [{ family: 4 }]);
    const dhcp = await this.jsonRpc<Record<string, unknown>>('uci', 'get_all', ['dhcp']).catch(() => undefined);
    const devices = this.normalizeDevices(neighbors, dhcp, legacy, onlyReachable);
    const rawData: ILuciRawData = {
      router: {
        host: this.routerHost(),
        version,
        legacy,
        authenticated: true,
      },
      devices,
      dhcp,
      neighbors: legacy ? undefined : neighbors,
      arp: legacy ? neighbors : undefined,
    };
    return LuciMapper.toSnapshotFromApi(this.configData, rawData, true, 'http');
  }

  private async login(): Promise<void> {
    this.token = await this.jsonRpc<string>('auth', 'login', [this.configData.username || 'root', this.configData.password || ''], false);
    if (!this.token) {
      throw new Error('LuCI RPC login did not return an authentication token.');
    }
  }

  private async determineVersion(): Promise<string> {
    try {
      const version = await this.jsonRpc<string>('sys', 'exec', [openWrtVersionCommand]);
      return String(version || '18.06').replace(/\n/g, '').trim() || '18.06';
    } catch {
      return '18.06';
    }
  }

  private normalizeDevices(neighborsArg: unknown, dhcpArg: unknown, legacyArg: boolean, onlyReachableArg: boolean): ILuciConnectedDevice[] {
    if (!Array.isArray(neighborsArg)) {
      return [];
    }
    const devices: ILuciConnectedDevice[] = [];
    for (const entry of neighborsArg) {
      if (!entry || typeof entry !== 'object') {
        continue;
      }
      const normalized = normalizeKeys(entry as Record<string, unknown>);
      const mac = stringValue(normalized.mac);
      if (!mac) {
        continue;
      }
      if (legacyArg && onlyReachableArg && stringValue(normalized.Flags) && (Number.parseInt(String(normalized.Flags), 16) & 0x2) === 0) {
        continue;
      }
      if (!legacyArg && onlyReachableArg && normalized.reachable === false) {
        continue;
      }
      devices.push({
        ...normalized,
        mac,
        ip: stringValue(normalized.ip),
        hostname: hostnameFromDhcp(dhcpArg, mac),
        reachable: typeof normalized.reachable === 'boolean' ? normalized.reachable : true,
        dev: stringValue(normalized.dev),
        host: this.routerHost(),
        raw: normalized,
      });
    }
    return devices;
  }

  private async jsonRpc<TValue>(pathArg: 'auth' | 'sys' | 'ip' | 'uci', methodArg: string, paramsArg: unknown[] = [], includeAuthArg = true): Promise<TValue> {
    const token = includeAuthArg && this.token ? `?auth=${encodeURIComponent(this.token)}` : '';
    const response = await this.fetchWithTimeout(`${this.baseUrl()}/cgi-bin/luci/rpc/${pathArg}${token}`, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ method: methodArg, params: paramsArg }),
    });
    const text = await response.text();
    if (!response.ok) {
      throw new Error(`LuCI RPC ${pathArg}.${methodArg} failed with HTTP ${response.status}: ${text.slice(0, 160)}`);
    }
    const body = JSON.parse(text || '{}') as { result?: TValue | null; error?: { message?: string; code?: number } | null };
    if (body.result !== undefined && body.result !== null) {
      return body.result;
    }
    if (body.error) {
      throw new Error(body.error.message || `LuCI RPC ${pathArg}.${methodArg} error ${body.error.code ?? 'unknown'}`);
    }
    throw new Error(`LuCI RPC ${pathArg}.${methodArg} returned no result.`);
  }

  private async fetchWithTimeout(urlArg: string, initArg: RequestInit): Promise<Response> {
    const abortController = new AbortController();
    const timeout = setTimeout(() => abortController.abort(), this.configData.timeoutMs || 5000);
    try {
      return await globalThis.fetch(urlArg, { ...initArg, signal: abortController.signal });
    } finally {
      clearTimeout(timeout);
    }
  }

  private canUseNativeHttp(): boolean {
    return this.configData.nativeHttpEnabled !== false && Boolean(this.baseUrl(false));
  }

  private routerHost(): string | undefined {
    const baseUrl = this.baseUrl(false);
    if (baseUrl) {
      try {
        return new URL(baseUrl).host;
      } catch {}
    }
    return this.configData.host;
  }

  private baseUrl(requiredArg = true): string {
    if (this.configData.url) {
      return this.configData.url.replace(/\/+$/, '');
    }
    if (!this.configData.host) {
      if (requiredArg) {
        throw new Error('LuCI native HTTP requires config.url or config.host.');
      }
      return '';
    }
    if (/^https?:\/\//i.test(this.configData.host)) {
      return this.configData.host.replace(/\/+$/, '');
    }
    const ssl = this.configData.ssl ?? this.configData.useSsl ?? this.configData.useTls ?? false;
    const port = this.configData.port;
    const host = this.configData.host;
    const portPart = port && !host.includes(':') && !((ssl && port === 443) || (!ssl && port === 80)) ? `:${port}` : '';
    return `${ssl ? 'https' : 'http'}://${host}${portPart}`;
  }
}

const normalizeKeys = (entryArg: Record<string, unknown>): Record<string, unknown> => {
  const result: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(entryArg)) {
    result[key.replace(/[. ]/g, '_')] = value;
  }
  if ('HW_address' in result) {
    result.mac = result.HW_address;
    delete result.HW_address;
  }
  if ('IP_address' in result) {
    result.ip = result.IP_address;
    delete result.IP_address;
  }
  if ('dest' in result) {
    result.ip = result.dest;
    delete result.dest;
  }
  return result;
};

const hostnameFromDhcp = (dhcpArg: unknown, macArg: string): string | null => {
  if (!dhcpArg || typeof dhcpArg !== 'object' || Array.isArray(dhcpArg)) {
    return null;
  }
  const needle = macArg.toUpperCase();
  for (const value of Object.values(dhcpArg as Record<string, unknown>)) {
    if (!value || typeof value !== 'object' || Array.isArray(value)) {
      continue;
    }
    const record = value as Record<string, unknown>;
    if (record['.type'] === 'host' && typeof record.mac === 'string' && typeof record.name === 'string' && record.mac.toUpperCase().includes(needle)) {
      return record.name;
    }
  }
  return null;
};

const isLegacyVersion = (versionArg: string): boolean => {
  if (versionArg.toLowerCase().includes('snapshot')) {
    return false;
  }
  const match = versionArg.match(/\d+(?:\.\d+)?/);
  const value = match ? Number.parseFloat(match[0]) : 18.06;
  return value >= 15.06 && value < 18.06;
};

const stringValue = (valueArg: unknown): string | undefined => {
  return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
};

const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;

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