"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SERVICE_DRAIN_REASONS = exports.SERVICE_DRAIN_STATUSES = exports.SERVICE_DRAIN_ACTIONS = void 0;
exports.createServiceDrainState = createServiceDrainState;
exports.readServiceDrainState = readServiceDrainState;
exports.writeServiceDrainState = writeServiceDrainState;
exports.createServiceDrainStateIfAbsent = createServiceDrainStateIfAbsent;
exports.updateServiceDrainState = updateServiceDrainState;
exports.removeServiceDrainStateIfCurrent = removeServiceDrainStateIfCurrent;
exports.isServiceDrainLeaseActive = isServiceDrainLeaseActive;
exports.isServiceDrainTarget = isServiceDrainTarget;
exports.maxServiceDrainStatus = maxServiceDrainStatus;
const promises_1 = require("node:fs/promises");
const node_path_1 = __importDefault(require("node:path"));
const zod_1 = require("zod");
const file_readers_1 = require("./file-readers");
const fs_errors_1 = require("./fs-errors");
const paths_1 = require("./paths");
exports.SERVICE_DRAIN_ACTIONS = {
    disable: "disable",
    restart: "restart",
};
exports.SERVICE_DRAIN_STATUSES = {
    requested: "requested",
    draining: "draining",
    drained: "drained",
};
exports.SERVICE_DRAIN_REASONS = {
    packageUpdate: "package_update",
};
const serviceDrainActionSchema = zod_1.z.enum([
    exports.SERVICE_DRAIN_ACTIONS.disable,
    exports.SERVICE_DRAIN_ACTIONS.restart,
]);
const serviceDrainStatusSchema = zod_1.z.enum([
    exports.SERVICE_DRAIN_STATUSES.requested,
    exports.SERVICE_DRAIN_STATUSES.draining,
    exports.SERVICE_DRAIN_STATUSES.drained,
]);
const serviceDrainReasonSchema = zod_1.z.enum([exports.SERVICE_DRAIN_REASONS.packageUpdate]);
const serviceDrainStateSchema = zod_1.z.object({
    version: zod_1.z.literal(1),
    requestId: zod_1.z.string().min(1),
    action: serviceDrainActionSchema,
    targetPid: zod_1.z.number().int().positive(),
    targetStartedAt: zod_1.z.string().min(1),
    requestedAt: zod_1.z.string().min(1),
    leaseExpiresAt: zod_1.z.string().min(1),
    status: serviceDrainStatusSchema,
    activeTaskCount: zod_1.z.number().int().nonnegative(),
    reason: serviceDrainReasonSchema.optional(),
    fromVersion: zod_1.z.string().min(1).optional(),
    toVersion: zod_1.z.string().min(1).optional(),
    observedAt: zod_1.z.string().min(1).optional(),
    drainedAt: zod_1.z.string().min(1).optional(),
});
const SERVICE_DRAIN_STATUS_ORDER = {
    [exports.SERVICE_DRAIN_STATUSES.requested]: 0,
    [exports.SERVICE_DRAIN_STATUSES.draining]: 1,
    [exports.SERVICE_DRAIN_STATUSES.drained]: 2,
};
const SERVICE_DRAIN_LOCK_POLL_MS = 10;
const SERVICE_DRAIN_LOCK_TIMEOUT_MS = 5_000;
function createServiceDrainState(input) {
    const now = input.now ?? new Date();
    const leaseMs = input.leaseMs ?? 30_000;
    return {
        version: 1,
        requestId: `${now.getTime()}-${Math.random().toString(36).slice(2)}`,
        action: input.action,
        targetPid: input.targetPid,
        targetStartedAt: input.targetStartedAt,
        requestedAt: now.toISOString(),
        leaseExpiresAt: new Date(now.getTime() + leaseMs).toISOString(),
        status: exports.SERVICE_DRAIN_STATUSES.requested,
        activeTaskCount: 0,
        ...(input.reason ? { reason: input.reason } : {}),
        ...(input.fromVersion ? { fromVersion: input.fromVersion } : {}),
        ...(input.toVersion ? { toVersion: input.toVersion } : {}),
    };
}
async function readServiceDrainState(homeDirectory) {
    return readServiceDrainStateUnlocked(homeDirectory);
}
async function readServiceDrainStateUnlocked(homeDirectory) {
    return (0, file_readers_1.readJsonFileOrNull)((0, paths_1.getServiceDrainStatePath)(homeDirectory), serviceDrainStateSchema);
}
async function writeServiceDrainState(state, homeDirectory) {
    await withServiceDrainStateLock(homeDirectory, () => writeServiceDrainStateUnlocked(state, homeDirectory));
}
async function createServiceDrainStateIfAbsent(state, homeDirectory) {
    return withServiceDrainStateLock(homeDirectory, async () => {
        if ((await readServiceDrainStateUnlocked(homeDirectory)) !== null) {
            return false;
        }
        await writeServiceDrainStateUnlocked(state, homeDirectory);
        return true;
    });
}
async function writeServiceDrainStateUnlocked(state, homeDirectory) {
    const statePath = (0, paths_1.getServiceDrainStatePath)(homeDirectory);
    await (0, promises_1.mkdir)(node_path_1.default.dirname(statePath), { recursive: true });
    const normalized = serviceDrainStateSchema.parse(state);
    const tempPath = `${statePath}.${process.pid}.${Date.now()}.tmp`;
    try {
        await (0, promises_1.writeFile)(tempPath, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
        await (0, promises_1.rename)(tempPath, statePath);
    }
    catch (error) {
        await (0, promises_1.rm)(tempPath, { force: true });
        throw error;
    }
}
async function updateServiceDrainState(homeDirectory, requestId, update) {
    return withServiceDrainStateLock(homeDirectory, async () => {
        const previous = await readServiceDrainStateUnlocked(homeDirectory);
        if (previous === null || previous.requestId !== requestId) {
            return null;
        }
        const current = update(previous);
        await writeServiceDrainStateUnlocked(current, homeDirectory);
        return { previous, current };
    });
}
async function removeServiceDrainStateIfCurrent(homeDirectory, requestId) {
    return withServiceDrainStateLock(homeDirectory, async () => {
        const current = await readServiceDrainStateUnlocked(homeDirectory);
        if (current?.requestId !== requestId) {
            return false;
        }
        await (0, promises_1.rm)((0, paths_1.getServiceDrainStatePath)(homeDirectory), { force: true });
        return true;
    });
}
function isServiceDrainLeaseActive(state, now = new Date()) {
    return Date.parse(state.leaseExpiresAt) > now.getTime();
}
function isServiceDrainTarget(state, target) {
    return (state.targetPid === target.pid && state.targetStartedAt === target.startedAt);
}
function maxServiceDrainStatus(left, right) {
    return SERVICE_DRAIN_STATUS_ORDER[left] >= SERVICE_DRAIN_STATUS_ORDER[right]
        ? left
        : right;
}
async function withServiceDrainStateLock(homeDirectory, callback) {
    const statePath = (0, paths_1.getServiceDrainStatePath)(homeDirectory);
    const lockPath = `${statePath}.lock`;
    await (0, promises_1.mkdir)(node_path_1.default.dirname(statePath), { recursive: true });
    const deadline = Date.now() + SERVICE_DRAIN_LOCK_TIMEOUT_MS;
    while (true) {
        try {
            const lockFile = await (0, promises_1.open)(lockPath, "wx");
            try {
                await lockFile.writeFile(`${process.pid}\n`, "utf8");
                return await callback();
            }
            finally {
                await lockFile.close();
                await (0, promises_1.rm)(lockPath, { force: true });
            }
        }
        catch (error) {
            if (!(0, fs_errors_1.isAlreadyExistsError)(error) || Date.now() >= deadline) {
                throw error;
            }
            await sleep(SERVICE_DRAIN_LOCK_POLL_MS);
        }
    }
}
async function sleep(milliseconds) {
    await new Promise((resolve) => setTimeout(resolve, milliseconds));
}
