import Foundation

// Identifiers (SessionID/ApprovalID/BackendID/WorktreePath) and AgentInput now live in
// NucleicProtocol/CoreIdentifiers.swift so the sync wire layer shares them with iOS.
// NucleicCore re-exports NucleicProtocol (ProtocolExports.swift), so they stay in scope here.

// MARK: - Capabilities (BACKEND_PROTOCOL §2.1)

public struct BackendCapabilities: Sendable {
    public var interactiveApprovals: Bool
    public var allowAlwaysScopes: Set<AlwaysScope>
    public var canModifyToolInput: Bool
    public var partialMessageStreaming: Bool
    public var emitsThinking: Bool
    public var emitsFileChangeEvents: Bool
    public var nativeResume: Bool
    public var sandboxModes: [SandboxMode]
    public var followUpWhileRunning: Bool

    public init(
        interactiveApprovals: Bool,
        allowAlwaysScopes: Set<AlwaysScope>,
        canModifyToolInput: Bool,
        partialMessageStreaming: Bool,
        emitsThinking: Bool,
        emitsFileChangeEvents: Bool,
        nativeResume: Bool,
        sandboxModes: [SandboxMode],
        followUpWhileRunning: Bool
    ) {
        self.interactiveApprovals = interactiveApprovals
        self.allowAlwaysScopes = allowAlwaysScopes
        self.canModifyToolInput = canModifyToolInput
        self.partialMessageStreaming = partialMessageStreaming
        self.emitsThinking = emitsThinking
        self.emitsFileChangeEvents = emitsFileChangeEvents
        self.nativeResume = nativeResume
        self.sandboxModes = sandboxModes
        self.followUpWhileRunning = followUpWhileRunning
    }
}

public enum SandboxMode: String, Sendable, Codable {
    case readOnly = "read-only"
    case workspaceWrite = "workspace-write"
    case fullAccess = "danger-full-access"
}

/// Request to run a session's `claude` process inside an isolated Linux VM (built on Apple's
/// `containerization` framework) instead of on the host. Built by `SessionController` from
/// `Project.sandbox` and carried on `RunSpec`/`ResumeSpec`; consumed by `ClaudeCodeBackend` (which
/// `exec`s the invocation inside the running container VM via `ContainerManager`/`ContainerEngine`)
/// and `ContainerManager` (lifecycle). `nil` → host-spawned.
public struct ContainerSpec: Sendable, Equatable {
    /// One bind mount: a host path made visible inside the container.
    public struct Mount: Sendable, Equatable {
        public let host: String
        public let container: String
        public let readOnly: Bool
        public init(host: String, container: String, readOnly: Bool) {
            self.host = host
            self.container = container
            self.readOnly = readOnly
        }
    }

