import { MongoManager } from '../db/mongoClient.js';
import { ConfigManager } from '../utils/configManager.js';
import { Logger } from '../utils/logger.js';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const configManager = ConfigManager.getInstance();
/**
 * Centralized registry for loading Scenarios, Flows, and Selectors.
 * Purely Database-Driven (MongoDB).
 */
export class TestRegistry {
    static instance;
    scenarioCache = new Map();
    flowCache = new Map();
    selectorCache = new Map();
    sharedDataCache = new Map();
    mongo = null;
    constructor() { }
    getMongo() {
        return this.mongo;
    }
    static async getInstance() {
        if (!this.instance) {
            this.instance = new TestRegistry();
            await this.instance.init();
        }
        return this.instance;
    }
    async init() {
        const config = ConfigManager.getInstance().getMongoConfig();
        const remote = ConfigManager.getInstance().getRemoteConfig();
        if (remote.mode === 'api' && remote.token) {
            Logger.log(`[TestRegistry] Initialized in REMOTE mode (API: ${remote.apiUrl})`);
            return;
        }
        try {
            if (config.enabled) {
                this.mongo = await MongoManager.getInstance(config);
                await this.mongo.connect();
            }
        }
        catch (err) {
            Logger.error('Failed to initialize Mongo in TestRegistry', err);
        }
    }
    async fetchFromApi(endpoint, options = {}) {
        const remote = ConfigManager.getInstance().getRemoteConfig();
        const url = `${remote.apiUrl}${endpoint}`;
        const res = await fetch(url, {
            ...options,
            headers: {
                'Authorization': `Bearer ${remote.token}`,
                'Content-Type': 'application/json',
                ...options.headers
            }
        });
        if (!res.ok) {
            const error = await res.json().catch(() => ({ error: 'Unknown API error' }));
            throw new Error(`Registry API Error [${res.status}]: ${error.error || res.statusText}`);
        }
        return await res.json();
    }
    checkMongo() {
        if (!this.mongo) {
            throw new Error('Database connection not initialized. Check framework/config/db.json');
        }
    }
    /**
     * Loads a scenario by name and organization.
     */
    async getScenario(name, organizationId) {
        const orgId = organizationId || ConfigManager.getInstance().getOrganizationId();
        const cleanName = name.replace('.json', '').split('/').pop() || name;
        const cacheKey = `${orgId}:${cleanName}`;
        if (this.scenarioCache.has(cacheKey))
            return this.scenarioCache.get(cacheKey);
        const remote = ConfigManager.getInstance().getRemoteConfig();
        if (remote.mode === 'api') {
            const scenario = await this.fetchFromApi(`/scenarios/${cleanName}`);
            this.scenarioCache.set(cacheKey, scenario);
            return scenario;
        }
        this.checkMongo();
        // 1. Try Exact Match
        let dbScenario = await this.mongo.findOne('scenarios', { name: cleanName, organizationId });
        // 2. Fallback to Suffixes if not found
        if (!dbScenario) {
            for (const suffix of ['_test', '_scrapper']) {
                if (!cleanName.endsWith(suffix)) {
                    const suffixedName = `${cleanName}${suffix}`;
                    dbScenario = await this.mongo.findOne('scenarios', { name: suffixedName, organizationId });
                    if (dbScenario)
                        break;
                }
            }
        }
        if (dbScenario) {
            this.scenarioCache.set(cacheKey, dbScenario);
            return dbScenario;
        }
        throw new Error(`Scenario not found for Org [${organizationId}]: ${cleanName}`);
    }
    /**
     * Fetches all tests of a specific type for an organization.
     */
    async getTests(type, organizationId) {
        const remote = ConfigManager.getInstance().getRemoteConfig();
        if (remote.mode === 'api') {
            const endpoint = type === 'sharedData' ? '/shared-data' : `/${type}`;
            return await this.fetchFromApi(endpoint);
        }
        this.checkMongo();
        const collection = type === 'sharedData' ? 'shared_data' : type;
        return await this.mongo.findTests(collection, { organizationId });
    }
    /**
     * Loads a flow by name and organization.
     */
    async getFlow(name, organizationId) {
        const orgId = organizationId || ConfigManager.getInstance().getOrganizationId();
        const cleanName = name.replace('.flow.json', '').split('/').pop() || name;
        const cacheKey = `${orgId}:${cleanName}`;
        if (this.flowCache.has(cacheKey))
            return this.flowCache.get(cacheKey);
        const remote = ConfigManager.getInstance().getRemoteConfig();
        if (remote.mode === 'api') {
            const flow = await this.fetchFromApi(`/flows/${cleanName}`);
            this.flowCache.set(cacheKey, flow);
            return flow;
        }
        this.checkMongo();
        const dbFlow = await this.mongo.findOne('flows', { name: cleanName, organizationId });
        if (dbFlow) {
            this.flowCache.set(cacheKey, dbFlow);
            return dbFlow;
        }
        throw new Error(`Flow not found for Org [${organizationId}]: ${cleanName}`);
    }
    /**
     * Loads shared data by name and organization.
     */
    async getSharedData(name, organizationId) {
        const orgId = organizationId || ConfigManager.getInstance().getOrganizationId();
        const cleanName = name.replace('.json', '').split('/').pop() || name;
        const cacheKey = `${orgId}:${cleanName}`;
        if (this.sharedDataCache.has(cacheKey))
            return this.sharedDataCache.get(cacheKey);
        const remote = ConfigManager.getInstance().getRemoteConfig();
        if (remote.mode === 'api') {
            const data = await this.fetchFromApi(`/shared-data/${cleanName}`);
            this.sharedDataCache.set(cacheKey, data);
            return data;
        }
        this.checkMongo();
        const data = await this.mongo.findOne('shared_data', { name: cleanName, organizationId });
        if (data) {
            this.sharedDataCache.set(cacheKey, data);
            return data;
        }
        throw new Error(`Shared data not found for Org [${organizationId}]: ${cleanName}`);
    }
    /**
     * Loads a selector set by name and organization.
     */
    async getSelectorSet(name, organizationId) {
        const orgId = organizationId || ConfigManager.getInstance().getOrganizationId();
        const cacheKey = `${orgId}:${name}`;
        if (this.selectorCache.has(cacheKey))
            return this.selectorCache.get(cacheKey);
        const remote = ConfigManager.getInstance().getRemoteConfig();
        if (remote.mode === 'api') {
            const data = await this.fetchFromApi(`/selectors/${name}`);
            this.selectorCache.set(cacheKey, data);
            return data;
        }
        this.checkMongo();
        const data = await this.mongo.findOne('selectors', { name, organizationId });
        if (data) {
            this.selectorCache.set(cacheKey, data);
            return data;
        }
        throw new Error(`Selector set not found for Org [${organizationId}]: ${name}`);
    }
    /**
     * Fetches a sample record from a collection for bootstrap resolution.
     */
    async getSampleData(collection) {
        const remote = ConfigManager.getInstance().getRemoteConfig();
        if (remote.mode === 'api') {
            try {
                return await this.fetchFromApi(`/collections/${collection}/sample`);
            }
            catch (err) {
                Logger.warn(`[Registry] Failed to fetch sample data for ${collection} via API: ${err.message}`);
                return null;
            }
        }
        this.checkMongo();
        return await this.mongo.findOne(collection, {});
    }
    /**
     * Lists all scenarios.
     */
    async listScenarios(organizationId, filters = {}, limit = 50, skip = 0) {
        const orgId = organizationId || ConfigManager.getInstance().getOrganizationId();
        const remote = ConfigManager.getInstance().getRemoteConfig();
        if (remote.mode === 'api') {
            return await this.fetchFromApi('/scenarios');
        }
        this.checkMongo();
        return await this.mongo.findTests('scenarios', { ...filters, organizationId: orgId }, limit, skip);
    }
    /**
     * Lists all flows.
     */
    async listFlows(organizationId, filters = {}) {
        const orgId = organizationId || ConfigManager.getInstance().getOrganizationId();
        const remote = ConfigManager.getInstance().getRemoteConfig();
        if (remote.mode === 'api') {
            return await this.fetchFromApi('/flows');
        }
        this.checkMongo();
        return await this.mongo.findTests('flows', { ...filters, organizationId: orgId });
    }
    /**
     * Lists all selector sets.
     */
    async listSelectors(organizationId, filters = {}) {
        const orgId = organizationId || ConfigManager.getInstance().getOrganizationId();
        const remote = ConfigManager.getInstance().getRemoteConfig();
        if (remote.mode === 'api') {
            return await this.fetchFromApi('/selectors');
        }
        this.checkMongo();
        return await this.mongo.findTests('selectors', { ...filters, organizationId: orgId });
    }
    /**
     * Lists all shared data sets.
     */
    async listSharedData(organizationId, filters = {}) {
        const orgId = organizationId || ConfigManager.getInstance().getOrganizationId();
        const remote = ConfigManager.getInstance().getRemoteConfig();
        if (remote.mode === 'api') {
            return await this.fetchFromApi('/shared-data');
        }
        this.checkMongo();
        return await this.mongo.findTests('shared_data', { ...filters, organizationId: orgId });
    }
    /**
     * Finds tests by tag.
     */
    async getScenariosByTag(tag, organizationId) {
        const orgId = organizationId || ConfigManager.getInstance().getOrganizationId();
        this.checkMongo();
        return await this.mongo.findTests('scenarios', { tags: tag, organizationId: orgId });
    }
    /**
     * Finds tests by namespace.
     */
    async getScenariosByNamespace(namespace, organizationId) {
        const orgId = organizationId || ConfigManager.getInstance().getOrganizationId();
        this.checkMongo();
        return await this.mongo.findTests('scenarios', { namespace, organizationId: orgId });
    }
    /**
     * Saves a definition to DB with organization scoping.
     */
    async persist(type, name, data, options = {}) {
        const cleanName = name.replace('.json', '').split('/').pop() || name;
        const remote = ConfigManager.getInstance().getRemoteConfig();
        if (remote.mode === 'api' && !options.onlyCache) {
            const endpointMap = {
                scenarios: '/scenarios',
                flows: '/flows',
                selectors: '/selectors',
                sharedData: '/shared-data',
                results: '/collections' // New endpoint for raw data
            };
            const payload = {
                name: cleanName,
                project: data.project?.toLowerCase(),
                namespace: data.namespace?.toLowerCase(),
                tags: data.tags?.map((t) => t.toLowerCase()),
                organizationId: data.organizationId || ConfigManager.getInstance().getOrganizationId()
            };
            if (type === 'scenarios')
                payload.config = data;
            if (type === 'flows')
                payload.steps = data.steps;
            if (type === 'selectors') {
                payload.mappings = data.mappings;
                payload.namespace = cleanName;
            }
            if (type === 'sharedData')
                payload.data = data.data;
            if (type === 'results') {
                payload.data = data;
                payload.collection = name;
            }
            await fetch(`${remote.apiUrl}${endpointMap[type]}`, {
                method: 'POST',
                headers: {
                    'Authorization': `Bearer ${remote.token}`,
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(payload)
            });
            // Update Cache (Always, so it's available for Preview runs)
            const orgId = data.organizationId || ConfigManager.getInstance().getOrganizationId();
            const cacheKey = `${orgId}:${cleanName}`;
            if (type === 'scenarios')
                this.scenarioCache.set(cacheKey, data);
            if (type === 'flows')
                this.flowCache.set(cacheKey, data);
            if (type === 'selectors')
                this.selectorCache.set(cacheKey, data);
            if (type === 'sharedData')
                this.sharedDataCache.set(cacheKey, data);
            return;
        }
        if (options.onlyCache) {
            const cacheKey = `${data.organizationId || ConfigManager.getInstance().getOrganizationId()}:${cleanName}`;
            if (type === 'scenarios')
                this.scenarioCache.set(cacheKey, data);
            if (type === 'flows')
                this.flowCache.set(cacheKey, data);
            if (type === 'selectors')
                this.selectorCache.set(cacheKey, data);
            if (type === 'sharedData')
                this.sharedDataCache.set(cacheKey, data);
            return;
        }
        this.checkMongo();
        if (!data.organizationId && type !== 'results') {
            throw new Error(`Cannot persist ${type} [${name}]: Missing organizationId`);
        }
        const collection = type === 'results' ? name : (type === 'sharedData' ? 'shared_data' : type);
        const col = this.mongo.db.collection(collection);
        if (type === 'results') {
            const { doc_hash, organizationId } = data;
            if (doc_hash) {
                await col.updateOne({ doc_hash, organizationId }, {
                    $set: { ...data, collection: name, updatedAt: new Date() },
                    $setOnInsert: { createdAt: new Date() }
                }, { upsert: true });
            }
            else {
                await col.insertOne({ ...data, collection: name, createdAt: new Date() });
            }
            return;
        }
        const { _id, createdAt, ...updateData } = data;
        await col.updateOne({ name: cleanName, organizationId: data.organizationId }, {
            $set: { ...updateData, updatedAt: new Date() },
            $setOnInsert: { createdAt: new Date() }
        }, { upsert: true });
        // Update Cache
        const cacheKey = `${data.organizationId}:${cleanName}`;
        if (type === 'scenarios')
            this.scenarioCache.set(cacheKey, data);
        if (type === 'flows')
            this.flowCache.set(cacheKey, data);
    }
    /**
     * Ensures a project or tag exists in the global registry.
     */
    async ensureRegistryItem(type, name, organizationId) {
        const remote = ConfigManager.getInstance().getRemoteConfig();
        if (remote.mode === 'api')
            return; // Handled implicitly by server on asset save
        this.checkMongo();
        const lowerName = name.toLowerCase();
        const col = this.mongo.db.collection(type);
        await col.updateOne({ name: lowerName, organizationId }, {
            $setOnInsert: { name: lowerName, organizationId, createdAt: new Date() }
        }, { upsert: true });
    }
    /**
     * Saves a run log.
     */
    async saveRunLog(runId, batchId, log, organizationId) {
        const orgId = organizationId || ConfigManager.getInstance().getOrganizationId();
        const remote = ConfigManager.getInstance().getRemoteConfig();
        if (remote.mode === 'api') {
            await fetch(`${remote.apiUrl}/logs`, {
                method: 'POST',
                headers: {
                    'Authorization': `Bearer ${remote.token}`,
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ runId, batchId, organizationId, ...log })
            }).catch(() => { });
            return;
        }
        this.checkMongo();
        await this.mongo.saveRunLog(runId, batchId, log, organizationId);
    }
    /**
     * Fetches organization-specific settings.
     */
    async getOrganizationSettings(organizationId) {
        const remote = ConfigManager.getInstance().getRemoteConfig();
        if (remote.mode === 'api') {
            return await this.fetchFromApi('/settings/org');
        }
        this.checkMongo();
        const org = await this.mongo.findOne('organizations', { organizationId });
        return org?.settings || {};
    }
    /**
     * Deletes a definition from DB.
     */
    async deleteTest(type, name, organizationId) {
        const orgId = organizationId || ConfigManager.getInstance().getOrganizationId();
        const cleanName = name.replace('.json', '').split('/').pop() || name;
        const remote = ConfigManager.getInstance().getRemoteConfig();
        if (remote.mode === 'api') {
            await this.fetchFromApi(`/${type}/${cleanName}`, { method: 'DELETE' });
            return;
        }
        this.checkMongo();
        const collection = type === 'sharedData' ? 'shared_data' : type;
        await this.mongo.deleteFrom(collection, { name: cleanName, organizationId });
        // Update Cache
        const cacheKey = `${organizationId}:${cleanName}`;
        if (type === 'scenarios') {
            this.scenarioCache.delete(cacheKey);
        }
        else if (type === 'flows') {
            this.flowCache.delete(cacheKey);
        }
    }
    /**
     * Fetches distinct values for a field.
     */
    /**
     * Fetches distinct values for a field.
     */
    async distinct(collectionName, fieldName, query = {}) {
        this.checkMongo();
        return await this.mongo.distinct(collectionName, fieldName, query);
    }
    /**
     * Updates multiple documents in a collection.
     */
    async updateMany(collectionName, query, update) {
        this.checkMongo();
        await this.mongo.updateMany(collectionName, query, update);
    }
}
