import type { NodeOAuthClient } from '@atproto/oauth-client-node'
import { createLogger } from '@wispplace/observability'
import { Elysia } from 'elysia'
import { eventualRead } from '../lib/db'
import {
	authorizeWisp,
	authorizeWispLegacy,
	isLegacyScopeState,
	missingGrantedCapabilities,
	unmarkLegacyScopeState,
} from '../lib/oauth-authorize'
import { authenticateRequest, invalidateSessionCache, SESSION_COOKIE_NAME } from '../lib/wisp-auth'
import { resolvePrivateShareState } from './private-redeem'

const logger = createLogger('main-app')

export const authRoutes = (client: NodeOAuthClient, cookieSecret: string) =>
	new Elysia({
		cookie: {
			secrets: cookieSecret,
			sign: [SESSION_COOKIE_NAME],
		},
	})
		/**
		 * GET /api/auth/login
		 * 302 redirect to the AT Protocol OAuth authorize URL.
		 * On error, redirects to /?error=missing_handle or /?error=auth_failed.
		 */
		.get('/api/auth/login', async (c) => {
			// GET endpoint for initiating OAuth via atproto.wisp.place entryway
			// Accepts: login_hint (handle) or pds (server)
			try {
				const query = c.query as { login_hint?: string; pds?: string }
				const handle = query.login_hint || ''
				const pds = query.pds || ''

				// Use login_hint if provided, otherwise use PDS URL
				const identifier = handle || (pds ? `https://${pds}` : '')

				if (!identifier) {
					logger.error('Login attempt with no login_hint or pds')
					return c.redirect('/?error=missing_handle')
				}

				logger.info('Login attempt via entryway', { identifier })
				const state = crypto.randomUUID()
				const url = await authorizeWisp(client, identifier, { state })
				logger.info('Authorization URL generated', { identifier })

				// Redirect to the OAuth authorization URL
				return c.redirect(url.toString())
			} catch (err) {
				logger.error('Login error', err)
				return c.redirect('/?error=auth_failed')
			}
		})
		/**
		 * POST /api/auth/signin
		 * Success: { url } where url is the OAuth authorize URL.
		 * Failure: { error, details }.
		 */
		.post('/api/auth/signin', async (c) => {
			let handle = 'unknown'
			try {
				const body = c.body as { handle: string }
				handle = body.handle
				logger.info('Sign-in attempt', { handle })
				const state = crypto.randomUUID()
				const url = await authorizeWisp(client, handle, { state })
				logger.info('Authorization URL generated', { handle })
				return { url: url.toString() }
			} catch (err) {
				logger.error('Signin error', err, { handle })
				c.set.status = 401
				return { error: 'Authentication failed', details: 'Unable to start authentication' }
			}
		})
		/**
		 * GET /api/auth/callback
		 * 302 redirect to /onboarding (new users) or /editor (existing users).
		 * On error, redirects to /?error=auth_failed.
		 */
		.get('/api/auth/callback', async (c) => {
			try {
				const params = new URLSearchParams(c.query)

				// client.callback() validates the state parameter internally
				// It will throw an error if state validation fails (CSRF protection)
				const { session, state } = await client.callback(params)

				if (!session) {
					logger.error('[Auth] OAuth callback failed: no session returned')
					c.cookie[SESSION_COOKIE_NAME].remove()
					return c.redirect('/?error=auth_failed')
				}

				// A new grant supersedes whatever this node last restored for the DID.
				invalidateSessionCache(session.did)

				const cookieSession = c.cookie
				cookieSession[SESSION_COOKIE_NAME].set({
					value: session.did,
					httpOnly: true,
					secure: process.env.NODE_ENV === 'production',
					sameSite: 'lax',
					path: '/',
					maxAge: 30 * 24 * 60 * 60, // 30 days
				})

				// An authorization server that predates permission sets accepts the
				// `include:place.wisp.*` values and then drops them, leaving a session
				// that can not write records. Retry once with the granular expansion.
				const missing = await missingGrantedCapabilities(session)
				if (missing.length > 0) {
					if (!isLegacyScopeState(state)) {
						logger.warn('[Auth] Permission sets were not granted, retrying with granular scopes', {
							did: session.did,
							missing,
						})
						const retryUrl = await authorizeWispLegacy(client, session.did, state)
						return c.redirect(retryUrl.toString())
					}
					logger.error('[Auth] Session is missing required permissions', { did: session.did, missing })
				}

				// Revalidate the OAuth state token before returning a share visitor to its site.
				const redeem = await resolvePrivateShareState(unmarkLegacyScopeState(state), session.did)
				if (redeem) {
					return c.redirect(redeem.url ?? '/private/denied')
				}

				// Which page to land on is presentation, not authorization, so it reads
				// the local replica. Both lookups used to go to the primary, which from
				// a distant region cost two more round trips on the sign-in path than
				// the choice of redirect is worth.
				const { sites, domain } = await eventualRead.getUserStatus(session.did)

				// If no sites and no domain, redirect to onboarding
				if (sites.length === 0 && !domain) {
					return c.redirect('/onboarding')
				}

				return c.redirect('/editor')
			} catch (err) {
				// This catches state validation failures and other OAuth errors
				logger.error('[Auth] OAuth callback error', err)
				c.cookie[SESSION_COOKIE_NAME].remove()
				return c.redirect('/?error=auth_failed')
			}
		})
		/**
		 * POST /api/auth/logout
		 * Success: { success: true }
		 * Failure: { error: 'Logout failed' }
		 */
		.post('/api/auth/logout', async (c) => {
			try {
				const cookieSession = c.cookie
				const did = cookieSession[SESSION_COOKIE_NAME]?.value

				// Clear the session cookie
				cookieSession[SESSION_COOKIE_NAME].remove()

				// If we have a DID, try to revoke the OAuth session
				if (did && typeof did === 'string') {
					// Drop the cached session first: a concurrent request must not be
					// handed a session this logout is about to revoke.
					invalidateSessionCache(did)
					try {
						await client.revoke(did)
						logger.debug('[Auth] Revoked OAuth session for', did as any)
					} catch (err) {
						logger.error('[Auth] Failed to revoke session', err)
						// Continue with logout even if revoke fails
					}
				}

				return { success: true }
			} catch (err) {
				logger.error('[Auth] Logout error', err)
				c.set.status = 500
				return { error: 'Logout failed' }
			}
		})
		/**
		 * GET /api/auth/status
		 * Authenticated: { authenticated: true, did }
		 * Not authenticated: { authenticated: false }
		 */
		.get('/api/auth/status', async (c) => {
			try {
				const auth = await authenticateRequest(client, c.cookie, c.request.headers.get('cookie'))

				if (!auth) {
					c.cookie[SESSION_COOKIE_NAME].remove()
					return { authenticated: false }
				}

				return {
					authenticated: true,
					did: auth.did,
				}
			} catch (err) {
				logger.error('[Auth] Status check error', err)
				c.cookie[SESSION_COOKIE_NAME].remove()
				return { authenticated: false }
			}
		})
