import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
const PORT_RE = /^PORT=(\d+)/m;
const ENV_PORT_RE = /^Environment=PORT=(\d+)/m;
const ENV_EDGE_PORT_RE = /^Environment=EDGE_PORT=(\d+)/m;
function parseValid(raw) {
    if (!raw)
        return null;
    const n = parseInt(raw, 10);
    if (Number.isNaN(n) || n < 1024 || n > 65535)
        return null;
    return n;
}
export function resolveInstallPort(opts) {
    const defaultPort = opts.defaultPort ?? 19200;
    // Flag beats everything. Caller is responsible for validation prior.
    if (typeof opts.portFlag === "number") {
        return { port: opts.portFlag, source: "--port flag", driftLogs: [] };
    }
    let port = defaultPort;
    let source = "default";
    const envPort = opts.envContent !== null ? parseValid(opts.envContent.match(PORT_RE)?.[1]) : null;
    if (envPort !== null) {
        port = envPort;
        source = ".env override";
    }
    if (source === "default") {
        const svcPort = opts.maxyServiceContent !== null
            ? parseValid(opts.maxyServiceContent.match(ENV_PORT_RE)?.[1])
            : null;
        if (svcPort !== null) {
            port = svcPort;
            source = "running service";
        }
    }
    // one-shot drift recovery. Pre-task `maxy.service` unit files
    // carried the INTERNAL port in the `Environment=PORT=` slot, so the
    // reader above just inherited drift. `maxy-edge.service`'s EDGE_PORT is
    // authoritative for the tunnel (always has been), so we
    // trust it over the maxy.service value when they disagree — exactly
    // once per device, logged loudly.
    const driftLogs = [];
    const edgePort = opts.edgeServiceContent !== null
        ? parseValid(opts.edgeServiceContent.match(ENV_EDGE_PORT_RE)?.[1])
        : null;
    if (edgePort !== null && edgePort !== port) {
        driftLogs.push(`  [port-recovery] detected drift maxy=${port} edge=${edgePort} — pinning at ${edgePort}`);
        port = edgePort;
        source = "edge-recovery";
    }
    return { port, source, driftLogs };
}
export function resolveInstallPortFromFs(opts) {
    const readIfExists = (path) => {
        try {
            if (!existsSync(path))
                return null;
            return readFileSync(path, "utf-8");
        }
        catch {
            return null;
        }
    };
    return resolveInstallPort({
        portFlag: opts.portFlag,
        envContent: readIfExists(join(opts.persistDir, ".env")),
        maxyServiceContent: readIfExists(join(opts.serviceDir, opts.brandServiceFileName)),
        edgeServiceContent: readIfExists(join(opts.serviceDir, opts.brandEdgeServiceFileName)),
        defaultPort: opts.defaultPort,
    });
}
export function buildMaxyUnitFile(o) {
    if (!o.accountId) {
        // Caller (installService) must resolve the on-disk accountId before
        // calling. Falling through with an empty value would emit a unit file
        // with no Environment=ACCOUNT_ID line — bootValidator would FATAL on
        // every restart with reason=missing. Throw at build time so the install
        // aborts loudly instead of bricking the boot loop.
        throw new Error("buildMaxyUnitFile: accountId is required — caller must resolve the on-disk account UUID before stamping the systemd unit.");
    }
    const neo4jServiceDep = o.neo4jDedicated
        ? `neo4j-${o.brandHostname}.service`
        : "neo4j.service";
    const edgeUnitName = `${o.brandHostname}-edge.service`;
    const claudeSessionManagerUnitName = `${o.brandHostname}-claude-session-manager.service`;
    return `[Unit]
Description=${o.productName} AI Assistant
After=${neo4jServiceDep} ${edgeUnitName} ${claudeSessionManagerUnitName}
Wants=${edgeUnitName}
Requires=${claudeSessionManagerUnitName}

[Service]
Type=notify
NotifyAccess=all
WorkingDirectory=${o.installDir}/server
ExecStartPre=-/bin/bash ${o.installDir}/platform/scripts/resume-tunnel.sh
ExecStart=/usr/bin/node --require ./server-init.cjs server.js
Restart=on-failure
RestartSec=5
WatchdogSec=120
TimeoutStartSec=60
TimeoutStopSec=10
Environment=NODE_ENV=production
Environment=ACCOUNT_ID=${o.accountId}
Environment=PORT=${o.port}
Environment=MAXY_UI_INTERNAL_PORT=${o.maxyUiInternalPort}
Environment=HOSTNAME=127.0.0.1
Environment=KEEP_ALIVE_TIMEOUT=61000
Environment=DISPLAY=:${o.vncDisplay}
Environment=RFB_PORT=${o.rfbPort}
Environment=WEBSOCKIFY_PORT=${o.websockifyPort}
Environment=CDP_PORT=${o.cdpPort}
Environment=CLAUDE_SESSION_MANAGER_PORT=${o.claudeSessionManagerPort}
Environment=PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=${o.chromiumBin}
Environment=MAXY_PLATFORM_ROOT=${o.installDir}/platform
Environment=CLAUDE_CONFIG_DIR=${o.persistDir}/.claude
Environment=PATH=%h/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
EnvironmentFile=-${o.persistDir}/.env
StandardOutput=append:${o.persistDir}/logs/server.log
StandardError=append:${o.persistDir}/logs/server.log

[Install]
WantedBy=default.target
`;
}
export function buildClaudeSessionManagerUnitFile(o) {
    if (!o.accountId) {
        throw new Error("buildClaudeSessionManagerUnitFile: accountId is required — caller must resolve the on-disk account UUID before stamping the systemd unit.");
    }
    return `[Unit]
Description=${o.productName} Claude Session Manager
After=network.target

[Service]
Type=simple
WorkingDirectory=${o.installDir}/platform/services/claude-session-manager
ExecStart=/usr/bin/node dist/index.js
Restart=on-failure
RestartSec=5
TimeoutStartSec=30
TimeoutStopSec=15
Environment=NODE_ENV=production
Environment=ACCOUNT_ID=${o.accountId}
Environment=CLAUDE_SESSION_MANAGER_PORT=${o.claudeSessionManagerPort}
Environment=CLAUDE_SESSION_MANAGER_PERSIST_DIR=${o.persistDir}
Environment=CLAUDE_CONFIG_DIR=${o.persistDir}/.claude
Environment=PLATFORM_ROOT=${o.installDir}/platform
Environment=MAXY_BRAND_PORT=${o.brandPort}
Environment=MAXY_UI_INTERNAL_PORT=${o.maxyUiInternalPort}
Environment=PLATFORM_PORT=${o.maxyUiInternalPort}
Environment=CDP_PORT=${o.cdpPort}
EnvironmentFile=-${o.persistDir}/.env
StandardOutput=append:${o.persistDir}/logs/server.log
StandardError=append:${o.persistDir}/logs/server.log

[Install]
WantedBy=default.target
`;
}
// ---------------------------------------------------------------------------
// Task 250 — `claude-ptys.slice` unit. Every claude PTY scope the session
// manager spawns (`claude-session-<token>.scope`) is created under this
// slice so the operator can see the cohort with a single `systemctl --user
// status claude-ptys.slice`. The slice is not strictly required for cgroup
// isolation — scopes ride directly under user.slice if no `--slice=` flag
// is given — but cohort observability and any future per-cohort resource
// limit (MemoryMax=, CPUWeight=) live on this unit.
//
// Brand-agnostic: there is one `claude-ptys.slice` shared across every
// brand on the host. The user-systemd manager owns the slice; multiple
// brands' session-managers can drop their per-spawn scopes under the same
// slice without interference because scopes are uniquely named via the
// per-spawn unit token. The installer writes the slice file at
// `~/.config/systemd/user/claude-ptys.slice` and runs
// `systemctl --user daemon-reload` so the new unit is picked up.
// ---------------------------------------------------------------------------
export function buildClaudePtysSliceUnitFile() {
    return `[Unit]
Description=Claude Code PTY sessions (Task 250)
Documentation=https://github.com/rubytech/maxy-code/blob/main/.tasks/archive/250-claude-ptys-survive-manager-restart-via-systemd-scope-per-pty.md
Before=shutdown.target

[Slice]

[Install]
WantedBy=default.target
`;
}
// ---------------------------------------------------------------------------
// Task 602 — `cloudflared.slice` unit. Every cloudflared-<brand>.scope
// spawned by resume-tunnel.sh drops under this slice for one-glance cohort
// observability (`systemctl --user status cloudflared.slice`).
//
// Brand-agnostic: one slice across all co-resident brands, mirroring the
// Task-250 claude-ptys.slice precedent.
// ---------------------------------------------------------------------------
export function buildCloudflaredSliceUnitFile() {
    return `[Unit]
Description=Cloudflare tunnel scopes (Task 602)
Documentation=https://github.com/rubytech/maxy-code/blob/main/.tasks/archive/602-cloudflared-tunnel-must-survive-brand-service-restart-via-systemd-scope.md
Before=shutdown.target

[Slice]

[Install]
WantedBy=default.target
`;
}
