const AUTH_COOKIE = "bmp_demo_auth";
const CSRF_COOKIE = "bmp_demo_csrf";
const AUTH_TTL_SECONDS = 8 * 60 * 60;
const DEFAULT_LLM_MODEL = "openai/gpt-5.4-nano";

const configuredPassword =
  process.env.BMP_DEMO_PASSWORD ??
  process.env.APP_ACCESS_TOKEN ??
  process.env.DEMO_ACCESS_TOKEN;

export const demoPassword = configuredPassword || crypto.randomUUID();

if (!configuredPassword) {
  console.warn("BMP_DEMO_PASSWORD is not set. Using this one-time demo password:");
  console.warn(`  ${demoPassword}`);
}

const rateLimitBuckets = new Map<string, { count: number; resetAt: number }>();

export interface SecurityOptions {
  csrf?: boolean;
  limit?: {
    key: string;
    max: number;
    windowMs: number;
  };
}

export type RouteHandler<T extends Request = Request> = (req: T) => Response | Promise<Response>;

export const securityHeaders: Record<string, string> = {
  "X-Content-Type-Options": "nosniff",
  "Referrer-Policy": "same-origin",
  "X-Frame-Options": "DENY",
  "Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=()",
};

export function isSafeStem(value: string): boolean {
  return /^[a-z0-9][a-z0-9_-]{0,80}$/i.test(value);
}

export function normalizeStem(value: string): string {
  const ascii = value
    .normalize("NFKD")
    .replace(/\p{Diacritic}/gu, "")
    .replace(/ß/g, "ss")
    .replace(/ẞ/g, "SS");
  return ascii
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/_+/g, "_")
    .replace(/^_|_$/g, "")
    .slice(0, 80);
}

export function allowedModelFromEnv(): string {
  return process.env.OPENROUTER_MODEL || DEFAULT_LLM_MODEL;
}

export function validateRequestedModel(value: unknown): string | undefined {
  const allowed = allowedModelFromEnv();
  if (value == null || value === "") return allowed;
  return value === allowed ? allowed : undefined;
}

export function getCsrfCookieName(): string {
  return CSRF_COOKIE;
}

export function csrfClientScript(): string {
  return `
    function csrfHeaders(extra = {}) {
      const csrf = document.cookie
        .split("; ")
        .find((part) => part.startsWith("${CSRF_COOKIE}="))
        ?.slice("${CSRF_COOKIE}=".length);
      return csrf ? { ...extra, "X-BMP-CSRF": decodeURIComponent(csrf) } : extra;
    }
  `;
}

export function addSecurityHeaders(response: Response): Response {
  for (const [key, value] of Object.entries(securityHeaders)) {
    response.headers.set(key, value);
  }
  return response;
}

export async function requireAuth(req: Request, options: SecurityOptions = {}): Promise<Response | undefined> {
  if (!rateLimit(req, "auth-check", 600, 60_000)) {
    return jsonError("Too many requests", 429);
  }

  if (options.limit && !rateLimit(req, options.limit.key, options.limit.max, options.limit.windowMs)) {
    return jsonError("Too many requests", 429);
  }

  if (!(await isAuthenticated(req))) {
    return unauthorized(req);
  }

  if (options.csrf && !hasValidCsrf(req)) {
    return jsonError("Invalid CSRF token", 403);
  }

  return undefined;
}

export function withAuth<T extends Request>(handler: (req: T) => unknown | Promise<unknown>, options: SecurityOptions = {}): RouteHandler<T> {
  return async (req: T) => {
    const blocked = await requireAuth(req, options);
    if (blocked) return blocked;
    const response = await handler(req);
    return (response instanceof Response ? addSecurityHeaders(response) : response) as Response;
  };
}

export function loginPage(req: Request): Response {
  const next = safeNextUrl(new URL(req.url).searchParams.get("next") || "/");
  return html(`<!DOCTYPE html>
<html lang="de">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>BMP Demo Login</title>
  <style>
    *, *::before, *::after { box-sizing: border-box; }
    body { min-height: 100vh; margin: 0; display: grid; place-items: center; padding: 24px; background: #f4f7fb; color: #182230; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
    form { width: min(420px, 100%); display: grid; gap: 16px; padding: 24px; border: 1px solid #d7e0ea; border-radius: 8px; background: white; box-shadow: 0 20px 60px rgba(15, 23, 42, 0.1); }
    h1 { margin: 0; font-size: 24px; }
    p { margin: 0; color: #627084; line-height: 1.5; }
    label { display: grid; gap: 6px; font-size: 13px; color: #405166; font-weight: 700; }
    input { min-height: 44px; border: 1px solid #b9c7d6; border-radius: 7px; padding: 10px 12px; font: inherit; }
    button { min-height: 44px; border: 0; border-radius: 7px; background: #2563eb; color: white; font: inherit; font-weight: 760; cursor: pointer; }
  </style>
</head>
<body>
  <form method="post" action="/login">
    <h1>BMP Demo</h1>
    <p>Bitte melden Sie sich an, bevor Bewerbungen, Berichte oder LLM-Jobs geöffnet werden.</p>
    <input type="hidden" name="next" value="${escapeHtml(next)}" />
    <label>Demo-Passwort <input name="password" type="password" autocomplete="current-password" autofocus required /></label>
    <button type="submit">Einloggen</button>
  </form>
</body>
</html>`);
}

