import Foundation
#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#endif
#if os(macOS)
import Security
#endif

#if os(Linux)
@_silgen_name("pipe2")
private func linuxPipe2(_ pipeDescriptors: UnsafeMutablePointer<Int32>, _ flags: Int32) -> Int32
#endif

public enum PathPurpose: Hashable, Sendable {
    case rpc
    case tty
    case nodeTooling
}

public struct PathDebugSnapshot: Equatable, Sendable {
    public let codexBinary: String?
    public let claudeBinary: String?
    public let geminiBinary: String?
    public let effectivePATH: String
    public let loginShellPATH: String?

    public static let empty = PathDebugSnapshot(
        codexBinary: nil,
        claudeBinary: nil,
        geminiBinary: nil,
        effectivePATH: "",
        loginShellPATH: nil)

    public init(
        codexBinary: String?,
        claudeBinary: String?,
        geminiBinary: String? = nil,
        effectivePATH: String,
        loginShellPATH: String?)
    {
        self.codexBinary = codexBinary
        self.claudeBinary = claudeBinary
        self.geminiBinary = geminiBinary
        self.effectivePATH = effectivePATH
        self.loginShellPATH = loginShellPATH
    }
}

public enum BinaryLocator {
    /// Test-only override so parallel Gemini suites can point at fake binaries
    /// without mutating process-wide `GEMINI_CLI_PATH`.
    @TaskLocal public static var geminiBinaryPathOverrideForTesting: String?

    public static func resolveClaudeBinary(
        env: [String: String] = ProcessInfo.processInfo.environment,
        loginPATH: [String]? = LoginShellPathCache.shared.current,
        commandV: (String, String?, TimeInterval, FileManager) -> String? = ShellCommandLocator.commandV,
        aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = ShellCommandLocator
            .resolveAlias,
        fileManager: FileManager = .default,
        home: String = NSHomeDirectory()) -> String?
    {
        // Provider-specific by design: This named resolver supplies Claude's actual CLI executable name.
        self.resolveBinary(
            name: "claude",
            overrideKey: "CLAUDE_CLI_PATH",
            env: env,
            loginPATH: loginPATH,
            commandV: commandV,
            aliasResolver: aliasResolver,
            wellKnownPaths: self.claudeWellKnownPaths(home: home),
            fileManager: fileManager,
            home: home)
    }

    public static func resolveArkcliBinary(
        env: [String: String] = ProcessInfo.processInfo.environment,
        loginPATH: [String]? = LoginShellPathCache.shared.current,
        commandV: (String, String?, TimeInterval, FileManager) -> String? = ShellCommandLocator.commandV,
        aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = ShellCommandLocator
            .resolveAlias,
        fileManager: FileManager = .default,
        home: String = NSHomeDirectory()) -> String?
    {
        self.resolveBinary(
            name: "arkcli",
            overrideKey: "ARKCLI_PATH",
            env: env,
            loginPATH: loginPATH,
            commandV: commandV,
            aliasResolver: aliasResolver,
            wellKnownPaths: [
                "\(home)/.local/bin/arkcli",
                "/opt/homebrew/bin/arkcli",
                "/usr/local/bin/arkcli",
            ],
            fileManager: fileManager,
            home: home)
    }

    /// Well-known installation paths for the Claude CLI binary.
    /// Covers Anthropic's native installer (`~/.local/bin`), the `claude migrate-installer`
    /// self-updating location (`~/.claude/local`), the legacy per-user installer
    /// (`~/.claude/bin`), Homebrew, and the macOS Terminal installer (cmux.app).
    static func claudeWellKnownPaths(home: String) -> [String] {
        [
            "\(home)/.local/bin/claude",
            "\(home)/.claude/local/claude",
            "\(home)/.claude/bin/claude",
            "/opt/homebrew/bin/claude",
            "/usr/local/bin/claude",
            "/Applications/cmux.app/Contents/Resources/bin/claude",
        ]
    }

    public static func resolveAntigravityBinary(
        env: [String: String] = ProcessInfo.processInfo.environment,
        loginPATH: [String]? = LoginShellPathCache.shared.current,
        commandV: (String, String?, TimeInterval, FileManager) -> String? = ShellCommandLocator.commandV,
        aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = ShellCommandLocator
            .resolveAlias,
        fileManager: FileManager = .default,
        home: String = NSHomeDirectory()) -> String?
    {
        self.resolveBinary(
            name: "agy",
            overrideKey: "ANTIGRAVITY_CLI_PATH",
            env: env,
            loginPATH: loginPATH,
            commandV: commandV,
            aliasResolver: aliasResolver,
            wellKnownPaths: [
                "\(home)/.local/bin/agy",
                "/opt/homebrew/bin/agy",
                "/usr/local/bin/agy",
            ],
            fileManager: fileManager,
            home: home)
    }

    public static func resolveCodexBinary(
        env: [String: String] = ProcessInfo.processInfo.environment,
        loginPATH: [String]? = LoginShellPathCache.shared.current,
        commandV: (String, String?, TimeInterval, FileManager) -> String? = ShellCommandLocator.commandV,
        aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = ShellCommandLocator
            .resolveAlias,
        launchCandidateFilter: (String, FileManager) -> Bool = CodexLaunchPreflight.isLaunchCandidateAllowed,
        fileManager: FileManager = .default,
        home: String = NSHomeDirectory()) -> String?
    {
        // Provider-specific by design: This named resolver supplies Codex's actual CLI executable name.
        self.resolveBinary(
            name: "codex",
            overrideKey: "CODEX_CLI_PATH",
            env: env,
            loginPATH: loginPATH,
            commandV: commandV,
            aliasResolver: aliasResolver,
            wellKnownPaths: self.codexWellKnownPaths(home: home),
            launchCandidateFilter: launchCandidateFilter,
            fileManager: fileManager,
            home: home)
    }

