/**
 * Creates or updates a user account from the command line.
 *
 * Accounts are normally managed in the Users screen; this exists so an
 * installation can be provisioned before anyone has signed in, and so a batch
 * of accounts can be created reproducibly.
 *
 *   npm run account -- --email dean.med@example.edu --name "Dr Jane Cruz" \
 *                      --role DEAN_ADMIN --program MED
 *
 * Omit --password and a strong one is generated and printed ONCE. Omit
 * --program to grant global (all-programs) access — reserve that for Super
 * Administrators.
 */
import { randomInt } from "node:crypto";
import { getDb, newId, nowIso } from "@/lib/db";
import { hashPassword, passwordProblems } from "@/lib/auth/password";
import { recordAudit } from "@/lib/audit";
import { ROLES, type Role } from "@/types";

function arg(name: string): string | undefined {
  const index = process.argv.indexOf(`--${name}`);
  return index === -1 ? undefined : process.argv[index + 1];
}
const flag = (name: string) => process.argv.includes(`--${name}`);

/** Readable but strong: no ambiguous glyphs, guaranteed to satisfy the policy. */
function generatePassword(): string {
  const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
  const lower = "abcdefghijkmnopqrstuvwxyz";
  const digits = "23456789";
  const all = upper + lower + digits;
  for (let attempt = 0; attempt < 50; attempt += 1) {
    const chars = [
      upper[randomInt(upper.length)],
      lower[randomInt(lower.length)],
      digits[randomInt(digits.length)],
      ...Array.from({ length: 13 }, () => all[randomInt(all.length)]),
    ];
    // Fisher-Yates so the guaranteed characters are not always first.
    for (let i = chars.length - 1; i > 0; i -= 1) {
      const j = randomInt(i + 1);
      [chars[i], chars[j]] = [chars[j], chars[i]];
    }
    const candidate = chars.join("");
    if (passwordProblems(candidate).length === 0) return candidate;
  }
  throw new Error("Could not generate a compliant password.");
}

const email = arg("email")?.trim().toLowerCase();
const fullName = arg("name")?.trim();
const role = (arg("role")?.trim().toUpperCase() ?? "DEAN_ADMIN") as Role;
const programCode = arg("program")?.trim().toUpperCase();
const suppliedPassword = arg("password");
const dryRun = flag("dry-run");

if (!email || !fullName) {
  console.error('Usage: npm run account -- --email <email> --name "<full name>" [--role DEAN_ADMIN] [--program MED]');
  process.exit(1);
}
if (!ROLES.includes(role)) {
  console.error(`--role must be one of: ${ROLES.join(", ")}`);
  process.exit(1);
}
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
  console.error(`"${email}" is not a valid email address.`);
  process.exit(1);
}

const db = await getDb();

let programId: string | null = null;
if (programCode) {
  const program = await db.get<{ id: string; name: string }>(
    "SELECT id, name FROM programs WHERE code = ? AND active = 1",
    [programCode],
  );
  if (!program) {
    const available = await db.all<{ code: string }>("SELECT code FROM programs WHERE active = 1 ORDER BY code");
    console.error(
      `No active program with code "${programCode}". Available: ${available.map((p) => p.code).join(", ") || "(none)"}`,
    );
    process.exit(1);
  }
  programId = program.id;
}

const existing = await db.get<{ id: string; full_name: string; role: Role; program_id: string | null }>(
  "SELECT id, full_name, role, program_id FROM profiles WHERE email_key = ?",
  [email],
);

const password = suppliedPassword ?? generatePassword();
const problems = passwordProblems(password);
if (problems.length > 0) {
  console.error(`Password rejected: ${problems.join(" ")}`);
  process.exit(1);
}

if (dryRun) {
  console.log(
    `[dry run] would ${existing ? "update" : "create"} ${email} — ${fullName} — ${role} — ${programCode ?? "ALL PROGRAMS"}`,
  );
  await db.close();
  process.exit(0);
}

const { hash, salt } = await hashPassword(password);
const ts = nowIso();

if (existing) {
  await db.run(
    `UPDATE profiles SET full_name = ?, role = ?, program_id = ?, password_hash = ?, password_salt = ?,
            active = 1, updated_at = ?
      WHERE id = ?`,
    [fullName, role, programId, hash, salt, ts, existing.id],
  );
  // A changed password or program must not leave a live session behind.
  await db.run("DELETE FROM sessions WHERE user_id = ?", [existing.id]);
  await recordAudit({
    actor: null,
    action: "USER_UPDATED",
    entityType: "profile",
    entityId: existing.id,
    before: { full_name: existing.full_name, role: existing.role, program_id: existing.program_id },
    after: { full_name: fullName, role, program_id: programId, password_changed: true },
    reason: "Provisioned from the command line during setup.",
  });
} else {
  const id = newId();
  await db.run(
    `INSERT INTO profiles (id, email, email_key, full_name, role, password_hash, password_salt, active,
       program_id, created_at, updated_at)
     VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)`,
    [id, arg("email")!.trim(), email, fullName, role, hash, salt, programId, ts, ts],
  );
  await recordAudit({
    actor: null,
    action: "USER_CREATED",
    entityType: "profile",
    entityId: id,
    after: { email, full_name: fullName, role, program_id: programId },
    reason: "Provisioned from the command line during setup.",
  });
}

console.log(`${existing ? "Updated" : "Created"}  ${email}`);
console.log(`  Name     ${fullName}`);
console.log(`  Role     ${role}`);
console.log(`  Program  ${programCode ?? "ALL PROGRAMS (global access)"}`);
if (!suppliedPassword) {
  console.log(`  Password ${password}`);
  console.log("\n  This password is shown once. Give it to the account holder over a secure channel");
  console.log("  and have them change it at first sign-in (Users screen).");
}

await db.close();
