"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sortWorkflowFrontierCandidates = sortWorkflowFrontierCandidates;
exports.selectExecutionFrontier = selectExecutionFrontier;
const types_1 = require("../../platform/ticket-tracker/types");
const types_2 = require("../../platform/workflow-state/types");
const auto_merge_state_1 = require("../../workflows/auto-merge-state");
const plan_progress_1 = require("../../workflows/plan-progress");
const recurring_1 = require("../../workflows/recurring");
const persisted_state_1 = require("../../workflows/state/persisted-state");
const workflow_state_read_support_1 = require("../../workflows/workflow-state-read-support");
const execution_ownership_1 = require("../execution-ownership");
const workflow_issue_graph_1 = require("../workflow-issue-graph");
const controller_child_reflection_1 = require("./controller-child-reflection");
const dependencies_1 = require("./dependencies");
const preflight_resolution_blockers_1 = require("./preflight-resolution-blockers");
const shared_1 = require("./shared");
function sortWorkflowFrontierCandidates(candidates, graph) {
    return [...candidates]
        .filter((candidate) => {
        const node = graph.nodeByIssueId.get(candidate.issueId);
        return ((candidate.workflowState?.pendingPlanningIntent !== undefined ||
            node?.structuralViolation !== "non_leaf_execution") &&
            node?.isOrphan !== true);
    })
        .sort((left, right) => (0, workflow_issue_graph_1.compareWorkflowFrontierPriority)(left, right, graph));
}
async function selectExecutionFrontier(context, issues) {
    const selectionContext = await prepareWorkflowFrontierSelectionContext(context, issues);
    const controllerChildIssues = await collectControllerChildControlPlaneIssues(context, issues, selectionContext);
    const runningIssues = await collectExecutableRunningIssues(context, issues, selectionContext, {
        humanResumeOnly: !shouldResumeRunningContinuations(context),
    });
    const readyIssues = await collectExecutableReadyIssues(context, issues, selectionContext);
    return selectLaunchableWorkflowFrontier([...controllerChildIssues, ...runningIssues, ...readyIssues], selectionContext, context.issueIds ? undefined : context.config.parallelism.maxIssues);
}
async function prepareWorkflowFrontierSelectionContext(context, issues) {
    const graphIssues = dedupeTrackerIssues([
        ...issues,
        ...(context.workflowGraphIssues ?? []),
    ]);
    const workflowStateByIssueId = await (0, workflow_state_read_support_1.loadPersistedWorkflowStatesForIssues)(context.ticketTracker, graphIssues);
    return {
        workflowStateByIssueId,
        graph: (0, workflow_issue_graph_1.buildWorkflowIssueGraph)(graphIssues.map((issue) => ({
            issue,
            workflowState: resolvePersistedWorkflowStateForIssue(workflowStateByIssueId, issue.id),
        }))),
    };
}
async function collectExecutableReadyIssues(context, issues, selectionContext) {
    return collectExecutableWorkflowIssues(context, selectionContext, (0, shared_1.collectManagedIssues)(issues).filter((entry) => (0, types_2.isRunnableWorkflowState)(entry.snapshot)));
}
function dedupeTrackerIssues(issues) {
    return [
        ...new Map(issues.map((issue) => [issue.id, issue])).values(),
    ];
}
async function collectExecutableRunningIssues(context, issues, selectionContext, options) {
    const runningEntries = (0, shared_1.collectManagedIssues)(issues).filter((entry) => entry.snapshot.status === types_2.WORKFLOW_STATUSES.aiRunning);
    const filteredRunningEntries = options.humanResumeOnly
        ? runningEntries.filter((entry) => (0, persisted_state_1.isHumanInputResumeProjection)({
            snapshot: entry.snapshot,
            workflowState: resolvePersistedWorkflowStateForIssue(selectionContext.workflowStateByIssueId, entry.issue.id),
        }))
        : runningEntries;
    return collectExecutableWorkflowIssues(context, selectionContext, filteredRunningEntries);
}
async function collectExecutableWorkflowIssues(context, selectionContext, runnableIssues) {
    const executionStateByIssue = await Promise.all(runnableIssues.map(async (entry) => {
        const workflowState = resolvePersistedWorkflowStateForIssue(selectionContext.workflowStateByIssueId, entry.issue.id);
        const blockers = await context.ticketTracker.listBlockingDependencies(entry.issue.id);
        const openExecutionBlockers = (0, dependencies_1.resolveOpenExecutionBlockingDependencies)({
            strategy: context.config.dependencies.strategy,
            workflowState,
            blockers,
            parentIssueDisplayId: entry.issue.displayId,
        });
        const hasPendingPlanning = workflowState?.pendingPlanningIntent !== undefined;
        const canBypassOpenBlockersForPendingPlanning = await (0, preflight_resolution_blockers_1.canPendingPlanningBypassOpenExecutionBlockers)({
            parentIssueId: entry.issue.id,
            hasPendingPlanning,
            openExecutionBlockers,
            ticketTracker: context.ticketTracker,
        });
        return {
            entry,
            workflowState,
            openExecutionBlockers,
            canBypassOpenBlockersForPendingPlanning,
        };
    }));
    return sortWorkflowFrontierCandidates(executionStateByIssue
        .filter(({ entry, workflowState, openExecutionBlockers, canBypassOpenBlockersForPendingPlanning, }) => {
        return ((openExecutionBlockers.length === 0 ||
            canBypassOpenBlockersForPendingPlanning) &&
            isExecutableWorkflowIssue(entry.issue, workflowState) &&
            hasBasicWorkflowFrontierEligibility(entry.issue, workflowState));
    })
        .map(({ entry, workflowState }) => ({
        entry,
        issueId: entry.issue.id,
        workflowState,
    })), selectionContext.graph).map(({ entry }) => entry);
}
async function collectControllerChildControlPlaneIssues(context, issues, selectionContext) {
    const executionStateByIssue = await Promise.all((0, shared_1.collectManagedIssues)(issues)
        .filter((entry) => entry.snapshot.status === types_2.WORKFLOW_STATUSES.aiRunning ||
        (0, types_2.isRunnableWorkflowState)(entry.snapshot))
        .map(async (entry) => {
        const workflowState = resolvePersistedWorkflowStateForIssue(selectionContext.workflowStateByIssueId, entry.issue.id);
        const blockers = await context.ticketTracker.listBlockingDependencies(entry.issue.id);
        const openExecutionBlockers = (0, dependencies_1.resolveOpenExecutionBlockingDependencies)({
            strategy: context.config.dependencies.strategy,
            workflowState,
            blockers,
            parentIssueDisplayId: entry.issue.displayId,
        });
        const isControlPlaneCandidate = isControllerChildControlPlaneEligibleIssue(entry.issue, workflowState) &&
            workflowState?.pendingPlanningIntent === undefined &&
            (0, plan_progress_1.hasControllerChildIssueControlPlaneAction)(workflowState) &&
            hasBasicWorkflowFrontierEligibility(entry.issue, workflowState);
        const hasOnlyOpenControllerChildBlockers = openExecutionBlockers.length > 0 &&
            (0, controller_child_reflection_1.areOpenBlockersOnlyUnresolvedMaterializedControllerChildren)({
                workflowState,
                blockers: openExecutionBlockers,
            });
        const isWaitingOnOpenDescendant = isMaterializedControllerChildWaitingOnOpenDescendant({
            issueId: entry.issue.id,
            workflowState,
            graph: selectionContext.graph,
        });
        const shouldCheckReflectableCompletedChild = isControlPlaneCandidate &&
            (hasOnlyOpenControllerChildBlockers ||
                (openExecutionBlockers.length === 0 && isWaitingOnOpenDescendant));
        return {
            entry,
            workflowState,
            openExecutionBlockers,
            isControlPlaneCandidate,
            hasOnlyOpenControllerChildBlockers,
            isWaitingOnOpenDescendant,
            hasReflectableCompletedChild: shouldCheckReflectableCompletedChild
                ? await (0, controller_child_reflection_1.hasReflectableCompletedControllerChild)({
                    ticketTracker: context.ticketTracker,
                    parentIssue: entry.issue,
                    workflowState,
                    blockers,
                })
                : false,
        };
    }));
    return sortWorkflowFrontierCandidates(executionStateByIssue
        .filter(({ openExecutionBlockers, isControlPlaneCandidate, hasOnlyOpenControllerChildBlockers, isWaitingOnOpenDescendant, hasReflectableCompletedChild, }) => {
        const blockersAllowControlPlaneReflection = openExecutionBlockers.length === 0 ||
            (hasOnlyOpenControllerChildBlockers &&
                hasReflectableCompletedChild);
        return (blockersAllowControlPlaneReflection &&
            isControlPlaneCandidate &&
            (!isWaitingOnOpenDescendant || hasReflectableCompletedChild));
    })
        .map(({ entry, workflowState }) => ({
        entry: entry.issue.workflowState.status === types_2.WORKFLOW_STATUSES.aiRunning
            ? {
                issue: entry.issue,
                snapshot: (0, shared_1.cloneWorkflowStateSnapshot)(entry.issue.workflowState, types_2.WORKFLOW_STATUSES.aiRunning),
            }
            : {
                issue: entry.issue,
                snapshot: entry.issue
                    .workflowState,
            },
        issueId: entry.issue.id,
        workflowState,
    })), selectionContext.graph).map(({ entry }) => entry);
}
function selectLaunchableWorkflowFrontier(entries, selectionContext, limit) {
    const dedupedEntries = entries.filter((entry, index, allEntries) => allEntries.findIndex((candidate) => candidate.issue.id === entry.issue.id) === index);
    const sortedEntries = sortWorkflowFrontierCandidates(dedupedEntries.map((entry) => ({
        entry,
        issueId: entry.issue.id,
        workflowState: resolvePersistedWorkflowStateForIssue(selectionContext.workflowStateByIssueId, entry.issue.id),
    })), selectionContext.graph).map(({ entry }) => entry);
    const reservedIssueIds = new Set();
    const launchableEntries = [];
    let workerEntryCount = 0;
    for (const entry of sortedEntries) {
        const workflowState = resolvePersistedWorkflowStateForIssue(selectionContext.workflowStateByIssueId, entry.issue.id);
        const usesWorkerSlot = doesWorkflowFrontierEntryConsumeWorkerSlot(workflowState);
        if (limit !== undefined && usesWorkerSlot && workerEntryCount >= limit) {
            continue;
        }
        const affectedIssueIds = resolveWorkflowFrontierAffectedIssueIds({
            issueId: entry.issue.id,
            workflowState,
            graph: selectionContext.graph,
        });
        if (affectedIssueIds.some((issueId) => reservedIssueIds.has(issueId))) {
            continue;
        }
        launchableEntries.push(entry);
        for (const issueId of affectedIssueIds) {
            reservedIssueIds.add(issueId);
        }
        if (usesWorkerSlot) {
            workerEntryCount += 1;
        }
    }
    return launchableEntries;
}
function isMaterializedControllerChildWaitingOnOpenDescendant(input) {
    const node = input.graph.nodeByIssueId.get(input.issueId);
    return ((node?.openDescendantCount ?? 0) > 0 &&
        (0, plan_progress_1.hasMaterializedSelectedControllerChildIssue)(input.workflowState));
}
function isExecutableWorkflowIssue(issue, workflowState) {
    if ((0, persisted_state_1.isHumanInputResumeProjection)({
        snapshot: issue.workflowState,
        workflowState,
    })) {
        return (0, plan_progress_1.hasWorkflowHumanResumeExecutableAction)(workflowState);
    }
    if (workflowState?.pendingPlanningIntent !== undefined) {
        return true;
    }
    if ((0, plan_progress_1.hasControllerChildIssueControlPlaneAction)(workflowState)) {
        return false;
    }
    return (0, plan_progress_1.hasWorkflowWorkerExecutableAction)(workflowState);
}
function doesWorkflowFrontierEntryConsumeWorkerSlot(workflowState) {
    return (!(0, plan_progress_1.hasControllerChildIssueControlPlaneAction)(workflowState) &&
        !(0, recurring_1.isRecurringParentWorkflowState)(workflowState));
}
function isControllerChildControlPlaneEligibleIssue(issue, workflowState) {
    return (issue.kind === types_1.TRACKER_ITEM_KINDS.issue &&
        (workflowState?.workflowSurface ?? "issue") === "issue");
}
function resolveWorkflowFrontierAffectedIssueIds(input) {
    if ((0, plan_progress_1.hasControllerChildIssueControlPlaneAction)(input.workflowState)) {
        return collectWorkflowSubtreeIssueIds(input.issueId, input.graph);
    }
    return [input.issueId];
}
function collectWorkflowSubtreeIssueIds(issueId, graph) {
    const issueIds = new Set();
    const pendingIssueIds = [issueId];
    while (pendingIssueIds.length > 0) {
        const nextIssueId = pendingIssueIds.pop();
        if (nextIssueId === undefined || issueIds.has(nextIssueId)) {
            continue;
        }
        issueIds.add(nextIssueId);
        const node = graph.nodeByIssueId.get(nextIssueId);
        if (node === undefined) {
            continue;
        }
        pendingIssueIds.push(...node.childIssueIds);
    }
    return [...issueIds];
}
function shouldResumeRunningContinuations(context) {
    return (context.runSource === "service" ||
        (context.runSource === "manual" &&
            Array.isArray(context.issueIds) &&
            context.issueIds.length > 0));
}
function resolvePersistedWorkflowStateForIssue(workflowStateByIssueId, issueId) {
    return workflowStateByIssueId.get(issueId) ?? null;
}
function hasBasicWorkflowFrontierEligibility(issue, workflowState) {
    return (issue.isOpen &&
        !(0, execution_ownership_1.isExecutionOwnershipActive)(workflowState?.executionOwnership) &&
        !(0, auto_merge_state_1.hasActiveAutoMergeState)(workflowState));
}
