import Foundation
import Security

// MARK: - Managed Git credentials
//
// An app-level choice of how Nucleic's `git` / `gh` reach GitHub: nothing, an HTTPS personal
// access token, a user-supplied SSH key (optionally passphrase-protected, and optionally used to
// sign commits too), or a Nucleic-owned SSH key Nucleic generates and never reveals. This one
// credential serves two consumers: Nucleic's own *host-side* git (cloning a project, pushing a
// branch, opening a PR — see `provisionHost`), pinned so the host never falls back to the user's
// personal `~/.ssh` keys; and, when "Make available in sandbox" is on (always for Nucleic Control,
// whose shared container has no other way out), a sandboxed session's git/gh. The *mode* and the
// flags live in `UserDefaults`; the actual secrets (token, private key, passphrase) live in the
// login Keychain, never in defaults and never on a command line.
//
// Sandbox provisioning is host-side and mirrors `SessionController.exportLoginCredentials`: the
// secret files are written into the session's bind-mounted claude-home (0600), and the
// container's git/ssh are pointed at them via environment variables. Nothing is `container
// exec`'d in; the files refresh each turn, so changing the secret takes effect on the next
// turn without recreating the container.

/// How Nucleic authenticates to GitHub. App-level — one credential for host git and sandboxes.
public enum GitHubAuthMode: String, Sendable, Codable, CaseIterable, Identifiable {
    /// No credentials injected — git/gh reach only public repos (today's behavior).
    case none
    /// An HTTPS personal access token, injected as a git credential for github.com and as
    /// `GITHUB_TOKEN`/`GH_TOKEN` for the `gh` CLI.
    case token
    /// A user-supplied SSH private key, injected at a known path with `GIT_SSH_COMMAND` pointed at
    /// it. May be passphrase-protected (the passphrase is unlocked non-interactively via askpass).
    case ssh
    /// An SSH key Nucleic generates and owns: the private half lives only in the login Keychain and
    /// is never shown to the user (only the public key can be exported). Provisioned exactly like
    /// `.ssh` — same `GIT_SSH_COMMAND` / signing — but with no passphrase, since Nucleic mints it.
    case managed

    public var id: String { rawValue }
}

/// App-wide GitHub-credential preferences persisted in `UserDefaults`. Keys (and the
/// Keychain accounts) live here so `NucleicCore` and the SwiftUI settings layer agree on
/// them. The secrets themselves are in `GitHubCredentialStore` (Keychain).
public enum GitHubCredentialSettings {
    /// Selected `GitHubAuthMode` (raw value). Absent/invalid → `.none`.
    public static let authModeKey = "nucleic.github.authMode"
    /// Whether to SSH-sign commits with the configured SSH key. Only meaningful in `.ssh` mode.
    public static let signCommitsKey = "nucleic.github.signCommitsSSH"
    /// Force the `gh` CLI to use SSH for git operations (`gh config set git_protocol ssh`). Only
    /// meaningful in `.ssh` mode (it needs the injected key). Off by default.
    public static let ghForceSSHKey = "nucleic.github.ghForceSSH"
    /// Restrict `git` to SSH for github.com — rewrite `https://github.com/` remotes to SSH so even
    /// an HTTPS remote pushes/fetches over the injected key. Only meaningful in `.ssh`/`.managed`
    /// mode. Off by default.
    public static let gitForceSSHKey = "nucleic.github.gitForceSSH"
    /// The OpenSSH public key for the Nucleic-managed SSH key (`.managed` mode). Public, so it lives
    /// in `UserDefaults` for the UI to display/export; the matching private key is in the Keychain
    /// (`GitHubCredentialStore.managedPrivateKey`). Empty/absent → no managed key has been generated.
    public static let managedPublicKeyKey = "nucleic.github.managedPublicKey"
    /// Whether the Managed Git credential is also provisioned into sandboxes. The credential is
    /// always used for Nucleic's own host-side git (cloning, pushing, PRs); this governs whether it's
    /// *also* injected into a sandboxed session's `git`/`gh`. Nucleic Control sessions always get it
    /// (their shared container has no other way to reach GitHub), so this only affects per-session
    /// sandboxes. Absent → `true`, preserving the prior always-provision behavior.
    public static let availableInSandboxKey = "nucleic.github.availableInSandbox"
    /// Extra OpenSSH config applied to Managed Git's SSH connections to GitHub (host and sandbox
    /// alike). Managed Git ignores the user's real `~/.ssh/config` to stay off their keys and agent,
    /// which also drops any connectivity directives there — so this lets the user re-supply just the
    /// connectivity they need (a custom `Hostname`/`Port`, a `ProxyCommand`, etc.) without reopening
    /// the door to their identities. Plain text, not a secret; empty/absent → none.
    public static let sshConfigKey = "nucleic.github.sshConfig"

