"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.hasActiveRecurringSchedule = hasActiveRecurringSchedule;
exports.isRecurringParentWorkflowState = isRecurringParentWorkflowState;
exports.hasActiveRecurringParentSchedule = hasActiveRecurringParentSchedule;
exports.resolveRecurringLabelNames = resolveRecurringLabelNames;
exports.isRecurringMonitoringIssueProjection = isRecurringMonitoringIssueProjection;
exports.hasRecurringChildIssueTitlePrefix = hasRecurringChildIssueTitlePrefix;
exports.resolveRecurringProjection = resolveRecurringProjection;
exports.resolveNextRunAtFromAcceptedAt = resolveNextRunAtFromAcceptedAt;
exports.resolveInitialRecurringNextRunAt = resolveInitialRecurringNextRunAt;
exports.isWorkflowScheduleInputValid = isWorkflowScheduleInputValid;
exports.resolveNextRunAtAfterExecution = resolveNextRunAtAfterExecution;
const catalog_1 = require("../i18n/catalog");
const title_progress_1 = require("../platform/ticket-tracker/title-progress");
const workflow_labels_1 = require("../platform/ticket-tracker/workflow-labels");
const types_1 = require("../platform/workflow-state/types");
const schedule_primitives_1 = require("./schedule-primitives");
const types_2 = require("./types");
const recurring_auto_stop_1 = require("./recurring-auto-stop");
const CRON_FIELD_RANGES = [
    { min: 0, max: 59 },
    { min: 0, max: 23 },
    { min: 1, max: 31 },
    { min: 1, max: 12 },
    { min: 0, max: 6 },
];
const RECURRING_CHILD_TITLE_PREFIX_KEY = "run.recurring.child.titlePrefix";
function hasActiveRecurringSchedule(workflowState) {
    return (workflowState !== null &&
        workflowState !== undefined &&
        workflowState.schedule !== undefined &&
        workflowState.schedule.mode !== "off");
}
function isRecurringParentWorkflowState(workflowState) {
    return (workflowState !== null &&
        workflowState !== undefined &&
        workflowState.recurring?.kind === "parent");
}
function hasActiveRecurringParentSchedule(workflowState) {
    return (isRecurringParentWorkflowState(workflowState) &&
        hasActiveRecurringSchedule(workflowState));
}
function resolveRecurringLabelNames(aiRecurringLabel, workflowState) {
    return aiRecurringLabel && isRecurringParentWorkflowState(workflowState)
        ? [aiRecurringLabel]
        : [];
}
function buildRecurringMonitorSnapshot(snapshot) {
    return {
        status: types_1.WORKFLOW_STATUSES.aiRecurring,
        rules: snapshot.rules.filter((rule) => rule !== types_1.WORKFLOW_RULES.aiDisabled),
    };
}
function buildDisabledRecurringSnapshot(snapshot) {
    return {
        status: types_1.WORKFLOW_STATUSES.aiRecurring,
        rules: snapshot.rules.includes(types_1.WORKFLOW_RULES.aiDisabled)
            ? [...snapshot.rules]
            : [...snapshot.rules, types_1.WORKFLOW_RULES.aiDisabled],
    };
}
function buildReadyRecurringSnapshot(snapshot) {
    return {
        status: types_1.WORKFLOW_STATUSES.aiReady,
        rules: snapshot.rules.filter((rule) => rule !== types_1.WORKFLOW_RULES.aiDisabled),
    };
}
function isRecurringMonitoringIssueProjection(input) {
    const labels = new Set(input.issue.labelNames ?? []);
    const resolvedLabels = (0, workflow_labels_1.resolveWorkflowLabels)(input.workflowLabels);
    const hasRecurringLabel = input.issue.labelNames === undefined ||
        labels.has(resolvedLabels.aiRecurring);
    return (input.issue.workflowState.status === types_1.WORKFLOW_STATUSES.aiRecurring &&
        !input.issue.workflowState.rules.includes(types_1.WORKFLOW_RULES.aiDisabled) &&
        hasRecurringLabel &&
        !labels.has(resolvedLabels.aiReady));
}
function hasRecurringChildIssueTitlePrefix(title) {
    const baseTitle = (0, title_progress_1.stripIssueTitleProgressPrefix)(title);
    return (0, catalog_1.translateAllLocales)(RECURRING_CHILD_TITLE_PREFIX_KEY).some((prefix) => baseTitle.startsWith(prefix));
}
function resolveRecurringProjection(input) {
    if (!isRecurringParentWorkflowState(input.workflowState)) {
        return {
            snapshot: input.snapshot,
        };
    }
    if (input.snapshot.status === types_1.WORKFLOW_STATUSES.aiNeedsHuman) {
        return {
            snapshot: input.snapshot,
        };
    }
    if (input.workflowState.schedule?.mode === "off" ||
        input.snapshot.rules.includes(types_1.WORKFLOW_RULES.aiDisabled)) {
        return {
            snapshot: buildDisabledRecurringSnapshot(input.snapshot),
        };
    }
    const decision = (0, recurring_auto_stop_1.resolveRecurringLifecycleDecision)(input.workflowState);
    if (decision === "wait" || decision === "inactive") {
        return {
            snapshot: buildRecurringMonitorSnapshot(input.snapshot),
        };
    }
    return {
        snapshot: buildReadyRecurringSnapshot(input.snapshot),
    };
}
function resolveNextRunAtFromAcceptedAt(schedule, acceptedAt, displayTimeZone) {
    return resolveNextRunAt(schedule, acceptedAt, null, displayTimeZone);
}
function resolveInitialRecurringNextRunAt(input) {
    if (input.schedule.mode === "off") {
        return null;
    }
    if (input.firstRun === undefined) {
        return resolveNextRunAtFromAcceptedAt(input.schedule, input.acceptedAt, input.displayTimeZone);
    }
    const acceptedAtMs = Date.parse(input.acceptedAt);
    if (!Number.isFinite(acceptedAtMs)) {
        return null;
    }
    if (input.firstRun.mode === "now") {
        return new Date(acceptedAtMs).toISOString();
    }
    const runAt = input.firstRun.mode === "run_after"
        ? (0, schedule_primitives_1.resolveWorkflowRunAfter)(input.firstRun.value, input.acceptedAt)
        : (0, schedule_primitives_1.normalizeWorkflowRunAt)(input.firstRun.value, input.displayTimeZone);
    if (runAt === null) {
        return null;
    }
    const runAtMs = Date.parse(runAt);
    if (!Number.isFinite(runAtMs) || runAtMs <= acceptedAtMs) {
        return null;
    }
    return runAt;
}
function isWorkflowScheduleInputValid(schedule) {
    if (schedule.mode === "off") {
        return true;
    }
    if (schedule.mode === "every") {
        return (0, schedule_primitives_1.parseWorkflowDurationToMilliseconds)(schedule.value) !== null;
    }
    return parseCronExpression(schedule.value) !== null;
}
function resolveNextRunAtAfterExecution(input) {
    return resolveNextRunAt(input.workflowState.schedule ?? (0, types_2.createOffWorkflowScheduleState)(), input.executedAt, input.workflowState.nextRunAt ?? null, input.displayTimeZone);
}
function resolveNextRunAt(schedule, referenceAt, previousNextRunAt, displayTimeZone) {
    if (schedule.mode === "off") {
        return null;
    }
    if (schedule.mode === "every") {
        const durationMs = (0, schedule_primitives_1.parseWorkflowDurationToMilliseconds)(schedule.value);
        if (durationMs === null) {
            return null;
        }
        const referenceMs = Date.parse(referenceAt);
        if (!Number.isFinite(referenceMs)) {
            return null;
        }
        const previousMs = previousNextRunAt !== null ? Date.parse(previousNextRunAt) : Number.NaN;
        const baseMs = Number.isFinite(previousMs) ? previousMs : referenceMs;
        let nextMs = baseMs;
        while (nextMs <= referenceMs) {
            nextMs += durationMs;
        }
        return new Date(nextMs).toISOString();
    }
    return resolveNextCronOccurrence(schedule.value, referenceAt, displayTimeZone);
}
function resolveNextCronOccurrence(expression, referenceAt, displayTimeZone) {
    const matcher = parseCronExpression(expression);
    if (matcher === null) {
        return null;
    }
    const referenceMs = Date.parse(referenceAt);
    if (!Number.isFinite(referenceMs)) {
        return null;
    }
    let cursorMs = Math.floor(referenceMs / 60_000) * 60_000 + 60_000;
    for (let index = 0; index < 525_600; index += 1) {
        const parts = (0, schedule_primitives_1.renderDateTimePartsInZone)(cursorMs, displayTimeZone);
        if (parts === null) {
            return null;
        }
        if (matcher(parts)) {
            return new Date(cursorMs).toISOString();
        }
        cursorMs += 60_000;
    }
    return null;
}
function parseCronExpression(expression) {
    const fields = expression.trim().split(/\s+/u);
    if (fields.length !== CRON_FIELD_RANGES.length) {
        return null;
    }
    const [minuteField, hourField, dayOfMonthField, monthField, dayOfWeekField] = fields;
    const [minuteRange, hourRange, dayOfMonthRange, monthRange, dayOfWeekRange] = CRON_FIELD_RANGES;
    if (minuteField === undefined ||
        hourField === undefined ||
        dayOfMonthField === undefined ||
        monthField === undefined ||
        dayOfWeekField === undefined) {
        return null;
    }
    const minuteMatcher = parseCronField(minuteField, minuteRange);
    const hourMatcher = parseCronField(hourField, hourRange);
    const dayOfMonthMatcher = parseCronField(dayOfMonthField, dayOfMonthRange);
    const monthMatcher = parseCronField(monthField, monthRange);
    const dayOfWeekMatcher = parseCronField(dayOfWeekField, dayOfWeekRange);
    if (minuteMatcher === null ||
        hourMatcher === null ||
        dayOfMonthMatcher === null ||
        monthMatcher === null ||
        dayOfWeekMatcher === null) {
        return null;
    }
    return (parts) => minuteMatcher(parts.minute) &&
        hourMatcher(parts.hour) &&
        dayOfMonthMatcher(parts.day) &&
        monthMatcher(parts.month) &&
        dayOfWeekMatcher(parts.dayOfWeek);
}
function parseCronField(field, range) {
    const parts = field.split(",");
    const predicates = [];
    for (const part of parts) {
        const predicate = parseCronPart(part, range);
        if (predicate === null) {
            return null;
        }
        predicates.push(predicate);
    }
    return (value) => predicates.some((predicate) => predicate(value));
}
function parseCronPart(part, range) {
    const text = part.trim();
    if (text === "*") {
        return () => true;
    }
    const [base, stepText] = text.split("/");
    const baseText = base ?? "";
    const step = stepText === undefined ? 1 : Number.parseInt(stepText, 10);
    if (!Number.isInteger(step) || step <= 0) {
        return null;
    }
    if (baseText === "*") {
        return (value) => value >= range.min &&
            value <= range.max &&
            (value - range.min) % step === 0;
    }
    if (baseText.includes("-")) {
        const [startText, endText] = baseText.split("-");
        const start = Number.parseInt(startText ?? "", 10);
        const end = Number.parseInt(endText ?? "", 10);
        if (!Number.isInteger(start) ||
            !Number.isInteger(end) ||
            start < range.min ||
            end > range.max ||
            start > end) {
            return null;
        }
        return (value) => value >= start && value <= end && (value - start) % step === 0;
    }
    const exact = Number.parseInt(baseText, 10);
    if (!Number.isInteger(exact) || exact < range.min || exact > range.max) {
        return null;
    }
    return (value) => value === exact;
}
