import Foundation

/// Shared seam between the per-session backends and the one object that knows about
/// every session (`AppStore`). Injected exactly like `ContainerManager`: created once
/// in the app, captured by the backend factory, and handed to `AppStore`, which
/// registers itself as the arbiter at startup. This breaks the AppStore↔backend cycle.
///
/// **Fail-open**: if no arbiter is attached (tests, early startup) it returns
/// `.noConflict`, so a coordination gap can never block an agent.
public actor ConflictCoordinator {
    /// Held weakly: `AppStore` owns the app's lifetime, not this coordinator.
    private weak var arbiter: (any ConflictArbiter)?

    public init() {}

    public func setArbiter(_ arbiter: any ConflictArbiter) {
        self.arbiter = arbiter
    }

    /// Forward a candidate task to the arbiter for detection + (if needed) the user
    /// prompt. Returns `.noConflict` when nothing is attached.
    public func arbitrate(sessionID: SessionID, task: String, files: [String]) async -> ArbitrationOutcome {
        guard let arbiter else { return ArbitrationOutcome(.noConflict) }
        return await arbiter.arbitrate(sessionID: sessionID, task: task, files: files)
    }

    /// Forward a git op the interceptor shim observed to the arbiter. Best-effort: dropped when
    /// no arbiter is attached (tests / early startup).
    public func observeGitOp(sessionID: SessionID, argv: [String], exitCode: Int) async {
        await arbiter?.observeGitOp(sessionID: sessionID, argv: argv, exitCode: exitCode)
    }

    /// Forward a `gh` action the interceptor shim observed to the arbiter. Best-effort, as above.
    public func observeGhOp(sessionID: SessionID, argv: [String], exitCode: Int) async {
        await arbiter?.observeGhOp(sessionID: sessionID, argv: argv, exitCode: exitCode)
    }

    /// Forward a non-git command the interceptor observed to the arbiter. Best-effort: dropped
    /// when no arbiter is attached (tests / early startup).
    public func observeCommand(sessionID: SessionID, call: MCPApprovalServer.CommandReportCall) async {
        await arbiter?.observeCommand(sessionID: sessionID, call: call)
    }

    /// Try to claim a host-run slot before a `host_exec` command runs (HOST_EXEC §concurrency).
    /// **Fail-open**: returns `.clear` when no arbiter is attached, so a coordination gap never
    /// blocks a host command.
    public func beginHostCommand(sessionID: SessionID, command: String, override: Bool) async -> HostCommandClearance {
        guard let arbiter else { return .clear }
        return await arbiter.beginHostCommand(sessionID: sessionID, command: command, override: override)
    }

    /// Release a host-run slot. Best-effort: dropped when no arbiter is attached.
    public func endHostCommand(sessionID: SessionID) async {
        await arbiter?.endHostCommand(sessionID: sessionID)
    }
}
