import { ConfigManager } from '../config/manager.js';
import { createDomainDispatch } from '../runtime/store/index.js';
import { AgentMessageBus } from '../agents/message-bus.js';
import { computeSchedulerCapacity } from './scheduler-capacity.js';
import { AutomationJobStore } from './store/jobs.js';
import { AutomationRunStore } from './store/runs.js';
import { AutomationDeliveryManager } from './delivery-manager.js';
import { RouteBindingManager } from '../channels/index.js';
import { SharedSessionBroker } from '../control-plane/index.js';
import { applyFailureToJob as applyAutomationFailureToJob, executeAutomationJob, resolveAutomationExecution, resolveRouteForTarget as resolveAutomationRouteForTarget, resolveSharedSessionExecution as resolveAutomationSharedSessionExecution, syncExecutionRoute as syncAutomationExecutionRoute, toResolvedExecution as toAutomationResolvedExecution, } from './manager-runtime-execution.js';
import { formatExternalContentSource, normalizeExternalTelemetry, normalizeJobRecord, normalizeRunRecord, normalizeRunTelemetry, normalizeSourceRecord, sortJobs, sortRuns, } from './manager-runtime-helpers.js';
import { advanceScheduledHeartbeatAutomationJob, cancelAutomationTimer, queueDueHeartbeatAutomationJobs, queueHeartbeatWake, scheduleAutomationJob, } from './manager-runtime-scheduling.js';
import { maybeDeliverAutomationFailureNotice, maybeDeliverAutomationRun, scheduleAutomationFailureFollowUp, } from './manager-runtime-delivery.js';
import { emitAutomationManagerJobAutoDisabled, emitAutomationManagerJobCreated, emitAutomationManagerJobUpdated, emitAutomationManagerRunCompleted, emitAutomationManagerRunFailed, emitAutomationManagerRunQueued, emitAutomationManagerRunStarted, } from './manager-runtime-events.js';
import { collectAutomationSources, syncAutomationJobToRuntime, syncAutomationRunToRuntime, syncAutomationRuntimeSnapshot, } from './manager-runtime-sync.js';
import { reconcileAutomationActiveRuns } from './manager-runtime-reconcile.js';
import { summarizeError } from '../utils/error-display.js';
import { createAutomationJobRecord, toggleAutomationJobEnabled, updateAutomationJobRecord, } from './manager-runtime-job-mutations.js';
import { isFeatureGateEnabled, requireFeatureGate } from '../runtime/feature-flags/index.js';
export class AutomationManager {
    jobStore;
    runStore;
    spawnTask;
    cancelTask;
    agentStatusProvider;
    configManager;
    routeBindings;
    sessionBroker;
    defaultSurfaceKind;
    defaultSurfaceId;
    jobs = new Map();
    runs = new Map();
    timers = new Map();
    retryTimers = new Map();
    heartbeatWakes = new Map();
    deliveryManager;
    deliveryInFlight = new Set();
    // runtimeDispatch and runtimeBus are wired lazily via attachRuntime() after
    // construction; null-init here avoids a required-constructor-arg dependency.
    runtimeDispatch = null;
    runtimeBus = null;
    featureFlags;
    loaded = false;
    running = false;
    startPromise = null;
    reconcileTimer = null;
    constructor(config) {
        this.configManager = config.configManager;
        this.jobStore = config.jobStore ?? new AutomationJobStore({ configManager: this.configManager });
        this.runStore = config.runStore ?? new AutomationRunStore({ configManager: this.configManager });
        this.routeBindings = config.routeBindings;
        this.sessionBroker = config.sessionBroker;
        this.defaultSurfaceKind = config.defaultSurfaceKind;
        this.defaultSurfaceId = config.defaultSurfaceId;
        this.agentStatusProvider = config.agentStatusProvider;
        this.spawnTask = config.spawnTask;
        this.cancelTask = config.cancelTask;
        if (config.runtimeStore) {
            this.runtimeDispatch = createDomainDispatch(config.runtimeStore);
        }
        this.runtimeBus = config.runtimeBus ?? null;
        this.deliveryManager = config.deliveryManager ?? null;
        this.featureFlags = config.featureFlags ?? null;
    }
    isEnabled() {
        return isFeatureGateEnabled(this.featureFlags, 'automation-domain');
    }
    requireEnabled(operation) {
        requireFeatureGate(this.featureFlags, 'automation-domain', operation);
    }
    requireSpawnTask() {
        if (!this.spawnTask) {
            throw new Error('AutomationManager requires an explicit spawnTask callback.');
        }
        return this.spawnTask;
    }
    requireCancelTask() {
        if (!this.cancelTask) {
            throw new Error('AutomationManager requires an explicit cancelTask callback.');
        }
        return this.cancelTask;
    }
    runtimeExecutionContext() {
        return {
            configManager: this.configManager,
            routeBindings: this.routeBindings,
            sessionBroker: this.sessionBroker,
            defaultSurfaceKind: this.defaultSurfaceKind,
            defaultSurfaceId: this.defaultSurfaceId,
            spawnTask: (input) => this.requireSpawnTask()(input),
            saveJobs: () => this.saveJobs(),
            saveRuns: () => this.saveRuns(),
            pruneRunHistory: (jobId) => this.pruneRunHistory(jobId),
            activeRunCount: () => this.activeRunCount(),
            maxConcurrentRuns: () => this.maxConcurrentRuns(),
            syncExecutionRoute: (job, run) => this.syncExecutionRoute(job, run),
            syncRunToRuntime: (run, source) => this.syncRunToRuntime(run, source),
            syncJobToRuntime: (job, source) => this.syncJobToRuntime(job, source),
            emitRunQueued: (job, run) => this.emitRunQueued(job, run),
            emitRunStarted: (job, run) => this.emitRunStarted(job, run),
            emitRunCompleted: (job, run, outcome) => this.emitRunCompleted(job, run, outcome),
            emitRunFailed: (job, run, error, retryable) => this.emitRunFailed(job, run, error, retryable),
            maybeDeliverRun: (job, run) => this.maybeDeliverRun(job, run),
            scheduleFailureFollowUp: (job, run) => this.scheduleFailureFollowUp(job, run),
            applyFailureToJob: (job, timestamp, countRun = true) => this.applyFailureToJob(job, timestamp, countRun),
            jobs: this.jobs,
            runs: this.runs,
        };
    }
    jobMutationContext() {
        return {
            configManager: this.configManager,
            jobs: this.jobs,
            saveJobs: () => this.saveJobs(),
            scheduleJob: (job) => this.scheduleJob(job),
            syncJobToRuntime: (job, source) => this.syncJobToRuntime(job, source),
            emitJobCreated: (job) => this.emitJobCreated(job),
            emitJobUpdated: (job, changedFields) => this.emitJobUpdated(job, changedFields),
        };
    }
    async start() {
        if (!this.isEnabled()) {
            this.stop();
            return;
        }
        if (this.running)
            return;
        if (this.startPromise)
            return await this.startPromise;
        this.startPromise = this.load()
            .then(() => {
            this.running = true;
            this.reconcileActiveRuns();
            this.reconcileTimer = setInterval(() => {
                this.reconcileActiveRuns();
            }, 2_000);
            this.reconcileTimer.unref?.();
            for (const job of this.jobs.values()) {
                this.scheduleJob(job);
            }
        })
            .finally(() => {
            this.startPromise = null;
        });
        return await this.startPromise;
    }
    stop() {
        for (const timer of this.timers.values()) {
            clearTimeout(timer);
        }
        this.timers.clear();
        for (const timer of this.retryTimers.values()) {
            clearTimeout(timer);
        }
        this.retryTimers.clear();
        if (this.reconcileTimer !== null) {
            clearInterval(this.reconcileTimer);
            this.reconcileTimer = null;
        }
        this.running = false;
    }
    attachRuntime(config) {
        if (config.runtimeStore) {
            this.runtimeDispatch = createDomainDispatch(config.runtimeStore);
            this.syncRuntimeSnapshot();
        }
        if (config.runtimeBus) {
            this.runtimeBus = config.runtimeBus;
        }
        if (config.deliveryManager) {
            this.deliveryManager = config.deliveryManager;
            this.deliveryManager.attachRuntime({
                runtimeStore: config.runtimeStore,
                runtimeBus: config.runtimeBus,
            });
        }
    }
    listJobs() {
        if (!this.isEnabled())
            return [];
        this.reconcileActiveRuns();
        return sortJobs(this.jobs.values());
    }
    listRuns(jobId) {
        if (!this.isEnabled())
            return [];
        this.reconcileActiveRuns();
        const runs = jobId
            ? [...this.runs.values()].filter((run) => run.jobId === jobId)
            : this.runs.values();
        return sortRuns(runs);
    }
    listHeartbeatWakes() {
        if (!this.isEnabled())
            return [];
        this.queueDueHeartbeatJobs('inspect');
        return [...this.heartbeatWakes.values()].sort((a, b) => a.queuedAt - b.queuedAt || a.jobId.localeCompare(b.jobId));
    }
    getRun(runId) {
        if (!this.isEnabled())
            return undefined;
        this.reconcileActiveRuns();
        return this.runs.get(runId);
    }
    getJob(jobId) {
        if (!this.isEnabled())
            return undefined;
        this.reconcileActiveRuns();
        return this.jobs.get(jobId);
    }
    async createJob(input) {
        this.requireEnabled('create automation job');
        await this.start();
        return await createAutomationJobRecord(this.jobMutationContext(), input);
    }
    async removeJob(jobId) {
        this.requireEnabled('remove automation job');
        await this.start();
        this.cancelTimer(jobId);
        const removed = this.jobs.delete(jobId);
        if (!removed)
            return false;
        await this.saveJobs();
        return true;
    }
    async setEnabled(jobId, enabled) {
        this.requireEnabled('set automation job enabled state');
        await this.start();
        const updated = await toggleAutomationJobEnabled(this.jobMutationContext(), jobId, enabled);
        if (!enabled)
            this.cancelTimer(jobId);
        return updated;
    }
    async updateJob(jobId, patch) {
        this.requireEnabled('update automation job');
        await this.start();
        return await updateAutomationJobRecord(this.jobMutationContext(), jobId, patch);
    }
    async runNow(jobId) {
        this.requireEnabled('run automation job');
        await this.start();
        const job = this.jobs.get(jobId);
        if (!job)
            throw new Error(`Automation job not found: ${jobId}`);
        if (this.activeRunCount() >= this.maxConcurrentRuns()) {
            throw new Error(`Automation concurrency limit reached (${this.maxConcurrentRuns()})`);
        }
        return await this.executeJob(job, 'manual', false);
    }
    async triggerHeartbeat(_input = {}) {
        this.requireEnabled('trigger automation heartbeat');
        await this.start();
        this.queueDueHeartbeatJobs('heartbeat');
        const queued = this.listHeartbeatWakes();
        const processed = [];
        const failed = [];
        for (const wake of queued) {
            if (this.activeRunCount() >= this.maxConcurrentRuns())
                break;
            const job = this.jobs.get(wake.jobId);
            this.heartbeatWakes.delete(wake.jobId);
            if (!job?.enabled)
                continue;
            try {
                const run = await this.executeJob(job, wake.trigger, wake.dueRun, wake.attempt);
                processed.push(run);
                this.advanceScheduledHeartbeatJob(job.id);
            }
            catch (error) {
                failed.push({
                    jobId: wake.jobId,
                    error: summarizeError(error),
                });
            }
        }
        return {
            processed,
            failed,
            pending: this.listHeartbeatWakes(),
            checkedAt: Date.now(),
        };
    }
    async retryRun(runId) {
        this.requireEnabled('retry automation run');
        await this.start();
        const run = this.runs.get(runId);
        if (!run)
            throw new Error(`Automation run not found: ${runId}`);
        const job = this.jobs.get(run.jobId);
        if (!job)
            throw new Error(`Automation job not found: ${run.jobId}`);
        if (this.activeRunCount() >= this.maxConcurrentRuns()) {
            throw new Error(`Automation concurrency limit reached (${this.maxConcurrentRuns()})`);
        }
        return await this.executeJob(job, 'manual', false, run.attempt + 1);
    }
    async cancelRun(runId, reason = 'operator-cancelled') {
        this.requireEnabled('cancel automation run');
        await this.start();
        const run = this.runs.get(runId);
        if (!run)
            return null;
        if (run.status !== 'running')
            return run;
        if (run.agentId) {
            this.requireCancelTask()(run.agentId);
        }
        const endedAt = Date.now();
        const updatedRun = {
            ...run,
            status: 'cancelled',
            endedAt,
            durationMs: Math.max(0, endedAt - (run.startedAt ?? run.queuedAt)),
            cancelledReason: reason,
            updatedAt: endedAt,
        };
        this.runs.set(run.id, updatedRun);
        this.syncRunToRuntime(updatedRun, 'automation.cancel');
        const job = this.jobs.get(run.jobId);
        if (job) {
            const updatedJob = {
                ...job,
                lastRunAt: endedAt,
                updatedAt: endedAt,
            };
            this.jobs.set(job.id, updatedJob);
            await this.syncExecutionRoute(updatedJob, updatedRun);
            if (updatedRun.sessionId && updatedRun.continuationMode !== 'continued-live') {
                await this.sessionBroker.completeAgent(updatedRun.sessionId, updatedRun.agentId ?? run.id, reason, {
                    status: 'cancelled',
                    automationJobId: updatedJob.id,
                    automationRunId: updatedRun.id,
                });
            }
            this.syncJobToRuntime(updatedJob, 'automation.cancel');
            this.emitRunCompleted(updatedJob, updatedRun, 'cancelled');
            this.maybeDeliverRun(updatedJob, updatedRun);
        }
        await this.saveRuns();
        await this.saveJobs();
        return updatedRun;
    }
    async recordExternalRunResult(runId, input) {
        this.requireEnabled('record automation run result');
        await this.start();
        const run = this.runs.get(runId);
        if (!run)
            return null;
        if (run.status !== 'running')
            return run;
        const endedAt = Date.now();
        const updatedRun = {
            ...run,
            status: input.status,
            endedAt,
            durationMs: Math.max(0, endedAt - (run.startedAt ?? run.queuedAt)),
            updatedAt: endedAt,
            ...(input.status === 'completed'
                ? { result: input.result }
                : input.status === 'failed'
                    ? { error: input.error ?? 'Remote work failed' }
                    : { cancelledReason: input.error ?? 'Remote work cancelled' }),
            ...(input.telemetry ? { telemetry: normalizeExternalTelemetry(input.telemetry, run, input.metadata) } : {}),
        };
        this.runs.set(run.id, updatedRun);
        this.syncRunToRuntime(updatedRun, 'automation.external');
        const job = this.jobs.get(run.jobId);
        if (!job) {
            await this.saveRuns();
            return updatedRun;
        }
        const wasEnabled = job.enabled;
        const updatedJob = input.status === 'completed'
            ? {
                ...job,
                successCount: job.successCount + 1,
                failureCount: 0,
                updatedAt: endedAt,
            }
            : input.status === 'failed'
                ? this.applyFailureToJob(job, endedAt, false)
                : {
                    ...job,
                    updatedAt: endedAt,
                };
        this.jobs.set(job.id, updatedJob);
        await this.syncExecutionRoute(updatedJob, updatedRun);
        this.syncJobToRuntime(updatedJob, 'automation.external');
        if (input.status === 'completed') {
            this.emitRunCompleted(updatedJob, updatedRun, 'success');
        }
        else if (input.status === 'failed') {
            this.emitRunFailed(updatedJob, updatedRun, updatedRun.error ?? 'Remote work failed', false);
        }
        else {
            this.emitRunCompleted(updatedJob, updatedRun, 'cancelled');
        }
        this.maybeDeliverRun(updatedJob, updatedRun);
        if (input.status === 'completed' && updatedJob.deleteAfterRun) {
            this.cancelTimer(updatedJob.id);
            this.jobs.delete(updatedJob.id);
        }
        else if (input.status === 'failed') {
            this.scheduleFailureFollowUp(updatedJob, updatedRun);
        }
        if (!updatedJob.enabled && wasEnabled && input.status === 'failed') {
            this.emitJobAutoDisabled(updatedJob, updatedJob.pausedReason ?? 'failure-threshold-reached');
        }
        await this.saveRuns();
        await this.saveJobs();
        return updatedRun;
    }
    async load() {
        if (this.loaded)
            return;
        let [jobSnapshot, runSnapshot] = await Promise.all([
            this.jobStore.load(),
            this.runStore.load(),
        ]);
        for (const job of jobSnapshot.jobs) {
            const normalizedJob = normalizeJobRecord(job, this.configManager);
            this.jobs.set(normalizedJob.id, normalizedJob);
        }
        for (const run of runSnapshot.runs) {
            const normalizedRun = normalizeRunRecord(run, this.jobs.get(run.jobId));
            this.runs.set(normalizedRun.id, normalizedRun);
        }
        this.loaded = true;
        this.pruneRunHistory();
        this.syncRuntimeSnapshot();
    }
    scheduleJob(job) {
        scheduleAutomationJob({
            configManager: this.configManager,
            jobs: this.jobs,
            timers: this.timers,
            heartbeatWakes: this.heartbeatWakes,
            running: () => this.running,
            saveJobs: () => this.saveJobs(),
            activeRunCount: () => this.activeRunCount(),
            maxConcurrentRuns: () => this.maxConcurrentRuns(),
            executeJob: (scheduledJob, trigger, dueRun, attempt) => this.executeJob(scheduledJob, trigger, dueRun, attempt),
        }, job);
    }
    cancelTimer(jobId) {
        cancelAutomationTimer(this.timers, jobId);
    }
    queueDueHeartbeatJobs(reason) {
        queueDueHeartbeatAutomationJobs(this.jobs.values(), this.heartbeatWakes, reason);
    }
    advanceScheduledHeartbeatJob(jobId) {
        advanceScheduledHeartbeatAutomationJob({
            jobs: this.jobs,
            saveJobs: () => this.saveJobs(),
        }, this.timers, jobId, (job) => this.scheduleJob(job));
    }
    async executeJob(job, trigger, dueRun, attempt = 1) {
        return await executeAutomationJob(this.runtimeExecutionContext(), job, trigger, dueRun, attempt);
    }
    async syncExecutionRoute(job, run) {
        await syncAutomationExecutionRoute(this.runtimeExecutionContext(), job, run);
    }
    applyFailureToJob(job, timestamp, countRun = true) {
        return applyAutomationFailureToJob(job, timestamp, countRun);
    }
    reconcileActiveRuns() {
        if (!this.agentStatusProvider) {
            return;
        }
        reconcileAutomationActiveRuns({
            configManager: this.configManager,
            sessionBroker: this.sessionBroker,
            agentStatusProvider: this.agentStatusProvider,
            jobs: this.jobs,
            runs: this.runs,
            saveJobs: () => this.saveJobs(),
            saveRuns: () => this.saveRuns(),
            syncExecutionRoute: (job, run) => this.syncExecutionRoute(job, run),
            syncRunToRuntime: (run, source) => this.syncRunToRuntime(run, source),
            syncJobToRuntime: (job, source) => this.syncJobToRuntime(job, source),
            emitRunCompleted: (job, run, outcome) => this.emitRunCompleted(job, run, outcome),
            emitRunFailed: (job, run, error, retryable) => this.emitRunFailed(job, run, error, retryable),
            emitJobAutoDisabled: (job, reason) => this.emitJobAutoDisabled(job, reason),
            maybeDeliverRun: (job, run) => this.maybeDeliverRun(job, run),
            scheduleFailureFollowUp: (job, run) => this.scheduleFailureFollowUp(job, run),
            applyFailureToJob: (job, timestamp, countRun) => this.applyFailureToJob(job, timestamp, countRun),
            pruneRunHistory: () => this.pruneRunHistory(),
            cancelTimer: (jobId) => this.cancelTimer(jobId),
        });
    }
    async saveJobs() {
        await this.jobStore.save(sortJobs(this.jobs.values()));
    }
    async saveRuns() {
        this.pruneRunHistory();
        await this.runStore.save(sortRuns(this.runs.values()));
    }
    maxConcurrentRuns() {
        return Math.max(1, Number(this.configManager.get('automation.maxConcurrentRuns') ?? 4));
    }
    activeRunCount() {
        let total = 0;
        for (const run of this.runs.values()) {
            if (run.status === 'running')
                total += 1;
        }
        return total;
    }
    getSchedulerCapacity() {
        return computeSchedulerCapacity(this.maxConcurrentRuns(), this.runs.values());
    }
    pruneRunHistory(jobId) {
        const limit = Math.max(1, Number(this.configManager.get('automation.runHistoryLimit') ?? 100));
        const runs = sortRuns(this.runs.values());
        const keep = new Set();
        if (jobId) {
            const scoped = runs.filter((run) => run.jobId === jobId).slice(0, limit);
            for (const run of scoped)
                keep.add(run.id);
            for (const run of runs) {
                if (run.jobId !== jobId)
                    keep.add(run.id);
            }
        }
        else {
            const grouped = new Map();
            for (const run of runs) {
                const count = grouped.get(run.jobId) ?? 0;
                if (count < limit) {
                    grouped.set(run.jobId, count + 1);
                    keep.add(run.id);
                }
            }
        }
        for (const runId of [...this.runs.keys()]) {
            if (!keep.has(runId))
                this.runs.delete(runId);
        }
    }
    scheduleFailureFollowUp(job, run) {
        scheduleAutomationFailureFollowUp({
            jobs: this.jobs,
            retryTimers: this.retryTimers,
            deliveryManager: this.deliveryManager,
            activeRunCount: () => this.activeRunCount(),
            maxConcurrentRuns: () => this.maxConcurrentRuns(),
            executeJob: (scheduledJob, trigger, dueRun, attempt) => this.executeJob(scheduledJob, trigger, dueRun, attempt),
            saveJobs: () => this.saveJobs(),
            scheduleJob: (scheduledJob) => this.scheduleJob(scheduledJob),
        }, (failureJob, failureRun) => this.maybeDeliverFailureNotice(failureJob, failureRun), job, run);
    }
    maybeDeliverFailureNotice(job, run) {
        maybeDeliverAutomationFailureNotice(this.deliveryManager, job, run);
    }
    syncRuntimeSnapshot() {
        syncAutomationRuntimeSnapshot(this.runtimeDispatch, this.jobs.values(), this.runs.values());
    }
    syncJobToRuntime(job, source) {
        syncAutomationJobToRuntime(this.runtimeDispatch, job, source);
    }
    syncRunToRuntime(run, source) {
        syncAutomationRunToRuntime(this.runtimeDispatch, run, source);
    }
    emitJobCreated(job) {
        emitAutomationManagerJobCreated(this.runtimeBus, job);
    }
    emitJobUpdated(job, changedFields) {
        emitAutomationManagerJobUpdated(this.runtimeBus, job, changedFields);
    }
    emitJobAutoDisabled(job, reason) {
        emitAutomationManagerJobAutoDisabled(this.runtimeBus, job, reason);
    }
    emitRunQueued(job, run) {
        emitAutomationManagerRunQueued(this.runtimeBus, job, run);
    }
    emitRunStarted(job, run) {
        emitAutomationManagerRunStarted(this.runtimeBus, job, run);
    }
    emitRunCompleted(job, run, outcome) {
        emitAutomationManagerRunCompleted(this.runtimeBus, job, run, outcome);
    }
    emitRunFailed(job, run, error, retryable) {
        emitAutomationManagerRunFailed(this.runtimeBus, job, run, error, retryable);
    }
    maybeDeliverRun(job, run) {
        maybeDeliverAutomationRun({
            runs: this.runs,
            deliveryInFlight: this.deliveryInFlight,
            deliveryManager: this.deliveryManager,
            syncRunToRuntime: (nextRun, source) => this.syncRunToRuntime(nextRun, source),
            saveRuns: () => this.saveRuns(),
        }, job, run);
    }
}
