import Foundation
import NucleicProtocol

/// The authority seam between the sync server and the running host (RUNTIME §1; the phone is
/// "another renderer of the same authority"). `SyncHost` depends only on this, so it's
/// testable with a fake and the real `AppStore` conformance stays thin.
///
/// All methods hop to wherever the authority lives (the `AppStore` conformance is
/// `@MainActor`); callers `await` them off the connection tasks.
public protocol SyncHostBridge: Sendable {
    var hostInfo: HostInfo { get async }
    var capabilities: WireCapabilities { get async }
    /// The model/effort catalog the phone's pickers mirror (SYNC §5.2), sent in `Welcome`.
    var modelCatalog: WireModelCatalog { get async }
    /// This host's build channel. `ConnectionHandler` rejects a `hello` whose channel is
    /// incompatible with it (`ReleaseChannel.isCompatible`) and echoes it in `Welcome`.
    /// `nil` disables the gate — the default for minimal/test hosts that don't declare one.
    var releaseChannel: ReleaseChannel? { get async }

    /// Current session list, newest-activity first (the phone sorts attention-first itself).
    func sessionSummaries() async -> [WireSessionSummary]

    /// Snapshot for a cold/warm subscribe: status + pending approvals + a tail (cold) or
    /// the events after `sinceSeq` (warm). Returns nil if the session is unknown.
    func snapshot(_ id: SessionID, sinceSeq: UInt64?) async -> SessionSnapshot?

    /// First-responder-wins approval (SYNC §5.4). Throws `WireError(.alreadyResolved)` if the
    /// race was lost, `.unknownSession` if gone. `responder` is the `ResponderLabel` of the
    /// remote device answering (recorded as `ApprovalResolved.decidedBy`).
    func respondToApproval(_ id: ApprovalID, _ decision: Decision, by responder: String) async throws

    /// Queue a follow-up user turn (scope ≥ approve).
    func sendInput(_ id: SessionID, _ input: AgentInput) async throws

    /// Cooperative interrupt (scope ≥ control).
    func interrupt(_ id: SessionID) async throws

    /// Cancel one queued (not-yet-sent) follow-up by id (scope ≥ control).
    func cancelQueuedMessage(_ id: SessionID, _ messageID: UUID) async throws

    // Control scope: the same actions the Mac can take (UX_IOS §10, lifted to control).
    func dashboardSnapshot() async -> DashboardSnapshot
    func startChat(_ request: StartChatRequest) async throws
    func captureTodo(_ request: CaptureTodoRequest) async throws
    func dispatchTodo(_ id: TodoID, in projectID: ProjectID) async throws
    func remoteSetTodoStatus(_ id: TodoID, _ status: TodoStatus) async throws
    func remoteDeleteTodo(_ id: TodoID) async throws
    func renameSession(_ id: SessionID, to title: String) async throws
    func setFavorite(_ id: SessionID, _ favorite: Bool) async throws
    func setArchived(_ id: SessionID, _ archived: Bool) async throws
    func deleteSessionRemote(_ id: SessionID) async throws
    func integrate(_ id: SessionID, _ mode: IntegrationMode) async throws
    func discard(_ id: SessionID) async throws
    // Mid-session model / reasoning / automation changes (the Mac header's affordances).
    func setSessionModel(_ id: SessionID, _ model: String?) async throws
    func setSessionEffort(_ id: SessionID, _ effort: String?) async throws
    func setSessionAuto(_ id: SessionID, _ auto: Bool) async throws
    func setSessionAutoShip(_ id: SessionID, _ autoShip: Bool) async throws
    func setSessionShipBranch(_ id: SessionID, _ branch: String?) async throws

    /// The session's full worktree diff for the phone's Diff tab (`ClientMsg.fetchDiff`).
    /// Nil for an unknown session or one whose diff can't be computed. Defaulted so
    /// bridges that don't serve diffs (fakes, minimal hosts) keep compiling — they just
    /// shouldn't advertise `canFetchDiff`.
    func sessionDiff(_ id: SessionID) async -> WireSessionDiff?