    /// Well-known installation paths for the signed Codex CLI bundled with current and legacy desktop apps.
    /// Keep these after PATH lookups, but use them as a safe fallback when a PATH shim is blocked.
    static func codexWellKnownPaths(home: String) -> [String] {
        #if os(macOS)
        [
            "\(home)/Applications/ChatGPT.app/Contents/Resources/codex",
            "\(home)/Applications/Codex.app/Contents/Resources/codex",
            "/Applications/ChatGPT.app/Contents/Resources/codex",
            "/Applications/Codex.app/Contents/Resources/codex",
        ]
        #else
        []
        #endif
    }

    public static func resolveGeminiBinary(
        env: [String: String] = ProcessInfo.processInfo.environment,
        loginPATH: [String]? = LoginShellPathCache.shared.current,
        commandV: (String, String?, TimeInterval, FileManager) -> String? = ShellCommandLocator.commandV,
        aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = ShellCommandLocator
            .resolveAlias,
        fileManager: FileManager = .default,
        home: String = NSHomeDirectory()) -> String?
    {
        if let override = self.geminiBinaryPathOverrideForTesting,
           fileManager.isExecutableFile(atPath: override)
        {
            return override
        }
        // Provider-specific by design: This named resolver supplies Gemini's actual CLI executable name.
        return self.resolveBinary(
            name: "gemini",
            overrideKey: "GEMINI_CLI_PATH",
            env: env,
            loginPATH: loginPATH,
            commandV: commandV,
            aliasResolver: aliasResolver,
            fileManager: fileManager,
            home: home)
    }

    public static func resolveGrokBinary(
        env: [String: String] = ProcessInfo.processInfo.environment,
        loginPATH: [String]? = LoginShellPathCache.shared.current,
        commandV: (String, String?, TimeInterval, FileManager) -> String? = ShellCommandLocator.commandV,
        aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = ShellCommandLocator
            .resolveAlias,
        fileManager: FileManager = .default,
        home: String = NSHomeDirectory()) -> String?
    {
        // Provider-specific by design: This named resolver supplies Grok's actual CLI executable name.
        self.resolveBinary(
            name: "grok",
            overrideKey: "GROK_CLI_PATH",
            env: env,
            loginPATH: loginPATH,
            commandV: commandV,
            aliasResolver: aliasResolver,
            wellKnownPaths: self.grokWellKnownPaths(home: home),
            fileManager: fileManager,
            home: home)
    }

    public static func resolveAmpBinary(
        env: [String: String] = ProcessInfo.processInfo.environment,
        loginPATH: [String]? = LoginShellPathCache.shared.current,
        commandV: (String, String?, TimeInterval, FileManager) -> String? = ShellCommandLocator.commandV,
        aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = ShellCommandLocator
            .resolveAlias,
        fileManager: FileManager = .default,
        home: String = NSHomeDirectory()) -> String?
    {
        // Provider-specific by design: This named resolver supplies Amp's actual CLI executable name.
        self.resolveBinary(
            name: "amp",
            overrideKey: "AMP_CLI_PATH",
            env: env,
            loginPATH: loginPATH,
            commandV: commandV,
            aliasResolver: aliasResolver,
            wellKnownPaths: self.ampWellKnownPaths(home: home),
            fileManager: fileManager,
            home: home)
    }

    static func ampWellKnownPaths(home: String) -> [String] {
        [
            "\(home)/.local/bin/amp",
            "\(home)/.amp/bin/amp",
            "/opt/homebrew/bin/amp",
            "/usr/local/bin/amp",
        ]
    }

    /// Well-known install locations for the Grok Build CLI binary.
    /// Covers the installer's default (`~/.grok/bin/grok`) and the symlinks it sometimes
    /// creates into `~/.local/bin` and `/usr/local/bin`.
    static func grokWellKnownPaths(home: String) -> [String] {
        [
            "\(home)/.grok/bin/grok",
            "\(home)/.local/bin/grok",
            "/usr/local/bin/grok",
            "/opt/homebrew/bin/grok",
        ]
    }

    public static func resolveAWSBinary(
        env: [String: String] = ProcessInfo.processInfo.environment,
        loginPATH: [String]? = LoginShellPathCache.shared.current,
        commandV: (String, String?, TimeInterval, FileManager) -> String? = ShellCommandLocator.commandV,
        aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = ShellCommandLocator
            .resolveAlias,
        fileManager: FileManager = .default,
        home: String = NSHomeDirectory()) -> String?
    {
        self.resolveBinary(
            name: "aws",
            overrideKey: "AWS_CLI_PATH",
            env: env,
            loginPATH: loginPATH,
            commandV: commandV,
            aliasResolver: aliasResolver,
            wellKnownPaths: self.awsWellKnownPaths(home: home),
            fileManager: fileManager,
            home: home)
    }

    /// Well-known install locations for the AWS CLI v2 (`aws`).
    /// Covers Homebrew (Apple Silicon + Intel) and the per-user pip/uv install path.
    static func awsWellKnownPaths(home: String) -> [String] {
        [
            "/opt/homebrew/bin/aws",
            "/usr/local/bin/aws",
            "\(home)/.local/bin/aws",
        ]
    }

    public static func resolveAuggieBinary(
        env: [String: String] = ProcessInfo.processInfo.environment,
        loginPATH: [String]? = LoginShellPathCache.shared.current,
        commandV: (String, String?, TimeInterval, FileManager) -> String? = ShellCommandLocator.commandV,
        aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = ShellCommandLocator
            .resolveAlias,
        fileManager: FileManager = .default,
        home: String = NSHomeDirectory()) -> String?
    {
        self.resolveBinary(
            name: "auggie",
            overrideKey: "AUGGIE_CLI_PATH",
            env: env,
            loginPATH: loginPATH,
            commandV: commandV,
            aliasResolver: aliasResolver,
            fileManager: fileManager,
            home: home)
    }

