import type { DataStore } from "../db/store";
import type { InMemoryEventBus } from "../hub/eventBus";
import { createEvent } from "../hub/events";
import type { HubStateRegistry } from "../hub/stateRegistry";
import type { QuickActionRegistry } from "./quickActions";
import type { ExtensionAPI, ExtensionCleanup, ExtensionDefinition } from "./types";

type LoaderLog = (message: string, fields?: Record<string, unknown>) => void;

export type ExtensionLoaderOptions = {
    cleanupTimeoutMs?: number;
};

type LoadedExtensionRuntime = {
    cleanup: ExtensionCleanup | null;
    unsubscribers: Array<() => void>;
};

export type ExtensionKind = "builtin" | "external";

type RegisteredExtension = {
    definition: ExtensionDefinition;
    kind: ExtensionKind;
};

export type ExtensionStatus = {
    id: string;
    kind: ExtensionKind;
    loaded: boolean;
};

export class ExtensionLoader {
    private readonly registry = new Map<string, RegisteredExtension>();
    private readonly loaded = new Map<string, LoadedExtensionRuntime>();
    private readonly cleanupTimeoutMs: number;

    public constructor(
        private readonly eventBus: InMemoryEventBus,
        private readonly stateRegistry: HubStateRegistry,
        private readonly db: DataStore,
        private readonly log: LoaderLog,
        private readonly quickActionRegistry?: QuickActionRegistry,
        options: ExtensionLoaderOptions = {}
    ) {
        this.cleanupTimeoutMs = options.cleanupTimeoutMs ?? 2000;
    }

    public register(extensions: ExtensionDefinition[], kind: ExtensionKind = "external"): void {
        for (const extension of extensions) {
            if (!extension.id.trim()) {
                throw new Error("Extension id cannot be empty.");
            }
            if (this.registry.has(extension.id)) {
                throw new Error(`Duplicate extension id detected: ${extension.id}`);
            }
            this.registry.set(extension.id, {
                definition: extension,
                kind
            });
        }
    }

    public async load(extensions: ExtensionDefinition[], kind: ExtensionKind = "external"): Promise<void> {
        this.register(extensions, kind);
        for (const extension of extensions) {
            await this.enable(extension.id);
        }
    }

    public async enable(extensionId: string): Promise<boolean> {
        const entry = this.registry.get(extensionId);
        if (!entry) {
            return false;
        }
        if (this.loaded.has(extensionId)) {
            return true;
        }

        const unsubscribers: Array<() => void> = [];
        const api = this.createApi(extensionId, unsubscribers);
        let cleanup: ExtensionCleanup | null = null;

        try {
            const initResult = await entry.definition.init(api);
            cleanup = typeof initResult === "function" ? initResult : null;
            this.loaded.set(extensionId, { cleanup, unsubscribers });
            this.log("extension_loaded", { extensionId });
            return true;
        } catch (error) {
            for (const unsubscribe of unsubscribers) {
                try {
                    unsubscribe();
                } catch {
                    // ignore unsubscribe failures
                }
            }
            this.log("extension_load_failed", {
                extensionId,
                error: error instanceof Error ? error.message : String(error)
            });
            throw error;
        }
    }

    public async disable(extensionId: string): Promise<boolean> {
        const runtime = this.loaded.get(extensionId);
        if (!runtime) {
            return false;
        }

        this.loaded.delete(extensionId);

        for (const unsubscribe of runtime.unsubscribers) {
            try {
                unsubscribe();
            } catch {
                // ignore unsubscribe failures
            }
        }

        if (runtime.cleanup) {
            try {
                await this.withTimeout(runtime.cleanup(), this.cleanupTimeoutMs);
            } catch (error) {
                this.log("extension_unload_cleanup_failed", {
                    extensionId,
                    timeoutMs: this.cleanupTimeoutMs,
                    error: error instanceof Error ? error.message : String(error)
                });
            }
        }

        this.log("extension_unloaded", { extensionId });
        return true;
    }

    public async shutdown(): Promise<void> {
        const loadedIds = Array.from(this.loaded.keys()).sort((a, b) => a.localeCompare(b));
        for (const extensionId of loadedIds) {
            await this.disable(extensionId);
        }
    }

    public has(extensionId: string): boolean {
        return this.registry.has(extensionId);
    }

    public isLoaded(extensionId: string): boolean {
        return this.loaded.has(extensionId);
    }

    public listStatuses(): ExtensionStatus[] {
        return Array.from(this.registry.keys())
            .sort((a, b) => a.localeCompare(b))
            .map((id) => ({
                id,
                kind: this.registry.get(id)?.kind ?? "external",
                loaded: this.loaded.has(id)
            }));
    }

    private createApi(extensionId: string, unsubscribers: Array<() => void>): ExtensionAPI {
        return {
            on: (name, handler) => {
                const unsubscribe = this.eventBus.on(name, handler, {
                    componentId: extensionId,
                    componentKind: "extension"
                });
                unsubscribers.push(unsubscribe);
                return unsubscribe;
            },
            emit: (name, payload) => this.eventBus.emit(createEvent(name, payload)),
            getState: (streamId, key, defaultValue) =>
                this.stateRegistry.getState(streamId, extensionId, key, defaultValue),
            setState: (streamId, key, value) =>
                this.stateRegistry.setState(streamId, extensionId, key, value),
            registerQuickAction: (action) => {
                if (!this.quickActionRegistry) {
                    return () => {};
                }
                const unregister = this.quickActionRegistry.register(extensionId, action);
                unsubscribers.push(unregister);
                return unregister;
            },
            db: this.db,
            log: (message, fields = {}) => {
                this.log("extension_log", {
                    extensionId,
                    message,
                    ...fields
                });
            }
        };
    }

    private async withTimeout<T>(task: Promise<T> | T, timeoutMs: number): Promise<T> {
        if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
            return await Promise.resolve(task);
        }
        let timeout: ReturnType<typeof setTimeout> | null = null;
        try {
            return await Promise.race<T>([
                Promise.resolve(task),
                new Promise<T>((_resolveValue, rejectValue) => {
                    timeout = setTimeout(
                        () => rejectValue(new Error("Extension cleanup timeout.")),
                        timeoutMs
                    );
                })
            ]);
        } finally {
            if (timeout) {
                clearTimeout(timeout);
            }
        }
    }
}
