/**
 * POST /oauth/token — code exchange & refresh token rotation (wave-12).
 *
 * Server-to-server endpoint — no user session required.
 * Supports:
 *   - grant_type=authorization_code: exchange code for access+refresh tokens
 *   - grant_type=refresh_token: rotate refresh token
 *
 * Security:
 *   - Rate-limited by RATELIMIT_KV per client_id+IP (brute-force protection).
 *   - Client secret verified via timingSafeEqual (confidential clients).
 *   - PKCE verified (public clients MUST use PKCE).
 *   - Refresh-token family revoke on reuse detection.
 */
import { Hono } from 'hono'
import { z } from 'zod'
import {
  generateOpaqueToken,
  hashToken,
  verifyPKCE,
  OAuthError,
  ACCESS_TOKEN_PREFIX,
  REFRESH_TOKEN_PREFIX,
  ACCESS_TTL_SECONDS,
  REFRESH_TTL_SECONDS,
  timingSafeEqual,
} from '@zync/auth'
import {
  createDb,
  getOAuthClientByClientId,
  lookupAuthorizationCode,
  markAuthorizationCodeUsed,
  insertTokenPair,
  lookupRefreshToken,
  rotateOAuthRefreshToken,
  revokeOAuthTokenFamily,
} from '@zync/db/queries'
import type { AppEnv } from '../../types'

const tokenBodySchema = z.record(z.string(), z.string())

const OAUTH_TOKEN_RATE_LIMIT_MAX = 20
const OAUTH_TOKEN_RATE_LIMIT_TTL = 60
const CLIENT_ID_PATTERN = /^[a-zA-Z0-9_-]{1,128}$/

function oauthTokenRateLimitKey(clientId: string, ip: string): string {
  return `oauth_token:${clientId}:${ip}`
}

async function getOAuthTokenRateLimitCount(kv: KVNamespace, key: string): Promise<number> {
  return parseInt((await kv.get(key)) ?? '0', 10)
}

async function incrementOAuthTokenRateLimit(kv: KVNamespace, key: string, current: number): Promise<number> {
  const next = current + 1
  await kv.put(key, String(next), { expirationTtl: OAUTH_TOKEN_RATE_LIMIT_TTL })
  return next
}

const tokenRoute = new Hono<AppEnv>()