    public static func resolveKiroCLIBinary(
        env: [String: String] = ProcessInfo.processInfo.environment,
        loginPATH: [String]? = LoginShellPathCache.shared.current,
        commandV: (String, String?, TimeInterval, FileManager) -> String? = ShellCommandLocator.commandV,
        aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = ShellCommandLocator
            .resolveAlias,
        fileManager: FileManager = .default,
        home: String = NSHomeDirectory()) -> String?
    {
        self.resolveBinary(
            name: "kiro-cli",
            overrideKey: "KIRO_CLI_PATH",
            env: env,
            loginPATH: loginPATH,
            commandV: commandV,
            aliasResolver: aliasResolver,
            wellKnownPaths: [
                "\(home)/.local/bin/kiro-cli",
                "/opt/homebrew/bin/kiro-cli",
                "/usr/local/bin/kiro-cli",
            ],
            fileManager: fileManager,
            home: home)
    }

    // swiftlint:disable function_parameter_count
    private static func resolveBinary(
        name: String,
        overrideKey: String,
        env: [String: String],
        loginPATH: [String]?,
        commandV: (String, String?, TimeInterval, FileManager) -> String?,
        aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String?,
        wellKnownPaths: [String] = [],
        launchCandidateFilter: (String, FileManager) -> Bool = { _, _ in true },
        fileManager: FileManager,
        home: String) -> String?
    {
        // swiftlint:enable function_parameter_count
        // 1) Explicit override
        if let override = env[overrideKey], fileManager.isExecutableFile(atPath: override) {
            return override
        }

        // 2) Login-shell PATH (captured once per launch)
        if let loginPATH,
           let pathHit = self.find(
               name,
               in: loginPATH,
               fileManager: fileManager,
               launchCandidateFilter: launchCandidateFilter)
        {
            return pathHit
        }

        // 3) Existing PATH
        if let existingPATH = env["PATH"],
           let pathHit = self.find(
               name,
               in: existingPATH.split(separator: ":").map(String.init),
               fileManager: fileManager,
               launchCandidateFilter: launchCandidateFilter)
        {
            return pathHit
        }

        // 4) Well-known installation paths (e.g. Homebrew, cmux.app bundle, ~/.claude/bin).
        // Prefer these before shell probing to avoid running interactive shell init for common installs.
        for candidate in wellKnownPaths
            where fileManager.isExecutableFile(atPath: candidate) && launchCandidateFilter(candidate, fileManager)
        {
            return candidate
        }

        // 5) Interactive login shell lookup (captures nvm/fnm/mise paths from .zshrc/.bashrc)
        if let shellHit = commandV(name, env["SHELL"], 2.0, fileManager),
           fileManager.isExecutableFile(atPath: shellHit),
           launchCandidateFilter(shellHit, fileManager)
        {
            return shellHit
        }

        // 5b) Alias fallback (login shell); only attempt after all standard lookups fail.
        if let aliasHit = aliasResolver(name, env["SHELL"], 2.0, fileManager, home),
           fileManager.isExecutableFile(atPath: aliasHit),
           launchCandidateFilter(aliasHit, fileManager)
        {
            return aliasHit
        }

        // 6) Minimal fallback
        let fallback = ["/usr/bin", "/bin", "/usr/sbin", "/sbin"]
        if let pathHit = self.find(
            name,
            in: fallback,
            fileManager: fileManager,
            launchCandidateFilter: launchCandidateFilter)
        {
            return pathHit
        }

        return nil
    }

    private static func find(
        _ binary: String,
        in paths: [String],
        fileManager: FileManager,
        launchCandidateFilter: (String, FileManager) -> Bool = { _, _ in true }) -> String?
    {
        for path in paths where !path.isEmpty {
            let candidate = "\(path.hasSuffix("/") ? String(path.dropLast()) : path)/\(binary)"
            if fileManager.isExecutableFile(atPath: candidate), launchCandidateFilter(candidate, fileManager) {
                return candidate
            }
        }
        return nil
    }
}

public enum CodexLaunchPreflight {
    struct GatekeeperAssessment {
        let output: String
        let exitStatus: Int32
    }

    public static func isLaunchCandidateAllowed(path: String, fileManager: FileManager = .default) -> Bool {
        #if os(macOS)
        self.isLaunchCandidateAllowed(
            path: path,
            fileManager: fileManager,
            hasExtendedAttribute: self.hasExtendedAttribute,
            spctlAssessment: { self.spctlAssessment(path: $0) },
            appSignatureIsTrusted: self.isExpectedOpenAIAppSignature,
            isMachOExecutable: self.isMachOExecutable)
        #else
        _ = path
        _ = fileManager
        return true
        #endif
    }

