#!/usr/bin/env node
/**
 * msger-native postinstall:
 * 1. Copy binary from target/release/ to bin/ (if newer — after cargo build)
 * 2. Create timestamped copy in the PER-USER bin dir (not node_modules)
 *    so the locked previous-version exe doesn't block npm's aside-dir cleanup
 *    on upgrade — npm only manages the stable msgernative.exe in bin/.
 * 3. Clean up old timestamped copies in the user dir (skip if locked)
 * 4. Set binary permissions on Linux/Mac
 *
 * NOTE: keep this in sync with `getUserBinDir()` in shower.ts — same path
 * convention so the launcher finds what postinstall just wrote.
 */
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const nativeDir = path.join(__dirname, "..");
const binDir = path.join(nativeDir, "bin");

const isWindows = process.platform === "win32";
const arch = process.arch;

let baseName, ext;
if (isWindows) {
    baseName = "msgernative";
    ext = ".exe";
} else if (process.platform === "darwin") {
    // Mac binaries use explicit darwin names so they don't collide with the
    // Pi (`msgernative-arm64`) or generic Linux ARM (`msgernative-linux-aarch64`)
    // binaries. Built on a Mac via build-mac.sh, synced via OneDrive's `mac/`,
    // copied into bin/ at prepublishOnly time.
    baseName = arch === "arm64" ? "msgernative-darwin-arm64" : "msgernative-darwin-x64";
    ext = "";
} else {
    baseName = arch === "arm64" ? "msgernative-linux-aarch64" : "msgernative";
    ext = "";
}

const srcBinary = path.join(binDir, `${baseName}${ext}`);
const buildBinary = path.join(nativeDir, "target", "release", `${baseName}${ext}`);

function getUserBinDir() {
    if (isWindows) {
        const base = process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || ".", "AppData", "Local");
        return path.join(base, "msger", "bin");
    }
    const xdg = process.env.XDG_DATA_HOME;
    const base = xdg || path.join(process.env.HOME || ".", ".local", "share");
    return path.join(base, "msger", "bin");
}

const userBinDir = getUserBinDir();

// Step 1: Copy from target/release/ → bin/ if build output is newer
if (fs.existsSync(buildBinary)) {
    const buildTime = fs.statSync(buildBinary).mtimeMs;
    const srcTime = fs.existsSync(srcBinary) ? fs.statSync(srcBinary).mtimeMs : 0;
    if (buildTime > srcTime) {
        try {
            fs.copyFileSync(buildBinary, srcBinary);
            console.log(`  msger: updated ${baseName}${ext} from build`);
        } catch (e) {
            // Source might be locked by running process — that's OK, timestamped copy will use what we have
            console.log(`  msger: could not update ${baseName}${ext} (locked?) — using existing`);
        }
    }
}

// Step 2: Create timestamped copy in the user bin dir (NOT node_modules)
try {
    fs.mkdirSync(userBinDir, { recursive: true });
} catch (e) {
    // recursive:true normally makes this idempotent, so a real failure here
    // means the path can't be created at all (permissions, disk full, etc.).
    if (e.code !== "EEXIST") {
        console.error(`  msger: could not create user bin dir ${userBinDir}: ${e.message}`);
        throw e;
    }
}
const stamp = Date.now();
const versionedBinary = path.join(userBinDir, `${baseName}-${stamp}${ext}`);
if (fs.existsSync(srcBinary)) {
    try {
        fs.copyFileSync(srcBinary, versionedBinary);
    } catch (e) {
        console.error(`  msger: failed to create versioned binary: ${e.message}`);
    }
}

// Step 2b: Create/update a stable-name hardlink so the launcher always uses
// the same path. Taskbar pins and AUMID grouping depend on a fixed exe path.
//
// Rename-aside pattern: Windows lets you rename a running exe's path but
// refuses to delete it. So we rename the current stable link aside (to a
// timestamped `.old-<ts>` name) before creating the new hardlink. This works
// whether or not the old binary is currently executing — the retired name
// keeps the running process's path valid; the stable slot is freed for the
// new hardlink. Previously we tried unlink+link, which silently failed when
// the stable name was locked, leaving users on a stale binary indefinitely.
const stableLink = path.join(userBinDir, `${baseName}${ext}`);
if (fs.existsSync(stableLink)) {
    const retired = path.join(userBinDir, `${baseName}${ext}.old-${stamp}`);
    try {
        fs.renameSync(stableLink, retired);
    } catch (e) {
        console.error(`  msger: could not rename stable link aside: ${e.message}`);
        console.error(`  msger: update cannot proceed — stable link ${stableLink} is in an unexpected state`);
    }
}
try {
    fs.linkSync(versionedBinary, stableLink);
} catch (e) {
    // Hardlink failed (cross-device, weird FS). Copy as a fallback.
    console.error(`  msger: hardlink ${stableLink} -> ${versionedBinary} failed (${e.message}); falling back to copy`);
    try {
        fs.copyFileSync(versionedBinary, stableLink);
    } catch (e2) {
        console.error(`  msger: copy fallback also failed: ${e2.message}`);
        console.error(`  msger: launcher will fall back to timestamped copies; taskbar pins may not group.`);
    }
}

