import Foundation

/// `AgentBackend` over `codex exec --json` — the **unattended** Codex path (ADAPTERS §3), for
/// `ApprovalPolicy.fixed` runs. `codex exec` has no approval channel, so it is fail-closed:
/// `interactiveApprovals = false`, run with `--ask-for-approval never` inside a sandbox. Its
/// JSONL surface is a distinct snake_case schema decoded by `CodexExecDecoder`.
///
/// Like the other backends each turn is a single-shot process: `start` runs `codex exec`;
/// `resume` runs `codex exec resume <sessionId>`. Validated against codex-cli 0.141.0.
public actor CodexExecBackend: AgentBackend {
    public static let id = BackendID.codexExec

    public nonisolated let capabilities = BackendCapabilities(
        interactiveApprovals: false,
        allowAlwaysScopes: [],
        canModifyToolInput: false,
        partialMessageStreaming: false,
        emitsThinking: true,
        emitsFileChangeEvents: true,
        nativeResume: true,
        sandboxModes: [.readOnly, .workspaceWrite, .fullAccess],
        followUpWhileRunning: false)

    public struct Configuration: Sendable {
        public var executable: String
        public var now: @Sendable () -> Date
        public init(
            executable: String = "codex",
            now: @escaping @Sendable () -> Date = { Date() }
        ) {
            self.executable = executable
            self.now = now
        }
    }

    private let configuration: Configuration
    private let processHost: ProcessHost
    public let approvals: ApprovalCoordinator  // unused (no interactive approvals); kept for parity
    /// Optional sandbox orchestrator: execs `codex exec` inside a vsock control container when a run
    /// carries one (isolation for unattended/autoship runs). `nil` → always host.
    private let containerManager: ContainerManager?
    private var activeContainerName: String?
    /// Conflict coordinator + approval-server registry — for the git/gh/command interceptor only.
    /// `codex exec` runs `--ask-for-approval never`, so there is no interactive seam to *acquire*
    /// locks on edits; it joins the lock system on the **observe/release** side (merges → release +
    /// autoship + the activity feed), not the acquire side.
    private let conflictCoordinator: ConflictCoordinator?
    private let approvalServerRegistry: ApprovalServerRegistry?
    private var interceptorServer: MCPApprovalServer?
    private var interceptorToken: String?

    private var handle: (any ProcessHandle)?
    private var continuation: AsyncThrowingStream<AgentEvent, Error>.Continuation?
    private var sessionID: SessionID?
    private var provisionalSeq: UInt64 = 0
    private var interruptRequested = false
    private var terminating = false
    private var sawRunFinished = false
    public private(set) var backendSessionID: String?

    public init(
        configuration: Configuration = Configuration(),
        processHost: ProcessHost = ProcessHost(),
        approvals: ApprovalCoordinator = ApprovalCoordinator(),
        containerManager: ContainerManager? = nil,
        conflictCoordinator: ConflictCoordinator? = nil,
        approvalServerRegistry: ApprovalServerRegistry? = nil
    ) {
        self.configuration = configuration
        self.processHost = processHost
        self.approvals = approvals
        self.containerManager = containerManager
        self.conflictCoordinator = conflictCoordinator
        self.approvalServerRegistry = approvalServerRegistry
    }

    // MARK: - AgentBackend

    public nonisolated func start(_ run: RunSpec) -> AsyncThrowingStream<AgentEvent, Error> {
        makeStream(run: run, resumeSessionID: nil)
    }

    public nonisolated func resume(_ resume: ResumeSpec) -> AsyncThrowingStream<AgentEvent, Error> {
        let run = RunSpec(
            sessionID: resume.sessionID, worktree: resume.worktree,
            prompt: resume.prompt ?? AgentInput(parts: []),
            model: resume.model, effort: resume.effort, container: resume.container)
        return makeStream(run: run, resumeSessionID: resume.backendSessionID)
    }

    public func send(_ input: AgentInput) async throws {
        throw BackendError.unsupported("codex exec is single-shot; follow-ups resume a new run")
    }

    public func respond(to approvalID: ApprovalID, _ decision: Decision) async throws {
        throw BackendError.unsupported("codex exec has no interactive approvals (fail-closed)")
    }

    public func interrupt() async {
        interruptRequested = true
        // `codex exec` is single-shot; SIGINT ends it, escalating to a group SIGKILL if it ignores
        // the interrupt so Stop always takes effect.
        handle?.interruptThenKill()
    }

    public func shutdown() async {
        terminating = true
        guard let handle else { return }
        handle.closeStdin()
        await handle.terminate()
        self.handle = nil
    }

    // MARK: - Run loop

    private nonisolated func makeStream(run: RunSpec, resumeSessionID: String?)
        -> AsyncThrowingStream<AgentEvent, Error>
    {
        AsyncThrowingStream { continuation in
            let task = Task {
                await self.runLoop(run: run, resumeSessionID: resumeSessionID, continuation: continuation)
            }
            continuation.onTermination = { _ in task.cancel() }
        }
    }

    private func runLoop(
        run: RunSpec,
        resumeSessionID: String?,
        continuation: AsyncThrowingStream<AgentEvent, Error>.Continuation
    ) async {
        self.continuation = continuation
        self.sessionID = run.sessionID
        self.sawRunFinished = false
        self.interruptRequested = false

        let stderrTail = CodexExecStderrTail()
        do {
            let prompt = run.prompt.plainText ?? ""
            var args = ["exec"]
            if let resumeSessionID { args += ["resume", resumeSessionID] }
            args += [
                "--json",
                "--sandbox", (run.sandbox ?? .workspaceWrite).rawValue,
                "--ask-for-approval", "never",
                "--skip-git-repo-check",
                "-C", run.worktree,
            ]
            if let model = run.model { args += ["-m", model] }
            if !prompt.isEmpty { args.append(prompt) }

            // Spawn `codex exec` — inside the shared control container (stdio over vsock) when this
            // run carries a vsock control socket, else on the host. Gated, so default is unchanged.
            // Host-run (unsandboxed) agent: pin its git (every remote) to the Managed Git key for the
            // run's lifetime (shredded when this `do` block exits — after the agent process ends). A
            // containerized run gets the key via `provision` (cspec.env). No-op when unset.
            let isHostRun = !(containerManager != nil && run.container?.controlSocketHostPath != nil)
            var hostManagedGitEnv: [String: String] = [:]
            let hostManagedGitCleanup: @Sendable () -> Void
            if isHostRun {
                let (e, c) = GitHubCredentialProvisioner.provisionHost()
                hostManagedGitEnv = e
                hostManagedGitEnv.removeValue(forKey: "GH_CONFIG_DIR")
                hostManagedGitCleanup = c
            } else {
                hostManagedGitCleanup = {}
            }
            defer { hostManagedGitCleanup() }

            let handle: any ProcessHandle
            if let containerManager, let cspec = run.container, let controlSock = cspec.controlSocketHostPath {
                let (name, _) = try await containerManager.ensureRunning(cspec)
                activeContainerName = name
                var env = cspec.env.merging(run.extraEnv) { _, new in new }
                // Git/gh/command interceptor (observe/release only — no acquire seam): the in-container
                // shims POST observed ops to the shared per-container server → conflict coordinator.
                if cspec.installGitInterceptor, conflictCoordinator != nil,
                    let registry = approvalServerRegistry
                {
                    let server = await registry.server(for: cspec.name)
                    try await server.start(unixSocketPath: controlSock)
                    let token = UUID().uuidString
                    interceptorServer = server
                    interceptorToken = token
                    await server.registerGitReport(token: token) { [weak self] call in
                        Task { await self?.handleGitReport(call) }
                    }
                    await server.registerGhReport(token: token) { [weak self] call in
                        Task { await self?.handleGhReport(call) }
                    }
                    await server.registerCommandReport(token: token) { [weak self] call in
                        Task { await self?.handleCommandReport(call) }
                    }
                    env.merge(
                        CommandInterceptor.hookEnv(
                            host: "127.0.0.1", port: ContainerSpec.controlBridgePort,
                            token: token, sessionID: run.sessionID,
                            commandTracing: ContainerServiceSettings.commandTracingEnabled)
                    ) { _, new in new }
                }
                handle = try await containerManager.exec(
                    name: name, workdir: cspec.workdir, env: env,
                    argv: [configuration.executable] + args,
                    uid: cspec.runAsUID, gid: cspec.runAsGID)
                handle.closeStdin()  // single-shot; no stdin (matches the host .closed mode)
            } else {
                let spec = ProcessSpec(
                    executable: configuration.executable, args: args, cwd: run.worktree,
                    env: run.extraEnv.merging(hostManagedGitEnv) { _, new in new }, stdinMode: .closed)
                handle = try await processHost.launch(spec)
            }
            self.handle = handle

            let stderrTask = Task {
                for try await line in handle.stderrLines {
                    stderrTail.append(String(decoding: line, as: UTF8.self))
                }
            }

            let decoder = CodexExecDecoder()
            for try await line in handle.stdoutLines {
                for decoded in decoder.decode(line: line) {
                    if case .sessionStarted(let started) = decoded.kind {
                        backendSessionID = started.backendSessionID
                    }
                    emit(decoded.kind, nativeType: decoded.nativeType)
                }
            }

            let exitCode = await handle.wait()
            stderrTask.cancel()
            if !sawRunFinished {
                if exitCode != 0 && !interruptRequested && !terminating {
                    emit(
                        .error(
                            AgentError(
                                recoverable: false,
                                message: BackendDiagnostics.abnormalExitMessage(
                                    tool: "codex exec", exitCode: exitCode,
                                    stderrTail: stderrTail.joined(),
                                    containerized: false))),
                        nativeType: nil)
                }
                let outcome: RunFinished.Outcome =
                    interruptRequested ? .interrupted : (terminating || exitCode == 0) ? .completed : .errored
                sawRunFinished = true
                emit(.runFinished(RunFinished(outcome: outcome)), nativeType: nil)
            }
            await teardownRunState()
            self.handle = nil
            continuation.finish()
        } catch {
            if interruptRequested || terminating {
                if !sawRunFinished {
                    emit(
                        .runFinished(
                            RunFinished(outcome: interruptRequested ? .interrupted : .completed)),
                        nativeType: nil)
                }
                await teardownRunState()
                self.handle = nil
                continuation.finish()
            } else {
                await teardownRunState()
                self.handle = nil
                continuation.finish(throwing: error)
            }
        }
    }

    private func emit(_ kind: AgentEvent.Kind, nativeType: String?) {
        if case .runFinished = kind { sawRunFinished = true }
        provisionalSeq += 1
        continuation?.yield(
            AgentEvent(
                sessionID: sessionID ?? SessionID(rawValue: "unknown"),
                seq: provisionalSeq, at: configuration.now(),
                backend: .codexExec, nativeType: nativeType, kind: kind))
    }

    /// Release the run's interceptor token + container hold at run end. Idempotent.
    private func teardownRunState() async {
        if let token = interceptorToken {
            await interceptorServer?.unregister(token: token)
            interceptorToken = nil
            interceptorServer = nil
        }
        if let name = activeContainerName {
            await containerManager?.finished(name: name)
            activeContainerName = nil
        }
    }

    // MARK: - Interceptor reporting (Nucleic Control containers; observe/release)

    private func handleGitReport(_ call: MCPApprovalServer.GitReportCall) async {
        guard let sessionID, let conflictCoordinator else { return }
        await conflictCoordinator.observeGitOp(
            sessionID: sessionID, argv: call.argv, exitCode: call.exitCode)
    }

    private func handleGhReport(_ call: MCPApprovalServer.GhReportCall) async {
        guard let sessionID, let conflictCoordinator else { return }
        await conflictCoordinator.observeGhOp(
            sessionID: sessionID, argv: call.argv, exitCode: call.exitCode)
    }

    private func handleCommandReport(_ call: MCPApprovalServer.CommandReportCall) async {
        guard let sessionID, let conflictCoordinator else { return }
        await conflictCoordinator.observeCommand(sessionID: sessionID, call: call)
    }
}

private final class CodexExecStderrTail: @unchecked Sendable {
    private let lock = NSLock()
    private var lines: [String] = []
    func append(_ line: String) {
        lock.lock(); defer { lock.unlock() }
        lines.append(line)
        if lines.count > 50 { lines.removeFirst(lines.count - 50) }
    }
    func joined() -> String {
        lock.lock(); defer { lock.unlock() }
        return lines.suffix(10).joined(separator: "\n")
    }
}
