import {
	createContext,
	type Message,
	type StepTracer,
	type Logger,
	type TestAgent,
	type TestVoiceAgent
} from '@recombine-ai/engine'
import { getAgentFactories } from '../lib/get-agent-factories'
import { getRootPath } from '../lib/get-root-path'

export async function loadAgents(props: {
	agentId: string
	getMessages: () => Message[]
	sendMessage: (message: Message | string) => Promise<void>
	stepTracer: StepTracer
	logger?: Logger
	context?: Record<string, any>
	timeoutSeconds: number
	abortSignal?: AbortSignal
}): Promise<void> {
	const {
		agentId,
		getMessages,
		sendMessage,
		stepTracer: stepTracer,
		logger: providedLogger,
		context,
		timeoutSeconds,
		abortSignal
	} = props
	const { factories } = await getAgentFactories(await getRootPath())
	const factory = factories[agentId]
	if (!factory) throw new Error('Factory not found')

	// Minimal stubs
	const logger = providedLogger || {
		log: () => {},
		debug: () => {},
		error: () => {},
		warn: () => {}
	}
	const scheduler = {
		clearAllPendingActions: async () => {},
		registerAction: () => async () => {},
		triggerAction: async () => {},
		serialize: () => ({}) as any,
		setState: (_: any) => {}
	}
	const ctx = createContext({})

	// Maintain in-run history so sendMessage appends to it, mirroring backend behavior
	const history = getMessages()
	const getMessagesWithHistory = () => history
	const sendMessageWithHistory = async (message: Message | string) => {
		const m =
			typeof message === 'string' ? ({ sender: 'agent', text: message } as Message) : message
		try {
			history.push(m)
		} catch {
			// Best-effort: if history is immutable, ignore push errors
			logger.log('Failed to push message to history:', m)
		}
		await sendMessage(m)
	}

	const stepRegistry = {
		addStep: () => {}
	}

	const agent = await factory({
		stepTracer: stepTracer,
		stepRegistry,
		tracer: stepRegistry,
		logger: logger,
		scheduler: scheduler as any,
		getMessages: getMessagesWithHistory,
		sendMessage: sendMessageWithHistory,
		sendAction: async () => {},
		ctx
	})

	if (context && typeof context === 'object') {
		// Apply provided context before agent lifecycle
		ctx.swap(context)
	}

	process.on('unhandledRejection', (r) => {
		console.error('UNHANDLED', r)
	})

	const withAbort = async <T>(p: Promise<T>): Promise<T> => {
		if (!abortSignal) return p
		if (abortSignal.aborted) throw new Error('Operation cancelled')
		return await new Promise<T>((resolve, reject) => {
			const onAbort = () => {
				cleanup()
				reject(new Error('Operation cancelled'))
			}
			const cleanup = () => {
				try {
					abortSignal.removeEventListener('abort', onAbort)
				} catch {
					/* noop */
				}
			}
			try {
				abortSignal.addEventListener('abort', onAbort, { once: true })
			} catch {
				/* noop */
			}
			p.then(
				(v) => {
					cleanup()
					resolve(v)
				},
				(e) => {
					cleanup()
					reject(e)
				}
			)
		})
	}

	try {
		if (isVoiceAgent(agent)) {
			await withAbort(agent.streamResponse())
		} else {
			await withAbort(agent.respondToMessage())
		}
		await withAbort(new Promise((resolve) => setTimeout(resolve, timeoutSeconds * 1000)))
	} catch (err) {
		const msg = err instanceof Error ? `${err.name}: ${err.message}` : String(err)
		const stack = err instanceof Error ? err.stack : undefined
		try {
			logger.error('Error in agent.respondToMessage():', msg)
		} catch {
			/* noop */
		}
		if (stack) {
			try {
				logger.error(stack)
			} catch {
				/* noop */
			}
		}
	}
}

function isVoiceAgent(agent: TestAgent): agent is TestVoiceAgent {
	return 'streamResponse' in agent
}
