"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.REMINDER_STATE_SCHEMA_VERSION = exports.REMINDER_STATE_FILE_NAME = exports.DAY_SHIFT_REMINDER_SEED_ENV = void 0;
exports.parseReminderStateContents = parseReminderStateContents;
exports.createUnreadableReminderStateDiagnostic = createUnreadableReminderStateDiagnostic;
exports.parseReminderStateValue = parseReminderStateValue;
exports.createMissingReminderState = createMissingReminderState;
exports.createReminderStateInitializationOptions = createReminderStateInitializationOptions;
exports.serializeReminderState = serializeReminderState;
exports.resolveReminderStatePath = resolveReminderStatePath;
exports.readReminderState = readReminderState;
exports.writeReminderState = writeReminderState;
exports.utcTimestampValue = utcTimestampValue;
exports.timestampMilliseconds = timestampMilliseconds;
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const node_crypto_1 = require("node:crypto");
const path_resolution_1 = require("../runtime/path-resolution");
const reminder_config_1 = require("./reminder-config");
Object.defineProperty(exports, "DAY_SHIFT_REMINDER_SEED_ENV", { enumerable: true, get: function () { return reminder_config_1.DAY_SHIFT_REMINDER_SEED_ENV; } });
Object.defineProperty(exports, "REMINDER_STATE_FILE_NAME", { enumerable: true, get: function () { return reminder_config_1.REMINDER_STATE_FILE_NAME; } });
Object.defineProperty(exports, "REMINDER_STATE_SCHEMA_VERSION", { enumerable: true, get: function () { return reminder_config_1.REMINDER_STATE_SCHEMA_VERSION; } });
const REQUIRED_REMINDER_STATE_FIELDS = [
    'reminder_state_schema_version',
    'first_eligible_command_at',
    'eligible_command_count',
    'last_reminder_at',
    'eligible_commands_since_last_reminder',
    'prng_seed',
    'last_invalid_license_warning_at',
];
const UTC_RFC3339_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?Z$/;
const BASE64URL_SEED_PATTERN = /^[A-Za-z0-9_-]+$/;
function parseReminderStateContents(contents) {
    let decoded;
    try {
        decoded = JSON.parse(contents);
    }
    catch {
        return invalid('malformed_json');
    }
    return parseReminderStateValue(decoded);
}
function createUnreadableReminderStateDiagnostic() {
    return invalid('unreadable_state');
}
function parseReminderStateValue(value) {
    if (!isRecord(value)) {
        return invalid('malformed_state');
    }
    const missingField = REQUIRED_REMINDER_STATE_FIELDS.find((field) => !Object.hasOwn(value, field));
    if (missingField !== undefined) {
        return invalid('missing_required_field', missingField);
    }
    if (value.reminder_state_schema_version !== reminder_config_1.REMINDER_STATE_SCHEMA_VERSION) {
        return invalid('unsupported_schema_version', 'reminder_state_schema_version');
    }
    const firstEligibleCommandAt = utcTimestampValue(value.first_eligible_command_at);
    if (firstEligibleCommandAt === undefined) {
        return invalid('invalid_timestamp', 'first_eligible_command_at');
    }
    const eligibleCommandCount = nonNegativeIntegerValue(value.eligible_command_count);
    if (eligibleCommandCount === undefined) {
        return invalid('invalid_counter', 'eligible_command_count');
    }
    const lastReminderAt = nullableUtcTimestampValue(value.last_reminder_at);
    if (lastReminderAt === undefined) {
        return invalid('invalid_nullable_timestamp', 'last_reminder_at');
    }
    const eligibleCommandsSinceLastReminder = nonNegativeIntegerValue(value.eligible_commands_since_last_reminder);
    if (eligibleCommandsSinceLastReminder === undefined) {
        return invalid('invalid_counter', 'eligible_commands_since_last_reminder');
    }
    const prngSeed = prngSeedValue(value.prng_seed);
    if (prngSeed === undefined) {
        return invalid('invalid_prng_seed', 'prng_seed');
    }
    const lastInvalidLicenseWarningAt = nullableUtcTimestampValue(value.last_invalid_license_warning_at);
    if (lastInvalidLicenseWarningAt === undefined) {
        return invalid('invalid_nullable_timestamp', 'last_invalid_license_warning_at');
    }
    return {
        ok: true,
        state: {
            reminderStateSchemaVersion: reminder_config_1.REMINDER_STATE_SCHEMA_VERSION,
            firstEligibleCommandAt,
            eligibleCommandCount,
            lastReminderAt,
            eligibleCommandsSinceLastReminder,
            prngSeed,
            lastInvalidLicenseWarningAt,
        },
    };
}
function createMissingReminderState(options) {
    const firstEligibleCommandAt = utcTimestampValue(options.firstEligibleCommandAt);
    if (firstEligibleCommandAt === undefined) {
        throw new TypeError('firstEligibleCommandAt must be a UTC RFC 3339 timestamp');
    }
    const prngSeed = prngSeedValue(options.prngSeed);
    if (prngSeed === undefined) {
        throw new TypeError('prngSeed must be non-empty base64url seed material');
    }
    return {
        reminderStateSchemaVersion: reminder_config_1.REMINDER_STATE_SCHEMA_VERSION,
        firstEligibleCommandAt,
        eligibleCommandCount: 0,
        lastReminderAt: null,
        eligibleCommandsSinceLastReminder: 0,
        prngSeed,
        lastInvalidLicenseWarningAt: null,
    };
}
function createReminderStateInitializationOptions(options) {
    const explicitSeed = normalizeOptionalSeed(options.prngSeed);
    if (explicitSeed !== undefined) {
        assertValidPrngSeed(explicitSeed);
        return {
            firstEligibleCommandAt: options.firstEligibleCommandAt,
            prngSeed: explicitSeed,
            prngSeedSource: 'test-option',
        };
    }
    const environmentSeed = options.allowTestSeedFromEnvironment === true
        ? normalizeOptionalSeed((options.env ?? process.env)[reminder_config_1.DAY_SHIFT_REMINDER_SEED_ENV])
        : undefined;
    if (environmentSeed !== undefined) {
        assertValidPrngSeed(environmentSeed);
        return {
            firstEligibleCommandAt: options.firstEligibleCommandAt,
            prngSeed: environmentSeed,
            prngSeedSource: 'test-environment',
        };
    }
    const seedBytes = options.randomSeedBytes?.() ?? (0, node_crypto_1.randomBytes)(32);
    return {
        firstEligibleCommandAt: options.firstEligibleCommandAt,
        prngSeed: Buffer.from(seedBytes).toString('base64url'),
        prngSeedSource: 'local-generated',
    };
}
function serializeReminderState(state) {
    return {
        reminder_state_schema_version: reminder_config_1.REMINDER_STATE_SCHEMA_VERSION,
        first_eligible_command_at: state.firstEligibleCommandAt,
        eligible_command_count: state.eligibleCommandCount,
        last_reminder_at: state.lastReminderAt,
        eligible_commands_since_last_reminder: state.eligibleCommandsSinceLastReminder,
        prng_seed: state.prngSeed,
        last_invalid_license_warning_at: state.lastInvalidLicenseWarningAt,
    };
}
function resolveReminderStatePath(options = {}) {
    const resolvedDirectory = (0, path_resolution_1.resolveDayShiftUserStateDirectory)(options);
    const statePath = node_path_1.default.join(resolvedDirectory.stateDirectory, reminder_config_1.REMINDER_STATE_FILE_NAME);
    return {
        stateDirectory: resolvedDirectory.stateDirectory,
        stateDirectorySource: resolvedDirectory.source,
        statePath,
        isInsideRepository: options.repositoryRoot === undefined ? null : isPathInsideDirectory(statePath, options.repositoryRoot),
    };
}
function readReminderState(options) {
    const resolvedPath = resolveReminderStatePath(options);
    let contents;
    try {
        contents = node_fs_1.default.readFileSync(resolvedPath.statePath, 'utf8');
    }
    catch (error) {
        if (isMissingFileError(error)) {
            return recoveredReadResult('initialized', resolvedPath, {
                reason: 'missing_state',
            }, options.newState);
        }
        return recoveredReadResult('recovered', resolvedPath, {
            reason: 'unreadable_state',
        }, options.newState);
    }
    const serializedSchemaVersion = readSerializedSchemaVersion(contents);
    const parsed = parseReminderStateContents(contents);
    if (!parsed.ok) {
        return recoveredReadResult('recovered', resolvedPath, parsed.diagnostic, options.newState, serializedSchemaVersion);
    }
    return {
        status: 'loaded',
        state: parsed.state,
        statePath: resolvedPath.statePath,
        stateDirectory: resolvedPath.stateDirectory,
        stateDirectorySource: resolvedPath.stateDirectorySource,
        diagnostic: null,
        loadDiagnostic: createLoadDiagnostic({
            status: 'loaded',
            resolvedPath,
            state: parsed.state,
            diagnostic: null,
            schemaVersion: parsed.state.reminderStateSchemaVersion,
            seedSource: 'stored-state',
        }),
    };
}
function writeReminderState(state, options = {}) {
    const resolvedPath = resolveReminderStatePath(options);
    writeFileAtomically(resolvedPath.statePath, `${JSON.stringify(serializeReminderState(state), null, 2)}\n`);
    return resolvedPath;
}
function invalid(reason, field) {
    return {
        ok: false,
        diagnostic: {
            reason,
            ...(field === undefined ? {} : { field }),
        },
    };
}
function utcTimestampValue(value) {
    if (typeof value !== 'string' || !isUtcRfc3339Timestamp(value)) {
        return undefined;
    }
    return value;
}
function nullableUtcTimestampValue(value) {
    if (value === null) {
        return null;
    }
    return utcTimestampValue(value);
}
function nonNegativeIntegerValue(value) {
    return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined;
}
function prngSeedValue(value) {
    return typeof value === 'string' && BASE64URL_SEED_PATTERN.test(value) ? value : undefined;
}
function assertValidPrngSeed(value) {
    if (prngSeedValue(value) === undefined) {
        throw new TypeError('prngSeed must be non-empty base64url seed material');
    }
}
function normalizeOptionalSeed(value) {
    if (value === undefined || value.trim() === '') {
        return undefined;
    }
    return value;
}
function timestampMilliseconds(value) {
    return new Date(value).getTime();
}
function isUtcRfc3339Timestamp(value) {
    const match = UTC_RFC3339_PATTERN.exec(value);
    if (match === null) {
        return false;
    }
    const [, yearValue, monthValue, dayValue, hourValue, minuteValue, secondValue] = match;
    const year = Number(yearValue);
    const month = Number(monthValue);
    const day = Number(dayValue);
    const hour = Number(hourValue);
    const minute = Number(minuteValue);
    const second = Number(secondValue);
    if (month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59) {
        return false;
    }
    const candidate = new Date(Date.UTC(year, month - 1, day, hour, minute, second));
    return (candidate.getUTCFullYear() === year &&
        candidate.getUTCMonth() === month - 1 &&
        candidate.getUTCDate() === day &&
        candidate.getUTCHours() === hour &&
        candidate.getUTCMinutes() === minute &&
        candidate.getUTCSeconds() === second);
}
function recoveredReadResult(status, resolvedPath, diagnostic, newStateOptions, serializedSchemaVersion = null) {
    const state = createMissingReminderState(newStateOptions);
    return {
        status,
        state,
        statePath: resolvedPath.statePath,
        stateDirectory: resolvedPath.stateDirectory,
        stateDirectorySource: resolvedPath.stateDirectorySource,
        diagnostic,
        loadDiagnostic: createLoadDiagnostic({
            status: diagnosticStatus(status, diagnostic.reason),
            resolvedPath,
            state,
            diagnostic,
            schemaVersion: diagnostic.reason === 'unsupported_schema_version' ? serializedSchemaVersion : null,
            seedSource: newStateOptions.prngSeedSource ?? 'local-generated',
        }),
    };
}
function readSerializedSchemaVersion(contents) {
    try {
        const parsed = JSON.parse(contents);
        if (isRecord(parsed) && typeof parsed.reminder_state_schema_version === 'number') {
            return parsed.reminder_state_schema_version;
        }
    }
    catch {
        return null;
    }
    return null;
}
function createLoadDiagnostic(options) {
    return {
        status: options.status,
        state_path: options.resolvedPath.statePath,
        state_directory_source: options.resolvedPath.stateDirectorySource,
        schema_version: options.schemaVersion,
        recovery_reason: options.diagnostic?.reason ?? null,
        ...(options.diagnostic?.field === undefined ? {} : { recovery_field: options.diagnostic.field }),
        first_eligible_command_at: options.state.firstEligibleCommandAt,
        eligible_command_count: options.state.eligibleCommandCount,
        last_reminder_at: options.state.lastReminderAt,
        eligible_commands_since_last_reminder: options.state.eligibleCommandsSinceLastReminder,
        last_invalid_license_warning_at: options.state.lastInvalidLicenseWarningAt,
        seed_source: options.seedSource,
    };
}
function diagnosticStatus(readStatus, reason) {
    if (readStatus === 'initialized') {
        return 'initialized-missing';
    }
    if (reason === 'unreadable_state') {
        return 'recovered-unreadable';
    }
    if (reason === 'unsupported_schema_version') {
        return 'recovered-unsupported-version';
    }
    return 'recovered-malformed';
}
function writeFileAtomically(filePath, contents) {
    const directoryPath = node_path_1.default.dirname(filePath);
    node_fs_1.default.mkdirSync(directoryPath, { recursive: true });
    const tempFilePath = node_path_1.default.join(directoryPath, `.${node_path_1.default.basename(filePath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
    let fileDescriptor = null;
    try {
        fileDescriptor = node_fs_1.default.openSync(tempFilePath, 'wx', 0o600);
        node_fs_1.default.writeFileSync(fileDescriptor, contents, 'utf8');
        bestEffortFsync(fileDescriptor);
        node_fs_1.default.closeSync(fileDescriptor);
        fileDescriptor = null;
        node_fs_1.default.renameSync(tempFilePath, filePath);
        bestEffortDirectoryFsync(directoryPath);
    }
    catch (error) {
        if (fileDescriptor !== null) {
            try {
                node_fs_1.default.closeSync(fileDescriptor);
            }
            catch {
                // Ignore close failures while preserving the original write failure.
            }
        }
        try {
            node_fs_1.default.rmSync(tempFilePath, { force: true });
        }
        catch {
            // Temporary-file cleanup is best effort; the active state path is never the temp file.
        }
        throw error;
    }
}
function bestEffortFsync(fileDescriptor) {
    try {
        node_fs_1.default.fsyncSync(fileDescriptor);
    }
    catch {
        // Some filesystems do not support fsync for this target; writes still use rename replacement.
    }
}
function bestEffortDirectoryFsync(directoryPath) {
    let directoryDescriptor = null;
    try {
        directoryDescriptor = node_fs_1.default.openSync(directoryPath, 'r');
        node_fs_1.default.fsyncSync(directoryDescriptor);
    }
    catch {
        // Directory fsync is not portable across all Node platforms and filesystems.
    }
    finally {
        if (directoryDescriptor !== null) {
            try {
                node_fs_1.default.closeSync(directoryDescriptor);
            }
            catch {
                // Directory sync cleanup is best effort.
            }
        }
    }
}
function isMissingFileError(error) {
    return isNodeError(error) && (error.code === 'ENOENT' || error.code === 'ENOTDIR');
}
function isNodeError(error) {
    return error instanceof Error && 'code' in error;
}
function isPathInsideDirectory(candidatePath, directoryPath) {
    const relativePath = node_path_1.default.relative(node_path_1.default.resolve(directoryPath), node_path_1.default.resolve(candidatePath));
    return (relativePath === '' ||
        (relativePath !== '..' && !relativePath.startsWith(`..${node_path_1.default.sep}`) && !node_path_1.default.isAbsolute(relativePath)));
}
function isRecord(value) {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
}