    #if os(macOS)
    // Keep each security boundary injectable so preflight tests never inspect or launch host binaries.
    // swiftlint:disable:next function_parameter_count
    static func isLaunchCandidateAllowed(
        path: String,
        fileManager: FileManager,
        hasExtendedAttribute: (String, String) -> Bool,
        spctlAssessment: (String) -> GatekeeperAssessment?,
        appSignatureIsTrusted: (String) -> Bool,
        isMachOExecutable: (String) -> Bool) -> Bool
    {
        let realPath = URL(fileURLWithPath: path).resolvingSymlinksInPath().path
        let sourceAppBundlePath = self.containingAppBundlePath(for: path)
        let resolvedAppBundlePath = self.containingAppBundlePath(for: realPath)
        let appBundlePath: String?
        if let sourceAppBundlePath {
            let resolvedSourceBundle = URL(fileURLWithPath: sourceAppBundlePath).resolvingSymlinksInPath().path
            guard resolvedAppBundlePath == resolvedSourceBundle else { return false }
            appBundlePath = resolvedSourceBundle
        } else {
            appBundlePath = resolvedAppBundlePath
        }
        let appBundlePaths = [sourceAppBundlePath, appBundlePath].compactMap(\.self)
        let pathsToCheck = [path, realPath] + appBundlePaths + self
            .nativeCodexExecutableCandidates(
                for: realPath,
                fileManager: fileManager)

        for candidate in Set(pathsToCheck) where hasExtendedAttribute(candidate, "com.apple.malware") {
            return false
        }

        let hasQuarantine = Set(pathsToCheck).contains { hasExtendedAttribute($0, "com.apple.quarantine") }
        if let appBundlePath {
            guard appSignatureIsTrusted(appBundlePath),
                  let assessment = spctlAssessment(appBundlePath),
                  assessment.exitStatus == 0,
                  self.isAcceptedAssessment(assessment.output, path: appBundlePath),
                  !self.isExplicitlyBlockedAssessment(assessment.output, path: appBundlePath)
            else {
                return false
            }
            return true
        }

        guard let native = pathsToCheck.first(where: isMachOExecutable) else {
            return !hasQuarantine
        }

        guard let assessment = spctlAssessment(native)
        else {
            return !hasQuarantine
        }

        return !self.isExplicitlyBlockedAssessment(assessment.output, path: native)
    }

    private static func containingAppBundlePath(for path: String) -> String? {
        var candidate = URL(fileURLWithPath: path).standardizedFileURL
        while candidate.path != "/" {
            if candidate.pathExtension.caseInsensitiveCompare("app") == .orderedSame {
                return candidate.path
            }
            let parent = candidate.deletingLastPathComponent()
            guard parent.path != candidate.path else { return nil }
            candidate = parent
        }
        return nil
    }

    private static func nativeCodexExecutableCandidates(for path: String, fileManager: FileManager) -> [String] {
        let url = URL(fileURLWithPath: path)
        guard url.lastPathComponent == "codex.js" else { return [] }

        let packageRoot = url.deletingLastPathComponent().deletingLastPathComponent()
        return self.npmNativeCodexCandidates(packageRoot: packageRoot)
            .map(\.path)
            .filter { fileManager.isExecutableFile(atPath: $0) }
    }

    private static func npmNativeCodexCandidates(packageRoot: URL) -> [URL] {
        guard let target = self.darwinCodexTarget else { return [] }
        let optionalPackage = packageRoot
            .appendingPathComponent("node_modules")
            .appendingPathComponent("@openai")
            .appendingPathComponent(target.packageName)

        return [
            optionalPackage,
            packageRoot,
        ].map {
            $0.appendingPathComponent("vendor")
                .appendingPathComponent(target.triple)
                .appendingPathComponent("codex")
                .appendingPathComponent("codex")
        }
    }

    private static var darwinCodexTarget: (packageName: String, triple: String)? {
        #if arch(arm64)
        ("codex-darwin-arm64", "aarch64-apple-darwin")
        #elseif arch(x86_64)
        ("codex-darwin-x64", "x86_64-apple-darwin")
        #else
        nil
        #endif
    }

    private static func hasExtendedAttribute(path: String, name: String) -> Bool {
        path.withCString { pathPointer in
            name.withCString { namePointer in
                getxattr(pathPointer, namePointer, nil, 0, 0, 0) >= 0
            }
        }
    }

    private static func isMachOExecutable(path: String) -> Bool {
        guard let handle = try? FileHandle(forReadingFrom: URL(fileURLWithPath: path)) else { return false }
        defer { try? handle.close() }

        guard let data = try? handle.read(upToCount: 4), data.count == 4 else { return false }
        let bytes = [UInt8](data)
        return bytes == [0xFE, 0xED, 0xFA, 0xCE] ||
            bytes == [0xCE, 0xFA, 0xED, 0xFE] ||
            bytes == [0xFE, 0xED, 0xFA, 0xCF] ||
            bytes == [0xCF, 0xFA, 0xED, 0xFE] ||
            bytes == [0xCA, 0xFE, 0xBA, 0xBE] ||
            bytes == [0xCA, 0xFE, 0xBA, 0xBF]
    }

    private static func spctlAssessment(path: String, timeout: TimeInterval = 2.0) -> GatekeeperAssessment? {
        let spctlPath = "/usr/sbin/spctl"
        guard FileManager.default.isExecutableFile(atPath: spctlPath) else { return nil }

        let process = Process()
        process.executableURL = URL(fileURLWithPath: spctlPath)
        process.arguments = ["--assess", "--type", "execute", "--verbose=4", path]

        let output = Pipe()
        process.standardOutput = output
        process.standardError = output

        let finished = DispatchSemaphore(value: 0)
        process.terminationHandler = { _ in finished.signal() }

        do {
            try process.run()
        } catch {
            return nil
        }

        if finished.wait(timeout: .now() + timeout) != .success {
            process.terminate()
            return nil
        }

        let data = output.fileHandleForReading.readDataToEndOfFile()
        guard let text = String(data: data, encoding: .utf8) else { return nil }
        return GatekeeperAssessment(output: text, exitStatus: process.terminationStatus)
    }

