import { cache } from "react"
import { headers } from "next/headers"
import { redirect } from "next/navigation"

import { eq } from "drizzle-orm"

import { auth } from "@/lib/auth"
import { db } from "@/lib/db/client"
import { departments, people, positions, tierOf } from "@/lib/db/schema"
import type { Locale } from "@/lib/i18n"
import type { Level } from "@/lib/levels"

/**
 * Who is looking at the screen.
 *
 * The session says which login it is. Everything else, the name to show, the
 * position, the department, the tier, is read from the roster on every request
 * and never copied into the session. A tier cached in a cookie is a tier that
 * survives a demotion, and the whole point of deriving it from the rank on the
 * position is that changing one row changes what somebody can do.
 */

export type SignedIn = {
  personId: string
  employeeCode: string
  /** Their name as the roster spells it, which for most people is Thai. */
  name: string
  /**
   * The position they hold, null while they are between two.
   *
   * The id and not only the title, because a scope is measured against it: a
   * staff member may write a duty onto their own position and onto no other,
   * and `withinScope` in duties.ts compares this. Read on every request like
   * everything else here, so a transfer changes what somebody can reach on
   * their next click rather than on their next sign-in.
   */
  positionId: string | null
  positionTitle: string | null
  department: string | null
  level: Level | null
  tier: "admin" | "manager" | "staff"
  /**
   * Read on every request, like the tier and for the same reason: a language
   * cached in a cookie survives the moment somebody changes it, and these are
   * shared terminals where the next person's language is a different one.
   */
  language: Locale
}

/**
 * Memoised for the length of one request, and only that long.
 *
 * Both halves matter. A page asks who is looking, the shell asks again for the
 * notification count, and the root layout asks a third time for the language to
 * put on `<html>`. Three identical session lookups and three identical joins
 * against the roster, to answer a question whose answer cannot change while a
 * single response is being rendered.
 *
 * React's `cache` is scoped to the request rather than shared between them,
 * which is the only kind of caching this function may have. Everything it
 * returns is read fresh from the roster on purpose, and the comment above says
 * why: a tier or a language held longer than a request is a tier that survives
 * a demotion and a language that survives somebody changing it.
 */
export const currentUser = cache(
  async function currentUser(): Promise<SignedIn | null> {
    const session = await auth.api.getSession({ headers: await headers() })
    if (!session) return null

    const [row] = await db
      .select({
        personId: people.id,
        employeeCode: people.employeeCode,
        firstName: people.firstName,
        lastName: people.lastName,
        displayName: people.displayName,
        archivedAt: people.archivedAt,
        language: people.language,
        positionId: positions.id,
        positionTitle: positions.title,
        level: positions.level,
        department: departments.name,
      })
      .from(people)
      .leftJoin(positions, eq(positions.id, people.positionId))
      .leftJoin(departments, eq(departments.id, positions.departmentId))
      .where(eq(people.authUserId, session.user.id))

    // A login with nobody behind it. It should not happen, and treating it as
    // signed out is the only safe reading: there is no roster row to decide what
    // this person may see.
    if (!row) return null
    // Archived after the session was issued. The sign-in hook refuses these, and
    // this refuses the ones already holding a cookie.
    if (row.archivedAt) return null

    return {
      personId: row.personId,
      employeeCode: row.employeeCode,
      name: row.displayName ?? `${row.firstName} ${row.lastName}`.trim(),
      positionId: row.positionId,
      positionTitle: row.positionTitle,
      department: row.department,
      level: row.level,
      tier: row.level ? tierOf(row.level) : "staff",
      language: row.language,
    }
  },
)

/**
 * The same, for a screen that has nothing to show a stranger. Redirects rather
 * than throwing, because a person who is not signed in has not made a mistake.
 */
export async function requireUser(): Promise<SignedIn> {
  const user = await currentUser()
  if (!user) redirect("/login")
  return user
}

/**
 * There is no `requireTier`.
 *
 * It sent somebody in the wrong tier to `/preview/${tier}`, and those three
 * screens appeared nowhere in `routes.json`: the specification redirects `/` to
 * `/tasks` and gives every tier the same pages with different actions visible.
 * The three placeholders are gone and `requireAtLeast` is what a screen asks.
 */

/**
 * The three tiers nest, exactly as the sidebar already draws them: everybody
 * has My work, a manager also has My department, an admin also has
 * Administration. So most screens want "this tier or above" rather than one
 * tier exactly.
 *
 * This exists because hiding a link is not a guard. The sidebar shows
 * Departments & positions to admins alone, and until this was written the page
 * itself answered 200 to anybody signed in, which is the gap between what an
 * interface suggests and what it enforces.
 */
const RANK: Record<SignedIn["tier"], number> = {
  staff: 0,
  manager: 1,
  admin: 2,
}

export async function requireAtLeast(
  tier: SignedIn["tier"],
): Promise<SignedIn> {
  const user = await requireUser()
  // To their task list and not to `/preview/${user.tier}`: those three screens
  // were deleted with the i18n work because they appear nowhere in routes.json,
  // and redirecting to them sent a refused manager to a 404. Everybody has a
  // task list, which is what the specification makes "/" mean.
  if (RANK[user.tier] < RANK[tier]) redirect("/tasks")
  return user
}
