import Foundation

/// Static profile for one **ACP** (Agent Client Protocol) coding agent — the invocation, auth,
/// and container-seeding facts that differ per agent, factored out so a single generic
/// [`ACPBackend`](ACPBackend.swift) can drive all of them. Every ACP agent speaks standards
/// JSON-RPC 2.0 over stdio with native `session/request_permission` approvals, so above the
/// process spawn they are identical (ADAPTERS §5); only these fields vary.
///
/// **Confidence:** Grok is validated against the shipped `grok agent stdio`; the four wrapper
/// agents (OpenCode/OpenClaw/Hermes/Cursor) are pinned from each vendor's ACP docs (the `<bin> acp`
/// invocation is confirmed) but their auth env-var names / on-disk credential paths are
/// best-effort and marked 🔴 — re-pin from a live capture, exactly as Grok was (GROK_ADAPTER §6).
public struct ACPAgent: Sendable, Equatable {
    /// The backend identity this profile drives.
    public let backend: BackendID
    /// User-facing agent name (Settings, model catalog), e.g. "OpenCode".
    public let displayName: String
    /// CLI executable, resolved on PATH, e.g. "opencode".
    public let executable: String
    /// Arguments that put the CLI into ACP-server mode over stdio, e.g. `["acp"]`.
    public let launchArgs: [String]
    /// Model SKU the picker offers for this agent — the picker doubles as the backend selector
    /// (`BackendID.forModel`). A single SKU per wrapper agent (they're model-agnostic).
    public let sku: String
    /// `$HOME`-relative directories holding this agent's login/config, seeded into a per-session
    /// container home so a sandboxed run authenticates as the user (Codex/Grok analog,
    /// SessionController). Host runs inherit auth directly and never seed. 🔴 best-effort for the
    /// wrapper agents.
    public let containerHomeDirs: [String]
    /// Environment variables that, when present and non-blank, count as authenticated (checked
    /// before any on-disk credential). 🔴 names inferred for the wrapper agents.
    public let authEnvVars: [String]
    /// `$HOME`-relative credential file whose existence indicates a completed CLI login. 🔴 path
    /// inferred for the wrapper agents.
    public let credentialFile: String?
    /// True when authentication is negotiated by the agent itself rather than inferred from a
    /// vendor-specific credential.
    public let authenticationIsManagedByAgent: Bool
    /// Whether this executable is present in Nucleic's bundled Linux sandbox image.
    public let supportsContainerizedRuns: Bool

    public init(
        backend: BackendID, displayName: String, executable: String, launchArgs: [String],
        sku: String, containerHomeDirs: [String], authEnvVars: [String], credentialFile: String?,
        authenticationIsManagedByAgent: Bool = false, supportsContainerizedRuns: Bool = false
    ) {
        self.backend = backend
        self.displayName = displayName
        self.executable = executable
        self.launchArgs = launchArgs
        self.sku = sku
        self.containerHomeDirs = containerHomeDirs
        self.authEnvVars = authEnvVars
        self.credentialFile = credentialFile
        self.authenticationIsManagedByAgent = authenticationIsManagedByAgent
        self.supportsContainerizedRuns = supportsContainerizedRuns
    }

    public static let genericExecutableDefaultsKey = "agents.acp.executable"
    public static let genericArgumentsDefaultsKey = "agents.acp.arguments"
    public static let genericExecutableEnvironmentKey = "NUCLEIC_ACP_EXECUTABLE"
    public static let genericArgumentsEnvironmentKey = "NUCLEIC_ACP_ARGUMENTS"

    /// xAI Grok Build — `grok agent stdio` (validated; GROK_ADAPTER).
    public static let grok = ACPAgent(
        backend: .grok, displayName: "Grok Build", executable: "grok",
        launchArgs: ["agent", "stdio"], sku: "grok-build",
        containerHomeDirs: [".grok"],
        authEnvVars: ["XAI_API_KEY", "GROK_CODE_XAI_API_KEY"],
        credentialFile: ".grok/config.toml", supportsContainerizedRuns: true)

    /// SST OpenCode — `opencode acp` (opencode.ai/docs/acp). Stores auth under
    /// `~/.local/share/opencode`; also honors provider keys (already forwarded).
    public static let opencode = ACPAgent(
        backend: .opencode, displayName: "OpenCode", executable: "opencode",
        launchArgs: ["acp"], sku: "opencode",
        containerHomeDirs: [".config/opencode", ".local/share/opencode"],
        authEnvVars: ["OPENCODE_API_KEY"],
        credentialFile: ".local/share/opencode/auth.json")

