import type { AtprotoDid } from '@atproto/did'
import type { HandleResolver, ResolvedHandle, ResolveHandleOptions } from '@atproto-labs/handle-resolver'
import {
	createPinnedIdentityFetcher,
	didWebToHttps,
	type IdentityDnsResolver,
	type PinnedIdentityTransport,
	readBoundedIdentityJson,
} from '@wispplace/atproto-utils'
import { logger } from './logger'

/** Maximum response accepted from the fixed Slingshot resolver. */
const MAX_RESOLVER_RESPONSE_BYTES = 64 * 1024
const RESOLVER_TIMEOUT_MS = 5_000
const PLC_DID_PATTERN = /^did:plc:[a-z2-7]{24}$/
const HANDLE_PATTERN = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i

type SlingshotOptions = {
	resolver?: IdentityDnsResolver
	transport?: PinnedIdentityTransport
}

function safeLogHandle(value: string): string {
	return value.replace(/[^\x20-\x7e]/g, '?').slice(0, 253)
}

function isValidHandle(value: string): boolean {
	return value.length <= 253 && HANDLE_PATTERN.test(value)
}

function isSupportedDid(value: string): boolean {
	if (PLC_DID_PATTERN.test(value)) return true
	try {
		didWebToHttps(value)
		return true
	} catch {
		return false
	}
}

/**
 * Custom HandleResolver that uses Slingshot's identity resolver service
 * to work around bugs in atproto-oauth-node when handles have redirects
 * in their well-known configuration.
 *
 * Every resolver request goes through the built-in DNS-pinned identity
 * transport. The resolver response is bounded before JSON parsing and the
 * returned DID is restricted to the DID methods this service supports.
 */
export class SlingshotHandleResolver implements HandleResolver {
	private readonly endpoint: string
	private readonly fetcher: ReturnType<typeof createPinnedIdentityFetcher>

	constructor(
		endpoint = process.env.OAUTH_HANDLE_RESOLVER_URL ??
			'https://slingshot.microcosm.blue/xrpc/com.atproto.identity.resolveHandle',
		options: SlingshotOptions = {},
	) {
		this.endpoint = endpoint
		this.fetcher = createPinnedIdentityFetcher({
			resolver: options.resolver,
			transport: options.transport,
			timeoutMs: RESOLVER_TIMEOUT_MS,
			maxResponseBytes: MAX_RESOLVER_RESPONSE_BYTES,
			allowLoopback: true,
		})
	}

	async resolve(handle: string, options?: ResolveHandleOptions): Promise<ResolvedHandle> {
		const safeHandle = handle.trim().toLowerCase()
		if (!isValidHandle(safeHandle)) return null
		const logHandle = safeLogHandle(safeHandle)

		try {
			const url = new URL(this.endpoint)
			url.searchParams.set('handle', safeHandle)

			// The identity fetcher owns its own five-second deadline and forwards
			// the OAuth caller's signal through its pinned transport.
			const response = await this.fetcher(url.toString(), options?.signal ? { signal: options.signal } : undefined)
			const data = await readBoundedIdentityJson<unknown>(response, MAX_RESOLVER_RESPONSE_BYTES, options?.signal)
			if (!data || typeof data !== 'object' || typeof (data as { did?: unknown }).did !== 'string') {
				logger.warn('[SlingshotHandleResolver] Resolver returned no DID', { handle: logHandle })
				return null
			}
			const did = (data as { did: string }).did
			if (!isSupportedDid(did)) {
				logger.warn('[SlingshotHandleResolver] Resolver returned an unsupported DID', { handle: logHandle })
				return null
			}

			logger.debug('[SlingshotHandleResolver] Handle resolved', { handle: logHandle })
			return did as AtprotoDid
		} catch (error) {
			if (options?.signal?.aborted) throw error
			if (error instanceof Error && error.name === 'AbortError') throw error
			// Do not turn a transient network failure into a cacheable “handle not
			// found” result. The OAuth client may safely cache authoritative nulls,
			// but a rejected lookup remains retryable on the next request.
			const transient =
				error instanceof Error &&
				(error.message === 'Identity request failed' ||
					error.message === 'Identity request timed out' ||
					typeof (error as Error & { code?: unknown }).code === 'string')
			logger.warn('[SlingshotHandleResolver] Handle resolution failed', { handle: logHandle, transient })
			if (transient) throw new Error('Handle resolver temporarily unavailable')
			return null
		}
	}
}