    /// Stable container name for this session (e.g. `nucleic-<sessionID.short>`).
    public let name: String
    /// Image reference to run (already resolved to the bundled default if no override).
    public let image: String
    /// Bind mounts. Repo root + worktree base are mounted at their identical host paths so
    /// git worktree links and the cwd-hash resolve; `~/.claude` is mounted read-only at a
    /// staging path and seeded into a writable claude-home.
    public let mounts: [Mount]
    /// Working directory inside the container (the session's worktree path).
    public let workdir: String
    /// Environment passed into the `claude` process (e.g. ANTHROPIC_API_KEY).
    public let env: [String: String]
    /// Stop the container after this much inactivity.
    public let idleTimeout: TimeInterval
    /// Read-only staging path where the host `~/.claude` is mounted inside the container.
    public let claudeHomeStaging: String
    /// Writable path seeded from the staging copy and used as the container's `~/.claude`.
    public let claudeHomeWritable: String
    /// Install Nucleic's `git` interceptor shim ahead of real git on PATH (Nucleic Control
    /// containers). The shim execs real git and reports each invocation back to the host so
    /// merges and other git ops are observed with certainty. The runtime callback URL + token
    /// are injected into the exec env by `ClaudeCodeBackend` (gateway/port/token aren't known
    /// until the container and approval server are up), so the spec only carries the flag.
    public let installGitInterceptor: Bool
    /// CPU count for the container VM (`container run --cpus`).
    public let cpus: Int
    /// Memory for the container VM in GiB (`container run --memory Ng`).
    public let memoryGiB: Int
    /// UID the agent (`claude` + its subprocesses) runs as inside the container, set on the exec's
    /// `process.user`. `nil` → root (uid 0). Set to the *host* uid for a sandboxed session so the
    /// agent owns the bind-mounted repo/worktree/claude-home (virtiofs presents the host owner on
    /// the shared mounts) while the root-installed instrumentation (the git + command shims, the
    /// bash tracer, `/etc/gitconfig`) stays read-only to it — the agent can run it but not tamper
    /// with it. The container's own root process (`sleep infinity`) and the admin execs that install
    /// the instrumentation still run as root; only the agent drops down.
    public let runAsUID: Int?
    /// GID paired with `runAsUID` (the host gid). `nil` → root's group.
    public let runAsGID: Int?
    /// Host path of the per-**container** control socket — the token-multiplexed approval server +
    /// interceptor endpoint. When set, ``ContainerEngine`` relays it into the guest over vsock (the
    /// framework's `UnixSocketConfiguration(.into)`), where it appears at ``controlSocketGuestPath``,
    /// so the agent's MCP bridge and the git/gh/command shims reach the host with **no IP listener**
    /// (hence no macOS incoming-connection / local-network prompts). `nil` → no relay (the legacy
    /// TCP-over-vmnet-gateway path). MUST be short: AF_UNIX `sun_path` caps at ~104 bytes, so it
    /// lives under a short dir (e.g. `/tmp`), never the long Application-Support session path.
    public let controlSocketHostPath: String?

    /// Fixed in-guest path the relayed control socket appears at; the in-container control bridge
    /// forwards to it. Paired with ``controlSocketHostPath`` on the host side.
    public static let controlSocketGuestPath = "/run/nucleic/control.sock"

    /// Fixed in-guest loopback port the control bridge listens on. The agent's MCP client and the
    /// git/gh/command interceptor shims target `http://127.0.0.1:<this>` (loopback, inside the VM
    /// netns — invisible to macOS); the bridge forwards to ``controlSocketGuestPath``. Must match
    /// the default in `containers/nucleic-sandbox/control-bridge.js`.
    public static let controlBridgePort: UInt16 = 9099

    public init(
        name: String,
        image: String,
        mounts: [Mount],
        workdir: String,
        env: [String: String],
        idleTimeout: TimeInterval,
        claudeHomeStaging: String,
        claudeHomeWritable: String,
        installGitInterceptor: Bool = false,
        cpus: Int = ContainerServiceSettings.defaultContainerCPUs,
        memoryGiB: Int = ContainerServiceSettings.defaultContainerMemoryGiB,
        runAsUID: Int? = nil,
        runAsGID: Int? = nil,
        controlSocketHostPath: String? = nil
    ) {
        self.name = name
        self.image = image
        self.mounts = mounts
        self.workdir = workdir
        self.env = env
        self.idleTimeout = idleTimeout
        self.claudeHomeStaging = claudeHomeStaging
        self.claudeHomeWritable = claudeHomeWritable
        self.installGitInterceptor = installGitInterceptor
        self.cpus = max(1, cpus)
        self.memoryGiB = max(1, memoryGiB)
        self.runAsUID = runAsUID
        self.runAsGID = runAsGID
        self.controlSocketHostPath = controlSocketHostPath
    }

