"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONFIG_SCAN_RETRY_DELAY_MS = exports.CONFIG_SCAN_MAX_ATTEMPTS_PER_ISSUE_UPDATE = exports.REPO_SYNC_LOOKBACK_MS = exports.REPO_FULL_RESYNC_INTERVAL_MS = void 0;
exports.createRepoServiceStateRegistry = createRepoServiceStateRegistry;
exports.getOrCreateRepoSnapshotState = getOrCreateRepoSnapshotState;
exports.getActiveRepoCooldown = getActiveRepoCooldown;
exports.recordRepoCooldown = recordRepoCooldown;
exports.pruneExpiredRepoCooldowns = pruneExpiredRepoCooldowns;
exports.resolveRepoSyncPlan = resolveRepoSyncPlan;
exports.mergeRepoSyncResult = mergeRepoSyncResult;
exports.recordRepoSyncCheckpoint = recordRepoSyncCheckpoint;
exports.recordRepoConfigScanAttempt = recordRepoConfigScanAttempt;
exports.removeRepoSnapshotIssue = removeRepoSnapshotIssue;
exports.refreshRepoSnapshotIssue = refreshRepoSnapshotIssue;
const constants_1 = require("../platform/constants");
exports.REPO_FULL_RESYNC_INTERVAL_MS = 30 * 60 * 1_000;
// Keep delta queries wide enough to absorb provider timestamp granularity and
// short scan retries without turning every poll into a full comment scan.
exports.REPO_SYNC_LOOKBACK_MS = 120_000;
exports.CONFIG_SCAN_MAX_ATTEMPTS_PER_ISSUE_UPDATE = 2;
exports.CONFIG_SCAN_RETRY_DELAY_MS = 30_000;
function createRepoServiceStateRegistry() {
    return {
        snapshotsByTrackedProjectKey: new Map(),
        cooldownsByTrackedProjectKey: new Map(),
    };
}
function getOrCreateRepoSnapshotState(registry, trackedProjectKey) {
    const existing = registry.snapshotsByTrackedProjectKey.get(trackedProjectKey);
    if (existing) {
        return existing;
    }
    const snapshot = {
        openIssuesById: new Map(),
        lastSuccessfulSyncAtMs: null,
        lastFullSyncAtMs: null,
        pendingConfigScanIssueIds: new Set(),
        configScanAttemptsByIssueId: new Map(),
        lastStatePatchWorkflowStateVersion: null,
        configScanActivityCursor: null,
    };
    registry.snapshotsByTrackedProjectKey.set(trackedProjectKey, snapshot);
    return snapshot;
}
function getActiveRepoCooldown(registry, trackedProjectKey, nowMs = Date.now()) {
    const cooldown = registry.cooldownsByTrackedProjectKey.get(trackedProjectKey);
    return cooldown && cooldown.nextRetryAtMs > nowMs ? cooldown : null;
}
function recordRepoCooldown(registry, trackedProjectKey, cooldown) {
    const existing = registry.cooldownsByTrackedProjectKey.get(trackedProjectKey);
    const normalized = existing
        ? {
            ...cooldown,
            nextRetryAtMs: Math.max(existing.nextRetryAtMs, cooldown.nextRetryAtMs),
        }
        : cooldown;
    registry.cooldownsByTrackedProjectKey.set(trackedProjectKey, normalized);
    return normalized;
}
function pruneExpiredRepoCooldowns(registry, nowMs = Date.now()) {
    let removed = 0;
    for (const [trackedProjectKey, cooldown,] of registry.cooldownsByTrackedProjectKey) {
        if (cooldown.nextRetryAtMs > nowMs) {
            continue;
        }
        registry.cooldownsByTrackedProjectKey.delete(trackedProjectKey);
        removed += 1;
    }
    return removed;
}
function resolveRepoSyncPlan(snapshot, nowMs = Date.now()) {
    const needsFullSync = snapshot.lastFullSyncAtMs === null ||
        nowMs - snapshot.lastFullSyncAtMs >= exports.REPO_FULL_RESYNC_INTERVAL_MS;
    if (needsFullSync) {
        return {
            mode: "full",
            query: {
                state: constants_1.PLATFORM_ISSUE_LIST_STATES.open,
                includeSourceBranchName: true,
            },
        };
    }
    const sinceMs = snapshot.lastSuccessfulSyncAtMs === null
        ? null
        : Math.max(0, snapshot.lastSuccessfulSyncAtMs - exports.REPO_SYNC_LOOKBACK_MS);
    return {
        mode: "delta",
        query: {
            state: "all",
            includeSourceBranchName: true,
            ...(sinceMs === null ? {} : { since: new Date(sinceMs).toISOString() }),
        },
    };
}
function mergeRepoSyncResult(snapshot, issues, mode, options) {
    const nextIssuesById = mode === "full"
        ? mergeFullRepoSyncResult(snapshot, issues)
        : mergeDeltaRepoSyncResult(snapshot, issues);
    snapshot.openIssuesById = nextIssuesById;
    for (const issueId of options.additionalConfigScanCandidateIssueIds ?? []) {
        if (snapshot.openIssuesById.has(issueId)) {
            snapshot.pendingConfigScanIssueIds.add(issueId);
        }
    }
    pruneStaleConfigScanIssueState(snapshot);
    const configScanCandidateIssueIds = resolveConfigScanCandidateIssueIds(snapshot, options.nowMs);
    for (const issueId of options.excludedConfigScanCandidateIssueIds ?? []) {
        configScanCandidateIssueIds.delete(issueId);
    }
    return {
        openIssues: [...nextIssuesById.values()],
        configScanCandidateIssueIds,
    };
}
function recordRepoSyncCheckpoint(snapshot, mode, checkpointMs, options = {}) {
    snapshot.lastSuccessfulSyncAtMs = checkpointMs;
    if (mode === "full") {
        snapshot.lastFullSyncAtMs = checkpointMs;
    }
    if ("configScanActivityCursor" in options) {
        snapshot.configScanActivityCursor =
            options.configScanActivityCursor ?? null;
    }
}
function recordRepoConfigScanAttempt(snapshot, issue, nowMs) {
    const updatedAtMs = parseIssueUpdatedAt(issue.updatedAt);
    const existing = snapshot.configScanAttemptsByIssueId.get(issue.id);
    const attempts = existing !== undefined && existing.updatedAtMs === updatedAtMs
        ? existing.attempts + 1
        : 1;
    snapshot.configScanAttemptsByIssueId.set(issue.id, {
        updatedAt: normalizeIssueUpdatedAt(issue.updatedAt, updatedAtMs),
        updatedAtMs,
        attempts,
        lastAttemptedAtMs: nowMs,
    });
    snapshot.pendingConfigScanIssueIds.delete(issue.id);
}
function removeRepoSnapshotIssue(snapshot, issueId) {
    snapshot.openIssuesById.delete(issueId);
    pruneConfigScanIssueState(snapshot, issueId);
}
function refreshRepoSnapshotIssue(snapshot, issue) {
    const current = snapshot.openIssuesById.get(issue.id);
    if (isIncomingIssueOlder(current, issue)) {
        return;
    }
    if (!issue.isOpen) {
        removeRepoSnapshotIssue(snapshot, issue.id);
        return;
    }
    snapshot.openIssuesById.set(issue.id, issue);
    if (current === undefined ||
        compareTrackerIssueUpdatedAt(issue, current) !== 0) {
        resetConfigScanAttemptForIssue(snapshot, issue.id);
    }
}
function mergeDeltaRepoSyncResult(snapshot, issues) {
    const nextIssuesById = snapshot.openIssuesById;
    for (const issue of issues) {
        const current = nextIssuesById.get(issue.id);
        if (isIncomingIssueOlder(current, issue)) {
            continue;
        }
        if (issue.isOpen) {
            markConfigScanPendingWhenNewer(snapshot, current, issue);
            nextIssuesById.set(issue.id, selectSnapshotIssueValue(current, issue));
        }
        else {
            nextIssuesById.delete(issue.id);
            pruneConfigScanIssueState(snapshot, issue.id);
        }
    }
    return nextIssuesById;
}
function mergeFullRepoSyncResult(snapshot, issues) {
    const nextIssuesById = new Map();
    const seenIssueIds = new Set();
    for (const issue of issues) {
        seenIssueIds.add(issue.id);
        const current = snapshot.openIssuesById.get(issue.id);
        if (isIncomingIssueOlder(current, issue)) {
            if (current?.isOpen) {
                nextIssuesById.set(current.id, current);
            }
            continue;
        }
        if (issue.isOpen) {
            markConfigScanPendingWhenNewer(snapshot, current, issue);
            nextIssuesById.set(issue.id, selectSnapshotIssueValue(current, issue));
        }
        else {
            pruneConfigScanIssueState(snapshot, issue.id);
        }
    }
    for (const issueId of snapshot.openIssuesById.keys()) {
        if (!seenIssueIds.has(issueId)) {
            pruneConfigScanIssueState(snapshot, issueId);
        }
    }
    return nextIssuesById;
}
function isIncomingIssueOlder(current, issue) {
    return (current !== undefined && compareTrackerIssueUpdatedAt(current, issue) > 0);
}
function selectSnapshotIssueValue(current, issue) {
    if (current !== undefined &&
        compareTrackerIssueUpdatedAt(issue, current) === 0) {
        return current;
    }
    return issue;
}
function markConfigScanPendingWhenNewer(snapshot, current, issue) {
    if (current !== undefined &&
        compareTrackerIssueUpdatedAt(issue, current) <= 0) {
        return;
    }
    resetConfigScanAttemptForIssue(snapshot, issue.id);
    snapshot.pendingConfigScanIssueIds.add(issue.id);
}
function compareTrackerIssueUpdatedAt(left, right) {
    const leftMs = parseIssueUpdatedAt(left.updatedAt);
    const rightMs = parseIssueUpdatedAt(right.updatedAt);
    if (leftMs === rightMs) {
        return 0;
    }
    return leftMs > rightMs ? 1 : -1;
}
function parseIssueUpdatedAt(updatedAt) {
    const parsed = Date.parse(updatedAt);
    return Number.isNaN(parsed) ? Number.NEGATIVE_INFINITY : parsed;
}
function normalizeIssueUpdatedAt(updatedAt, updatedAtMs) {
    return updatedAtMs === Number.NEGATIVE_INFINITY
        ? updatedAt
        : new Date(updatedAtMs).toISOString();
}
function resolveConfigScanCandidateIssueIds(snapshot, nowMs) {
    const candidateIssueIds = new Set();
    for (const issueId of snapshot.pendingConfigScanIssueIds) {
        if (snapshot.openIssuesById.has(issueId)) {
            candidateIssueIds.add(issueId);
        }
    }
    for (const [issueId, attempt] of snapshot.configScanAttemptsByIssueId) {
        if (attempt.attempts >= exports.CONFIG_SCAN_MAX_ATTEMPTS_PER_ISSUE_UPDATE) {
            continue;
        }
        if (nowMs - attempt.lastAttemptedAtMs < exports.CONFIG_SCAN_RETRY_DELAY_MS) {
            continue;
        }
        const issue = snapshot.openIssuesById.get(issueId);
        if (!issue ||
            parseIssueUpdatedAt(issue.updatedAt) !== attempt.updatedAtMs) {
            continue;
        }
        candidateIssueIds.add(issueId);
    }
    return candidateIssueIds;
}
function resetConfigScanAttemptForIssue(snapshot, issueId) {
    snapshot.configScanAttemptsByIssueId.delete(issueId);
}
function pruneConfigScanIssueState(snapshot, issueId) {
    snapshot.pendingConfigScanIssueIds.delete(issueId);
    snapshot.configScanAttemptsByIssueId.delete(issueId);
}
function pruneStaleConfigScanIssueState(snapshot) {
    for (const issueId of snapshot.pendingConfigScanIssueIds) {
        if (!snapshot.openIssuesById.has(issueId)) {
            snapshot.pendingConfigScanIssueIds.delete(issueId);
        }
    }
    for (const [issueId, attempt] of snapshot.configScanAttemptsByIssueId) {
        const issue = snapshot.openIssuesById.get(issueId);
        if (!issue ||
            parseIssueUpdatedAt(issue.updatedAt) !== attempt.updatedAtMs) {
            snapshot.configScanAttemptsByIssueId.delete(issueId);
        }
    }
}
