import { randomBytes } from "node:crypto";
import { extname } from "node:path";

const DEFAULT_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
const DEFAULT_SESSION_VALIDATION_TTL_MS = 20_000;
const MIN_SESSION_VALIDATION_TTL_MS = 15_000;
const MAX_SESSION_VALIDATION_TTL_MS = 30_000;
const DEFAULT_SESSION_VALIDATION_GRACE_MS = 30_000;
const MAX_SESSION_VALIDATION_GRACE_MS = 60_000;
const DEFAULT_SESSION_VALIDATION_RETRY_MS = 2_000;

function parseBoolean(value, fallback) {
  if (value === undefined || value === null || value === "") return fallback;
  return ["1", "true", "yes", "on"].includes(String(value).trim().toLowerCase());
}

function boundedInteger(value, fallback, minimum, maximum) {
  const parsed = Number.parseInt(String(value ?? ""), 10);
  if (!Number.isFinite(parsed)) return fallback;
  return Math.min(maximum, Math.max(minimum, parsed));
}

function normalizedBaseUrl(value, fallback) {
  return String(value || fallback).trim().replace(/\/$/, "");
}

function sanitizeReturnTo(value, fallback = "/") {
  return typeof value === "string" && value.startsWith("/") && !value.startsWith("//") ? value : fallback;
}

function parseCookies(cookieHeader) {
  if (!cookieHeader) return {};

  return Object.fromEntries(
    String(cookieHeader)
      .split(";")
      .flatMap((part) => {
        const separator = part.indexOf("=");
        if (separator === -1) return [];
        const key = part.slice(0, separator).trim();
        const rawValue = part.slice(separator + 1).trim();
        try {
          return [[key, decodeURIComponent(rawValue)]];
        } catch {
          return [[key, rawValue]];
        }
      })
  );
}

function appendSetCookie(response, cookie) {
  const current = response.getHeader("Set-Cookie");
  if (!current) {
    response.setHeader("Set-Cookie", cookie);
    return;
  }
  response.setHeader("Set-Cookie", Array.isArray(current) ? [...current, cookie] : [current, cookie]);
}

function sendJson(response, statusCode, body) {
  response.statusCode = statusCode;
  response.setHeader("Content-Type", "application/json; charset=utf-8");
  response.setHeader("Cache-Control", "no-store");
  response.end(JSON.stringify(body));
}

function sendText(response, statusCode, body) {
  response.statusCode = statusCode;
  response.setHeader("Content-Type", "text/plain; charset=utf-8");
  response.setHeader("Cache-Control", "no-store");
  response.end(body);
}

function isHtmlNavigation(request, url) {
  if (request.method !== "GET" && request.method !== "HEAD") return false;
  if (url.pathname.startsWith("/api/") || url.pathname.startsWith("/auth/")) return false;
  const accept = String(request.headers.accept || "");
  return url.pathname === "/" || (!extname(url.pathname) && accept.includes("text/html"));
}

function identityGroupNames(user) {
  if (!user || typeof user !== "object") return new Set();
  const values = [user.groups, user.roles, user.roleKeys, user.permissions];
  const names = new Set();
  for (const value of values) {
    const items = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
    for (const item of items) {
      const name = typeof item === "string"
        ? item
        : item && typeof item === "object"
          ? item.name || item.key || item.slug || item.id
          : "";
      const normalized = String(name || "").trim().toLowerCase();
      if (normalized) names.add(normalized);
    }
  }
  return names;
}

function resolveFoundryAccess(user, authRequired) {
  // Local development deliberately remains operable without a Launcher. In a
  // deployed instance every decision below comes only from the revalidated
  // Launcher/Authentik session identity, never from a browser header.
  if (!authRequired && !user) return { role: "admin", allowed: true, admin: true };
  if (!user || typeof user !== "object") return { role: "none", allowed: false, admin: false };

  const id = String(user.id || user.subject || user.sub || "").trim().toLowerCase();
  const groups = identityGroupNames(user);
  // A block is deny-first: it wins even over an accidentally retained admin
  // group, so Launcher can revoke a service session deterministically.
  if (groups.has("nodedc:module-foundry:blocked")) return { role: "blocked", allowed: false, admin: false };
  if (id === "user_root" || groups.has("nodedc:superadmin") || groups.has("nodedc:module-foundry:admin")) {
    return { role: "admin", allowed: true, admin: true };
  }
  if (groups.has("nodedc:module-foundry:user") || groups.has("nodedc:module-foundry:access")) {
    return { role: "user", allowed: true, admin: false };
  }
  return { role: "none", allowed: false, admin: false };
}

