import crypto from "node:crypto";
import { verifySession } from "./credentials.mjs";

const authMode = process.env.AUTH_MODE ?? "local";
const allowDevHeaders = process.env.ALLOW_DEV_HEADERS === "true" || process.env.ALLOW_DEV_HEADERS === "1";
const localDevRoles = (process.env.LOCAL_DEV_ROLES ?? "ADMIN,CLERK,AUDITOR,SUPPORT_ACCESS,REPORT_VIEWER,REPORT_REQUESTER,REPORT_APPROVER")
  .split(",")
  .map((role) => role.trim())
  .filter(Boolean);

let jwksCache = null;

function unauthorized(message) {
  const error = new Error(message);
  error.statusCode = 401;
  return error;
}

function decodeBase64Url(value) {
  return Buffer.from(value.replace(/-/g, "+").replace(/_/g, "/"), "base64");
}

function parseJwt(token) {
  const [headerPart, payloadPart, signaturePart] = token.split(".");
  if (!headerPart || !payloadPart || !signaturePart) throw unauthorized("bearer token must be a signed JWT");
  return {
    header: JSON.parse(decodeBase64Url(headerPart).toString("utf8")),
    payload: JSON.parse(decodeBase64Url(payloadPart).toString("utf8")),
    signingInput: headerPart + "." + payloadPart,
    signature: decodeBase64Url(signaturePart),
  };
}

async function loadJwks() {
  if (jwksCache) return jwksCache;
  const jwksUrl = process.env.OIDC_JWKS_URL;
  if (!jwksUrl) throw unauthorized("OIDC_JWKS_URL is required when AUTH_MODE=oidc");
  const response = await fetch(jwksUrl);
  if (!response.ok) throw unauthorized("failed to load JWKS: " + response.status);
  jwksCache = await response.json();
  return jwksCache;
}

async function verifyOidcActor(req) {
  const token = String(req.headers.authorization ?? "").match(/^Bearer\s+(.+)$/i)?.[1];
  if (!token) throw unauthorized("Authorization: Bearer token is required");
  const jwt = parseJwt(token);
  if (jwt.header.alg !== "RS256") throw unauthorized("only RS256 tokens are accepted");
  const jwks = await loadJwks();
  const jwk = jwks.keys?.find((key) => key.kid === jwt.header.kid);
  if (!jwk) throw unauthorized("token signing key was not found in JWKS");
  const key = crypto.createPublicKey({ key: jwk, format: "jwk" });
  const verified = crypto.verify("RSA-SHA256", Buffer.from(jwt.signingInput), key, jwt.signature);
  if (!verified) throw unauthorized("token signature is invalid");

  const now = Math.floor(Date.now() / 1000);
  if (jwt.payload.exp && jwt.payload.exp < now) throw unauthorized("token is expired");
  if (process.env.OIDC_ISSUER && jwt.payload.iss !== process.env.OIDC_ISSUER) throw unauthorized("token issuer is invalid");
  const expectedAudience = process.env.OIDC_AUDIENCE;
  const audience = Array.isArray(jwt.payload.aud) ? jwt.payload.aud : [jwt.payload.aud].filter(Boolean);
  if (expectedAudience && !audience.includes(expectedAudience)) throw unauthorized("token audience is invalid");

  const roles = [
    ...(jwt.payload.realm_access?.roles ?? []),
    ...Object.values(jwt.payload.resource_access ?? {}).flatMap((resource) => resource.roles ?? []),
  ].filter(Boolean);
  return {
    id: String(jwt.payload.sub),
    name: String(jwt.payload.name ?? jwt.payload.preferred_username ?? jwt.payload.sub),
    department: String(jwt.payload.department ?? jwt.payload.organization ?? "未設定"),
    roles: [...new Set(roles)],
    authMode: "oidc",
  };
}

function getLocalActor(req) {
  const roles = String(allowDevHeaders ? req.headers["x-user-roles"] ?? localDevRoles.join(",") : localDevRoles.join(","))
    .split(",")
    .map((role) => role.trim())
    .filter(Boolean);
  return {
    id: String(allowDevHeaders ? req.headers["x-user-id"] ?? "local-demo-user" : process.env.LOCAL_DEV_USER_ID ?? "local-demo-user"),
    name: String(allowDevHeaders ? req.headers["x-user-name"] ?? "ローカル検証ユーザー" : process.env.LOCAL_DEV_USER_NAME ?? "ローカル検証ユーザー"),
    department: String(allowDevHeaders ? req.headers["x-user-department"] ?? "住民課" : process.env.LOCAL_DEV_DEPARTMENT ?? "住民課"),
    roles,
    authMode: "local",
  };
}

function verifyPasswordActor(req) {
  const token = String(req.headers.authorization ?? "").match(/^Bearer\s+(.+)$/i)?.[1];
  const actor = token ? verifySession(token) : null;
  if (!actor) throw unauthorized("有効なセッションが必要です。/api/auth/login でログインしてください");
  return actor;
}

export async function getActor(req) {
  if (req.actor) return req.actor;
  if (authMode === "oidc") req.actor = await verifyOidcActor(req);
  else if (authMode === "password") req.actor = verifyPasswordActor(req);
  else req.actor = getLocalActor(req);
  return req.actor;
}

export function hasRole(actor, role) {
  if (role === "SUPPORT_ACCESS") return actor.roles.includes("SUPPORT_ACCESS");
  return actor.roles.includes("ADMIN") || actor.roles.includes(role);
}

export function requireRole(actor, role) {
  if (hasRole(actor, role)) return;
  const error = new Error(`role ${role} is required`);
  error.statusCode = 403;
  throw error;
}