    /// A copy of this spec under a different container `name`. `ContainerManager` runs shared control
    /// containers under a randomized **physical** name (so an agent can't recognize or target a
    /// sibling sandbox) while keeping the stable logical name as its public key — this renames the
    /// spec at the engine boundary.
    public func renamed(_ newName: String) -> ContainerSpec {
        ContainerSpec(
            name: newName, image: image, mounts: mounts, workdir: workdir, env: env,
            idleTimeout: idleTimeout, claudeHomeStaging: claudeHomeStaging,
            claudeHomeWritable: claudeHomeWritable, installGitInterceptor: installGitInterceptor,
            cpus: cpus, memoryGiB: memoryGiB, runAsUID: runAsUID, runAsGID: runAsGID,
            controlSocketHostPath: controlSocketHostPath)
    }
}

// MARK: - Run / resume / input specs (BACKEND_PROTOCOL §2.2)

public enum ApprovalPolicy: Sendable {
    case interactive
    /// Non-interactive backends (Codex exec) or unattended runs: a fixed rule set,
    /// fail-closed for anything unmatched (BACKEND_PROTOCOL §4.2).
    case fixed(rules: [FixedRule])

    public struct FixedRule: Sendable {
        public enum Effect: Sendable { case allow, deny }
        public let toolName: String
        public let pattern: String?
        public let effect: Effect

        public init(toolName: String, pattern: String? = nil, effect: Effect) {
            self.toolName = toolName
            self.pattern = pattern
            self.effect = effect
        }
    }
}

public struct RunSpec: Sendable {
    public let sessionID: SessionID
    public let worktree: WorktreePath
    public let prompt: AgentInput
    public let model: String?
    public let effort: String?
    /// Run with Claude's auto permission mode (its classifier auto-approves safe
    /// actions; the rest still route to our approval tool).
    public let autoApprove: Bool
    public let approvalPolicy: ApprovalPolicy
    public let sandbox: SandboxMode?
    /// When set, run `claude` inside this Apple `container` instead of on the host.
    public let container: ContainerSpec?
    /// Expose the `host_exec` tool so the (sandboxed) agent can request to build/run on the
    /// host, each call approval-gated and never auto-approved (HOST_EXEC).
    public let allowHostExec: Bool
    /// Orchestra (orchestration mode) is in effect for this run — the user selected it AND the
    /// project is under Nucleic Control (`SessionController.orchestraActive`). The Claude backend
    /// uses it to pre-allow the subagent-spawn tools (`Task`/`Agent`) so a fan-out isn't
    /// interrupted by an approval prompt on every spawn; other backends ignore it. The fan-out
    /// consent itself rides in separately on `appendSystemPrompt`.
    public let orchestraActive: Bool
    public let mcpConfigPath: URL?
    public let appendSystemPrompt: String?
    public let extraEnv: [String: String]
    public let extraArgs: [String]

    public init(
        sessionID: SessionID,
        worktree: WorktreePath,
        prompt: AgentInput,
        model: String? = nil,
        effort: String? = nil,
        autoApprove: Bool = false,
        approvalPolicy: ApprovalPolicy = .interactive,
        sandbox: SandboxMode? = nil,
        container: ContainerSpec? = nil,
        allowHostExec: Bool = false,
        orchestraActive: Bool = false,
        mcpConfigPath: URL? = nil,
        appendSystemPrompt: String? = nil,
        extraEnv: [String: String] = [:],
        extraArgs: [String] = []
    ) {
        self.sessionID = sessionID
        self.worktree = worktree
        self.prompt = prompt
        self.model = model
        self.effort = effort
        self.autoApprove = autoApprove
        self.approvalPolicy = approvalPolicy
        self.sandbox = sandbox
        self.container = container
        self.allowHostExec = allowHostExec
        self.orchestraActive = orchestraActive
        self.mcpConfigPath = mcpConfigPath
        self.appendSystemPrompt = appendSystemPrompt
        self.extraEnv = extraEnv
        self.extraArgs = extraArgs
    }
}