    /// A session's transcript events after `afterSeq` (0 ⇒ the whole transcript, with its header
    /// so a requester can build a faithful mirror file), for full-transcript mesh sync
    /// (`ClientMsg.fetchTranscript`). Served from a live controller OR an on-disk transcript this
    /// device holds — owned, or mirrored from another device — so a peer that doesn't *run* the
    /// session can still serve its history. Returns nil when this device holds no transcript for
    /// `id` (⇒ `transcriptUnavailable(notHeld)`). Defaulted nil so bridges that don't serve
    /// transcripts keep compiling — they just shouldn't advertise `canSyncTranscripts`.
    func transcriptEvents(_ id: SessionID, afterSeq: UInt64) async
        -> (header: SessionHeader?, events: [AgentEvent])?

    /// Live host-side changes to fan out to subscribed clients. One shared stream; `SyncHost`
    /// multiplexes per-connection filtering/verbosity on top.
    func broadcasts() async -> AsyncStream<HostBroadcast>

    /// The mesh peers this host knows (mesh P4) — for the device list and the transfer
    /// destination picker (`ClientMsg.listPeers`). Defaulted to empty so bridges that don't
    /// participate in the mesh keep compiling; they just shouldn't advertise `canListPeers`.
    func peerSummaries() async -> [PeerSummary]

    /// Where this host can currently be dialed (mesh P4) — sent in `Welcome.addresses` so
    /// clients keep fresh reconnect hints. Defaulted nil for bridges that don't mesh.
    var localAddresses: PeerAddresses? { get async }

    /// Whether to accept a *Mac* peer's first pairing (mesh P4). Consulted only on the TOFU
    /// path when the hello claims a device kind that can own sessions — pairing a Mac grants
    /// it control of this one, so the plan requires an explicit confirm on the accepting Mac
    /// on top of QR-secret possession. Phones keep the existing pure-TOFU flow. Defaulted to
    /// accept, so test/minimal bridges pair without UI.
    func approveMacPairing(deviceID: String, label: String) async -> Bool

    /// The mesh peer table or presence just changed (a device paired, connected, disconnected,
    /// or refreshed its addresses). A hook for the host app to refresh its published peer
    /// state; defaulted no-op.
    func meshPeersChanged() async

    // MARK: Mesh "join" — roster gossip

    /// This host's current view of the group (live members + revocation tombstones), pushed to a
    /// connecting client that advertised `canSyncRoster` so joining via any member converges the
    /// whole mesh. Defaulted empty for bridges that don't participate (they shouldn't advertise
    /// `canSyncRoster`).
    func meshRoster() async -> MeshRosterPush

    /// Merge a roster pushed by `deviceID`: auto-pin unknown mac members (auto-trusting the
    /// introducer), apply revocation tombstones, reconcile dial loops, and re-gossip onward. The
    /// receiving side of `ClientMsg.meshRoster` / `HostMsg.meshRoster`. Defaulted no-op.
    func mergeMeshRoster(_ push: MeshRosterPush, from deviceID: String) async

    // MARK: Remote pairing-code mint ("add a device to this mesh")

    /// Open a pairing window on this host and return its join code (`nucleic://pair?d=…`), for a
    /// control-scope client relaying it ("add a device"). Nil when the host can't mint one right
    /// now (e.g. the sync server isn't running). Defaulted nil so bridges that don't host pairing
    /// keep compiling — they just shouldn't advertise `canMintPairingCode`.
    func mintPairingCode() async -> String?
    /// Close a pairing window opened by `mintPairingCode` (the relaying client dismissed its
    /// sheet) so the one-time secret doesn't stay armed. Defaulted no-op.
    func cancelPairingCode() async

    /// Answer a Mac-pair confirm forwarded to a control client (`ClientMsg.respondMacPair`) — the
    /// phone that shared the join code approving/denying the joining Mac. Applies only when
    /// `deviceID` matches the pending request (a stale answer is ignored). Defaulted no-op.
    func respondToMacPairing(deviceID: String, approve: Bool) async

    // MARK: Session transfer — destination importer surface (mesh P5)
    // The source drives a transfer as our client; `ConnectionHandler` forwards each verb here
    // after gating on `.control` + mac peer. Each returns the `HostMsg`s to send back, in order
    // (usually one; a final chunk returns both its ack AND `transferReady`). Defaulted to reject
    // so bridges that don't accept transfers keep compiling — they just shouldn't advertise
    // `canReceiveSessionTransfer`.

