import { createHash } from 'node:crypto'
import { Agent } from '@atproto/api'
import type { NodeOAuthClient } from '@atproto/oauth-client-node'
import { createLogger } from '@wispplace/observability'
import { isValidSiteId, siteIdFromHostname } from '@wispplace/private-sites'
import { Elysia } from 'elysia'
import { publishDomainCacheInvalidation } from '../lib/cache-invalidation'
import {
	claimCustomDomain,
	claimDomain,
	deleteCustomDomain,
	deleteWispDomain,
	getCustomDomainById,
	getCustomDomainInfo,
	getDomainByDid,
	isDomainAvailable,
	isDomainRegistered,
	updateCustomDomainRkey,
	updateCustomDomainVerification,
	updateDomain,
	updateWispDomainSite,
	waitForSiteCache,
} from '../lib/db'
import { verifyCustomDomain } from '../lib/dns-verify'
import { extractWispHandle, isValidHandle, normalizeDomain, toDomain, validateCustomDomain } from '../lib/domain-utils'
import { privateHostname } from '../lib/private-site-origin'
import { hasLivePrivateSite } from '../lib/private-sites-db'
import { requireAuth, SESSION_COOKIE_NAME } from '../lib/wisp-auth'

const logger = createLogger('main-app')

export const domainRoutes = (client: NodeOAuthClient, cookieSecret: string) =>
	new Elysia({
		prefix: '/api/domain',
		cookie: {
			secrets: cookieSecret,
			sign: [SESSION_COOKIE_NAME],
		},
	})
		// Public endpoints (no auth required)
		/**
		 * GET /api/domain/check
		 * Success: { available, domain } or { available: false, reason: 'invalid' }.
		 * Failure: { available: false }.
		 */
		.get('/check', async ({ query }) => {
			try {
				const handle = (query.handle || '').trim().toLowerCase()

				if (!isValidHandle(handle)) {
					return {
						available: false,
						reason: 'invalid',
					}
				}

				const available = await isDomainAvailable(handle)
				return {
					available,
					domain: toDomain(handle),
				}
			} catch (err) {
				logger.error('[Domain] Check error', err)
				return {
					available: false,
				}
			}
		})
		/**
		 * GET /api/domain/registered
		 * 200: public domain registration, or { registered: true, type: 'private', domain } for a live private site.
		 * 404: { registered: false }
		 * 400: { error: 'Domain parameter required' }
		 */
		.get('/registered', async ({ query, set }) => {
			try {
				const domain = normalizeDomain(String(query.domain || ''))

				if (!domain) {
					set.status = 400
					return { error: 'Domain parameter required' }
				}

				const configuredPrivateHost = privateHostname()
				const isPrivateDomain = domain === configuredPrivateHost || domain.endsWith(`.${configuredPrivateHost}`)

				if (isPrivateDomain) {
					// Private hostnames are reserved. Only one generated site-id label may
					// receive a certificate. The helper performs a direct, uncached primary
					// database lookup so readiness and expiry cannot use a lagging replica.
					const siteId = siteIdFromHostname(domain, configuredPrivateHost)
					if (!siteId || !isValidSiteId(siteId) || !(await hasLivePrivateSite(siteId))) {
						set.status = 404
						return { registered: false }
					}

					set.status = 200
					return { registered: true, type: 'private' as const, domain }
				}

				const result = await isDomainRegistered(domain)

				// For Caddy on-demand TLS: 200 = allow, 404 = deny
				if (result.registered) {
					set.status = 200
					return result
				} else {
					set.status = 404
					return { registered: false }
				}
			} catch (err) {
				logger.error('[Domain] Registered check error', err)
				set.status = 500
				return { error: 'Failed to check domain' }
			}
		})
		// Authenticated endpoints (require auth)
		.derive(async ({ cookie, request }) => {
			const auth = await requireAuth(client, cookie, request.headers.get('cookie'))
			return { auth }
		})
		/**
		 * POST /api/domain/claim
		 * Success: { success: true, domain }
		 */
		.post('/claim', async ({ body, auth, set }) => {
			try {
				const { handle } = body as { handle?: string }
				const normalizedHandle = (handle || '').trim().toLowerCase()

				if (!isValidHandle(normalizedHandle)) {
					set.status = 400
					throw new Error('Invalid handle')
				}

				// Check if user already has 3 domains (handled in claimDomain)
				// claim in DB
				let domain: string
				try {
					domain = await claimDomain(auth.did, normalizedHandle)
				} catch (err) {
					const message = err instanceof Error ? err.message : 'Unknown error'
					if (message === 'domain_limit_reached') {
						set.status = 400
						throw new Error('Domain limit reached: You can only claim up to 3 wisp.place domains')
					}
					set.status = 409
					throw new Error('Handle taken or error claiming domain')
				}

				// write place.wisp.domain record with unique rkey
				const agent = new Agent((url, init) => auth.session.fetchHandler(url, init))
				const rkey = normalizedHandle // Use handle as rkey for uniqueness
				await agent.com.atproto.repo.putRecord({
					repo: auth.did,
					collection: 'place.wisp.domain',
					rkey,
					record: {
						$type: 'place.wisp.domain',
						domain,
						createdAt: new Date().toISOString(),
					} as any,
					validate: false,
				})
				await publishDomainCacheInvalidation(domain, 'wisp')

				return { success: true, domain }
			} catch (err) {
				logger.error('[Domain] Claim error', err)
				throw new Error(`Failed to claim: ${err instanceof Error ? err.message : 'Unknown error'}`)
			}
		})
		/**
		 * POST /api/domain/update
		 * Success: { success: true, domain }
		 */
		.post('/update', async ({ body, auth, set }) => {
			try {
				const { handle } = body as { handle?: string }
				const normalizedHandle = (handle || '').trim().toLowerCase()

				if (!isValidHandle(normalizedHandle)) {
					set.status = 400
					throw new Error('Invalid handle')
				}

				const desiredDomain = toDomain(normalizedHandle)
				const current = await getDomainByDid(auth.did)

				if (current === desiredDomain) {
					return { success: true, domain: current }
				}

				let domain: string
				try {
					domain = await updateDomain(auth.did, normalizedHandle)
				} catch (_err) {
					set.status = 409
					throw new Error('Handle taken')
				}

				const agent = new Agent((url, init) => auth.session.fetchHandler(url, init))
				await agent.com.atproto.repo.putRecord({
					repo: auth.did,
					collection: 'place.wisp.domain',
					rkey: 'self',
					record: {
						$type: 'place.wisp.domain',
						domain,
						createdAt: new Date().toISOString(),
					} as any,
					validate: false,
				})
				if (current) await publishDomainCacheInvalidation(current, 'wisp')
				await publishDomainCacheInvalidation(domain, 'wisp')

				return { success: true, domain }
			} catch (err) {
				logger.error('[Domain] Update error', err)
				throw new Error(`Failed to update: ${err instanceof Error ? err.message : 'Unknown error'}`)
			}
		})
		/**
		 * POST /api/domain/custom/add
		 * Success: { success: true, id, domain, verified: false }
		 */
		.post('/custom/add', async ({ body, auth, set }) => {
			try {
				const { domain } = body as { domain: string }
				const domainLower = normalizeDomain(domain || '')

				const domainError = validateCustomDomain(domainLower)
				if (domainError) {
					set.status = 400
					throw new Error(`Invalid domain: ${domainError}`)
				}

				// Verified claims are DID-locked. Pending claims can be reclaimed.
				const existing = await getCustomDomainInfo(domainLower)
				if (existing?.verified && existing.did !== auth.did) {
					set.status = 409
					throw new Error('Domain already claimed')
				}

				if (existing && existing.did === auth.did) {
					await publishDomainCacheInvalidation(domainLower, 'custom', existing.id)
					return {
						success: true,
						id: existing.id,
						domain: domainLower,
						verified: Boolean(existing.verified),
					}
				}

				// Create hash for ID
				const hash = createHash('sha256').update(`${auth.did}:${domainLower}`).digest('hex').substring(0, 16)

				// Store in database only
				await claimCustomDomain(auth.did, domainLower, hash)
				await publishDomainCacheInvalidation(domainLower, 'custom', hash)

				return {
					success: true,
					id: hash,
					domain: domainLower,
					verified: false,
				}
			} catch (err) {
				logger.error('[Domain] Custom domain add error', err)
				throw new Error(`Failed to add domain: ${err instanceof Error ? err.message : 'Unknown error'}`)
			}
		})
		/**
		 * POST /api/domain/custom/verify
		 * Success: { success: true, verified, error, found }
		 */
		.post('/custom/verify', async ({ body, auth, set }) => {
			try {
				const { id } = body as { id: string }

				// Get domain from database
				const domainInfo = await getCustomDomainById(id)
				if (!domainInfo) {
					set.status = 404
					throw new Error('Domain not found')
				}

				// Verify DNS records (TXT + CNAME)
				logger.debug(`[Domain] Verifying custom domain: ${domainInfo.domain}`)
				const result = await verifyCustomDomain(domainInfo.domain, auth.did, id)

				// Update verification status in database
				await updateCustomDomainVerification(id, result.verified)
				await publishDomainCacheInvalidation(domainInfo.domain, 'custom', id)

				return {
					success: true,
					verified: result.verified,
					error: result.error,
					warning: result.warning,
					found: result.found,
				}
			} catch (err) {
				logger.error('[Domain] Custom domain verify error', err)
				throw new Error(`Failed to verify domain: ${err instanceof Error ? err.message : 'Unknown error'}`)
			}
		})
		/**
		 * DELETE /api/domain/custom/:id
		 * Success: { success: true }
		 */
		.delete('/custom/:id', async ({ params, auth, set }) => {
			try {
				const { id } = params

				// Verify ownership before deleting
				const domainInfo = await getCustomDomainById(id)
				if (!domainInfo) {
					set.status = 404
					throw new Error('Domain not found')
				}

				if (domainInfo.did !== auth.did) {
					set.status = 403
					throw new Error('Unauthorized: You do not own this domain')
				}

				// Delete from database
				await deleteCustomDomain(id)
				await publishDomainCacheInvalidation(domainInfo.domain, 'custom', id)

				return { success: true }
			} catch (err) {
				logger.error('[Domain] Custom domain delete error', err)
				throw new Error(`Failed to delete domain: ${err instanceof Error ? err.message : 'Unknown error'}`)
			}
		})
		/**
		 * POST /api/domain/wisp/map-site
		 * Success: { success: true }
		 */
		.post('/wisp/map-site', async ({ body, auth, set }) => {
			try {
				const { domain, siteRkey } = body as { domain: string; siteRkey: string | null }

				if (!domain) {
					set.status = 400
					throw new Error('Domain parameter required')
				}

				// Verify domain belongs to user
				const domainLower = normalizeDomain(domain)
				const info = await isDomainRegistered(domainLower)

				if (!info.registered || info.type !== 'wisp') {
					set.status = 404
					throw new Error('Domain not found')
				}

				if (info.did !== auth.did) {
					set.status = 403
					throw new Error('Unauthorized: You do not own this domain')
				}

				if (siteRkey && !(await waitForSiteCache(auth.did, siteRkey))) {
					set.status = 503
					throw new Error('Site is still being processed. Please try again in a moment.')
				}

				// Update wisp.place domain to point to this site
				await updateWispDomainSite(domainLower, siteRkey)
				await publishDomainCacheInvalidation(domainLower, 'wisp')

				return { success: true }
			} catch (err) {
				logger.error('[Domain] Wisp domain map error', err)
				throw new Error(`Failed to map site: ${err instanceof Error ? err.message : 'Unknown error'}`)
			}
		})
		/**
		 * DELETE /api/domain/wisp/:domain
		 * Success: { success: true }
		 */
		.delete('/wisp/:domain', async ({ params, auth, set }) => {
			try {
				const { domain } = params

				// Verify domain belongs to user
				const domainLower = normalizeDomain(domain)
				const info = await isDomainRegistered(domainLower)

				if (!info.registered || info.type !== 'wisp') {
					set.status = 404
					throw new Error('Domain not found')
				}

				if (info.did !== auth.did) {
					set.status = 403
					throw new Error('Unauthorized: You do not own this domain')
				}

				// Delete from database
				await deleteWispDomain(domainLower)
				await publishDomainCacheInvalidation(domainLower, 'wisp')

				// Delete from PDS
				const agent = new Agent((url, init) => auth.session.fetchHandler(url, init))
				const handle = extractWispHandle(domainLower)
				if (!handle) {
					set.status = 400
					throw new Error('Invalid wisp domain')
				}
				try {
					await agent.com.atproto.repo.deleteRecord({
						repo: auth.did,
						collection: 'place.wisp.domain',
						rkey: handle,
					})
				} catch (err) {
					// Record might not exist in PDS, continue anyway
					logger.warn('[Domain] Could not delete wisp domain from PDS', err as any)
				}

				return { success: true }
			} catch (err) {
				logger.error('[Domain] Wisp domain delete error', err)
				throw new Error(`Failed to delete domain: ${err instanceof Error ? err.message : 'Unknown error'}`)
			}
		})
		/**
		 * POST /api/domain/custom/:id/map-site
		 * Success: { success: true }
		 */
		.post('/custom/:id/map-site', async ({ params, body, auth, set }) => {
			try {
				const { id } = params
				const { siteRkey } = body as { siteRkey: string | null }

				// Verify ownership before updating
				const domainInfo = await getCustomDomainById(id)
				if (!domainInfo) {
					set.status = 404
					throw new Error('Domain not found')
				}

				if (domainInfo.did !== auth.did) {
					set.status = 403
					throw new Error('Unauthorized: You do not own this domain')
				}

				if (siteRkey && !(await waitForSiteCache(auth.did, siteRkey))) {
					set.status = 503
					throw new Error('Site is still being processed. Please try again in a moment.')
				}

				// Update custom domain to point to this site
				await updateCustomDomainRkey(id, siteRkey)
				await publishDomainCacheInvalidation(domainInfo.domain, 'custom', id)

				return { success: true }
			} catch (err) {
				logger.error('[Domain] Custom domain map error', err)
				throw new Error(`Failed to map site: ${err instanceof Error ? err.message : 'Unknown error'}`)
			}
		})