public struct ResumeSpec: Sendable {
    public let sessionID: SessionID
    public let backendSessionID: String
    public let worktree: WorktreePath
    /// The follow-up user message for this resumed turn (nil = re-attach only).
    public let prompt: AgentInput?
    public let model: String?
    public let effort: String?
    public let autoApprove: Bool
    public let fork: Bool
    /// When set, resume `claude` inside this Apple `container` instead of on the host.
    public let container: ContainerSpec?
    /// Expose the `host_exec` tool on this resumed turn (HOST_EXEC). See `RunSpec`.
    public let allowHostExec: Bool
    /// Orchestra is in effect for this resumed turn — pre-allow the subagent-spawn tools so a
    /// fan-out isn't interrupted by an approval prompt on every spawn. Re-applied each turn (the
    /// CLI doesn't persist `--allowedTools` across `--resume`), since every follow-up Claude turn
    /// is a fresh single-shot `--resume` run. See `RunSpec.orchestraActive`.
    public let orchestraActive: Bool
    /// Re-applied every resumed turn (the CLI doesn't persist it across `--resume`), so
    /// environment guidance like the sandbox build instructions stays in effect. See `RunSpec`.
    public let appendSystemPrompt: String?

    public init(
        sessionID: SessionID, backendSessionID: String, worktree: WorktreePath,
        prompt: AgentInput? = nil, model: String? = nil, effort: String? = nil,
        autoApprove: Bool = false, fork: Bool = false, container: ContainerSpec? = nil,
        allowHostExec: Bool = false, orchestraActive: Bool = false,
        appendSystemPrompt: String? = nil
    ) {
        self.sessionID = sessionID
        self.backendSessionID = backendSessionID
        self.worktree = worktree
        self.prompt = prompt
        self.model = model
        self.effort = effort
        self.autoApprove = autoApprove
        self.fork = fork
        self.container = container
        self.allowHostExec = allowHostExec
        self.orchestraActive = orchestraActive
        self.appendSystemPrompt = appendSystemPrompt
    }
}

// AgentInput moved to NucleicProtocol/CoreIdentifiers.swift (now Codable for the wire).

// MARK: - The backend protocol (BACKEND_PROTOCOL §2)

public protocol AgentBackend: Sendable {
    static var id: BackendID { get }
    var capabilities: BackendCapabilities { get }

    /// Begin a fresh run in a prepared worktree. Returns the live event stream.
    func start(_ run: RunSpec) -> AsyncThrowingStream<AgentEvent, Error>

    /// Reattach to an existing backend session (native resume).
    func resume(_ resume: ResumeSpec) -> AsyncThrowingStream<AgentEvent, Error>

    /// Queue a follow-up user turn on a running session.
    func send(_ input: AgentInput) async throws

    /// Answer an outstanding approval. Throws if the backend lacks interactive approvals.
    func respond(to approvalID: ApprovalID, _ decision: Decision) async throws

    /// Cooperative interrupt of the current turn (keeps session resumable).
    func interrupt() async

    /// Terminate the process/connection and release resources.
    func shutdown() async
}

public enum BackendError: Error, Sendable {
    case unsupported(String)
    case notRunning
    case spawnFailed(String)
    case protocolViolation(String)
}

/// Shared diagnostics for the agent backends (Claude/Codex), so an abnormal CLI exit reads the
/// same way no matter which backend spawned it.
public enum BackendDiagnostics {
    /// True for the exit codes that mean "killed by SIGKILL": `137 = 128 + 9`, or a bare `-9`
    /// (Foundation reports a process killed by signal N as `-N` when the wrapper itself is signalled).
    public static func isSIGKILL(_ exitCode: Int32) -> Bool { exitCode == 137 || exitCode == -9 }

