import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { randomUUID } from 'crypto';
import { dedupeArtifactsByPreference, deriveCatalogMetadata, detectDomains, readMarkdownArtifacts, slugify, } from './runtime-assets.js';
import { nexusEventBus } from './event-bus.js';
export const LIFECYCLE_TOUCHPOINTS = [
    {
        id: 'memory-review',
        trigger: 'memory.stored',
        phase: 'mid-run',
        channel: 'hook',
        selectors: ['memory-stored-review', 'memory-shield-escalation', 'memory-governance-automation'],
        deterministicOrder: 'catalog-priority-then-name',
        canonicalCatalog: '.agents',
        compatibilityCatalog: '.agent',
    },
    {
        id: 'failure-summary',
        trigger: 'run.failed',
        phase: 'completion',
        channel: 'hook',
        selectors: ['failure-summary', 'failure-followup', 'failure-recovery-automation'],
        deterministicOrder: 'catalog-priority-then-name',
        canonicalCatalog: '.agents',
        compatibilityCatalog: '.agent',
    },
    {
        id: 'session-close',
        trigger: 'run.verified',
        phase: 'completion',
        channel: 'automation',
        selectors: ['session-close-followup', 'verified-followup-automation'],
        deterministicOrder: 'catalog-priority-then-name',
        canonicalCatalog: '.agents',
        compatibilityCatalog: '.agent',
    },
];
const BUILTIN_HOOKS = [
    {
        name: 'run-created-brief',
        description: 'Attach planning context and default orchestration on run creation.',
        trigger: 'run.created',
        riskClass: 'read',
        conditions: ['run created'],
        guardrails: ['Do not widen scope before reading plan is available.'],
        skillSelectors: ['orchestration-playbook'],
        workflowSelectors: ['workflows-execution-loop'],
        domain: 'orchestration',
    },
    {
        name: 'before-verify-approval',
        description: 'Attach approval-loop workflows before verification for domain work.',
        trigger: 'before-verify',
        riskClass: 'orchestrate',
        conditions: ['verification checkpoint'],
        guardrails: ['Do not skip verifier evidence before promotion.'],
        workflowSelectors: ['workflows-approval-loop'],
        domain: 'workflows',
    },
    {
        name: 'retry-narrow-scope',
        description: 'Focus retries on the failing surface after an error cluster.',
        trigger: 'retry',
        riskClass: 'orchestrate',
        conditions: ['retry requested'],
        guardrails: ['Do not widen retry scope without new evidence.'],
        workflowSelectors: ['workflows-execution-loop'],
        domain: 'workflows',
    },
    {
        name: 'memory-shield-escalation',
        description: 'Flag stored memories for shield review when policy risk is detected.',
        trigger: 'memory.stored',
        riskClass: 'read',
        conditions: ['high priority memory stored'],
        guardrails: ['Do not promote quarantined memory into global patterns.'],
        skillSelectors: ['security-reviewer'],
        domain: 'security',
    },
    {
        name: 'promotion-audit',
        description: 'Require a security-oriented audit note on promotion approval.',
        trigger: 'promotion.approved',
        riskClass: 'read',
        conditions: ['promotion approved'],
        guardrails: ['Do not silently promote unverified mutate artifacts.'],
        skillSelectors: ['security-reviewer'],
        workflowSelectors: ['security-approval-loop'],
        domain: 'security',
    },
    {
        name: 'before-compaction-flush',
        description: 'Flush memory prefrontal to SQLite before context compaction.',
        trigger: 'before-compaction',
        riskClass: 'read',
        conditions: ['context budget nearing limit'],
        guardrails: ['Always preserve prefrontal memories before compaction.'],
        workflowSelectors: [],
        domain: 'orchestration',
    },
];
export class HookRuntime {
    rootDir;
    workspaceRoot;
    artifacts = new Map();
    bootstrapped = false;
    constructor(rootDir, workspaceRoot) {
        this.rootDir = rootDir ?? path.join(os.tmpdir(), 'nexus-prime-runtime-hooks');
        this.workspaceRoot = workspaceRoot ?? process.cwd();
        fs.mkdirSync(this.rootDir, { recursive: true });
        this.ensureBootstrapped();
    }
    listArtifacts(options = {}) {
        this.ensureBootstrapped();
        const policy = options.selectionPolicy ?? 'registry-first';
        const dedupe = options.dedupe !== false;
        const artifacts = dedupe
            ? dedupeArtifactsByPreference([...this.artifacts.values()], policy)
            : [...this.artifacts.values()];
        return artifacts.sort((a, b) => a.name.localeCompare(b.name));
    }
    getArtifact(hookId) {
        this.ensureBootstrapped();
        return this.artifacts.get(hookId);
    }
    findByName(name) {
        this.ensureBootstrapped();
        const normalized = name.toLowerCase();
        return this.listArtifacts().find((artifact) => artifact.name.toLowerCase() === normalized || artifact.hookId.toLowerCase() === normalized);
    }
    createHook(input) {
        const artifact = {
            ...input,
            hookId: `hook_${randomUUID().slice(0, 8)}`,
            version: 1,
            validationStatus: 'pending',
            rolloutStatus: 'staged',
            ...deriveCatalogMetadata(input.provenance),
            effectiveness: { fired: 0, blocked: 0, queued: 0 },
        };
        return this.stage(artifact);
    }
    deploy(hookId, scope = 'session') {
        const artifact = this.artifacts.get(hookId);
        if (!artifact)
            return undefined;
        artifact.scope = scope;
        artifact.rolloutStatus = scope === 'global' ? 'promoted' : 'hot';
        this.persistArtifact(artifact);
        return artifact;
    }
    revoke(hookId) {
        const artifact = this.artifacts.get(hookId);
        if (!artifact)
            return undefined;
        artifact.rolloutStatus = 'revoked';
        this.persistArtifact(artifact);
        return artifact;
    }
    resolveHookSelectors(names, goal, trigger) {
        this.ensureBootstrapped();
        const selectors = new Set(names.map((name) => name.toLowerCase()));
        const domains = detectDomains(goal, names);
        return dedupeHooks(this.listArtifacts({ dedupe: false }).filter((artifact) => {
            if (artifact.rolloutStatus === 'revoked')
                return false;
            if (trigger && artifact.trigger !== trigger)
                return false;
            return selectors.has(artifact.name.toLowerCase()) ||
                selectors.has(artifact.hookId.toLowerCase()) ||
                (artifact.domain ? domains.includes(artifact.domain) : false);
        }));
    }
    dispatch(trigger, hooks, context) {
        const selected = hooks
            .filter((hook) => hook.trigger === trigger && hook.rolloutStatus !== 'revoked')
            .sort(compareHookDispatchOrder);
        const notes = [];
        const events = [];
        const workflowSelectors = new Set();
        const skillSelectors = new Set();
        const toolBindings = [];
        let blocked = false;
        for (const hook of selected) {
            try {
                const mutateBlocked = hook.riskClass === 'mutate' && !context.allowMutateHooks;
                hook.effectiveness.fired += 1;
                if (mutateBlocked) {
                    hook.effectiveness.blocked += 1;
                    blocked = true;
                    notes.push(`Blocked mutate hook ${hook.name} at ${trigger}.`);
                    events.push({
                        type: 'hook.blocked',
                        hookId: hook.hookId,
                        name: hook.name,
                        trigger,
                        reason: 'mutate-hooks-disabled',
                    });
                    continue;
                }
                hook.workflowSelectors.forEach((selector) => workflowSelectors.add(selector));
                hook.skillSelectors.forEach((selector) => skillSelectors.add(selector));
                toolBindings.push(...hook.toolBindings);
                hook.effectiveness.queued += hook.workflowSelectors.length + hook.skillSelectors.length + hook.toolBindings.length;
                notes.push(`Hook ${hook.name} fired at ${trigger}.`);
                events.push({
                    type: 'hook.fired',
                    hookId: hook.hookId,
                    name: hook.name,
                    trigger,
                    domain: hook.domain,
                    riskClass: hook.riskClass,
                });
                this.persistArtifact(hook);
            }
            catch (err) {
                const message = err instanceof Error ? err.message : String(err);
                notes.push(`Hook ${hook.name} errored: ${message}`);
                nexusEventBus.emit('hook.error', { hookId: hook.hookId, error: message });
            }
        }
        return {
            trigger,
            blocked,
            events,
            notes,
            workflowSelectors: [...workflowSelectors],
            skillSelectors: [...skillSelectors],
            toolBindings,
        };
    }
    stage(artifact) {
        artifact.validationStatus = this.validate(artifact).valid ? 'validated' : 'rejected';
        const hydrated = {
            ...artifact,
            ...deriveCatalogMetadata(artifact.provenance, artifact.catalogPath),
        };
        this.artifacts.set(hydrated.hookId, hydrated);
        this.persistArtifact(hydrated);
        return hydrated;
    }
    validate(artifact) {
        const errors = [];
        if (!artifact.name.trim())
            errors.push('Hook name is required.');
        if (!artifact.description.trim())
            errors.push('Hook description is required.');
        return { valid: errors.length === 0, errors };
    }
    lastLocalRefresh = 0;
    ensureBootstrapped() {
        if (!this.bootstrapped) {
            this.bootstrapped = true;
            this.lastLocalRefresh = Date.now();
        }
        else if (Date.now() - this.lastLocalRefresh > 2000) {
            this.lastLocalRefresh = Date.now();
        }
        else {
            return;
        }
        if (this.artifacts.size === 0) {
            for (const seed of BUILTIN_HOOKS) {
                this.stage({
                    hookId: `hook_${slugify(seed.name)}`,
                    version: 1,
                    name: seed.name,
                    description: seed.description,
                    domain: seed.domain,
                    trigger: seed.trigger,
                    conditions: seed.conditions,
                    guardrails: seed.guardrails,
                    skillSelectors: seed.skillSelectors ?? [],
                    workflowSelectors: seed.workflowSelectors ?? [],
                    toolBindings: seed.toolBindings ?? [],
                    riskClass: seed.riskClass,
                    scope: 'base',
                    provenance: 'bundled',
                    validationStatus: 'validated',
                    rolloutStatus: 'promoted',
                    ...deriveCatalogMetadata('bundled'),
                    effectiveness: { fired: 0, blocked: 0, queued: 0 },
                });
            }
        }
        const hookDirs = [
            path.join(this.workspaceRoot, '.agent', 'hooks'),
            path.join(this.workspaceRoot, '.agents', 'hooks'),
        ];
        const foundLocalIds = new Set();
        for (const hookDir of hookDirs) {
            for (const entry of readMarkdownArtifacts(hookDir)) {
                const frontmatter = entry.parsed.frontmatter;
                const name = String(frontmatter.name ?? path.basename(entry.path, '.md'));
                const hookId = `hook_local_${slugify(name)}`;
                foundLocalIds.add(hookId);
                this.stage({
                    hookId,
                    version: 1,
                    name,
                    description: String(frontmatter.description ?? 'Local hook'),
                    domain: String(frontmatter.domain ?? detectDomains(name)[0] ?? ''),
                    trigger: normalizeTrigger(frontmatter.trigger),
                    conditions: toStringArray(frontmatter.conditions),
                    guardrails: toStringArray(frontmatter.guardrails),
                    skillSelectors: toStringArray(frontmatter.skills),
                    workflowSelectors: toStringArray(frontmatter.workflows),
                    toolBindings: [],
                    riskClass: normalizeRiskClass(frontmatter.riskClass),
                    scope: 'base',
                    provenance: `local:${entry.path}`,
                    validationStatus: 'validated',
                    rolloutStatus: 'promoted',
                    ...deriveCatalogMetadata(`local:${entry.path}`, entry.path),
                    effectiveness: { fired: 0, blocked: 0, queued: 0 },
                });
            }
        }
        for (const [id, art] of this.artifacts.entries()) {
            if (art.provenance.startsWith('local:') && !foundLocalIds.has(id)) {
                this.artifacts.delete(id);
            }
        }
    }
    persistArtifact(artifact) {
        const dir = path.join(this.rootDir, artifact.hookId, `v${artifact.version}`);
        fs.mkdirSync(dir, { recursive: true });
        fs.writeFileSync(path.join(dir, 'artifact.json'), JSON.stringify(artifact, null, 2), 'utf8');
    }
}
function normalizeRiskClass(value) {
    return value === 'mutate' || value === 'orchestrate' ? value : 'read';
}
function normalizeTrigger(value) {
    const candidate = String(value ?? 'run.created');
    const valid = [
        'run.created',
        'before-read',
        'before-mutate',
        'before-verify',
        'retry',
        'run.failed',
        'run.verified',
        'promotion.approved',
        'memory.stored',
        'shield.blocked',
        'before-compaction',
    ];
    return valid.includes(candidate) ? candidate : 'run.created';
}
function toStringArray(value) {
    return Array.isArray(value) ? value.map(String).filter(Boolean) : [];
}
function dedupeHooks(artifacts, policy = 'registry-first') {
    return dedupeArtifactsByPreference(artifacts, policy);
}
function compareHookDispatchOrder(left, right) {
    return (right.catalogPriority - left.catalogPriority)
        || left.name.localeCompare(right.name)
        || left.hookId.localeCompare(right.hookId);
}
export const createHookRuntime = (rootDir, workspaceRoot) => new HookRuntime(rootDir, workspaceRoot);
