import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import https from "node:https";
import { getConfigDir } from "../config/index.js";
import { logger } from "../logger.js";
const PACKAGE_NAME = "visionclaw";
/**
 * Detect whether we are running inside the esbuild CJS bundle.
 * The build script replaces `process.env.VISIONCLAW_BUNDLED_VERSION` with a
 * string literal at compile time, so at runtime the value is a real version
 * string (e.g. "0.1.160") in bundled mode and `undefined` otherwise.
 */
function isBundled() {
    const v = process.env.VISIONCLAW_BUNDLED_VERSION;
    return typeof v === "string" && v !== "undefined";
}
/**
 * Resolve the package root directory.
 *
 * - Unbundled (dist/utils/version-check.js): __dirname is `<root>/dist/utils/`
 *   → two levels up = `<root>/`
 * - Bundled (dist-agent/bundle.cjs): __dirname is `<root>/dist-agent/`
 *   → one level up = `<root>/`
 */
export function getPackageRoot() {
    const __dir = path.dirname(fileURLToPath(import.meta.url));
    return path.resolve(__dir, isBundled() ? ".." : "../..");
}
const PACKAGE_ROOT = getPackageRoot();
const PACKAGE_JSON_PATH = path.join(PACKAGE_ROOT, "package.json");
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
const STATE_FILENAME = "version-check.json";
export function isLocalCheckoutPackageRoot(packageRoot) {
    return (fs.existsSync(path.join(packageRoot, ".git")) ||
        fs.existsSync(path.join(packageRoot, "src")) ||
        fs.existsSync(path.join(packageRoot, "tsconfig.json")));
}
export function getInstallationInfo() {
    return {
        packageRoot: PACKAGE_ROOT,
        isLocalCheckout: isLocalCheckoutPackageRoot(PACKAGE_ROOT),
    };
}
export function getCurrentVersion() {
    // When bundled by esbuild, version is injected at build time
    const bundledVersion = process.env.VISIONCLAW_BUNDLED_VERSION;
    if (bundledVersion && bundledVersion !== "undefined") {
        return bundledVersion;
    }
    try {
        const pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, "utf-8"));
        return pkg.version ?? "0.0.0";
    }
    catch {
        return "0.0.0";
    }
}
export function compareSemver(a, b) {
    const parse = (v) => {
        const [core, ...rest] = v.split("-");
        return {
            nums: core.split(".").map((x) => parseInt(x, 10) || 0).concat([0, 0, 0]).slice(0, 3),
            pre: rest.length > 0 ? rest.join("-") : null,
        };
    };
    const pa = parse(a);
    const pb = parse(b);
    const [aMaj, aMin, aPatch] = pa.nums;
    const [bMaj, bMin, bPatch] = pb.nums;
    if (aMaj !== bMaj)
        return aMaj - bMaj;
    if (aMin !== bMin)
        return aMin - bMin;
    if (aPatch !== bPatch)
        return aPatch - bPatch;
    // Same core version: no prerelease > prerelease (per semver spec)
    if (!pa.pre && !pb.pre)
        return 0;
    if (!pa.pre)
        return 1; // a is stable, b is prerelease → a > b
    if (!pb.pre)
        return -1; // b is stable, a is prerelease → a < b
    // Both have prerelease: compare segments
    const aParts = pa.pre.split(".");
    const bParts = pb.pre.split(".");
    for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
        if (i >= aParts.length)
            return -1; // a has fewer segments → a < b
        if (i >= bParts.length)
            return 1;
        const aNum = parseInt(aParts[i], 10);
        const bNum = parseInt(bParts[i], 10);
        if (!isNaN(aNum) && !isNaN(bNum)) {
            if (aNum !== bNum)
                return aNum - bNum;
        }
        else {
            const cmp = aParts[i].localeCompare(bParts[i]);
            if (cmp !== 0)
                return cmp;
        }
    }
    return 0;
}
export async function getLatestNpmVersion() {
    return getNpmVersionByTag("latest");
}
export async function getNpmVersionByTag(tag) {
    const body = await httpGet(`https://registry.npmjs.org/${PACKAGE_NAME}/${tag}`);
    const parsed = JSON.parse(body);
    if (typeof parsed.version !== "string" || parsed.version.length === 0) {
        throw new Error(`npm registry response for tag "${tag}" did not include a version`);
    }
    return parsed.version;
}
function getStatePath() {
    return path.join(getConfigDir(), STATE_FILENAME);
}
function loadState() {
    try {
        const raw = fs.readFileSync(getStatePath(), "utf-8");
        return JSON.parse(raw);
    }
    catch {
        return { lastCheckAt: 0 };
    }
}
function saveState(state) {
    try {
        fs.writeFileSync(getStatePath(), JSON.stringify(state, null, 2), "utf-8");
    }
    catch {
        // best-effort
    }
}
/**
 * Whether enough time has passed since the last notification for a given version.
 * Returns true at most once per 24 hours per new version.
 */
