/**
 * The `auth` server global (docs/backend-plan.md §3.4) — the BUNDLE side. State (`auth.user`,
 * `auth.session`) is read from the current request, which the host resolved before the endpoint ran;
 * operations (`auth.email.*`, `auth.oauth.url`, `signIn/signOut`, `sessions`) forward to the host's
 * `AuthOps` on `globalThis.__lecodesAuth` (a seam: the compiled bundle carries its own SDK copy). Nothing
 * here touches the db or a secret; `auth.model.*` are pure schema factories.
 */

import { currentRequest, type RequestContext } from "../context"
import type { Model, Query } from "../db/types"
import { ApiError } from "../errors"
import { authSessionModel, authUserModel, type AuthProvider, type SessionFields, type UserFields } from "./models"
import type { AuthOps, AuthState, AuthUser, OAuthCredential } from "./types"

const seam = globalThis as unknown as { __lecodesAuth?: AuthOps }
const ops = (): AuthOps => {
  const o = seam.__lecodesAuth
  if (!o) throw new Error("auth: not available — the host did not install the auth runtime (globalThis.__lecodesAuth)")
  return o
}
const ctx = (): RequestContext => {
  const c = currentRequest()
  if (!c) throw new Error("auth: no current request — auth.* is only available while handling an endpoint or a channel join")
  return c
}
const state = (): AuthState => {
  const c = ctx()
  const s = c.auth as AuthState | undefined
  if (!s) throw new Error("auth: this project has no database — sessions need a defineDb() (auth.model.* are optional, Session is added for you)")
  return s
}

/** The schema of the two built-in models, for typing `auth.sessions` (developer extras aren't visible here). */
type AuthSchema = { User: Model<UserFields>, Session: Model<SessionFields> }

export const auth = {
  /** The signed-in user (built-in fields; custom ones via `db.user`) or `null`. Request-scoped. */
  get user(): AuthUser | null { return state().user },
  /** The device session — exists from the first request, before any login. */
  get session(): { readonly id: number } { return { id: state().sessionId } },
  /**
   * The signed-in user, or `throw new ApiError(401)`; with `match`, every given field must equal the user's
   * (`{ role: "admin" }`, `{ plan: "pro" }`) or it's a 403.
   */
  requireUser(match?: Record<string, unknown>): AuthUser {
    const u = state().user
    if (!u) throw new ApiError(401, "Sign in required")
    if (match) for (const [k, v] of Object.entries(match)) if ((u as Record<string, unknown>)[k] !== v) throw new ApiError(403, "Forbidden")
    return u
  },
  email: {
    /** Emails a 6-digit code to `email` (always resolves — no "email exists" oracle; rate-limited). Needs `"email"` in app.json providers. */
    sendCode: (email: string): Promise<void> => ops().email.sendCode(ctx(), email),
    /** The only email sign-in: checks the code sent to this device session, finds/creates the `User`, binds the session. */
    verify: (email: string, code: string): Promise<AuthUser> => ops().email.verify(ctx(), email, code),
  },
  oauth: {
    /** A one-time sign-in URL for the platform's OAuth broker (`"google" | "apple" | "yandex" | "vk"`), bound to this device session — open it in the browser. */
    url: (provider: Exclude<AuthProvider, "email">): Promise<string> => ops().oauth.url(ctx(), provider),
    /**
     * Sign in with a credential a NATIVE provider SDK produced in the app (client `OAuth.signIn(provider)`):
     * the platform verifies it with the provider, finds/links/creates the `User`, binds this session. The
     * in-app counterpart of the browser broker — same accounts either way.
     */
    signIn: (provider: Exclude<AuthProvider, "email">, credential: OAuthCredential): Promise<AuthUser> => ops().oauth.signIn(ctx(), provider, credential),
  },
  /** Escape hatch for developer-verified identities (Telegram initData, SSO): binds this session to `userId`. */
  signIn: (userId: number): Promise<AuthUser> => ops().signIn(ctx(), userId),
  /** Unbinds the user; the device stays a guest with the same `session.id` (token rotated). */
  signOut: (): Promise<void> => ops().signOut(ctx()),
  /** The current user's sessions ("my devices"): a `db.session` query — `await`, `.select()`, `.where(...).updateMany({ revoked: true })`. */
  get sessions(): Query<AuthSchema, "Session"> { return ops().sessions(ctx()) },
  /** Schema-time factories — see ./models.ts. */
  model: {
    user: authUserModel,
    session: authSessionModel,
  },
}

export type { AuthUser }
