"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildLaunchableServiceTasks = buildLaunchableServiceTasks;
exports.normalizeAffectedIssueIds = normalizeAffectedIssueIds;
const worker_slot_budget_1 = require("../../run/worker-slot-budget");
const plan_progress_1 = require("../../workflows/plan-progress");
const repo_scheduling_support_1 = require("./repo-scheduling-support");
const workflow_task_graph_1 = require("./workflow-task-graph");
function buildLaunchableServiceTasks(input) {
    const workerCapacity = resolveAvailableWorkerCapacity(input.activeTasks, input.config);
    const availableMergeSlots = Math.max(0, 1 - (0, repo_scheduling_support_1.countActiveMergeTasks)(input.activeTasks));
    const pendingPublicationRecoveryTasks = filterLaunchableServiceTasks({
        tasks: sortServiceIssueTasksByIssueId(input.pendingPublicationRecoveryTasks),
        blockedIssueIds: new Set(),
        activeTasks: input.activeTasks,
    });
    const controlPlaneActionTasks = buildLaunchableControlPlaneActionTasks({
        activeTasks: input.activeTasks,
        blockedIssueIds: input.blockedIssueIds,
        controlPlaneActionIssues: excludeTerminalSourceFinalizationIssues(input.controlPlaneActionIssues, input.terminalSourceFinalizationTasks),
        controlPlaneActionIssueIdsWithReflectableCompletedChild: input.controlPlaneActionIssueIdsWithReflectableCompletedChild,
        workflowGraph: input.workflowGraph,
        workflowGraphIssues: input.workflowGraphIssues,
        workflowStateByIssueId: input.workflowStateByIssueId,
    });
    const controlPlaneActionTasksWithWorkerCapacity = filterTasksByWorkerCapacity(controlPlaneActionTasks, workerCapacity);
    const reservedIssueIdsBeforeMerge = collectReservedIssueIds([
        ...controlPlaneActionTasksWithWorkerCapacity,
        ...pendingPublicationRecoveryTasks,
    ]);
    const mergeTasks = selectLaunchableMergeTasks({
        activeTasks: input.activeTasks,
        availableMergeSlots,
        blockedIssueIds: input.blockedIssueIds,
        autoMergePullRequestTasks: input.autoMergePullRequestTasks,
        needsHumanPullRequestTasks: input.needsHumanPullRequestTasks,
        reservedIssueIds: reservedIssueIdsBeforeMerge,
        workerCapacity,
    });
    const reservedIssueIdsBeforeTerminalFinalization = collectReservedIssueIds([
        ...controlPlaneActionTasksWithWorkerCapacity,
        ...pendingPublicationRecoveryTasks,
        ...mergeTasks,
    ]);
    const terminalSourceFinalizationTasks = selectLaunchableTerminalSourceFinalizationTasks({
        activeTasks: input.activeTasks,
        blockedIssueIds: input.blockedIssueIds,
        tasks: input.terminalSourceFinalizationTasks,
        reservedIssueIds: reservedIssueIdsBeforeTerminalFinalization,
        workerCapacity,
    });
    const cleanupAndRecurringTasks = buildLaunchableCleanupAndRecurringTasks({
        activeTasks: input.activeTasks,
        blockedIssueIds: input.blockedIssueIds,
        recurringParentIssues: input.recurringParentIssues,
        workingInPrCleanupIssues: input.workingInPrCleanupIssues,
        workflowGraphIssues: input.workflowGraphIssues,
        workflowStateByIssueId: input.workflowStateByIssueId,
    });
    const reservedIssueIdsBeforeWorkers = collectReservedIssueIds([
        ...controlPlaneActionTasksWithWorkerCapacity,
        ...pendingPublicationRecoveryTasks,
        ...mergeTasks,
        ...terminalSourceFinalizationTasks,
    ]);
    const workerTaskCandidates = sortWorkerTasksForSlotBudgets(filterTasksReservedByIssueIds(filterLaunchableServiceTasks({
        tasks: [...(input.singleSlotWorkerTasks ?? []), ...input.workerTasks],
        blockedIssueIds: input.blockedIssueIds,
        activeTasks: input.activeTasks,
    }), reservedIssueIdsBeforeWorkers), input.workflowGraph, input.workflowStateByIssueId).slice(0, Math.min(workerCapacity.availableWorkerIssueSlots, workerCapacity.availableWorkerSlots));
    const workerTasks = assignWorkerSlotBudgets(workerTaskCandidates, workerCapacity.availableWorkerSlots, input.config.parallelism.mode, input.workflowStateByIssueId);
    reserveWorkerCapacityForSelectedTasks(workerTasks, workerCapacity);
    const reservedIssueIdsBeforeCleanup = collectReservedIssueIds([
        ...controlPlaneActionTasksWithWorkerCapacity,
        ...pendingPublicationRecoveryTasks,
        ...mergeTasks,
        ...terminalSourceFinalizationTasks,
        ...workerTasks,
    ]);
    const cleanupAndRecurringTasksNotReservedByWorkers = filterTasksByWorkerCapacity(filterTasksReservedByIssueIds(cleanupAndRecurringTasks, reservedIssueIdsBeforeCleanup), workerCapacity);
    return filterReservedServiceTasks((0, workflow_task_graph_1.sortServiceTasksByWorkflowFrontier)([
        ...controlPlaneActionTasksWithWorkerCapacity,
        ...pendingPublicationRecoveryTasks,
        ...mergeTasks,
        ...terminalSourceFinalizationTasks,
        ...workerTasks,
        ...cleanupAndRecurringTasksNotReservedByWorkers,
    ], input.workflowGraph, input.workflowStateByIssueId));
}
function excludeTerminalSourceFinalizationIssues(issues, terminalSourceFinalizationTasks) {
    const issueIds = new Set((terminalSourceFinalizationTasks ?? []).map((task) => task.issue.id));
    return issueIds.size === 0
        ? issues
        : issues.filter((issue) => !issueIds.has(issue.id));
}
function buildLaunchableControlPlaneActionTasks(input) {
    const runnableIssues = input.controlPlaneActionIssues.filter((issue) => input.controlPlaneActionIssueIdsWithReflectableCompletedChild?.has(issue.id) === true ||
        !isMaterializedControllerChildWaitingOnOpenDescendant({
            issueId: issue.id,
            workflowState: input.workflowStateByIssueId.get(issue.id) ?? null,
            workflowGraph: input.workflowGraph,
        }));
    return filterLaunchableServiceTasks({
        tasks: toScheduledServiceIssueTasks(runnableIssues, {
            workerSlotCount: 1,
            requiresWorkerRuntime: true,
            consumesMergeSlot: false,
            workflowGraphIssues: input.workflowGraphIssues,
            resolveAffectedIssueIds: (issue) => (0, workflow_task_graph_1.collectWorkflowSubtreeIssueIds)(issue.id, input.workflowGraph),
        }),
        blockedIssueIds: input.blockedIssueIds,
        activeTasks: input.activeTasks,
    });
}
function selectLaunchableMergeTasks(input) {
    if (input.availableMergeSlots <= 0) {
        return [];
    }
    const selectedTasks = [];
    for (const tasks of [
        input.autoMergePullRequestTasks,
        input.needsHumanPullRequestTasks,
    ]) {
        for (const task of filterTasksReservedByIssueIds(filterLaunchableServiceTasks({
            tasks: sortServiceIssueTasksByIssueId(tasks),
            blockedIssueIds: input.blockedIssueIds,
            activeTasks: input.activeTasks,
        }), input.reservedIssueIds)) {
            if (selectedTasks.length >= input.availableMergeSlots) {
                return selectedTasks;
            }
            if (!tryReserveWorkerCapacity(task, input.workerCapacity)) {
                continue;
            }
            selectedTasks.push(task);
        }
    }
    return selectedTasks;
}
function selectLaunchableTerminalSourceFinalizationTasks(input) {
    return filterTasksByWorkerCapacity(filterTasksReservedByIssueIds(filterLaunchableServiceTasks({
        tasks: sortServiceIssueTasksByIssueId(input.tasks ?? []),
        blockedIssueIds: input.blockedIssueIds,
        activeTasks: input.activeTasks,
    }), input.reservedIssueIds), input.workerCapacity);
}
function buildLaunchableCleanupAndRecurringTasks(input) {
    return filterLaunchableServiceTasks({
        tasks: [
            ...toScheduledServiceIssueTasks(input.workingInPrCleanupIssues, {
                workerSlotCount: 0,
                requiresWorkerRuntime: false,
                consumesMergeSlot: false,
                workflowGraphIssues: input.workflowGraphIssues,
                resolveRequiresWorkerRuntime: (issue) => (input.workflowStateByIssueId.get(issue.id) ?? null) !== null,
            }),
            ...toScheduledServiceIssueTasks(input.recurringParentIssues, {
                workerSlotCount: 0,
                requiresWorkerRuntime: false,
                consumesMergeSlot: false,
                workflowGraphIssues: input.workflowGraphIssues,
            }),
        ],
        blockedIssueIds: input.blockedIssueIds,
        activeTasks: input.activeTasks,
    });
}
function sortWorkerTasksForSlotBudgets(tasks, workflowGraph, workflowStateByIssueId) {
    const hasWorkflowStateContext = tasks.some((task) => (workflowStateByIssueId.get(task.issue.id) ?? null) !== null);
    if (!hasWorkflowStateContext) {
        return tasks;
    }
    return (0, workflow_task_graph_1.sortServiceTasksByWorkflowFrontier)(tasks, workflowGraph, workflowStateByIssueId);
}
function isMaterializedControllerChildWaitingOnOpenDescendant(input) {
    const node = input.workflowGraph.nodeByIssueId.get(input.issueId);
    return ((node?.openDescendantCount ?? 0) > 0 &&
        (0, plan_progress_1.hasMaterializedSelectedControllerChildIssue)(input.workflowState));
}
function normalizeAffectedIssueIds(task) {
    return [...new Set([task.issue.id, ...task.affectedIssueIds])];
}
function filterLaunchableServiceTasks(input) {
    return input.tasks
        .filter((task) => !input.blockedIssueIds.has(task.issue.id))
        .filter((task) => !input.activeTasks.byIssueId.has(task.issue.id))
        .filter((task) => !normalizeAffectedIssueIds(task).some((issueId) => input.activeTasks.byIssueId.has(issueId) ||
        input.activeTasks.refreshingIssueIds.has(issueId)));
}
function toScheduledServiceIssueTasks(issues, options) {
    return issues.map((issue) => {
        const requiresWorkerRuntime = options.resolveRequiresWorkerRuntime?.(issue) ??
            options.requiresWorkerRuntime;
        return {
            kind: "run_cycle",
            issue,
            affectedIssueIds: options.resolveAffectedIssueIds?.(issue) ?? [issue.id],
            ...(options.workflowGraphIssues
                ? { workflowGraphIssues: options.workflowGraphIssues }
                : {}),
            requiresWorkerRuntime,
            workerSlotCount: requiresWorkerRuntime
                ? Math.max(1, options.workerSlotCount)
                : options.workerSlotCount,
            ...(options.workerSlotDemand === undefined
                ? {}
                : { workerSlotDemand: options.workerSlotDemand }),
            consumesMergeSlot: options.consumesMergeSlot,
        };
    });
}
function assignWorkerSlotBudgets(tasks, availableWorkerSlots, mode, workflowStateByIssueId) {
    const workerSlotBudgets = (0, worker_slot_budget_1.distributeWorkerSlots)({
        issueCount: tasks.length,
        maxIssues: tasks.length,
        maxWorkers: availableWorkerSlots,
        mode,
        issueDemands: tasks.map((task) => task.workerSlotDemand ??
            (0, plan_progress_1.resolveWorkflowWorkerSlotDemand)(workflowStateByIssueId.get(task.issue.id) ?? null)),
    });
    return tasks.slice(0, workerSlotBudgets.length).map((task, index) => {
        return {
            ...task,
            requiresWorkerRuntime: true,
            workerSlotCount: workerSlotBudgets[index] ?? 1,
        };
    });
}
function sortServiceIssueTasksByIssueId(tasks) {
    return [...tasks].sort((left, right) => {
        const leftNumeric = Number(left.issue.id);
        const rightNumeric = Number(right.issue.id);
        if (!Number.isNaN(leftNumeric) && !Number.isNaN(rightNumeric)) {
            return leftNumeric - rightNumeric;
        }
        return left.issue.id.localeCompare(right.issue.id);
    });
}
function collectReservedIssueIds(tasks) {
    return new Set(tasks.flatMap((task) => normalizeAffectedIssueIds(task)));
}
function filterTasksReservedByIssueIds(tasks, reservedIssueIds) {
    if (reservedIssueIds.size === 0) {
        return tasks;
    }
    return tasks.filter((task) => !normalizeAffectedIssueIds(task).some((issueId) => reservedIssueIds.has(issueId)));
}
function filterReservedServiceTasks(tasks) {
    const reservedIssueIds = new Set();
    const launchableTasks = [];
    for (const task of tasks) {
        const affectedIssueIds = normalizeAffectedIssueIds(task);
        if (affectedIssueIds.some((issueId) => reservedIssueIds.has(issueId))) {
            continue;
        }
        launchableTasks.push(task);
        for (const issueId of affectedIssueIds) {
            reservedIssueIds.add(issueId);
        }
    }
    return launchableTasks;
}
function resolveAvailableWorkerCapacity(activeTasks, config) {
    return {
        availableWorkerIssueSlots: Math.max(0, config.parallelism.maxIssues - (0, repo_scheduling_support_1.countActiveWorkerIssueTasks)(activeTasks)),
        availableWorkerSlots: Math.max(0, config.parallelism.maxWorkers - (0, repo_scheduling_support_1.countActiveWorkerSlots)(activeTasks)),
    };
}
function filterTasksByWorkerCapacity(tasks, capacity) {
    return tasks.filter((task) => tryReserveWorkerCapacity(task, capacity));
}
function reserveWorkerCapacityForSelectedTasks(tasks, capacity) {
    for (const task of tasks) {
        tryReserveWorkerCapacity(task, capacity);
    }
}
function tryReserveWorkerCapacity(task, capacity) {
    if (task.workerSlotCount <= 0) {
        return true;
    }
    if (capacity.availableWorkerIssueSlots <= 0 ||
        capacity.availableWorkerSlots < task.workerSlotCount) {
        return false;
    }
    capacity.availableWorkerIssueSlots -= 1;
    capacity.availableWorkerSlots -= task.workerSlotCount;
    return true;
}
