// pure classifier for the install-time port collision pre-flight.
// extended with Xtigervnc and websockify holder anchors, closing
// the laptop + Pi miss where the brand's own VNC stack was misclassified
// UNRELATED because Xtigervnc has no `--user-data-dir=` argv to anchor on.
//
// The wrapper in index.ts owns the side effects: ss read, /proc/<pid>/cmdline
// read, SIGTERM/SIGKILL escalation, and the operator-override exit. This
// module owns only the classification rule — inputs in, decision out.
//
// Holder detection (post-pid):
//   argv[0] basename ∈ chromium-set                    → chromium
//   argv[0] basename === "Xtigervnc"                   → xtigervnc
//   any argv basename === "websockify" (full scan      → websockify
//     because Pi Bookworm invokes it as
//     `python3 /usr/bin/websockify …`, so argv[0] is
//     the interpreter, not websockify itself)
//   else                                                → unknown
//
// Per-holder anchor:
//   chromium   — `--user-data-dir=PATH`; PATH contains `/<configDir>/chromium-profile`
//                → OWN_BRAND if configDir matches own; PEER_BRAND if matches peer.
//                False-match guard: argv-anchor on the flag, never substring,
//                so `--load-extension=…/<configDir>/…` cannot poison.
//   xtigervnc  — first non-flag argv of the form `:N` (display number);
//                vnc.sh invokes `Xtigervnc "${VNC_DISPLAY}" -rfbport …` so
//                argv[1] is literally `:99`, `:100`, etc. Match N against
//                own.vncDisplay / peer.vncDisplay.
//   websockify — collect every port number embedded in argv (covers both
//                `[::]:6080` bind form and `localhost:5900` upstream form).
//, brand websockifyPort/rfbPort sets are
//                disjoint, so at most one brand's websockifyPort can appear.
//
// Why "last pid=" instead of "first pid=": ss with `-tlnpH sport = :PORT`
// emits the LISTEN row last; if a future ss locale prepends a header line
// containing `pid=` text, the first-match heuristic would lift the wrong
// pid. The last `pid=` is always inside the LISTEN row's `users:((…))`.
/**
 * `cmdline` should be the raw `/proc/<pid>/cmdline` contents — NUL-separated
 * argv as the kernel emits it. Do NOT replace NUL with space before passing:
 * the argv boundaries are what disambiguate `--user-data-dir=PATH` from a
 * different flag whose value happens to contain `--user-data-dir=`.
 */