    public static var authMode: GitHubAuthMode {
        GitHubAuthMode(rawValue: UserDefaults.standard.string(forKey: authModeKey) ?? "") ?? .none
    }

    public static var signCommitsWithSSHKey: Bool {
        UserDefaults.standard.bool(forKey: signCommitsKey)
    }

    public static var ghForceSSH: Bool { UserDefaults.standard.bool(forKey: ghForceSSHKey) }
    public static var gitForceSSH: Bool { UserDefaults.standard.bool(forKey: gitForceSSHKey) }

    /// Whether to provision the Managed Git credential into a (per-session) sandbox. Defaults to
    /// `true` when unset — a fresh install behaves as before, injecting the credential into sandboxes.
    public static var availableInSandbox: Bool {
        UserDefaults.standard.object(forKey: availableInSandboxKey) as? Bool ?? true
    }

    /// The user's extra OpenSSH config for Managed Git, or `nil` when none is set.
    public static var sshConfig: String? {
        let v = (UserDefaults.standard.string(forKey: sshConfigKey) ?? "")
            .trimmingCharacters(in: .whitespacesAndNewlines)
        return v.isEmpty ? nil : v
    }

    /// The stored Nucleic-managed public key, or `nil` when none has been generated.
    public static var managedPublicKey: String? {
        let v = UserDefaults.standard.string(forKey: managedPublicKeyKey) ?? ""
        return v.isEmpty ? nil : v
    }
}

/// Stores the GitHub secrets (HTTPS token, SSH private key) in the login Keychain, following
/// the same generic-password pattern as `HostIdentityStore`. Secrets are kept out of
/// `UserDefaults` and never logged or passed on a command line.
public enum GitHubCredentialStore {
    private static let tokenAccount = "com.nucleic.github.token"
    private static let sshKeyAccount = "com.nucleic.github.sshPrivateKey"
    private static let sshPassphraseAccount = "com.nucleic.github.sshPassphrase"
    private static let managedKeyAccount = "com.nucleic.github.managedSSHPrivateKey"

    public static func loadToken() -> String? { readString(tokenAccount) }
    public static func saveToken(_ value: String) { write(tokenAccount, value) }
    public static func deleteToken() { delete(tokenAccount) }

    public static func loadSSHPrivateKey() -> String? { readString(sshKeyAccount) }
    public static func saveSSHPrivateKey(_ value: String) { write(sshKeyAccount, value) }
    public static func deleteSSHPrivateKey() { delete(sshKeyAccount) }

    /// Passphrase that unlocks the user-supplied `.ssh` private key, when it's encrypted. Kept in
    /// the Keychain alongside the key; the sandbox unlocks the key non-interactively from it.
    public static func loadSSHPassphrase() -> String? { readString(sshPassphraseAccount) }
    public static func saveSSHPassphrase(_ value: String) { write(sshPassphraseAccount, value) }
    public static func deleteSSHPassphrase() { delete(sshPassphraseAccount) }

    /// The private half of the Nucleic-managed SSH key (`.managed` mode). Written only by
    /// `ManagedSSHKey.generate()`; never returned to the UI, only used to provision the sandbox.
    public static func loadManagedPrivateKey() -> String? { readString(managedKeyAccount) }
    public static func saveManagedPrivateKey(_ value: String) { write(managedKeyAccount, value) }
    public static func deleteManagedPrivateKey() { delete(managedKeyAccount) }

    // MARK: Keychain primitives

    private static func readString(_ account: String) -> String? {
        guard let data = read(account) else { return nil }
        return String(data: data, encoding: .utf8)
    }

