import { eq } from "drizzle-orm"

import {
  clearAttempts,
  recordFailure,
  retryAfter,
  tooManyAttempts,
} from "@/lib/attempt-limit"
import { auth } from "@/lib/auth"
import { db } from "@/lib/db/client"
import { people, positions, tierOf } from "@/lib/db/schema"
import { normaliseUsername } from "@/lib/username"

/**
 * Signing somebody in, and saying which thing went wrong.
 *
 * Better Auth answers `Invalid username or password` to every failure, on
 * purpose, so a stranger cannot learn who exists. That is the right default for
 * a service on the open internet and the wrong one here: this runs on a factory
 * network and 183 of the 193 people reading the message do so in their second
 * language. Somebody who mistypes their name should not be left staring at a
 * password field that was never the problem.
 *
 * So a failure is followed by one lookup, which turns the single generic answer
 * into three real ones. The enumeration this exposes is the roster of a factory
 * to people already on its network. ELEVA-133 records the trade; reverting it
 * means returning `wrong_credentials` for all three and deleting the lookup.
 *
 * Being that helpful is only affordable because guessing is capped here as
 * well. Ten failures per username per ten minutes, counted in
 * `lib/attempt-limit.ts`, which also explains why the limiter Better Auth ships
 * never sees this path.
 */

export type SignInOutcome =
  /**
   * The tier comes back with the answer so the form knows where to send them.
   * It is read from the rank on their position, never stored on the login: a
   * promotion changes one row and the next sign-in lands somewhere else.
   */
  | { ok: true; tier: "admin" | "manager" | "staff" }
  /** No such username in the roster. Almost always a typo. */
  | { ok: false; reason: "unknown_user" }
  /**
   * The person exists and has no login yet. This is the common case rather than
   * the exotic one: the roster holds 193 people and accounts are created one at
   * a time, so most of them cannot sign in and telling them their password is
   * wrong would send them looking for a password they were never given.
   *
   * It is reachable at all because the username lives on the person and not
   * only on the account. Somebody hired last week can type the name HR gave
   * them before HR has opened the account, and be told exactly that.
   */
  | { ok: false; reason: "no_login" }
  | { ok: false; reason: "wrong_password" }
  /**
   * They have left. The sign-in hook in lib/auth.ts refuses them before any
   * password is checked, so this is only ever reached to choose the sentence.
   */
  | { ok: false; reason: "archived" }
  /**
   * Too many wrong passwords for this username lately. `retryAfter` is in
   * seconds, so the screen can say how long rather than saying no twice.
   *
   * It is the one answer that says nothing about the roster: the same sentence
   * comes back whether or not the username exists, because the three reasons
   * above are a deliberate trade (ELEVA-133) and a limiter that answered
   * differently for a real name would be a faster oracle than the thing it was
   * added to protect.
   */
  | { ok: false; reason: "too_many_attempts"; retryAfter: number }

export async function signInByUsername(
  rawUsername: string,
  password: string,
): Promise<SignInOutcome> {
  // Folded here as well as in the lookup, so caps lock cannot make the two
  // halves of this function disagree about who is being asked for.
  const username = normaliseUsername(rawUsername)
  // One reading, so a slow database cannot let the check and the record of it
  // disagree about which window they are in.
  const now = Date.now()

  /*
    Before the password is looked at, never after. Verifying a password is a
    deliberately expensive hash, and that cost is exactly the resource somebody
    guessing is trying to spend. A limit applied after the attempt would refuse
    the answer and still pay for it.

    Better Auth's own limiter would have done this and cannot: it lives in the
    router's `onRequest` hook, and `auth.api.signInUsername` below never builds
    a request through that router. lib/attempt-limit.ts has the long version.
  */
  if (tooManyAttempts(username, now)) {
    return {
      ok: false,
      reason: "too_many_attempts",
      retryAfter: retryAfter(username, now),
    }
  }

  try {
    await auth.api.signInUsername({ body: { username, password } })
    // Getting in on the ninth try starts again from ten. Nobody who has proved
    // they know the password should be carrying somebody else's typing.
    clearAttempts(username)
    const [holder] = await db
      .select({ level: positions.level })
      .from(people)
      .innerJoin(positions, eq(positions.id, people.positionId))
      .where(eq(people.username, username))
    // Somebody with a login and no position is not impossible: a person can be
    // between jobs. They get the plainest of the three screens rather than none.
    return { ok: true, tier: holder ? tierOf(holder.level) : "staff" }
  } catch {
    // Deliberately not reading the error: Better Auth says the same thing for
    // every failure, so there is nothing in it to branch on. The roster is what
    // distinguishes the cases.
  }

  // Counted before the roster is asked anything, so a name nobody has ever held
  // costs a guesser the same budget as a real one.
  recordFailure(username, now)

  const [person] = await db
    .select({ authUserId: people.authUserId, archivedAt: people.archivedAt })
    .from(people)
    .where(eq(people.username, username))

  if (!person) return { ok: false, reason: "unknown_user" }
  if (person.archivedAt) return { ok: false, reason: "archived" }
  if (!person.authUserId) return { ok: false, reason: "no_login" }
  return { ok: false, reason: "wrong_password" }
}
