import { COOKIE_NAME, ONE_YEAR_MS } from "@shared/const";
import { ForbiddenError } from "@shared/_core/errors";
import { SignJWT, jwtVerify } from "jose";
import * as crypto from "node:crypto";
import { promisify } from "node:util";
import { parse as parseCookieHeader } from "cookie";
import type { Request, Response } from "express";
import type { User } from "../../drizzle/schema";
import * as db from "../db";
import { ENV } from "./env";
import { getSessionCookieOptions } from "./cookies";
import { canSendOperationalEmails, sendOperationalEmail } from "../mailer";

const scryptAsync = promisify(crypto.scrypt);
const argon2Async =
  typeof (crypto as any).argon2 === "function"
    ? promisify((crypto as any).argon2)
    : null;
const KEY_LENGTH = 64;
const LOGIN_FAILURE_LIMIT = 5;
const LOGIN_LOCK_DURATION_MS = 15 * 60 * 1000;
const MFA_CODE_TTL_MS = 10 * 60 * 1000;
const MFA_CODE_ATTEMPT_LIMIT = 5;
const RETENTION_REPORT_SETTING_KEY = "retention.lastReport";
const SYSTEM_ACTOR_USER_ID = 0;

const INTERNAL_MFA_POLICY = {
  super_admin: {
    required: true,
    preferredMethod: "authenticator_app",
    enforcedMethod: "authenticator_app",
    emailFallbackAllowed: false,
    phaseLabel: "Phase 2 cible",
  },
  admin: {
    required: true,
    preferredMethod: "authenticator_app",
    enforcedMethod: "authenticator_app",
    emailFallbackAllowed: false,
    phaseLabel: "Phase 2 active",
  },
  directrice: {
    required: true,
    preferredMethod: "authenticator_app",
    enforcedMethod: "authenticator_app",
    emailFallbackAllowed: false,
    phaseLabel: "Phase 2 active",
  },
  accueil: {
    required: true,
    preferredMethod: "authenticator_app",
    enforcedMethod: null,
    emailFallbackAllowed: true,
    phaseLabel: "Phase 1 active",
  },
  service_terrain: {
    required: true,
    preferredMethod: "authenticator_app",
    enforcedMethod: null,
    emailFallbackAllowed: true,
    phaseLabel: "Phase 1 active",
  },
  logistique_controle: {
    required: true,
    preferredMethod: "authenticator_app",
    enforcedMethod: null,
    emailFallbackAllowed: true,
    phaseLabel: "Phase 1 active",
  },
} as const;

class AccountLockedError extends Error {
  constructor(public lockedUntil: Date) {
    super(`Compte temporairement verrouillé jusqu'au ${lockedUntil.toLocaleTimeString("fr-FR", {
      hour: "2-digit",
      minute: "2-digit",
    })}.`);
  }
}

class MfaChallengeRequiredError extends Error {
  constructor(
    public challengeType: "email" | "totp",
    public challengeToken: string,
    public expiresAt: Date,
    public maskedEmail?: string,
  ) {
    super("Code de verification requis");
  }
}

type JwtPayload = {
  userId: number;
  openId: string;
};

type BootstrapUserConfig = {
  email: string;
  password: string;
  name?: string;
  role?: User["role"];
};

function createBootstrapUser(config: BootstrapUserConfig, index: number): User {
  const email = normalizeEmail(config.email);
  const now = new Date();

  return {
    id: index + 1,
    openId: email,
    name: config.name ?? email.split("@")[0] ?? "Utilisateur",
    email,
    passwordHash: null,
    loginMethod: "bootstrap_jwt",
    role: config.role ?? "admin",
    canManageLogistics: config.role === "super_admin",
    delegatedSalleSignerUserId: null,
    privacyConsentVersion: null,
    privacyConsentAcceptedAt: null,
    privacyConsentContext: null,
    failedLoginAttempts: 0,
    lockedUntil: null,
    mfaEnabled: false,
    mfaMethod: null,
    mfaChallengeToken: null,
    mfaCodeHash: null,
    mfaCodeExpiresAt: null,
    mfaCodeAttempts: 0,
    mfaTotpSecretEncrypted: null,
    mfaTotpPendingSecretEncrypted: null,
    deletionRequestedAt: null,
    purgeScheduledAt: null,
    purgedAt: null,
    legalHold: false,
    legalHoldReason: null,
    isActive: true,
    createdAt: now,
    updatedAt: now,
    lastSignedIn: now,
  };
}