    private static func read(_ account: String) -> Data? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne,
        ]
        var item: CFTypeRef?
        guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess else { return nil }
        return item as? Data
    }

    private static func write(_ account: String, _ value: String) {
        delete(account)
        let base: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
            kSecValueData as String: Data(value.utf8),
        ]
        // Preferred path: store the item with an access list that names *this* app as the only
        // trusted application, so the app reads it back silently. On the macOS login Keychain the
        // item's ACL is what decides between a silent read and the "<app> wants to use your
        // confidential information stored in <item>" authorization panel — a trusted app reads with
        // no prompt, any other process prompts. Nucleic reads the managed Git key host-side when it
        // provisions a (control) container, so owning the item removes that panel while still gating
        // the secret against unrelated processes.
        if let access = appOwnedAccess() {
            var owned = base
            owned[kSecAttrAccess as String] = access
            if SecItemAdd(owned as CFDictionary, nil) == errSecSuccess { return }
            // The ACL-bearing add was rejected — fall through and store the secret with the plain
            // accessibility attribute (the prior behavior) rather than drop it on the floor.
        }
        var plain = base
        plain[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
        SecItemAdd(plain as CFDictionary, nil)
    }

    private static func delete(_ account: String) {
        SecItemDelete([
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
        ] as CFDictionary)
    }

    /// An access object naming the running app as the only trusted application — the item it's
    /// attached to is read back by this app without an authorization prompt, and gated behind one
    /// for everything else. Returns `nil` if the platform can't vend the access object, in which
    /// case the caller stores the item un-owned (preserving the prior prompt-on-access behavior)
    /// rather than failing the write. Uses the legacy `SecAccess` API because it is the only way to
    /// set a login-Keychain ACL; it's deprecated but still current on the macOS 26 floor.
    private static func appOwnedAccess() -> SecAccess? {
        var trustedApp: SecTrustedApplication?
        // A nil path means "the application calling this function" — i.e. Nucleic itself.
        guard SecTrustedApplicationCreateFromPath(nil, &trustedApp) == errSecSuccess,
              let trustedApp
        else { return nil }
        var access: SecAccess?
        guard SecAccessCreate(
                "Nucleic Git credential" as CFString,
                [trustedApp] as CFArray,
                &access) == errSecSuccess
        else { return nil }
        return access
    }
}

/// A Nucleic-owned SSH key (`.managed` mode): Nucleic mints an ed25519 keypair with `ssh-keygen`,
/// keeps the *private* half in the login Keychain (never shown, only ever provisioned into the
/// sandbox), and keeps the *public* half in `UserDefaults` for the UI to display/export. The user
/// adds the public key to GitHub as an authentication (and, when signing is on, signing) key.
public enum ManagedSSHKey {
    /// Comment baked into the generated key so it's recognizable in GitHub's key list.
    public static let comment = "nucleic-managed"

    /// The exported public key, or `nil` when no managed key has been generated.
    public static var publicKey: String? { GitHubCredentialSettings.managedPublicKey }

    /// True when both halves are present (a complete, usable managed key).
    public static var exists: Bool {
        publicKey != nil && !(GitHubCredentialStore.loadManagedPrivateKey() ?? "").isEmpty
    }

    /// The private key, for provisioning only. Intentionally not surfaced to the UI layer.
    static var privateKey: String? { GitHubCredentialStore.loadManagedPrivateKey() }

    /// Generate a fresh ed25519 keypair, replacing any existing managed key. Stores the private
    /// half in the Keychain and the public half in `UserDefaults`, and returns the public key.
    /// Runs host-side `ssh-keygen` against a 0700 temp dir that's removed before returning, so the
    /// private key never lingers on disk outside the Keychain.
    @discardableResult
    public static func generate() throws -> String {
        let fm = FileManager.default
        let dir = (NSTemporaryDirectory() as NSString)
            .appendingPathComponent("nucleic-keygen-\(UUID().uuidString)")
        try fm.createDirectory(
            atPath: dir, withIntermediateDirectories: true,
            attributes: [.posixPermissions: 0o700])
        defer { try? fm.removeItem(atPath: dir) }
        let keyPath = (dir as NSString).appendingPathComponent("id_ed25519")

        let proc = Process()
        proc.executableURL = URL(fileURLWithPath: "/usr/bin/ssh-keygen")
        // -N "" → no passphrase (Nucleic owns the key; the sandbox uses it non-interactively).
        proc.arguments = ["-t", "ed25519", "-N", "", "-C", comment, "-f", keyPath]
        let pipe = Pipe()
        proc.standardOutput = pipe
        proc.standardError = pipe
        try proc.run()
        proc.waitUntilExit()
        guard proc.terminationStatus == 0 else {
            let out = String(
                data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
            throw ManagedSSHKeyError.keygenFailed(out.trimmingCharacters(in: .whitespacesAndNewlines))
        }

        let priv = try String(contentsOfFile: keyPath, encoding: .utf8)
        let pub = try String(contentsOfFile: keyPath + ".pub", encoding: .utf8)
            .trimmingCharacters(in: .whitespacesAndNewlines)
        GitHubCredentialStore.saveManagedPrivateKey(priv)
        UserDefaults.standard.set(pub, forKey: GitHubCredentialSettings.managedPublicKeyKey)
        return pub
    }

    /// Forget the managed key entirely (both halves).
    public static func delete() {
        GitHubCredentialStore.deleteManagedPrivateKey()
        UserDefaults.standard.removeObject(forKey: GitHubCredentialSettings.managedPublicKeyKey)
    }
}

public enum ManagedSSHKeyError: Error, LocalizedError {
    case keygenFailed(String)
    public var errorDescription: String? {
        switch self {
        case .keygenFailed(let detail):
            return detail.isEmpty ? "ssh-keygen failed." : "ssh-keygen failed: \(detail)"
        }
    }
}

/// A resolved snapshot of the app-level GitHub credential config — the mode, the secret, and
/// the sign-commits flag — ready to provision into a sandbox. Kept separate from the global
/// stores so the provisioning logic is pure and unit-testable.
public struct GitHubCredentialConfig: Sendable, Equatable {
    public var mode: GitHubAuthMode
    public var token: String?
    public var sshPrivateKey: String?
    /// Passphrase for `sshPrivateKey` when it's encrypted (`.ssh` mode only). `nil`/empty → the key
    /// is unencrypted. Managed keys are always passphrase-free.
    public var sshPassphrase: String?
    public var signCommits: Bool
    /// Force `gh` to use SSH for git operations (`git_protocol ssh`). Applied only in `.ssh` mode.
    public var ghForceSSH: Bool
    /// Rewrite github.com HTTPS remotes to SSH for `git`. Applied only in `.ssh` mode.
    public var gitForceSSH: Bool
    /// Extra OpenSSH config (Hostname/Port/ProxyCommand…) for Managed Git's SSH connections, or
    /// `nil`/empty for none. Applied in `.ssh`/`.managed` mode: written to a file `ssh -F` points at,
    /// so connectivity directives are honored while identity/agent stay pinned on the command line.
    public var sshConfig: String?

