import { type NextRequest } from "next/server";

const MAX_STORED_REJECTED_ATTEMPTS = 50;

export type TrustedOriginDiagnostics = {
  originPresent: boolean;
  deliveredOrigin: string | null;
  normalizedDeliveredOrigin: string | null;
  trustedOriginsConfigured: boolean;
  expectedTrustedOrigins: string[];
  expectedTrustedOriginsRaw: string;
  allowed: boolean;
  suggestedTrustedOrigin: string | null;
  suggestedTrustedOriginsRaw: string;
  suggestedTrustedOriginsEnvLine: string | null;
  suggestionReason: string;
};

export type TrustedOriginViolationPhase = "proxy_preflight" | "proxy_request";

export type TrustedOriginViolationAttempt = TrustedOriginDiagnostics & {
  id: string;
  occurredAt: string;
  phase: TrustedOriginViolationPhase;
  method: string;
  requestPath: string;
  requestUrl: string;
  referer: string | null;
};

const recentRejectedAttempts: TrustedOriginViolationAttempt[] = [];

function escapeRegex(value: string): string {
  return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
}

function patternToRegExp(pattern: string): RegExp {
  const source = pattern
    .split("*")
    .map((part) => escapeRegex(part))
    .join(".*");
  return new RegExp(`^${source}$`, "i");
}