    /// Evaluate an offer and reply `transferAccept` or `transferReject`.
    func receiveTransferOffer(_ offer: TransferOffer, from deviceID: String) async -> [HostMsg]
    /// Persist a chunk and reply `transferChunkAck`; the last chunk of the last item also replies
    /// `transferReady` once the session is staged (or `transferReject` on a hash/git failure).
    func receiveTransferChunk(_ chunk: TransferChunk) async -> [HostMsg]
    /// Phase-2 commit: activate the staged session and reply `transferCommitted` (idempotent).
    func receiveTransferCommit(_ transferID: String) async -> [HostMsg]
    /// The source abandoned the transfer; discard any staged data.
    func receiveTransferCancel(_ transferID: String) async
}

extension SyncHostBridge {
    public func sessionDiff(_ id: SessionID) async -> WireSessionDiff? { nil }
    public func transcriptEvents(_ id: SessionID, afterSeq: UInt64) async
        -> (header: SessionHeader?, events: [AgentEvent])? { nil }
    public func peerSummaries() async -> [PeerSummary] { [] }
    /// Default: no declared channel, so the channel gate is a no-op (test/minimal hosts).
    public var releaseChannel: ReleaseChannel? { get async { nil } }
    public var localAddresses: PeerAddresses? { get async { nil } }
    public func approveMacPairing(deviceID: String, label: String) async -> Bool { true }
    public func meshPeersChanged() async {}
    public func meshRoster() async -> MeshRosterPush { MeshRosterPush() }
    public func mergeMeshRoster(_ push: MeshRosterPush, from deviceID: String) async {}
    public func mintPairingCode() async -> String? { nil }
    public func cancelPairingCode() async {}
    public func respondToMacPairing(deviceID: String, approve: Bool) async {}

    public func receiveTransferOffer(_ offer: TransferOffer, from deviceID: String) async -> [HostMsg] {
        [.transferReject(TransferReject(
            transferID: offer.transferID, reason: .internalError,
            message: "This device can't receive session transfers."))]
    }
    public func receiveTransferChunk(_ chunk: TransferChunk) async -> [HostMsg] { [] }
    public func receiveTransferCommit(_ transferID: String) async -> [HostMsg] {
        [.transferReject(TransferReject(transferID: transferID, reason: .internalError))]
    }
    public func receiveTransferCancel(_ transferID: String) async {}
}

/// The wire `SessionSummary` lives in NucleicProtocol but collides by name with the app's
/// own view-model `SessionSummary`; alias it so host code is unambiguous.
public typealias WireSessionSummary = NucleicProtocol.SessionSummary

/// A host-side change worth pushing to subscribers. `SyncHost` translates these into
/// `HostMsg`s, filtered by each connection's subscriptions and verbosity.
public enum HostBroadcast: Sendable {
    case events(SessionID, [AgentEvent])
    case approvalRequested(ApprovalRequest)
    case approvalResolved(ApprovalResolved)
    case sessionUpdated(WireSessionSummary)
    /// The session *list* changed in a way `sessionUpdated` can't express — a deletion.
    /// Clients replace their mirrored list wholesale (the same `HostMsg.sessionList` they
    /// seed from on connect), so a removed row disappears without waiting for a reconnect.
    case sessionList([WireSessionSummary])
    case dashboard(DashboardSnapshot)
    /// The current collapsed Bash summary lines for a session changed (mesh session sync) — the
    /// owner (re)computed them for the session it has open. `SyncHost` fans this out to that
    /// session's subscribers as `HostMsg.toolSummaries` so peer viewers can resolve the loading
    /// placeholder on each card without recomputing the model work themselves.
    case toolSummaries(SessionID, [ToolSummaryLine])
    /// A Mac is mid-pairing and needs allow/deny (mesh P4) — fanned out to control-scope clients so
    /// the phone that shared the join code can approve it (`HostMsg.macPairRequested`).
    case macPairRequested(WireMacPairRequest)
    /// The pending Mac-pair was answered/withdrawn — clients dismiss the forwarded prompt.
    case macPairResolved(deviceID: String, approved: Bool)
}