    public init(
        mode: GitHubAuthMode,
        token: String? = nil,
        sshPrivateKey: String? = nil,
        sshPassphrase: String? = nil,
        signCommits: Bool = false,
        ghForceSSH: Bool = false,
        gitForceSSH: Bool = false,
        sshConfig: String? = nil
    ) {
        self.mode = mode
        self.token = token
        self.sshPrivateKey = sshPrivateKey
        self.sshPassphrase = sshPassphrase
        self.signCommits = signCommits
        self.ghForceSSH = ghForceSSH
        self.gitForceSSH = gitForceSSH
        self.sshConfig = sshConfig
    }

    /// Read the live app-level config from `UserDefaults` + the Keychain. The SSH secret is sourced
    /// from whichever slot the active mode uses: the user's key (+ passphrase) for `.ssh`, the
    /// Nucleic-owned key for `.managed`.
    public static func current() -> GitHubCredentialConfig {
        let mode = GitHubCredentialSettings.authMode
        let sshKey: String?
        let passphrase: String?
        switch mode {
        case .ssh:
            sshKey = GitHubCredentialStore.loadSSHPrivateKey()
            passphrase = GitHubCredentialStore.loadSSHPassphrase()
        case .managed:
            sshKey = ManagedSSHKey.privateKey
            passphrase = nil  // Nucleic-minted keys are passphrase-free.
        case .none, .token:
            sshKey = nil
            passphrase = nil
        }
        return GitHubCredentialConfig(
            mode: mode,
            token: GitHubCredentialStore.loadToken(),
            sshPrivateKey: sshKey,
            sshPassphrase: passphrase,
            signCommits: GitHubCredentialSettings.signCommitsWithSSHKey,
            ghForceSSH: GitHubCredentialSettings.ghForceSSH,
            gitForceSSH: GitHubCredentialSettings.gitForceSSH,
            sshConfig: GitHubCredentialSettings.sshConfig)
    }

    /// True when the selected mode actually has a usable secret behind it.
    public var isActive: Bool {
        switch mode {
        case .none: return false
        case .token: return !(token ?? "").isEmpty
        case .ssh, .managed: return !(sshPrivateKey ?? "").isEmpty
        }
    }
}

/// Writes the configured GitHub secret into a sandbox's bind-mounted home and returns the
/// environment variables that point the container's git/ssh (and `gh`) at it. The only side
/// effect is writing the secret file(s) under `hostHomeDir`; everything else is a function of
/// the inputs, so the env construction is unit-testable.
public enum GitHubCredentialProvisioner {
    /// Filenames written into the sandbox home. Dot-prefixed and Nucleic-namespaced so they
    /// never collide with the agent's own dotfiles in the bind-mounted claude-home.
    public static let credentialsFile = ".nucleic-git-credentials"
    public static let sshKeyFile = ".nucleic-github-id"
    public static let knownHostsFile = ".nucleic-known-hosts"
    /// Subdirectory holding a minimal `gh` config (`config.yml` with `git_protocol: ssh`), pointed
    /// at via `GH_CONFIG_DIR` when `ghForceSSH` is on.
    public static let ghConfigDir = ".nucleic-gh"
    /// Holds the SSH key passphrase (0600) and the tiny askpass script (0700) that prints it, used
    /// to unlock a passphrase-protected `.ssh` key non-interactively via OpenSSH's `SSH_ASKPASS`.
    public static let sshPassphraseFile = ".nucleic-ssh-passphrase"
    public static let sshAskpassFile = ".nucleic-ssh-askpass"
    /// The user's extra OpenSSH config for Managed Git, written here and pointed at via `ssh -F`.
    public static let sshConfigFile = ".nucleic-ssh-config"

