import 'server-only'

import { createAdminClient } from '@/lib/supabase/admin'
import { findEnvUserByUsername, verifyCredentials, type EnvUser } from '@/lib/auth/env-users'
import { hashPassword, verifyPassword } from '@/lib/auth/passwords'
import type { UserRole } from '@/types/db'

/**
 * Account overrides — real password reset and role change (2026-09-11).
 *
 * AUTH_USERS (environment) says WHO can sign in. `auth_accounts` (database)
 * holds what an admin has changed since, and wins over the env:
 *   • a stored password hash replaces the env password
 *   • a stored role replaces the env role
 *   • tokens_valid_after revokes every session issued before it
 *
 * Every read tolerates migration 0015 not being applied: a missing table
 * means "no overrides", so sign-in behaves exactly as it did before.
 */

export interface AccountOverride {
  user_id: string
  username: string
  password_hash: string | null
  role: UserRole | null
  tokens_valid_after: string | null
  password_updated_at: string | null
  updated_by: string | null
}

function tableMissing(error: { message?: string; code?: string } | null | undefined): boolean {
  if (!error) return false
  return (
    error.code === '42P01' ||
    error.code === 'PGRST205' ||
    /does not exist|schema cache|could not find the table/i.test(error.message ?? '')
  )
}

/* ---------------- Short cache for the per-request revocation check -------------
 * getSessionUser runs on every page and API call, several times per request.
 * A 30-second in-process cache keeps that to about one lookup per user per
 * half-minute. A change made through the admin console clears the entry at
 * once, so on a single-process deployment revocation is immediate; across
 * several processes it takes effect within 30 seconds.
 */
const TTL_MS = 30_000
const cache = new Map<string, { value: AccountOverride | null; expires: number }>()

export function invalidateAccount(userId: string) {
  cache.delete(userId)
}

/** Whether migration 0015 is applied — the admin console says so honestly. */
export async function accountsAvailable(): Promise<boolean> {
  const { error } = await createAdminClient().from('auth_accounts').select('user_id').limit(1)
  return !tableMissing(error)
}

/**
 * The override row, or `unavailable` when the database could not answer.
 * A missing table (migration 0015 not applied) is NOT unavailable — it means
 * no override can exist, so "none" is the true answer. Only real answers are
 * cached; a transient failure is retried on the next call.
 */
async function loadOverride(
  userId: string,
): Promise<{ value: AccountOverride | null } | 'unavailable'> {
  const hit = cache.get(userId)
  if (hit && hit.expires > Date.now()) return { value: hit.value }
  const { data, error } = await createAdminClient()
    .from('auth_accounts')
    .select('*')
    .eq('user_id', userId)
    .maybeSingle()
  if (error && !tableMissing(error)) return 'unavailable'
  const value = error ? null : ((data ?? null) as AccountOverride | null)
  cache.set(userId, { value, expires: Date.now() + TTL_MS })
  return { value }
}

/**
 * For sessions and the role switch: an unreadable database counts as "no
 * override", so a brief outage never signs everyone out. (Sign-in itself is
 * stricter — see verifyLogin.)
 */
export async function getAccountOverride(userId: string): Promise<AccountOverride | null> {
  const result = await loadOverride(userId)
  return result === 'unavailable' ? null : result.value
}

/** The role a login actually has: an admin override first, then AUTH_USERS. */
export function effectiveRole(env: EnvUser, override: AccountOverride | null): UserRole {
  return override?.role ?? env.role
}

/**
 * Sign-in. The username must exist in AUTH_USERS; the password is checked
 * against the admin-set hash when there is one, otherwise against the env.
 * Returns the env user carrying its EFFECTIVE role.
 */
export async function verifyLogin(username: string, password: string): Promise<EnvUser | null> {
  const env = findEnvUserByUsername(username)
  if (!env) return null
  // Fail CLOSED. If the database cannot say whether an admin reset this
  // password, refuse — falling back to AUTH_USERS would let an old password
  // back in, which is exactly what a reset exists to stop.
  const loaded = await loadOverride(env.id)
  if (loaded === 'unavailable') {
    throw new Error('Sign-in is temporarily unavailable. Please try again in a moment.')
  }
  const override = loaded.value
  const ok = override?.password_hash
    ? await verifyPassword(password, override.password_hash)
    : verifyCredentials(username, password) !== null
  if (!ok) return null
  const role = effectiveRole(env, override)
  return { ...env, role, internal: role === 'admin' }
}

/**
 * True when a session was issued before its account was revoked. Checks the
 * signed-in account and, for a pilot role switch, the admin who started it —
 * so demoting that admin also ends the session they switched into.
 */
export async function isSessionRevoked(
  issuedAtSeconds: number | undefined,
  userIds: Array<string | undefined>,
): Promise<boolean> {
  if (!issuedAtSeconds) return false
  for (const id of new Set(userIds.filter(Boolean) as string[])) {
    const override = await getAccountOverride(id)
    const after = override?.tokens_valid_after ? Date.parse(override.tokens_valid_after) : NaN
    // Whole seconds on both sides: a JWT's `iat` is floored to the second, so
    // comparing against milliseconds would revoke a login made in the same
    // second as the reset that allowed it.
    if (Number.isFinite(after) && issuedAtSeconds < Math.floor(after / 1000)) return true
  }
  return false
}

async function upsertOverride(
  env: EnvUser,
  patch: Partial<Pick<AccountOverride, 'password_hash' | 'role' | 'password_updated_at'>>,
  actorLabel: string,
): Promise<{ ok: true } | { ok: false; error: string; missing?: boolean }> {
  const now = new Date().toISOString()
  const { error } = await createAdminClient()
    .from('auth_accounts')
    .upsert(
      {
        user_id: env.id,
        username: env.username.toLowerCase(),
        ...patch,
        // Any credential or role change ends the person's existing sessions.
        tokens_valid_after: now,
        updated_by: actorLabel,
        updated_at: now,
      },
      { onConflict: 'user_id' },
    )
  invalidateAccount(env.id)
  if (tableMissing(error)) {
    return { ok: false, missing: true, error: 'Password reset and role change need database migration 0015.' }
  }
  if (error) return { ok: false, error: 'Could not save the account change.' }
  return { ok: true }
}

export async function setAccountPassword(env: EnvUser, newPassword: string, actorLabel: string) {
  return upsertOverride(
    env,
    { password_hash: await hashPassword(newPassword), password_updated_at: new Date().toISOString() },
    actorLabel,
  )
}

export async function setAccountRole(env: EnvUser, role: UserRole, actorLabel: string) {
  return upsertOverride(env, { role }, actorLabel)
}
