/**
 * src/cli/daemon.ts
 *
 * `muassis daemon install|uninstall|status` — register Muassis to auto-start
 * on user login and restart on crash. Without this, every cron job is
 * academic: the gateway dies the moment the laptop sleeps and the daily
 * morning brief never fires.
 *
 * Platforms:
 *   - macOS:  ~/Library/LaunchAgents/ai.taseesai.muassis.plist (launchd)
 *   - Linux:  ~/.config/systemd/user/muassis.service (systemd --user)
 *   - Windows: schtasks /Create — register a logon-time scheduled task
 *
 * Design choices:
 *   - User-scoped (not system-scoped). Muassis is a per-user agent;
 *     installing as root would cross security boundaries.
 *   - KeepAlive=true on Mac, Restart=always on Linux. Crash-recovery
 *     happens automatically without human intervention.
 *   - StandardOut/Error logs to <dataDir>/logs/daemon.{out,err}.log so
 *     `muassis daemon status` can tail them.
 *   - Idempotent: install over an existing install replaces the unit file
 *     and reloads.
 */
import { execFileSync } from 'node:child_process';
import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { homedir, platform } from 'node:os';
import { fileURLToPath } from 'node:url';
import { getDataDir } from '../utils/data-dir.js';
const SERVICE_LABEL = 'ai.taseesai.muassis';
function logsDir() {
    const d = join(getDataDir(), 'logs');
    if (!existsSync(d))
        mkdirSync(d, { recursive: true });
    return d;
}
/**
 * (Round-3 audit fix) Log rotation. Daemon stdout/stderr append unbounded.
 * After 6 months running 24/7 these can hit GBs and fill the disk on small
 * devices. Rotates files when they exceed MAX_LOG_BYTES, keeping the last 3.
 *
 * Called from `tailDaemonLogs` and (idempotent, cheap) on each daemon
 * status check. Real launchd/systemd integration with logrotate would be
 * cleaner but requires external tooling — this is the zero-config path.
 */
const MAX_LOG_BYTES = 50 * 1024 * 1024; // 50MB
const MAX_LOG_BACKUPS = 3;
export function rotateDaemonLogs() {
    const dir = logsDir();
    const rotated = [];
    const fs = require('node:fs');
    for (const baseName of ['daemon.out.log', 'daemon.err.log']) {
        const filePath = join(dir, baseName);
        if (!fs.existsSync(filePath))
            continue;
        let size = 0;
        try {
            size = fs.statSync(filePath).size;
        }
        catch {
            continue;
        }
        if (size < MAX_LOG_BYTES)
            continue;
        // Shift existing backups: .2 → .3, .1 → .2, current → .1
        for (let i = MAX_LOG_BACKUPS - 1; i >= 1; i--) {
            const fromPath = join(dir, `${baseName}.${i}`);
            const toPath = join(dir, `${baseName}.${i + 1}`);
            if (fs.existsSync(fromPath)) {
                try {
                    fs.renameSync(fromPath, toPath);
                }
                catch { /* ignore */ }
            }
        }
        try {
            fs.renameSync(filePath, join(dir, `${baseName}.1`));
            // Recreate empty file with same permissions
            fs.writeFileSync(filePath, '', { mode: 0o644 });
            rotated.push(baseName);
        }
        catch { /* ignore */ }
    }
    return { rotated };
}
function muassisBin() {
    const npmBin = process.env.npm_node_execpath ? process.execPath : process.execPath;
    return npmBin;
}
/**
 * Resolve the absolute path to the entry script the daemon should exec.
 * In a published install, the bin field of package.json points at the
 * compiled entry — we use $(npm root -g)/.bin/muassis where it's a node
 * shim. We avoid that and exec the .js directly so launchd sees the real
 * argv0 instead of /usr/local/bin/node.
 */
