/**
 * Unified Bearer-token auth middleware — zync-public-api (tenant-public-api wave-11 leaf-D).
 *
 * Flow:
 *  1. Extract Bearer token from Authorization header.
 *  2. Brute-force check via RATE_LIMITER_AUTH KV (5 invalid attempts / min → 429).
 *  3. Compute SHA-256 hash.
 *     a. Look up tenant_api_keys by key_hash — API-key path.
 *     b. On miss: look up oauth_access_tokens (OAuth token path).
 *  4. Reject revoked/expired tokens.
 *  5. API-key: tier-gate (min 'business'), quota gate, fire-and-forget last_used_at.
 *     OAuth: tier-gate (min 'business'), KV-cache resolved token (TTL ≤ remaining
 *            lifetime, real tenant tier), fire-and-forget oauth_connections.last_used_at.
 *  6. Store resolved { tenantId, userId, scopes, authKind, ... } in context.
 *
 * NOTE: AE quota check is best-effort (~1–2 min lag). A brief overage near the
 * boundary is possible — this is a soft quota as per spec.
 */
import type { Context, Next } from 'hono'
import { hashToken } from '@zync/auth'
import { meetsMinimumTier } from '@zync/auth'
import { TenantTier } from '@zync/types'
import { getApiKeyByHash, updateApiKeyLastUsed, resolveOAuthAccessToken, touchOAuthConnectionLastUsed } from '@zync/db/queries'
import { errUnauthorized, errTierRequired, errRateLimited, mapOAuthScopesToApiScopes } from '@zync/public-api'
import { getMonthlyUsage, writeApiUsage, nextUtcMonthStart } from '@zync/api-usage'
import { createDb } from '../db'
import type { Env } from '../env'

export interface ApiKeyContext {
  tenantId: string
  userId: string
  scopes: string[]
  tier: string
  keyId: string
  createdBy: string | null
  authKind: 'api_key'
  clientId?: undefined
}

export interface OAuthTokenContext {
  tenantId: string
  userId: string
  scopes: string[]
  tier: string
  keyId?: undefined
  createdBy?: undefined
  authKind: 'oauth'
  clientId: string
  oauthClientRowId: string
}

export type ResolvedAuth = ApiKeyContext | OAuthTokenContext

// KV cache key for OAuth token resolution (v3: mapped API scopes — S10-003)
const OAUTH_AT_CACHE_PREFIX = 'oauth_at:v3:'
/** Max OAuth token context cache TTL — tier downgrades must propagate within this window (S10-i2-004). */
const OAUTH_AT_CACHE_MAX_TTL = 600 // 10 minutes

const BRUTE_FORCE_MAX = 5
const BRUTE_FORCE_TTL = 60 // seconds

async function getBruteForceCount(kv: KVNamespace, ip: string): Promise<number> {
  const val = await kv.get(`bf:${ip}`)
  return val ? parseInt(val, 10) : 0
}

async function incrementBruteForce(kv: KVNamespace, ip: string): Promise<void> {
  const count = await getBruteForceCount(kv, ip)
  await kv.put(`bf:${ip}`, String(count + 1), { expirationTtl: BRUTE_FORCE_TTL })
}

