import { Buffer } from "node:buffer";
import { timingSafeEqual } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { DAEMON_CONFIG_DIR, DAEMON_CONFIG_FILENAME, DAEMON_PORT_DEFAULT } from "./constants.js";
function trimDaemonToken(raw) {
    const token = raw.trim();
    if (!token)
        throw new Error("Missing token");
    return token;
}
function resolveHomeDir(env) {
    const home = env.HOME?.trim() || env.USERPROFILE?.trim();
    if (!home)
        throw new Error("Missing HOME (required for daemon config)");
    return home;
}
export function resolveDaemonConfigPath(env) {
    const home = resolveHomeDir(env);
    return path.join(home, DAEMON_CONFIG_DIR, DAEMON_CONFIG_FILENAME);
}
export function normalizeDaemonToken(raw) {
    const token = trimDaemonToken(raw);
    if (token.length < 16)
        throw new Error("Token too short (expected >= 16 chars)");
    return token;
}
export function normalizeDaemonTokens(raw) {
    if (!Array.isArray(raw))
        throw new Error("Missing tokens");
    const seen = new Set();
    const tokens = [];
    for (const item of raw) {
        if (typeof item !== "string")
            continue;
        const normalized = normalizeDaemonToken(item);
        if (seen.has(normalized))
            continue;
        seen.add(normalized);
        tokens.push(normalized);
    }
    if (tokens.length === 0)
        throw new Error("Missing tokens");
    return tokens;
}
export function daemonConfigPrimaryToken(config) {
    if (config.version === 2)
        return trimDaemonToken(config.token);
    return trimDaemonToken(config.token);
}
export function daemonConfigTokens(config) {
    if (config.version === 2) {
        const tokens = Array.from(new Set(config.tokens
            .filter((token) => typeof token === "string")
            .map((token) => trimDaemonToken(token))));
        const primary = trimDaemonToken(config.token);
        return tokens.includes(primary) ? tokens : [primary, ...tokens];
    }
    return [trimDaemonToken(config.token)];
}
/**
 * Constant-time check that `candidate` matches one of the configured tokens.
 *
 * `Array.prototype.includes` short-circuits on `===` (both per-token and
 * per-character), so the time it takes to reject a guess leaks how much of a
 * configured token matched. We compare every configured token, using
 * `crypto.timingSafeEqual` for each, so the running time depends only on the
 * number of configured tokens — never on the candidate's content.
 */
export function isAuthorizedDaemonToken(candidate, tokens) {
    if (typeof candidate !== "string" || candidate.length === 0)
        return false;
    const candidateBuf = Buffer.from(candidate, "utf8");
    let matched = false;
    for (const token of tokens) {
        if (typeof token !== "string" || token.length === 0)
            continue;
        const tokenBuf = Buffer.from(token, "utf8");
        // timingSafeEqual requires equal-length buffers; mismatched length is
        // already not a match, but we still iterate every token to keep the work
        // independent of which (if any) entry matches.
        if (tokenBuf.length !== candidateBuf.length)
            continue;
        if (timingSafeEqual(tokenBuf, candidateBuf))
            matched = true;
    }
    return matched;
}
export function normalizeDaemonPort(raw) {
    const port = typeof raw === "number" ? raw : DAEMON_PORT_DEFAULT;
    if (!Number.isFinite(port) || port <= 0 || port > 65535) {
        throw new Error(`Invalid port: ${String(raw)}`);
    }
    return Math.floor(port);
}
export async function readDaemonConfig({ env, }) {
    const configPath = resolveDaemonConfigPath(env);
    let text;
    try {
        text = await fs.readFile(configPath, "utf8");
    }
    catch {
        return null;
    }
    let parsed;
    try {
        parsed = JSON.parse(text);
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        throw new Error(`Invalid daemon config JSON at ${configPath}: ${message}`);
    }
    if (!parsed || typeof parsed !== "object") {
        throw new Error(`Invalid daemon config at ${configPath}: expected object`);
    }
    const obj = parsed;
    const version = typeof obj.version === "number" ? obj.version : Number.NaN;
    if (version !== 1 && version !== 2) {
        throw new Error(`Invalid daemon config at ${configPath}: version`);
    }
    const port = normalizeDaemonPort(typeof obj.port === "number" ? obj.port : DAEMON_PORT_DEFAULT);
    const envRaw = obj.env && typeof obj.env === "object" ? obj.env : {};
    const envSnapshot = {};
    for (const [k, v] of Object.entries(envRaw)) {
        if (typeof v !== "string")
            continue;
        const trimmed = v.trim();
        if (!trimmed)
            continue;
        envSnapshot[k] = trimmed;
    }
    const installedAt = typeof obj.installedAt === "string" ? obj.installedAt : new Date().toISOString();
    if (version === 1) {
        const tokenRaw = typeof obj.token === "string" ? obj.token : "";
        const token = normalizeDaemonToken(tokenRaw);
        return { version: 2, token, tokens: [token], port, env: envSnapshot, installedAt };
    }
    const tokens = normalizeDaemonTokens(Array.isArray(obj.tokens) ? obj.tokens : typeof obj.token === "string" ? [obj.token] : []);
    const token = typeof obj.token === "string" && obj.token.trim().length > 0
        ? normalizeDaemonToken(obj.token)
        : tokens[0];
    return {
        version: 2,
        token: tokens.includes(token) ? token : tokens[0],
        tokens: tokens.includes(token) ? tokens : [token, ...tokens],
        port,
        env: envSnapshot,
        installedAt,
    };
}
export async function writeDaemonConfig({ env, config, }) {
    const configPath = resolveDaemonConfigPath(env);
    const dir = path.dirname(configPath);
    await fs.mkdir(dir, { recursive: true, mode: 0o700 });
    await fs.chmod(dir, 0o700).catch(() => {
        // Best effort: Windows and some filesystems do not support POSIX modes.
    });
    await fs.chmod(configPath, 0o600).catch(() => {
        // Tighten existing files before rewriting secrets; ignore first install.
    });
    const primaryToken = normalizeDaemonToken(config.token);
    const tokens = normalizeDaemonTokens(Array.isArray(config.tokens) ? [primaryToken, ...config.tokens] : [primaryToken]);
    const payload = {
        version: 2,
        token: primaryToken,
        tokens,
        port: normalizeDaemonPort(config.port),
        env: config.env ?? {},
        installedAt: config.installedAt ?? new Date().toISOString(),
    };
    await fs.writeFile(configPath, `${JSON.stringify(payload, null, 2)}\n`, {
        encoding: "utf8",
        mode: 0o600,
    });
    await fs.chmod(configPath, 0o600).catch(() => {
        // Best effort: Windows and some filesystems do not support POSIX modes.
    });
    return configPath;
}
//# sourceMappingURL=config.js.map