import "server-only";
import { createHash, randomBytes } from "node:crypto";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getDb, nowIso } from "@/lib/db";
import { verifyPassword } from "@/lib/auth/password";
import { AuthorizationError, can, type Permission } from "@/lib/auth/permissions";
import { scopeFor, type AccessScope } from "@/lib/auth/scope";
import type { Profile, Role } from "@/types";

export const SESSION_COOKIE = "occp_session";
const SESSION_TTL_HOURS = 12;

export interface SessionUser {
  id: string;
  email: string;
  full_name: string;
  role: Role;
  /** NULL grants access to every program; otherwise the user is confined to it. */
  program_id: string | null;
  program_code: string | null;
  program_name: string | null;
}

function hashToken(token: string): string {
  return createHash("sha256").update(token).digest("hex");
}

// -----------------------------------------------------------------------------
// Rate limiting (SPEC 29)
// -----------------------------------------------------------------------------

export interface RateLimitResult {
  allowed: boolean;
  retryAfterSeconds: number;
}

export async function checkRateLimit(
  bucket: string,
  limit: number,
  windowSeconds: number,
): Promise<RateLimitResult> {
  const db = await getDb();
  const cutoff = new Date(Date.now() - windowSeconds * 1000).toISOString();
  await db.run("DELETE FROM auth_attempts WHERE created_at < ?", [cutoff]);
  const row = await db.get<{ n: number }>(
    "SELECT COUNT(*) AS n FROM auth_attempts WHERE bucket = ? AND created_at >= ?",
    [bucket, cutoff],
  );
  const count = Number(row?.n ?? 0);
  if (count >= limit) {
    return { allowed: false, retryAfterSeconds: windowSeconds };
  }
  await db.run("INSERT INTO auth_attempts (id, bucket, created_at) VALUES (?, ?, ?)", [
    crypto.randomUUID(),
    bucket,
    nowIso(),
  ]);
  return { allowed: true, retryAfterSeconds: 0 };
}

export async function clearRateLimit(bucket: string): Promise<void> {
  const db = await getDb();
  await db.run("DELETE FROM auth_attempts WHERE bucket = ?", [bucket]);
}

// -----------------------------------------------------------------------------
// Sign in / out
// -----------------------------------------------------------------------------

export interface SignInResult {
  ok: boolean;
  error?: string;
  user?: SessionUser;
}

export async function signIn(email: string, password: string, userAgent?: string): Promise<SignInResult> {
  const emailKey = email.trim().toLowerCase();
  if (!emailKey || !password) {
    return { ok: false, error: "Enter your email address and password." };
  }

  const limit = await checkRateLimit(`login:${emailKey}`, 8, 15 * 60);
  if (!limit.allowed) {
    return { ok: false, error: "Too many sign-in attempts. Please wait 15 minutes and try again." };
  }

  const db = await getDb();
  const profile = await db.get<Profile & { password_hash: string; password_salt: string }>(
    "SELECT * FROM profiles WHERE email_key = ?",
    [emailKey],
  );

  // Deliberately identical message for unknown account, wrong password and
  // deactivated account, so the form cannot be used to enumerate users.
  const genericFailure = { ok: false as const, error: "Invalid email address or password." };
  if (!profile) return genericFailure;

  const valid = await verifyPassword(password, profile.password_hash, profile.password_salt);
  if (!valid) return genericFailure;
  if (!profile.active) {
    return { ok: false, error: "This account has been deactivated. Contact a system administrator." };
  }

  await clearRateLimit(`login:${emailKey}`);

  const token = randomBytes(32).toString("hex");
  const expires = new Date(Date.now() + SESSION_TTL_HOURS * 3600 * 1000);
  await db.run(
    "INSERT INTO sessions (id, user_id, created_at, expires_at, user_agent) VALUES (?, ?, ?, ?, ?)",
    [hashToken(token), profile.id, nowIso(), expires.toISOString(), userAgent ?? null],
  );

  const jar = await cookies();
  jar.set(SESSION_COOKIE, token, {
    httpOnly: true,
    sameSite: "lax",
    secure: process.env.NODE_ENV === "production",
    path: "/",
    expires,
  });

  const program = profile.program_id
    ? await db.get<{ code: string; name: string }>("SELECT code, name FROM programs WHERE id = ?", [
        profile.program_id,
      ])
    : undefined;

  return {
    ok: true,
    user: {
      id: profile.id,
      email: profile.email,
      full_name: profile.full_name,
      role: profile.role,
      program_id: profile.program_id ?? null,
      program_code: program?.code ?? null,
      program_name: program?.name ?? null,
    },
  };
}

