"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.runServiceLoop = runServiceLoop;
exports.runServiceStatusCommand = runServiceStatusCommand;
const branding_1 = require("../../core/branding");
const package_metadata_1 = require("../../core/package-metadata");
const paths_1 = require("../../core/paths");
const repo_registry_1 = require("../../core/repo-registry");
const service_state_1 = require("../../core/service-state");
const service_drain_state_1 = require("../../core/service-drain-state");
const catalog_1 = require("../../i18n/catalog");
const locale_1 = require("../locale");
const events_1 = require("./events");
const log_rotation_1 = require("./log-rotation");
const repo_1 = require("./repo");
const tracked_project_1 = require("./tracked-project");
const service_repo_state_1 = require("../../core/service-repo-state");
const service_repo_state_storage_1 = require("../../core/service-repo-state-storage");
const shared_1 = require("../service/shared");
const SCHEDULER_TICK_MS = 1_000;
const SERVICE_PACKAGE_UPDATE_DRAIN_LEASE_MS = 24 * 60 * 60 * 1_000;
const SERVICE_LOG_REGISTRY_READ_FAILED = `Failed to read ${branding_1.REPO_REGISTRY_PATH_EXAMPLE}.`;
// biome-ignore lint/complexity: Service loop intentionally keeps registry, cooldown, and scheduling control flow in one place.
async function runServiceLoop(options, deps = {}) {
    const locale = await (0, locale_1.loadCommandLocale)(process.cwd());
    ensureServiceRunAuthorized(options.env ?? deps.env ?? process.env, locale);
    await (0, log_rotation_1.rotateServiceLogIfNeededSafely)(options.homeDirectory);
    const sleep = options.sleep ?? deps.sleep ?? defaultSleep;
    const nextRunAtByRepo = new Map();
    const activeTasksByRepo = new Map();
    const repoStateRegistry = await (0, service_repo_state_storage_1.loadRepoServiceStateRegistry)(options.homeDirectory);
    let iterationCount = 0;
    let shuttingDown = false;
    const startedAt = new Date().toISOString();
    const servicePackageMetadata = await resolveServicePackageMetadata(options.entrypointPath);
    const pidHandle = await (0, service_state_1.registerServiceProcess)({
        ...(options.homeDirectory ? { homeDirectory: options.homeDirectory } : {}),
        pid: process.pid,
        startedAt,
        capabilities: [service_state_1.SERVICE_PROCESS_CAPABILITIES.serviceDrain],
        ...(servicePackageMetadata
            ? {
                packageMetadata: {
                    packageName: servicePackageMetadata.packageName,
                    packageVersion: servicePackageMetadata.packageVersion,
                    entrypointPath: servicePackageMetadata.entrypointPath,
                },
            }
            : {}),
    });
    const drainSignal = {
        isDraining: async () => {
            if ((await resolveActiveServiceDrainState(options.homeDirectory, pidHandle.state)) !== null) {
                return true;
            }
            return ((await createPackageUpdateRestartDrainIfNeeded({
                homeDirectory: options.homeDirectory,
                pidState: pidHandle.state,
            })) !== null);
        },
    };
    const signalHandler = async () => {
        if (shuttingDown) {
            return;
        }
        shuttingDown = true;
        await (0, events_1.logServiceEvent)({
            type: shared_1.SERVICE_EVENT_TYPES.serviceStopping,
            timestamp: new Date().toISOString(),
        });
        await pidHandle.release();
        process.exitCode = 0;
        process.exit();
    };
    process.once("SIGINT", signalHandler);
    process.once("SIGTERM", signalHandler);
    try {
        await (0, events_1.logServiceEvent)({
            type: shared_1.SERVICE_EVENT_TYPES.serviceStarted,
            timestamp: startedAt,
        });
        while (true) {
            await (0, log_rotation_1.rotateServiceLogIfNeededSafely)(options.homeDirectory);
            const drainState = await resolveActiveServiceDrainState(options.homeDirectory, pidHandle.state);
            const pendingDrainState = drainState ??
                (await createPackageUpdateRestartDrainIfNeeded({
                    homeDirectory: options.homeDirectory,
                    pidState: pidHandle.state,
                }));
            if (pendingDrainState !== null) {
                if (await updateServiceDrainAndCompletePackageRestart({
                    state: pendingDrainState,
                    activeTasksByRepo,
                    homeDirectory: options.homeDirectory,
                })) {
                    return 0;
                }
                iterationCount += 1;
                if (options.maxIterations !== undefined &&
                    iterationCount >= options.maxIterations) {
                    await Promise.all([...activeTasksByRepo.values()].map((tasks) => (0, repo_1.awaitActiveRepoTasks)(tasks)));
                    if (await completeLatestDrainAfterActiveTasks({
                        pidState: pidHandle.state,
                        activeTasksByRepo,
                        homeDirectory: options.homeDirectory,
                    })) {
                        return 0;
                    }
                    return 0;
                }
                await sleep(SCHEDULER_TICK_MS);
                continue;
            }
            const registry = await (0, repo_registry_1.loadRepoRegistry)(options.homeDirectory, locale).catch(async (error) => {
                await (0, events_1.logServiceEvent)({
                    type: shared_1.SERVICE_EVENT_TYPES.registryError,
                    timestamp: new Date().toISOString(),
                    reason: error instanceof Error
                        ? error.message
                        : SERVICE_LOG_REGISTRY_READ_FAILED,
                });
                return null;
            });
            if (registry) {
                const knownTrackedProjectKeys = new Set(registry.repos.map((entry) => (0, tracked_project_1.getEntryTrackedProjectKey)(entry)));
                let repoStateChanged = (0, service_repo_state_1.pruneExpiredRepoCooldowns)(repoStateRegistry, Date.now()) > 0;
                for (const trackedProjectKey of nextRunAtByRepo.keys()) {
                    if (!knownTrackedProjectKeys.has(trackedProjectKey)) {
                        nextRunAtByRepo.delete(trackedProjectKey);
                        activeTasksByRepo.delete(trackedProjectKey);
                        repoStateRegistry.snapshotsByTrackedProjectKey.delete(trackedProjectKey);
                        repoStateRegistry.cooldownsByTrackedProjectKey.delete(trackedProjectKey);
                        repoStateChanged = true;
                    }
                }
                if (repoStateChanged) {
                    await (0, service_repo_state_storage_1.persistRepoServiceStateRegistrySafely)(repoStateRegistry, options.homeDirectory);
                }
                const now = Date.now();
                for (const entry of registry.repos) {
                    if (!entry.enabled) {
                        continue;
                    }
                    const trackedProjectKey = (0, tracked_project_1.getEntryTrackedProjectKey)(entry);
                    const activeTasks = activeTasksByRepo.get(trackedProjectKey) ??
                        (0, repo_1.createActiveRepoTaskRegistry)();
                    activeTasksByRepo.set(trackedProjectKey, activeTasks);
                    const nextRunAt = nextRunAtByRepo.get(trackedProjectKey) ?? 0;
                    if (nextRunAt > now) {
                        continue;
                    }
                    const delayUntilNextRun = await (0, repo_1.runScheduledRepoEntry)(entry, activeTasks, locale, options.homeDirectory, repoStateRegistry, { drainSignal });
                    nextRunAtByRepo.set(trackedProjectKey, Date.now() + delayUntilNextRun);
                }
            }
            iterationCount += 1;
            if (options.maxIterations !== undefined &&
                iterationCount >= options.maxIterations) {
                await Promise.all([...activeTasksByRepo.values()].map((tasks) => (0, repo_1.awaitActiveRepoTasks)(tasks)));
                return 0;
            }
            await sleep(SCHEDULER_TICK_MS);
        }
    }
    finally {
        process.off("SIGINT", signalHandler);
        process.off("SIGTERM", signalHandler);
        await pidHandle.release();
    }
}
async function runServiceStatusCommand(homeDirectory) {
    const locale = await (0, locale_1.loadCommandLocale)(process.cwd());
    const liveService = await (0, service_state_1.getLiveServiceState)(homeDirectory);
    const drainState = await (0, service_drain_state_1.readServiceDrainState)(homeDirectory);
    const logPath = (0, paths_1.getServiceLogPath)(homeDirectory);
    const lastResults = await (0, events_1.loadRecentRepoResults)(homeDirectory);
    const registry = await (0, repo_registry_1.loadRepoRegistry)(homeDirectory, locale);
    const repoStateRegistry = await (0, service_repo_state_storage_1.loadRepoServiceStateRegistry)(homeDirectory);
    const repoStatuses = await Promise.all(registry.repos.map(async (entry) => {
        const status = await (0, repo_1.inspectRegisteredRepo)(entry, lastResults.get((0, tracked_project_1.getEntryTrackedProjectKey)(entry)), locale, { validateWorkerRuntime: true });
        const cooldown = (0, service_repo_state_1.getActiveRepoCooldown)(repoStateRegistry, (0, tracked_project_1.getEntryTrackedProjectKey)(entry));
        if (cooldown === null || status.state === shared_1.SERVICE_REPO_STATES.disabled) {
            return status;
        }
        return {
            ...status,
            state: shared_1.SERVICE_REPO_STATES.broken,
            reason: cooldown.reason,
        };
    }));
    const serviceStatus = liveService
        ? shared_1.SERVICE_PROCESS_STATUSES.running
        : shared_1.SERVICE_PROCESS_STATUSES.stopped;
    console.log((0, catalog_1.translate)(locale, "service.command.status.service", {
        value: serviceStatus,
    }));
    console.log((0, catalog_1.translate)(locale, "service.command.status.pid", {
        value: String(liveService?.pid ?? "-"),
    }));
    console.log((0, catalog_1.translate)(locale, "service.command.status.startedAt", {
        value: liveService?.startedAt ?? "-",
    }));
    const activeDrainState = liveService &&
        drainState &&
        (0, service_drain_state_1.isServiceDrainTarget)(drainState, liveService) &&
        (0, service_drain_state_1.isServiceDrainLeaseActive)(drainState)
        ? drainState
        : null;
    const packageUpdate = await resolveServicePackageUpdate(liveService);
    if (packageUpdate !== null) {
        console.log((0, catalog_1.translate)(locale, "service.command.status.update", {
            value: formatServicePackageUpdateStatus(locale, packageUpdate, activeDrainState),
        }));
    }
    if (activeDrainState) {
        console.log((0, catalog_1.translate)(locale, "service.command.status.drain", {
            value: `${activeDrainState.status} ${activeDrainState.action}`,
        }));
    }
    console.log((0, catalog_1.translate)(locale, "service.command.status.logPath", { value: logPath }));
    console.log("");
    console.log((0, catalog_1.translate)(locale, "service.command.status.repos"));
    if (repoStatuses.length === 0) {
        console.log((0, catalog_1.translate)(locale, "service.command.status.none"));
        return 0;
    }
    for (const status of repoStatuses) {
        console.log((0, catalog_1.translate)(locale, "service.command.status.repositoryHost", {
            kind: status.repositoryHost.kind,
            projectRef: status.repositoryHost.projectRef,
        }));
        console.log((0, catalog_1.translate)(locale, "service.command.status.ticketTracker", {
            kind: status.ticketTracker.kind,
            projectRef: status.ticketTracker.projectRef,
        }));
        console.log((0, catalog_1.translate)(locale, "service.command.status.trackedProjectKey", {
            value: status.trackedProjectKey,
        }));
        console.log((0, catalog_1.translate)(locale, "service.command.status.cwd", { value: status.cwd }));
        console.log((0, catalog_1.translate)(locale, "service.command.status.state", {
            value: status.state,
        }));
        console.log((0, catalog_1.translate)(locale, "service.command.status.lastResult", {
            value: status.lastResult ??
                (0, catalog_1.translate)(locale, "service.command.status.notAvailable"),
        }));
        if (status.reason) {
            console.log((0, catalog_1.translate)(locale, "service.command.status.reason", {
                value: status.reason,
            }));
        }
    }
    return 0;
}
function formatServicePackageUpdateStatus(locale, update, activeDrainState) {
    const key = activeDrainState?.action === service_drain_state_1.SERVICE_DRAIN_ACTIONS.disable
        ? "service.command.status.updateAvailable"
        : "service.command.status.updatePendingRestart";
    return (0, catalog_1.translate)(locale, key, {
        fromVersion: update.fromVersion,
        toVersion: update.toVersion,
    });
}
async function resolveActiveServiceDrainState(homeDirectory, pidState) {
    const state = await (0, service_drain_state_1.readServiceDrainState)(homeDirectory);
    if (state === null) {
        return null;
    }
    if (!(0, service_drain_state_1.isServiceDrainTarget)(state, pidState)) {
        if (await (0, service_drain_state_1.removeServiceDrainStateIfCurrent)(homeDirectory, state.requestId)) {
            await (0, events_1.logServiceEvent)({
                type: shared_1.SERVICE_EVENT_TYPES.serviceDrainCancelled,
                timestamp: new Date().toISOString(),
                requestId: state.requestId,
                action: state.action,
            });
        }
        return null;
    }
    if ((0, service_drain_state_1.isServiceDrainLeaseActive)(state)) {
        return state;
    }
    if (await (0, service_drain_state_1.removeServiceDrainStateIfCurrent)(homeDirectory, state.requestId)) {
        await (0, events_1.logServiceEvent)({
            type: shared_1.SERVICE_EVENT_TYPES.serviceDrainCancelled,
            timestamp: new Date().toISOString(),
            requestId: state.requestId,
            action: state.action,
        });
    }
    return null;
}
async function updateServiceDrainProgress(input) {
    const activeTaskCount = countActiveServiceWork(input.activeTasksByRepo);
    const now = Date.now();
    const timestamp = new Date(now).toISOString();
    const nextStatus = activeTaskCount === 0
        ? service_drain_state_1.SERVICE_DRAIN_STATUSES.drained
        : service_drain_state_1.SERVICE_DRAIN_STATUSES.draining;
    const updated = await (0, service_drain_state_1.updateServiceDrainState)(input.homeDirectory, input.state.requestId, (state) => {
        const status = (0, service_drain_state_1.maxServiceDrainStatus)(state.status, nextStatus);
        return {
            ...state,
            status,
            activeTaskCount,
            ...(state.reason === service_drain_state_1.SERVICE_DRAIN_REASONS.packageUpdate
                ? {
                    leaseExpiresAt: new Date(now + SERVICE_PACKAGE_UPDATE_DRAIN_LEASE_MS).toISOString(),
                }
                : {}),
            observedAt: state.observedAt ?? timestamp,
            ...(status === service_drain_state_1.SERVICE_DRAIN_STATUSES.drained
                ? { drainedAt: state.drainedAt ?? timestamp }
                : {}),
        };
    });
    if (updated === null) {
        return null;
    }
    if (updated.previous.status === service_drain_state_1.SERVICE_DRAIN_STATUSES.requested) {
        await (0, events_1.logServiceEvent)({
            type: shared_1.SERVICE_EVENT_TYPES.serviceDrainStarted,
            timestamp,
            requestId: input.state.requestId,
            action: input.state.action,
            activeTaskCount,
        });
    }
    if (updated.current.status === service_drain_state_1.SERVICE_DRAIN_STATUSES.drained &&
        updated.previous.status !== service_drain_state_1.SERVICE_DRAIN_STATUSES.drained) {
        await (0, events_1.logServiceEvent)({
            type: shared_1.SERVICE_EVENT_TYPES.serviceDrainCompleted,
            timestamp,
            requestId: input.state.requestId,
            action: input.state.action,
            activeTaskCount,
        });
    }
    return updated.current;
}
async function updateServiceDrainAndCompletePackageRestart(input) {
    const updatedDrainState = await updateServiceDrainProgress(input);
    return completePackageUpdateRestartIfDrained(updatedDrainState ?? input.state, input.homeDirectory);
}
async function completeLatestDrainAfterActiveTasks(input) {
    const latestDrainState = await resolveActiveServiceDrainState(input.homeDirectory, input.pidState);
    if (latestDrainState === null) {
        return false;
    }
    return updateServiceDrainAndCompletePackageRestart({
        state: latestDrainState,
        activeTasksByRepo: input.activeTasksByRepo,
        homeDirectory: input.homeDirectory,
    });
}
async function createPackageUpdateRestartDrainIfNeeded(input) {
    const update = await resolveServicePackageUpdate(input.pidState);
    if (update === null) {
        return null;
    }
    const drainState = (0, service_drain_state_1.createServiceDrainState)({
        action: service_drain_state_1.SERVICE_DRAIN_ACTIONS.restart,
        targetPid: input.pidState.pid,
        targetStartedAt: input.pidState.startedAt,
        leaseMs: SERVICE_PACKAGE_UPDATE_DRAIN_LEASE_MS,
        reason: service_drain_state_1.SERVICE_DRAIN_REASONS.packageUpdate,
        fromVersion: update.fromVersion,
        toVersion: update.toVersion,
    });
    if (!(await (0, service_drain_state_1.createServiceDrainStateIfAbsent)(drainState, input.homeDirectory))) {
        return null;
    }
    await (0, events_1.logServiceEvent)({
        type: shared_1.SERVICE_EVENT_TYPES.serviceUpdateDetected,
        timestamp: new Date().toISOString(),
        requestId: drainState.requestId,
        packageName: update.packageName,
        fromVersion: update.fromVersion,
        toVersion: update.toVersion,
    });
    return drainState;
}
async function completePackageUpdateRestartIfDrained(state, homeDirectory) {
    if (state.action !== service_drain_state_1.SERVICE_DRAIN_ACTIONS.restart ||
        state.reason !== service_drain_state_1.SERVICE_DRAIN_REASONS.packageUpdate ||
        state.status !== service_drain_state_1.SERVICE_DRAIN_STATUSES.drained) {
        return false;
    }
    if (!(await (0, service_drain_state_1.removeServiceDrainStateIfCurrent)(homeDirectory, state.requestId))) {
        return false;
    }
    await (0, events_1.logServiceEvent)({
        type: shared_1.SERVICE_EVENT_TYPES.serviceStopping,
        timestamp: new Date().toISOString(),
        reason: service_drain_state_1.SERVICE_DRAIN_REASONS.packageUpdate,
        requestId: state.requestId,
    });
    return true;
}
async function resolveServicePackageUpdate(pidState) {
    if (pidState?.packageName === undefined ||
        pidState.packageVersion === undefined ||
        pidState.entrypointPath === undefined) {
        return null;
    }
    const currentPackageMetadata = await (0, package_metadata_1.readPackageMetadataForEntrypoint)(pidState.entrypointPath);
    if (currentPackageMetadata === null ||
        currentPackageMetadata.packageName !== pidState.packageName ||
        currentPackageMetadata.packageVersion === pidState.packageVersion) {
        return null;
    }
    return {
        packageName: pidState.packageName,
        fromVersion: pidState.packageVersion,
        toVersion: currentPackageMetadata.packageVersion,
    };
}
async function resolveServicePackageMetadata(entrypointPath) {
    const resolvedEntrypointPath = entrypointPath ?? (0, package_metadata_1.resolveCurrentEntrypointPath)();
    return resolvedEntrypointPath
        ? (0, package_metadata_1.readPackageMetadataForEntrypoint)(resolvedEntrypointPath)
        : null;
}
function countActiveServiceWork(activeTasksByRepo) {
    return [...activeTasksByRepo.values()].reduce((count, registry) => count + (0, repo_1.countActiveRepoWork)(registry), 0);
}
function ensureServiceRunAuthorized(env, locale = "en") {
    if (env[branding_1.INTERNAL_SERVICE_ENV_NAME] !== "1") {
        throw new Error((0, catalog_1.translate)(locale, "service.command.run.requiresServiceDefinition"));
    }
}
async function defaultSleep(milliseconds) {
    await new Promise((resolve) => setTimeout(resolve, milliseconds));
}