    /// Provision `config` for a sandbox whose home is `hostHomeDir` on the host (where files
    /// are written) and `containerHomeDir` as seen from inside the container (where the env
    /// vars reference them — equal to the host path when the home is bind-mounted at its
    /// identical path, as for the shared control container). Returns the env to merge into the
    /// container exec environment; `[:]` when nothing is configured.
    @discardableResult
    public static func provision(
        _ config: GitHubCredentialConfig,
        hostHomeDir: String,
        containerHomeDir: String
    ) -> [String: String] {
        guard config.isActive else { return [:] }

        var env: [String: String] = [:]
        // (key, value) pairs injected as git config via GIT_CONFIG_COUNT/KEY_n/VALUE_n — this
        // layers over the container's on-disk gitconfig without ever editing a file there.
        var gitConfig: [(String, String)] = []

        switch config.mode {
        case .none:
            return [:]

        case .token:
            guard let token = config.token, !token.isEmpty else { return [:] }
            let hostPath = (hostHomeDir as NSString).appendingPathComponent(credentialsFile)
            let containerPath = (containerHomeDir as NSString).appendingPathComponent(credentialsFile)
            // `x-access-token` is the conventional username for a token; GitHub accepts the PAT
            // as the password for any username over HTTPS.
            writeSecret("https://x-access-token:\(token)@github.com\n", to: hostPath)
            // `git credential-store --file=<path>` serves the saved credential non-interactively.
            gitConfig.append(("credential.helper", "store --file=\(containerPath)"))
            // `gh` authenticates from the environment for API calls (e.g. `gh pr create`).
            env["GITHUB_TOKEN"] = token
            env["GH_TOKEN"] = token

        case .ssh, .managed:
            // `.ssh` (user-supplied key, maybe passphrase-protected) and `.managed` (Nucleic-owned
            // key, never) provision identically — both arrive here as `config.sshPrivateKey`.
            guard let key = config.sshPrivateKey, !key.isEmpty else { return [:] }
            let hostKey = (hostHomeDir as NSString).appendingPathComponent(sshKeyFile)
            let containerKey = (containerHomeDir as NSString).appendingPathComponent(sshKeyFile)
            let containerKnown = (containerHomeDir as NSString).appendingPathComponent(knownHostsFile)
            // OpenSSH refuses a private key whose file is group/world-readable, so write 0600.
            writeSecret(key.hasSuffix("\n") ? key : key + "\n", to: hostKey)
            // `IdentitiesOnly` forces use of exactly this key; `IdentityAgent=none` keeps ssh off any
            // ssh-agent (so an agent — e.g. 1Password's — is never consulted, whether reached via env
            // or a user `-F` config); `accept-new` trusts GitHub's host key on first connect and
            // records it in a writable known_hosts inside the sandbox — avoids bundling host keys that
            // GitHub periodically rotates.
            // Every path is single-quoted: `GIT_SSH_COMMAND` is word-split by the shell when git
            // invokes it, so an unquoted macOS home (`~/Library/Application Support/…`) would split
            // mid-path — ssh would read a truncated `-i` path and treat the rest as the hostname.
            var sshCommand =
                "ssh -i \(singleQuoted(containerKey)) -o IdentitiesOnly=yes -o IdentityAgent=none"
                + " -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=\(singleQuoted(containerKnown))"
            if let extraConfig = config.sshConfig?.trimmingCharacters(in: .whitespacesAndNewlines),
               !extraConfig.isEmpty {
                // The user's extra ssh config (a custom Hostname/Port, a ProxyCommand…). Written to a
                // file ssh reads via `-F`, so its connectivity directives are honored. Identity/agent
                // are pinned on the command line above, which wins over anything the config sets
                // (first-obtained-value), so a stray `IdentityFile`/`IdentityAgent` here can't divert
                // Managed Git off its own key. Host-side, `-F` also replaces the `-F /dev/null` that
                // would otherwise shut out the user's real `~/.ssh/config`.
                let hostConfig = (hostHomeDir as NSString).appendingPathComponent(sshConfigFile)
                let containerConfig = (containerHomeDir as NSString).appendingPathComponent(sshConfigFile)
                writeSecret(extraConfig.hasSuffix("\n") ? extraConfig : extraConfig + "\n", to: hostConfig)
                sshCommand += " -F \(singleQuoted(containerConfig))"
            } else {
                // No user-supplied ssh config: pin ssh to its command-line options only, ignoring BOTH
                // the container's `/etc/ssh/ssh_config` AND any `~/.ssh/config`. Without this, a
                // `Host github.com` stanza in either — one a tool, `gh`, or the agent itself could write
                // into the bind-mounted home mid-session — would be honored, and a stray `IdentityFile`
                // there is added to ssh's identity list *even under `IdentitiesOnly`* (which only
                // suppresses agent-offered keys, not config/default IdentityFiles), diverting the push
                // off the Managed Git key onto whatever that file names. `IdentityAgent=none` already
                // covers an agent; `-F /dev/null` closes the config-file vector too. This makes the
                // sandbox ssh hermetic exactly like the host path (which previously appended this in
                // `provisionHost`); a user who needs real connectivity directives supplies them via the
                // "Extra OpenSSH config" setting, which takes the `-F <that file>` branch above instead.
                sshCommand += " -F /dev/null"
            }
            // `GIT_SSH_COMMAND` pins git's ssh to the Managed Git key for **every** remote (GitLab,
            // Bitbucket, self-hosted, …), not just github.com — Managed Git is the one git identity.
            // It governs only git's own ssh, so a standalone `ssh somehost` the agent/build runs is
            // unaffected; a host needing different connectivity uses the "Extra OpenSSH config" setting.
            env["GIT_SSH_COMMAND"] = sshCommand
            if let passphrase = config.sshPassphrase, !passphrase.isEmpty {
                // Encrypted key: OpenSSH can read the passphrase from an askpass helper instead of a
                // terminal. Write the passphrase to a 0600 file and a 0700 script that prints it,
                // then point ssh (and `ssh-keygen` when signing) at the script. `SSH_ASKPASS_REQUIRE
                // =force` makes OpenSSH ≥ 8.4 use askpass even with a tty, so the non-interactive
                // git/signing paths in the sandbox never block on a passphrase prompt.
                let hostPass = (hostHomeDir as NSString).appendingPathComponent(sshPassphraseFile)
                let containerPass = (containerHomeDir as NSString).appendingPathComponent(sshPassphraseFile)
                let hostAskpass = (hostHomeDir as NSString).appendingPathComponent(sshAskpassFile)
                let containerAskpass = (containerHomeDir as NSString).appendingPathComponent(sshAskpassFile)
                // One trailing newline: `cat` re-emits it and OpenSSH strips the single trailing
                // newline from askpass output, recovering the passphrase exactly.
                writeSecret(passphrase.hasSuffix("\n") ? passphrase : passphrase + "\n", to: hostPass)
                writeScript("#!/bin/sh\nexec cat \(singleQuoted(containerPass))\n", to: hostAskpass)
                env["SSH_ASKPASS"] = containerAskpass
                env["SSH_ASKPASS_REQUIRE"] = "force"
                // Some OpenSSH builds still gate askpass on a non-empty DISPLAY; harmless to set.
                env["DISPLAY"] = ":0"
            }
            if config.signCommits {
                // SSH commit signing: ssh-keygen signs with the private key at `user.signingkey`.
                // Pin the signer to OpenSSH's ssh-keygen at its standard path. In the container this
                // just makes the default explicit (the image's `openssh-client` installs it there); the
                // payoff is host-side — when this env is used by `provisionHost`/host_exec, it overrides
                // a global `gpg.ssh.program` like 1Password's `op-ssh-sign`, which can't sign with this
                // key (it would fail the commit). Mirrors `commitSigning`'s same pin.
                gitConfig.append(("gpg.format", "ssh"))
                gitConfig.append(("gpg.ssh.program", "/usr/bin/ssh-keygen"))
                gitConfig.append(("user.signingkey", containerKey))
                gitConfig.append(("commit.gpgsign", "true"))
                gitConfig.append(("tag.gpgsign", "true"))
            }
            if config.gitForceSSH {
                // Restrict github.com to SSH: rewrite HTTPS remotes to scp-style SSH so even an
                // `https://github.com/…` origin pushes/fetches over the injected key. The subsection
                // (`git@github.com:`) is the substring between the first and last dot of the key.
                gitConfig.append(("url.git@github.com:.insteadOf", "https://github.com/"))
            }
            if config.ghForceSSH {
                // Force `gh`'s own git operations (clone/checkout, the push in `gh pr create`) onto
                // SSH via a minimal config in a dir we point `GH_CONFIG_DIR` at — the file-write
                // equivalent of `gh config set git_protocol ssh`.
                let hostGhDir = (hostHomeDir as NSString).appendingPathComponent(Self.ghConfigDir)
                let containerGhDir = (containerHomeDir as NSString).appendingPathComponent(Self.ghConfigDir)
                try? FileManager.default.createDirectory(
                    atPath: hostGhDir, withIntermediateDirectories: true)
                let configPath = (hostGhDir as NSString).appendingPathComponent("config.yml")
                writeSecret("git_protocol: ssh\n", to: configPath)
                env["GH_CONFIG_DIR"] = containerGhDir
            }
            if let token = config.token, !token.isEmpty {
                // Optional: a PAT so `gh`'s API calls (and other GITHUB_TOKEN-aware tooling)
                // authenticate while `git` itself stays on SSH. `git` never reads these env vars, so
                // the SSH transport is unaffected — and no HTTPS credential helper is configured, so
                // git can't fall back to token-over-HTTPS.
                env["GH_TOKEN"] = token
                env["GITHUB_TOKEN"] = token
            }
        }

        if !gitConfig.isEmpty {
            env["GIT_CONFIG_COUNT"] = String(gitConfig.count)
            for (i, entry) in gitConfig.enumerated() {
                env["GIT_CONFIG_KEY_\(i)"] = entry.0
                env["GIT_CONFIG_VALUE_\(i)"] = entry.1
            }
        }
        return env
    }

