import { Metadata, LogError } from '@memberjunction/core';
import { ComponentMetadataEngine } from '@memberjunction/core-entities';
export class ComponentManager {
    constructor(compiler, registry, runtimeContext, config = {}) {
        this.fetchCache = new Map();
        this.registryNotifications = new Set();
        this.loadingPromises = new Map();
        this.componentEngine = ComponentMetadataEngine.Instance;
        this.graphQLClient = null;
        this.compiler = compiler;
        this.registry = registry;
        this.runtimeContext = runtimeContext;
        this.config = {
            debug: false,
            maxCacheSize: 100,
            cacheTTL: 3600000,
            enableUsageTracking: true,
            dependencyBatchSize: 5,
            fetchTimeout: 30000,
            ...config
        };
        this.log('ComponentManager initialized', {
            debug: this.config.debug,
            cacheTTL: this.config.cacheTTL,
            usageTracking: this.config.enableUsageTracking
        });
    }
    async loadComponent(spec, options = {}) {
        const startTime = Date.now();
        const componentKey = this.getComponentKey(spec, options);
        this.log(`Loading component: ${spec.name}`, {
            key: componentKey,
            location: spec.location,
            registry: spec.registry,
            forceRefresh: options.forceRefresh
        });
        const existingPromise = this.loadingPromises.get(componentKey);
        if (existingPromise && !options.forceRefresh) {
            const namespace = spec.namespace || options.defaultNamespace || 'Global';
            const version = spec.version || options.defaultVersion || 'latest';
            const contentHash = this.calculateHash(spec);
            const registered = this.registry.get(spec.name, namespace, version, contentHash);
            if (registered) {
                this.log(`Component already registered (circular dep resolution): ${spec.name}`);
                return {
                    success: true,
                    component: registered,
                    spec,
                    fromCache: true
                };
            }
            this.log(`Component already loading: ${spec.name}, waiting...`);
            return existingPromise;
        }
        const loadPromise = this.doLoadComponent(spec, options, componentKey, startTime);
        this.loadingPromises.set(componentKey, loadPromise);
        try {
            const result = await loadPromise;
            return result;
        }
        finally {
            this.loadingPromises.delete(componentKey);
        }
    }
    async doLoadComponent(spec, options, componentKey, startTime) {
        const errors = [];
        try {
            const namespace = spec.namespace || options.defaultNamespace || 'Global';
            const version = spec.version || options.defaultVersion || 'latest';
            const contentHash = this.calculateHash(spec);
            const existing = this.registry.get(spec.name, namespace, version, contentHash);
            if (existing && !options.forceRefresh && !options.forceRecompile) {
                this.log(`Component found in registry: ${spec.name}`);
                if (options.trackUsage !== false) {
                    await this.notifyRegistryUsageIfNeeded(spec, componentKey);
                }
                const cachedEntry = this.fetchCache.get(componentKey);
                return {
                    success: true,
                    component: existing,
                    spec: cachedEntry?.spec || spec,
                    fromCache: true
                };
            }
            let fullSpec = spec;
            if (this.needsFetch(spec)) {
                this.log(`Fetching component spec: ${spec.name}`);
                try {
                    fullSpec = await this.fetchComponentSpec(spec, options.contextUser, {
                        resolutionMode: options.resolutionMode
                    });
                    this.fetchCache.set(componentKey, {
                        spec: fullSpec,
                        fetchedAt: new Date(),
                        hash: this.calculateHash(fullSpec),
                        usageNotified: false
                    });
                }
                catch (error) {
                    errors.push({
                        message: `Failed to fetch component: ${error instanceof Error ? error.message : String(error)}`,
                        phase: 'fetch',
                        componentName: spec.name
                    });
                    throw error;
                }
            }
            else {
                if (spec.location === 'registry' && spec.code) {
                    this.log(`Skipping fetch for registry component: ${spec.name} (code already provided)`, {
                        location: spec.location,
                        registry: spec.registry
                    });
                }
                if (spec.code && !this.fetchCache.has(componentKey)) {
                    this.fetchCache.set(componentKey, {
                        spec: fullSpec,
                        fetchedAt: new Date(),
                        hash: this.calculateHash(fullSpec),
                        usageNotified: false
                    });
                }
            }
            if (options.trackUsage !== false) {
                await this.notifyRegistryUsageIfNeeded(fullSpec, componentKey);
            }
            let compiledComponent = existing;
            if (!compiledComponent || options.forceRecompile) {
                this.log(`Compiling component: ${spec.name}`);
                try {
                    compiledComponent = await this.compileComponent(fullSpec, options);
                }
                catch (error) {
                    errors.push({
                        message: `Failed to compile component: ${error instanceof Error ? error.message : String(error)}`,
                        phase: 'compile',
                        componentName: spec.name
                    });
                    throw error;
                }
            }
            if (!existing || options.forceRefresh || options.forceRecompile) {
                this.log(`Registering component: ${spec.name}`);
                this.registry.register(fullSpec.name, compiledComponent, namespace, version, contentHash);
            }
            const dependencies = {};
            if (fullSpec.dependencies && fullSpec.dependencies.length > 0) {
                this.log(`Loading ${fullSpec.dependencies.length} dependencies for ${spec.name}`);
                const depResults = await this.loadDependenciesBatched(fullSpec.dependencies, { ...options, isDependent: true });
                for (const result of depResults) {
                    if (result.success && result.component) {
                        const depSpec = fullSpec.dependencies.find(d => d.name === (result.spec?.name || ''));
                        if (depSpec) {
                            dependencies[depSpec.name] = result.component;
                        }
                    }
                    else if (result.errors) {
                        errors.push(...result.errors);
                    }
                }
            }
            const elapsed = Date.now() - startTime;
            this.log(`Component loaded successfully: ${spec.name} (${elapsed}ms)`, {
                fromCache: false,
                dependencyCount: Object.keys(dependencies).length
            });
            return {
                success: errors.length === 0,
                component: compiledComponent,
                spec: fullSpec,
                fromCache: false,
                dependencies,
                errors: errors.length > 0 ? errors : undefined
            };
        }
        catch (error) {
            const elapsed = Date.now() - startTime;
            this.log(`Failed to load component: ${spec.name} (${elapsed}ms)`, error);
            return {
                success: false,
                fromCache: false,
                errors: errors.length > 0 ? errors : [{
                        message: error instanceof Error ? error.message : String(error),
                        phase: 'fetch',
                        componentName: spec.name
                    }]
            };
        }
    }
    async loadHierarchy(rootSpec, options = {}) {
        const startTime = Date.now();
        const loaded = [];
        const errors = [];
        const components = {};
        const stats = {
            fromCache: 0,
            fetched: 0,
            compiled: 0,
            totalTime: 0
        };
        this.log(`Loading component hierarchy: ${rootSpec.name}`, {
            location: rootSpec.location,
            registry: rootSpec.registry
        });
        try {
            if (this.componentEngine && typeof this.componentEngine.Config === 'function') {
                await this.componentEngine.Config(false, options.contextUser);
            }
            const result = await this.loadComponentRecursive(rootSpec, options, loaded, errors, components, stats, new Set());
            stats.totalTime = Date.now() - startTime;
            this.log(`Hierarchy loaded: ${rootSpec.name}`, {
                success: errors.length === 0,
                loadedCount: loaded.length,
                errors: errors.length,
                ...stats
            });
            const unwrappedComponents = {};
            for (const [name, componentObject] of Object.entries(components)) {
                if (componentObject && typeof componentObject === 'object' && 'component' in componentObject) {
                    unwrappedComponents[name] = componentObject.component;
                }
                else {
                    unwrappedComponents[name] = componentObject;
                }
            }
            return {
                success: errors.length === 0,
                rootComponent: result.component,
                resolvedSpec: result.spec,
                loadedComponents: loaded,
                errors,
                components: unwrappedComponents,
                stats
            };
        }
        catch (error) {
            stats.totalTime = Date.now() - startTime;
            this.log(`Failed to load hierarchy: ${rootSpec.name}`, error);
            return {
                success: false,
                loadedComponents: loaded,
                errors: [...errors, {
                        message: error instanceof Error ? error.message : String(error),
                        phase: 'fetch',
                        componentName: rootSpec.name
                    }],
                stats
            };
        }
    }
    async loadComponentRecursive(spec, options, loaded, errors, components, stats, visited) {
        const componentKey = this.getComponentKey(spec, options);
        if (visited.has(componentKey)) {
            this.log(`Circular dependency detected: ${spec.name}`);
            return {
                success: true,
                component: components[spec.name],
                spec,
                fromCache: true
            };
        }
        visited.add(componentKey);
        const result = await this.loadComponent(spec, options);
        if (result.success && result.component) {
            loaded.push(spec.name);
            components[spec.name] = result.component;
            if (stats) {
                if (result.fromCache)
                    stats.fromCache++;
                else {
                    stats.fetched++;
                    stats.compiled++;
                }
            }
            const dependencies = this.resolveDependencies(spec.dependencies, result.spec?.dependencies);
            if (dependencies) {
                for (const dep of dependencies) {
                    const depSpec = { ...dep };
                    if (depSpec.code) {
                        this.log(`Dependency ${depSpec.name} already has code (from registry population), optimizing load`);
                    }
                    if (depSpec.location === 'registry' && !depSpec.registry) {
                        depSpec.registry = undefined;
                        this.log(`Dependency ${depSpec.name} is a local registry component (registry=undefined)`);
                    }
                    await this.loadComponentRecursive(depSpec, { ...options, isDependent: true }, loaded, errors, components, stats, visited);
                }
            }
        }
        else if (result.errors) {
            errors.push(...result.errors);
        }
        return result;
    }
    async loadDependenciesBatched(dependencies, options) {
        const batchSize = this.config.dependencyBatchSize || 5;
        const results = [];
        for (let i = 0; i < dependencies.length; i += batchSize) {
            const batch = dependencies.slice(i, i + batchSize);
            const batchPromises = batch.map(dep => this.loadComponent(dep, options));
            const batchResults = await Promise.all(batchPromises);
            results.push(...batchResults);
        }
        return results;
    }
    resolveDependencies(inputDeps, fetchedDeps) {
        const inputHasDeps = inputDeps && inputDeps.length > 0;
        const inputHasCode = inputHasDeps && inputDeps.some(d => !!d.code);
        if (inputHasCode)
            return inputDeps;
        if (!inputHasDeps)
            return fetchedDeps || inputDeps;
        if (fetchedDeps?.some(d => !!d.code))
            return fetchedDeps;
        return inputDeps;
    }
    needsFetch(spec) {
        return spec.location === 'registry' && !spec.code;
    }
    async fetchComponentSpec(spec, contextUser, options) {
        const cacheKey = this.getComponentKey(spec, {});
        const cached = this.fetchCache.get(cacheKey);
        if (cached && this.isCacheValid(cached)) {
            this.log(`Using cached spec for: ${spec.name}`);
            return cached.spec;
        }
        if (!spec.registry) {
            const localComponent = await this.componentEngine.FindComponent(spec.name, spec.namespace);
            if (!localComponent) {
                throw new Error(`Local component not found: ${spec.name}`);
            }
            if (!localComponent.Specification) {
                throw new Error(`Local component ${spec.name} has no specification`);
            }
            const fullSpec = JSON.parse(localComponent.Specification);
            this.fetchCache.set(cacheKey, {
                spec: fullSpec,
                fetchedAt: new Date(),
                usageNotified: false
            });
            return fullSpec;
        }
        if (!this.graphQLClient) {
            await this.initializeGraphQLClient();
        }
        if (!this.graphQLClient) {
            throw new Error('GraphQL client not available for registry fetching');
        }
        this.log(`Fetching from external registry: ${spec.registry}/${spec.name}`);
        const cachedHash = cached?.hash;
        const response = await this.graphQLClient.GetRegistryComponentWithHash({
            registryName: spec.registry,
            namespace: spec.namespace || 'Global',
            name: spec.name,
            version: spec.version || 'latest',
            hash: cachedHash
        });
        if (response.notModified && cached) {
            this.log(`Registry returned 304 for ${spec.name}, reusing cached spec`);
            this.fetchCache.set(cacheKey, {
                ...cached,
                fetchedAt: new Date()
            });
            return cached.spec;
        }
        if (!response.specification) {
            throw new Error(`Component not found in registry: ${spec.registry}/${spec.name}`);
        }
        const fullSpec = response.specification;
        const processedSpec = this.applyResolutionMode(fullSpec, spec, options?.resolutionMode);
        this.fetchCache.set(cacheKey, {
            spec: processedSpec,
            fetchedAt: new Date(),
            hash: response.hash,
            usageNotified: false
        });
        return processedSpec;
    }
    applyResolutionMode(fullSpec, originalSpec, resolutionMode) {
        let processedSpec;
        if (resolutionMode === 'embed') {
            processedSpec = {
                ...fullSpec,
                location: 'embedded',
                registry: undefined,
            };
        }
        else {
            processedSpec = {
                ...fullSpec,
                location: originalSpec.location,
                registry: originalSpec.registry,
                namespace: originalSpec.namespace || fullSpec.namespace,
                name: originalSpec.name || fullSpec.name
            };
        }
        if (processedSpec.dependencies && processedSpec.dependencies.length > 0) {
            processedSpec.dependencies = processedSpec.dependencies.map(dep => {
                return this.applyResolutionMode(dep, dep, resolutionMode);
            });
        }
        return processedSpec;
    }
    async compileComponent(spec, options) {
        const allLibraries = options.allLibraries || this.componentEngine.ComponentLibraries || [];
        const validLibraries = spec.libraries?.filter(lib => lib && lib.name && lib.globalVariable &&
            lib.name !== 'unknown' && lib.globalVariable !== 'undefined');
        const result = await this.compiler.compile({
            componentName: spec.name,
            componentCode: spec.code || '',
            libraries: validLibraries,
            dependencies: spec.dependencies,
            allLibraries
        });
        if (!result.success || !result.component) {
            throw new Error(result.error?.message || 'Compilation failed');
        }
        if (result.loadedLibraries && result.loadedLibraries.size > 0) {
            if (!this.runtimeContext.libraries) {
                this.runtimeContext.libraries = {};
            }
            result.loadedLibraries.forEach((value, key) => {
                this.runtimeContext.libraries[key] = value;
            });
        }
        const componentObject = result.component.factory(this.runtimeContext, undefined, {});
        return componentObject;
    }
    async notifyRegistryUsageIfNeeded(spec, componentKey) {
        if (!spec.registry || !this.config.enableUsageTracking) {
            return;
        }
        const notificationKey = `${spec.registry}:${componentKey}`;
        if (this.registryNotifications.has(notificationKey)) {
            this.log(`Usage already notified for: ${spec.name}`);
            return;
        }
        try {
            this.log(`Notifying registry usage for: ${spec.name}`);
            this.registryNotifications.add(notificationKey);
            const cached = this.fetchCache.get(componentKey);
            if (cached) {
                cached.usageNotified = true;
            }
        }
        catch (error) {
            console.warn(`Failed to notify registry usage for ${componentKey}:`, error);
        }
    }
    async initializeGraphQLClient() {
        try {
            const provider = Metadata?.Provider;
            if (provider && provider.ExecuteGQL) {
                const { GraphQLComponentRegistryClient } = await import('@memberjunction/graphql-dataprovider');
                this.graphQLClient = new GraphQLComponentRegistryClient(provider);
                this.log('GraphQL client initialized');
            }
        }
        catch (error) {
            LogError(`Failed to initialize GraphQL client: ${error instanceof Error ? error.message : String(error)}`);
        }
    }
    isCacheValid(entry) {
        const age = Date.now() - entry.fetchedAt.getTime();
        return age < (this.config.cacheTTL || 3600000);
    }
    calculateHash(spec) {
        const content = JSON.stringify({
            name: spec.name,
            version: spec.version,
            code: spec.code,
            libraries: spec.libraries
        });
        let hash = 0;
        for (let i = 0; i < content.length; i++) {
            const char = content.charCodeAt(i);
            hash = ((hash << 5) - hash) + char;
            hash = hash & hash;
        }
        return hash.toString(16);
    }
    getComponentKey(spec, options) {
        const registry = spec.registry || 'local';
        const namespace = spec.namespace || options.defaultNamespace || 'Global';
        const version = spec.version || options.defaultVersion || 'latest';
        const hash = this.calculateHash(spec);
        return `${registry}:${namespace}:${spec.name}:${version}#${hash}`;
    }
    clearCache() {
        this.fetchCache.clear();
        this.registryNotifications.clear();
        this.loadingPromises.clear();
        this.log('All caches cleared');
    }
    getCacheStats() {
        return {
            fetchCacheSize: this.fetchCache.size,
            notificationsCount: this.registryNotifications.size,
            loadingCount: this.loadingPromises.size
        };
    }
    log(message, data) {
        if (this.config.debug) {
            console.log(`🎯 [ComponentManager] ${message}`, data || '');
        }
    }
}
//# sourceMappingURL=component-manager.js.map