import {
  ExchangeCodeSchema,
  RequestMagicLinkSchema,
  UpdateHandleSchema,
  VerifyTokenSchema,
} from '@crumbs/core/schemas'
import {eq} from 'drizzle-orm'
import {Hono} from 'hono'
import {HTTPException} from 'hono/http-exception'
import {secureHeaders} from 'hono/secure-headers'

import {auth, requireUser, type AuthVars} from '../auth/middleware'
import {createSession, deleteSession} from '../auth/session'
import {
  consumeExchangeCode,
  consumeMagicToken,
  issueExchangeCode,
  issueMagicToken,
  peekMagicToken,
} from '../auth/tokens'
import {toPublicUser, upsertUserByEmail} from '../auth/users'
import {db, schema} from '../db/client'
import {env} from '../env'
import {
  confirmPage,
  expiredPage,
  REDIRECT_SCRIPT_CSP_HASH,
  successPage,
} from '../lib/auth-pages'
import {avatarKey, processAvatar} from '../lib/avatar'
import {
  removeReplacedImage,
  storeUploadedImage,
} from '../lib/blob-image'
import {devNetworkGuard} from '../lib/dev-network'
import {sendMagicLinkEmail} from '../lib/email'
import {
  authExchangeLimiter,
  authRequestLimiter,
  authVerifyLimiter,
  avatarLimiter,
  consumeEmailQuota,
} from '../lib/rate-limit'
import {rethrowUniqueAs} from '../lib/sql'
import {validate} from '../lib/validate'

const callbackUrl = (params: Record<string, string>) => {
  const qs = new URLSearchParams(params).toString()
  return `${env.APP_SCHEME}://auth/callback?${qs}`
}

const MAX_AVATAR_UPLOAD = 8 * 1024 * 1024

// the signed-in profile endpoints share one session guard stack
const profile = new Hono<{Variables: AuthVars}>()
  .use('*', devNetworkGuard, auth, requireUser)
  .get('/me', (c) => {
    const user = c.get('user')
    return c.json({user: toPublicUser(user)})
  })
  .patch(
    '/handle',
    validate('json', UpdateHandleSchema),
    async (c) => {
      const user = c.get('user')
      const {handle} = c.req.valid('json')
      const [updated] = await db
        .update(schema.users)
        .set({handle, updatedAt: new Date()})
        .where(eq(schema.users.id, user.id))
        .returning()
        .catch(rethrowUniqueAs('handle_taken'))
      return c.json({user: toPublicUser(updated!)})
    },
  )
  .put('/avatar', avatarLimiter, async (c) => {
    const user = c.get('user')
    const ref = await storeUploadedImage(c.req, {
      max: MAX_AVATAR_UPLOAD,
      process: processAvatar,
      key: (hash) => avatarKey(user.id, hash),
    })

    const previousRef = user.avatarRef
    const [updated] = await db
      .update(schema.users)
      .set({avatarRef: ref, updatedAt: new Date()})
      .where(eq(schema.users.id, user.id))
      .returning()

    await removeReplacedImage(previousRef, ref)
    return c.json({user: toPublicUser(updated!)})
  })
  .delete('/avatar', async (c) => {
    const user = c.get('user')
    const previousRef = user.avatarRef
    const [updated] = await db
      .update(schema.users)
      .set({avatarRef: null, updatedAt: new Date()})
      .where(eq(schema.users.id, user.id))
      .returning()

    await removeReplacedImage(previousRef, null)
    return c.json({user: toPublicUser(updated!)})
  })

const authRoutes = new Hono<{Variables: AuthVars}>()
  .use(
    '/verify',
    secureHeaders({
      contentSecurityPolicy: {
        defaultSrc: ["'none'"],
        styleSrc: ["'unsafe-inline'"],
        scriptSrc: [REDIRECT_SCRIPT_CSP_HASH],
        formAction: ["'self'"],
        baseUri: ["'none'"],
        frameAncestors: ["'none'"],
      },
    }),
  )
  .post(
    '/request',
    authRequestLimiter,
    validate('json', RequestMagicLinkSchema),
    async (c) => {
      const {email} = c.req.valid('json')
      if (!(await consumeEmailQuota(email)))
        throw new HTTPException(429, {message: 'too_many_requests'})
      const token = await issueMagicToken(email)
      const url = `${env.PUBLIC_API_URL}/auth/verify?token=${encodeURIComponent(token)}`
      await sendMagicLinkEmail({to: email, url})
      if (env.NODE_ENV === 'development')
        return c.json({ok: true, devVerifyUrl: url})
      return c.json({ok: true})
    },
  )
  // token is consumed on POST only — GET must stay side-effect free so
  // prefetchers (safari preload, email scanners) can't burn the link
  .get('/verify', async (c) => {
    c.header('Cache-Control', 'no-store')
    const parsed = VerifyTokenSchema.safeParse({
      token: c.req.query('token'),
    })
    if (!parsed.success) return c.html(expiredPage(), 400)
    if (!(await peekMagicToken(parsed.data.token)))
      return c.html(expiredPage(), 400)
    return c.html(confirmPage(parsed.data.token))
  })
  .post('/verify', authVerifyLimiter, async (c) => {
    c.header('Cache-Control', 'no-store')
    const body = await c.req.parseBody()
    const parsed = VerifyTokenSchema.safeParse({token: body.token})
    if (!parsed.success) return c.html(expiredPage(), 400)
    const email = await consumeMagicToken(parsed.data.token)
    if (!email) return c.html(expiredPage(), 400)
    const user = await upsertUserByEmail(email)
    const session = await createSession(user.id)
    const code = await issueExchangeCode(session)
    return c.html(successPage(callbackUrl({code})))
  })
  .post(
    '/exchange',
    authExchangeLimiter,
    validate('json', ExchangeCodeSchema),
    async (c) => {
      const {code} = c.req.valid('json')
      const token = await consumeExchangeCode(code)
      if (!token)
        throw new HTTPException(401, {message: 'invalid_code'})
      return c.json({token})
    },
  )
  .post('/logout', devNetworkGuard, auth, async (c) => {
    const token = c.get('sessionToken')
    if (token) await deleteSession(token)
    return c.json({ok: true})
  })
  .route('/', profile)

export default authRoutes
