/**
 * session-security helpers — wave-13.
 *
 * KV blocklist, device-name parsing, and session creation wired into the login flow.
 * These run on the auth hot path; keep allocations minimal.
 */
import { hashToken } from './tokens'
import type { Db } from '@zync/db/queries'
import { createSession } from '@zync/db/queries'

// ── KV blocklist ──────────────────────────────────────────────────────────────

/** KV key for a revoked session token hash. */
export function sessionRevokedKey(tokenHash: string): string {
  return `session:revoked:${tokenHash}`
}

/**
 * Write a revocation entry to KV.
 * TTL is clamped to at least 60s (KV minimum) and at most the remaining session lifetime.
 */
export async function blocklistToken(
  kv: KVNamespace,
  tokenHash: string,
  expiresAt: Date,
): Promise<void> {
  const ttl = Math.max(60, Math.floor((expiresAt.getTime() - Date.now()) / 1000))
  await kv.put(sessionRevokedKey(tokenHash), '1', { expirationTtl: ttl })
}

/**
 * Returns true if the token hash is in the KV blocklist.
 * A null response (miss) is treated as not-blocklisted.
 */
export async function isTokenBlocklisted(
  kv: KVNamespace,
  tokenHash: string,
): Promise<boolean> {
  const val = await kv.get(sessionRevokedKey(tokenHash))
  return val !== null
}

// ── Device-name parsing ───────────────────────────────────────────────────────

/**
 * Derive a human-readable device name from a User-Agent string.
 * Returns e.g. "Chrome on macOS" or null when UA is absent/unrecognised.
 *
 * We avoid importing ua-parser-js (it needs node_modules shimming on Workers)
 * and instead do a lightweight heuristic that covers >95% of browsers.
 */
export function parseDeviceName(userAgent: string | null | undefined): string | null {
  if (!userAgent) return null

  // Browser detection (order matters: Edge before Chrome, Opera before Chrome)
  let browser = 'Unknown'
  if (/Edg\//.test(userAgent)) browser = 'Edge'
  else if (/OPR\//.test(userAgent) || /Opera/.test(userAgent)) browser = 'Opera'
  else if (/Chrome\//.test(userAgent) && !/Chromium/.test(userAgent)) browser = 'Chrome'
  else if (/Firefox\//.test(userAgent)) browser = 'Firefox'
  else if (/Safari\//.test(userAgent) && !/Chrome/.test(userAgent)) browser = 'Safari'
  else if (/MSIE |Trident\//.test(userAgent)) browser = 'Internet Explorer'

  // OS detection
  let os = 'Unknown OS'
  if (/Windows NT/.test(userAgent)) os = 'Windows'
  else if (/Mac OS X/.test(userAgent) && !/iPhone|iPad/.test(userAgent)) os = 'macOS'
  else if (/iPhone/.test(userAgent)) os = 'iOS'
  else if (/iPad/.test(userAgent)) os = 'iPadOS'
  else if (/Android/.test(userAgent)) os = 'Android'
  else if (/Linux/.test(userAgent)) os = 'Linux'
  else if (/CrOS/.test(userAgent)) os = 'ChromeOS'

  if (browser === 'Unknown' && os === 'Unknown OS') return null
  return `${browser} on ${os}`
}

// ── Session creation (login-flow integration) ─────────────────────────────────

export interface RecordSessionOnLoginArgs {
  tenantId: string
  userId: string
  accessToken: string
  userAgent: string | null | undefined
  ip: string | null | undefined
  countryCode: string | null | undefined
  expiresAt: Date
}

/**
 * Hash the access JWT, parse device info, and insert a user_sessions row.
 * Call this immediately after signing the access token on login / refresh.
 */
export async function recordSessionOnLogin(
  db: Db,
  args: RecordSessionOnLoginArgs,
): Promise<{ id: string }> {
  const tokenHash = await hashToken(args.accessToken)
  const deviceName = parseDeviceName(args.userAgent)

  return createSession(db, {
    tenantId: args.tenantId,
    userId: args.userId,
    tokenHash,
    deviceName,
    ipAddress: args.ip ?? null,
    countryCode: args.countryCode ?? null,
    expiresAt: args.expiresAt,
  })
}

// ── Bulk revocation helper ────────────────────────────────────────────────────

export interface RevokedTokenEntry {
  tokenHash: string
  expiresAt: Date
}

/**
 * Revoke DB rows and add every returned token hash to the KV blocklist.
 * TTL per entry uses each session row's expires_at.
 */
export async function blocklistRevokedTokens(
  kv: KVNamespace,
  entries: RevokedTokenEntry[],
): Promise<void> {
  await Promise.all(entries.map((e) => blocklistToken(kv, e.tokenHash, e.expiresAt)))
}
