import Foundation
import NucleicProtocol

/// Install + auth state of one agent provider (backend), for the Settings → Agents →
/// Backend list. A provider is "connected" only when its CLI is both **installed**
/// (resolvable on the same PATH the app spawns sessions with) and **authenticated**
/// (a usable credential is present — an API-key env var, the login keychain item, or the
/// CLI's on-disk credential file).
public struct ProviderStatus: Sendable, Identifiable, Equatable {
    public let backend: BackendID
    /// User-facing provider/agent name, e.g. "Claude Code".
    public let name: String
    /// The CLI executable that backs this provider, e.g. "claude".
    public let executable: String
    /// The CLI resolves on PATH (a `<exe> --version` probe didn't come back "not found").
    public let installed: Bool
    /// A usable credential is present.
    public let authenticated: Bool

    public var id: String { backend.rawValue }

    /// Installed *and* authenticated — the green-dot / "Connected" state.
    public var isConnected: Bool { installed && authenticated }

    /// Hover/accessibility detail distinguishing the two not-connected modes.
    public var detail: String {
        switch (installed, authenticated) {
        case (true, true): return "Installed and authenticated"
        case (true, false): return "Installed, but not signed in"
        case (false, _): return "Not installed"
        }
    }

    public init(
        backend: BackendID, name: String, executable: String,
        installed: Bool, authenticated: Bool
    ) {
        self.backend = backend
        self.name = name
        self.executable = executable
        self.installed = installed
        self.authenticated = authenticated
    }
}

/// Probes whether each agent provider's CLI is installed and authenticated. Stateless and
/// safe to call from the main actor — the install probes shell out off-main via `ProcessHost`,
/// matching how the backends themselves are spawned, so a probe's verdict reflects what an
/// actual session launch could do (same PATH, same env).
public enum ProviderAvailability {
    /// One provider's identity plus how to tell whether it's authenticated. Listed in the
    /// order shown in Settings; mirrors the `BackendID.forModel` routing.
    private struct Probe {
        let backend: BackendID
        let name: String
        let executable: String
        /// Cheap, prompt-free auth heuristic; the env-var branch is checked first so a
        /// configured key short-circuits before any keychain/disk access.
        let isAuthenticated: @Sendable () -> Bool
    }

    private static let probes: [Probe] = [
        Probe(backend: .claudeCode, name: "Claude Code", executable: "claude",
              isAuthenticated: {
                  // ANTHROPIC_API_KEY (Claude Code prefers it over OAuth), else the login
                  // keychain item Claude Code writes (read via the existing usage fetcher).
                  envKeyPresent("ANTHROPIC_API_KEY") || SubscriptionUsageFetcher.oauthToken() != nil
              }),
        Probe(backend: .codex, name: "Codex", executable: "codex",
              isAuthenticated: {
                  // OPENAI_API_KEY, else the credentials `codex login` writes.
                  envKeyPresent("OPENAI_API_KEY") || credentialFileExists(".codex/auth.json")
              }),
        Probe(backend: .grok, name: "Grok Build", executable: "grok",
              isAuthenticated: {
                  // XAI_API_KEY (Grok Build prefers it over login), else the config
                  // `grok auth login` persists under ~/.grok (GROK_ADAPTER §1).
                  envKeyPresent("XAI_API_KEY") || credentialFileExists(".grok/config.toml")
              }),
    ]

    /// Probe every provider concurrently, returning their statuses in display order.
    public static func probeAll(processHost: ProcessHost = ProcessHost()) async -> [ProviderStatus] {
        await withTaskGroup(of: (Int, ProviderStatus).self) { group in
            for (index, probe) in probes.enumerated() {
                group.addTask {
                    let installed = await isInstalled(probe.executable, processHost: processHost)
                    return (index, ProviderStatus(
                        backend: probe.backend, name: probe.name, executable: probe.executable,
                        installed: installed, authenticated: probe.isAuthenticated()))
                }
            }
            var collected: [(Int, ProviderStatus)] = []
            for await result in group { collected.append(result) }
            return collected.sorted { $0.0 < $1.0 }.map(\.1)
        }
    }

    // MARK: - Installed

    /// True when `<exe> --version` resolves and runs. `ProcessHost` resolves a bare name
    /// through `/usr/bin/env`, which exits 127 for "command not found" and 126 for "found but
    /// not executable"; any other exit means the binary is on PATH. (This probes host agent CLIs;
    /// container-runtime support is a separate static check — `ContainerEngine.isSupported`.)
    private static func isInstalled(_ executable: String, processHost: ProcessHost) async -> Bool {
        let spec = ProcessSpec(
            executable: executable, args: ["--version"], cwd: NSTemporaryDirectory(),
            env: [:], stdinMode: .closed)
        guard let handle = try? await processHost.launch(spec) else { return false }
        // Drain both pipes concurrently so the child can exit instead of blocking on a full
        // stdout buffer; the readers finish at EOF when it does.
        async let out: Void = drain(handle.stdoutLines)
        async let err: Void = drain(handle.stderrLines)
        let status = await handle.wait()
        _ = await out
        _ = await err
        return status != 127 && status != 126
    }

    private static func drain(_ stream: AsyncThrowingStream<Data, Error>) async {
        do { for try await _ in stream {} } catch { /* partial output is irrelevant here */ }
    }

    // MARK: - Authenticated

    /// True when the named environment variable is present and not just whitespace. (The app
    /// purges blank `ANTHROPIC_API_KEY`/`XAI_API_KEY` at launch, but trim defensively anyway.)
    private static func envKeyPresent(_ name: String) -> Bool {
        guard let value = ProcessInfo.processInfo.environment[name] else { return false }
        return !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
    }

    /// True when a credential file exists at `~/<relativePath>`.
    private static func credentialFileExists(_ relativePath: String) -> Bool {
        let url = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(relativePath)
        return FileManager.default.fileExists(atPath: url.path)
    }
}