    private static func isExpectedOpenAIAppSignature(path: String) -> Bool {
        let requirementText =
            "identifier \"com.openai.codex\" and anchor apple generic " +
            "and certificate leaf[subject.OU] = \"2DC432GLL2\""
        var staticCode: SecStaticCode?
        guard SecStaticCodeCreateWithPath(
            URL(fileURLWithPath: path) as CFURL,
            SecCSFlags(),
            &staticCode) == errSecSuccess,
            let staticCode
        else {
            return false
        }

        var requirement: SecRequirement?
        guard SecRequirementCreateWithString(
            requirementText as CFString,
            SecCSFlags(),
            &requirement) == errSecSuccess
        else {
            return false
        }

        // Pin the publisher and bundle identity here; the Gatekeeper assessment below performs full bundle validation.
        let validationFlags = SecCSFlags(rawValue: kSecCSBasicValidateOnly)
        return SecStaticCodeCheckValidity(staticCode, validationFlags, requirement) == errSecSuccess
    }

    private static func isAcceptedAssessment(_ assessment: String, path: String) -> Bool {
        self.assessmentDiagnosticText(assessment, path: path)
            .split(whereSeparator: \.isNewline)
            .first?
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .localizedCaseInsensitiveCompare("accepted") == .orderedSame
    }

    private static func isExplicitlyBlockedAssessment(_ assessment: String, path: String) -> Bool {
        let lower = self.assessmentDiagnosticText(assessment, path: path).lowercased()
        if lower.contains("denied") ||
            lower.contains("cssmerr_tp_cert_revoked") ||
            lower.contains("revoked") ||
            lower.contains("malware") ||
            lower.contains("quarantine")
        {
            return true
        }
        if lower.contains("rejected") {
            return !lower.contains("code is valid but does not seem to be an app")
        }
        return false
    }

    private static func assessmentDiagnosticText(_ assessment: String, path: String) -> String {
        assessment
            .split(whereSeparator: \.isNewline)
            .enumerated()
            .compactMap { offset, line -> String? in
                var text = line.trimmingCharacters(in: .whitespacesAndNewlines)
                if offset == 0, text.hasPrefix("\(path):") {
                    text = String(text.dropFirst(path.count + 1))
                        .trimmingCharacters(in: .whitespacesAndNewlines)
                }
                let lower = text.lowercased()
                guard !lower.hasPrefix("source="), !lower.hasPrefix("origin=") else {
                    return nil
                }
                return text
            }
            .joined(separator: "\n")
    }
    #endif
}

public enum ShellCommandLocator {
    #if canImport(Darwin)
    private static let shellSpawnFlags = Int16(POSIX_SPAWN_SETSID | POSIX_SPAWN_CLOEXEC_DEFAULT)
    #else
    private static let shellSpawnFlags: Int16 = 0x80 // glibc/musl POSIX_SPAWN_SETSID.
    #endif
    private static let shellSpawnLock = NSLock()

    static func test_runShellCommand(
        shell: String,
        arguments: [String],
        timeout: TimeInterval) -> Data?
    {
        self.runShellCommand(shell: shell, arguments: arguments, timeout: timeout)
    }

    static func test_makeCloseOnExecPipe() -> (read: Int32, write: Int32)? {
        self.makeCloseOnExecPipe()
    }

    static var test_shellSpawnFlags: Int16 {
        self.shellSpawnFlags
    }

    public static func commandV(
        _ tool: String,
        _ shell: String?,
        _ timeout: TimeInterval,
        _ fileManager: FileManager) -> String?
    {
        let text = self.runShellCapture(shell, timeout, "command -v \(tool)")?
            .trimmingCharacters(in: .whitespacesAndNewlines)
        guard let text, !text.isEmpty else { return nil }

        let lines = text.split(separator: "\n").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
        for line in lines.reversed() where line.hasPrefix("/") {
            let path = line
            if fileManager.isExecutableFile(atPath: path) {
                return path
            }
        }

        return nil
    }

    public static func resolveAlias(
        _ tool: String,
        _ shell: String?,
        _ timeout: TimeInterval,
        _ fileManager: FileManager,
        _ home: String) -> String?
    {
        let command = "alias \(tool) 2>/dev/null; type -a \(tool) 2>/dev/null"
        guard let text = self.runShellCapture(shell, timeout, command) else { return nil }
        let lines = text.split(separator: "\n").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }

        if let aliasPath = self.parseAliasPath(lines, tool: tool, home: home, fileManager: fileManager) {
            return aliasPath
        }

        for line in lines {
            if let path = self.extractPathCandidate(line: line, tool: tool, home: home),
               fileManager.isExecutableFile(atPath: path)
            {
                return path
            }
        }

