import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import * as crypto from "node:crypto";
import { execSync } from "node:child_process";
function getMachineId() {
    // macOS: IOPlatformUUID
    if (process.platform === "darwin") {
        try {
            const out = execSync("ioreg -rd1 -c IOPlatformExpertDevice | awk '/IOPlatformUUID/{print $3}'", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
            const id = out.trim().replace(/"/g, "");
            if (id)
                return id;
        }
        catch { /* fall through */ }
    }
    // Linux: /etc/machine-id or /var/lib/dbus/machine-id
    if (process.platform === "linux") {
        for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
            try {
                const id = fs.readFileSync(p, "utf-8").trim();
                if (id)
                    return id;
            }
            catch { /* continue */ }
        }
    }
    // Fallback: hostname (stable enough for local dev tool)
    return os.hostname();
}
export class MachineKeyStore {
    filePath;
    key;
    cache = null;
    constructor(filePath) {
        this.filePath = filePath;
        const machineId = getMachineId();
        const username = os.userInfo().username;
        const salt = Buffer.from("agentgazer-machine-key-v1");
        this.key = crypto.scryptSync(`${machineId}:${username}`, salt, 32, { N: 16384, r: 8, p: 1 });
    }
    async isAvailable() {
        return true;
    }
    async get(service, account) {
        const data = this.load();
        return data[service]?.[account] ?? null;
    }
    async set(service, account, value) {
        const data = this.load();
        if (!data[service]) {
            data[service] = {};
        }
        data[service][account] = value;
        this.save(data);
    }
    async delete(service, account) {
        const data = this.load();
        if (data[service]) {
            delete data[service][account];
            if (Object.keys(data[service]).length === 0) {
                delete data[service];
            }
        }
        this.save(data);
    }
    async list(service) {
        const data = this.load();
        return Object.keys(data[service] ?? {});
    }
    load() {
        if (this.cache)
            return this.cache;
        if (!fs.existsSync(this.filePath)) {
            this.cache = {};
            return this.cache;
        }
        const raw = fs.readFileSync(this.filePath, "utf-8");
        let envelope;
        try {
            envelope = JSON.parse(raw);
        }
        catch {
            throw new Error("Corrupted secrets file: invalid JSON envelope.");
        }
        if (!envelope.iv || !envelope.tag || !envelope.ciphertext) {
            throw new Error("Corrupted secrets file: missing encryption fields.");
        }
        try {
            const iv = Buffer.from(envelope.iv, "hex");
            const tag = Buffer.from(envelope.tag, "hex");
            const ciphertext = Buffer.from(envelope.ciphertext, "hex");
            const decipher = crypto.createDecipheriv("aes-256-gcm", this.key, iv);
            decipher.setAuthTag(tag);
            const decrypted = Buffer.concat([
                decipher.update(ciphertext),
                decipher.final(),
            ]);
            this.cache = JSON.parse(decrypted.toString("utf-8"));
        }
        catch {
            throw new Error("Failed to decrypt secrets file. The machine identity or user may have changed.");
        }
        return this.cache;
    }
    save(data) {
        this.cache = data;
        // Ensure directory exists
        const dir = path.dirname(this.filePath);
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir, { recursive: true });
        }
        const iv = crypto.randomBytes(12);
        const cipher = crypto.createCipheriv("aes-256-gcm", this.key, iv);
        const plaintext = Buffer.from(JSON.stringify(data), "utf-8");
        const ciphertext = Buffer.concat([
            cipher.update(plaintext),
            cipher.final(),
        ]);
        const tag = cipher.getAuthTag();
        const envelope = {
            version: 1,
            iv: iv.toString("hex"),
            tag: tag.toString("hex"),
            ciphertext: ciphertext.toString("hex"),
        };
        fs.writeFileSync(this.filePath, JSON.stringify(envelope, null, 2), {
            encoding: "utf-8",
            mode: 0o600,
        });
    }
}
// ---------------------------------------------------------------------------
// KeychainStore (macOS)
// ---------------------------------------------------------------------------
export class KeychainStore {
    async isAvailable() {
        if (process.platform !== "darwin")
            return false;
        // SSH sessions can't interact with Keychain even if WindowServer is running
        if (process.env.SSH_CLIENT || process.env.SSH_CONNECTION || process.env.SSH_TTY) {
            return false;
        }
        try {
            // Check if WindowServer is running (indicates GUI session)
            execSync("pgrep -x WindowServer", { stdio: "pipe" });
            return true;
        }
        catch {
            return false;
        }
    }
    async get(service, account) {
        try {
            const result = execSync(`/usr/bin/security find-generic-password -s ${shellEscape(service)} -a ${shellEscape(account)} -w`, { stdio: ["pipe", "pipe", "pipe"], encoding: "utf-8" });
            return result.trim();
        }
        catch {
            return null;
        }
    }
    async set(service, account, value) {
        // Delete existing entry first (add-generic-password fails if it exists)
        try {
            execSync(`/usr/bin/security delete-generic-password -s ${shellEscape(service)} -a ${shellEscape(account)}`, { stdio: "pipe" });
        }
        catch {
            // OK if it doesn't exist
        }
        // Use spawnSync with stdin to avoid shell escaping issues with special characters in the password
        const { spawnSync } = await import("node:child_process");
        const result = spawnSync("/usr/bin/security", [
            "add-generic-password",
            "-s", service,
            "-a", account,
            "-w", value,
            "-U"
        ], { stdio: "pipe" });
        if (result.status !== 0) {
            const stderr = result.stderr?.toString() || "Unknown error";
            throw new Error(`Command failed: /usr/bin/security add-generic-password: ${stderr}`);
        }
    }
    async delete(service, account) {
        try {
            execSync(`/usr/bin/security delete-generic-password -s ${shellEscape(service)} -a ${shellEscape(account)}`, { stdio: "pipe" });
        }
        catch {
            // OK if it doesn't exist
        }
    }
    async list(service) {
        try {
            // Dump keychain and parse for entries matching this service
            const dump = execSync(`/usr/bin/security dump-keychain`, { stdio: ["pipe", "pipe", "pipe"], encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
            const accounts = [];
            // Split by entry boundaries and find entries with matching svce
            const entries = dump.split(/keychain:/);
            for (const entry of entries) {
                // Check if this entry has our service
                const svceMatch = entry.match(/"svce"<blob>="([^"]+)"/);
                if (svceMatch && svceMatch[1] === service) {
                    const acctMatch = entry.match(/"acct"<blob>="([^"]+)"/);
                    if (acctMatch) {
                        accounts.push(acctMatch[1]);
                    }
                }
            }
            return accounts;
        }
        catch {
            return [];
        }
    }
}
// ---------------------------------------------------------------------------
// LibsecretStore (Linux)
// ---------------------------------------------------------------------------
export class LibsecretStore {
    async isAvailable() {
        if (process.platform !== "linux")
            return false;
        try {
            // Actually attempt a lookup — will fail without D-Bus session
            execSync("secret-tool lookup service com.agentgazer.probe 2>/dev/null", { stdio: "pipe", timeout: 3000 });
            return true;
        }
        catch (err) {
            // Exit code 1 = "not found" (but D-Bus works) → available
            // Other errors (no D-Bus, timeout) → not available
            const exitCode = err.status;
            return exitCode === 1;
        }
    }
    async get(service, account) {
        try {
            const result = execSync(`secret-tool lookup service ${shellEscape(service)} account ${shellEscape(account)}`, { stdio: ["pipe", "pipe", "pipe"], encoding: "utf-8" });
            return result.trim() || null;
        }
        catch {
            return null;
        }
    }
    async set(service, account, value) {
        try {
            execSync(`echo -n ${shellEscape(value)} | secret-tool store --label=${shellEscape(`${service}:${account}`)} service ${shellEscape(service)} account ${shellEscape(account)}`, { stdio: "pipe" });
        }
        catch (err) {
            throw new Error(`Failed to store secret via secret-tool: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    async delete(service, account) {
        try {
            execSync(`secret-tool clear service ${shellEscape(service)} account ${shellEscape(account)}`, { stdio: "pipe" });
        }
        catch {
            // OK if it doesn't exist
        }
    }
    async list(service) {
        try {
            const result = execSync(`secret-tool search service ${shellEscape(service)}`, { stdio: ["pipe", "pipe", "pipe"], encoding: "utf-8" });
            const accounts = [];
            const lines = result.split("\n");
            for (const line of lines) {
                const match = line.match(/attribute\.account\s*=\s*(.+)/);
                if (match) {
                    accounts.push(match[1].trim());
                }
            }
            return accounts;
        }
        catch {
            return [];
        }
    }
}
// ---------------------------------------------------------------------------
// Backend auto-detection
// ---------------------------------------------------------------------------
export async function detectSecretStore(configDir) {
    // 1. Environment variable override
    const override = process.env.AGENTGAZER_SECRET_BACKEND;
    if (override) {
        return createBackend(override, configDir);
    }
    // 2. macOS Keychain (GUI session)
    if (process.platform === "darwin") {
        const keychain = new KeychainStore();
        if (await keychain.isAvailable()) {
            return { store: keychain, backendName: "keychain" };
        }
    }
    // 3. Linux libsecret
    if (process.platform === "linux") {
        const libsecret = new LibsecretStore();
        if (await libsecret.isAvailable()) {
            return { store: libsecret, backendName: "libsecret" };
        }
    }
    // 4. Machine-key encrypted file (SSH, headless, Docker, tty)
    return createMachineKeyBackend(configDir);
}
function createBackend(name, configDir) {
    switch (name) {
        case "keychain":
            return { store: new KeychainStore(), backendName: "keychain" };
        case "libsecret":
            return { store: new LibsecretStore(), backendName: "libsecret" };
        case "machine":
        case "file": // backwards compat alias
            return createMachineKeyBackend(configDir);
        default:
            throw new Error(`Unknown secret backend: "${name}". Valid values: keychain, libsecret, machine`);
    }
}
function createMachineKeyBackend(configDir) {
    const filePath = path.join(configDir, "secrets.enc");
    return {
        store: new MachineKeyStore(filePath),
        backendName: "machine-key",
    };
}
// ---------------------------------------------------------------------------
// Migration from plaintext config
// ---------------------------------------------------------------------------
export async function migrateFromPlaintextConfig(configPath, store) {
    if (!fs.existsSync(configPath))
        return 0;
    let config;
    try {
        config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
    }
    catch {
        return 0;
    }
    if (!config.providers)
        return 0;
    let migrated = 0;
    for (const [name, providerConfig] of Object.entries(config.providers)) {
        if (providerConfig.apiKey && typeof providerConfig.apiKey === "string") {
            // Write to secret store
            await store.set(PROVIDER_SERVICE, name, providerConfig.apiKey);
            // Remove from config
            delete providerConfig.apiKey;
            migrated++;
        }
    }
    if (migrated > 0) {
        fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
    }
    return migrated;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
export const PROVIDER_SERVICE = "com.agentgazer.provider";
export async function loadProviderKeys(store) {
    const accounts = await store.list(PROVIDER_SERVICE);
    const keys = {};
    for (const account of accounts) {
        const value = await store.get(PROVIDER_SERVICE, account);
        if (value) {
            keys[account] = value;
        }
    }
    return keys;
}
function shellEscape(s) {
    return "'" + s.replace(/'/g, "'\\''") + "'";
}
//# sourceMappingURL=secret-store.js.map