import { randomInt, randomUUID } from 'node:crypto';
import { ChannelDeliveryRouter, RouteBindingManager } from '../channels/index.js';
import { ServiceRegistry } from '../config/service-registry.js';
import { createDomainDispatch } from '../runtime/store/index.js';
import { emitDeliveryFailed, emitDeliveryQueued, emitDeliveryStarted, emitDeliverySucceeded, } from '../runtime/emitters/index.js';
import { classifyDeliveryError } from '../integrations/delivery.js';
import { summarizeError } from '../utils/error-display.js';
import { isFeatureGateEnabled, isSurfaceFeatureGateEnabled } from '../runtime/feature-flags/index.js';
function sleep(ms) {
    return new Promise((resolve) => {
        const timer = setTimeout(resolve, ms);
        timer.unref?.();
    });
}
function calculateRetryDelay(baseDelayMs, attempt, strategy, maxDelayMs, jitterMs) {
    const multiplier = strategy === 'fixed' ? 1 : strategy === 'linear' ? attempt : Math.pow(2, attempt - 1);
    const base = baseDelayMs * multiplier;
    const jitter = jitterMs ? randomInt(0, Math.max(1, Math.floor(jitterMs) + 1)) : 0;
    return Math.min(base + jitter, maxDelayMs ?? Number.MAX_SAFE_INTEGER);
}
function buildDeliveryMessage(job, run) {
    const lines = [
        `Automation: ${job.name}`,
        `Run: ${run.id}`,
        `Status: ${run.status}`,
    ];
    if (run.error)
        lines.push(`Error: ${run.error}`);
    if (job.delivery.includeSummary) {
        const content = typeof run.result === 'string'
            ? run.result
            : run.result !== undefined
                ? JSON.stringify(run.result, null, 2)
                : '';
        if (content) {
            lines.push('');
            lines.push(content.length > 3500 ? `${content.slice(0, 3500)}...` : content);
        }
    }
    return lines.join('\n');
}
function toDeliveryKind(surfaceKind) {
    return surfaceKind === 'webhook' ? 'callback' : 'notification';
}
function toDeliverySurfaceKind(surfaceKind) {
    return surfaceKind ?? 'webhook';
}
function extractArtifactReferences(result) {
    if (!result || typeof result !== 'object')
        return [];
    const record = result;
    const rawItems = [
        ...(Array.isArray(record.artifacts) ? record.artifacts : []),
        ...(Array.isArray(record.attachments) ? record.attachments : []),
        ...(Array.isArray(record.artifactIds) ? record.artifactIds.map((artifactId) => ({ artifactId })) : []),
    ];
    const references = [];
    for (const entry of rawItems) {
        if (typeof entry === 'string' && entry.trim().length > 0) {
            references.push({ artifactId: entry.trim() });
            continue;
        }
        if (!entry || typeof entry !== 'object')
            continue;
        const candidate = entry;
        const artifactId = typeof candidate.artifactId === 'string'
            ? candidate.artifactId
            : typeof candidate.id === 'string'
                ? candidate.id
                : '';
        if (!artifactId)
            continue;
        references.push({
            artifactId,
            ...(typeof candidate.label === 'string' ? { label: candidate.label } : {}),
            ...(typeof candidate.metadata === 'object' && candidate.metadata !== null
                ? { metadata: candidate.metadata }
                : {}),
        });
    }
    return references.filter((entry, index, values) => values.findIndex((candidate) => candidate.artifactId === entry.artifactId) === index);
}
export class AutomationDeliveryManager {
    routeBindings;
    deliveryRouter;
    featureFlags;
    runtimeDispatch = null;
    runtimeBus = null;
    constructor(config) {
        this.routeBindings = config.routeBindings;
        this.featureFlags = config.featureFlags ?? null;
        if (config.deliveryRouter) {
            this.deliveryRouter = config.deliveryRouter;
        }
        else {
            if (!config.serviceRegistry || !config.configManager || !config.artifactStore) {
                throw new Error('AutomationDeliveryManager requires serviceRegistry, configManager, and artifactStore when no deliveryRouter is provided.');
            }
            this.deliveryRouter = new ChannelDeliveryRouter({
                configManager: config.configManager,
                secretsManager: config.secretsManager,
                serviceRegistry: config.serviceRegistry,
                artifactStore: config.artifactStore,
            });
        }
        if (config.runtimeStore)
            this.runtimeDispatch = createDomainDispatch(config.runtimeStore);
        this.runtimeBus = config.runtimeBus ?? null;
    }
    attachRuntime(config) {
        if (config.runtimeStore) {
            this.runtimeDispatch = createDomainDispatch(config.runtimeStore);
        }
        if (config.runtimeBus) {
            this.runtimeBus = config.runtimeBus;
        }
    }
    getDeliveryRouter() {
        return this.deliveryRouter;
    }
    isEnabled() {
        return isFeatureGateEnabled(this.featureFlags, 'delivery-engine');
    }
    isTargetEnabled(target) {
        return isSurfaceFeatureGateEnabled(this.featureFlags, target.surfaceKind ?? 'webhook');
    }
    setControlPlaneGateway(gateway) {
        this.deliveryRouter.setControlPlaneGateway(gateway);
    }
    async deliverJobRun(job, run) {
        if (!this.isEnabled())
            return [];
        if (job.delivery.mode === 'none')
            return [];
        await this.routeBindings.start();
        const body = buildDeliveryMessage(job, run);
        const primaryTargets = this.resolveTargets(job.delivery.targets.length > 0
            ? [...job.delivery.targets]
            : job.delivery.replyToRouteId
                ? [{ kind: 'surface', routeId: job.delivery.replyToRouteId }]
                : []);
        const attempts = await this.deliverText(job, run, body, primaryTargets.map((entry) => entry.target));
        const needsFallback = attempts.length === 0 || attempts.every((attempt) => attempt.status === 'failed' || attempt.status === 'dead_lettered');
        if (!needsFallback || job.delivery.fallbackTargets.length === 0) {
            return attempts;
        }
        const fallbackAttempts = await this.deliverText(job, run, body, job.delivery.fallbackTargets);
        return [...attempts, ...fallbackAttempts];
    }
    async deliverText(job, run, body, targets) {
        if (!this.isEnabled())
            return [];
        await this.routeBindings.start();
        const resolvedTargets = this.resolveTargets(targets).filter((entry) => this.isTargetEnabled(entry.target));
        if (resolvedTargets.length === 0)
            return [];
        const attempts = [];
        for (const resolved of resolvedTargets) {
            const target = resolved.target;
            const deliveryId = `delivery-${randomUUID().slice(0, 8)}`;
            const startedAt = Date.now();
            let attempt = {
                id: deliveryId,
                runId: run.id,
                jobId: job.id,
                target,
                status: 'pending',
            };
            attempts.push(attempt);
            this.syncAttempt(attempt, run, 'queued');
            const retryPolicy = job.failure.retryPolicy;
            let attemptIndex = 0;
            let lastError = '';
            while (attemptIndex < Math.max(1, retryPolicy.maxAttempts)) {
                attemptIndex += 1;
                attempt = {
                    ...attempt,
                    status: 'sending',
                    startedAt: Date.now(),
                    error: undefined,
                };
                this.syncAttempt(attempt, run, 'started');
                try {
                    const responseId = await this.sendTarget(resolved, body, job, run);
                    if (resolved.binding?.id && responseId) {
                        await this.routeBindings.captureReplyTarget(resolved.binding.id, responseId, resolved.binding.threadId);
                    }
                    attempt = {
                        ...attempt,
                        status: 'sent',
                        endedAt: Date.now(),
                        responseId: responseId ?? target.address ?? target.routeId ?? target.label ?? target.surfaceKind ?? 'delivered',
                    };
                    attempts[attempts.length - 1] = attempt;
                    this.syncAttempt(attempt, run, 'succeeded');
                    lastError = '';
                    break;
                }
                catch (error) {
                    lastError = summarizeError(error);
                    const retryable = classifyDeliveryError(error) === 'retryable';
                    const lastTry = attemptIndex >= Math.max(1, retryPolicy.maxAttempts);
                    if (!retryable || lastTry) {
                        attempt = {
                            ...attempt,
                            status: retryable ? 'dead_lettered' : 'failed',
                            endedAt: Date.now(),
                            error: lastError,
                        };
                        attempts[attempts.length - 1] = attempt;
                        this.syncAttempt(attempt, run, 'failed', lastError, retryable);
                        break;
                    }
                    const delayMs = calculateRetryDelay(retryPolicy.delayMs, attemptIndex, retryPolicy.strategy, retryPolicy.maxDelayMs, retryPolicy.jitterMs);
                    await sleep(delayMs);
                }
            }
            if (lastError && attempts[attempts.length - 1]?.status === 'sending') {
                const failedAttempt = {
                    ...attempts[attempts.length - 1],
                    status: 'failed',
                    endedAt: Date.now(),
                    error: lastError,
                };
                attempts[attempts.length - 1] = failedAttempt;
                this.syncAttempt(failedAttempt, run, 'failed', lastError, true);
            }
        }
        return attempts;
    }
    resolveTargets(targets) {
        return targets.map((target) => {
            if (!target.routeId)
                return target;
            const binding = this.routeBindings.getBinding(target.routeId);
            if (!binding)
                return { target };
            return {
                target: {
                    ...target,
                    surfaceKind: target.surfaceKind ?? binding.surfaceKind,
                    address: target.address
                        ?? (typeof binding.metadata.responseUrl === 'string' ? binding.metadata.responseUrl
                            : typeof binding.metadata.callbackUrl === 'string' ? binding.metadata.callbackUrl
                                : binding.channelId ?? binding.externalId),
                    label: target.label ?? binding.title,
                },
                binding,
            };
        }).map((entry) => ('target' in entry ? entry : { target: entry }));
    }
    async sendTarget(resolved, body, job, run) {
        return this.deliveryRouter.deliver({
            target: resolved.target,
            body,
            title: job.name,
            jobId: job.id,
            runId: run.id,
            status: run.status,
            includeLinks: job.delivery.includeLinks,
            ...(run.result !== undefined ? { attachments: extractArtifactReferences(run.result) } : {}),
            ...(run.agentId ? { agentId: run.agentId } : {}),
            ...(resolved.binding ? { binding: resolved.binding } : {}),
        });
    }
    syncAttempt(attempt, run, phase, error, retryable = false) {
        this.runtimeDispatch?.syncDeliveryAttempt(attempt, `deliveries.${phase}`);
        if (!this.runtimeBus)
            return;
        const surfaceKind = toDeliverySurfaceKind(attempt.target.surfaceKind);
        const targetId = attempt.target.address ?? attempt.target.routeId ?? 'unknown';
        const ctx = {
            sessionId: run.target.sessionId ?? 'automation',
            source: 'automation-delivery-manager',
            traceId: attempt.id,
        };
        if (phase === 'queued') {
            emitDeliveryQueued(this.runtimeBus, ctx, {
                deliveryId: attempt.id,
                jobId: attempt.jobId,
                runId: attempt.runId,
                surfaceKind,
                targetId,
                deliveryKind: toDeliveryKind(surfaceKind),
            });
            return;
        }
        if (phase === 'started') {
            emitDeliveryStarted(this.runtimeBus, ctx, {
                deliveryId: attempt.id,
                jobId: attempt.jobId,
                runId: attempt.runId,
                surfaceKind,
                targetId,
                startedAt: attempt.startedAt ?? Date.now(),
            });
            return;
        }
        if (phase === 'succeeded') {
            emitDeliverySucceeded(this.runtimeBus, ctx, {
                deliveryId: attempt.id,
                jobId: attempt.jobId,
                runId: attempt.runId,
                surfaceKind,
                targetId,
                completedAt: attempt.endedAt ?? Date.now(),
                durationMs: Math.max(0, (attempt.endedAt ?? Date.now()) - (attempt.startedAt ?? Date.now())),
                statusCode: 200,
            });
            return;
        }
        emitDeliveryFailed(this.runtimeBus, ctx, {
            deliveryId: attempt.id,
            jobId: attempt.jobId,
            runId: attempt.runId,
            surfaceKind,
            targetId,
            failedAt: attempt.endedAt ?? Date.now(),
            error: error ?? attempt.error ?? 'delivery failed',
            retryable,
        });
    }
}