function getBootstrapUserConfigs(): BootstrapUserConfig[] {
  const users: BootstrapUserConfig[] = [];

  if (ENV.bootstrapUsers) {
    try {
      const parsed = JSON.parse(ENV.bootstrapUsers) as BootstrapUserConfig[];
      if (Array.isArray(parsed)) {
        users.push(...parsed);
      }
    } catch (error) {
      console.error("[Auth] BOOTSTRAP_USERS is not valid JSON", error);
    }
  }

  if (ENV.bootstrapAdminEmail && ENV.bootstrapAdminPassword) {
    users.push({
      email: ENV.bootstrapAdminEmail,
      password: ENV.bootstrapAdminPassword,
      name: ENV.bootstrapAdminName,
      role: "super_admin",
    });
  }

  return users;
}

function getBootstrapUsers(): User[] {
  return getBootstrapUserConfigs().map((config, index) =>
    createBootstrapUser(config, index)
  );
}

function findBootstrapUserByCredentials(input: { email: string; password: string }) {
  const email = normalizeEmail(input.email);
  const configs = getBootstrapUserConfigs();
  const index = configs.findIndex(
    config => normalizeEmail(config.email) === email && config.password === input.password
  );

  if (index === -1) return null;
  return createBootstrapUser(configs[index], index);
}

function findBootstrapUserByOpenId(openId: string) {
  return getBootstrapUsers().find(user => user.openId === openId) ?? null;
}

function getJwtSecret() {
  const secret = ENV.cookieSecret;
  if (!secret && ENV.isProduction) {
    throw new Error("JWT_SECRET is required in production");
  }
  return new TextEncoder().encode(secret || "local-dev-secret-change-me");
}

function normalizeEmail(email: string) {
  return email.trim().toLowerCase();
}

async function ensureConfiguredOwnerPrivileges(user: User): Promise<User> {
  if (!user.email || !ENV.adminEmail) return user;

  const isConfiguredOwner = normalizeEmail(user.email) === normalizeEmail(ENV.adminEmail);
  if (!isConfiguredOwner) return user;

  if (user.role === "super_admin" && user.canManageLogistics) {
    return user;
  }

  await db.updateUser(user.id, {
    role: "super_admin",
    canManageLogistics: true,
  });

  return {
    ...user,
    role: "super_admin",
    canManageLogistics: true,
  };
}

function parseCookies(cookieHeader: string | undefined) {
  if (!cookieHeader) return new Map<string, string>();
  return new Map(Object.entries(parseCookieHeader(cookieHeader)));
}

function hashOtpCode(code: string) {
  return crypto.createHash("sha256").update(code).digest("hex");
}

function buildSecurityCipherKey() {
  return crypto.createHash("sha256")
    .update(ENV.cookieSecret || "local-dev-auth-security")
    .digest();
}

function encryptSecuritySecret(value: string) {
  const iv = crypto.randomBytes(12);
  const key = buildSecurityCipherKey();
  const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
  const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
  const tag = cipher.getAuthTag();
  return `${iv.toString("base64")}.${tag.toString("base64")}.${encrypted.toString("base64")}`;
}

function decryptSecuritySecret(value?: string | null) {
  if (!value) return "";
  const [ivBase64, tagBase64, encryptedBase64] = value.split(".");
  if (!ivBase64 || !tagBase64 || !encryptedBase64) return "";
  try {
    const key = buildSecurityCipherKey();
    const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(ivBase64, "base64"));
    decipher.setAuthTag(Buffer.from(tagBase64, "base64"));
    const decrypted = Buffer.concat([
      decipher.update(Buffer.from(encryptedBase64, "base64")),
      decipher.final(),
    ]);
    return decrypted.toString("utf8");
  } catch {
    return "";
  }
}