        return nil
    }

    /// Thread-safe buffer for collecting pipe output from a readability handler.
    private final class CapturedData: @unchecked Sendable {
        private let lock = NSLock()
        private var data = Data()

        func append(_ other: Data) {
            self.lock.lock()
            self.data.append(other)
            self.lock.unlock()
        }

        func drain() -> Data {
            self.lock.lock()
            let result = self.data
            self.lock.unlock()
            return result
        }
    }

    /// Idempotent one-shot flag — `fire()` returns true exactly once.
    /// Used to make `DispatchGroup.leave()` safe to attempt from multiple paths.
    private final class OnceFlag: @unchecked Sendable {
        private let lock = NSLock()
        private var fired = false

        func fire() -> Bool {
            self.lock.lock()
            defer { self.lock.unlock() }
            if self.fired {
                return false
            }
            self.fired = true
            return true
        }
    }

    private static func makeCloseOnExecPipe() -> (read: Int32, write: Int32)? {
        var fds: (read: Int32, write: Int32) = (-1, -1)
        #if os(Linux)
        // Glibc and Musl export pipe2, but their Swift modules do not consistently declare it.
        guard withUnsafeMutablePointer(to: &fds, {
            $0.withMemoryRebound(to: Int32.self, capacity: 2) { linuxPipe2($0, O_CLOEXEC) == 0 }
        }) else { return nil }
        #else
        guard withUnsafeMutablePointer(to: &fds, {
            $0.withMemoryRebound(to: Int32.self, capacity: 2) { pipe($0) == 0 }
        }) else { return nil }

        for fd in [fds.read, fds.write] {
            let flags = fcntl(fd, F_GETFD)
            guard flags >= 0, fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == 0 else {
                close(fds.read)
                close(fds.write)
                return nil
            }
        }
        #endif
        return fds
    }

    // swiftlint:disable cyclomatic_complexity
    /// Runs a shell command, draining both stdout and stderr concurrently so that
    /// verbose shell init scripts (oh-my-zsh, nvm, pyenv, etc.) cannot deadlock on
    /// a full pipe buffer.  The child is launched via `posix_spawn` with
    /// `POSIX_SPAWN_SETSID` so it cannot take ownership of the caller's controlling
    /// terminal. The new session also makes the child its own process-group leader;
    /// cleanup tracks both that group and helpers retaining the command's output pipes.
    fileprivate static func runShellCommand(
        shell: String,
        arguments: [String],
        timeout: TimeInterval) -> Data?
    {
        // Darwin needs a lock around raw descriptor creation, close-on-exec flagging,
        // and spawn. Linux creates close-on-exec descriptors atomically with pipe2.
        self.shellSpawnLock.lock()
        var shellSpawnLockHeld = true
        defer {
            if shellSpawnLockHeld {
                self.shellSpawnLock.unlock()
            }
        }

        // Pipes for stdout/stderr.  stdin is redirected from /dev/null in the child
        // via posix_spawn_file_actions_addopen below. Close-on-exec prevents a
        // concurrently spawned probe from retaining these descriptors and being
        // mistaken for one of this probe's output holders during cleanup.
        guard let stdoutFds = self.makeCloseOnExecPipe() else { return nil }
        guard let stderrFds = self.makeCloseOnExecPipe() else {
            close(stdoutFds.read); close(stdoutFds.write)
            return nil
        }

        // Build file actions: redirect stdin from /dev/null, dup pipe write ends to
        // fds 1 and 2, and close every pipe fd in the child.  The init pattern
        // differs between platforms because the typedef is an opaque pointer on
        // Darwin and a struct on Linux C modules.
        #if canImport(Darwin)
        var fileActions: posix_spawn_file_actions_t?
        #else
        var fileActions = posix_spawn_file_actions_t()
        #endif
        guard posix_spawn_file_actions_init(&fileActions) == 0 else {
            close(stdoutFds.read); close(stdoutFds.write)
            close(stderrFds.read); close(stderrFds.write)
            return nil
        }
        defer { posix_spawn_file_actions_destroy(&fileActions) }
        posix_spawn_file_actions_addopen(&fileActions, 0, "/dev/null", O_RDONLY, 0)
        posix_spawn_file_actions_adddup2(&fileActions, stdoutFds.write, 1)
        posix_spawn_file_actions_adddup2(&fileActions, stderrFds.write, 2)
        posix_spawn_file_actions_addclose(&fileActions, stdoutFds.read)
        posix_spawn_file_actions_addclose(&fileActions, stdoutFds.write)
        posix_spawn_file_actions_addclose(&fileActions, stderrFds.read)
        posix_spawn_file_actions_addclose(&fileActions, stderrFds.write)

        // Build attributes: detach the child into a new session before exec. This
        // prevents interactive shell startup from changing the caller's foreground
        // process group while retaining a stable process group for cleanup.
        #if canImport(Darwin)
        var attr: posix_spawnattr_t?
        #else
        var attr = posix_spawnattr_t()
        #endif
        guard posix_spawnattr_init(&attr) == 0 else {
            close(stdoutFds.read); close(stdoutFds.write)
            close(stderrFds.read); close(stderrFds.write)
            return nil
        }
        defer { posix_spawnattr_destroy(&attr) }
        guard posix_spawnattr_setflags(&attr, self.shellSpawnFlags) == 0 else {
            close(stdoutFds.read); close(stdoutFds.write)
            close(stderrFds.read); close(stderrFds.write)
            return nil
        }

        // Build argv (argv[0] is conventionally the executable path).
        var cArgs: [UnsafeMutablePointer<CChar>?] = []
        cArgs.append(strdup(shell))
        for arg in arguments {
            cArgs.append(strdup(arg))
        }
        cArgs.append(nil)
        defer {
            for p in cArgs {
                if let p {
                    free(p)
                }
            }
        }

        // Inherit the parent environment.  Build a NULL-terminated `KEY=VALUE`
        // array since `extern char **environ` isn't directly visible from Swift.
        var cEnv: [UnsafeMutablePointer<CChar>?] = []
        for (key, value) in ProcessInfo.processInfo.environment {
            cEnv.append(strdup("\(key)=\(value)"))
        }
        cEnv.append(nil)
        defer {
            for p in cEnv {
                if let p {
                    free(p)
                }
            }
        }

        var pid: pid_t = 0
        let spawnResult = shell.withCString { execPath in
            posix_spawn(&pid, execPath, &fileActions, &attr, cArgs, cEnv)
        }

        // Close the write ends in the parent so EOF will arrive on the read ends
        // once every descendant in the process group also closes them.
        close(stdoutFds.write)
        close(stderrFds.write)
        self.shellSpawnLock.unlock()
        shellSpawnLockHeld = false

        guard spawnResult == 0 else {
            close(stdoutFds.read); close(stderrFds.read)
            return nil
        }

        // Track EOF on each pipe so we can wait for full drain instead of sleeping.
        // The readability handler fires with empty data when every writer end is
        // closed (i.e. the child *and* any inheriting background helpers are gone).
        let drainGroup = DispatchGroup()
        drainGroup.enter()
        drainGroup.enter()
        let stdoutDone = OnceFlag()
        let stderrDone = OnceFlag()

        let stdoutCollector = CapturedData()
        let stdoutHandle = FileHandle(fileDescriptor: stdoutFds.read, closeOnDealloc: true)
        stdoutHandle.readabilityHandler = { handle in
            let data = handle.availableData
            if data.isEmpty {
                handle.readabilityHandler = nil
                if stdoutDone.fire() {
                    drainGroup.leave()
                }
            } else {
                stdoutCollector.append(data)
            }
        }

        let stderrHandle = FileHandle(fileDescriptor: stderrFds.read, closeOnDealloc: true)
        stderrHandle.readabilityHandler = { handle in
            let data = handle.availableData
            if data.isEmpty {
                handle.readabilityHandler = nil
                if stderrDone.fire() {
                    drainGroup.leave()
                }
            }
        }

        // Adopt the already-spawned session so cleanup can also discover helpers
        // that escape into a new process group while retaining our output pipes.
        let process = SpawnedProcessGroup.adopt(
            pid: pid,
            outputFileDescriptors: [stdoutFds.read, stderrFds.read])
        let deadline = Date().addingTimeInterval(timeout)
        while process.isRunning, Date() < deadline {
            usleep(10000)
        }

        if process.isRunning {
            process.terminateSynchronously()
            stdoutHandle.readabilityHandler = nil
            stderrHandle.readabilityHandler = nil
            if stdoutDone.fire() {
                drainGroup.leave()
            }
            if stderrDone.fire() {
                drainGroup.leave()
            }
            return nil
        }

        // Normal completion — clean up background children spawned by shell init,
        // including session-escaped helpers that still hold our output pipes open.
        process.terminateSynchronously()

        // Wait for both pipes to deliver EOF so no buffered bytes are lost.
        // Bounded so a stuck handler can't hang the caller indefinitely.
        if drainGroup.wait(timeout: .now() + 0.4) != .success {
            process.terminateSynchronously(grace: 0)
        }
        if drainGroup.wait(timeout: .now() + 0.6) != .success {
            stdoutHandle.readabilityHandler = nil
            stderrHandle.readabilityHandler = nil
            if stdoutDone.fire() {
                drainGroup.leave()
            }
            if stderrDone.fire() {
                drainGroup.leave()
            }
        }
        return stdoutCollector.drain()
    }

    // swiftlint:enable cyclomatic_complexity

    private static func runShellCapture(_ shell: String?, _ timeout: TimeInterval, _ command: String) -> String? {
        let shellPath = (shell?.isEmpty == false) ? shell! : "/bin/zsh"
        let isCI = ["1", "true"].contains(ProcessInfo.processInfo.environment["CI"]?.lowercased())
        // Interactive login shell to pick up PATH mutations from shell init (nvm/fnm/mise).
        // CI runners can have shell init hooks that emit missing CLI errors; avoid them in CI.
        let args = isCI ? ["-c", command] : ["-l", "-i", "-c", command]
        guard let data = runShellCommand(shell: shellPath, arguments: args, timeout: timeout) else {
            return nil
        }
        return String(data: data, encoding: .utf8)
    }

    private static func parseAliasPath(
        _ lines: [String],
        tool: String,
        home: String,
        fileManager: FileManager) -> String?
    {
        for line in lines {
            if line.hasPrefix("alias \(tool)=") {
                let value = line.replacingOccurrences(of: "alias \(tool)=", with: "")
                if let path = self.extractAliasExpansion(value, home: home),
                   fileManager.isExecutableFile(atPath: path)
                {
                    return path
                }
            }
            if line.lowercased().contains("aliased to") {
                if let range = line.range(of: "aliased to") {
                    let value = line[range.upperBound...].trimmingCharacters(in: .whitespacesAndNewlines)
                    if let path = self.extractAliasExpansion(String(value), home: home),
                       fileManager.isExecutableFile(atPath: path)
                    {
                        return path
                    }
                }
            }
        }
        return nil
    }

    private static func extractAliasExpansion(_ raw: String, home: String) -> String? {
        let trimmed = raw.trimmingCharacters(in: CharacterSet(charactersIn: " \t\"'`"))
        guard !trimmed.isEmpty else { return nil }
        let parts = trimmed.split(separator: " ").map(String.init)
        guard let first = parts.first else { return nil }
        return self.expandPath(first, home: home)
    }

    private static func extractPathCandidate(line: String, tool: String, home: String) -> String? {
        let tokens = line.split(whereSeparator: { $0 == " " || $0 == "\t" }).map(String.init)
        for token in tokens {
            let candidate = self.expandPath(token, home: home)
            if candidate.hasPrefix("/"),
               URL(fileURLWithPath: candidate).lastPathComponent == tool
            {
                return candidate
            }
        }
        return nil
    }

    private static func expandPath(_ raw: String, home: String) -> String {
        if raw == "~" {
            return home
        }
        if raw.hasPrefix("~/") {
            return home + String(raw.dropFirst())
        }
        return raw
    }
}