export function classifyPortHolder(args) {
    const { ssOutput, ownBrand, peerBrands, getCmdline } = args;
    if (ssOutput.trim() === "")
        return { kind: "EMPTY" };
    const pidMatches = [...ssOutput.matchAll(/pid=(\d+)/g)];
    if (pidMatches.length === 0)
        return { kind: "UNRELATED" };
    const pid = Number(pidMatches[pidMatches.length - 1][1]);
    let cmdline;
    try {
        cmdline = getCmdline(pid);
    }
    catch {
        return { kind: "UNRELATED", pid, cmdlineReadFailed: true };
    }
    const prettyCmdline = cmdline.replace(/\0/g, " ").trim();
    const argv = cmdline.split("\0").filter(s => s.length > 0);
    const holderType = detectHolderType(argv);
    switch (holderType) {
        case "chromium":
            return classifyChromium(argv, prettyCmdline, pid, ownBrand, peerBrands);
        case "xtigervnc":
            return classifyXtigervnc(argv, prettyCmdline, pid, ownBrand, peerBrands);
        case "websockify":
            return classifyWebsockify(argv, prettyCmdline, pid, ownBrand, peerBrands);
        case "unknown":
            return { kind: "UNRELATED", pid, cmdline: prettyCmdline, holderType };
    }
}
// ---------------------------------------------------------------------------
// Holder detection
// ---------------------------------------------------------------------------
const CHROMIUM_BASENAMES = new Set([
    "chrome",
    "chromium",
    "chromium-browser",
    "google-chrome",
    "google-chrome-stable",
]);
function basename(p) {
    const i = p.lastIndexOf("/");
    return i === -1 ? p : p.slice(i + 1);
}
function detectHolderType(argv) {
    if (argv.length === 0)
        return "unknown";
    const head = basename(argv[0]);
    if (CHROMIUM_BASENAMES.has(head))
        return "chromium";
    if (head === "Xtigervnc")
        return "xtigervnc";
    // websockify can be invoked directly (shebang) or via python; scan argv.
    for (const a of argv) {
        if (basename(a) === "websockify")
            return "websockify";
    }
    return "unknown";
}
// ---------------------------------------------------------------------------
// Per-holder anchors
// ---------------------------------------------------------------------------
function classifyChromium(argv, prettyCmdline, pid, ownBrand, peerBrands) {
    const userDataDir = findFlagValue(argv, "--user-data-dir");
    if (userDataDir === null) {
        // Kernel threads, GPU/zygote/utility chrome processes inherit profile
        // via fork without re-stating --user-data-dir. They wouldn't be
        // listening anyway, but classify UNRELATED if one ever shows up here
        // — never OWN_BRAND on a substring fluke.
        return { kind: "UNRELATED", pid, cmdline: prettyCmdline, holderType: "chromium" };
    }
    const ownSuffix = `/${ownBrand.configDir}/chromium-profile`;
    if (userDataDir.includes(ownSuffix)) {
        return {
            kind: "OWN_BRAND", pid, cmdline: prettyCmdline,
            profilePath: userDataDir, holderType: "chromium",
        };
    }
    for (const peer of peerBrands) {
        const peerSuffix = `/${peer.configDir}/chromium-profile`;
        if (userDataDir.includes(peerSuffix)) {
            return {
                kind: "PEER_BRAND", pid, cmdline: prettyCmdline,
                profilePath: userDataDir, holderType: "chromium",
            };
        }
    }
    return { kind: "UNRELATED", pid, cmdline: prettyCmdline, holderType: "chromium" };
}
function classifyXtigervnc(argv, prettyCmdline, pid, ownBrand, peerBrands) {
    const display = parseDisplayArg(argv);
    if (display === null) {
        return { kind: "UNRELATED", pid, cmdline: prettyCmdline, holderType: "xtigervnc" };
    }
    if (display === ownBrand.vncDisplay) {
        return {
            kind: "OWN_BRAND", pid, cmdline: prettyCmdline,
            vncDisplay: display, holderType: "xtigervnc",
        };
    }
    for (const peer of peerBrands) {
        if (display === peer.vncDisplay) {
            return {
                kind: "PEER_BRAND", pid, cmdline: prettyCmdline,
                vncDisplay: display, holderType: "xtigervnc",
            };
        }
    }
    return {
        kind: "UNRELATED", pid, cmdline: prettyCmdline,
        vncDisplay: display, holderType: "xtigervnc",
    };
}
function classifyWebsockify(argv, prettyCmdline, pid, ownBrand, peerBrands) {
    const ports = collectPorts(argv);
    if (ports.has(ownBrand.websockifyPort)) {
        return {
            kind: "OWN_BRAND", pid, cmdline: prettyCmdline,
            websockifyPort: ownBrand.websockifyPort, holderType: "websockify",
        };
    }
    for (const peer of peerBrands) {
        if (ports.has(peer.websockifyPort)) {
            return {
                kind: "PEER_BRAND", pid, cmdline: prettyCmdline,
                websockifyPort: peer.websockifyPort, holderType: "websockify",
            };
        }
    }
    return { kind: "UNRELATED", pid, cmdline: prettyCmdline, holderType: "websockify" };
}
// ---------------------------------------------------------------------------
// argv parsing helpers
// ---------------------------------------------------------------------------
// Find --flag=VALUE (single argv) or --flag VALUE (split across two argvs).
function findFlagValue(argv, flag) {
    const PREFIX = `${flag}=`;
    for (let i = 0; i < argv.length; i++) {
        const a = argv[i];
        if (a.startsWith(PREFIX))
            return a.slice(PREFIX.length);
        if (a === flag && i + 1 < argv.length)
            return argv[i + 1];
    }
    return null;
}
// First non-flag argv of the form `:N` after argv[0]. vnc.sh's invocation
// puts the display in argv[1], but the function tolerates additional
// pre-positional flags by scanning past any `-`-prefixed token.
function parseDisplayArg(argv) {
    for (let i = 1; i < argv.length; i++) {
        const a = argv[i];
        if (a.startsWith("-"))
            continue;
        const m = a.match(/^:(\d+)$/);
        if (m)
            return Number(m[1]);
    }
    return null;
}
// Collect every port number embedded in argv. Matches:
//   "[::]:6080"        → 6080  (websockify bind, IPv6 wildcard)
//   "0.0.0.0:8080"     → 8080  (bind, IPv4 wildcard)
//   "localhost:5900"   → 5900  (websockify upstream)
//   "6080"             → 6080  (bare positional)
// Skips flag tokens beginning with "-".
function collectPorts(argv) {
    const ports = new Set();
    const portRe = /(?::|^)(\d{1,5})$/;
    for (const a of argv) {
        if (a.startsWith("-"))
            continue;
        const m = a.match(portRe);
        if (m === null)
            continue;
        const n = Number(m[1]);
        if (n >= 1 && n <= 65535)
            ports.add(n);
    }
    return ports;
}