    /// Provision the current GitHub credential config for a **host-side** `git` invocation that
    /// targets GitHub (a project clone, the `createPR` push) — as opposed to a sandbox. The app runs
    /// these on the host, inheriting the user's environment *and* their `~/.ssh/config`, so OpenSSH
    /// would otherwise fall back to the user's personal keys / agent — prompting for them — even when
    /// the user picked a Nucleic-managed / SSH key for GitHub. This writes the configured secret into
    /// a private 0700 temp dir and returns an env that pins `git`/`ssh` to *only* that key, hermetic
    /// from the user's ssh setup (see below). In token mode it returns the same HTTPS credential
    /// helper used in the sandbox.
    ///
    /// The caller MUST invoke `cleanup()` once the git command finishes, to shred the temp secret.
    /// Returns an empty env and a no-op cleanup when nothing is configured (mode `.none`/inactive) —
    /// leaving the host git operation to behave exactly as before.
    public static func provisionHost(
        _ config: GitHubCredentialConfig = .current()
    ) -> (env: [String: String], cleanup: @Sendable () -> Void) {
        guard config.isActive else { return ([:], {}) }
        let dir = (NSTemporaryDirectory() as NSString)
            .appendingPathComponent("nucleic-githost-\(UUID().uuidString)")
        try? FileManager.default.createDirectory(
            atPath: dir, withIntermediateDirectories: true,
            attributes: [.posixPermissions: 0o700])
        // On the host the secret dir *is* the path the env references (no bind-mount remap), so the
        // host home and the "container" home are the same directory. `provision` already makes the
        // ssh command hermetic — `-F /dev/null` (no user config) or `-F <that file>` (user config) —
        // so the host inherits the same isolation from the user's `~/.ssh/config` / ssh-agent that the
        // sandbox gets, with no extra step here.
        let env = provision(config, hostHomeDir: dir, containerHomeDir: dir)
        let cleanup: @Sendable () -> Void = { try? FileManager.default.removeItem(atPath: dir) }
        return (env, cleanup)
    }