public enum PathBuilder {
    public static func effectivePATH(
        purposes _: Set<PathPurpose>,
        env: [String: String] = ProcessInfo.processInfo.environment,
        loginPATH: [String]? = LoginShellPathCache.shared.current,
        home _: String = NSHomeDirectory()) -> String
    {
        var parts: [String] = []

        if let loginPATH, !loginPATH.isEmpty {
            parts.append(contentsOf: loginPATH)
        }

        if let existing = env["PATH"], !existing.isEmpty {
            parts.append(contentsOf: existing.split(separator: ":").map(String.init))
        }

        if parts.isEmpty {
            parts.append(contentsOf: ["/usr/bin", "/bin", "/usr/sbin", "/sbin"])
        }

        var seen = Set<String>()
        let deduped = parts.compactMap { part -> String? in
            guard !part.isEmpty else { return nil }
            if seen.insert(part).inserted {
                return part
            }
            return nil
        }

        return deduped.joined(separator: ":")
    }

    public static func debugSnapshot(
        purposes: Set<PathPurpose>,
        env: [String: String] = ProcessInfo.processInfo.environment,
        home: String = NSHomeDirectory()) -> PathDebugSnapshot
    {
        let login = LoginShellPathCache.shared.current
        let effective = self.effectivePATH(
            purposes: purposes,
            env: env,
            loginPATH: login,
            home: home)
        let codex = BinaryLocator.resolveCodexBinary(env: env, loginPATH: login, home: home)
        let claude = BinaryLocator.resolveClaudeBinary(env: env, loginPATH: login, home: home)
        let gemini = BinaryLocator.resolveGeminiBinary(env: env, loginPATH: login, home: home)
        let loginString = login?.joined(separator: ":")
        return PathDebugSnapshot(
            codexBinary: codex,
            claudeBinary: claude,
            geminiBinary: gemini,
            effectivePATH: effective,
            loginShellPATH: loginString)
    }

