import { isIP } from "net";
import { isBlockedAddress } from "../utils/safe-outbound-fetch.js";
import { createCurrentSettingsRepository } from "../database/repositories/factory.js";

/**
 * Where the assistant is allowed to send requests.
 *
 * Cloud providers are reached through the SSRF-guarded path, which refuses to
 * resolve to a private address. That guard is exactly what a self-hosted Ollama
 * on localhost trips over, so private destinations are permitted only when an
 * admin has named the host. Without that split, any logged-in user could point
 * a "provider" at an internal service and use the backend as an authenticated
 * probe of the server's own network.
 */

export const AI_PRIVATE_ALLOWLIST_KEY = "ai_private_endpoint_allowlist";

/** Hosts a self-hoster almost certainly wants, and which reach only this machine. */
export const DEFAULT_PRIVATE_ALLOWLIST = [
  "localhost",
  "127.0.0.1",
  "::1",
  "host.docker.internal",
];

export function parseAllowlist(raw: string | null): string[] {
  if (!raw) return [...DEFAULT_PRIVATE_ALLOWLIST];
  try {
    const parsed = JSON.parse(raw);
    if (!Array.isArray(parsed)) return [...DEFAULT_PRIVATE_ALLOWLIST];
    return parsed
      .filter((entry): entry is string => typeof entry === "string")
      .map((entry) => entry.trim().toLowerCase())
      .filter(Boolean);
  } catch {
    return [...DEFAULT_PRIVATE_ALLOWLIST];
  }
}

export async function readPrivateAllowlist(): Promise<string[]> {
  const raw = await createCurrentSettingsRepository().get(
    AI_PRIVATE_ALLOWLIST_KEY,
  );
  return parseAllowlist(raw);
}

function normalizeHost(hostname: string): string {
  return hostname.replace(/^\[|\]$/g, "").toLowerCase();
}

/**
 * True when the URL names a destination the SSRF guard would refuse. A bare
 * hostname that is not an IP literal (e.g. "ollama.internal") is treated as
 * private only if it is "localhost" -- anything else needs DNS resolution.
 */
export function isPrivateDestination(rawUrl: string): boolean {
  let url: URL;
  try {
    url = new URL(rawUrl);
  } catch {
    return false;
  }
  const host = normalizeHost(url.hostname);
  if (host === "localhost" || host.endsWith(".localhost")) return true;
  if (isIP(host)) return isBlockedAddress(host);
  return false;
}

export interface EgressDecision {
  allowed: boolean;
  /** True when the destination needs the allowlisted-private path. */
  isPrivate: boolean;
  reason?: string;
}

export function evaluateEgress(
  rawUrl: string,
  allowlist: string[],
): EgressDecision {
  let url: URL;
  try {
    url = new URL(rawUrl);
  } catch {
    return { allowed: false, isPrivate: false, reason: "Invalid URL" };
  }

  if (!["http:", "https:"].includes(url.protocol)) {
    return { allowed: false, isPrivate: false, reason: "Unsupported protocol" };
  }
  if (url.username || url.password) {
    return {
      allowed: false,
      isPrivate: false,
      reason: "Credentials in URL are not allowed",
    };
  }

  const host = normalizeHost(url.hostname);
  const isPrivate = isPrivateDestination(rawUrl);
  const normalized = allowlist.map((entry) => entry.trim().toLowerCase());

  // An explicitly allowlisted hostname may resolve to a private address. It
  // must use the private fetch path; sending it through safeOutboundFetch
  // would reject it after DNS resolution and make hostname allowlist entries
  // ineffective. Only administrators can write this list.
  if (normalized.includes(host)) {
    return { allowed: true, isPrivate: true };
  }

  if (!isPrivate) return { allowed: true, isPrivate: false };

  return {
    allowed: false,
    isPrivate: true,
    reason:
      "This address is on a private network. An administrator must add its host to the AI endpoint allowlist first.",
  };
}