export function shouldNotifyUpdate() {
    const state = loadState();
    const elapsed = Date.now() - state.lastCheckAt;
    const due = elapsed >= CHECK_INTERVAL_MS;
    logger.debug(`Version check: last=${state.lastCheckAt ? new Date(state.lastCheckAt).toISOString() : "never"} elapsed=${Math.round(elapsed / 60000)}min due=${due}`);
    return due;
}
export function recordUpdateNotification(version) {
    logger.debug(`Version check: recording notification for v${version}`);
    saveState({
        lastCheckAt: Date.now(),
        lastNotifiedVersion: version,
    });
}
/**
 * Fetch the CHANGELOG.md section for a specific version from unpkg CDN.
 * Returns the changelog text for entries newer than `fromVersion`, or undefined on failure.
 */
export async function fetchChangelogFromNpm(latestVersion, fromVersion) {
    try {
        const url = `https://unpkg.com/${PACKAGE_NAME}@${latestVersion}/CHANGELOG.md`;
        logger.debug(`Version check: fetching changelog from ${url}`);
        const body = await httpGet(url);
        logger.debug(`Version check: fetched changelog (${body.length} bytes)`);
        const sections = extractChangelogSections(body, fromVersion);
        logger.debug(`Version check: extracted changelog sections (${sections.length} chars)`);
        return sections;
    }
    catch (err) {
        logger.warn(`Version check: failed to fetch changelog: ${err instanceof Error ? err.message : String(err)}`);
        return undefined;
    }
}
/**
 * Extract all changelog sections newer than `fromVersion`.
 * Sections are delimited by `## [x.y.z]` headers.
 */
function extractChangelogSections(changelog, fromVersion) {
    const lines = changelog.split("\n");
    const result = [];
    let capturing = false;
    for (const line of lines) {
        const match = /^## \[([^\]]+)\]/.exec(line);
        if (match) {
            const sectionVersion = match[1];
            if (compareSemver(sectionVersion, fromVersion) <= 0) {
                break;
            }
            capturing = true;
        }
        if (capturing) {
            result.push(line);
        }
    }
    return result.join("\n").trim();
}
function httpGet(url, redirects = 0) {
    if (redirects > 5)
        return Promise.reject(new Error("Too many redirects"));
    return new Promise((resolve, reject) => {
        https
            .get(url, { timeout: 10000 }, (res) => {
            if (res.statusCode &&
                res.statusCode >= 300 &&
                res.statusCode < 400 &&
                res.headers.location) {
                resolve(httpGet(res.headers.location, redirects + 1));
                return;
            }
            if (res.statusCode && res.statusCode >= 400) {
                reject(new Error(`HTTP ${res.statusCode}`));
                return;
            }
            let data = "";
            res.on("data", (chunk) => {
                data += chunk.toString();
            });
            res.on("end", () => resolve(data));
            res.on("error", reject);
        })
            .on("error", reject);
    });
}
/**
 * Full version check: queries npm, compares versions, fetches changelog.
 */
export async function checkForUpdate() {
    const currentVersion = getCurrentVersion();
    logger.debug(`Version check: current=${currentVersion}, querying npm...`);
    const latestVersion = await getLatestNpmVersion();
    logger.debug(`Version check: latest=${latestVersion}`);
    if (compareSemver(latestVersion, currentVersion) <= 0) {
        logger.debug("Version check: already up to date");
        return { currentVersion, latestVersion, hasUpdate: false };
    }
    logger.debug(`Version check: update available ${currentVersion} → ${latestVersion}, fetching changelog...`);
    const changelog = await fetchChangelogFromNpm(latestVersion, currentVersion);
    return {
        currentVersion,
        latestVersion,
        hasUpdate: true,
        changelog,
    };
}
//# sourceMappingURL=version-check.js.map