const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

function encodeBase32(buffer: Buffer) {
  let bits = 0;
  let value = 0;
  let output = "";
  for (let index = 0; index < buffer.length; index += 1) {
    const byte = buffer[index]!;
    value = (value << 8) | byte;
    bits += 8;
    while (bits >= 5) {
      output += BASE32_ALPHABET[(value >>> (bits - 5)) & 31];
      bits -= 5;
    }
  }
  if (bits > 0) {
    output += BASE32_ALPHABET[(value << (5 - bits)) & 31];
  }
  return output;
}

function decodeBase32(input: string) {
  const normalized = input.toUpperCase().replace(/=+$/g, "").replace(/[^A-Z2-7]/g, "");
  let bits = 0;
  let value = 0;
  const output: number[] = [];

  for (const char of normalized) {
    const index = BASE32_ALPHABET.indexOf(char);
    if (index === -1) continue;
    value = (value << 5) | index;
    bits += 5;
    if (bits >= 8) {
      output.push((value >>> (bits - 8)) & 255);
      bits -= 8;
    }
  }

  return Buffer.from(output);
}

function generateTotpSecret() {
  return encodeBase32(crypto.randomBytes(20));
}

function buildOtpAuthUrl(email: string, secret: string) {
  const label = encodeURIComponent(`Portail Associations:${email}`);
  const issuer = encodeURIComponent("Portail Associations");
  return `otpauth://totp/${label}?secret=${secret}&issuer=${issuer}&algorithm=SHA1&digits=6&period=30`;
}

function generateTotpCode(secret: string, timestamp: number) {
  const key = decodeBase32(secret);
  const counter = Math.floor(timestamp / 30_000);
  const counterBuffer = Buffer.alloc(8);
  counterBuffer.writeBigUInt64BE(BigInt(counter));
  const hmac = crypto.createHmac("sha1", key).update(counterBuffer).digest();
  const offset = hmac[hmac.length - 1] & 0x0f;
  const binary = ((hmac[offset] & 0x7f) << 24)
    | ((hmac[offset + 1] & 0xff) << 16)
    | ((hmac[offset + 2] & 0xff) << 8)
    | (hmac[offset + 3] & 0xff);
  return String(binary % 1_000_000).padStart(6, "0");
}

function verifyTotpCode(secret: string, code: string) {
  const trimmedCode = code.trim();
  const now = Date.now();
  for (const drift of [-30_000, 0, 30_000]) {
    if (generateTotpCode(secret, now + drift) === trimmedCode) {
      return true;
    }
  }
  return false;
}

function maskEmail(email: string) {
  const [localPart, domain = ""] = email.split("@");
  if (!localPart) return email;
  const visibleStart = localPart.slice(0, 2);
  const visibleEnd = localPart.length > 4 ? localPart.slice(-1) : "";
  const maskedCore = "*".repeat(Math.max(localPart.length - visibleStart.length - visibleEnd.length, 1));
  return `${visibleStart}${maskedCore}${visibleEnd}@${domain}`;
}

function generateOtpCode() {
  return String(Math.floor(100000 + Math.random() * 900000));
}

function getPasswordHashAlgorithm(passwordHash: string | null | undefined) {
  if (!passwordHash) return null;
  const [algorithm] = passwordHash.split(":");
  return algorithm || null;
}

export function getInternalMfaPolicy(role: User["role"]) {
  return INTERNAL_MFA_POLICY[role as keyof typeof INTERNAL_MFA_POLICY] ?? null;
}

export function isInternalRole(role: User["role"]) {
  return Boolean(getInternalMfaPolicy(role));
}