    /// Fallback message for an abnormal (non-zero) CLI exit when no container post-mortem is
    /// available — host-spawned agents, or a containerized exit we couldn't probe. Deliberately
    /// does NOT assert OOM: a SIGKILL has several causes (see `containerKillMessage`), so it only
    /// names the signal. Containerized 137s should go through `containerKillMessage` instead, which
    /// checks what actually happened.
    public static func abnormalExitMessage(
        tool: String, exitCode: Int32, stderrTail: String, containerized: Bool
    ) -> String {
        let suffix = stderrSuffix(stderrTail, joiner: ": ", emptyEnd: "")
        if isSIGKILL(exitCode) {
            let lever = containerized
                ? " — the sandbox may have run out of memory or been stopped"
                : " — possibly out of memory"
            return "\(tool) was killed (SIGKILL, status 137)\(lever)\(suffix.isEmpty ? "." : suffix)"
        }
        return "\(tool) exited with status \(exitCode)\(suffix)"
    }

    /// Build the message for a containerized agent killed by SIGKILL, from a kill-time post-mortem
    /// of the container (`ContainerEngine.diagnoseKill`). Reports what actually happened instead of
    /// assuming OOM — because the symptom "all sessions in the container die at once with memory
    /// low" is the container going *down*, not a per-process OOM:
    ///   • container gone   → it was stopped/restarted/crashed (kills every session at once); not memory.
    ///   • `oom_kill` > 0   → a real kernel OOM kill (memory can read low now — the kill freed it).
    ///   • memory near full → most likely OOM even without the counter.
    ///   • healthy + low    → something *else* killed it (a competing agent, a brief spike); not memory.
    public static func containerKillMessage(
        tool: String, diagnosis: ContainerKillDiagnosis, stderrTail: String
    ) -> String {
        let suffix = stderrSuffix(stderrTail, joiner: " — ", emptyEnd: "")
        let head = "\(tool) was killed (SIGKILL, status 137)"

        if !diagnosis.containerRunning {
            return head + " — the sandbox container is no longer running. It was stopped, restarted, "
                + "or crashed, which takes down every session inside it at once. This usually isn't "
                + "an out-of-memory problem — check the container runtime, not the memory limit." + suffix
        }
        if let oom = diagnosis.oomKills, oom > 0 {
            let n = oom == 1 ? "a process" : "\(oom) processes"
            return head + " — the sandbox container's kernel has OOM-killed \(n), so it did hit its "
                + "memory limit" + memNote(diagnosis.sample, prefix: " (memory reads ", suffix: " now — "
                + "the kill already freed it)") + ". Raise the container's memory in Settings → "
                + "Sandbox, or run fewer sessions at once." + suffix
        }
        if let s = diagnosis.sample, s.memoryPercent >= 85 {
            return head + " — the sandbox container's memory is nearly full"
                + memNote(s, prefix: " (", suffix: ")") + ", most likely out of memory. Raise the "
                + "container's memory in Settings → Sandbox, or run fewer sessions at once." + suffix
        }
        return head + " — but the sandbox container is still running and healthy"
            + memNote(diagnosis.sample, prefix: " (memory only ", suffix: ")") + ", so this is NOT an "
            + "out-of-memory kill. Something inside the sandbox killed the process — e.g. a competing "
            + "agent, or the OOM killer acting on a brief spike that has since cleared." + suffix
    }

    /// "1.2/8.0 GB, 15%" — a compact memory note, or "" when there's no usable sample.
    private static func memNote(
        _ sample: ContainerResourceSample?, prefix: String, suffix: String
    ) -> String {
        guard let s = sample, s.memoryTotalBytes > 0 else { return "" }
        func gb(_ b: UInt64) -> String { String(format: "%.1f", Double(b) / 1_073_741_824) }
        return prefix + "\(gb(s.memoryUsedBytes))/\(gb(s.memoryTotalBytes)) GB, "
            + "\(Int(s.memoryPercent.rounded()))%" + suffix
    }

    /// Trimmed stderr tail joined onto a message, or "" when there's nothing to add.
    private static func stderrSuffix(_ tail: String, joiner: String, emptyEnd: String) -> String {
        let trimmed = tail.trimmingCharacters(in: .whitespacesAndNewlines)
        return trimmed.isEmpty ? emptyEnd : joiner + trimmed
    }
}
