import { and, eq, gt, isNull, lt, or, sql } from 'drizzle-orm'
import type { Db } from './db.js'
import { oauthClients, oauthCodes, oauthTokens } from './schema.js'

/** Persistence for the built-in OAuth 2.1 authorization server. Callers pass hashes, never raw secrets. */

export async function getOAuthClient(db: Db, clientId: string) {
  const [row] = await db.select().from(oauthClients).where(eq(oauthClients.clientId, clientId))
  return row?.info
}

export async function saveOAuthClient(db: Db, clientId: string, info: Record<string, unknown>) {
  await db.insert(oauthClients).values({ clientId, info })
}

export async function saveOAuthCode(db: Db, row: typeof oauthCodes.$inferInsert) {
  await db.insert(oauthCodes).values(row)
}

/** A live (unused, unexpired) code for this client, without consuming it. */
export async function peekOAuthCode(db: Db, codeHash: string, clientId: string) {
  const [row] = await db
    .select()
    .from(oauthCodes)
    .where(and(eq(oauthCodes.codeHash, codeHash), eq(oauthCodes.clientId, clientId), isNull(oauthCodes.usedAt), gt(oauthCodes.expiresAt, sql`now()`)))
  return row ?? null
}

/** Atomically mark a code used; returns it only if this call won (codes are single-use). */
export async function consumeOAuthCode(db: Db, codeHash: string, clientId: string) {
  const [row] = await db
    .update(oauthCodes)
    .set({ usedAt: sql`now()` })
    .where(and(eq(oauthCodes.codeHash, codeHash), eq(oauthCodes.clientId, clientId), isNull(oauthCodes.usedAt), gt(oauthCodes.expiresAt, sql`now()`)))
    .returning()
  return row ?? null
}

export async function saveOAuthTokens(db: Db, rows: Array<typeof oauthTokens.$inferInsert>) {
  await db.insert(oauthTokens).values(rows)
}

/** A live token of the given kind. */
export async function findOAuthToken(db: Db, tokenHash: string, kind: 'access' | 'refresh') {
  const [row] = await db
    .select()
    .from(oauthTokens)
    .where(and(eq(oauthTokens.tokenHash, tokenHash), eq(oauthTokens.kind, kind), isNull(oauthTokens.revokedAt), gt(oauthTokens.expiresAt, sql`now()`)))
  return row ?? null
}

/** Revoke one token; a refresh token is only revoked once, so concurrent refreshes cannot both win. */
export async function revokeOAuthToken(db: Db, tokenHash: string) {
  const res = await db
    .update(oauthTokens)
    .set({ revokedAt: sql`now()` })
    .where(and(eq(oauthTokens.tokenHash, tokenHash), isNull(oauthTokens.revokedAt)))
    .returning({ h: oauthTokens.tokenHash })
  return res.length > 0
}

/** Sign a user out everywhere. */
export async function revokeOAuthSubject(db: Db, subject: string) {
  const res = await db
    .update(oauthTokens)
    .set({ revokedAt: sql`now()` })
    .where(and(eq(oauthTokens.subject, subject), isNull(oauthTokens.revokedAt), gt(oauthTokens.expiresAt, sql`now()`)))
    .returning({ h: oauthTokens.tokenHash })
  return res.length
}

export async function listOAuthSessions(db: Db) {
  return db.execute<{ subject: string; client_id: string; scopes: string[]; last_issued: Date; refresh_expires: Date }>(sql`
    SELECT subject, client_id, scopes, max(created_at) AS last_issued, max(expires_at) AS refresh_expires
    FROM ops.oauth_tokens WHERE kind = 'refresh' AND revoked_at IS NULL AND expires_at > now()
    GROUP BY subject, client_id, scopes ORDER BY subject`)
}

/** Housekeeping: drop expired codes and long-dead tokens. */
export async function purgeOAuth(db: Db) {
  await db.delete(oauthCodes).where(lt(oauthCodes.expiresAt, sql`now() - interval '1 day'`))
  await db.delete(oauthTokens).where(or(lt(oauthTokens.expiresAt, sql`now() - interval '7 days'`), lt(oauthTokens.revokedAt, sql`now() - interval '7 days'`)))
}
