/**
 * POST /api/auth/forgot-password & /api/auth/reset-password — Task 12.
 *
 *  - forgot-password: ALWAYS 204 (no account enumeration). If the email exists,
 *    emails a 1h signed reset token. Rate limited 3/hour/email.
 *  - reset-password: verifies the signed token, updates password_hash, and
 *    bumpUserVersion (kills all live sessions). 400 if expired/used.
 *
 * Public routes; Origin checked inline.
 */
import { Hono, type Context } from 'hono'
import {
  hashPassword,
  signSignedToken,
  verifySignedToken,
  consumeSignedTokenJti,
  PASSWORD_RESET_JTI_PREFIX,
} from '@zync/auth'
import { createDb, findUserByEmail, findUserById, resetUserPassword } from '@zync/db/queries'
import type { UserId } from '@zync/types'
import type { AppEnv } from '../../types'
import { forgotPasswordSchema, resetPasswordSchema } from '../../schemas/auth'
import { sendPasswordResetEmail } from '../../adapters/email'
import { issueSessionForUser } from '../../lib/issue-session'
import { bumpUserVersion } from '../../middleware/user-version'
import { appOriginForRequest, getAppOrigins } from '../../lib/origins'
import { withDoHash } from '../../lib/password-hash-do'
const RESET_TTL_SECONDS = 60 * 60 // 1h
const FORGOT_LIMIT_PER_HOUR = 3

function checkOrigin(
  originHeader: string | undefined,
  environment: string | undefined,
): boolean {
  const allowedOrigins = getAppOrigins({ environment })
  return !!originHeader && allowedOrigins.has(originHeader)
}

export const passwordRoute = new Hono<AppEnv>()

async function handleResetRequest(c: Context<AppEnv>) {
  if (!checkOrigin(c.req.header('Origin'), (c.env as { ENVIRONMENT?: string }).ENVIRONMENT)) {
    return c.json({ error: 'Forbidden: invalid Origin' }, 403)
  }
  const parsed = forgotPasswordSchema.safeParse(await c.req.json())
  if (!parsed.success) {
    // Still respond 204 to avoid signalling input shape; but malformed JSON =>
    // 400 is acceptable since it carries no account information.
    return c.json({ error: 'Invalid request' }, 400)
  }
  const { email } = parsed.data

  // Per-email rate limit: 3/hour. KV counter with 1h TTL.
  const rlKey = `forgot_pw:${email.toLowerCase()}`
  const countRaw = await c.env.KV.get(rlKey)
  const count = countRaw ? Number(countRaw) : 0
  if (count >= FORGOT_LIMIT_PER_HOUR) {
    return c.body(null, 204) // silently drop (still no enumeration)
  }
  await c.env.KV.put(rlKey, String(count + 1), { expirationTtl: 60 * 60 })

  const db = createDb(c.env)
  const user = await findUserByEmail(db, email)
  if (user) {
    const token = await signSignedToken(
      { sub: user.id, purpose: 'password_reset' },
      c.env.JWT_SECRET,
      RESET_TTL_SECONDS,
    )
    await sendPasswordResetEmail(c.env, email, token, user.id, appOriginForRequest(c.req.url))
  }

  return c.json({ ok: true }, 200)
}

passwordRoute.post('/forgot-password', handleResetRequest)
passwordRoute.post('/reset-password', handleResetRequest)

passwordRoute.post('/reset-password/confirm', async (c) => {
  if (!checkOrigin(c.req.header('Origin'), (c.env as { ENVIRONMENT?: string }).ENVIRONMENT)) {
    return c.json({ error: 'Forbidden: invalid Origin' }, 403)
  }

  const ip = c.req.header('CF-Connecting-IP') ?? 'unknown'
  const rl = await (async () => { try { const _r = await c.env.RATE_LIMITER_AUTH?.limit({ key: `reset_pw:${ip}` }); return _r ?? { success: true }; } catch { return { success: true }; } })()
  if (!rl.success) {
    return c.json({ error: 'Too many requests' }, 429)
  }

  const parsed = resetPasswordSchema.safeParse(await c.req.json())
  if (!parsed.success) {
    return c.json({ error: 'Invalid request' }, 400)
  }
  const { token, password } = parsed.data

  let claims: Record<string, unknown>
  try {
    claims = await verifySignedToken(token, c.env.JWT_SECRET)
  } catch (error) {
    if (
      error instanceof Error &&
      (error.name === 'JWTExpired' || error.message.toLowerCase().includes('exp'))
    ) {
      return c.json({ error: 'token_expired' }, 410)
    }
    return c.json({ error: 'invalid_token' }, 400)
  }
  if (claims.purpose !== 'password_reset' || typeof claims.sub !== 'string') {
    return c.json({ error: 'invalid_token' }, 400)
  }
  if (typeof claims.jti !== 'string' || !claims.jti) {
    return c.json({ error: 'invalid_token' }, 400)
  }
  const userId = claims.sub as UserId

  const exp = typeof claims.exp === 'number' ? claims.exp : Math.floor(Date.now() / 1000) + RESET_TTL_SECONDS
  const jtiTtl = Math.max(1, exp - Math.floor(Date.now() / 1000))
  const consumed = await consumeSignedTokenJti(c.env.KV, PASSWORD_RESET_JTI_PREFIX, claims.jti, jtiTtl)
  if (!consumed) {
    return c.json({ error: 'token_expired' }, 410)
  }

  const db = createDb(c.env)
  const user = await findUserById(db, userId)
  if (!user) return c.json({ error: 'invalid_token' }, 400)

  const passwordHash = await hashPassword(password, withDoHash(c.env))
  await resetUserPassword(db, userId, passwordHash)

  // Invalidate all live sessions for this user.
  await bumpUserVersion(c.env, userId)

  const issued = await issueSessionForUser(c, db, userId)
  if (!issued) {
    return c.json({ error: 'invalid_token' }, 400)
  }

  return c.json({ ok: true }, 200)
})
