import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { SelectorResolver } from '../core/selectorResolver.js';
import { Logger } from '../utils/logger.js';
import { TestRegistry } from '../core/registry.js';
import { ConfigManager } from '../utils/configManager.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
 * Generates modular framework files (v2) from a recording session.
 */
export class Generator {
    static async generate(name, events, config) {
        const siteName = config.namespace || config.project || name.split('_')[0] || 'site';
        const baseUrl = config.url || events[0]?.url || '';
        const saveAsFlow = config.saveAsFlow || false;
        // Initialize resolver to ensure we have the latest registry for aliasing
        await SelectorResolver.init();
        // 1. COMPACT EVENTS (De-duplicate keystrokes and group by field)
        let processedEvents = events;
        if (config.isOverwrite) {
            Logger.log(`[Generator] Overwrite mode active. Filtering out ${events.filter(e => e.isBootstrap).length} bootstrap events.`);
            processedEvents = events.filter(e => !e.isBootstrap);
        }
        const compacted = this.compactEvents(processedEvents);
        // Load existing selectors to preserve them
        const selectorPath = path.resolve(__dirname, '../selectors', `${siteName}.selectors.json`);
        const registry = await TestRegistry.getInstance();
        let existingSelectors = {};
        try {
            const existingSet = await registry.getSelectorSet(siteName);
            existingSelectors = existingSet?.mappings || {};
        }
        catch (e) {
            Logger.warn(`Could not load existing selectors for ${siteName}, starting fresh.`);
        }
        // 2. GENERATE REGISTRY
        const newSelectors = { ...existingSelectors };
        const collectSelectors = (evts) => {
            evts.forEach((event, index) => {
                if (!event.selector)
                    return;
                if (event.isBootstrap)
                    return;
                // 1. Check if this exact selector already exists in the CURRENT site's registry
                const existingKeyInSite = Object.keys(newSelectors).find(k => {
                    const val = newSelectors[k];
                    return Array.isArray(val) ? val[0] === event.selector : val === event.selector;
                });
                if (existingKeyInSite) {
                    event.registryAlias = `${siteName}.${existingKeyInSite}`;
                }
                else {
                    // 2. Add to new selectors for current site (including alt selectors for healing)
                    const key = event.label || (event.selector.split(/[#\.\[\s]/)[0] || 'element') + `_${Math.floor(Math.random() * 1000)}`;
                    const candidates = [event.selector, ...(event.altSelectors || [])];
                    newSelectors[key] = [...new Set(candidates.filter(Boolean))];
                }
                // Recursively collect from sub-steps (for scrapeDeep)
                if (event.type === 'scrapeDeep' && event.value) {
                    try {
                        const subEvents = JSON.parse(event.value);
                        collectSelectors(subEvents);
                    }
                    catch (e) { }
                }
            });
        };
        collectSelectors(compacted);
        const getRegistryKey = (event, index) => {
            if (event.registryAlias)
                return event.registryAlias;
            // Only treat as raw if it matches namespace.key AND the namespace exists
            if (event.selector && event.selector.includes('.') && !event.selector.startsWith('#') && !event.selector.startsWith('.') && !event.selector.includes(' ')) {
                const parts = event.selector.split('.');
                if (parts.length === 2 && SelectorResolver.getNamespaces().includes(parts[0])) {
                    return event.selector;
                }
            }
            const key = event.label || (event.selector.split(/[#\.\[\s]/)[0] || 'element') + `_${index}`;
            return `${siteName}.${key}`;
        };
        // 3. GENERATE FLOW
        // Reuse original flow name if resuming, otherwise follow convention
        let flowName = saveAsFlow ? name : `${name}_flow`;
        if (!saveAsFlow && config.isResuming && config.originalFlows?.length > 0) {
            flowName = config.originalFlows[0];
        }
        const flowContents = {
            name: flowName,
            steps: compacted.map((event, index) => {
                return this.mapEventToStep(event, index, siteName, getRegistryKey);
            }).filter((step) => step !== null)
        };
        // Save common assets (DISABLED: DB-ONLY MODE)
        // this.saveFile(selectorPath, newSelectors);
        // this.saveFile(path.resolve(__dirname, '../flows', `${flowName}.flow.json`), flowContents);
        const tags = [...new Set([...(config.tags || [])])].filter(Boolean);
        // Sync to DB
        await registry.persist('selectors', siteName, {
            name: siteName,
            mappings: newSelectors,
            namespace: siteName,
            project: config.project,
            tags,
            organizationId: config.organizationId || ConfigManager.getInstance().getOrganizationId()
        }, { onlyCache: config.onlyCache });
        // Seed the local resolver registry immediately for 'Local Run' capability
        SelectorResolver.updateRegistry(siteName, newSelectors);
        await registry.persist('flows', flowName, {
            ...flowContents,
            namespace: siteName,
            tags,
            project: config.project,
            organizationId: config.organizationId || ConfigManager.getInstance().getOrganizationId()
        }, { onlyCache: config.onlyCache });
        // 2.5 Ensure project and tags exist in the global registry
        if (!config.onlyCache) {
            const orgId = config.organizationId || ConfigManager.getInstance().getOrganizationId();
            if (config.project) {
                await registry.ensureRegistryItem('projects', config.project, orgId);
            }
            for (const tag of tags) {
                await registry.ensureRegistryItem('tags', tag, orgId);
            }
        }
        // Skip scenario-specific assets if in Flow Factory mode
        if (!saveAsFlow) {
            // 4. GENERATE SCENARIO
            const scenario = {
                name,
                mode: config.mode || 'scrape',
                baseUrl,
                doc_type: config.docType || 'generic',
                flows: [flowName],
                sharedDataRefs: (config.sharedDataRefs && config.sharedDataRefs.length > 0) ? config.sharedDataRefs : [],
                namespace: siteName,
                tags,
                project: config.project,
                organizationId: config.organizationId || ConfigManager.getInstance().getOrganizationId(),
                database: {
                    enabled: config.dbEnabled ?? true,
                    collection: config.dbCollection || `${name}_data`
                }
            };
            let finalInputs = compacted.reduce((acc, event, index) => {
                if (event.type === 'input' || event.type === 'validate' || event.type === 'scrape' || event.type === 'select' || event.type === 'type' || event.type === 'scrapeAttr' || event.type === 'scrapeProp') {
                    const registryRef = getRegistryKey(event, index);
                    const key = registryRef.includes('.') ? registryRef.split('.')[1] : registryRef;
                    const inputKey = event.label || key;
                    if (event.value !== undefined && event.value !== null) {
                        acc[inputKey] = event.value;
                    }
                }
                return acc;
            }, {});
            // Merge with local inputData if provided
            scenario.inputData = { ...(config.inputData || {}), ...finalInputs };
            // Sync Scenario to DB
            await registry.persist('scenarios', name, scenario, { onlyCache: config.onlyCache });
            Logger.log(`[Generator v2] ${saveAsFlow ? 'Flow Module' : 'Scenario assets'} generated for: ${name}`);
            return scenario;
        }
        Logger.log(`[Generator v2] ${saveAsFlow ? 'Flow Module' : 'Scenario assets'} generated for: ${name}`);
    }
    static mapEventToStep(event, index, siteName, getRegistryKey) {
        if (event.isBootstrap && event.originalStep) {
            return event.originalStep;
        }
        const registryKey = getRegistryKey(event, index);
        const key = registryKey.includes('.') ? registryKey.split('.')[1] : registryKey;
        if (event.type === 'navigate')
            return { action: 'navigate', target: event.url, timeout: event.timeout, optional: event.optional };
        if (event.type === 'click')
            return { action: 'click', target: registryKey, frame: event.frame, timeout: event.timeout, optional: event.optional };
        if (event.type === 'input' || event.type === 'validate' || event.type === 'scrape' || event.type === 'type' || event.type === 'select') {
            const action = (event.type === 'input' || event.type === 'type') ? 'type' : (event.type === 'scrape' ? 'scrape' : (event.type === 'select' ? 'select' : 'validate'));
            const isParametrized = (event.type === 'input' || event.type === 'type' || event.type === 'select') && (event.value !== undefined && event.value !== null);
            return {
                action,
                target: registryKey,
                inputKey: isParametrized ? (event.label || key) : (action === 'validate' ? `${key}_value` : undefined),
                value: isParametrized ? undefined : event.value,
                attribute: event.attribute,
                frame: event.frame,
                timeout: event.timeout,
                optional: event.optional
            };
        }
        if (event.type === 'scrapeAttr')
            return { action: 'scrapeAttr', target: registryKey, inputKey: `${key}`, value: event.value, frame: event.frame, optional: event.optional };
        if (event.type === 'scrapeProp')
            return { action: 'scrapeProp', target: registryKey, inputKey: `${key}`, value: event.value, frame: event.frame, optional: event.optional };
        if (event.type === 'scrapeItems')
            return { action: 'scrapeItems', target: registryKey, value: event.value, frame: event.frame };
        if (event.type === 'scrapePairs')
            return { action: 'scrapePairs', target: registryKey, label: event.label, frame: event.frame };
        if (event.type === 'scrapeDeep') {
            let subSteps = [];
            if (event.value) {
                try {
                    const subEvents = JSON.parse(event.value);
                    const compactedSub = this.compactEvents(subEvents);
                    // Recursively map sub-events using same siteName and registry logic
                    subSteps = compactedSub
                        .map((e, i) => this.mapEventToStep(e, i, siteName, getRegistryKey))
                        .filter((s) => s !== null);
                }
                catch (e) {
                    Logger.error('Failed to parse sub-steps for scrapeDeep', e);
                }
            }
            return {
                action: 'scrapeDeep',
                target: registryKey,
                steps: subSteps,
                paginateTarget: event.paginateTarget
            };
        }
        if (event.type === 'eval')
            return { action: 'eval', target: registryKey, code: event.code, label: event.label, frame: event.frame };
        if (event.type === 'screenshot')
            return { action: 'screenshot', target: registryKey, inputKey: event.label, frame: event.frame };
        if (event.type === 'regex')
            return { action: 'regex', target: registryKey, regex: event.regex || event.value, label: event.label, frame: event.frame };
        if (event.type === 'wait')
            return { action: 'wait', target: registryKey, value: event.value, timeout: event.timeout, frame: event.frame };
        if (event.type === 'hover')
            return { action: 'hover', target: registryKey, label: event.label, frame: event.frame };
        if (event.type === 'sleep')
            return { action: 'sleep', target: registryKey, value: event.value, label: event.label };
        if (event.type === 'network_idle')
            return { action: 'network_idle', target: registryKey, label: event.label };
        if (event.type === 'dispatch_event')
            return { action: 'dispatch_event', target: registryKey, value: event.value, label: event.label, frame: event.frame };
        if (event.type === 'solve_captcha')
            return { action: 'solve_captcha', label: event.label || 'bypass_captcha' };
        if (event.type === 'humanize')
            return { action: 'humanize', label: event.label || 'stealth_behavior' };
        return null;
    }
    /**
     * Compactor: Merges interaction clusters (Click -> Type) and repetitive inputs.
     */
    static compactEvents(events) {
        const result = [];
        const fieldValues = new Map();
        for (let i = 0; i < events.length; i++) {
            const current = events[i];
            const next = events[i + 1];
            // 1. UNIFY CLICK -> (TYPE or INPUT)
            if (current.type === 'click' && next && (next.type === 'input' || next.type === 'type') && next.selector === current.selector) {
                if (current.label && !next.label)
                    next.label = current.label;
                if (current.optional)
                    next.optional = true;
                continue;
            }
            // 2. COMPACT INPUTS & DYNAMIC TYPES
            if (current.type === 'input' || current.type === 'type') {
                const fieldKey = (current.label || current.selector || 'unknown');
                fieldValues.set(fieldKey, current);
                const isNextOnSame = next && (next.type === 'input' || next.type === 'type') && (next.label === current.label || next.selector === current.selector);
                if (!isNextOnSame) {
                    const finalEvent = fieldValues.get(fieldKey);
                    if (finalEvent)
                        result.push(finalEvent);
                }
            }
            else {
                result.push(current);
            }
        }
        // Ensure isBootstrap flag is preserved through compaction for the first event in result if it was a bootstrap event
        // (Actually result.push(current) already does this)
        return result;
    }
    static saveFile(filePath, content) {
        const dir = path.dirname(filePath);
        if (!fs.existsSync(dir))
            fs.mkdirSync(dir, { recursive: true });
        // If it's a registry, we might want to MERGE instead of overwrite? 
        // For now, simple overwrite to ensure consistency with current session.
        fs.writeFileSync(filePath, JSON.stringify(content, null, 2));
        Logger.log(`[Generator] Wrote: ${filePath}`);
    }
}