tokenRoute.post('/token', async (c) => {
  let body: Record<string, string>
  const ct = c.req.header('content-type') ?? ''
  if (ct.includes('application/x-www-form-urlencoded')) {
    const form = await c.req.formData()
    body = Object.fromEntries(form.entries()) as Record<string, string>
  } else {
    try {
      const jsonResult = tokenBodySchema.safeParse(await c.req.json())
      if (!jsonResult.success) return c.json({ error: 'invalid_request', error_description: 'Could not parse request body' }, 400)
      body = jsonResult.data
    } catch {
      return c.json({ error: 'invalid_request', error_description: 'Could not parse request body' }, 400)
    }
  }

  const { grant_type, client_id, client_secret, redirect_uri, code, code_verifier, refresh_token } = body

  if (!grant_type) {
    return c.json({ error: 'invalid_request', error_description: 'grant_type is required' }, 400)
  }
  if (!client_id) {
    return c.json({ error: 'invalid_request', error_description: 'client_id is required' }, 400)
  }
  if (!CLIENT_ID_PATTERN.test(client_id)) {
    return c.json({ error: 'invalid_client', error_description: 'Invalid client_id format' }, 400)
  }

  const ip = c.req.header('CF-Connecting-IP') ?? 'unknown'
  const rateLimitKey = oauthTokenRateLimitKey(client_id, ip)
  let rateLimitCount = await getOAuthTokenRateLimitCount(c.env.RATELIMIT_KV, rateLimitKey)
  if (rateLimitCount >= OAUTH_TOKEN_RATE_LIMIT_MAX) {
    return c.json({ error: 'rate_limited', error_description: 'Too many requests' }, 429)
  }

  const db = createDb(c.env)
  const client = await getOAuthClientByClientId(db, client_id)
  if (!client) {
    rateLimitCount = await incrementOAuthTokenRateLimit(c.env.RATELIMIT_KV, rateLimitKey, rateLimitCount)
    return c.json({ error: 'invalid_client', error_description: 'Unknown client' }, 401)
  }

  // Verify client secret for confidential clients.
  // Public clients (no clientSecretHash) must use PKCE — enforced below per grant.
  const isPublicClient = !client.clientSecretHash
  if (!isPublicClient) {
    if (!client_secret) {
      return c.json({ error: 'invalid_client', error_description: 'client_secret required' }, 401)
    }
    const secretHash = await hashToken(client_secret)
    const valid = timingSafeEqual(secretHash, client.clientSecretHash)
    if (!valid) {
      rateLimitCount = await incrementOAuthTokenRateLimit(c.env.RATELIMIT_KV, rateLimitKey, rateLimitCount)
      return c.json({ error: 'invalid_client', error_description: 'Invalid client_secret' }, 401)
    }
  }

  const failInvalidGrant = async (description: string) => {
    rateLimitCount = await incrementOAuthTokenRateLimit(c.env.RATELIMIT_KV, rateLimitKey, rateLimitCount)
    return c.json({ error: 'invalid_grant', error_description: description }, 400)
  }

  try {
    if (grant_type === 'authorization_code') {
      if (!code) return c.json({ error: 'invalid_request', error_description: 'code is required' }, 400)
      if (!redirect_uri) return c.json({ error: 'invalid_request', error_description: 'redirect_uri is required' }, 400)

      const now = new Date()
      const codeHash = await hashToken(code)
      const codeRow = await lookupAuthorizationCode(db, codeHash)

      if (!codeRow) return failInvalidGrant('Authorization code not found')
      if (codeRow.oauthClientId !== client.id) return failInvalidGrant('client_id mismatch')
      if (codeRow.usedAt) return failInvalidGrant('Authorization code already used')
      if (codeRow.expiresAt < now) return failInvalidGrant('Authorization code expired')
      if (codeRow.redirectUri !== redirect_uri) return failInvalidGrant('redirect_uri mismatch')

      // PKCE — public clients MUST use PKCE (S256); confidential clients optional
      if (isPublicClient) {
        if (!codeRow.codeChallenge) {
          return failInvalidGrant('PKCE required for public clients')
        }
        if (!code_verifier) {
          return failInvalidGrant('code_verifier required')
        }
        const ok = await verifyPKCE(code_verifier, codeRow.codeChallenge)
        if (!ok) return failInvalidGrant('code_verifier mismatch')
      } else if (codeRow.codeChallenge) {
        if (!code_verifier) return failInvalidGrant('code_verifier required')
        const ok = await verifyPKCE(code_verifier, codeRow.codeChallenge)
        if (!ok) return failInvalidGrant('code_verifier mismatch')
      }

      const consumed = await markAuthorizationCodeUsed(db, codeRow.tenantId, codeRow.id, now)
      if (!consumed) return failInvalidGrant('Authorization code already used')

      // Mint tokens
      const accessPlain   = ACCESS_TOKEN_PREFIX  + generateOpaqueToken(32)
      const refreshPlain  = REFRESH_TOKEN_PREFIX + generateOpaqueToken(32)
      const accessHash    = await hashToken(accessPlain)
      const refreshHash   = await hashToken(refreshPlain)
      const familyId      = crypto.randomUUID()
      const accessExp     = new Date(Date.now() + ACCESS_TTL_SECONDS  * 1000)
      const refreshExp    = new Date(Date.now() + REFRESH_TTL_SECONDS * 1000)

      await insertTokenPair(db, {
        accessTokenHash: accessHash,
        refreshTokenHash: refreshHash,
        oauthClientId: codeRow.oauthClientId,
        tenantId: codeRow.tenantId,
        userId: codeRow.userId,
        scope: codeRow.scope,
        familyId,
        accessExpiresAt: accessExp,
        refreshExpiresAt: refreshExp,
      })

      return c.json({
        access_token: accessPlain,
        refresh_token: refreshPlain,
        token_type: 'Bearer',
        expires_in: ACCESS_TTL_SECONDS,
        scope: codeRow.scope,
      })
    }

    if (grant_type === 'refresh_token') {
      if (!refresh_token) return c.json({ error: 'invalid_request', error_description: 'refresh_token is required' }, 400)

      const now = new Date()
      const rtHash = await hashToken(refresh_token)
      const rt = await lookupRefreshToken(db, rtHash)

      if (!rt) {
        rateLimitCount = await incrementOAuthTokenRateLimit(c.env.RATELIMIT_KV, rateLimitKey, rateLimitCount)
        return c.json({ error: 'invalid_grant', error_description: 'Refresh token not found' }, 400)
      }

      // Reuse detection: already rotated or revoked → compromise
      if (rt.rotatedToId || rt.revokedAt) {
        await revokeOAuthTokenFamily(db, {
          familyId: rt.familyId,
          tenantId: rt.tenantId,
          userId: rt.userId,
          oauthClientId: rt.oauthClientId,
        })
        rateLimitCount = await incrementOAuthTokenRateLimit(c.env.RATELIMIT_KV, rateLimitKey, rateLimitCount)
        return c.json({ error: 'invalid_grant', error_description: 'Refresh token reuse detected' }, 400)
      }

      if (rt.expiresAt < now) {
        rateLimitCount = await incrementOAuthTokenRateLimit(c.env.RATELIMIT_KV, rateLimitKey, rateLimitCount)
        return c.json({ error: 'invalid_grant', error_description: 'Refresh token expired' }, 400)
      }
      if (rt.oauthClientId !== client.id) {
        rateLimitCount = await incrementOAuthTokenRateLimit(c.env.RATELIMIT_KV, rateLimitKey, rateLimitCount)
        return c.json({ error: 'invalid_grant', error_description: 'client_id mismatch' }, 400)
      }

      const newAccessPlain   = ACCESS_TOKEN_PREFIX  + generateOpaqueToken(32)
      const newRefreshPlain  = REFRESH_TOKEN_PREFIX + generateOpaqueToken(32)
      const newAccessHash    = await hashToken(newAccessPlain)
      const newRefreshHash   = await hashToken(newRefreshPlain)
      const newAccessExp     = new Date(Date.now() + ACCESS_TTL_SECONDS  * 1000)
      const newRefreshExp    = new Date(Date.now() + REFRESH_TTL_SECONDS * 1000)

      await rotateOAuthRefreshToken(db, {
        oldRefreshTokenId: rt.id,
        newRefreshTokenHash: newRefreshHash,
        newAccessTokenHash: newAccessHash,
        familyId: rt.familyId,
        oauthClientId: rt.oauthClientId,
        tenantId: rt.tenantId,
        userId: rt.userId,
        scope: rt.scope,
        newAccessExpiresAt: newAccessExp,
        newRefreshExpiresAt: newRefreshExp,
      })

      return c.json({
        access_token: newAccessPlain,
        refresh_token: newRefreshPlain,
        token_type: 'Bearer',
        expires_in: ACCESS_TTL_SECONDS,
        scope: rt.scope,
      })
    }

    return c.json({ error: 'unsupported_grant_type', error_description: `grant_type '${grant_type}' is not supported` }, 400)
  } catch (e) {
    if (e instanceof OAuthError) {
      return c.json({ error: e.code, error_description: e.message }, 400)
    }
    throw e
  }
})

export { tokenRoute }