export async function apiKeyAuth(c: Context<{ Bindings: Env }>, next: Next): Promise<Response | void> {
  const authHeader = c.req.header('Authorization')
  if (!authHeader?.startsWith('Bearer ')) {
    return errUnauthorized()
  }
  const rawToken = authHeader.slice(7).trim()
  if (!rawToken) {
    return errUnauthorized()
  }

  const ip = c.req.header('cf-connecting-ip') ?? 'unknown'

  // Brute-force gate
  const bruteCount = await getBruteForceCount(c.env.RATE_LIMITER_AUTH, ip)
  if (bruteCount >= BRUTE_FORCE_MAX) {
    return errRateLimited(BRUTE_FORCE_TTL)
  }

  const db = createDb(c.env)
  const tokenHash = await hashToken(rawToken)

  // ── Path A: API key ──────────────────────────────────────────────────────────
  const keyRow = await getApiKeyByHash(db, tokenHash)

  if (keyRow) {
    // revokedAt already filtered by getApiKeyByHash (WHERE revokedAt IS NULL)
    if (keyRow.expiresAt && keyRow.expiresAt < new Date()) {
      console.warn('api_key_auth_failed', { reason: 'expired', keyId: keyRow.id })
      return errUnauthorized()
    }

    // Tier gate — minimum 'business'
    const tenantRows = await db.execute({
      sql: 'SELECT tier FROM tenants WHERE id = $1',
      params: [keyRow.tenantId],
    } as unknown as Parameters<typeof db.execute>[0])
    const tenantRow = (tenantRows as unknown as Array<{ tier: string }>)[0]
    const tier = tenantRow?.tier ?? 'freelancer'

    if (!meetsMinimumTier(tier as TenantTier, TenantTier.BUSINESS)) {
      return errTierRequired('business')
    }

    // Quota gate
    if (keyRow.monthlyQuota !== null && keyRow.monthlyQuota !== undefined) {
      const used = await getMonthlyUsage(c.env, keyRow.tenantId, keyRow.id)
      if (used >= keyRow.monthlyQuota) {
        return c.json(
          { error: 'quota_exceeded', message: 'Monthly API quota exceeded' },
          429,
          { 'Retry-After': nextUtcMonthStart().toUTCString() },
        )
      }
    }

    c.set('apiKey' as never, {
      tenantId: keyRow.tenantId,
      userId: keyRow.createdBy ?? '',
      scopes: (keyRow.scopes as string[]) ?? [],
      tier,
      keyId: keyRow.id,
      createdBy: keyRow.createdBy ?? null,
      authKind: 'api_key',
    } satisfies ApiKeyContext)

    void updateApiKeyLastUsed(db, keyRow.tenantId, keyRow.id)
    c.executionCtx.waitUntil(
      Promise.resolve(
        writeApiUsage(c.env, {
          tenantId: keyRow.tenantId,
          keyId: keyRow.id,
          endpoint: c.req.routePath,
          method: c.req.method,
        }),
      ),
    )

    await next()
    return
  }

  // ── Path B: OAuth access token ───────────────────────────────────────────────
  // Check KV cache first (keyed by token hash, TTL ≤ remaining token lifetime)
  const cacheKey = `${OAUTH_AT_CACHE_PREFIX}${tokenHash}`
  const cached = await c.env.RATELIMIT_KV.get(cacheKey, 'json') as OAuthTokenContext | null

  let oauthCtx: OAuthTokenContext | null = cached

  if (!oauthCtx) {
    const resolved = await resolveOAuthAccessToken(db, tokenHash)
    if (resolved) {
      const tenantRows = await db.execute({
        sql: 'SELECT tier FROM tenants WHERE id = $1',
        params: [resolved.tenantId],
      } as unknown as Parameters<typeof db.execute>[0])
      const tenantRow = (tenantRows as unknown as Array<{ tier: string }>)[0]
      const tier = tenantRow?.tier ?? 'freelancer'

      oauthCtx = {
        tenantId: resolved.tenantId,
        userId: resolved.userId,
        scopes: mapOAuthScopesToApiScopes(resolved.scope.join(' ')),
        tier,
        authKind: 'oauth',
        clientId: resolved.clientId,
        oauthClientRowId: resolved.oauthClientRowId,
      }
      // Cache with TTL ≤ remaining token lifetime, max 1h
      const remainingSecs = Math.max(0, Math.floor((resolved.expiresAt.getTime() - Date.now()) / 1000))
      const cacheTtl = Math.min(remainingSecs, OAUTH_AT_CACHE_MAX_TTL)
      if (cacheTtl > 0) {
        await c.env.RATELIMIT_KV.put(cacheKey, JSON.stringify(oauthCtx), { expirationTtl: cacheTtl })
      }
    }
  }

  if (!oauthCtx) {
    await incrementBruteForce(c.env.RATE_LIMITER_AUTH, ip)
    return errUnauthorized()
  }

  if (!meetsMinimumTier(oauthCtx.tier as TenantTier, TenantTier.BUSINESS)) {
    return errTierRequired('business')
  }

  c.set('apiKey' as never, oauthCtx)

  // Fire-and-forget: update oauth_connections.last_used_at
  c.executionCtx.waitUntil(
    touchOAuthConnectionLastUsed(db, oauthCtx.tenantId, oauthCtx.userId, oauthCtx.oauthClientRowId),
  )

  await next()
}