function requiresMfaByRole(user: User) {
  return Boolean(getInternalMfaPolicy(user.role)?.required);
}

function requiresTotpByRole(user: User) {
  return getInternalMfaPolicy(user.role)?.enforcedMethod === "authenticator_app";
}

function allowsEmailMfaByRole(user: User) {
  return getInternalMfaPolicy(user.role)?.emailFallbackAllowed !== false;
}

function getEffectiveMfaMethod(user: User): "email" | "totp" | null {
  const hasTotp = Boolean(user.mfaMethod === "authenticator_app" && user.mfaTotpSecretEncrypted);
  if (hasTotp) return "totp";
  if ((user.mfaEnabled || requiresMfaByRole(user)) && user.email && allowsEmailMfaByRole(user)) return "email";
  return null;
}

async function hashPassword(password: string) {
  if (!argon2Async) {
    const salt = crypto.randomBytes(16).toString("hex");
    const derivedKey = (await scryptAsync(password, salt, KEY_LENGTH)) as Buffer;
    return `scrypt:${salt}:${derivedKey.toString("hex")}`;
  }

  const nonce = crypto.randomBytes(16);
  const derivedKey = (await argon2Async("argon2id", {
    message: Buffer.from(password),
    nonce,
    parallelism: 1,
    tagLength: 32,
    memory: 65536,
    passes: 3,
  })) as Buffer;
  return `argon2id:${nonce.toString("base64url")}:${derivedKey.toString("base64url")}`;
}

export async function hashLocalPassword(password: string) {
  return hashPassword(password);
}

async function verifyPassword(password: string, passwordHash: string | null | undefined) {
  if (!passwordHash) return false;

  const [algorithm, salt, key] = passwordHash.split(":");
  if (algorithm === "argon2id" && salt && key && argon2Async) {
    const nonce = Buffer.from(salt, "base64url");
    const expected = Buffer.from(key, "base64url");
    const derivedKey = (await argon2Async("argon2id", {
      message: Buffer.from(password),
      nonce,
      parallelism: 1,
      tagLength: expected.length,
      memory: 65536,
      passes: 3,
    })) as Buffer;
    return derivedKey.length === expected.length && crypto.timingSafeEqual(derivedKey, expected);
  }

  if (algorithm !== "scrypt" || !salt || !key) return false;

  const storedKey = Buffer.from(key, "hex");
  const derivedKey = (await scryptAsync(password, salt, storedKey.length)) as Buffer;

  if (storedKey.length !== derivedKey.length) return false;
  return crypto.timingSafeEqual(storedKey, derivedKey);
}

async function resetLoginSecurityState(userId: number) {
  await db.updateUser(userId, {
    failedLoginAttempts: 0,
    lockedUntil: null,
  });
}

async function registerLoginFailure(user: User) {
  const nextAttempts = (user.failedLoginAttempts || 0) + 1;
  const lockNow = nextAttempts >= LOGIN_FAILURE_LIMIT;
  await db.updateUser(user.id, {
    failedLoginAttempts: lockNow ? 0 : nextAttempts,
    lockedUntil: lockNow ? new Date(Date.now() + LOGIN_LOCK_DURATION_MS) : null,
  });
}

function assertUserNotLocked(user: User) {
  if (user.lockedUntil && new Date(user.lockedUntil) > new Date()) {
    throw new AccountLockedError(new Date(user.lockedUntil));
  }
}