export function createFoundryAuth(options = {}) {
  const now = typeof options.now === "function" ? options.now : Date.now;
  const fetchImpl = typeof options.fetch === "function" ? options.fetch : fetch;
  const authRequired = parseBoolean(process.env.NODEDC_FOUNDRY_AUTH_REQUIRED, process.env.NODE_ENV === "production");
  const serviceSlug = String(process.env.NODEDC_FOUNDRY_SERVICE_SLUG || "module-foundry").trim() || "module-foundry";
  const launcherBaseUrl = normalizedBaseUrl(process.env.NODEDC_LAUNCHER_BASE_URL, "http://127.0.0.1:5173");
  const launcherInternalUrl = normalizedBaseUrl(process.env.NODEDC_LAUNCHER_INTERNAL_URL, launcherBaseUrl);
  const internalAccessToken = String(
    process.env.NODEDC_INTERNAL_ACCESS_TOKEN || process.env.NODEDC_PLATFORM_SERVICE_TOKEN || ""
  ).trim();
  const sessionCookie = String(process.env.NODEDC_FOUNDRY_SESSION_COOKIE || "nodedc_foundry_session").trim();
  const sessionTtlMs = Math.max(
    60 * 1000,
    Number.parseInt(process.env.NODEDC_FOUNDRY_SESSION_TTL_MS || String(DEFAULT_SESSION_TTL_MS), 10) || DEFAULT_SESSION_TTL_MS
  );
  // Launcher revocation remains bounded while a tile burst no longer performs
  // one control-plane validation per object. Production configuration is
  // deliberately clamped to a narrow range; tests may inject deterministic
  // values without changing runtime policy.
  const sessionValidationTtlMs = options.validationTtlMs ?? boundedInteger(
    process.env.NODEDC_FOUNDRY_SESSION_VALIDATION_TTL_MS,
    DEFAULT_SESSION_VALIDATION_TTL_MS,
    MIN_SESSION_VALIDATION_TTL_MS,
    MAX_SESSION_VALIDATION_TTL_MS,
  );
  const sessionValidationGraceMs = options.validationGraceMs ?? boundedInteger(
    process.env.NODEDC_FOUNDRY_SESSION_VALIDATION_GRACE_MS,
    DEFAULT_SESSION_VALIDATION_GRACE_MS,
    0,
    MAX_SESSION_VALIDATION_GRACE_MS,
  );
  const sessionValidationRetryMs = options.validationRetryMs ?? boundedInteger(
    process.env.NODEDC_FOUNDRY_SESSION_VALIDATION_RETRY_MS,
    DEFAULT_SESSION_VALIDATION_RETRY_MS,
    250,
    5_000,
  );
  const cookieSecure = parseBoolean(process.env.NODEDC_FOUNDRY_COOKIE_SECURE, authRequired);
  const cookieSameSite = String(process.env.NODEDC_FOUNDRY_COOKIE_SAMESITE || "Lax").trim() || "Lax";
  const sessions = new Map();

  function emitAuthDiagnostic({ outcome, reason, launcherStatus, durationMs }) {
    // The allowlisted record intentionally excludes the local cookie id,
    // Launcher session id, internal bearer token and user identity.
    const record = Object.freeze({
      event: "foundry_session_validation",
      outcome,
      reason,
      launcherStatus: Number.isInteger(launcherStatus) ? launcherStatus : null,
      durationMs: Math.max(0, Math.round(Number(durationMs) || 0)),
      serviceSlug,
    });
    if (typeof options.onDiagnostic === "function") {
      try {
        options.onDiagnostic(record);
      } catch {
        // Diagnostics are best-effort and must never change an authorization
        // decision or turn a successful validation into a transient failure.
      }
      return;
    }
    const method = outcome === "active" ? "info" : "warn";
    try {
      console[method](JSON.stringify(record));
    } catch {
      // A broken log sink must not affect the request path.
    }
  }

  function buildCookie(value, maxAgeSeconds) {
    const parts = [
      `${sessionCookie}=${encodeURIComponent(value)}`,
      "Path=/",
      "HttpOnly",
      `SameSite=${cookieSameSite}`,
      `Max-Age=${Math.max(0, Math.floor(maxAgeSeconds))}`,
    ];
    if (cookieSecure) parts.push("Secure");
    return parts.join("; ");
  }

  function clearSession(request, response) {
    const sessionId = parseCookies(request.headers.cookie || "")[sessionCookie];
    if (sessionId) sessions.delete(sessionId);
    appendSetCookie(response, buildCookie("", 0));
  }

  function pruneExpiredSessions() {
    const currentTime = now();
    for (const [id, session] of sessions.entries()) {
      if (!session || session.expiresAt <= currentTime) sessions.delete(id);
    }
  }

  function createSession(response, handoff) {
    pruneExpiredSessions();
    const createdAtMs = now();
    const id = randomBytes(32).toString("base64url");
    const session = {
      id,
      user: handoff.user || null,
      launcherSessionId: typeof handoff.launcherSessionId === "string" ? handoff.launcherSessionId : null,
      createdAt: new Date(createdAtMs).toISOString(),
      expiresAt: createdAtMs + sessionTtlMs,
      lastValidatedAt: createdAtMs,
      validationInFlight: null,
      validationRetryAt: 0,
      lastValidationOutcome: "active",
    };
    sessions.set(id, session);
    appendSetCookie(response, buildCookie(id, sessionTtlMs / 1000));
    return session;
  }

  function currentSession(request) {
    const sessionId = parseCookies(request.headers.cookie || "")[sessionCookie];
    if (!sessionId) return null;
    const session = sessions.get(sessionId);
    if (!session || session.expiresAt <= now()) {
      sessions.delete(sessionId);
      return null;
    }
    return session;
  }

  function buildLauncherLaunchUrl(nextPath) {
    const launchUrl = new URL(`/api/services/${encodeURIComponent(serviceSlug)}/launch`, launcherBaseUrl);
    launchUrl.searchParams.set("returnTo", sanitizeReturnTo(nextPath));
    return launchUrl.toString();
  }

  function buildLauncherLoginUrl(nextPath) {
    const loginUrl = new URL("/auth/login", launcherBaseUrl);
    const launchUrl = new URL(buildLauncherLaunchUrl(nextPath));
    loginUrl.searchParams.set("returnTo", `${launchUrl.pathname}${launchUrl.search}`);
    return loginUrl.toString();
  }

  async function launcherRequest(pathname, payload) {
    if (!internalAccessToken) {
      throw new Error("NODE.DC internal access configuration is not available.");
    }

    const response = await fetchImpl(new URL(pathname, launcherInternalUrl), {
      method: "POST",
      headers: {
        Authorization: `Bearer ${internalAccessToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
      signal: AbortSignal.timeout(8_000),
    });
    const body = await response.json().catch(() => null);
    return { response, body };
  }

  async function consumeHandoff(token) {
    const { response, body } = await launcherRequest("/api/internal/handoff/consume", { token, serviceSlug });
    if (!response.ok || !body?.ok) {
      throw new Error(body?.error || `Launcher handoff failed: HTTP ${response.status}`);
    }
    return {
      user: body.user || null,
      launcherSessionId: typeof body.launcherSessionId === "string" ? body.launcherSessionId : null,
    };
  }

  async function validateSession(session) {
    if (!authRequired) return { outcome: "active", user: session?.user || null, reason: "auth_not_required" };
    if (!session?.launcherSessionId) {
      return { outcome: "inactive", reason: "launcher_session_missing", launcherStatus: null };
    }
    const { response, body } = await launcherRequest("/api/internal/session/validate", {
      serviceSlug,
      launcherSessionId: session.launcherSessionId,
    });
    if (response.ok && body?.ok === true && body.active === true) {
      return {
        outcome: "active",
        user: body.user || session.user || null,
        reason: "launcher_session_active",
        launcherStatus: response.status,
      };
    }
    // Launcher deliberately represents a revoked/expired user session as a
    // successful validation response with active=false. HTTP failures here
    // describe the internal validation service (including its own bearer-token
    // configuration), so they must not destroy an otherwise valid user cookie.
    if (response.ok && body?.ok === true && body.active === false) {
      return {
        outcome: "inactive",
        reason: "launcher_session_inactive",
        launcherStatus: response.status,
      };
    }
    return {
      outcome: "transient",
      reason: response.status >= 500
        ? "launcher_unavailable"
        : response.status === 401 || response.status === 403
          ? "launcher_internal_auth_rejected"
          : "launcher_validation_invalid_response",
      launcherStatus: response.status,
    };
  }

  function isReadOnlyRequest(request) {
    return request.method === "GET" || request.method === "HEAD";
  }

  function transientFailureReason(error) {
    return error?.name === "TimeoutError" || error?.name === "AbortError"
      ? "launcher_timeout"
      : "launcher_network_error";
  }

  function startSessionValidation(session) {
    if (session.validationInFlight) return session.validationInFlight;
    const startedAt = now();
    const validation = (async () => {
      try {
        const result = await validateSession(session);
        if (result.outcome === "active") {
          session.user = result.user;
          session.lastValidatedAt = now();
          session.validationRetryAt = 0;
        } else if (result.outcome === "transient") {
          session.validationRetryAt = now() + sessionValidationRetryMs;
        }
        session.lastValidationOutcome = result.outcome;
        emitAuthDiagnostic({
          outcome: result.outcome,
          reason: result.reason,
          launcherStatus: result.launcherStatus,
          durationMs: now() - startedAt,
        });
        return result;
      } catch (error) {
        const result = {
          outcome: "transient",
          reason: transientFailureReason(error),
          launcherStatus: null,
        };
        session.validationRetryAt = now() + sessionValidationRetryMs;
        session.lastValidationOutcome = result.outcome;
        emitAuthDiagnostic({
          ...result,
          durationMs: now() - startedAt,
        });
        return result;
      } finally {
        session.validationInFlight = null;
      }
    })();
    session.validationInFlight = validation;
    return validation;
  }

  function useSession(request, session) {
    request.nodedcFoundrySession = session;
    return session;
  }

  function canUseValidationGrace(request, session) {
    if (!isReadOnlyRequest(request)) return false;
    const validationAge = Math.max(0, now() - session.lastValidatedAt);
    return validationAge <= sessionValidationTtlMs + sessionValidationGraceMs;
  }

  async function getValidatedSession(request, response) {
    const session = currentSession(request);
    if (!session) {
      // Sessions intentionally remain process-local: after a deploy/restart the
      // opaque cookie cannot be reconstructed safely. Expire a stale cookie and
      // require a fresh Launcher handoff instead of treating it as recoverable.
      const staleSessionId = parseCookies(request.headers.cookie || "")[sessionCookie];
      if (staleSessionId) {
        clearSession(request, response);
        emitAuthDiagnostic({
          outcome: "inactive",
          reason: "local_session_missing_or_expired",
          launcherStatus: null,
          durationMs: 0,
        });
      }
      return null;
    }

    if (now() - session.lastValidatedAt <= sessionValidationTtlMs) {
      return useSession(request, session);
    }

    let validation;
    if (session.lastValidationOutcome === "transient" && now() < session.validationRetryAt) {
      validation = { outcome: "transient", reason: "launcher_retry_backoff", launcherStatus: null };
    } else {
      validation = await startSessionValidation(session);
    }

    // Logout or another explicit invalidation may have removed the process-local
    // session while a shared validation was in flight. Never resurrect it.
    if (sessions.get(session.id) !== session) return null;

    if (validation.outcome === "active") return useSession(request, session);
    if (validation.outcome === "inactive") {
      clearSession(request, response);
      return null;
    }
    if (canUseValidationGrace(request, session)) return useSession(request, session);

    request.nodedcFoundryAuthUnavailable = true;
    return null;
  }

  function sendAuthRequired(request, response, url) {
    const nextPath = sanitizeReturnTo(`${url.pathname}${url.search || ""}`);
    if (!internalAccessToken) {
      sendText(response, 503, "Module Foundry access is not configured on this server.");
      return;
    }
    const loginUrl = buildLauncherLoginUrl(nextPath);
    if (isHtmlNavigation(request, url)) {
      response.statusCode = 302;
      response.setHeader("Location", loginUrl);
      response.setHeader("Cache-Control", "no-store");
      response.end();
      return;
    }
    sendJson(response, 401, {
      ok: false,
      authenticated: false,
      error: "module_foundry_auth_required",
      loginUrl,
    });
  }

  function sendAuthUnavailable(request, response) {
    if (String(request.url || "").startsWith("/api/")) {
      sendJson(response, 503, {
        ok: false,
        authenticated: true,
        error: "module_foundry_auth_unavailable",
      });
      return;
    }
    sendText(response, 503, "Module Foundry session validation is temporarily unavailable.");
  }

  function sendAccessDenied(response, access) {
    sendJson(response, 403, {
      ok: false,
      authenticated: true,
      error: access?.role === "blocked" ? "module_foundry_access_blocked" : "module_foundry_access_denied",
    });
  }

  async function handleLauncherHandoff(request, response, url) {
    if (!authRequired) {
      response.statusCode = 302;
      response.setHeader("Location", sanitizeReturnTo(url.searchParams.get("next_path") || url.searchParams.get("returnTo") || "/"));
      response.end();
      return;
    }

    const token = String(url.searchParams.get("token") || "");
    const nextPath = sanitizeReturnTo(url.searchParams.get("next_path") || url.searchParams.get("returnTo") || "/");
    if (!token) {
      sendText(response, 400, "Missing Launcher handoff token.");
      return;
    }
    try {
      const handoff = await consumeHandoff(token);
      createSession(response, handoff);
      response.statusCode = 302;
      response.setHeader("Location", nextPath);
      response.setHeader("Cache-Control", "no-store");
      response.end();
    } catch (error) {
      sendText(response, 401, `Launcher handoff failed: ${error.message || "unknown error"}`);
    }
  }

  function handleLogout(request, response) {
    clearSession(request, response);
    response.statusCode = 302;
    response.setHeader("Location", "/");
    response.setHeader("Cache-Control", "no-store");
    response.end();
  }

  async function ensureRequestSession(request, response, url) {
    if (!authRequired) return false;
    if (url.pathname === "/auth/nodedc/handoff" || url.pathname === "/auth/logout") return false;
    if (url.pathname === "/healthz") return false;
    if (url.pathname === "/api/mcp" || url.pathname === "/api/ai-workspace/entitlements") return false;
    if (await getValidatedSession(request, response)) {
      const access = currentUserAccess(request);
      if (access.allowed) return false;
      sendAccessDenied(response, access);
      return true;
    }
    if (request.nodedcFoundryAuthUnavailable) {
      sendAuthUnavailable(request, response);
      return true;
    }
    sendAuthRequired(request, response, url);
    return true;
  }

  function currentUserAccess(request) {
    return resolveFoundryAccess(request.nodedcFoundrySession?.user, authRequired);
  }

  function currentUserProfile(request) {
    const user = request.nodedcFoundrySession?.user;
    if (!user || typeof user !== "object") return null;
    const id = String(user.id || user.subject || user.sub || "").trim();
    const email = String(user.email || "").trim();
    const displayName = String(user.name || user.displayName || email || "NODE.DC").trim().slice(0, 240);
    const avatarCandidate = String(user.avatarUrl || user.avatar_url || user.picture || "").trim();
    const avatarUrl = /^https:\/\//i.test(avatarCandidate) || avatarCandidate.startsWith("/") ? avatarCandidate : null;
    const initials = displayName.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]).join("").toUpperCase() || "DC";
    return { id, email, displayName, avatarUrl, initials };
  }

  return {
    authRequired,
    serviceSlug,
    handleLauncherHandoff,
    handleLogout,
    ensureRequestSession,
    currentUserAccess,
    currentUserProfile,
    profileUrl: new URL("/profile", launcherBaseUrl).toString(),
  };
}
