/**
 * Pure utility functions extracted from caddy.ts.
 * No DB, network, or filesystem dependencies — safe to unit-test directly.
 */
import { isIP } from "node:net";

// ---------------------------------------------------------------------------
// Private range expansion
// ---------------------------------------------------------------------------

export const PRIVATE_RANGES_CIDRS = [
  "10.0.0.0/8",
  "172.16.0.0/12",
  "192.168.0.0/16",
  "127.0.0.0/8",
  "fd00::/8",
  "::1/128",
];

export function expandPrivateRanges(proxies: string[]): string[] {
  if (!proxies.includes("private_ranges")) return proxies;
  return proxies.flatMap((p) => (p === "private_ranges" ? PRIVATE_RANGES_CIDRS : [p]));
}

// ---------------------------------------------------------------------------
// Type helpers
// ---------------------------------------------------------------------------

export function isPlainObject(value: unknown): value is Record<string, unknown> {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

// ---------------------------------------------------------------------------
// Deep merge (prototype-pollution safe)
// ---------------------------------------------------------------------------

export function mergeDeep(
  target: Record<string, unknown>,
  source: Record<string, unknown>
) {
  for (const [key, value] of Object.entries(source)) {
    if (key === "__proto__" || key === "constructor" || key === "prototype") {
      continue;
    }
    const existing = target[key];
    if (isPlainObject(existing) && isPlainObject(value)) {
      mergeDeep(existing, value);
    } else {
      target[key] = value;
    }
  }
}

// ---------------------------------------------------------------------------
// JSON helpers
// ---------------------------------------------------------------------------

export function parseJson<T>(value: string | null, fallback: T): T {
  if (!value) return fallback;
  try {
    return JSON.parse(value) as T;
  } catch (error) {
    console.warn("Failed to parse JSON value", value, error);
    return fallback;
  }
}

export function parseOptionalJson(value: string | null | undefined) {
  if (!value) return null;
  try {
    return JSON.parse(value);
  } catch (error) {
    console.warn("Failed to parse custom JSON", error);
    return null;
  }
}

export function parseCustomHandlers(
  value: string | null | undefined
): Record<string, unknown>[] {
  const parsed = parseOptionalJson(value);
  if (!parsed) return [];
  const list = Array.isArray(parsed) ? parsed : [parsed];
  const handlers: Record<string, unknown>[] = [];
  for (const item of list) {
    if (isPlainObject(item)) {
      handlers.push(item);
    } else {
      console.warn("Ignoring custom handler entry that is not an object", item);
    }
  }
  return handlers;
}

// ---------------------------------------------------------------------------
// Address / upstream parsing
// ---------------------------------------------------------------------------

export function formatDialAddress(host: string, port: string) {
  return isIP(host) === 6 ? `[${host}]:${port}` : `${host}:${port}`;
}

export function parseHostPort(
  value: string
): { host: string; port: string } | null {
  const trimmed = value.trim();
  if (!trimmed) return null;

  if (trimmed.startsWith("[")) {
    const closeIndex = trimmed.indexOf("]");
    if (closeIndex <= 1) return null;
    const host = trimmed.slice(1, closeIndex);
    const remainder = trimmed.slice(closeIndex + 1);
    if (!remainder.startsWith(":")) return null;
    const port = remainder.slice(1).trim();
    if (!port) return null;
    return { host, port };
  }

  const firstColon = trimmed.indexOf(":");
  const lastColon = trimmed.lastIndexOf(":");
  if (firstColon === -1 || firstColon !== lastColon) return null;

  const host = trimmed.slice(0, lastColon).trim();
  const port = trimmed.slice(lastColon + 1).trim();
  if (!host || !port) return null;

  return { host, port };
}

export type ParsedUpstreamTarget = {
  original: string;
  dial: string;
  scheme: "http" | "https" | null;
  host: string | null;
  port: string | null;
};

export function parseUpstreamTarget(upstream: string): ParsedUpstreamTarget {
  const trimmed = upstream.trim();
  if (!trimmed) {
    return { original: upstream, dial: upstream, scheme: null, host: null, port: null };
  }

  try {
    const url = new URL(trimmed);
    if (url.protocol === "http:" || url.protocol === "https:") {
      const scheme = url.protocol === "https:" ? "https" : "http";
      const port = url.port || (scheme === "https" ? "443" : "80");
      const host = url.hostname;
      return { original: trimmed, dial: formatDialAddress(host, port), scheme, host, port };
    }
  } catch {
    // fall through
  }

  const parsed = parseHostPort(trimmed);
  if (!parsed) {
    return { original: trimmed, dial: trimmed, scheme: null, host: null, port: null };
  }

  return {
    original: trimmed,
    dial: formatDialAddress(parsed.host, parsed.port),
    scheme: null,
    host: parsed.host,
    port: parsed.port,
  };
}

// ---------------------------------------------------------------------------
// Duration parsing
// ---------------------------------------------------------------------------

export function toDurationMs(value: string | null | undefined): number | null {
  if (!value) return null;
  const trimmed = value.trim();
  if (!trimmed) return null;

  const regex = /(\d+(?:\.\d+)?)(ms|s|m|h)/g;
  let total = 0;
  let matched = false;
  let consumed = 0;

  while (true) {
    const match = regex.exec(trimmed);
    if (!match) break;
    matched = true;
    consumed += match[0].length;
    const valueNum = Number.parseFloat(match[1]);
    if (!Number.isFinite(valueNum)) return null;
    const unit = match[2];
    if (unit === "ms") total += valueNum;
    else if (unit === "s") total += valueNum * 1000;
    else if (unit === "m") total += valueNum * 60_000;
    else if (unit === "h") total += valueNum * 3_600_000;
  }

  if (!matched || consumed !== trimmed.length) return null;

  const rounded = Math.round(total);
  return rounded > 0 ? rounded : null;
}
