import { PACKAGE_VERSION, init_package_version } from "../package-version.js";
import { init_write_file_atomic, writeTextAtomic } from "./write-file-atomic.js";
import { createLogger } from "../utils/logger/index.js";
import { init_logger } from "../utils/logger.js";
import { init_paths_state, resolveUpdateCheckStatePath } from "../config/paths-state.js";
import { acquireUpdateLock } from "./update-lock.js";
import { normalizeUpdateChannel } from "./update-channels.js";
import { compareSemver, detectInstallKind, resolveNpmChannelTag, resolvePackageRoot } from "./update-check.js";
import { createHash, randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
//#region src/infra/update-startup.ts
init_write_file_atomic();
init_paths_state();
init_package_version();
init_logger();
const log = createLogger("UpdateCheck");
const CHECK_INTERVAL_MS = 1440 * 60 * 1e3;
const ONE_HOUR_MS = 3600 * 1e3;
let updateAvailableCache = null;
/** Get the cached update-available state (populated after startup check). */
function getUpdateAvailable() {
	return updateAvailableCache;
}
async function readState(statePath) {
	try {
		const raw = await readFile(statePath, "utf-8");
		const parsed = JSON.parse(raw);
		if (!parsed || typeof parsed !== "object") {
			log.warn({ statePath }, "Update check state file contains non-object; resetting");
			return {};
		}
		return parsed;
	} catch (err) {
		if (err.code !== "ENOENT") log.warn({
			err,
			statePath
		}, "Failed to read update check state; resetting");
		return {};
	}
}
async function writeState(statePath, state) {
	await writeTextAtomic(statePath, JSON.stringify(state, null, 2));
}
function resolveCheckIntervalMs(config) {
	const auto = config.update?.auto;
	if (!auto?.enabled) return CHECK_INTERVAL_MS;
	if ((normalizeUpdateChannel(config.update?.channel) ?? "stable") === "beta") {
		const hours = auto.betaCheckIntervalHours ?? 1;
		return Math.max(ONE_HOUR_MS / 4, Math.floor(hours * ONE_HOUR_MS));
	}
	return ONE_HOUR_MS;
}
/**
* Compute a deterministic delay for stable auto-update rollout,
* based on a per-installation hash to spread updates over time.
*/
function resolveStableJitterMs(installId, version, tag, jitterWindowMs) {
	if (jitterWindowMs <= 0) return 0;
	return createHash("sha256").update(`${installId}:${version}:${tag}`).digest().readUInt32BE(0) % (Math.floor(jitterWindowMs) + 1);
}
/**
* Main startup update check. Called once when the gateway starts (or on demand).
*/
async function runGatewayUpdateCheck(params) {
	const { config, force } = params;
	const autoEnabled = config.update?.auto?.enabled ?? false;
	const shouldCheckHints = config.update?.checkOnStart !== false;
	if (!force && !shouldCheckHints && !autoEnabled) return;
	const statePath = resolveUpdateCheckStatePath();
	const state = await readState(statePath);
	const now = Date.now();
	const lastCheckedAt = state.lastCheckedAt ? Date.parse(state.lastCheckedAt) : null;
	if (state.lastAvailableVersion && (shouldCheckHints || force)) {
		const comparison = compareSemver(PACKAGE_VERSION, state.lastAvailableVersion);
		if (comparison !== null && comparison < 0) {
			const cached = {
				currentVersion: PACKAGE_VERSION,
				latestVersion: state.lastAvailableVersion,
				channel: state.lastAvailableTag ?? "latest"
			};
			updateAvailableCache = cached;
			params.onUpdateAvailableChange?.(cached);
		}
	}
	const checkIntervalMs = resolveCheckIntervalMs(config);
	const shouldBypassThrottleForVersion = state.lastCheckPackageVersion === void 0 || state.lastCheckPackageVersion !== PACKAGE_VERSION;
	if (!force && !shouldBypassThrottleForVersion && lastCheckedAt && Number.isFinite(lastCheckedAt) && now - lastCheckedAt < checkIntervalMs) return;
	const root = await resolvePackageRoot();
	let installKind = "unknown";
	if (root) {
		installKind = await detectInstallKind(root);
		if (installKind === "git") log.info("Update check: git checkout (hint-only; use git pull to update, no auto npm install)");
	}
	const channel = normalizeUpdateChannel(config.update?.channel) ?? "stable";
	const resolved = await resolveNpmChannelTag({
		channel,
		timeoutMs: 2500
	});
	const nextState = {
		...state,
		lastCheckedAt: new Date(now).toISOString()
	};
	if (!resolved.version) {
		nextState.lastCheckPackageVersion = PACKAGE_VERSION;
		await writeState(statePath, nextState);
		return;
	}
	const comparison = compareSemver(PACKAGE_VERSION, resolved.version);
	if (comparison !== null && comparison < 0) {
		const updateInfo = {
			currentVersion: PACKAGE_VERSION,
			latestVersion: resolved.version,
			channel: resolved.tag
		};
		if (shouldCheckHints || force) {
			updateAvailableCache = updateInfo;
			params.onUpdateAvailableChange?.(updateInfo);
		}
		nextState.lastAvailableVersion = resolved.version;
		nextState.lastAvailableTag = resolved.tag;
		const shouldNotify = state.lastNotifiedVersion !== resolved.version || state.lastNotifiedTag !== resolved.tag;
		if ((shouldCheckHints || force) && shouldNotify) {
			log.info({
				currentVersion: PACKAGE_VERSION,
				latestVersion: resolved.version,
				tag: resolved.tag
			}, `Update available (${resolved.tag}): v${resolved.version} (current v${PACKAGE_VERSION}). Run: xopc update`);
			nextState.lastNotifiedVersion = resolved.version;
			nextState.lastNotifiedTag = resolved.tag;
		}
		if (autoEnabled && (channel === "stable" || channel === "beta") && installKind !== "git") await handleAutoUpdate({
			channel,
			version: resolved.version,
			tag: resolved.tag,
			state,
			nextState,
			now,
			root,
			config
		});
	} else {
		delete nextState.lastAvailableVersion;
		delete nextState.lastAvailableTag;
		updateAvailableCache = null;
		params.onUpdateAvailableChange?.(null);
	}
	nextState.lastCheckPackageVersion = PACKAGE_VERSION;
	await writeState(statePath, nextState);
}
async function handleAutoUpdate(params) {
	const { channel, version, tag, state, nextState, now, root, config } = params;
	const auto = config.update?.auto;
	if (!auto) return;
	const stableDelayHours = auto.stableDelayHours ?? 6;
	const stableJitterHours = auto.stableJitterHours ?? 12;
	const betaCheckIntervalHours = auto.betaCheckIntervalHours ?? 1;
	const attemptIntervalMs = channel === "beta" ? Math.max(ONE_HOUR_MS / 4, Math.floor(betaCheckIntervalHours * ONE_HOUR_MS)) : ONE_HOUR_MS;
	const lastAttemptAt = state.autoLastAttemptAt ? Date.parse(state.autoLastAttemptAt) : null;
	if (state.autoLastAttemptVersion === version && lastAttemptAt !== null && Number.isFinite(lastAttemptAt) && now - lastAttemptAt < attemptIntervalMs) {
		log.info({
			version,
			tag
		}, "Auto-update deferred: recent attempt exists");
		return;
	}
	if (channel === "stable") {
		if (!nextState.autoInstallId) nextState.autoInstallId = state.autoInstallId?.trim() || randomUUID();
		if (state.autoFirstSeenVersion !== version || state.autoFirstSeenTag !== tag) {
			nextState.autoFirstSeenVersion = version;
			nextState.autoFirstSeenTag = tag;
			nextState.autoFirstSeenAt = new Date(now).toISOString();
		} else nextState.autoFirstSeenAt = state.autoFirstSeenAt;
		const firstSeenMs = nextState.autoFirstSeenAt ? Date.parse(nextState.autoFirstSeenAt) : now;
		const baseDelayMs = Math.max(0, stableDelayHours) * ONE_HOUR_MS;
		const jitterWindowMs = Math.max(0, stableJitterHours) * ONE_HOUR_MS;
		const jitterMs = resolveStableJitterMs(nextState.autoInstallId, version, tag, jitterWindowMs);
		const applyAfterMs = firstSeenMs + baseDelayMs + jitterMs;
		if (now < applyAfterMs) {
			log.info({
				version,
				tag,
				applyAfter: new Date(applyAfterMs).toISOString()
			}, "Auto-update deferred: stable rollout window not yet due");
			return;
		}
	}
	nextState.autoLastAttemptVersion = version;
	nextState.autoLastAttemptAt = new Date(now).toISOString();
	log.info({
		channel,
		version,
		tag
	}, "Starting auto-update");
	const lock = await acquireUpdateLock("auto");
	if (!lock) {
		log.info({
			version,
			tag
		}, "Auto-update skipped: another update is in progress");
		return;
	}
	try {
		const { runAutoUpdateCommand } = await import("./update-runner.js");
		const result = await runAutoUpdateCommand({
			channel,
			root
		});
		if (result.ok) {
			nextState.autoLastSuccessVersion = version;
			nextState.autoLastSuccessAt = new Date(now).toISOString();
			log.info({
				channel,
				version,
				tag
			}, "Auto-update applied successfully; restart gateway to refresh browser extension artifacts");
		} else log.warn({
			channel,
			version,
			tag,
			exitCode: result.exitCode,
			reason: result.reason
		}, `Auto-update attempt failed: ${result.reason ?? `exit ${result.exitCode}`}`);
	} catch (err) {
		log.error({
			err,
			channel,
			version
		}, "Auto-update command threw");
	} finally {
		await lock.release();
	}
}
/**
* Schedule periodic update checks. Returns a cleanup function to stop the timer.
*/
function scheduleGatewayUpdateCheck(params) {
	let stopped = false;
	let timer = null;
	const tick = async () => {
		if (stopped) return;
		try {
			await runGatewayUpdateCheck(params);
		} catch (err) {
			log.warn({ err }, "Periodic update check failed");
		}
		if (!stopped) {
			const intervalMs = resolveCheckIntervalMs(params.config);
			timer = setTimeout(() => void tick(), intervalMs);
		}
	};
	timer = setTimeout(() => void tick(), 5e3);
	return () => {
		stopped = true;
		if (timer) {
			clearTimeout(timer);
			timer = null;
		}
	};
}
//#endregion
export { getUpdateAvailable, runGatewayUpdateCheck, scheduleGatewayUpdateCheck };

//# sourceMappingURL=update-startup.js.map