import { spawn } from "node:child_process";
const SECURITY_CLI = "security";
const KEYCHAIN_ITEM_NOT_FOUND_EXIT_CODE = 44;
export class KeychainStore {
    runCommand;
    service;
    account;
    constructor(input) {
        this.runCommand = input.runCommand ?? runSecurityCommand;
        this.service = input.service.trim();
        this.account = input.account.trim();
        if (this.service.length === 0) {
            throw new Error("Keychain service must not be empty");
        }
        if (this.account.length === 0) {
            throw new Error("Keychain account must not be empty");
        }
    }
    async get() {
        const result = await this.executeSecurityCommand(["find-generic-password", "-s", this.service, "-a", this.account, "-w"], "read secret from macOS Keychain");
        if (getCommandExitCode(result) === 0) {
            return stripTrailingLineBreak(getCommandOutput(result, "stdout"));
        }
        if (isKeychainEntryNotFound(result)) {
            return null;
        }
        throw createSecurityCliFailure("read secret from macOS Keychain", result);
    }
    async set(value) {
        if (value.includes("\n") || value.includes("\r")) {
            throw new Error("Keychain secrets cannot contain line breaks");
        }
        const result = await this.executeSecurityCommand([
            "add-generic-password",
            "-s",
            this.service,
            "-a",
            this.account,
            "-U",
            "-w",
            value
        ], "store secret in macOS Keychain");
        if (getCommandExitCode(result) !== 0) {
            throw createSecurityCliFailure("store secret in macOS Keychain", result);
        }
    }
    async delete() {
        const result = await this.executeSecurityCommand(["delete-generic-password", "-s", this.service, "-a", this.account], "delete secret from macOS Keychain");
        if (getCommandExitCode(result) === 0 || isKeychainEntryNotFound(result)) {
            return;
        }
        throw createSecurityCliFailure("delete secret from macOS Keychain", result);
    }
    async executeSecurityCommand(args, operation, options) {
        try {
            if (options === undefined) {
                return await this.runCommand(SECURITY_CLI, args);
            }
            return await this.runCommand(SECURITY_CLI, args, options);
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            throw new Error(`Failed to ${operation}: ${message}`);
        }
    }
}
function runSecurityCommand(command, args, options) {
    return new Promise((resolve) => {
        const child = spawn(command, args, {
            stdio: [options?.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"]
        });
        let stdout = "";
        let stderr = "";
        let stdinErrorMessage;
        const appendStderr = (message) => {
            stderr = stderr.length === 0
                ? message
                : `${stderr}${stderr.endsWith("\n") ? "" : "\n"}${message}`;
        };
        const appendStdinError = () => {
            if (stdinErrorMessage === undefined) {
                return;
            }
            appendStderr(stdinErrorMessage);
            stdinErrorMessage = undefined;
        };
        child.stdout?.setEncoding("utf8");
        child.stdout?.on("data", (chunk) => {
            stdout += chunk.toString();
        });
        child.stderr?.setEncoding("utf8");
        child.stderr?.on("data", (chunk) => {
            stderr += chunk.toString();
        });
        if (options?.stdin !== undefined) {
            child.stdin?.once("error", (error) => {
                stdinErrorMessage = error instanceof Error ? error.message : String(error);
            });
            child.stdin?.end(options.stdin);
        }
        child.on("error", (error) => {
            const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
            appendStdinError();
            appendStderr(message);
            resolve({
                stdout,
                stderr,
                exitCode: 127
            });
        });
        child.on("close", (code) => {
            appendStdinError();
            resolve({
                stdout,
                stderr,
                exitCode: code ?? 1
            });
        });
    });
}
function stripTrailingLineBreak(value) {
    if (value.endsWith("\r\n")) {
        return value.slice(0, -2);
    }
    if (value.endsWith("\n") || value.endsWith("\r")) {
        return value.slice(0, -1);
    }
    return value;
}
function isKeychainEntryNotFound(result) {
    return getCommandExitCode(result) === KEYCHAIN_ITEM_NOT_FOUND_EXIT_CODE;
}
function createSecurityCliFailure(operation, result) {
    const exitCode = getCommandExitCode(result);
    const details = getCommandOutput(result, "stderr").trim()
        || getCommandOutput(result, "stdout").trim();
    if (details) {
        return new Error(`Failed to ${operation}: security exited with code ${exitCode}: ${details}`);
    }
    return new Error(`Failed to ${operation}: security exited with code ${exitCode}`);
}
function getCommandExitCode(result) {
    const value = getOwnEntry(result, "exitCode");
    return typeof value === "number" && Number.isInteger(value) ? value : 1;
}
function getCommandOutput(result, key) {
    const value = getOwnEntry(result, key);
    return typeof value === "string" ? value : "";
}
function getOwnEntry(record, key) {
    return Object.prototype.hasOwnProperty.call(record, key)
        ? record[key]
        : undefined;
}
