/**
 * Core Rule Engine for V3.1 Inference System
 * Provides pattern matching and rule application for both logical and deployment inference
 */
import * as yaml from 'js-yaml';
import Handlebars from 'handlebars';
import { registerCoreHelpers } from './template-helpers.js';
// Register Handlebars helpers at module load (idempotent). Phase 1 of
// the template-engine rewrite — infrastructure lands here; templates
// continue to run through the old string-substitution apply*Template
// paths below until Phase 2 switches them over. Registering helpers
// early costs nothing and makes them available the moment the rewrite
// swaps in `Handlebars.compile(...)`.
// See docs/plans/2026-04-17-HANDLEBARS-TEMPLATE-ENGINE-REWRITE.md.
registerCoreHelpers();
export class RuleEngine {
    debug;
    rules = new Map();
    conditionCache = new Map();
    compiledConditions = new WeakMap();
    // Single-entry memoization for the template context. Generators call
    // apply() in a for-loop over matching rules with the same (input, context)
    // pair. Before this cache, buildTemplateContext rebuilt identical helper
    // closures on every iteration.
    lastTemplateContext = null;
    stats = {
        conditionsCompiled: 0,
        conditionCacheHits: 0,
        conditionCacheMisses: 0,
        templateContextHits: 0,
        templateContextMisses: 0,
    };
    constructor(debug = false) {
        this.debug = debug;
    }
    /**
     * Load rules for a specific category (e.g., 'controllers', 'services')
     */
    loadRules(category, rules) {
        const validation = {
            valid: true,
            errors: [],
            warnings: []
        };
        // Validate rules before loading
        for (const rule of rules) {
            const ruleValidation = this.validateRule(rule);
            validation.errors.push(...ruleValidation.errors);
            validation.warnings.push(...ruleValidation.warnings);
            if (ruleValidation.errors.length > 0) {
                validation.valid = false;
            }
        }
        if (validation.valid) {
            // Sort rules by priority (highest first)
            const sortedRules = [...rules].sort((a, b) => b.priority - a.priority);
            this.rules.set(category, sortedRules);
            // Compile each rule's condition once at load time. Subsequent
            // evaluations reuse the compiled function. Without this, every
            // evaluateCondition() call re-parsed the expression via new Function().
            for (const rule of sortedRules) {
                if (rule.condition && !this.compiledConditions.has(rule)) {
                    this.compiledConditions.set(rule, this.compileCondition(rule.condition));
                    this.stats.conditionsCompiled++;
                }
            }
            if (this.debug) {
                console.log(`Loaded ${rules.length} rules for category: ${category}`);
                console.log(`Rule priorities: ${sortedRules.map(r => `${r.name}:${r.priority}`).join(', ')}`);
            }
        }
        return validation;
    }
    /**
     * Compile a condition expression to a reusable function. Called once per
     * rule at load time (or from the validator, which exercises the parse).
     */
    compileCondition(condition) {
        return new Function('input', 'context', 'model', 'relationships', 'hasParentRelationship', 'hasChildRelationships', 'hasLifecycle', 'belongsTo', 'hasMany', `return ${condition}`);
    }
    /**
     * Return cache + compilation statistics. Useful for measuring the impact
     * of the condition cache and spotting cache-defeating call patterns.
     */
    getStatistics() {
        const rulesLoaded = Array.from(this.rules.values()).reduce((sum, rs) => sum + rs.length, 0);
        const total = this.stats.conditionCacheHits + this.stats.conditionCacheMisses;
        return {
            rulesLoaded,
            conditionsCompiled: this.stats.conditionsCompiled,
            conditionCacheSize: this.conditionCache.size,
            conditionCacheHits: this.stats.conditionCacheHits,
            conditionCacheMisses: this.stats.conditionCacheMisses,
            conditionCacheHitRate: total === 0 ? 0 : this.stats.conditionCacheHits / total,
            templateContextHits: this.stats.templateContextHits,
            templateContextMisses: this.stats.templateContextMisses,
        };
    }
    /**
     * Find all matching rules for input in a category
     */
    findMatches(category, input, context) {
        const categoryRules = this.rules.get(category) || [];
        const matches = [];
        for (const rule of categoryRules) {
            if (this.evaluateCondition(rule, input, context)) {
                matches.push(rule);
                if (this.debug) {
                    console.log(`Rule matched: ${rule.name} (priority: ${rule.priority})`);
                }
            }
        }
        return matches;
    }
    /**
     * Apply a rule to generate output
     */
    apply(rule, input, context) {
        try {
            // Template context is expensive to build (closures for humanize,
            // pluralize, etc.). Generators call apply() in a loop over all
            // matching rules with the same (input, context) pair — reuse the
            // last built context when the references match.
            let templateContext;
            if (this.lastTemplateContext &&
                this.lastTemplateContext.input === input &&
                this.lastTemplateContext.context === context) {
                templateContext = this.lastTemplateContext.built;
                this.stats.templateContextHits++;
            }
            else {
                templateContext = this.buildTemplateContext(input, context);
                this.lastTemplateContext = { input, context, built: templateContext };
                this.stats.templateContextMisses++;
            }
            // Apply template based on type
            switch (rule.template.type) {
                case 'handlebars':
                    return this.applyHandlebarsTemplate(rule, templateContext);
                case 'json':
                    return this.applyJsonTemplate(rule, templateContext);
                case 'yaml':
                    return this.applyYamlTemplate(rule, templateContext);
                case 'specly':
                    return this.applySpeclyTemplate(rule, templateContext);
                default:
                    throw new Error(`Unsupported template type: ${rule.template.type}`);
            }
        }
        catch (error) {
            throw new Error(`Failed to apply rule '${rule.name}': ${error instanceof Error ? error.message : String(error)}`);
        }
    }
    /**
     * Get all loaded rules for debugging
     */
    getAllRules() {
        return new Map(this.rules);
    }
    /**
     * Clear all loaded rules
     */
    clearRules() {
        this.rules.clear();
        this.conditionCache.clear();
        // compiledConditions is a WeakMap; entries are released once the rule
        // objects themselves become unreachable.
        this.lastTemplateContext = null;
        this.stats = {
            conditionsCompiled: 0,
            conditionCacheHits: 0,
            conditionCacheMisses: 0,
            templateContextHits: 0,
            templateContextMisses: 0,
        };
    }
    // ===============================
    // Private Implementation
    // ===============================
    validateRule(rule) {
        const result = {
            valid: true,
            errors: [],
            warnings: []
        };
        // Validate required fields
        if (!rule.name) {
            result.errors.push({
                code: 'MISSING_NAME',
                message: 'Rule must have a name',
                location: 'rule.name'
            });
        }
        if (!rule.pattern) {
            result.errors.push({
                code: 'MISSING_PATTERN',
                message: 'Rule must have a pattern',
                location: 'rule.pattern'
            });
        }
        if (!rule.template || !rule.template.content) {
            result.errors.push({
                code: 'MISSING_TEMPLATE',
                message: 'Rule must have template content',
                location: 'rule.template.content'
            });
        }
        // Validate priority
        if (typeof rule.priority !== 'number') {
            result.errors.push({
                code: 'INVALID_PRIORITY',
                message: 'Rule priority must be a number',
                location: 'rule.priority'
            });
        }
        // Validate condition syntax if present
        if (rule.condition) {
            try {
                // Basic syntax check - attempt to create function
                new Function('input', 'context', `return ${rule.condition}`);
            }
            catch (error) {
                result.errors.push({
                    code: 'INVALID_CONDITION',
                    message: `Invalid condition syntax: ${error instanceof Error ? error.message : String(error)}`,
                    location: 'rule.condition'
                });
            }
        }
        // Validate template type
        const validTemplateTypes = ['handlebars', 'json', 'yaml', 'specly'];
        if (rule.template && !validTemplateTypes.includes(rule.template.type)) {
            result.errors.push({
                code: 'INVALID_TEMPLATE_TYPE',
                message: `Template type must be one of: ${validTemplateTypes.join(', ')}`,
                location: 'rule.template.type'
            });
        }
        // Warnings for best practices
        if (!rule.description) {
            result.warnings.push({
                code: 'MISSING_DESCRIPTION',
                message: 'Rule should have a description for documentation',
                location: 'rule.description'
            });
        }
        if (rule.priority === 0) {
            result.warnings.push({
                code: 'ZERO_PRIORITY',
                message: 'Rule priority of 0 may cause unexpected ordering',
                location: 'rule.priority'
            });
        }
        result.valid = result.errors.length === 0;
        return result;
    }
    evaluateCondition(rule, input, context) {
        if (!rule.condition) {
            return true; // No condition means always matches
        }
        // Cache key is the rule name + a compact signature of the shape the
        // condition can observe (current model name + relationship topology +
        // lifecycle presence). The previous key serialized the entire input
        // and context via JSON.stringify per call, which is both expensive
        // and instance-specific — so identical-shaped inputs kept missing.
        const cacheKey = this.buildConditionCacheKey(rule, input, context);
        const cached = this.conditionCache.get(cacheKey);
        if (cached !== undefined) {
            this.stats.conditionCacheHits++;
            return cached;
        }
        this.stats.conditionCacheMisses++;
        let conditionFn = this.compiledConditions.get(rule);
        if (!conditionFn) {
            // Rule didn't go through loadRules() (direct test use, etc.) — compile
            // lazily so behaviour matches the normal path.
            conditionFn = this.compileCondition(rule.condition);
            this.compiledConditions.set(rule, conditionFn);
            this.stats.conditionsCompiled++;
        }
        try {
            const model = context.currentModel;
            const relationships = context.relationships;
            const hasParentRelationship = () => (relationships?.parentRelationships?.length || 0) > 0;
            const hasChildRelationships = () => (relationships?.childRelationships?.length || 0) > 0;
            const hasLifecycle = () => model?.lifecycle !== undefined;
            const belongsTo = (targetModel) => relationships?.parentRelationships?.some(r => r.targetModel === targetModel) || false;
            const hasMany = (targetModel) => relationships?.childRelationships?.some(r => r.name === targetModel) || false;
            const result = conditionFn(input, context, model, relationships, hasParentRelationship, hasChildRelationships, hasLifecycle, belongsTo, hasMany);
            this.conditionCache.set(cacheKey, result);
            return result;
        }
        catch (error) {
            if (this.debug) {
                console.warn(`Condition evaluation failed for rule '${rule.name}': ${error instanceof Error ? error.message : String(error)}`);
            }
            return false;
        }
    }
    /**
     * Compact cache key describing only what the condition can observe. Two
     * invocations with the same rule + model + relationship topology + lifecycle
     * presence produce identical keys — unlike the old JSON.stringify-based
     * key, which was instance-specific.
     */
    buildConditionCacheKey(rule, input, context) {
        const modelName = (context.currentModel?.name) ?? input?.name ?? '';
        const rels = context.relationships;
        const parentCount = rels?.parentRelationships?.length || 0;
        const childCount = rels?.childRelationships?.length || 0;
        const m2mCount = rels?.manyToManyRelationships?.length || 0;
        const cascadeCount = rels?.cascadeDeleteTargets?.length || 0;
        const lifecycle = context.currentModel?.lifecycle ? 'L' : '';
        const profiles = (context.currentModel?.profiles?.length || 0);
        return `${rule.name}|${modelName}|p${parentCount}|c${childCount}|m${m2mCount}|d${cascadeCount}|${lifecycle}|f${profiles}`;
    }
    buildTemplateContext(input, context) {
        const model = context.currentModel;
        const relationships = context.relationships;
        return {
            // Input data
            input,
            model,
            modelName: model?.name,
            // Context data
            context,
            version: context.version,
            models: context.models,
            // Relationship analysis
            relationships,
            parentRelationships: relationships?.parentRelationships || [],
            childRelationships: relationships?.childRelationships || [],
            manyToManyRelationships: relationships?.manyToManyRelationships || [],
            // Naming helpers
            controllerName: model?.name ? `${model.name}Controller` : undefined,
            serviceName: model?.name ? `${model.name}Service` : undefined,
            // Common patterns
            curedOps: ['create', 'update', 'retrieve', 'retrieve_many', 'evolve', 'delete'],
            // Helper functions for templates
            humanize: (str) => {
                if (typeof str !== 'string') {
                    return String(str || '');
                }
                return str
                    .replace(/([A-Z])/g, ' $1')
                    .replace(/^./, c => c.toUpperCase())
                    .trim();
            },
            camelCase: (str) => {
                const s = typeof str === 'string' ? str : String(str || '');
                return s.charAt(0).toLowerCase() + s.slice(1);
            },
            pascalCase: (str) => {
                const s = typeof str === 'string' ? str : String(str || '');
                return s.charAt(0).toUpperCase() + s.slice(1);
            },
            pluralize: (str) => {
                const s = typeof str === 'string' ? str : String(str || '');
                if (s.endsWith('y')) {
                    return s.slice(0, -1) + 'ies';
                }
                if (s.endsWith('s') || s.endsWith('x') || s.endsWith('z') || s.endsWith('ch') || s.endsWith('sh')) {
                    return s + 'es';
                }
                return s + 's';
            }
        };
    }
    applyHandlebarsTemplate(rule, templateContext) {
        // Cycle 2: real Handlebars. Previously this method ran one-off
        // hacks (hardcoded parentRelationships[0].targetModel substitution,
        // blanket /each/ strip) because the home-rolled substitutor didn't
        // understand `{{#each}}` or sub-expressions. Handlebars handles
        // those natively — if multiple parentRelationships exist, the
        // iteration emits one block per entry instead of just the first.
        const template = Array.isArray(rule.template.content)
            ? rule.template.content.join('\n')
            : rule.template.content;
        if (typeof template !== 'string') {
            throw new Error(`Template content must be string or string array, got ${typeof template} in applyHandlebarsTemplate`);
        }
        const rendered = Handlebars.compile(template, { strict: false, noEscape: true })(templateContext);
        // If rendered output looks like JSON, parse it; otherwise return
        // as string. Same shape as before, but the renderer now produces
        // valid JSON more often (proper iteration, helpers resolved).
        try {
            const trimmed = rendered.trim();
            if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
                return JSON.parse(rendered);
            }
        }
        catch {
            // fall through to string return
        }
        return rendered;
    }
    applyJsonTemplate(rule, templateContext) {
        // Cycle 2 of the template-engine rewrite: JSON templates now go
        // through real Handlebars. Helpers (eq / humanize / pluralize /
        // lowerCase / upperCase) are globally registered at module load.
        // strict:false makes missing variables render as empty strings
        // (old behaviour preserved the literal `{{name}}` — which would
        // break JSON.parse anyway, so this is strictly safer). noEscape
        // because rule output is machine-read JSON, not HTML.
        try {
            const template = Array.isArray(rule.template.content)
                ? rule.template.content.join('\n')
                : rule.template.content;
            const rendered = Handlebars.compile(template, { strict: false, noEscape: true })(templateContext);
            return JSON.parse(rendered);
        }
        catch (error) {
            throw new Error(`Failed to parse JSON template: ${error instanceof Error ? error.message : String(error)}`);
        }
    }
    applyYamlTemplate(rule, templateContext) {
        // Cycle 3: real Handlebars. Replaces four load-bearing hacks:
        //   1. Hardcoded parentRelationships[0].targetModel substitution
        //      — real Handlebars' \`{{#each}}\` iterates every entry.
        //   2. Global /each/ strip — ate nested closers. Real Handlebars
        //      pairs opens/closes correctly.
        //   3. Fake \`{{#if required}}\` (always passed through) — real
        //      Handlebars evaluates the condition against loop context.
        //   4. Hardcoded \`{{name}}\` → 'sampleAttribute' when parent
        //      relationships exist — real Handlebars resolves \`{{name}}\`
        //      against the iterated attribute, which is what the template
        //      always intended.
        // Plus the processSpeclyHandlebars conditional handler, also gone.
        try {
            const template = Array.isArray(rule.template.content)
                ? rule.template.content.join('\n')
                : rule.template.content;
            if (typeof template !== 'string') {
                throw new Error(`Template content must be string or string array, got ${typeof template}`);
            }
            const rendered = Handlebars.compile(template, { strict: false, noEscape: true })(templateContext);
            // Collapse runs of blank lines — same cleanup the old path did,
            // still useful because YAML parsers reject trailing garbage in
            // some shapes.
            const cleaned = rendered.replace(/\n\n+/g, '\n\n');
            return yaml.load(cleaned);
        }
        catch (error) {
            throw new Error(`Failed to parse YAML template: ${error instanceof Error ? error.message : String(error)}`);
        }
    }
    applySpeclyTemplate(rule, templateContext) {
        try {
            // StandardCURED now has a real Handlebars template in
            // entities/src/core/models/inference/controller-rules.json
            // that renders to the equivalent ControllerSpec. Generator
            // method removed. See Cycle 5 in the Handlebars-rewrite plan.
            //
            // ProcessingService / ValidationService / RelationshipService
            // rules + generators fully retired (Cycle 6). See
            // engines/src/inference/DISABLED-RULES.md for the rationale.
            //
            // StandardEvents (Cycle 7) + RelationshipEvents (Cycle 8) now
            // render from real Handlebars templates in
            // entities/src/core/events/inference/event-rules.json.
            // EnhancedListView / AnalyticsView (Cycle 9): retired.
            // Superseded by the view-triggered specialist-views expansion
            // system (specialist_analytics, specialist_dashboard, etc.) —
            // see entities/src/core/views/inference/specialist-views.json
            // and engines/src/inference/DISABLED-RULES.md.
            // IntegrationService (Cycle 10) + LifecycleService (Cycle 11)
            // now render from real Handlebars templates in service-rules.json.
            // Cycle 5: real Handlebars + yaml.load for specly templates that
            // produce a structured object (like the cured_root_controller
            // template which renders a ControllerSpec). Falls back to the
            // raw string if yaml.load fails — for templates that genuinely
            // produce non-YAML output.
            const template = Array.isArray(rule.template.content)
                ? rule.template.content.join('\n')
                : rule.template.content;
            const rendered = Handlebars.compile(template, { strict: false, noEscape: true })(templateContext);
            try {
                const trimmed = rendered.trim();
                if (trimmed.includes(':') || trimmed.startsWith('[') || trimmed.startsWith('{')) {
                    return yaml.load(rendered);
                }
            }
            catch {
                // YAML parse failed — return the raw rendered string.
            }
            return rendered;
        }
        catch (error) {
            throw new Error(`Failed to process Specly template: ${error instanceof Error ? error.message : String(error)}`);
        }
    }
    // processSpeclyHandlebars / processHandlebarsInJson /
    // processHandlebarsConditionals — removed in Cycle 3. They were
    // the home-rolled string-substitution layer that the four
    // apply*Template methods delegated to. All four methods now go
    // through real Handlebars (Handlebars.compile + render), so these
    // have zero callers and are gone.
    convertJsonToSpecly(obj) {
        // Convert processed JSON object to .specly syntax
        let result = '';
        if (obj.description) {
            result += `description: "${obj.description}"\n`;
        }
        if (obj.model) {
            result += `model: ${obj.model}\n`;
        }
        if (obj.actions) {
            result += 'actions:\n';
            for (const [actionName, action] of Object.entries(obj.actions)) {
                result += `  ${actionName}:\n`;
                const actionObj = action;
                if (actionObj.parameters) {
                    result += '    parameters:\n';
                    for (const [paramName, paramType] of Object.entries(actionObj.parameters)) {
                        result += `      ${paramName}: ${paramType}\n`;
                    }
                }
                if (actionObj.returns) {
                    result += `    returns: ${actionObj.returns}\n`;
                }
                if (actionObj.requires && Array.isArray(actionObj.requires)) {
                    result += '    requires:\n';
                    for (const req of actionObj.requires) {
                        result += `      - "${req}"\n`;
                    }
                }
                if (actionObj.ensures && Array.isArray(actionObj.ensures)) {
                    result += '    ensures:\n';
                    for (const ens of actionObj.ensures) {
                        result += `      - "${ens}"\n`;
                    }
                }
                if (actionObj.publishes && Array.isArray(actionObj.publishes)) {
                    result += '    publishes:\n';
                    for (const pub of actionObj.publishes) {
                        result += `      - ${pub}\n`;
                    }
                }
            }
        }
        return result;
    }
}
//# sourceMappingURL=rule-engine.js.map