export async function signOut(): Promise<void> {
  const jar = await cookies();
  const token = jar.get(SESSION_COOKIE)?.value;
  if (token) {
    const db = await getDb();
    await db.run("DELETE FROM sessions WHERE id = ?", [hashToken(token)]);
  }
  jar.delete(SESSION_COOKIE);
}

/** Invalidates every session for a user — used when deactivating an account. */
export async function revokeSessionsForUser(userId: string): Promise<void> {
  const db = await getDb();
  await db.run("DELETE FROM sessions WHERE user_id = ?", [userId]);
}

// -----------------------------------------------------------------------------
// Reading the current user
// -----------------------------------------------------------------------------

export async function getCurrentUser(): Promise<SessionUser | null> {
  let token: string | undefined;
  try {
    const jar = await cookies();
    token = jar.get(SESSION_COOKIE)?.value;
  } catch {
    return null;
  }
  if (!token) return null;

  try {
    const db = await getDb();
    const row = await db.get<{
      id: string;
      email: string;
      full_name: string;
      role: Role;
      active: number;
      expires_at: string;
      program_id: string | null;
      program_code: string | null;
      program_name: string | null;
    }>(
      `SELECT p.id, p.email, p.full_name, p.role, p.active, p.program_id,
              pr.code AS program_code, pr.name AS program_name, s.expires_at
         FROM sessions s
         JOIN profiles p ON p.id = s.user_id
         LEFT JOIN programs pr ON pr.id = p.program_id
        WHERE s.id = ?`,
      [hashToken(token)],
    );
    if (!row) return null;
    if (!row.active) return null;
    if (new Date(row.expires_at).getTime() < Date.now()) {
      await db.run("DELETE FROM sessions WHERE id = ?", [hashToken(token)]);
      return null;
    }
    return {
      id: row.id,
      email: row.email,
      full_name: row.full_name,
      role: row.role,
      program_id: row.program_id ?? null,
      program_code: row.program_code ?? null,
      program_name: row.program_name ?? null,
    };
  } catch {
    // Database not migrated yet: treat as signed out rather than crashing.
    return null;
  }
}

/** The program scope for the signed-in user. */
export async function requireScope(): Promise<{ user: SessionUser; scope: AccessScope }> {
  const user = await requireUser();
  return { user, scope: scopeFor(user) };
}

export class UnauthenticatedError extends Error {
  constructor() {
    super("You must be signed in to perform this action.");
    this.name = "UnauthenticatedError";
  }
}

/** Throws unless a user is signed in. */
export async function requireUser(): Promise<SessionUser> {
  const user = await getCurrentUser();
  if (!user) throw new UnauthenticatedError();
  return user;
}

/**
 * The single authorization gate used by every mutation. Server-side only —
 * never rely on the client having hidden a button (SPEC 44).
 */
export async function requirePermission(permission: Permission): Promise<SessionUser> {
  const user = await requireUser();
  if (!can(user.role, permission)) throw new AuthorizationError(permission);
  return user;
}

// -----------------------------------------------------------------------------
// Page-level guards
//
// Server actions throw (so the UI can show an inline error), but a page render
// must never surface a raw error to the user — it redirects instead (SPEC 36).
// -----------------------------------------------------------------------------

export async function requirePageUser(): Promise<SessionUser> {
  const user = await getCurrentUser();
  if (!user) redirect("/login");
  return user;
}

export async function requirePagePermission(permission: Permission): Promise<SessionUser> {
  const user = await getCurrentUser();
  if (!user) redirect("/login");
  if (!can(user.role, permission)) redirect(`/forbidden?permission=${encodeURIComponent(permission)}`);
  return user;
}