    /// Nous Research Hermes Agent — `hermes acp` (hermes-agent.nousresearch.com/docs). Model-
    /// agnostic (drives Claude/Codex under the hood), so it also honors those provider keys.
    public static let hermes = ACPAgent(
        backend: .hermes, displayName: "Hermes", executable: "hermes",
        launchArgs: ["acp"], sku: "hermes",
        containerHomeDirs: [".config/hermes", ".hermes"],
        authEnvVars: ["HERMES_API_KEY", "NOUS_API_KEY"],
        credentialFile: ".config/hermes/auth.json")

    /// Cursor CLI agent — `cursor-agent acp` (cursor.com/docs/cli/acp). Auth via `CURSOR_API_KEY`
    /// or `cursor-agent login`.
    public static let cursorAgent = ACPAgent(
        backend: .cursorAgent, displayName: "Cursor Agent", executable: "cursor-agent",
        launchArgs: ["acp"], sku: "cursor-agent",
        containerHomeDirs: [".cursor", ".config/cursor"],
        authEnvVars: ["CURSOR_API_KEY"],
        credentialFile: ".cursor/cli-config.json")

    /// OpenClaw — `openclaw acp` (docs.openclaw.ai/cli/acp). Unlike the other wrappers this is a
    /// **bridge**, not a self-contained harness: it speaks ACP over stdio but forwards every prompt
    /// over WebSocket to a running OpenClaw Gateway, which owns the actual model + tools. A
    /// reachable Gateway is therefore a prerequisite — `openclaw` being on PATH is necessary but
    /// not sufficient, and the availability probe below can't see the Gateway (see
    /// `ProviderAvailability`). Auth is the Gateway token/password. 🔴 credential path best-effort:
    /// pinned from the docs' `--token-file ~/.openclaw/gateway.token` example, not a live capture.
    public static let openclaw = ACPAgent(
        backend: .openclaw, displayName: "OpenClaw", executable: "openclaw",
        launchArgs: ["acp"], sku: "openclaw",
        containerHomeDirs: [".openclaw", ".config/openclaw"],
        authEnvVars: ["OPENCLAW_GATEWAY_TOKEN", "OPENCLAW_GATEWAY_PASSWORD"],
        credentialFile: ".openclaw/gateway.token")

    /// Built-in ACP profiles, in display order.
    public static let all: [ACPAgent] = [grok, opencode, openclaw, hermes, cursorAgent]

    /// The user's catch-all ACP profile. Environment variables take precedence so the headless
    /// runner can be configured without macOS defaults. Arguments use ordinary shell-style
    /// whitespace, quotes, and backslash escaping, but are passed directly to the process.
    public static func generic(
        defaults: UserDefaults = .standard,
        environment: [String: String] = ProcessInfo.processInfo.environment
    ) -> ACPAgent {
        let configuredExecutable = environment[genericExecutableEnvironmentKey]
            ?? defaults.string(forKey: genericExecutableDefaultsKey)
        let executable = configuredExecutable?.trimmingCharacters(in: .whitespacesAndNewlines)
        let configuredArguments = environment[genericArgumentsEnvironmentKey]
            ?? defaults.string(forKey: genericArgumentsDefaultsKey)
            ?? ""
        return ACPAgent(
            backend: .acp,
            displayName: "Custom ACP",
            executable: executable.flatMap { $0.isEmpty ? nil : $0 } ?? "acp-agent",
            launchArgs: splitArguments(configuredArguments),
            sku: "acp-agent",
            containerHomeDirs: [],
            authEnvVars: [],
            credentialFile: nil,
            authenticationIsManagedByAgent: true)
    }

    /// All currently configured profiles, including the user-defined catch-all profile.
    public static var configured: [ACPAgent] { all + [generic()] }

    /// The profile for a backend id, or nil if that backend isn't ACP-based.
    public static func forBackend(_ backend: BackendID) -> ACPAgent? {
        if backend == .acp { return generic() }
        return all.first { $0.backend == backend }
    }

    /// Split an argument field without invoking a shell. Supports single/double quotes and
    /// backslash escaping, including empty quoted arguments.
    public static func splitArguments(_ source: String) -> [String] {
        var result: [String] = []
        var current = ""
        var quote: Character?
        var escaping = false
        var hasToken = false

        func finishToken() {
            guard hasToken else { return }
            result.append(current)
            current = ""
            hasToken = false
        }

        for character in source {
            if escaping {
                current.append(character)
                escaping = false
                hasToken = true
            } else if character == "\\" {
                escaping = true
                hasToken = true
            } else if let activeQuote = quote {
                if character == activeQuote { quote = nil } else { current.append(character) }
                hasToken = true
            } else if character == "\"" || character == "'" {
                quote = character
                hasToken = true
            } else if character.isWhitespace {
                finishToken()
            } else {
                current.append(character)
                hasToken = true
            }
        }
        if escaping { current.append("\\") }
        finishToken()
        return result
    }
}
