import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { homedir, hostname, networkInterfaces } from 'node:os';
import { dirname, join } from 'node:path';
import { DEFAULT_PUBLIC_SPAWN_LIMITS, } from './spawn-rate-limiter.js';
const DEFAULT_PORT = 19400;
const DEFAULT_URL_TIMEOUT_MS = 30_000;
const DEFAULT_KILL_GRACE_MS = 5_000;
const DEFAULT_JSONL_READ_MAX_BYTES = 1 * 1024 * 1024;
const DEFAULT_BRAND_PORT = 19200;
const DEFAULT_SPECIALIST_CAP = 3;
const DEFAULT_OPERATOR_RESERVE = 2;
const DEFAULT_TOTAL_PTY_CAP = 64;
const DEFAULT_RECORDER_IDLE_TTL_MS = 120_000;
const PTY_MAX_PATH = '/proc/sys/kernel/pty/max';
function parsePositiveInt(raw, fallback) {
    if (raw === undefined)
        return fallback;
    const n = Number(raw);
    return Number.isInteger(n) && n > 0 ? n : fallback;
}
function readKernelPtyMax() {
    try {
        const raw = readFileSync(PTY_MAX_PATH, 'utf-8').trim();
        const n = Number(raw);
        return Number.isInteger(n) && n > 0 ? n : Number.POSITIVE_INFINITY;
    }
    catch {
        // Darwin (dev) / sysctl absent → no kernel-derived ceiling. Env cap holds.
        return Number.POSITIVE_INFINITY;
    }
}
const CHANNELS_FOR_LIMITS = [
    'browser',
    'whatsapp',
    'telegram',
    'webchat',
    'email',
];
function parseBucket(raw, fallback) {
    if (!raw || typeof raw !== 'object')
        return fallback;
    const r = raw;
    const cap = typeof r.cap === 'number' && Number.isInteger(r.cap) && r.cap > 0 ? r.cap : fallback.cap;
    const windowMs = typeof r.windowMs === 'number' && Number.isInteger(r.windowMs) && r.windowMs > 0
        ? r.windowMs
        : fallback.windowMs;
    return { cap, windowMs };
}
function parsePublicSpawnLimits(raw) {
    const defaults = DEFAULT_PUBLIC_SPAWN_LIMITS;
    if (!raw || typeof raw !== 'object')
        return defaults;
    const r = raw;
    const perChannelRaw = (r.perChannel && typeof r.perChannel === 'object'
        ? r.perChannel
        : {});
    const perChannel = {};
    for (const ch of CHANNELS_FOR_LIMITS) {
        perChannel[ch] = parseBucket(perChannelRaw[ch], defaults.perChannel[ch]);
    }
    return {
        perChannel,
        global: parseBucket(r.global, defaults.global),
    };
}
function applyEnvOverride(limits, env) {
    const capEnv = env.MAXY_PUBLIC_SPAWN_CAP;
    const winEnv = env.MAXY_PUBLIC_SPAWN_WINDOW_MS;
    const capN = capEnv ? Number(capEnv) : null;
    const winN = winEnv ? Number(winEnv) : null;
    if (capN === null && winN === null)
        return limits;
    const next = { perChannel: {}, global: { ...limits.global } };
    const overrideCap = capN !== null && Number.isInteger(capN) && capN > 0 ? capN : null;
    const overrideWindow = winN !== null && Number.isInteger(winN) && winN > 0 ? winN : null;
    for (const ch of CHANNELS_FOR_LIMITS) {
        next.perChannel[ch] = {
            cap: overrideCap ?? limits.perChannel[ch].cap,
            windowMs: overrideWindow ?? limits.perChannel[ch].windowMs,
        };
    }
    if (overrideCap !== null)
        next.global.cap = Math.max(next.global.cap, overrideCap * 3);
    if (overrideWindow !== null)
        next.global.windowMs = overrideWindow;
    return next;
}
function loadBrand(env) {
    const platformRoot = env.PLATFORM_ROOT ?? env.MAXY_PLATFORM_ROOT;
    if (!platformRoot) {
        throw new Error('[claude-session-manager] PLATFORM_ROOT (or MAXY_PLATFORM_ROOT) is not set — cannot resolve brand.json. ' +
            'The systemd unit must pass PLATFORM_ROOT=${installDir}/platform.');
    }
    const brandPath = join(platformRoot, 'config', 'brand.json');
    if (!existsSync(brandPath)) {
        throw new Error(`[claude-session-manager] brand.json missing at ${brandPath} — install is corrupted or PLATFORM_ROOT points to the wrong directory.`);
    }
    let brand;
    try {
        brand = JSON.parse(readFileSync(brandPath, 'utf-8'));
    }
    catch (err) {
        const detail = (err instanceof Error ? err.message : String(err)).slice(0, 200);
        throw new Error(`[claude-session-manager] brand.json at ${brandPath} parse failed: ${detail}`);
    }
    const hn = typeof brand.hostname === 'string' && brand.hostname.length > 0 ? brand.hostname : null;
    if (!hn) {
        throw new Error(`[claude-session-manager] brand.json at ${brandPath} missing required field: hostname`);
    }
    const productName = typeof brand.productName === 'string' && brand.productName.length > 0 ? brand.productName : null;
    if (!productName) {
        throw new Error(`[claude-session-manager] brand.json at ${brandPath} missing required field: productName`);
    }
    const publicSpawnLimits = parsePublicSpawnLimits(brand.publicSpawnLimits);
    return { brandPath, fields: { hostname: hn, productName, publicSpawnLimits } };
}
function resolveLanIPv4() {
    const ifaces = networkInterfaces();
    for (const [, addrs] of Object.entries(ifaces)) {
        if (!addrs)
            continue;
        for (const addr of addrs) {
            if (addr.family === 'IPv4' && !addr.internal) {
                return { ip: addr.address, diagnostic: null };
            }
        }
    }
    const names = Object.keys(ifaces).join(',') || '(no interfaces detected)';
    return { ip: null, diagnostic: `no non-loopback IPv4 across interfaces: ${names}` };
}
/** walk `<installDir>/data/accounts/` for the first sub-directory
 *  containing `account.json`. Mirrors the installer's `resolveInstallAccountId`
 *  (packages/create-maxy-code/src/index.ts) so the runtime reads the same
 *  account the installer wrote. Returns null when no account is found; the
 *  caller surfaces `account-dir-unresolved` and refuses to start.
 *
 *  `installDir` = parent of `PLATFORM_ROOT` (the systemd unit passes
 *  `PLATFORM_ROOT=<installDir>/platform`, so `dirname()` is exact). */
