/**
 * GET/POST /oauth/authorize — OAuth 2.0 consent endpoint (wave-12).
 *
 * GET:  validate params, serve consent screen data to the SPA.
 * POST: process Allow/Cancel user decision.
 *
 * Security:
 *   - Requires a verified user session (authMiddleware).
 *   - Unauthenticated GET → redirect to /login?return_to=<authorize_url>
 *   - Consent-form CSRF: POST must include a per-session anti-CSRF token.
 *   - redirect_uri: exact-match only; never redirect to unvalidated URI.
 *   - PKCE: required for public clients (S256 only); optional for confidential clients.
 */
import { Hono } from 'hono'
import { z } from 'zod'
import {
  generateOpaqueToken,
  hashToken,
  issueConsentToken,
  verifyConsentToken,
  normalizeRequestedScope,
  assertScopesAllowed,
  validateRedirectUri,
  CODE_TTL_SECONDS,
} from '@zync/auth'
import {
  createDb,
  getOAuthClientByClientId,
  insertAuthorizationCode,
} from '@zync/db/queries'
import { OAUTH_SCOPE_DEFINITIONS } from '@zync/public-api'
import type { AppEnv } from '../../types'
import type { SessionPayload } from '@zync/types'

const authorizeRoute = new Hono<AppEnv>()

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

function isPublicOAuthClient(client: { clientSecretHash: string | null }): boolean {
  return !client.clientSecretHash
}

/** Public clients MUST send PKCE with S256 per oauth-authorization-code spec. */
function redirectPublicClientPkceError(
  redirectUri: string,
  state: string | undefined,
  description: string,
): Response {
  const url = new URL(redirectUri)
  url.searchParams.set('error', 'invalid_request')
  url.searchParams.set('error_description', description)
  if (state) url.searchParams.set('state', state)
  return new Response(null, {
    status: 302,
    headers: { Location: url.toString() },
  })
}

function validatePublicClientPkce(
  client: { clientSecretHash: string | null },
  codeChallenge: string | undefined,
  codeChallengeMethod: string | undefined,
  redirectUri: string,
  state: string | undefined,
): Response | null {
  if (!isPublicOAuthClient(client)) return null

  if (!codeChallenge) {
    return redirectPublicClientPkceError(
      redirectUri,
      state,
      'PKCE code_challenge required for public clients',
    )
  }
  if (codeChallengeMethod !== 'S256') {
    return redirectPublicClientPkceError(
      redirectUri,
      state,
      'Only S256 code_challenge_method is supported',
    )
  }
  return null
}

// ── GET /oauth/authorize ───────────────────────────────────────────────────────

authorizeRoute.get('/authorize', async (c) => {
  // We use authMiddleware inline by applying it; for GET we redirect instead of 401
  const session = c.get('session') as SessionPayload | undefined
  if (!session || session.type !== 'user' || !session.tid) {
    const returnTo = encodeURIComponent(c.req.url)
    return c.redirect(`/login?return_to=${returnTo}`)
  }

  const {
    client_id: clientId,
    redirect_uri: redirectUri,
    response_type: responseType,
    scope,
    state,
    code_challenge: codeChallenge,
    code_challenge_method: codeChallengeMethod,
  } = c.req.query()

  // Validate response_type
  if (!responseType || responseType !== 'code') {
    return c.json({ error: 'unsupported_response_type', error_description: 'Only code response type is supported' }, 400)
  }

  // Missing required params
  if (!clientId || !redirectUri || !scope) {
    return c.json({ error: 'invalid_request', error_description: 'Missing required parameters' }, 400)
  }

  const db = createDb(c.env)
  const client = await getOAuthClientByClientId(db, clientId)

  // Unknown client — never redirect to unvalidated URI
  if (!client) {
    return c.json({ error: 'invalid_client', error_description: 'Unknown client_id' }, 400)
  }

  // Validate redirect_uri before any redirect
  try {
    validateRedirectUri(redirectUri, client.redirectUris as string[])
  } catch {
    return c.json({ error: 'invalid_request', error_description: 'redirect_uri not registered for this client' }, 400)
  }

  // Validate scope
  const normalizedScopes = normalizeRequestedScope(scope)
  try {
    assertScopesAllowed(normalizedScopes, client.scopes as string[])
  } catch {
    const url = new URL(redirectUri)
    url.searchParams.set('error', 'invalid_scope')
    if (state) url.searchParams.set('state', state)
    return c.redirect(url.toString())
  }

  // Validate code_challenge_method if provided (confidential clients may omit PKCE)
  if (codeChallengeMethod && codeChallengeMethod !== 'S256') {
    const url = new URL(redirectUri)
    url.searchParams.set('error', 'invalid_request')
    url.searchParams.set('error_description', 'Only S256 code_challenge_method is supported')
    if (state) url.searchParams.set('state', state)
    return c.redirect(url.toString())
  }

  const pkceError = validatePublicClientPkce(client, codeChallenge, codeChallengeMethod, redirectUri, state)
  if (pkceError) return pkceError

  // First-party: skip consent; issue code immediately
  if (client.isFirstParty) {
    const codePlain = generateOpaqueToken(32)
    const codeHash = await hashToken(codePlain)
    const expiresAt = new Date(Date.now() + CODE_TTL_SECONDS * 1000)
    await insertAuthorizationCode(db, {
      code: codeHash,
      oauthClientId: client.id,
      tenantId: session.tid!,
      userId: session.sub,
      redirectUri,
      scope: normalizedScopes.join(' '),
      codeChallenge: codeChallenge || null,
      codeChallengeMethod: codeChallengeMethod || null,
      expiresAt,
    })
    const url = new URL(redirectUri)
    url.searchParams.set('code', codePlain)
    if (state) url.searchParams.set('state', state)
    return c.redirect(url.toString())
  }

  // Issue consent CSRF token for the form
  const consentToken = await issueConsentToken(
    c.env.JWT_SECRET,
    session.sub,
    clientId,
  )

  // Get tenant name from session (slug as fallback)
  const tenantName = session.tid ?? 'your workspace'

  // Build scope labels for display
  const scopeLabels = normalizedScopes.map((s) => {
    const def = OAUTH_SCOPE_DEFINITIONS[s as keyof typeof OAUTH_SCOPE_DEFINITIONS]
    return def ? { scope: s, label: def.label, description: def.description } : { scope: s, label: s, description: '' }
  })

  // Return consent screen data as JSON for the SPA
  return c.json({
    clientId,
    clientName: client.name,
    logoUrl: client.logoUrl,
    redirectUri,
    scope: normalizedScopes.join(' '),
    scopeLabels,
    state: state ?? null,
    codeChallenge: codeChallenge ?? null,
    codeChallengeMethod: codeChallengeMethod ?? null,
    consentToken,
    tenantName,
  })
})