// Step 2c: Clean up retired `.old-<ts>` copies from previous updates. A file
// that's still locked by a not-yet-exited process stays behind and gets
// retried on the NEXT update — that's expected, so an EBUSY/EPERM on unlink
// isn't an error, but anything else is.
const oldPattern = new RegExp(`^${baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}${ext.replace('.', '\\.')}\\.old-\\d+$`);
try {
    for (const f of fs.readdirSync(userBinDir)) {
        if (!oldPattern.test(f)) continue;
        const full = path.join(userBinDir, f);
        try {
            fs.unlinkSync(full);
        } catch (e) {
            if (e.code !== "EBUSY" && e.code !== "EPERM") {
                console.error(`  msger: unexpected error cleaning up ${f}: ${e.message}`);
            }
            // else: expected — owning process still running, defer to next update
        }
    }
} catch (e) {
    console.error(`  msger: could not scan ${userBinDir} for retired copies: ${e.message}`);
}

// Step 3a: Clean up old timestamped copies in the user bin dir (keep the one
// we just created). EBUSY/EPERM on a locked copy means the owning process is
// still running — expected, retry on next update. Anything else is unexpected
// and gets logged.
const pattern = new RegExp(`^${baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-(\\d+)${ext.replace('.', '\\.')}$`);
try {
    for (const f of fs.readdirSync(userBinDir)) {
        if (!f.match(pattern) || f === path.basename(versionedBinary)) continue;
        const full = path.join(userBinDir, f);
        try {
            fs.unlinkSync(full);
        } catch (e) {
            if (e.code !== "EBUSY" && e.code !== "EPERM") {
                console.error(`  msger: unexpected error cleaning up ${f}: ${e.message}`);
            }
        }
    }
} catch (e) {
    console.error(`  msger: could not scan ${userBinDir} for stale timestamped copies: ${e.message}`);
}

// Step 3b: Migrate-and-clean any stale timestamped copies still living inside
// node_modules from older installs. They serve no purpose now and may be the
// EPERM blocker on upgrade.
try {
    for (const f of fs.readdirSync(binDir)) {
        if (!f.match(pattern)) continue;
        const full = path.join(binDir, f);
        try {
            fs.unlinkSync(full);
        } catch (e) {
            if (e.code !== "EBUSY" && e.code !== "EPERM") {
                console.error(`  msger: unexpected error cleaning up legacy ${f}: ${e.message}`);
            }
        }
    }
} catch (e) {
    // binDir should always exist inside the package — if it doesn't, something
    // is very wrong, but we can still let the install complete.
    console.error(`  msger: could not scan ${binDir} for legacy timestamped copies: ${e.message}`);
}

// Step 4: Set permissions on Linux/Mac
if (!isWindows) {
    for (const bin of [srcBinary, versionedBinary]) {
        try {
            fs.chmodSync(bin, 0o755);
        } catch (e) {
            // ENOENT = we never created that file (bin wasn't present); any
            // other error (EACCES, EROFS, etc.) means the binary will fail to
            // launch later, which is user-visible — log loud.
            if (e.code !== "ENOENT") {
                console.error(`  msger: could not chmod ${bin}: ${e.message}`);
            }
        }
    }
}

// Friendly message about the EPERM warnings npm itself emits during a
// subsequent global upgrade. They come from npm trying to delete its own
// staging directory while the previously-running msgernative.exe inside
// still holds a file handle. msger's rename-aside scheme above has already
// handled the actual upgrade safely; the warnings are cosmetic.
if (isWindows) {
    console.log("");
    console.log("  msger: install OK.");
    console.log("  msger: if a future `npm i -g @bobfrankston/msger` prints");
    console.log("         EPERM/ENOTEMPTY warnings about removing a staging");
    console.log("         directory, those are expected when an older msger");
    console.log("         is still running — the upgrade has already taken");
    console.log("         effect via rename-aside. Safe to ignore.");
    console.log("");
}
