import { context, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
import {
	KeyValueStorageService,
	StreamStorageService,
	VectorStorageService,
	QueueStorageService,
	EmailStorageService,
	ScheduleService,
	TaskStorageService,
	type FetchAdapter,
	type KeyValueStorage,
	type StreamStorage,
	type VectorStorage,
	type SandboxService,
	type QueueService,
	type EmailService,
	type TaskStorage,
	type ListStreamsResponse,
	type VectorUpsertResult,
	type VectorSearchResult,
	type Logger,
	type SessionEventProvider,
	type EvalRunEventProvider,
	StructuredError,
} from '@agentuity/core';
import { APIClient, createServerFetchAdapter, getServiceUrls } from '@agentuity/server';
import { HTTPSandboxService } from './services/sandbox';
import {
	CompositeSessionEventProvider,
	LocalSessionEventProvider,
	JSONSessionEventProvider,
	HTTPSessionEventProvider,
} from './services/session';
import {
	CompositeEvalRunEventProvider,
	LocalEvalRunEventProvider,
	JSONEvalRunEventProvider,
	HTTPEvalRunEventProvider,
} from './services/evalrun';
import { LocalThreadProvider } from './services/thread/local';
import { injectTraceContextToHeaders } from './otel/http';
import { getTracer } from './_server';
import { populateAgentsRegistry } from './agent.js';
import { getSDKVersion, isAuthenticated, isProduction } from './_config';
import type { AppConfig } from './app';
import { localServicesLogged } from './_globals';
import {
	DefaultSessionProvider,
	DefaultThreadProvider,
	type ThreadProvider,
	type SessionProvider,
} from './session';
import {
	LocalKeyValueStorage,
	LocalStreamStorage,
	LocalVectorStorage,
	LocalQueueStorage,
	LocalEmailStorage,
	LocalTaskStorage,
	getLocalDB,
	normalizeProjectPath,
	createLocalStorageRouter,
} from './services/local';

const userAgent = `Agentuity SDK/${getSDKVersion()}`;

// Lazy getters - these must be functions to read env vars AFTER bootstrapRuntimeEnv() runs
const getSdkKey = () => process.env.AGENTUITY_SDK_KEY;
const getBearerKey = () => `Bearer ${getSdkKey()}`;
const getRegion = () => process.env.AGENTUITY_REGION ?? 'usc';
const getLazyServiceUrls = () => getServiceUrls(getRegion());
const getKvBaseUrl = () => getLazyServiceUrls().keyvalue;
const getStreamBaseUrl = () => getLazyServiceUrls().stream;
const getVectorBaseUrl = () => getLazyServiceUrls().vector;
const getCatalystBaseUrl = () => getLazyServiceUrls().catalyst;
const getQueueBaseUrl = () => getCatalystBaseUrl();
const getEmailBaseUrl = () => getLazyServiceUrls().email;
const getTaskBaseUrl = () => getCatalystBaseUrl();

let adapter: FetchAdapter;

const createFetchAdapter = (logger: Logger) =>
	createServerFetchAdapter(
		{
			headers: {
				Authorization: getBearerKey(),
				'User-Agent': userAgent,
			},
			onBefore: async (url, options, callback) => {
				logger.debug('before request: %s with options: %s', url, options);
				if (!options.telemetry) {
					return callback();
				}
				options.headers = { ...options.headers, ...injectTraceContextToHeaders() };
				const tracer = getTracer() ?? trace.getTracer('agentuity');
				const currentContext = context.active();
				const span = tracer.startSpan(
					options.telemetry.name,
					{ attributes: options.telemetry.attributes, kind: SpanKind.CLIENT },
					currentContext
				);
				const spanContext = trace.setSpan(currentContext, span);
				try {
					await context.with(spanContext, callback);
					span.setStatus({ code: SpanStatusCode.OK });
				} catch (err) {
					const e = err as Error;
					span.recordException(e);
					span.setStatus({ code: SpanStatusCode.ERROR, message: e?.message ?? String(err) });
					throw err;
				} finally {
					span.end();
				}
			},
			onAfter: async (url, options, result, err) => {
				logger.debug('after request: %s (%d) => %s', url, result.response.status, err);
				if (err) {
					return;
				}
				const span = trace.getSpan(context.active());
				switch (options.telemetry?.name) {
					case 'agentuity.keyvalue.get': {
						if (result.response.status === 404) {
							span?.addEvent('miss');
						} else if (result.response.ok) {
							span?.addEvent('hit');
						}
						break;
					}
					case 'agentuity.stream.create': {
						if (result.response.ok) {
							const res = result.data as { id: string };
							span?.setAttributes({
								'stream.id': res.id,
								'stream.url': `${getStreamBaseUrl()}/${res.id}`,
							});
						}
						break;
					}
					case 'agentuity.stream.list': {
						const response = result.data as ListStreamsResponse;
						if (response && span) {
							span.setAttributes({
								'stream.count': response.streams.length,
								'stream.total': response.total,
							});
						}
						break;
					}
					case 'agentuity.vector.upsert': {
						if (result.response.ok) {
							const data = result.data as VectorUpsertResult[];
							span?.setAttributes({
								'vector.count': data.length,
							});
						}
						break;
					}
					case 'agentuity.vector.search': {
						if (result.response.ok) {
							const data = result.data as VectorSearchResult[];
							span?.setAttributes({
								'vector.results': data.length,
							});
						}
						break;
					}
					case 'agentuity.vector.get': {
						if (result.response.status === 404) {
							span?.addEvent('miss');
						} else if (result.response.ok) {
							span?.addEvent('hit');
						}
						break;
					}
				}
			},
		},
		logger
	);

let kv: KeyValueStorage;
let stream: StreamStorage;
let vector: VectorStorage;
let sandbox: SandboxService;
let queue: QueueService;
let email: EmailService;
let schedule: ScheduleService;
let task: TaskStorage;
let session: SessionProvider;
let thread: ThreadProvider;
let sessionEvent: SessionEventProvider;
let evalRunEvent: EvalRunEventProvider;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
let localRouter: any | null = null;

const ServerUrlMissingError = StructuredError(
	'ServerUrlMissingError',
	'serverUrl is required when using local services'
);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createServices(logger: Logger, config?: AppConfig, serverUrl?: string) {
	const authenticated = isAuthenticated();
	const useLocal = config?.services?.useLocal ?? false;
	adapter = createFetchAdapter(logger);

	// Use local services if explicitly requested OR if not authenticated
	const shouldUseLocal =
		useLocal || !authenticated || process.env.AGENTUITY_FORCE_LOCAL_SERVICES === 'true';

	if (shouldUseLocal) {
		const db = getLocalDB();
		const projectPath = normalizeProjectPath();

		if (!serverUrl) {
			throw new ServerUrlMissingError();
		}

		if (!localServicesLogged.get()) {
			localServicesLogged.set(true);
			logger.info('Using local services (development only)');
		}

		kv = config?.services?.keyvalue || new LocalKeyValueStorage(db, projectPath);
		stream = config?.services?.stream || new LocalStreamStorage(db, projectPath, serverUrl);
		vector = config?.services?.vector || new LocalVectorStorage(db, projectPath);
		queue = new LocalQueueStorage(db, projectPath);
		email = config?.services?.email || new LocalEmailStorage();
		task = config?.services?.task || new LocalTaskStorage(db, projectPath);
		session = config?.services?.session || new DefaultSessionProvider();
		thread = config?.services?.thread || new LocalThreadProvider();
		sessionEvent = config?.services?.sessionEvent
			? new CompositeSessionEventProvider(
					new LocalSessionEventProvider(),
					config.services.sessionEvent
				)
			: new LocalSessionEventProvider();
		evalRunEvent = config?.services?.evalRunEvent
			? new CompositeEvalRunEventProvider(
					new LocalEvalRunEventProvider(),
					config.services.evalRunEvent
				)
			: new LocalEvalRunEventProvider();

		localRouter = createLocalStorageRouter(db, projectPath);

		return { localRouter };
	}

	// Reset local router if not using local services
	localRouter = null;

	// At this point we must be authenticated (since !authenticated would trigger local services above)
	const catalystUrl = getCatalystBaseUrl();
	const streamBaseUrl = getStreamBaseUrl();
	kv = config?.services?.keyvalue || new KeyValueStorageService(getKvBaseUrl(), adapter);
	stream = config?.services?.stream || new StreamStorageService(streamBaseUrl, adapter);
	vector = config?.services?.vector || new VectorStorageService(getVectorBaseUrl(), adapter);
	queue = new QueueStorageService(getQueueBaseUrl(), adapter);
	email = config?.services?.email || new EmailStorageService(getEmailBaseUrl(), adapter);
	task = config?.services?.task || new TaskStorageService(getTaskBaseUrl(), adapter);
	sandbox = new HTTPSandboxService(new APIClient(catalystUrl, logger), streamBaseUrl);
	schedule = new ScheduleService(getCatalystBaseUrl(), adapter);
	session = config?.services?.session || new DefaultSessionProvider();
	thread = config?.services?.thread || new DefaultThreadProvider();
	// FIXME: this is turned off for now for production until we have the new changes deployed
	sessionEvent =
		isProduction() && process.env.AGENTUITY_CLOUD_EXPORT_DIR
			? new JSONSessionEventProvider(process.env.AGENTUITY_CLOUD_EXPORT_DIR)
			: new HTTPSessionEventProvider(new APIClient(catalystUrl, logger), logger);
	if (config?.services?.sessionEvent) {
		sessionEvent = new CompositeSessionEventProvider(sessionEvent, config.services.sessionEvent);
	}
	// FIXME: this is turned off for now for production until we have the new changes deployed
	logger.debug(
		'[SERVICES] Initializing eval run provider - region: %s, catalystBaseUrl: %s, isProduction: %s',
		getRegion(),
		catalystUrl,
		isProduction()
	);
	evalRunEvent =
		isProduction() && process.env.AGENTUITY_CLOUD_EXPORT_DIR
			? new JSONEvalRunEventProvider(process.env.AGENTUITY_CLOUD_EXPORT_DIR)
			: new HTTPEvalRunEventProvider(new APIClient(catalystUrl, logger), logger, catalystUrl);
	if (config?.services?.evalRunEvent) {
		evalRunEvent = new CompositeEvalRunEventProvider(evalRunEvent, config.services.evalRunEvent);
	}

	return {};
}

export function getThreadProvider(): ThreadProvider {
	return thread;
}

export function getSessionProvider(): SessionProvider {
	return session;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getLocalRouter(): any | null {
	return localRouter;
}

export function getSessionEventProvider() {
	return sessionEvent;
}

export function getEvalRunEventProvider() {
	return evalRunEvent;
}

export function getServices() {
	return { kv, stream, vector, sandbox, queue, email, schedule, task };
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function registerServices(o: any, includeAgents = false) {
	Object.defineProperty(o, 'kv', {
		get: () => kv,
		enumerable: false,
		configurable: false,
	});
	Object.defineProperty(o, 'stream', {
		get: () => stream,
		enumerable: false,
		configurable: false,
	});
	Object.defineProperty(o, 'vector', {
		get: () => vector,
		enumerable: false,
		configurable: false,
	});
	Object.defineProperty(o, 'sandbox', {
		get: () => sandbox,
		enumerable: false,
		configurable: false,
	});
	Object.defineProperty(o, 'queue', {
		get: () => queue,
		enumerable: false,
		configurable: false,
	});
	Object.defineProperty(o, 'email', {
		get: () => email,
		enumerable: false,
		configurable: false,
	});
	Object.defineProperty(o, 'schedule', {
		get: () => schedule,
		enumerable: false,
		configurable: false,
	});
	Object.defineProperty(o, 'task', {
		get: () => task,
		enumerable: false,
		configurable: false,
	});

	// Also register agent registry if requested
	if (includeAgents) {
		// Cache the populated registry to avoid re-creating on every access
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		let cachedRegistry: any;
		Object.defineProperty(o, 'agent', {
			get: () => {
				if (!cachedRegistry) {
					// ESM-friendly: populateAgentsRegistry is imported statically
					cachedRegistry = populateAgentsRegistry(o);
				}
				return cachedRegistry;
			},
			enumerable: false,
			configurable: false,
		});
	}
}
