/**
 * sessionGuard — session-security (wave-13).
 *
 * Run immediately after authMiddleware / JWT verification.
 * Responsibilities:
 *   1. KV blocklist check (revoked sessions) via RATELIMIT_KV.
 *   2. Load the user_sessions row by token_hash.
 *   3. Enforce idle timeout from tenant_security_settings.
 *   4. Touch last_active_at (fire-and-forget; never fails the request).
 *
 * Revoked or idle-expired sessions → 401.
 * If no session row exists → 401 (fail-closed).
 */
import type { MiddlewareHandler } from 'hono'
import { blocklistRevokedTokens } from '@zync/auth'
import { getSessionByTokenHash, touchSession, revokeSession, getTenantSecuritySettings } from '@zync/db/queries'
import type { AppEnv } from '../types'

/** Per-PoP in-memory cache for tenant security settings (60s TTL). */
const settingsCache = new Map<string, { data: Awaited<ReturnType<typeof getTenantSecuritySettings>>; expiresAt: number }>()

async function getCachedSecuritySettings(
  db: Parameters<typeof getTenantSecuritySettings>[0],
  tenantId: string,
) {
  const now = Date.now()
  const cached = settingsCache.get(tenantId)
  if (cached && cached.expiresAt > now) return cached.data

  const data = await getTenantSecuritySettings(db, tenantId)
  settingsCache.set(tenantId, { data, expiresAt: now + 60_000 })
  return data
}

type SessionRow = NonNullable<Awaited<ReturnType<typeof getSessionByTokenHash>>>

export type UserSessionRowEvaluation =
  | { ok: true; sessionRow: SessionRow }
  | { ok: false; reason: 'missing' | 'revoked' | 'idle_timeout'; sessionRow?: SessionRow }

/** Core session-row gate shared by sessionGuard and optional-auth paths (e.g. KB inline images). */
export async function evaluateUserSessionRow(
  db: Parameters<typeof getSessionByTokenHash>[0],
  tenantId: string,
  accessTokenHash: string,
): Promise<UserSessionRowEvaluation> {
  const sessionRow = await getSessionByTokenHash(db, accessTokenHash)
  if (!sessionRow) {
    return { ok: false, reason: 'missing' }
  }

  if (sessionRow.revokedAt) {
    return { ok: false, reason: 'revoked', sessionRow }
  }

  const settings = await getCachedSecuritySettings(db, tenantId)
  if (settings.idleTimeoutMinutes != null) {
    const idleMs = settings.idleTimeoutMinutes * 60 * 1000
    const idleSince = Date.now() - new Date(sessionRow.lastActiveAt).getTime()
    if (idleSince > idleMs) {
      return { ok: false, reason: 'idle_timeout', sessionRow }
    }
  }

  return { ok: true, sessionRow }
}

export const sessionGuard: MiddlewareHandler<AppEnv> = async (c, next) => {
  const session = c.get('session')
  const accessTokenHash = c.get('accessTokenHash')

  // Only applies to user sessions (not admin sessions / impersonation).
  if (!session || session.type !== 'user' || !accessTokenHash) {
    return next()
  }

  const db = c.get('db')
  const tenantId = (session as { tid?: string }).tid
  if (!tenantId) return next()

  // KV blocklist already checked in authMiddleware.
  const evaluation = await evaluateUserSessionRow(db, tenantId, accessTokenHash)
  if (!evaluation.ok) {
    if (evaluation.reason === 'missing') {
      return c.json({ error: 'Unauthorized' }, 401)
    }
    if (evaluation.reason === 'revoked') {
      return c.json({ error: 'Session revoked' }, 401)
    }
    // Idle timeout: revoke + blocklist (best-effort; don't block the 401 response on failure).
    const sessionRow = evaluation.sessionRow!
    c.executionCtx.waitUntil(revokeSession(db, sessionRow.id, sessionRow.userId, tenantId, 'idle_timeout')
      .then(async (result) => {
        if (result.tokens.length > 0) {
          await blocklistRevokedTokens(c.env.RATELIMIT_KV, result.tokens)
        }
      })
      .catch(() => undefined))
    return c.json({ error: 'Session expired due to inactivity', code: 'idle_timeout' }, 401)
  }

  c.set('sessionId', evaluation.sessionRow.id)

  // Touch last_active_at after the response is returned.
  c.executionCtx.waitUntil(
    touchSession(db, evaluation.sessionRow.tenantId, evaluation.sessionRow.id).catch(() => undefined),
  )

  return next()
}
