"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.syncPlannedIssueDependencies = syncPlannedIssueDependencies;
exports.applyDependencyCompletionPreflight = applyDependencyCompletionPreflight;
exports.reconcileCompletedDependencyRequeuesAfterMerge = reconcileCompletedDependencyRequeuesAfterMerge;
exports.resolveUnrequeuedManagedDependencies = resolveUnrequeuedManagedDependencies;
exports.resolvePullRequestManagedDependencyState = resolvePullRequestManagedDependencyState;
exports.markManagedDependenciesRequeuedInWorkflowState = markManagedDependenciesRequeuedInWorkflowState;
exports.resolveOpenExecutionBlockingDependencies = resolveOpenExecutionBlockingDependencies;
const canonical_workflow_state_1 = require("../../comments/canonical-workflow-state");
const workflow_update_1 = require("../../comments/workflow-update");
const transient_network_error_1 = require("../../core/transient-network-error");
const types_1 = require("../../platform/ticket-tracker/types");
const plan_progress_1 = require("../../workflows/plan-progress");
const publisher_shared_1 = require("../../workflows/publisher-shared");
const recurring_1 = require("../../workflows/recurring");
const recurring_auto_stop_1 = require("../../workflows/recurring-auto-stop");
const default_issue_workspace_manager_1 = require("../../workspaces/default-issue-workspace-manager");
const execution_ownership_1 = require("../execution-ownership");
const workflow_structure_1 = require("../workflow-structure");
const dependencies_helpers_1 = require("./dependencies-helpers");
const dependency_publication_1 = require("./dependency-publication");
const publisher_factory_1 = require("./publisher-factory");
const recurring_child_dependencies_1 = require("./recurring-child-dependencies");
const shared_1 = require("./shared");
const shared_2 = require("./spawn-child-controller/shared");
async function syncPlannedIssueDependencies(input) {
    if (input.issue.kind !== types_1.TRACKER_ITEM_KINDS.issue) {
        return { summary: null, transientFailure: false };
    }
    if (input.context.config.dependencies.strategy === "none") {
        return { summary: null, transientFailure: false };
    }
    const desiredDependencies = (await inferDesiredDependencyIssueIds(input.context, input.issue.id, input.workflowState, input.resolvePersistedIssueWorkflowState)).map(dependencies_helpers_1.toDesiredManagedDependency);
    const existingState = (0, dependencies_helpers_1.normalizeDependencySyncState)(input.workflowState.dependencySyncState);
    const liveBlockers = await input.context.ticketTracker.listBlockingDependencies(input.issue.id);
    const nextDependencySyncState = (0, dependencies_helpers_1.reconcileDependencySyncState)({
        existing: existingState,
        desired: desiredDependencies,
        strategy: input.context.config.dependencies.strategy,
    });
    const dependencySyncStateChanged = !(0, dependencies_helpers_1.areDependencySyncStatesEqual)(input.workflowState.dependencySyncState, nextDependencySyncState);
    const operations = (0, dependencies_helpers_1.buildDependencyTrackerOperations)({
        strategy: input.context.config.dependencies.strategy,
        desired: desiredDependencies,
        existing: existingState,
        liveBlockers,
    });
    if (operations.addIssueIds.length === 0 &&
        operations.removeIssueIds.length === 0 &&
        !dependencySyncStateChanged) {
        return { summary: null, transientFailure: false };
    }
    const failures = await applyDependencyReconcileOperations({
        context: input.context,
        issueId: input.issue.id,
        addIssueIds: operations.addIssueIds,
        removeIssueIds: operations.removeIssueIds,
        liveBlockers,
        locale: input.context.config.i18n.defaultLanguage,
    });
    if (failures.length === 0) {
        if (dependencySyncStateChanged) {
            const nextWorkflowState = {
                ...input.workflowState,
                dependencySyncState: nextDependencySyncState,
            };
            try {
                await (0, dependency_publication_1.persistDependencySyncState)({
                    context: input.context,
                    issueId: input.issue.id,
                    workflowState: nextWorkflowState,
                    locale: input.context.config.i18n.defaultLanguage,
                });
            }
            catch (error) {
                return handleDependencyMetadataPersistFailure({
                    context: input.context,
                    issue: input.issue,
                    workflowState: nextWorkflowState,
                    error,
                    handOffDependencyIssueToHuman: input.handOffDependencyIssueToHuman,
                });
            }
        }
        return { summary: null, transientFailure: false };
    }
    if (failures.some((failure) => !failure.transient)) {
        const issue = await input.context.ticketTracker.getIssue(input.issue.id);
        return {
            summary: await input.handOffDependencyIssueToHuman({
                context: input.context,
                executable: {
                    issue,
                    snapshot: issue.workflowState,
                },
                workflowState: input.workflowState,
                summary: "Dependency tracker sync was rejected and cannot be retried safely. Review the blocker state before resuming.",
            }),
            transientFailure: false,
        };
    }
    return { summary: null, transientFailure: true };
}
async function applyDependencyCompletionPreflight(input) {
    if (input.context.config.dependencies.strategy !== "auto") {
        return { workflowStateChanged: false };
    }
    const resolution = (0, canonical_workflow_state_1.resolveCanonicalWorkflowStateComment)(input.comments);
    if (resolution.kind !== "found") {
        return { workflowStateChanged: false };
    }
    const workflowState = resolution.workflowState;
    const { closedDependencies } = await resolveUnrequeuedManagedDependencies({
        ticketTracker: input.context.ticketTracker,
        workflowState,
    });
    if (closedDependencies.length === 0) {
        return { workflowStateChanged: false };
    }
    const requeue = await (0, dependency_publication_1.publishDependencyCompletionRequeue)({
        context: input.context,
        targetIssue: await input.context.ticketTracker.getIssue(input.issue.id),
        locale: input.locale,
        completedDependencyIssueIds: closedDependencies.map((dependency) => dependency.issueId),
        currentWorkflowState: workflowState,
        nextWorkflowState: markManagedDependenciesRequeuedInWorkflowState({
            workflowState,
            dependencyIssueIds: closedDependencies.map((dependency) => dependency.issueId),
        }),
        previousNeedsHumanReason: null,
    });
    return { workflowStateChanged: requeue.workflowStateChanged };
}
async function reconcileCompletedDependencyRequeuesAfterMerge(input) {
    if (input.context.config.dependencies.strategy !== "auto") {
        return 0;
    }
    const openIssues = await input.context.ticketTracker.listIssues({
        state: "open",
        includeSourceBranchName: true,
    });
    let requeuedCount = 0;
    for (const issue of openIssues) {
        if (issue.kind !== types_1.TRACKER_ITEM_KINDS.issue ||
            issue.id === input.sourceIssueId) {
            continue;
        }
        const issueComments = await (0, shared_1.loadSortedIssueComments)(input.context.ticketTracker, issue.id, {
            timeline: "issue-only",
        });
        const issueResolution = (0, canonical_workflow_state_1.resolveCanonicalWorkflowStateComment)(issueComments);
        if (issueResolution.kind !== "found") {
            continue;
        }
        const issueWorkflowState = issueResolution.workflowState;
        if (!issueWorkflowState.dependencySyncState?.managedDependencies.some((dependency) => dependency.issueId === input.sourceIssueId &&
            dependency.completionRequeuedAt === null)) {
            continue;
        }
        const primaryTarget = await resolvePrimaryDependencyCompletionTarget({
            context: input.context,
            issue,
        });
        if (primaryTarget === null || primaryTarget.isExecutionActive) {
            continue;
        }
        const completedDependencyIssueIds = [input.sourceIssueId];
        const nextIssueWorkflowState = markManagedDependenciesRequeuedInWorkflowState({
            workflowState: issueWorkflowState,
            dependencyIssueIds: completedDependencyIssueIds,
        });
        const primaryTargetIsSourceIssue = primaryTarget.issue.id === issue.id;
        const requeue = await (0, dependency_publication_1.publishDependencyCompletionRequeue)({
            context: input.context,
            targetIssue: primaryTarget.issue,
            locale: input.locale,
            completedDependencyIssueIds,
            ...(primaryTarget.workflowState !== undefined
                ? { currentWorkflowState: primaryTarget.workflowState }
                : {}),
            ...(primaryTargetIsSourceIssue
                ? { nextWorkflowState: nextIssueWorkflowState }
                : {}),
            ...(primaryTarget.previousNeedsHumanReason !== undefined
                ? {
                    previousNeedsHumanReason: primaryTarget.previousNeedsHumanReason,
                }
                : {}),
        });
        if (!requeue.accepted) {
            continue;
        }
        requeuedCount += 1;
        if (!primaryTargetIsSourceIssue &&
            !(0, dependencies_helpers_1.areDependencySyncStatesEqual)(issueWorkflowState.dependencySyncState, (0, dependencies_helpers_1.normalizeDependencySyncState)(nextIssueWorkflowState.dependencySyncState))) {
            await (0, dependency_publication_1.persistDependencySyncState)({
                context: input.context,
                issueId: issue.id,
                workflowState: nextIssueWorkflowState,
                locale: input.locale,
            });
        }
    }
    return requeuedCount;
}
async function resolveUnrequeuedManagedDependencies(input) {
    const managedDependencies = (0, dependencies_helpers_1.normalizeDependencySyncState)(input.workflowState?.dependencySyncState).managedDependencies.filter((dependency) => dependency.completionRequeuedAt === null);
    if (managedDependencies.length === 0) {
        return {
            openDependencies: [],
            closedDependencies: [],
        };
    }
    const cleanupStates = await Promise.all([
        ...new Set(managedDependencies.map((dependency) => dependency.issueId)),
    ].map(async (issueId) => ({
        issueId,
        cleanupState: await input.ticketTracker.getIssueCleanupState(issueId),
    })));
    const openIssueIds = new Set(cleanupStates
        .filter(({ cleanupState }) => cleanupState.isOpen === true)
        .map(({ issueId }) => issueId));
    return {
        openDependencies: managedDependencies.filter((dependency) => openIssueIds.has(dependency.issueId)),
        closedDependencies: managedDependencies.filter((dependency) => !openIssueIds.has(dependency.issueId)),
    };
}
async function resolvePullRequestManagedDependencyState(input) {
    const linkedIssueId = input.linkedIssueId ??
        (await (0, shared_1.resolveLinkedIssueIdFromPullRequestTimeline)(input.ticketTracker, input.issue));
    if (linkedIssueId === null || linkedIssueId === input.issue.id) {
        return emptyPullRequestManagedDependencyState(linkedIssueId);
    }
    const linkedIssue = await input.ticketTracker.getIssueRecord(linkedIssueId);
    if (linkedIssue.kind !== types_1.TRACKER_ITEM_KINDS.issue) {
        return emptyPullRequestManagedDependencyState(linkedIssueId);
    }
    const linkedComments = await (0, shared_1.loadSortedIssueComments)(input.ticketTracker, linkedIssueId, { timeline: "issue-only" });
    const workflowState = (0, canonical_workflow_state_1.resolveCanonicalWorkflowStateComment)(linkedComments, input.locale);
    if (workflowState.kind !== "found") {
        return emptyPullRequestManagedDependencyState(linkedIssueId);
    }
    const dependencies = await resolveUnrequeuedManagedDependencies({
        ticketTracker: input.ticketTracker,
        workflowState: workflowState.workflowState,
    });
    return {
        linkedIssueId,
        sourceWorkflowState: workflowState.workflowState,
        ...dependencies,
    };
}
function emptyPullRequestManagedDependencyState(linkedIssueId) {
    return {
        linkedIssueId,
        sourceWorkflowState: null,
        openDependencies: [],
        closedDependencies: [],
    };
}
function markManagedDependenciesRequeuedInWorkflowState(input) {
    const dependencyIssueIds = new Set(input.dependencyIssueIds);
    if (dependencyIssueIds.size === 0) {
        return input.workflowState;
    }
    const requeuedAt = input.requeuedAt ?? new Date().toISOString();
    return {
        ...input.workflowState,
        dependencySyncState: {
            ...(0, dependencies_helpers_1.normalizeDependencySyncState)(input.workflowState.dependencySyncState),
            managedDependencies: (0, dependencies_helpers_1.normalizeDependencySyncState)(input.workflowState.dependencySyncState).managedDependencies.map((dependency) => dependencyIssueIds.has(dependency.issueId)
                ? {
                    ...dependency,
                    completionRequeuedAt: dependency.completionRequeuedAt ?? requeuedAt,
                }
                : dependency),
        },
    };
}
function resolveOpenExecutionBlockingDependencies(input) {
    const openBlockers = input.blockers.filter((blocker) => blocker.isOpen &&
        !isSameParentRecurringChildBlocker({
            blocker,
            workflowState: input.workflowState,
            parentIssueDisplayId: input.parentIssueDisplayId,
        }));
    if (openBlockers.length === 0 ||
        input.strategy === "block" ||
        input.strategy === "none") {
        return openBlockers;
    }
    const ignoredBlockerIds = new Set([
        ...(0, dependencies_helpers_1.normalizeDependencySyncState)(input.workflowState?.dependencySyncState).appliedAutoBlockers.map((dependency) => dependency.issueId),
        ...resolveManagedControllerChildBlockerIdsToIgnore(input.workflowState),
    ]);
    return openBlockers.filter((blocker) => !ignoredBlockerIds.has(blocker.id));
}
function isSameParentRecurringChildBlocker(input) {
    return (input.parentIssueDisplayId !== undefined &&
        (0, recurring_1.isRecurringParentWorkflowState)(input.workflowState) &&
        (0, recurring_auto_stop_1.resolveRecurringLifecycleDecision)(input.workflowState) !== "stop_due" &&
        (0, recurring_child_dependencies_1.isSameParentRecurringChildDependency)({
            dependency: input.blocker,
            parentDisplayId: input.parentIssueDisplayId,
        }));
}
function resolveManagedControllerChildBlockerIdsToIgnore(workflowState) {
    const plan = workflowState?.plan ?? null;
    if (plan === null ||
        ((0, plan_progress_1.selectReadyTaskWorkflowSteps)(plan).length === 0 &&
            (0, plan_progress_1.selectReadyWorkflowRefStep)(plan) === null)) {
        return [];
    }
    return plan.steps.flatMap((step) => {
        if (step.kind !== "child_issue") {
            return [];
        }
        const childIssueId = (0, shared_2.normalizeControllerChildIssueRef)(step.childIssueRef);
        return childIssueId === null ? [] : [childIssueId];
    });
}
async function resolvePrimaryDependencyCompletionTarget(input) {
    const branchName = input.issue.sourceBranchName ??
        (0, default_issue_workspace_manager_1.resolveIssueBranchName)(input.context.config.branchPrefix, input.issue);
    const pullRequest = await input.context.repositoryHost.findPullRequestByBranch(branchName);
    if (pullRequest !== null && pullRequest.state === "open") {
        const pullRequestThread = await input.context.ticketTracker.getPullRequestThreadRecord({
            pullRequestNumber: pullRequest.number,
            pullRequestUrl: pullRequest.url,
        });
        if (pullRequestThread === null) {
            return null;
        }
        const pullRequestIssue = await input.context.ticketTracker.getIssue(pullRequestThread.id);
        const comments = await (0, shared_1.loadSortedIssueComments)(input.context.ticketTracker, pullRequestIssue.id, {
            timeline: "issue-only",
        });
        const resolution = (0, canonical_workflow_state_1.resolveCanonicalWorkflowStateComment)(comments);
        return {
            issue: pullRequestIssue,
            ...(resolution.kind === "found"
                ? { workflowState: resolution.workflowState }
                : {}),
            previousNeedsHumanReason: (0, workflow_update_1.findLatestNeedsHumanReason)(comments),
            isExecutionActive: resolution.kind === "found" &&
                (0, execution_ownership_1.isExecutionOwnershipActive)(resolution.workflowState.executionOwnership),
        };
    }
    const comments = await (0, shared_1.loadSortedIssueComments)(input.context.ticketTracker, input.issue.id, {
        timeline: "issue-only",
    });
    const resolution = (0, canonical_workflow_state_1.resolveCanonicalWorkflowStateComment)(comments);
    return {
        issue: input.issue,
        ...(resolution.kind === "found"
            ? { workflowState: resolution.workflowState }
            : {}),
        previousNeedsHumanReason: (0, workflow_update_1.findLatestNeedsHumanReason)(comments),
        isExecutionActive: resolution.kind === "found" &&
            (0, execution_ownership_1.isExecutionOwnershipActive)(resolution.workflowState.executionOwnership),
    };
}
async function inferDesiredDependencyIssueIds(context, issueId, workflowState, resolvePersistedIssueWorkflowState) {
    if ((0, workflow_structure_1.isWorkflowParentState)(workflowState) ||
        workflowState.plan === null ||
        workflowState.predictedTouchedPaths.length === 0) {
        return [];
    }
    const openIssues = await context.ticketTracker.listIssues({ state: "open" });
    const desiredDependencies = [];
    for (const candidate of openIssues) {
        if (candidate.id === issueId || !candidate.isOpen) {
            continue;
        }
        const candidateWorkflowState = await resolvePersistedIssueWorkflowState(context.ticketTracker, candidate.id);
        if (candidateWorkflowState === null ||
            (0, workflow_structure_1.isWorkflowParentState)(candidateWorkflowState) ||
            candidateWorkflowState.predictedTouchedPaths.length === 0) {
            continue;
        }
        const overlappingPaths = workflowState.predictedTouchedPaths.filter((entry) => candidateWorkflowState.predictedTouchedPaths.includes(entry));
        if (overlappingPaths.length === 0) {
            continue;
        }
        if (!(0, dependencies_helpers_1.shouldAutoBlockOnOverlappingPaths)(overlappingPaths)) {
            continue;
        }
        if ((0, shared_1.compareExecutionPriority)({
            issueId: candidate.id,
            workflowState: candidateWorkflowState,
        }, {
            issueId,
            workflowState,
        }) < 0) {
            desiredDependencies.push({
                issueId: candidate.id,
                overlappingPaths,
                workflowState: candidateWorkflowState,
            });
        }
    }
    return [...desiredDependencies]
        .sort((left, right) => (0, shared_1.compareExecutionPriority)({
        issueId: left.issueId,
        workflowState: left.workflowState,
    }, {
        issueId: right.issueId,
        workflowState: right.workflowState,
    }))
        .filter((dependency, index, entries) => index === 0 || dependency.issueId !== entries[index - 1]?.issueId)
        .map(({ issueId: blockedByIssueId, overlappingPaths }) => ({
        issueId: blockedByIssueId,
        overlappingPaths,
    }));
}
async function handleDependencyMetadataPersistFailure(input) {
    if ((0, transient_network_error_1.isTransientNetworkError)(input.error)) {
        return { summary: null, transientFailure: true };
    }
    const issue = await input.context.ticketTracker.getIssue(input.issue.id);
    return {
        summary: await input.handOffDependencyIssueToHuman({
            context: input.context,
            executable: {
                issue,
                snapshot: issue.workflowState,
            },
            workflowState: null,
            summary: "Dependency sync state comment sync was rejected after blocker updates. Review the canonical workflow state comment before resuming.",
        }),
        transientFailure: false,
    };
}
async function applyDependencyReconcileOperations(input) {
    const failures = [];
    const publisher = createRunContextWorkflowPublisher(input.context);
    for (const blockedByIssueId of input.removeIssueIds) {
        if (!input.liveBlockers.some((blocker) => blocker.id === blockedByIssueId)) {
            continue;
        }
        try {
            await publishDependencyChange({
                publisher,
                issueId: input.issueId,
                blockedByIssueId,
                kind: "remove_blocking_dependency",
                locale: input.locale,
            });
        }
        catch (error) {
            failures.push({
                issueId: blockedByIssueId,
                transient: (0, transient_network_error_1.isTransientNetworkError)(error),
            });
        }
    }
    for (const blockedByIssueId of input.addIssueIds) {
        const currentLiveBlockers = await input.context.ticketTracker.listBlockingDependencies(input.issueId);
        if (currentLiveBlockers.some((blocker) => blocker.id === blockedByIssueId)) {
            continue;
        }
        const reverseLiveBlockers = await input.context.ticketTracker.listBlockingDependencies(blockedByIssueId);
        if (reverseLiveBlockers.some((blocker) => blocker.id === input.issueId)) {
            failures.push({
                issueId: blockedByIssueId,
                transient: false,
            });
            continue;
        }
        try {
            await publishDependencyChange({
                publisher,
                issueId: input.issueId,
                blockedByIssueId,
                kind: "add_blocking_dependency",
                locale: input.locale,
            });
        }
        catch (error) {
            failures.push({
                issueId: blockedByIssueId,
                transient: (0, transient_network_error_1.isTransientNetworkError)(error),
            });
        }
    }
    return failures;
}
function createRunContextWorkflowPublisher(context) {
    return (0, publisher_factory_1.createRunWorkflowPublisher)(context);
}
async function publishDependencyChange(input) {
    await input.publisher.publishWorkflow({
        target: {
            kind: "existing_issue",
            issueId: input.issueId,
        },
        workflowState: null,
        dependencyChanges: [
            {
                kind: input.kind,
                issue: { kind: "target" },
                blockedBy: { kind: "issue", issueId: input.blockedByIssueId },
            },
        ],
        commentCursorPolicy: {
            mode: publisher_shared_1.WORKFLOW_COMMENT_CURSOR_POLICY_MODES.preserve,
        },
        responseLanguage: input.locale,
    });
}