export async function login(req: Request): Promise<Response> {
  if (!rateLimit(req, "login", 8, 5 * 60_000)) {
    return html("Zu viele Login-Versuche. Bitte kurz warten.", 429);
  }

  let form: FormData;
  try {
    form = await req.formData();
  } catch {
    return html("Ungültige Login-Anfrage.", 400);
  }

  const password = String(form.get("password") ?? "");
  const next = safeNextUrl(String(form.get("next") ?? "/"));
  if (!timingSafeEqual(password, demoPassword)) {
    return html("Falsches Demo-Passwort.", 401);
  }

  const expiresAt = Math.floor(Date.now() / 1000) + AUTH_TTL_SECONDS;
  const authCookie = await createAuthCookie(expiresAt);
  const csrf = crypto.randomUUID();
  const headers = new Headers({
    ...securityHeaders,
    Location: next,
  });
  headers.append("Set-Cookie", `${AUTH_COOKIE}=${authCookie}; Max-Age=${AUTH_TTL_SECONDS}; Path=/; HttpOnly; SameSite=Strict`);
  headers.append("Set-Cookie", `${CSRF_COOKIE}=${csrf}; Max-Age=${AUTH_TTL_SECONDS}; Path=/; SameSite=Strict`);
  return new Response(null, {
    status: 303,
    headers,
  });
}

export function logout(): Response {
  const headers = new Headers({
    ...securityHeaders,
    Location: "/login",
  });
  headers.append("Set-Cookie", `${AUTH_COOKIE}=; Max-Age=0; Path=/; HttpOnly; SameSite=Strict`);
  headers.append("Set-Cookie", `${CSRF_COOKIE}=; Max-Age=0; Path=/; SameSite=Strict`);
  return new Response(null, {
    status: 303,
    headers,
  });
}

export function jsonError(error: string, status: number): Response {
  return addSecurityHeaders(Response.json({ error }, { status }));
}

function unauthorized(req: Request): Response {
  const url = new URL(req.url);
  const acceptsHtml = req.headers.get("accept")?.includes("text/html");
  if (req.method === "GET" && acceptsHtml) {
    return new Response(null, {
      status: 303,
      headers: {
        ...securityHeaders,
        Location: `/login?next=${encodeURIComponent(url.pathname + url.search)}`,
      },
    });
  }
  return jsonError("Authentication required", 401);
}

function html(body: string, status = 200): Response {
  return new Response(body, {
    status,
    headers: {
      ...securityHeaders,
      "Content-Type": "text/html; charset=utf-8",
      "Content-Security-Policy": "default-src 'self'; style-src 'unsafe-inline' 'self'; script-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'",
    },
  });
}

function rateLimit(req: Request, key: string, max: number, windowMs: number): boolean {
  const bucketKey = `${key}:${clientIp(req)}`;
  const now = Date.now();
  const bucket = rateLimitBuckets.get(bucketKey);
  if (!bucket || bucket.resetAt <= now) {
    rateLimitBuckets.set(bucketKey, { count: 1, resetAt: now + windowMs });
    return true;
  }
  bucket.count += 1;
  return bucket.count <= max;
}

function clientIp(req: Request): string {
  return req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "local";
}

function hasValidCsrf(req: Request): boolean {
  const header = req.headers.get("x-bmp-csrf");
  const cookie = parseCookies(req.headers.get("cookie")).get(CSRF_COOKIE);
  return Boolean(header && cookie && timingSafeEqual(header, cookie));
}

export async function isAuthenticated(req: Request): Promise<boolean> {
  const auth = parseCookies(req.headers.get("cookie")).get(AUTH_COOKIE);
  if (auth && await verifyAuthCookie(auth)) return true;

  const authorization = req.headers.get("authorization") ?? "";
  if (authorization.startsWith("Bearer ")) {
    return timingSafeEqual(authorization.slice("Bearer ".length), demoPassword);
  }
  if (authorization.startsWith("Basic ")) {
    try {
      const decoded = atob(authorization.slice("Basic ".length));
      const password = decoded.includes(":") ? decoded.slice(decoded.indexOf(":") + 1) : decoded;
      return timingSafeEqual(password, demoPassword);
    } catch {
      return false;
    }
  }
  return false;
}

async function createAuthCookie(expiresAt: number): Promise<string> {
  const payload = String(expiresAt);
  return `${payload}.${await sign(payload)}`;
}

async function verifyAuthCookie(value: string): Promise<boolean> {
  const [payload, signature] = value.split(".");
  const expiresAt = Number(payload);
  if (!payload || !signature || !Number.isFinite(expiresAt) || expiresAt < Math.floor(Date.now() / 1000)) {
    return false;
  }
  return timingSafeEqual(signature, await sign(payload));
}

async function sign(payload: string): Promise<string> {
  const key = await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(demoPassword),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
  );
  const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload));
  return Array.from(new Uint8Array(signature), (byte) => byte.toString(16).padStart(2, "0")).join("");
}

function parseCookies(header: string | null): Map<string, string> {
  const cookies = new Map<string, string>();
  for (const part of (header ?? "").split(";")) {
    const index = part.indexOf("=");
    if (index < 0) continue;
    cookies.set(part.slice(0, index).trim(), decodeURIComponent(part.slice(index + 1).trim()));
  }
  return cookies;
}

function timingSafeEqual(a: string, b: string): boolean {
  const left = new TextEncoder().encode(a);
  const right = new TextEncoder().encode(b);
  const length = Math.max(left.length, right.length);
  let diff = left.length ^ right.length;
  for (let index = 0; index < length; index += 1) {
    diff |= (left[index] ?? 0) ^ (right[index] ?? 0);
  }
  return diff === 0;
}

function safeNextUrl(value: string): string {
  return value.startsWith("/") && !value.startsWith("//") ? value : "/";
}

function escapeHtml(value: string): string {
  return value.replace(/[&<>"']/g, (char) => ({
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&#39;",
  }[char]!));
}