    /// Host-side **commit signing**. When the user enabled "Sign commits with this SSH key" and an
    /// SSH/managed key is configured, write that key into a private 0700 temp dir and return the
    /// `git -c …` flags that make a host-side `git commit`/`git merge`/`git rebase` SSH-sign with it
    /// (`gpg.format=ssh`, `user.signingkey=<that key>`, `commit.gpgsign=true`, `tag.gpgsign=true`),
    /// plus any env needed to unlock a passphrase-protected `.ssh` key non-interactively. Returns
    /// empty flags + env and a no-op cleanup when signing is off or no usable key is configured, so a
    /// caller can splice the result in unconditionally.
    ///
    /// Distinct from `provisionHost`, which pins the *transport* key for clone/push: those operations
    /// create no commits, so the signing config it builds never reaches one. This is the piece the
    /// commit-creating host paths (nvrsion promote, WorktreeManager integrate) need — without it,
    /// Nucleic's own commits fall back to the host's ambient gitconfig, signing with the user's
    /// personal key (or, where Nucleic forces `commit.gpgsign=false`, not at all) instead of the
    /// configured Managed Git key. The flags are passed on the command line (not via `GIT_CONFIG_*`)
    /// so they compose cleanly with — and override — an existing `-c commit.gpgsign=false`.
    ///
    /// The caller MUST invoke `cleanup()` once the git command finishes, to shred the temp key.
    public static func commitSigning(
        _ config: GitHubCredentialConfig = .current()
    ) -> (flags: [String], env: [String: String], cleanup: @Sendable () -> Void) {
        guard config.signCommits,
              config.mode == .ssh || config.mode == .managed,
              let key = config.sshPrivateKey, !key.isEmpty
        else { return ([], [:], {}) }

        let dir = (NSTemporaryDirectory() as NSString)
            .appendingPathComponent("nucleic-gitsign-\(UUID().uuidString)")
        try? FileManager.default.createDirectory(
            atPath: dir, withIntermediateDirectories: true,
            attributes: [.posixPermissions: 0o700])
        let keyPath = (dir as NSString).appendingPathComponent(sshKeyFile)
        // ssh-keygen signs with the private key at `user.signingkey`; OpenSSH needs it 0600.
        writeSecret(key.hasSuffix("\n") ? key : key + "\n", to: keyPath)

        var env: [String: String] = [:]
        if let passphrase = config.sshPassphrase, !passphrase.isEmpty {
            // Encrypted `.ssh` key: ssh-keygen reads the passphrase from an askpass helper, so the
            // headless signing path never blocks on a prompt (mirrors `provision`'s sandbox askpass).
            // Managed keys are passphrase-free, so this branch only ever runs for a user `.ssh` key.
            let passPath = (dir as NSString).appendingPathComponent(sshPassphraseFile)
            let askpassPath = (dir as NSString).appendingPathComponent(sshAskpassFile)
            writeSecret(passphrase.hasSuffix("\n") ? passphrase : passphrase + "\n", to: passPath)
            writeScript("#!/bin/sh\nexec cat \(singleQuoted(passPath))\n", to: askpassPath)
            env["SSH_ASKPASS"] = askpassPath
            env["SSH_ASKPASS_REQUIRE"] = "force"
            env["DISPLAY"] = ":0"
        }

        let flags = [
            "-c", "gpg.format=ssh",
            // Pin the signer to OpenSSH's own ssh-keygen, hermetic from the user's global
            // `gpg.ssh.program`. A 1Password user (common — they're the reason `provision` pins
            // `IdentityAgent=none`) has `gpg.ssh.program` set to 1Password's `op-ssh-sign`, which
            // signs only with keys in *its* vault and rejects a raw private-key path outright
            // ("invalid ssh public key", "failed to write commit object"). Without this pin the
            // Managed Git key can't sign at all, and a key 1Password *does* hold would sign in its
            // place — a key Nucleic doesn't manage. Absolute path, matching `ManagedSSHKey.generate`.
            "-c", "gpg.ssh.program=/usr/bin/ssh-keygen",
            "-c", "user.signingkey=\(keyPath)",
            "-c", "commit.gpgsign=true",
            "-c", "tag.gpgsign=true",
        ]
        let cleanup: @Sendable () -> Void = { try? FileManager.default.removeItem(atPath: dir) }
        return (flags, env, cleanup)
    }

    /// Write `contents` to `path` with 0600 permissions, enforcing the mode even if the file
    /// already existed (a prior turn's copy). Best-effort, matching the credential-export path.
    private static func writeSecret(_ contents: String, to path: String) {
        let fm = FileManager.default
        fm.createFile(
            atPath: path, contents: Data(contents.utf8),
            attributes: [.posixPermissions: 0o600])
        try? fm.setAttributes([.posixPermissions: 0o600], ofItemAtPath: path)
    }

    /// Like `writeSecret` but 0700 — the file is meant to be executed (the askpass helper).
    private static func writeScript(_ contents: String, to path: String) {
        let fm = FileManager.default
        fm.createFile(
            atPath: path, contents: Data(contents.utf8),
            attributes: [.posixPermissions: 0o700])
        try? fm.setAttributes([.posixPermissions: 0o700], ofItemAtPath: path)
    }

    /// Single-quote a string for safe interpolation into a `/bin/sh` script. The only interpolated
    /// value is a Nucleic-controlled path, but quoting keeps it correct regardless of its contents.
    private static func singleQuoted(_ s: String) -> String {
        "'" + s.replacingOccurrences(of: "'", with: "'\\''") + "'"
    }
}