async function sendLoginMfaChallenge(user: User) {
  if (requiresTotpByRole(user) && !user.mfaTotpSecretEncrypted) {
    throw new Error("Ce rôle interne doit utiliser l'application Authenticator. Faites initialiser le compte avant la prochaine connexion.");
  }

  const effectiveMethod = getEffectiveMfaMethod(user);

  if (effectiveMethod === "totp") {
    const challengeToken = crypto.randomBytes(24).toString("base64url");
    const expiresAt = new Date(Date.now() + MFA_CODE_TTL_MS);
    await db.updateUser(user.id, {
      mfaChallengeToken: challengeToken,
      mfaCodeHash: null,
      mfaCodeExpiresAt: expiresAt,
      mfaCodeAttempts: 0,
    });
    return new MfaChallengeRequiredError("totp", challengeToken, expiresAt);
  }

  if (!user.email) {
    throw new Error("Ce compte ne dispose pas d'une adresse email exploitable pour le MFA.");
  }

  const mailReady = await canSendOperationalEmails();
  if (!mailReady) {
    throw new Error("Le MFA par email ne peut pas etre active tant que le SMTP n'est pas configure.");
  }

  const code = generateOtpCode();
  const expiresAt = new Date(Date.now() + MFA_CODE_TTL_MS);
  const challengeToken = crypto.randomBytes(24).toString("base64url");

  await db.updateUser(user.id, {
    mfaChallengeToken: challengeToken,
    mfaCodeHash: hashOtpCode(code),
    mfaCodeExpiresAt: expiresAt,
    mfaCodeAttempts: 0,
  });

  await sendOperationalEmail({
    to: [user.email],
    subject: "Code de verification - Portail Associations",
    text: `Votre code de verification est ${code}. Il expire dans 10 minutes.`,
    html: `
      <div style="font-family: Arial, sans-serif; line-height: 1.5; color: #161616;">
        <h2>Code de verification</h2>
        <p>Utilisez ce code pour finaliser votre connexion au Portail Associations :</p>
        <p style="font-size: 28px; font-weight: 700; letter-spacing: 6px;">${code}</p>
        <p>Ce code expire dans 10 minutes.</p>
      </div>
    `,
  });

  return new MfaChallengeRequiredError("email", challengeToken, expiresAt, maskEmail(user.email));
}

async function signSession(user: User) {
  const expiresAt = Math.floor((Date.now() + ONE_YEAR_MS) / 1000);
  return new SignJWT({
    userId: user.id,
    openId: user.openId,
  } satisfies JwtPayload)
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setSubject(String(user.id))
    .setExpirationTime(expiresAt)
    .sign(getJwtSecret());
}

async function verifySession(token: string | undefined | null) {
  if (!token) return null;

  try {
    const { payload } = await jwtVerify(token, getJwtSecret(), {
      algorithms: ["HS256"],
    });
    const userId = Number(payload.userId ?? payload.sub);
    const openId = typeof payload.openId === "string" ? payload.openId : "";

    if (!Number.isInteger(userId) || userId <= 0 || !openId) return null;
    return { userId, openId };
  } catch {
    return null;
  }
}

export function setSessionCookie(req: Request, res: Response, token: string) {
  res.cookie(COOKIE_NAME, token, {
    ...getSessionCookieOptions(req),
    maxAge: ONE_YEAR_MS,
  });
}

export function clearSessionCookie(req: Request, res: Response) {
  res.clearCookie(COOKIE_NAME, {
    ...getSessionCookieOptions(req),
    maxAge: -1,
  });
}

export async function registerLocalUser(input: {
  name: string;
  email: string;
  password: string;
}) {
  const email = normalizeEmail(input.email);

  const existing = await db.getUserByEmail(email);

  if (existing) {
    throw new Error("Un compte existe deja avec cette adresse email");
  }

  const bootstrapUser = findBootstrapUserByCredentials(input);
  if (bootstrapUser) {
    return bootstrapUser;
  }

  const role =
    ENV.adminEmail && normalizeEmail(ENV.adminEmail) === email ? "super_admin" : "user";

  await db.upsertUser({
    openId: email,
    name: input.name.trim(),
    email,
    passwordHash: await hashPassword(input.password),
    loginMethod: "local_jwt",
    role,
    lastSignedIn: new Date(),
  });

  const user = await db.getUserByEmail(email);
  if (!user) throw new Error("Impossible de creer le compte utilisateur");

  return user;
}