function muassisEntry() {
    const here = fileURLToPath(import.meta.url);
    const candidates = [
        join(dirname(here), '..', 'cli.js'), // dist/cli.js relative to dist/cli/daemon.js
        join(dirname(here), '..', '..', 'dist', 'cli.js'), // repo root + dist/cli.js
        join(dirname(here), '..', '..', '..', 'dist', 'cli.js'), // installed under node_modules
    ];
    for (const c of candidates) {
        if (existsSync(c))
            return c;
    }
    // (Caveat fix) Auto-build instead of just erroring. Most "daemon install
    // failed" reports trace back to a missing build. Run `npm run build` in
    // the repo root and try again.
    const repoRoot = (() => {
        let d = dirname(here);
        for (let i = 0; i < 6; i++) {
            if (existsSync(join(d, 'package.json')))
                return d;
            d = dirname(d);
        }
        return null;
    })();
    if (repoRoot) {
        // eslint-disable-next-line no-console
        console.log(`[muassis daemon] dist/cli.js not found — running \`npm run build\` in ${repoRoot}…`);
        try {
            execFileSync('npm', ['run', 'build'], { cwd: repoRoot, stdio: 'inherit' });
        }
        catch (err) {
            throw new Error(`Auto-build failed: ${err.message}\n` +
                `Run \`cd ${repoRoot} && npm run build\` manually, then re-run \`muassis daemon install\`.`);
        }
        for (const c of candidates) {
            if (existsSync(c))
                return c;
        }
    }
    throw new Error(`Cannot locate the compiled CLI entry for the daemon. Looked at:\n  ${candidates.join('\n  ')}\n` +
        `Run \`npm run build\` to produce dist/cli.js, then re-run \`muassis daemon install\`.`);
}
// ─── macOS launchd ───────────────────────────────────────────────────
function macPlistPath() {
    return join(homedir(), 'Library', 'LaunchAgents', `${SERVICE_LABEL}.plist`);
}
function macPlistBody(opts) {
    const escape = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;');
    // (Audit H5) Build PATH from the live process so nvm/asdf/volta installations
    // resolve. Hardcoding /usr/local/bin only worked for system-installed node.
    const nodeDir = opts.node.replace(/\/[^/]+$/, '');
    const livePath = (process.env.PATH || '/usr/local/bin:/usr/bin:/bin').split(':');
    const seen = new Set();
    const mergedPath = [nodeDir, '/usr/local/bin', '/opt/homebrew/bin', ...livePath, '/usr/bin', '/bin']
        .filter((p) => p && !seen.has(p) && (seen.add(p), true))
        .join(':');
    return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>${SERVICE_LABEL}</string>
  <key>ProgramArguments</key>
  <array>
    <string>${escape(opts.node)}</string>
    <string>${escape(opts.entry)}</string>
    <string>start</string>
  </array>
  <key>WorkingDirectory</key>
  <string>${escape(opts.cwd)}</string>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
  <key>ProcessType</key>
  <string>Background</string>
  <key>StandardOutPath</key>
  <string>${escape(opts.logs.out)}</string>
  <key>StandardErrorPath</key>
  <string>${escape(opts.logs.err)}</string>
  <key>EnvironmentVariables</key>
  <dict>
    <key>PATH</key>
    <string>${escape(mergedPath)}</string>
    <key>HOME</key>
    <string>${escape(homedir())}</string>
    <key>MUASSIS_MODE</key>
    <string>device</string>
  </dict>
  <key>ThrottleInterval</key>
  <integer>10</integer>
</dict>
</plist>
`;
}
async function macInstall() {
    const plistPath = macPlistPath();
    mkdirSync(dirname(plistPath), { recursive: true });
    const logs = { out: join(logsDir(), 'daemon.out.log'), err: join(logsDir(), 'daemon.err.log') };
    const body = macPlistBody({
        entry: muassisEntry(),
        node: muassisBin(),
        logs,
        cwd: getDataDir(),
    });
    writeFileSync(plistPath, body, 'utf-8');
    // Bootstrap the agent for this user session. `bootstrap` works for unloaded
    // services; if already loaded, fall back to `bootout` then `bootstrap`.
    const uid = String(process.getuid?.() ?? 501);
    try {
        execFileSync('launchctl', ['bootout', `gui/${uid}/${SERVICE_LABEL}`], { stdio: 'ignore' });
    }
    catch { /* not loaded — fine */ }
    execFileSync('launchctl', ['bootstrap', `gui/${uid}`, plistPath], { stdio: 'inherit' });
    execFileSync('launchctl', ['enable', `gui/${uid}/${SERVICE_LABEL}`], { stdio: 'inherit' });
    execFileSync('launchctl', ['kickstart', '-k', `gui/${uid}/${SERVICE_LABEL}`], { stdio: 'inherit' });
}
async function macUninstall() {
    const plistPath = macPlistPath();
    const uid = String(process.getuid?.() ?? 501);
    try {
        execFileSync('launchctl', ['bootout', `gui/${uid}/${SERVICE_LABEL}`], { stdio: 'ignore' });
    }
    catch { /* ignore */ }
    if (existsSync(plistPath))
        unlinkSync(plistPath);
}
async function macStatus() {
    const plistPath = macPlistPath();
    const installed = existsSync(plistPath);
    let running = false;
    let pid = null;
    try {
        const out = execFileSync('launchctl', ['list', SERVICE_LABEL], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
        const m = out.match(/"PID"\s*=\s*(\d+);/);
        if (m) {
            running = true;
            pid = parseInt(m[1], 10);
        }
    }
    catch { /* not loaded */ }
    return {
        installed,
        running,
        pid,
        port: parseInt(process.env.PORT || '3000', 10),
        unitPath: installed ? plistPath : null,
        logPaths: {
            out: join(logsDir(), 'daemon.out.log'),
            err: join(logsDir(), 'daemon.err.log'),
        },
    };
}
// ─── Linux systemd --user ────────────────────────────────────────────
function linuxUnitPath() {
    const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
    return join(xdgConfig, 'systemd', 'user', 'muassis.service');
}
function linuxUnitBody(opts) {
    return `[Unit]
Description=Al Moasis Gateway (المؤسس)
Documentation=https://github.com/taseesai/muassis-gateway
After=network-online.target

[Service]
Type=simple
ExecStart=${opts.node} ${opts.entry} start
WorkingDirectory=${opts.cwd}
Restart=always
RestartSec=10
Environment=MUASSIS_MODE=device
StandardOutput=append:${opts.logs.out}
StandardError=append:${opts.logs.err}
KillMode=mixed
TimeoutStopSec=15

[Install]
WantedBy=default.target
`;
}
async function linuxInstall() {
    const unitPath = linuxUnitPath();
    mkdirSync(dirname(unitPath), { recursive: true });
    const logs = { out: join(logsDir(), 'daemon.out.log'), err: join(logsDir(), 'daemon.err.log') };
    writeFileSync(unitPath, linuxUnitBody({ entry: muassisEntry(), node: muassisBin(), logs, cwd: getDataDir() }), 'utf-8');
    execFileSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'inherit' });
    execFileSync('systemctl', ['--user', 'enable', '--now', 'muassis.service'], { stdio: 'inherit' });
}
async function linuxUninstall() {
    try {
        execFileSync('systemctl', ['--user', 'disable', '--now', 'muassis.service'], { stdio: 'ignore' });
    }
    catch { /* ignore */ }
    const unitPath = linuxUnitPath();
    if (existsSync(unitPath))
        unlinkSync(unitPath);
    try {
        execFileSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore' });
    }
    catch { /* ignore */ }
}
async function linuxStatus() {
    const unitPath = linuxUnitPath();
    const installed = existsSync(unitPath);
    let running = false;
    let pid = null;
    try {
        const out = execFileSync('systemctl', ['--user', 'show', '-p', 'ActiveState,MainPID', 'muassis.service'], { encoding: 'utf-8' });
        running = /ActiveState=active/.test(out);
        const m = out.match(/MainPID=(\d+)/);
        if (m)
            pid = parseInt(m[1], 10) || null;
    }
    catch { /* not loaded */ }
    return {
        installed,
        running,
        pid,
        port: parseInt(process.env.PORT || '3000', 10),
        unitPath: installed ? unitPath : null,
        logPaths: {
            out: join(logsDir(), 'daemon.out.log'),
            err: join(logsDir(), 'daemon.err.log'),
        },
    };
}
// ─── Windows (schtasks logon trigger) ────────────────────────────────
const WIN_TASK_NAME = 'AlMoasisGateway';
async function winInstall() {
    const node = muassisBin();
    const entry = muassisEntry();
    // Build the action command. schtasks needs absolute paths.
    const tr = `"${node}" "${entry}" start`;
    // (Audit H5/M-windows) /RL HIGHEST requires admin. Try it first, fall
    // back to LIMITED if the elevation isn't granted — better to install at
    // user level than fail entirely.
    try {
        execFileSync('schtasks', ['/Create', '/SC', 'ONLOGON', '/RL', 'HIGHEST', '/F', '/TN', WIN_TASK_NAME, '/TR', tr], { stdio: 'inherit' });
    }
    catch {
        execFileSync('schtasks', ['/Create', '/SC', 'ONLOGON', '/RL', 'LIMITED', '/F', '/TN', WIN_TASK_NAME, '/TR', tr], { stdio: 'inherit' });
    }
    execFileSync('schtasks', ['/Run', '/TN', WIN_TASK_NAME], { stdio: 'inherit' });
}
async function winUninstall() {
    try {
        execFileSync('schtasks', ['/End', '/TN', WIN_TASK_NAME], { stdio: 'ignore' });
    }
    catch { /* ignore */ }
    try {
        execFileSync('schtasks', ['/Delete', '/F', '/TN', WIN_TASK_NAME], { stdio: 'ignore' });
    }
    catch { /* ignore */ }
}
async function winStatus() {
    let installed = false;
    let running = false;
    try {
        const out = execFileSync('schtasks', ['/Query', '/TN', WIN_TASK_NAME, '/V', '/FO', 'LIST'], { encoding: 'utf-8' });
        installed = true;
        running = /Status:\s*Running/.test(out);
    }
    catch { /* not registered */ }
    return {
        installed,
        running,
        pid: null,
        port: parseInt(process.env.PORT || '3000', 10),
        unitPath: null,
        logPaths: {
            out: join(logsDir(), 'daemon.out.log'),
            err: join(logsDir(), 'daemon.err.log'),
        },
    };
}
// ─── Public surface ──────────────────────────────────────────────────
export async function daemonInstall() {
    const p = platform();
    if (p === 'darwin')
        return macInstall();
    if (p === 'linux')
        return linuxInstall();
    if (p === 'win32')
        return winInstall();
    throw new Error(`Unsupported platform: ${p}`);
}
export async function daemonUninstall() {
    const p = platform();
    if (p === 'darwin')
        return macUninstall();
    if (p === 'linux')
        return linuxUninstall();
    if (p === 'win32')
        return winUninstall();
    throw new Error(`Unsupported platform: ${p}`);
}
export async function daemonStatus() {
    const p = platform();
    if (p === 'darwin')
        return macStatus();
    if (p === 'linux')
        return linuxStatus();
    if (p === 'win32')
        return winStatus();
    return {
        installed: false,
        running: false,
        pid: null,
        port: parseInt(process.env.PORT || '3000', 10),
        unitPath: null,
        logPaths: null,
        lastError: `Unsupported platform: ${p}`,
    };
}
/**
 * Tail the last N lines of the daemon log for `muassis daemon logs`.
 */
export function tailDaemonLogs(lines = 80) {
    // (Round-3) Rotate before tailing so we don't hand back a gigantic file
    try {
        rotateDaemonLogs();
    }
    catch { /* non-fatal */ }
    const out = join(logsDir(), 'daemon.out.log');
    const err = join(logsDir(), 'daemon.err.log');
    return {
        out: existsSync(out) ? tailFile(out, lines) : '(no stdout log yet)',
        err: existsSync(err) ? tailFile(err, lines) : '(no stderr log yet)',
    };
}
function tailFile(path, lines) {
    try {
        const buf = readFileSync(path, 'utf-8');
        const all = buf.split('\n');
        return all.slice(-lines).join('\n');
    }
    catch (err) {
        return `(read failed: ${err.message})`;
    }
}
/**
 * Sanity probe — when daemon claims to be running, can we actually reach the
 * gateway on the configured port? Used by `muassis daemon status`.
 */
export async function probeGateway(port) {
    const start = Date.now();
    try {
        const ctrl = new AbortController();
        const t = setTimeout(() => ctrl.abort(), 1500);
        const r = await fetch(`http://127.0.0.1:${port}/health`, { signal: ctrl.signal });
        clearTimeout(t);
        return { healthy: r.ok, latencyMs: Date.now() - start, status: r.status };
    }
    catch {
        return { healthy: false, latencyMs: Date.now() - start };
    }
}
//# sourceMappingURL=daemon.js.map