/**
 * user-config.ts — the CLI's user-global ("prefs") config tier.
 *
 * A machine-managed JSON file at an OS-correct location that sits between
 * project config and built-in defaults in the resolution cascade
 * (flag > env > project > user > default). Prefs-only: it never holds
 * secrets. See docs/design-docs/cli-user-config-tier.md.
 */
import envPaths from "env-paths";
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { z } from "zod";
/**
 * Resolve the user-config directory. `AILF_CONFIG_DIR` (absolute or
 * cwd-relative, resolved to absolute) overrides everything — the CI/test
 * escape hatch. Otherwise `env-paths` gives the OS-correct location;
 * `{ suffix: "" }` drops env-paths' default `-nodejs` suffix so the path is
 * `~/.config/ailf`, not `~/.config/ailf-nodejs`.
 */
export function resolveUserConfigDir() {
    const override = process.env.AILF_CONFIG_DIR?.trim();
    if (override)
        return resolve(override);
    return envPaths("ailf", { suffix: "" }).config;
}
/** Absolute path to the user config file. */
export function userConfigFilePath() {
    return resolve(resolveUserConfigDir(), "config.json");
}
export const UserConfigSchema = z
    .object({
    owner: z
        .object({
        team: z.string().min(1).optional(),
        individual: z.string().min(1).optional(),
    })
        .strict()
        .optional(),
    execution: z
        .object({
        apiUrl: z.string().url().optional(),
    })
        .strict()
        .optional(),
})
    .strict();
/**
 * Read + parse + validate the user config. Returns null when the file is
 * absent, and warns + returns null on malformed/invalid content (graceful
 * degradation, mirroring `loadRepoConfig`). Never throws on a bad file — a
 * run must not be blocked by user-tier corruption.
 */
export async function loadUserConfig(filePath = userConfigFilePath()) {
    let text;
    try {
        text = await readFile(filePath, "utf-8");
    }
    catch (err) {
        if (err.code === "ENOENT")
            return null;
        throw err;
    }
    let raw;
    try {
        raw = JSON.parse(text);
    }
    catch {
        console.warn(`  ⚠️  User config at ${filePath} is not valid JSON; ignoring it.`);
        return null;
    }
    const result = UserConfigSchema.safeParse(raw);
    if (!result.success) {
        console.warn(`  ⚠️  User config at ${filePath} failed validation; ignoring it.`);
        return null;
    }
    return result.data;
}
/** The only keys `ailf config set`/`unset` accept. Drives validation + `list`. */
export const SETTABLE_KEYS = [
    "owner.team",
    "owner.individual",
    "execution.apiUrl",
];
/** Read the effective value of a whitelisted key from a loaded config. */
export function getUserConfigValue(config, key) {
    switch (key) {
        case "owner.team":
            return config?.owner?.team;
        case "owner.individual":
            return config?.owner?.individual;
        case "execution.apiUrl":
            return config?.execution?.apiUrl;
    }
}
/**
 * Read the existing file for a write. Unlike `loadUserConfig`, a malformed or
 * invalid file THROWS here — a write must not silently clobber a file a user
 * may be hand-recovering.
 */
function readForWrite(filePath) {
    if (!existsSync(filePath))
        return {};
    let raw;
    try {
        raw = JSON.parse(readFileSync(filePath, "utf-8"));
    }
    catch {
        throw new Error(`User config at ${filePath} is not valid JSON. Fix or remove it before writing.`);
    }
    const result = UserConfigSchema.safeParse(raw);
    if (!result.success) {
        throw new Error(`User config at ${filePath} failed validation. Fix or remove it before writing.`);
    }
    return result.data;
}
/** Atomic write: temp file in the same dir, then rename. Mode 0600. */
function writeAtomic(filePath, config) {
    mkdirSync(resolve(filePath, ".."), { recursive: true });
    const tmp = `${filePath}.${process.pid}.tmp`;
    writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
    renameSync(tmp, filePath);
}
function applyKey(config, key, value) {
    switch (key) {
        case "owner.team":
            config.owner = { ...config.owner, team: value };
            break;
        case "owner.individual":
            config.owner = { ...config.owner, individual: value };
            break;
        case "execution.apiUrl":
            config.execution = { ...config.execution, apiUrl: value };
            break;
    }
}
function deleteKey(config, key) {
    switch (key) {
        case "owner.team":
            if (config.owner)
                delete config.owner.team;
            break;
        case "owner.individual":
            if (config.owner)
                delete config.owner.individual;
            break;
        case "execution.apiUrl":
            if (config.execution)
                delete config.execution.apiUrl;
            break;
    }
    if (config.owner && Object.keys(config.owner).length === 0)
        delete config.owner;
    if (config.execution && Object.keys(config.execution).length === 0) {
        delete config.execution;
    }
}
/** Set one whitelisted key, validate the whole config, write atomically. */
export function setUserConfigKey(key, value, filePath = userConfigFilePath()) {
    const config = readForWrite(filePath);
    applyKey(config, key, value);
    writeAtomic(filePath, UserConfigSchema.parse(config));
}
/** Remove one whitelisted key, write atomically. */
export function unsetUserConfigKey(key, filePath = userConfigFilePath()) {
    const config = readForWrite(filePath);
    deleteKey(config, key);
    writeAtomic(filePath, UserConfigSchema.parse(config));
}