export async function loginLocalUser(input: { email: string; password: string }) {
  const email = normalizeEmail(input.email);

  const user = await db.getUserByEmail(email);

  if (user) {
    assertUserNotLocked(user);

    if (!(await verifyPassword(input.password, user.passwordHash))) {
      await registerLoginFailure(user);
      throw ForbiddenError("Identifiants invalides");
    }

    await resetLoginSecurityState(user.id);

    if (argon2Async && getPasswordHashAlgorithm(user.passwordHash) === "scrypt") {
      await db.updateUser(user.id, {
        passwordHash: await hashPassword(input.password),
      });
    }

    const refreshedUser = (await db.getUserById(user.id)) ?? user;
    if (getEffectiveMfaMethod(refreshedUser)) {
      throw await sendLoginMfaChallenge(refreshedUser);
    }

    await db.upsertUser({
      openId: user.openId,
      lastSignedIn: new Date(),
    });

    return ensureConfiguredOwnerPrivileges((await db.getUserById(user.id)) ?? refreshedUser);
  }

  const bootstrapUser = findBootstrapUserByCredentials(input);
  if (bootstrapUser) {
    return bootstrapUser;
  }

  throw ForbiddenError("Identifiants invalides");
}

export async function verifyLocalUserMfa(input: { challengeToken: string; code: string }) {
  const user = await db.getUserByMfaChallengeToken(input.challengeToken);
  if (!user) {
    throw ForbiddenError("Verification invalide ou expirée");
  }

  const effectiveMethod = getEffectiveMfaMethod(user);
  if (!effectiveMethod) {
    throw ForbiddenError("Verification invalide ou expirée");
  }

  if (user.mfaCodeExpiresAt && new Date(user.mfaCodeExpiresAt) < new Date()) {
    await db.updateUser(user.id, {
      mfaChallengeToken: null,
      mfaCodeHash: null,
      mfaCodeExpiresAt: null,
      mfaCodeAttempts: 0,
    });
    throw ForbiddenError("Le code de verification a expiré");
  }

  if ((user.mfaCodeAttempts || 0) >= MFA_CODE_ATTEMPT_LIMIT) {
    await db.updateUser(user.id, {
      mfaChallengeToken: null,
      mfaCodeHash: null,
      mfaCodeExpiresAt: null,
      mfaCodeAttempts: 0,
      lockedUntil: new Date(Date.now() + LOGIN_LOCK_DURATION_MS),
    });
    throw new AccountLockedError(new Date(Date.now() + LOGIN_LOCK_DURATION_MS));
  }

  const isValidCode = effectiveMethod === "totp"
    ? verifyTotpCode(decryptSecuritySecret(user.mfaTotpSecretEncrypted), input.code)
    : Boolean(user.mfaCodeHash) && hashOtpCode(input.code.trim()) === user.mfaCodeHash;

  if (!isValidCode) {
    await db.updateUser(user.id, {
      mfaCodeAttempts: (user.mfaCodeAttempts || 0) + 1,
    });
    throw ForbiddenError("Code de verification incorrect");
  }

  await db.updateUser(user.id, {
    mfaChallengeToken: null,
    mfaCodeHash: null,
    mfaCodeExpiresAt: null,
    mfaCodeAttempts: 0,
    lastSignedIn: new Date(),
  });

  return ensureConfiguredOwnerPrivileges((await db.getUserById(user.id)) ?? user);
}

