import { Metadata } from '@memberjunction/core';
import { UUIDsEqual } from '@memberjunction/global';
import { ComponentMetadataEngine } from '@memberjunction/core-entities';
let GraphQLComponentRegistryClient;
export class ComponentRegistryService {
    static { this.instance = null; }
    get Provider() {
        return this._provider ?? new Metadata();
    }
    set Provider(value) {
        this._provider = value;
    }
    constructor(compiler, runtimeContext, debug = false, graphQLClient) {
        this.compiledComponentCache = new Map();
        this.componentReferences = new Map();
        this.componentEngine = ComponentMetadataEngine.Instance;
        this.registryProviders = new Map();
        this.debug = false;
        this._provider = null;
        this.cachedProviderClient = null;
        this.compiler = compiler;
        this.runtimeContext = runtimeContext;
        this.debug = debug;
        this.graphQLClient = graphQLClient;
    }
    static getInstance(compiler, context, debug = false, graphQLClient) {
        if (!ComponentRegistryService.instance) {
            ComponentRegistryService.instance = new ComponentRegistryService(compiler, context, debug, graphQLClient);
        }
        return ComponentRegistryService.instance;
    }
    setGraphQLClient(client) {
        this.graphQLClient = client;
        if (this.debug) {
            console.log('✅ GraphQL client configured for component registry');
        }
    }
    async getGraphQLClient() {
        if (this.graphQLClient) {
            return this.graphQLClient;
        }
        if (this.cachedProviderClient) {
            return this.cachedProviderClient;
        }
        try {
            const provider = Metadata?.Provider;
            if (provider && provider.ExecuteGQL !== undefined) {
                if (!GraphQLComponentRegistryClient) {
                    try {
                        const graphqlModule = await import('@memberjunction/graphql-dataprovider');
                        GraphQLComponentRegistryClient = graphqlModule.GraphQLComponentRegistryClient;
                    }
                    catch (importError) {
                        if (this.debug) {
                            console.log('⚠️ [ComponentRegistryService] @memberjunction/graphql-dataprovider not available');
                        }
                        return null;
                    }
                }
                if (GraphQLComponentRegistryClient) {
                    try {
                        const client = new GraphQLComponentRegistryClient(provider);
                        this.cachedProviderClient = client;
                        if (this.debug) {
                            console.log('📡 [ComponentRegistryService] Created GraphQL client from active metadata provider');
                        }
                        return client;
                    }
                    catch (error) {
                        if (this.debug) {
                            console.log('⚠️ [ComponentRegistryService] Failed to create GraphQL client:', error);
                        }
                    }
                }
            }
        }
        catch (error) {
            if (this.debug) {
                console.log('⚠️ [ComponentRegistryService] Could not access metadata provider:', error);
            }
        }
        return null;
    }
    async initialize(contextUser) {
        await this.componentEngine.Config(false, contextUser);
    }
    async calculateSpecHash(spec) {
        if (typeof crypto === 'undefined' || !crypto.subtle) {
            throw new Error('Web Crypto API not available. This typically happens when running in an insecure context. ' +
                'Please use HTTPS or localhost for development. ' +
                'Note: crypto.subtle is available in Node.js 15+ and all modern browsers on secure contexts.');
        }
        const specString = JSON.stringify(spec);
        const encoder = new TextEncoder();
        const data = encoder.encode(specString);
        const hashBuffer = await crypto.subtle.digest('SHA-256', data);
        const hashArray = Array.from(new Uint8Array(hashBuffer));
        const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
        return hashHex;
    }
    async getCompiledComponent(componentId, referenceId, contextUser) {
        await this.initialize(contextUser);
        const component = await this.componentEngine.FindComponentByID(componentId, contextUser);
        if (!component) {
            throw new Error(`Component not found: ${componentId}`);
        }
        const key = this.getComponentKey(component.Name, component.Namespace, component.Version, component.SourceRegistryID);
        if (this.compiledComponentCache.has(key)) {
            const cached = this.compiledComponentCache.get(key);
            cached.lastUsed = new Date();
            cached.useCount++;
            if (referenceId) {
                this.addComponentReference(key, referenceId);
            }
            if (this.debug) {
                console.log(`✅ Reusing compiled component from cache: ${key} (use count: ${cached.useCount})`);
            }
            return cached.component(this.runtimeContext);
        }
        if (this.debug) {
            console.log(`🔄 Loading and compiling component: ${key}`);
        }
        const spec = await this.getComponentSpec(componentId, contextUser);
        const allLibraries = this.componentEngine.ComponentLibraries || [];
        let compilationResult;
        try {
            compilationResult = await this.compiler.compile({
                componentName: component.Name,
                componentCode: spec.code,
                libraries: spec.libraries,
                allLibraries
            });
        }
        catch (compileEx) {
            console.error(`🔴 Error compiling component ${component.Name}`, compileEx);
            throw compileEx;
        }
        if (!compilationResult.success) {
            console.error(`🔴 Error compiling component ${component.Name}`, compilationResult, 'Code', spec.code);
            throw new Error(`Failed to compile component ${component.Name}: ${compilationResult.error}`);
        }
        const metadata = {
            name: component.Name,
            namespace: component.Namespace || '',
            version: component.Version,
            description: component.Description || '',
            title: component.Title || undefined,
            type: component.Type || undefined,
            status: component.Status || undefined,
            properties: spec.properties,
            events: spec.events,
            libraries: spec.libraries,
            dependencies: spec.dependencies,
            sourceRegistryID: component.SourceRegistryID,
            isLocal: !component.SourceRegistryID
        };
        if (!compilationResult.component) {
            throw new Error(`Component compilation succeeded but no component returned`);
        }
        const compiledComponentFactory = compilationResult.component.factory;
        this.compiledComponentCache.set(key, {
            component: compiledComponentFactory,
            metadata,
            compiledAt: new Date(),
            lastUsed: new Date(),
            useCount: 1
        });
        if (referenceId) {
            this.addComponentReference(key, referenceId);
        }
        return compiledComponentFactory(this.runtimeContext);
    }
    async getCompiledComponentFromRegistry(registryName, namespace, name, version, referenceId, contextUser) {
        await this.initialize(contextUser);
        if (this.debug) {
            console.log(`🌐 [ComponentRegistryService] Fetching from external registry: ${registryName}/${namespace}/${name}@${version}`);
        }
        const registry = this.componentEngine.ComponentRegistries?.find(r => r.Name === registryName && r.Status === 'Active');
        if (!registry) {
            throw new Error(`Registry not found or inactive: ${registryName}`);
        }
        if (this.debug) {
            console.log(`✅ [ComponentRegistryService] Found registry: ${registry.Name} (ID: ${registry.ID})`);
        }
        const graphQLClient = await this.getGraphQLClient();
        if (!graphQLClient) {
            throw new Error('GraphQL client not available for external registry fetching. No client provided and the active metadata provider is not a GraphQLDataProvider.');
        }
        const key = `external:${registryName}:${namespace}:${name}:${version}`;
        const cached = this.compiledComponentCache.get(key);
        try {
            const spec = await graphQLClient.GetRegistryComponent({
                registryName: registry.Name,
                namespace,
                name,
                version,
                hash: cached?.specHash
            });
            if (!spec && cached?.specHash) {
                if (this.debug) {
                    console.log(`♻️ [ComponentRegistryService] Component not modified, using cached: ${key}`);
                }
                cached.lastUsed = new Date();
                cached.useCount++;
                if (referenceId) {
                    this.addComponentReference(key, referenceId);
                }
                return cached.component(this.runtimeContext);
            }
            if (!spec) {
                throw new Error(`Component not found in registry ${registryName}: ${namespace}/${name}@${version}`);
            }
            if (this.debug) {
                console.log(`✅ [ComponentRegistryService] Fetched spec from external registry: ${spec.name}`);
            }
            const specHash = await this.calculateSpecHash(spec);
            if (cached && cached.specHash === specHash) {
                if (this.debug) {
                    console.log(`♻️ [ComponentRegistryService] Using cached compilation for: ${key} (hash match)`);
                }
                cached.lastUsed = new Date();
                cached.useCount++;
                if (referenceId) {
                    this.addComponentReference(key, referenceId);
                }
                return cached.component(this.runtimeContext);
            }
            if (cached && this.debug) {
                console.log(`🔄 [ComponentRegistryService] Spec changed for: ${key}, recompiling (old hash: ${cached.specHash?.substring(0, 8)}..., new hash: ${specHash.substring(0, 8)}...)`);
            }
            const allLibraries = this.componentEngine.ComponentLibraries || [];
            const compilationResult = await this.compiler.compile({
                componentName: spec.name,
                componentCode: spec.code || '',
                allLibraries: allLibraries
            });
            if (!compilationResult.success || !compilationResult.component) {
                throw new Error(`Failed to compile component: ${compilationResult.error?.message || 'Unknown error'}`);
            }
            this.compiledComponentCache.set(key, {
                component: compilationResult.component.factory,
                metadata: {
                    name: spec.name,
                    namespace: spec.namespace || '',
                    version: spec.version || '1.0.0',
                    description: spec.description || '',
                    type: spec.type,
                    isLocal: false
                },
                compiledAt: new Date(),
                lastUsed: new Date(),
                useCount: 1,
                specHash: specHash
            });
            if (referenceId) {
                this.addComponentReference(key, referenceId);
            }
            if (this.debug) {
                console.log(`🎯 [ComponentRegistryService] Successfully compiled external component: ${spec.name}`);
            }
            return compilationResult.component.factory(this.runtimeContext);
        }
        catch (error) {
            console.error(`❌ [ComponentRegistryService] Failed to fetch from external registry:`, error);
            throw error;
        }
    }
    async getComponentSpec(componentId, contextUser) {
        await this.initialize(contextUser);
        const component = await this.componentEngine.FindComponentByID(componentId, contextUser);
        if (!component) {
            throw new Error(`Component not found: ${componentId}`);
        }
        if (!component.SourceRegistryID) {
            if (!component.Specification) {
                throw new Error(`Local component ${component.Name} has no specification`);
            }
            return JSON.parse(component.Specification);
        }
        if (component.Specification && component.LastSyncedAt) {
            if (this.debug) {
                console.log(`Using cached external component: ${component.Name} (synced: ${component.LastSyncedAt})`);
            }
            return JSON.parse(component.Specification);
        }
        const registry = this.componentEngine.ComponentRegistries?.find(r => UUIDsEqual(r.ID, component.SourceRegistryID));
        if (!registry) {
            throw new Error(`Registry not found: ${component.SourceRegistryID}`);
        }
        let spec;
        if (this.graphQLClient) {
            if (this.debug) {
                console.log(`Fetching from registry via GraphQL: ${component.Name}`);
            }
            const result = await this.graphQLClient.GetRegistryComponent({
                registryName: registry.Name,
                namespace: component.Namespace || '',
                name: component.Name,
                version: component.Version
            });
            if (!result) {
                throw new Error(`Component not found in registry: ${component.Name}`);
            }
            spec = result;
        }
        else {
            spec = await this.fetchFromExternalRegistry(registry.URI || '', component.Name, component.Namespace || '', component.Version, this.getRegistryApiKey(registry.ID));
        }
        await this.cacheExternalComponent(componentId, spec, contextUser);
        return spec;
    }
    async fetchFromExternalRegistry(uri, name, namespace, version, apiKey) {
        const url = `${uri}/components/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/${version}`;
        const headers = {
            'Accept': 'application/json'
        };
        if (apiKey) {
            headers['Authorization'] = `Bearer ${apiKey}`;
        }
        if (this.debug) {
            console.log(`Fetching from external registry: ${url}`);
        }
        const response = await fetch(url, { headers });
        if (!response.ok) {
            throw new Error(`Registry fetch failed: ${response.status} ${response.statusText}`);
        }
        const spec = await response.json();
        return spec;
    }
    async cacheExternalComponent(componentId, spec, contextUser) {
        const md = this.Provider;
        const componentEntity = await md.GetEntityObject('MJ: Components', contextUser);
        if (!await componentEntity.Load(componentId)) {
            throw new Error(`Failed to load component entity: ${componentId}`);
        }
        componentEntity.Specification = JSON.stringify(spec);
        componentEntity.LastSyncedAt = new Date();
        if (!componentEntity.ReplicatedAt) {
            componentEntity.ReplicatedAt = new Date();
        }
        if (spec.name) {
            componentEntity.Name = spec.name;
        }
        if (spec.namespace) {
            componentEntity.Namespace = spec.namespace;
        }
        if (spec.version) {
            componentEntity.Version = spec.version;
        }
        if (spec.title) {
            componentEntity.Title = spec.title;
        }
        if (spec.description) {
            componentEntity.Description = spec.description;
        }
        if (spec.type) {
            const typeMap = {
                'report': 'Report',
                'dashboard': 'Dashboard',
                'form': 'Form',
                'table': 'Table',
                'chart': 'Chart',
                'navigation': 'Navigation',
                'search': 'Search',
                'widget': 'Widget',
                'utility': 'Utility',
                'other': 'Other'
            };
            const mappedType = typeMap[spec.type.toLowerCase()];
            if (mappedType) {
                componentEntity.Type = mappedType;
            }
        }
        if (spec.functionalRequirements) {
            componentEntity.FunctionalRequirements = spec.functionalRequirements;
        }
        if (spec.technicalDesign) {
            componentEntity.TechnicalDesign = spec.technicalDesign;
        }
        const result = await componentEntity.Save();
        if (!result) {
            throw new Error(`Failed to save cached component: ${componentEntity.Name}\n${componentEntity.LatestResult?.CompleteMessage || 'Unknown error'}`);
        }
        if (this.debug) {
            console.log(`Cached external component: ${componentEntity.Name} at ${componentEntity.LastSyncedAt}`);
        }
        await this.componentEngine.Config(true, contextUser);
    }
    async loadDependencies(componentId, contextUser) {
        await this.initialize(contextUser);
        const dependencies = this.componentEngine.ComponentDependencies?.filter(d => UUIDsEqual(d.ComponentID, componentId)) || [];
        const result = [];
        for (const dep of dependencies) {
            const depComponent = await this.componentEngine.FindComponentByID(dep.DependencyComponentID, contextUser);
            if (depComponent) {
                result.push({
                    name: depComponent.Name,
                    namespace: depComponent.Namespace || '',
                    version: depComponent.Version,
                    isRequired: true,
                    location: depComponent.SourceRegistryID ? 'registry' : 'embedded',
                    sourceRegistryID: depComponent.SourceRegistryID
                });
            }
        }
        return result;
    }
    async resolveDependencyTree(componentId, contextUser, visited = new Set()) {
        if (visited.has(componentId)) {
            return {
                componentId,
                circular: true
            };
        }
        visited.add(componentId);
        await this.initialize(contextUser);
        const component = await this.componentEngine.FindComponentByID(componentId, contextUser);
        if (!component) {
            return { componentId, dependencies: [] };
        }
        const directDeps = await this.loadDependencies(componentId, contextUser);
        const dependencies = [];
        for (const dep of directDeps) {
            const depComponent = await this.componentEngine.FindComponent(dep.name, dep.namespace, undefined, contextUser);
            if (depComponent) {
                const subTree = await this.resolveDependencyTree(depComponent.ID, contextUser, visited);
                dependencies.push(subTree);
            }
        }
        return {
            componentId,
            name: component.Name,
            namespace: component.Namespace || undefined,
            version: component.Version,
            dependencies,
            totalCount: dependencies.reduce((sum, d) => sum + (d.totalCount || 1), 1)
        };
    }
    async getComponentsToLoad(rootComponentId, contextUser) {
        const tree = await this.resolveDependencyTree(rootComponentId, contextUser);
        const ordered = [];
        const processNode = (node) => {
            if (node.dependencies) {
                node.dependencies.forEach(processNode);
            }
            if (!ordered.includes(node.componentId)) {
                ordered.push(node.componentId);
            }
        };
        processNode(tree);
        return ordered;
    }
    addComponentReference(componentKey, referenceId) {
        if (!this.componentReferences.has(componentKey)) {
            this.componentReferences.set(componentKey, new Set());
        }
        this.componentReferences.get(componentKey).add(referenceId);
    }
    removeComponentReference(componentKey, referenceId) {
        const refs = this.componentReferences.get(componentKey);
        if (refs) {
            refs.delete(referenceId);
            if (refs.size === 0) {
                this.considerCacheEviction(componentKey);
            }
        }
    }
    considerCacheEviction(componentKey) {
        const cached = this.compiledComponentCache.get(componentKey);
        if (cached) {
            const timeSinceLastUse = Date.now() - cached.lastUsed.getTime();
            const evictionThreshold = 5 * 60 * 1000;
            if (timeSinceLastUse > evictionThreshold) {
                if (this.debug) {
                    console.log(`🗑️ Evicting unused component from cache: ${componentKey}`);
                }
                this.compiledComponentCache.delete(componentKey);
            }
        }
    }
    getRegistryApiKey(registryId) {
        const envKey = `REGISTRY_API_KEY_${registryId.replace(/-/g, '_').toUpperCase()}`;
        return process.env[envKey];
    }
    getCacheStats() {
        let totalUseCount = 0;
        this.compiledComponentCache.forEach(cached => {
            totalUseCount += cached.useCount;
        });
        return {
            compiledComponents: this.compiledComponentCache.size,
            totalUseCount,
            memoryEstimate: `~${(this.compiledComponentCache.size * 50)}KB`
        };
    }
    clearCache() {
        if (this.debug) {
            console.log('🧹 Clearing all component caches');
        }
        this.compiledComponentCache.clear();
        this.componentReferences.clear();
    }
    forceClearAll() {
        this.compiledComponentCache.clear();
        this.componentReferences.clear();
        console.log('🧹 Component cache force cleared');
    }
    static reset() {
        if (ComponentRegistryService.instance) {
            ComponentRegistryService.instance.forceClearAll();
            ComponentRegistryService.instance = null;
        }
    }
    getComponentKey(name, namespace, version, sourceRegistryId) {
        const registryPart = sourceRegistryId || 'local';
        const namespacePart = namespace || 'global';
        return `${registryPart}/${namespacePart}/${name}@${version}`;
    }
}
//# sourceMappingURL=component-registry-service.js.map