function resolveAccountDir(platformRoot) {
    const installDir = dirname(platformRoot);
    const accountsRoot = join(installDir, 'data', 'accounts');
    if (!existsSync(accountsRoot))
        return null;
    let entries;
    try {
        entries = readdirSync(accountsRoot);
    }
    catch {
        return null;
    }
    for (const entry of entries) {
        const candidate = join(accountsRoot, entry);
        if (existsSync(join(candidate, 'account.json')))
            return candidate;
    }
    return null;
}
function resolveBrandPort(env) {
    const raw = env.MAXY_BRAND_PORT ?? env.PORT;
    if (!raw)
        return DEFAULT_BRAND_PORT;
    const n = Number(raw);
    if (!Number.isInteger(n) || n < 1 || n > 65535)
        return DEFAULT_BRAND_PORT;
    return n;
}
export function loadConfig(env = process.env) {
    const port = Number(env.CLAUDE_SESSION_MANAGER_PORT ?? DEFAULT_PORT);
    if (!Number.isInteger(port) || port < 1 || port > 65535) {
        throw new Error(`CLAUDE_SESSION_MANAGER_PORT invalid: ${env.CLAUDE_SESSION_MANAGER_PORT}`);
    }
    const persistDir = env.CLAUDE_SESSION_MANAGER_PERSIST_DIR ?? join(homedir(), '.maxy');
    const claudeBin = env.CLAUDE_BIN ?? 'claude';
    const brand = loadBrand(env);
    const platformRoot = env.PLATFORM_ROOT ?? env.MAXY_PLATFORM_ROOT;
    if (!platformRoot) {
        // `loadBrand` already validates this, but Typescript narrowing needs the
        // re-check here so `resolveAccountDir` sees a non-undefined string.
        throw new Error('[claude-session-manager] PLATFORM_ROOT unresolved after loadBrand — internal error');
    }
    const accountDir = resolveAccountDir(platformRoot);
    if (!accountDir) {
        throw new Error(`[claude-session-manager] account-dir-unresolved: no <installDir>/data/accounts/<uuid>/account.json under ${dirname(platformRoot)}/data/accounts. ` +
            'The installer (setup-account.sh / writeInstallDefaults) must run before manager start.');
    }
    // Claude Code walks `.claude/settings.json` from cwd to .git root. Defaulting
    // spawnCwd to process.cwd() — i.e. the manager service directory — strands
    // the account-scoped settings.json (and its Stop hook) outside that walk.
    // Default to accountDir so setup-account.sh's `<accountDir>/.claude/settings.json`
    // is on the discovery path. Env-var override is preserved for tests.
    const spawnCwd = env.CLAUDE_SESSION_MANAGER_SPAWN_CWD ?? accountDir;
    const lan = resolveLanIPv4();
    const brandPort = resolveBrandPort(env);
    const osHost = hostname();
    // adminUrl prefers the observed LAN IPv4 (which always routes inside the
    // device's network) over `<brand-hostname>.local`, because mDNS is a hint,
    // not a guarantee. When lanIPv4 resolution failed (no non-loopback
    // interface), fall back to the mDNS form so the agent still sees a URL
    // it can present to the operator. Boot logs both the chosen form and
    // the alternative for forensic review.
    const adminUrl = lan.ip
        ? `http://${lan.ip}:${brandPort}`
        : `http://${brand.fields.hostname}.local:${brandPort}`;
    const tunnelUrl = null;
    const specialistCap = parsePositiveInt(env.CLAUDE_SESSION_MANAGER_SPECIALIST_CAP, DEFAULT_SPECIALIST_CAP);
    const operatorReserve = parsePositiveInt(env.CLAUDE_SESSION_MANAGER_OPERATOR_RESERVE, DEFAULT_OPERATOR_RESERVE);
    const envTotalCap = parsePositiveInt(env.CLAUDE_SESSION_MANAGER_TOTAL_PTY_CAP, DEFAULT_TOTAL_PTY_CAP);
    const totalPtyCap = Math.min(readKernelPtyMax(), envTotalCap);
    const recorderIdleTtlMs = parsePositiveInt(env.CLAUDE_SESSION_MANAGER_RECORDER_IDLE_TTL_MS, DEFAULT_RECORDER_IDLE_TTL_MS);
    return {
        port,
        persistDir,
        claudeBin,
        spawnCwd,
        urlCaptureTimeoutMs: DEFAULT_URL_TIMEOUT_MS,
        killGraceMs: DEFAULT_KILL_GRACE_MS,
        jsonlReadMaxBytes: DEFAULT_JSONL_READ_MAX_BYTES,
        specialistCap,
        operatorReserve,
        totalPtyCap,
        recorderIdleTtlMs,
        host: {
            hostname: osHost,
            lanIPv4: lan.ip,
            adminUrl,
            tunnelUrl,
            lanIPv4Diagnostic: lan.diagnostic,
        },
        accountDir,
        publicSpawnLimits: applyEnvOverride(brand.fields.publicSpawnLimits, env),
        brandProductName: brand.fields.productName,
    };
}
//# sourceMappingURL=config.js.map