export async function resendLocalUserMfaChallenge(challengeToken: string) {
  const user = await db.getUserByMfaChallengeToken(challengeToken);
  if (!user || getEffectiveMfaMethod(user) !== "email") {
    throw ForbiddenError("Verification invalide ou expirée");
  }

  if (!user.email) {
    throw ForbiddenError("Adresse email indisponible pour ce compte");
  }

  const code = generateOtpCode();
  const expiresAt = new Date(Date.now() + MFA_CODE_TTL_MS);

  await db.updateUser(user.id, {
    mfaCodeHash: hashOtpCode(code),
    mfaCodeExpiresAt: expiresAt,
    mfaCodeAttempts: 0,
  });

  await sendOperationalEmail({
    to: [user.email],
    subject: "Nouveau code de verification - Portail Associations",
    text: `Votre nouveau code de verification est ${code}. Il expire dans 10 minutes.`,
    html: `
      <div style="font-family: Arial, sans-serif; line-height: 1.5; color: #161616;">
        <h2>Nouveau code de verification</h2>
        <p>Utilisez ce code pour finaliser votre connexion au Portail Associations :</p>
        <p style="font-size: 28px; font-weight: 700; letter-spacing: 6px;">${code}</p>
        <p>Ce code expire dans 10 minutes.</p>
      </div>
    `,
  });

  return {
    challengeToken,
    expiresAt,
    maskedEmail: maskEmail(user.email),
  };
}

export async function startAuthenticatorSetup(user: User) {
  if (user.loginMethod !== "local_jwt" || !user.email) {
    throw new Error("Authenticator est disponible uniquement pour les comptes locaux avec email.");
  }

  const secret = generateTotpSecret();
  await db.updateUser(user.id, {
    mfaTotpPendingSecretEncrypted: encryptSecuritySecret(secret),
  });

  return {
    manualEntryKey: secret,
    otpauthUrl: buildOtpAuthUrl(user.email, secret),
  };
}

export async function confirmAuthenticatorSetup(user: User, code: string) {
  const freshUser = await db.getUserById(user.id);
  if (!freshUser) {
    throw new Error("Compte introuvable");
  }
  const pendingSecret = decryptSecuritySecret(freshUser.mfaTotpPendingSecretEncrypted);
  if (!pendingSecret) {
    throw new Error("Aucune configuration Authenticator en attente.");
  }
  if (!verifyTotpCode(pendingSecret, code)) {
    throw new Error("Code Authenticator invalide.");
  }

  await db.updateUser(user.id, {
    mfaEnabled: true,
    mfaMethod: "authenticator_app",
    mfaTotpSecretEncrypted: encryptSecuritySecret(pendingSecret),
    mfaTotpPendingSecretEncrypted: null,
  });
}

export async function cancelAuthenticatorSetup(user: User) {
  await db.updateUser(user.id, {
    mfaTotpPendingSecretEncrypted: null,
  });
}

export async function scheduleAccountDeletion(user: User) {
  if (user.legalHold) {
    throw new Error("Ce compte est actuellement place en conservation legale et ne peut pas etre supprime.");
  }

  const now = new Date();
  const purgeAt = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);

  await db.updateUser(user.id, {
    deletionRequestedAt: now,
    purgeScheduledAt: purgeAt,
    isActive: false,
  });

  return { deletionRequestedAt: now, purgeScheduledAt: purgeAt };
}

export async function cancelScheduledAccountDeletion(user: User) {
  await db.updateUser(user.id, {
    deletionRequestedAt: null,
    purgeScheduledAt: null,
    isActive: true,
  });
}

