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';
const BUILTIN_AUTOMATIONS = [
    {
        name: 'verified-followup-automation',
        description: 'Queue a bounded approval workflow after a verified run.',
        domain: 'workflows',
        triggerMode: 'event',
        eventTrigger: 'run.verified',
        workflowSelectors: ['workflows-approval-loop'],
    },
    {
        name: 'failure-recovery-automation',
        description: 'Event marker for a failed run. Add workflowSelectors to enable recovery execution.',
        domain: 'orchestration',
        triggerMode: 'event',
        eventTrigger: 'run.failed',
        workflowSelectors: [],
    },
    {
        name: 'memory-governance-automation',
        description: 'Event marker for memory storage. Add workflowSelectors to enable governance execution.',
        domain: 'security',
        triggerMode: 'event',
        eventTrigger: 'memory.stored',
        workflowSelectors: [],
    },
];
export class AutomationRuntime {
    rootDir;
    workspaceRoot;
    clock;
    artifacts = new Map();
    scheduleStates = new Map();
    bootstrapped = false;
    constructor(rootDir, workspaceRoot, clock = () => Date.now()) {
        this.rootDir = rootDir ?? path.join(os.tmpdir(), 'nexus-prime-runtime-automations');
        this.workspaceRoot = workspaceRoot ?? process.cwd();
        this.clock = clock;
        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(automationId) {
        this.ensureBootstrapped();
        return this.artifacts.get(automationId);
    }
    findByName(name) {
        this.ensureBootstrapped();
        const normalized = name.toLowerCase();
        return this.listArtifacts().find((artifact) => artifact.name.toLowerCase() === normalized || artifact.automationId.toLowerCase() === normalized);
    }
    createAutomation(input) {
        const artifact = {
            ...input,
            automationId: `automation_${randomUUID().slice(0, 8)}`,
            version: 1,
            validationStatus: 'pending',
            rolloutStatus: 'staged',
            ...deriveCatalogMetadata(input.provenance),
            effectiveness: { queuedRuns: 0, deliveries: 0, failures: 0 },
        };
        return this.stage(artifact);
    }
    deploy(automationId, scope = 'session') {
        const artifact = this.artifacts.get(automationId);
        if (!artifact)
            return undefined;
        artifact.scope = scope;
        artifact.rolloutStatus = scope === 'global' ? 'promoted' : 'hot';
        this.persistArtifact(artifact);
        return artifact;
    }
    revoke(automationId) {
        const artifact = this.artifacts.get(automationId);
        if (!artifact)
            return undefined;
        artifact.rolloutStatus = 'revoked';
        this.persistArtifact(artifact);
        return artifact;
    }
    resolveAutomationSelectors(names, goal, triggerMode) {
        this.ensureBootstrapped();
        const selectors = new Set(names.map((name) => name.toLowerCase()));
        const domains = detectDomains(goal, names);
        return dedupeAutomations(this.listArtifacts({ dedupe: false }).filter((artifact) => {
            if (artifact.rolloutStatus === 'revoked')
                return false;
            if (triggerMode && artifact.triggerMode !== triggerMode)
                return false;
            return selectors.has(artifact.name.toLowerCase()) ||
                selectors.has(artifact.automationId.toLowerCase()) ||
                (artifact.domain ? domains.includes(artifact.domain) : false);
        }));
    }
    async dispatch(trigger, automations, context) {
        const selected = automations
            .filter((artifact) => artifact.triggerMode === 'event' &&
            artifact.eventTrigger === trigger &&
            artifact.rolloutStatus !== 'revoked')
            .sort(compareAutomationDispatchOrder);
        const dispatches = [];
        for (const artifact of selected) {
            const deliveries = await this.deliverConnectors(artifact, context.executeConnectors ?? false, context.payload ?? {});
            const queuedRun = artifact.workflowSelectors.length > 0 || artifact.skillSelectors.length > 0 || artifact.hookSelectors.length > 0
                ? {
                    goal: `${artifact.description} for ${context.goal}`,
                    workflowSelectors: artifact.workflowSelectors,
                    skillSelectors: artifact.skillSelectors,
                    hookSelectors: artifact.hookSelectors,
                    triggerMode: 'event',
                }
                : undefined;
            artifact.effectiveness.queuedRuns += queuedRun ? 1 : 0;
            artifact.effectiveness.deliveries += deliveries.filter((delivery) => delivery.status === 'delivered').length;
            artifact.effectiveness.failures += deliveries.filter((delivery) => delivery.status === 'failed').length;
            this.persistArtifact(artifact);
            dispatches.push({
                trigger,
                automationId: artifact.automationId,
                name: artifact.name,
                queuedRun,
                deliveries,
            });
        }
        return dispatches;
    }
    listScheduledAutomations(options = {}) {
        this.ensureBootstrapped();
        const now = options.now ?? this.clock();
        return this.listArtifacts()
            .filter((artifact) => artifact.triggerMode === 'schedule')
            .map((artifact) => this.snapshotScheduledAutomation(artifact, now));
    }
    getDueScheduledAutomations(options = {}) {
        return this.listScheduledAutomations(options).filter((snapshot) => snapshot.due);
    }
    async dispatchScheduled(options = {}) {
        this.ensureBootstrapped();
        const now = options.now ?? this.clock();
        const due = this.getDueScheduledAutomations({ now });
        const dispatches = [];
        for (const snapshot of due) {
            const artifact = this.artifacts.get(snapshot.automationId);
            if (!artifact || artifact.rolloutStatus === 'revoked') {
                continue;
            }
            const deliveries = await this.deliverConnectors(artifact, options.executeConnectors ?? false, options.payload ?? {});
            const queuedRun = {
                goal: options.goal ?? (artifact.description || artifact.name),
                workflowSelectors: artifact.workflowSelectors,
                skillSelectors: artifact.skillSelectors,
                hookSelectors: artifact.hookSelectors,
                triggerMode: 'schedule',
                schedule: artifact.schedule,
            };
            artifact.effectiveness.queuedRuns += queuedRun ? 1 : 0;
            artifact.effectiveness.deliveries += deliveries.filter((delivery) => delivery.status === 'delivered').length;
            artifact.effectiveness.failures += deliveries.filter((delivery) => delivery.status === 'failed').length;
            this.persistArtifact(artifact);
            this.completeScheduleRun(artifact, now);
            dispatches.push({
                trigger: 'schedule',
                automationId: artifact.automationId,
                name: artifact.name,
                queuedRun,
                deliveries,
            });
        }
        return dispatches;
    }
    stage(artifact) {
        artifact.validationStatus = this.validate(artifact).valid ? 'validated' : 'rejected';
        const hydrated = {
            ...artifact,
            ...deriveCatalogMetadata(artifact.provenance, artifact.catalogPath),
        };
        this.artifacts.set(hydrated.automationId, hydrated);
        this.persistArtifact(hydrated);
        this.syncScheduleState(hydrated, this.clock());
        return hydrated;
    }
    validate(artifact) {
        const errors = [];
        if (!artifact.name.trim())
            errors.push('Automation name is required.');
        if (!artifact.description.trim())
            errors.push('Automation description is required.');
        if (artifact.triggerMode === 'schedule') {
            const parsed = parseSchedule(artifact.schedule ?? '');
            if (!parsed.valid) {
                errors.push(parsed.reason);
            }
        }
        return { valid: errors.length === 0, errors };
    }
    syncScheduleState(artifact, now) {
        if (artifact.triggerMode !== 'schedule')
            return;
        const parsed = parseSchedule(artifact.schedule ?? '');
        const state = this.scheduleStates.get(artifact.automationId);
        if (!parsed.valid) {
            const reason = parsed.reason;
            this.scheduleStates.set(artifact.automationId, {
                schedule: artifact.schedule ?? '',
                signature: artifact.schedule ?? '',
                registeredAt: now,
                lastRunAt: state?.lastRunAt ?? null,
                nextRunAt: null,
                completedAt: state?.completedAt ?? null,
                reason,
                spec: null,
            });
            return;
        }
        const signature = artifact.schedule ?? '';
        if (state && state.signature === signature && state.spec) {
            return;
        }
        const registeredAt = state?.registeredAt ?? now;
        const nextRunAt = state?.nextRunAt ?? computeNextRunAt(parsed.spec, registeredAt, state?.lastRunAt ?? null, registeredAt);
        this.scheduleStates.set(artifact.automationId, {
            schedule: signature,
            signature,
            registeredAt,
            lastRunAt: state?.lastRunAt ?? null,
            nextRunAt,
            completedAt: state?.completedAt ?? null,
            reason: undefined,
            spec: parsed.spec,
        });
    }
    snapshotScheduledAutomation(artifact, now) {
        this.syncScheduleState(artifact, now);
        const state = this.scheduleStates.get(artifact.automationId);
        if (!state) {
            return {
                automationId: artifact.automationId,
                name: artifact.name,
                schedule: artifact.schedule ?? '',
                triggerMode: 'schedule',
                registeredAt: now,
                lastRunAt: null,
                nextRunAt: null,
                due: false,
                status: 'invalid',
                reason: 'schedule state unavailable',
            };
        }
        if (!state.spec) {
            return {
                automationId: artifact.automationId,
                name: artifact.name,
                schedule: artifact.schedule ?? '',
                triggerMode: 'schedule',
                registeredAt: state.registeredAt,
                lastRunAt: state.lastRunAt,
                nextRunAt: null,
                due: false,
                status: 'invalid',
                reason: state.reason ?? 'invalid schedule expression',
            };
        }
        const nextRunAt = state.nextRunAt ?? computeNextRunAt(state.spec, state.registeredAt, state.lastRunAt, now);
        const due = nextRunAt !== null && nextRunAt <= now;
        return {
            automationId: artifact.automationId,
            name: artifact.name,
            schedule: artifact.schedule ?? '',
            triggerMode: 'schedule',
            registeredAt: state.registeredAt,
            lastRunAt: state.lastRunAt,
            nextRunAt,
            due,
            status: state.completedAt !== null && nextRunAt === null
                ? 'complete'
                : due
                    ? 'due'
                    : 'waiting',
        };
    }
    completeScheduleRun(artifact, now) {
        const state = this.scheduleStates.get(artifact.automationId);
        if (!state || !state.spec)
            return;
        state.lastRunAt = now;
        state.completedAt = state.spec.kind === 'once' ? now : null;
        state.nextRunAt = computeNextRunAt(state.spec, state.registeredAt, state.lastRunAt, now);
        this.scheduleStates.set(artifact.automationId, state);
    }
    async deliverConnectors(artifact, executeConnectors, payload) {
        const deliveries = [];
        for (const connector of artifact.connectors) {
            if (!executeConnectors || !connector.target) {
                deliveries.push({
                    kind: connector.kind,
                    target: connector.target,
                    status: 'queued',
                    message: 'Connector queued for bounded delivery.',
                });
                continue;
            }
            try {
                const response = await fetch(connector.target, {
                    method: connector.method ?? 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        automationId: artifact.automationId,
                        automationName: artifact.name,
                        eventName: connector.eventName ?? artifact.eventTrigger,
                        payload,
                    }),
                });
                deliveries.push({
                    kind: connector.kind,
                    target: connector.target,
                    status: response.ok ? 'delivered' : 'failed',
                    responseCode: response.status,
                    message: response.ok ? 'Connector delivered successfully.' : `Connector failed with ${response.status}.`,
                });
            }
            catch (error) {
                deliveries.push({
                    kind: connector.kind,
                    target: connector.target,
                    status: 'failed',
                    message: String(error?.message ?? error),
                });
            }
        }
        return deliveries;
    }
    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_AUTOMATIONS) {
                this.stage({
                    automationId: `automation_${slugify(seed.name)}`,
                    version: 1,
                    name: seed.name,
                    description: seed.description,
                    domain: seed.domain,
                    triggerMode: seed.triggerMode,
                    eventTrigger: seed.eventTrigger,
                    workflowSelectors: seed.workflowSelectors,
                    hookSelectors: seed.hookSelectors ?? [],
                    skillSelectors: seed.skillSelectors ?? [],
                    connectors: [],
                    scope: 'base',
                    provenance: 'bundled',
                    validationStatus: 'validated',
                    rolloutStatus: 'promoted',
                    ...deriveCatalogMetadata('bundled'),
                    effectiveness: { queuedRuns: 0, deliveries: 0, failures: 0 },
                });
            }
        }
        const automationDirs = [
            path.join(this.workspaceRoot, '.agent', 'automations'),
            path.join(this.workspaceRoot, '.agents', 'automations'),
        ];
        const foundLocalIds = new Set();
        for (const automationDir of automationDirs) {
            for (const entry of readMarkdownArtifacts(automationDir)) {
                const frontmatter = entry.parsed.frontmatter;
                const name = String(frontmatter.name ?? path.basename(entry.path, '.md'));
                const automationId = `automation_local_${slugify(name)}`;
                foundLocalIds.add(automationId);
                this.stage({
                    automationId,
                    version: 1,
                    name,
                    description: String(frontmatter.description ?? 'Local automation'),
                    domain: String(frontmatter.domain ?? detectDomains(name)[0] ?? ''),
                    triggerMode: normalizeTriggerMode(frontmatter.triggerMode),
                    eventTrigger: normalizeEventTrigger(frontmatter.eventTrigger),
                    schedule: String(frontmatter.schedule ?? ''),
                    connectorEvent: String(frontmatter.connectorEvent ?? ''),
                    workflowSelectors: toStringArray(frontmatter.workflows),
                    hookSelectors: toStringArray(frontmatter.hooks),
                    skillSelectors: toStringArray(frontmatter.skills),
                    connectors: [],
                    scope: 'base',
                    provenance: `local:${entry.path}`,
                    validationStatus: 'validated',
                    rolloutStatus: 'promoted',
                    ...deriveCatalogMetadata(`local:${entry.path}`, entry.path),
                    effectiveness: { queuedRuns: 0, deliveries: 0, failures: 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.automationId, `v${artifact.version}`);
        fs.mkdirSync(dir, { recursive: true });
        fs.writeFileSync(path.join(dir, 'artifact.json'), JSON.stringify(artifact, null, 2), 'utf8');
    }
}
function normalizeTriggerMode(value) {
    return value === 'schedule' || value === 'connector' ? value : 'event';
}
function normalizeEventTrigger(value) {
    const candidate = String(value ?? '');
    if (!candidate)
        return undefined;
    const valid = [
        'run.created',
        'before-read',
        'before-mutate',
        'before-verify',
        'retry',
        'run.failed',
        'run.verified',
        'promotion.approved',
        'memory.stored',
        'shield.blocked',
    ];
    return valid.includes(candidate) ? candidate : undefined;
}
function toStringArray(value) {
    return Array.isArray(value) ? value.map(String).filter(Boolean) : [];
}
function dedupeAutomations(artifacts, policy = 'registry-first') {
    return dedupeArtifactsByPreference(artifacts, policy);
}
function compareAutomationDispatchOrder(left, right) {
    if ((right.catalogPriority ?? 0) !== (left.catalogPriority ?? 0)) {
        return (right.catalogPriority ?? 0) - (left.catalogPriority ?? 0);
    }
    const byName = left.name.localeCompare(right.name);
    if (byName !== 0) {
        return byName;
    }
    return left.automationId.localeCompare(right.automationId);
}
function parseSchedule(schedule) {
    const trimmed = schedule.trim();
    if (!trimmed) {
        return { valid: false, reason: 'schedule is required for scheduled automations' };
    }
    const interval = trimmed.match(/^every\s+(\d+)\s*(ms|s|m|h|d)$/i);
    if (interval) {
        const value = Number(interval[1]);
        const unit = interval[2].toLowerCase();
        const multiplier = { ms: 1, s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
        const intervalMs = value * (multiplier[unit] ?? 0);
        if (value <= 0 || !Number.isFinite(intervalMs) || intervalMs <= 0) {
            return { valid: false, reason: `invalid interval schedule: ${schedule}` };
        }
        return { valid: true, spec: { kind: 'interval', intervalMs } };
    }
    const daily = trimmed.match(/^daily(?:\s+at)?\s+(\d{2}):(\d{2})$/i);
    if (daily) {
        const hour = Number(daily[1]);
        const minute = Number(daily[2]);
        if (hour > 23 || minute > 59) {
            return { valid: false, reason: `invalid daily schedule: ${schedule}` };
        }
        return { valid: true, spec: { kind: 'daily', hour, minute } };
    }
    const weekly = trimmed.match(/^weekly\s+([a-z,\s]+)\s+(\d{2}):(\d{2})$/i);
    if (weekly) {
        const weekdays = weekly[1]
            .split(/[,\s]+/)
            .map((day) => normalizeWeekday(day))
            .filter((day) => day !== null);
        const hour = Number(weekly[2]);
        const minute = Number(weekly[3]);
        if (weekdays.length === 0 || hour > 23 || minute > 59) {
            return { valid: false, reason: `invalid weekly schedule: ${schedule}` };
        }
        return { valid: true, spec: { kind: 'weekly', weekdays: [...new Set(weekdays)], hour, minute } };
    }
    const once = trimmed.match(/^once\s+(.+)$/i);
    if (once) {
        const timestamp = Date.parse(once[1]);
        if (!Number.isFinite(timestamp)) {
            return { valid: false, reason: `invalid once schedule: ${schedule}` };
        }
        return { valid: true, spec: { kind: 'once', runAt: timestamp } };
    }
    return {
        valid: false,
        reason: `unsupported schedule expression "${schedule}"`,
    };
}
function computeNextRunAt(spec, registeredAt, lastRunAt, now) {
    const anchor = lastRunAt ?? registeredAt;
    switch (spec.kind) {
        case 'interval': {
            const intervalMs = spec.intervalMs ?? 0;
            if (intervalMs <= 0)
                return null;
            let next = anchor + intervalMs;
            while (next <= now) {
                next += intervalMs;
            }
            return next;
        }
        case 'daily':
            return nextDailyOccurrence(anchor, now, spec.hour ?? 0, spec.minute ?? 0);
        case 'weekly':
            return nextWeeklyOccurrence(anchor, now, spec.weekdays ?? [], spec.hour ?? 0, spec.minute ?? 0);
        case 'once': {
            const runAt = spec.runAt ?? 0;
            if (lastRunAt !== null)
                return null;
            return runAt;
        }
        default:
            return null;
    }
}
function nextDailyOccurrence(base, now, hour, minute) {
    const cursor = new Date(Math.max(base, now));
    cursor.setUTCHours(hour, minute, 0, 0);
    if (cursor.getTime() <= Math.max(base, now)) {
        cursor.setUTCDate(cursor.getUTCDate() + 1);
    }
    return cursor.getTime();
}
function nextWeeklyOccurrence(base, now, weekdays, hour, minute) {
    const start = Math.max(base, now);
    const cursor = new Date(start);
    cursor.setUTCHours(hour, minute, 0, 0);
    for (let offset = 0; offset < 14; offset += 1) {
        const candidate = new Date(cursor);
        candidate.setUTCDate(candidate.getUTCDate() + offset);
        candidate.setUTCHours(hour, minute, 0, 0);
        if (candidate.getTime() <= start)
            continue;
        if (weekdays.includes(candidate.getUTCDay())) {
            return candidate.getTime();
        }
    }
    return cursor.getTime();
}
function normalizeWeekday(day) {
    switch (day.toLowerCase()) {
        case 'sun':
        case 'sunday':
        case '0':
            return 0;
        case 'mon':
        case 'monday':
        case '1':
            return 1;
        case 'tue':
        case 'tues':
        case 'tuesday':
        case '2':
            return 2;
        case 'wed':
        case 'wednesday':
        case '3':
            return 3;
        case 'thu':
        case 'thur':
        case 'thurs':
        case 'thursday':
        case '4':
            return 4;
        case 'fri':
        case 'friday':
        case '5':
            return 5;
        case 'sat':
        case 'saturday':
        case '6':
            return 6;
        default:
            return null;
    }
}
export const createAutomationRuntime = (rootDir, workspaceRoot, clock) => new AutomationRuntime(rootDir, workspaceRoot, clock);