    public static func debugSnapshotAsync(
        purposes: Set<PathPurpose>,
        env: [String: String] = ProcessInfo.processInfo.environment,
        home: String = NSHomeDirectory()) async -> PathDebugSnapshot
    {
        await Task.detached(priority: .userInitiated) {
            self.debugSnapshot(purposes: purposes, env: env, home: home)
        }.value
    }
}

enum LoginShellPathCapturer {
    static let defaultTimeout: TimeInterval = 6.0

    static func capture(
        shell: String? = ProcessInfo.processInfo.environment["SHELL"],
        timeout: TimeInterval = Self.defaultTimeout) -> [String]?
    {
        let shellPath = (shell?.isEmpty == false) ? shell! : "/bin/zsh"
        let isCI = ["1", "true"].contains(ProcessInfo.processInfo.environment["CI"]?.lowercased())
        let marker = "__CODEXBAR_PATH__"
        // Skip interactive login shells in CI to avoid noisy init hooks.
        let args = isCI
            ? ["-c", "printf '\(marker)%s\(marker)' \"$PATH\""]
            : ["-l", "-i", "-c", "printf '\(marker)%s\(marker)' \"$PATH\""]
        guard let data = ShellCommandLocator.runShellCommand(
            shell: shellPath,
            arguments: args,
            timeout: timeout),
            let raw = String(data: data, encoding: .utf8),
            !raw.isEmpty
        else { return nil }

        let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
        let extracted = if let start = trimmed.range(of: marker),
                           let end = trimmed.range(of: marker, options: .backwards),
                           start.upperBound <= end.lowerBound
        {
            String(trimmed[start.upperBound..<end.lowerBound])
        } else {
            trimmed
        }

        let value = extracted.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !value.isEmpty else { return nil }
        return value.split(separator: ":").map(String.init)
    }
}

public final class LoginShellPathCache: @unchecked Sendable {
    public static let shared = LoginShellPathCache()

    private let lock = NSLock()
    private let capture: @Sendable (String?, TimeInterval) -> [String]?
    private var captured: [String]?
    private var isCapturing = false
    private var callbacks: [([String]?) -> Void] = []

    init(capture: @escaping @Sendable (String?, TimeInterval) -> [String]? = LoginShellPathCapturer.capture) {
        self.capture = capture
    }

    public var current: [String]? {
        self.lock.lock()
        let value = self.captured
        self.lock.unlock()
        return value
    }

    public func captureOnce(
        shell: String? = ProcessInfo.processInfo.environment["SHELL"],
        timeout: TimeInterval = 6.0,
        onFinish: (([String]?) -> Void)? = nil)
    {
        self.lock.lock()
        if let captured {
            self.lock.unlock()
            onFinish?(captured)
            return
        }

        if let onFinish {
            self.callbacks.append(onFinish)
        }

        if self.isCapturing {
            self.lock.unlock()
            return
        }

        self.isCapturing = true
        self.lock.unlock()

        let capture = self.capture
        DispatchQueue.global(qos: .utility).async { [weak self] in
            let result = capture(shell, timeout)
            guard let self else { return }

            self.lock.lock()
            self.captured = result
            self.isCapturing = false
            let callbacks = self.callbacks
            self.callbacks.removeAll()
            self.lock.unlock()

            callbacks.forEach { $0(result) }
        }
    }

    public func currentOrCapture(
        shell: String? = ProcessInfo.processInfo.environment["SHELL"],
        timeout: TimeInterval = 6.0) -> [String]?
    {
        self.lock.lock()
        if let captured {
            self.lock.unlock()
            return captured
        }

        if self.isCapturing {
            let semaphore = DispatchSemaphore(value: 0)
            var callbackResult: [String]?
            self.callbacks.append { result in
                callbackResult = result
                semaphore.signal()
            }
            self.lock.unlock()
            let deadline = DispatchTime.now() + timeout
            _ = semaphore.wait(timeout: deadline)
            return callbackResult ?? self.current
        }

        self.isCapturing = true
        self.lock.unlock()

        let result = self.capture(shell, timeout)
        self.lock.lock()
        self.captured = result
        self.isCapturing = false
        let callbacks = self.callbacks
        self.callbacks.removeAll()
        self.lock.unlock()

        callbacks.forEach { $0(result) }
        return result
    }
}