export async function runUserPurgeScheduler() {
  const startedAt = new Date();
  const dueUsers = await db.getUsersScheduledForPurge();
  let purgedUsers = 0;

  for (const user of dueUsers) {
    const association = await db.getAssociationByUserId(user.id);
    if (association) {
      await db.updateAssociation(association.id, {
        sourceDirectoryEntryId: null,
        nomAssociation: `Association supprimée #${association.id}`,
        siret: null,
        rna: null,
        thematique: null,
        adresse: null,
        codePostal: null,
        ville: null,
        telephone: null,
        emailContact: null,
        siteWeb: null,
        facebookUrl: null,
        instagramUrl: null,
        objetAssociation: null,
        nomRepresentant: null,
        fonctionRepresentant: null,
        gouvernance: null,
        profileComplete: false,
        isActive: false,
      });
    }

    await db.updateUser(user.id, {
      openId: `purged:${user.id}:${Date.now()}`,
      name: `Compte supprimé #${user.id}`,
      email: null,
      passwordHash: null,
      isActive: false,
      mfaEnabled: false,
      mfaMethod: null,
      mfaChallengeToken: null,
      mfaCodeHash: null,
      mfaCodeExpiresAt: null,
      mfaCodeAttempts: 0,
      mfaTotpSecretEncrypted: null,
      mfaTotpPendingSecretEncrypted: null,
      purgedAt: new Date(),
      legalHold: false,
      legalHoldReason: null,
    });

    await db.createAuditLog({
      userId: SYSTEM_ACTOR_USER_ID,
      action: "data_purge",
      entityType: "user",
      entityId: user.id,
      details: JSON.stringify({
        event: "DATA_PURGE",
        executedAt: new Date().toISOString(),
        status: "SUCCESS",
        purgeScheduledAt: user.purgeScheduledAt,
      }),
      ipAddress: "system",
    });

    purgedUsers += 1;
  }

  const allUsers = await db.getAllUsers();
  const pendingDeletionCount = allUsers.filter((item) => item.deletionRequestedAt && !item.purgedAt).length;
  const purgedTotalCount = allUsers.filter((item) => item.purgedAt).length;

  await db.setPortalSetting(
    RETENTION_REPORT_SETTING_KEY,
    JSON.stringify({
      generatedAt: new Date().toISOString(),
      startedAt: startedAt.toISOString(),
      reportType: "daily-retention-job",
      purgedUsers,
      pendingDeletionCount,
      purgedTotalCount,
    }),
    "Dernier rapport automatique de retention et purge"
  );

  return {
    generatedAt: new Date().toISOString(),
    purgedUsers,
    pendingDeletionCount,
    purgedTotalCount,
  };
}

export function isAccountLockedError(error: unknown): error is AccountLockedError {
  return error instanceof AccountLockedError;
}

export function isMfaCodeRequiredError(error: unknown): error is MfaChallengeRequiredError {
  return error instanceof MfaChallengeRequiredError;
}

export async function loginOAuthUser(input: {
  provider: "google" | "facebook";
  providerUserId: string;
  email: string;
  name?: string | null;
}) {
  const email = normalizeEmail(input.email);
  const existingByEmail = await db.getUserByEmail(email);
  const openId = existingByEmail?.openId || email || `${input.provider}:${input.providerUserId}`;
  const role = existingByEmail?.role
    ?? (ENV.adminEmail && normalizeEmail(ENV.adminEmail) === email ? "super_admin" : "user");

  await db.upsertUser({
    openId,
    name: input.name?.trim() || existingByEmail?.name || email.split("@")[0] || "Utilisateur",
    email,
    loginMethod: `${input.provider}_oauth`,
    role,
    lastSignedIn: new Date(),
  });

  if (existingByEmail) {
    return ensureConfiguredOwnerPrivileges((await db.getUserById(existingByEmail.id)) ?? existingByEmail);
  }

  const user = await db.getUserByOpenId(openId);
  if (!user) {
    throw new Error("Impossible de finaliser la connexion sociale");
  }

  return ensureConfiguredOwnerPrivileges(user);
}

export async function createSessionToken(user: User) {
  return signSession(user);
}

export async function authenticateRequest(req: Request): Promise<User> {
  const cookies = parseCookies(req.headers.cookie);
  const session = await verifySession(cookies.get(COOKIE_NAME));

  if (!session) {
    throw ForbiddenError("Invalid session cookie");
  }

  const user = await db.getUserById(session.userId);
  const userByOpenId = user ? null : await db.getUserByOpenId(session.openId);
  const bootstrapUser = findBootstrapUserByOpenId(session.openId);

  if (!user && userByOpenId && userByOpenId.isActive) {
    return ensureConfiguredOwnerPrivileges(userByOpenId);
  }

  if (!user && bootstrapUser) {
    return bootstrapUser;
  }

  if (!user || user.openId !== session.openId || !user.isActive) {
    throw ForbiddenError("User not found");
  }

  return ensureConfiguredOwnerPrivileges(user);
}
