import { randomBytes } from "node:crypto"

import { eq, isNull } from "drizzle-orm"

import { auth } from "@/lib/auth"
import { db, sql } from "@/lib/db/client"
import { people, positions } from "@/lib/db/schema"
import { tierOf } from "@/lib/db/schema"
import { checkUsername, normaliseUsername } from "@/lib/username"

/**
 * Give somebody a way in.
 *
 * A person exists in the roster from the day HR types their name. A login is a
 * separate act, and this is it: it records the username they will type, creates
 * the Better Auth account under it, sets a password nobody has seen before, and
 * links the records.
 *
 *     bun run db:provision 00001201 n.kulisra
 *
 * The person is named by employee code, which is what HR has in front of them
 * and what the accounting database already keys on. The username is what that
 * person will type; the code is never typed to sign in. Both live here and this
 * row is what links them.
 *
 * A person who already has a username keeps it, and the argument is only
 * required for somebody who has none. Renaming an account is a different act
 * from opening one and does not belong in the same command.
 *
 * The password is printed once, here, and never stored anywhere legible. Write
 * it down and hand it over in person. There is no email in this building and no
 * reset link to send.
 */

/**
 * Six groups of four, from real entropy. Long enough to be worth having, short
 * enough to read off paper and type on a factory floor without a mistake.
 * Digits and letters that cannot be confused with each other: no O, 0, I, l, 1.
 */
function password(): string {
  const alphabet = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
  const bytes = randomBytes(24)
  const chars = Array.from(bytes, (b) => alphabet[b % alphabet.length])
  return [0, 4, 8, 12, 16, 20]
    .map((i) => chars.slice(i, i + 4).join(""))
    .join("-")
}

const what = process.argv[2]
const wantedUsername = process.argv[3]

if (!what || !/^\d{8}$/.test(what)) {
  console.error("Usage: bun run db:provision <employee code> [username]")
  process.exit(1)
}

if (wantedUsername && checkUsername(wantedUsername)) {
  console.error(
    `"${wantedUsername}" is not a username. Letters and numbers, and it may hold a dot, a hyphen or an underscore.`,
  )
  process.exit(1)
}

const rows = await db
  .select({
    id: people.id,
    code: people.employeeCode,
    firstName: people.firstName,
    lastName: people.lastName,
    displayName: people.displayName,
    username: people.username,
    level: positions.level,
    title: positions.title,
  })
  .from(people)
  .innerJoin(positions, eq(positions.id, people.positionId))
  .where(isNull(people.authUserId))

const wanted = rows.filter((r) => r.code === what)

if (wanted.length === 0) {
  console.log(`${what} is not in the roster, or already has a login.`)
  await sql.end()
  process.exit(0)
}

for (const person of wanted) {
  const username = normaliseUsername(wantedUsername ?? person.username ?? "")
  if (!username) {
    console.error(
      `${person.code} has no username yet. Give one: bun run db:provision ${person.code} <username>`,
    )
    await sql.end()
    process.exit(1)
  }

  const secret = password()
  const name =
    person.displayName ?? `${person.firstName} ${person.lastName}`.trim()

  const result = await auth.api.signUpEmail({
    body: {
      // Derived and unreachable. See the note in lib/auth.ts.
      email: `${username}@advmfr.local`,
      password: secret,
      name,
      username,
    },
  })

  // The username is written to the person as well as the account, because a
  // person exists before an account does and the sign-in screen has to be able
  // to tell "no such name" from "no account opened yet".
  await db
    .update(people)
    .set({ authUserId: result.user.id, username })
    .where(eq(people.id, person.id))

  console.log(`  ${person.code}  ${username}  ${name}`)
  console.log(`  ${person.title} · ${tierOf(person.level)}`)
  console.log(`  password: ${secret}\n`)
}

console.log("Written down? These are not recoverable.")
await sql.end()