function stripPathQueryFragment(value: string): string {
  const schemeIndex = value.indexOf("://");
  const authorityStart = schemeIndex >= 0 ? schemeIndex + 3 : 0;
  const suffix = value.slice(authorityStart);
  const authorityEnd = suffix.search(/[/?#]/);
  if (authorityEnd < 0) return value;
  return value.slice(0, authorityStart + authorityEnd);
}

function extractHostFromAuthority(authority: string): string {
  const withoutUserInfo = authority.split("@").pop() ?? authority;
  if (withoutUserInfo.startsWith("[")) {
    const end = withoutUserInfo.indexOf("]");
    if (end > 0) return withoutUserInfo.slice(1, end);
  }
  return withoutUserInfo.split(":")[0] ?? withoutUserInfo;
}

function isLocalhostHost(host: string): boolean {
  const normalized = host.toLowerCase();
  return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
}

function parseTrustedOriginEntries(raw: string | undefined): string[] {
  return (raw ?? "")
    .split(",")
    .map((part) => part.trim())
    .filter(Boolean);
}

export function normalizeOrigin(value: string): string | null {
  const trimmed = value.trim();
  if (!trimmed || trimmed.toLowerCase() === "null") return null;
  try {
    return new URL(trimmed).origin.toLowerCase();
  } catch {
    return null;
  }
}

export function normalizeTrustedOriginPattern(pattern: string): string | null {
  const raw = pattern.trim().replace(/\/+$/, "");
  if (!raw) return null;
  if (raw === "*") return raw;

  if (raw.includes("://")) {
    return stripPathQueryFragment(raw).toLowerCase();
  }

  const authority = stripPathQueryFragment(raw).trim();
  if (!authority) return null;

  const host = extractHostFromAuthority(authority);
  const scheme = isLocalhostHost(host) ? "http" : "https";
  return `${scheme}://${authority}`.toLowerCase();
}

export function parseTrustedOriginPatterns(raw: string | undefined): string[] {
  return parseTrustedOriginEntries(raw)
    .map((part) => normalizeTrustedOriginPattern(part))
    .filter((part): part is string => part !== null);
}

export function isAllowedOrigin(origin: string, trustedPatterns: string[]): boolean {
  const normalizedOrigin = normalizeOrigin(origin);
  if (!normalizedOrigin) return false;
  if (trustedPatterns.length === 0) return false;

  return trustedPatterns.some((pattern) => {
    if (pattern === "*") return true;
    return patternToRegExp(pattern).test(normalizedOrigin);
  });
}

function buildSuggestedTrustedOriginsRaw(
  rawTrustedOrigins: string | undefined,
  suggestedTrustedOrigin: string | null,
): string {
  const entries = parseTrustedOriginEntries(rawTrustedOrigins);
  if (!suggestedTrustedOrigin) {
    return entries.join(",");
  }

  const normalizedEntries = new Set(parseTrustedOriginPatterns(rawTrustedOrigins));
  if (normalizedEntries.has(suggestedTrustedOrigin)) {
    return entries.join(",");
  }

  entries.push(suggestedTrustedOrigin);
  return entries.join(",");
}

export function getTrustedOriginDiagnostics(
  origin: string | null,
  rawTrustedOrigins = process.env.TRUSTED_ORIGINS,
): TrustedOriginDiagnostics {
  const expectedTrustedOrigins = parseTrustedOriginPatterns(rawTrustedOrigins);
  const normalizedDeliveredOrigin = origin ? normalizeOrigin(origin) : null;
  const suggestedTrustedOrigin = normalizedDeliveredOrigin;
  const suggestedTrustedOriginsRaw = buildSuggestedTrustedOriginsRaw(
    rawTrustedOrigins,
    suggestedTrustedOrigin,
  );
  const suggestedTrustedOriginsEnvLine =
    suggestedTrustedOriginsRaw.length > 0
      ? `TRUSTED_ORIGINS=${suggestedTrustedOriginsRaw}`
      : null;
  const suggestionReason = suggestedTrustedOrigin
    ? `Add ${suggestedTrustedOrigin} to TRUSTED_ORIGINS or widen an existing wildcard so it matches this origin.`
    : "The delivered Origin header was missing or invalid, so there is no safe TRUSTED_ORIGINS addition to suggest.";

  return {
    originPresent: typeof origin === "string" && origin.trim().length > 0,
    deliveredOrigin: origin,
    normalizedDeliveredOrigin,
    trustedOriginsConfigured: expectedTrustedOrigins.length > 0,
    expectedTrustedOrigins,
    expectedTrustedOriginsRaw: rawTrustedOrigins ?? "",
    allowed: origin ? isAllowedOrigin(origin, expectedTrustedOrigins) : false,
    suggestedTrustedOrigin,
    suggestedTrustedOriginsRaw,
    suggestedTrustedOriginsEnvLine,
    suggestionReason,
  };
}

export function recordTrustedOriginViolationAttempt(
  request: NextRequest,
  diagnostics: TrustedOriginDiagnostics,
  phase: TrustedOriginViolationPhase,
): TrustedOriginViolationAttempt {
  const entry: TrustedOriginViolationAttempt = {
    id: crypto.randomUUID(),
    occurredAt: new Date().toISOString(),
    phase,
    method: request.method,
    requestPath: request.nextUrl.pathname,
    requestUrl: request.nextUrl.toString(),
    referer: request.headers.get("referer"),
    ...diagnostics,
  };

  recentRejectedAttempts.unshift(entry);
  if (recentRejectedAttempts.length > MAX_STORED_REJECTED_ATTEMPTS) {
    recentRejectedAttempts.length = MAX_STORED_REJECTED_ATTEMPTS;
  }

  return entry;
}

export function snapshotTrustedOriginViolations() {
  return {
    maxStoredRejectedAttempts: MAX_STORED_REJECTED_ATTEMPTS,
    rejectedAttemptCount: recentRejectedAttempts.length,
    recentRejectedAttempts: recentRejectedAttempts.map((entry) => ({ ...entry })),
  };
}

export function buildProxyTrustedOriginDebugSnapshot(
  origin: string | null,
  rawTrustedOrigins = process.env.TRUSTED_ORIGINS,
) {
  return {
    currentRequest: getTrustedOriginDiagnostics(origin, rawTrustedOrigins),
    runtime: {
      trustedOriginsRaw: rawTrustedOrigins ?? "",
      trustedOrigins: parseTrustedOriginPatterns(rawTrustedOrigins),
    },
    ...snapshotTrustedOriginViolations(),
    note: "Recent rejected origin attempts are stored in-memory in the current Next.js process and reset on restart or redeploy.",
  };
}