// ── POST /oauth/authorize ──────────────────────────────────────────────────────

authorizeRoute.post('/authorize', async (c) => {
  const session = c.get('session') as SessionPayload | undefined
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  let body: Record<string, string>
  const ct = c.req.header('content-type') ?? ''
  if (ct.includes('application/json')) {
    const jsonResult = authorizeBodySchema.safeParse(await c.req.json())
    body = jsonResult.success ? jsonResult.data : {}
  } else {
    const form = await c.req.formData()
    body = Object.fromEntries(form.entries()) as Record<string, string>
  }

  const {
    client_id: clientId,
    redirect_uri: redirectUri,
    scope,
    state,
    code_challenge: codeChallenge,
    code_challenge_method: codeChallengeMethod,
    consent_token: consentToken,
    action,
  } = body

  // Verify consent CSRF token BEFORE any side effects
  if (!consentToken || !clientId) {
    return c.json({ error: 'invalid_request', error_description: 'Missing consent_token or client_id' }, 400)
  }
  const csrfOk = await verifyConsentToken(c.env.JWT_SECRET, consentToken, session.sub, clientId)
  if (!csrfOk) {
    return c.json({ error: 'invalid_request', error_description: 'Invalid consent token' }, 403)
  }

  if (!redirectUri || !scope) {
    return c.json({ error: 'invalid_request', error_description: 'Missing parameters' }, 400)
  }

  const db = createDb(c.env)
  const client = await getOAuthClientByClientId(db, clientId)
  if (!client) {
    return c.json({ error: 'invalid_client', error_description: 'Unknown client_id' }, 400)
  }

  // Re-validate redirect_uri
  try {
    validateRedirectUri(redirectUri, client.redirectUris as string[])
  } catch {
    return c.json({ error: 'invalid_request', error_description: 'redirect_uri not registered' }, 400)
  }

  // User denied access
  if (action === 'cancel' || action === 'deny') {
    const url = new URL(redirectUri)
    url.searchParams.set('error', 'access_denied')
    if (state) url.searchParams.set('state', state)
    return c.redirect(url.toString())
  }

  // Re-validate scope
  const normalizedScopes = normalizeRequestedScope(scope)
  try {
    assertScopesAllowed(normalizedScopes, client.scopes as string[])
  } catch {
    const url = new URL(redirectUri)
    url.searchParams.set('error', 'invalid_scope')
    if (state) url.searchParams.set('state', state)
    return c.redirect(url.toString())
  }

  const pkceError = validatePublicClientPkce(client, codeChallenge, codeChallengeMethod, redirectUri, state)
  if (pkceError) return pkceError

  // Issue authorization code
  const codePlain = generateOpaqueToken(32)
  const codeHash = await hashToken(codePlain)
  const expiresAt = new Date(Date.now() + CODE_TTL_SECONDS * 1000)
  await insertAuthorizationCode(db, {
    code: codeHash,
    oauthClientId: client.id,
    tenantId: session.tid!,
    userId: session.sub,
    redirectUri,
    scope: normalizedScopes.join(' '),
    codeChallenge: codeChallenge || null,
    codeChallengeMethod: codeChallengeMethod || null,
    expiresAt,
  })

  const url = new URL(redirectUri)
  url.searchParams.set('code', codePlain)
  if (state) url.searchParams.set('state', state)
  return c.redirect(url.toString())
})

export { authorizeRoute }
