import Foundation

/// The Mac ↔ iPhone application message set (SYNC_PROTOCOL §5). A projection of the
/// backend `AgentEvent` stream + approval round-trip: the host is the single authority,
/// the phone subscribes and answers. Identifiers (`SessionID`, `ApprovalID`, `Decision`,
/// `AgentEvent`) are the exact backend types — no parallel model.
///
/// Transport-agnostic: identical over LAN (v1) and the future Cloudflare relay; only the
/// bytes-moving layer swaps. Encoded as CBOR inside an encrypted `Frame`.

public enum SyncProtocol {
    /// Bumped on any breaking change to the message set; `hello`/`welcome` exchange it.
    public static let version = 1
}

/// What a paired device may do (SYNC §1.5). v1 iPhone is granted `approve`. Ordered so the
/// host can check "scope ≥ approve".
public enum DeviceScope: String, Sendable, Codable, Comparable {
    case view      // read sessions + transcripts + diffs
    case approve   // + answer approvals, send follow-up input
    case control   // + start/merge/discard/interrupt (later)

    private var rank: Int {
        switch self {
        case .view: 0
        case .approve: 1
        case .control: 2
        }
    }
    public static func < (lhs: DeviceScope, rhs: DeviceScope) -> Bool { lhs.rank < rhs.rank }
}

/// Per-subscriber firehose level (SYNC §5.5). The host coalesces before encryption, per
/// client, so each gets what it asked for without affecting others or the transcript.
public enum Verbosity: String, Sendable, Codable {
    case statusOnly   // only sessionUpdated / approvalRequested / runFinished
    case coalesced    // partial deltas buffered + flushed on block boundary (default on cell/relay)
    case full         // every AgentEvent including partial deltas (good on LAN)
}

// MARK: - Handshake payloads

public struct Hello: Sendable, Codable, Equatable {
    public let protocolVersion: Int
    public let deviceID: String
    public let deviceLabel: String
    public let scopeClaim: DeviceScope
    /// APNs token for background wake (SYNC §6); nil until push ships (M5).
    public let pushToken: String?

    public init(
        protocolVersion: Int = SyncProtocol.version,
        deviceID: String, deviceLabel: String,
        scopeClaim: DeviceScope = .approve, pushToken: String? = nil
    ) {
        self.protocolVersion = protocolVersion
        self.deviceID = deviceID
        self.deviceLabel = deviceLabel
        self.scopeClaim = scopeClaim
        self.pushToken = pushToken
    }
}

public struct HostInfo: Sendable, Codable, Equatable {
    public let hostID: String
    public let hostName: String

    public init(hostID: String, hostName: String) {
        self.hostID = hostID
        self.hostName = hostName
    }
}

/// The capability bits the phone UI reads to mirror the Mac's affordances (e.g. show
/// "modify-and-allow" only when the backend supports it — UX_IOS §5.2).
public struct WireCapabilities: Sendable, Codable, Equatable {
    public let canModifyToolInput: Bool
    public let allowAlwaysScopes: [AlwaysScope]

    public init(canModifyToolInput: Bool, allowAlwaysScopes: [AlwaysScope]) {
        self.canModifyToolInput = canModifyToolInput
        self.allowAlwaysScopes = allowAlwaysScopes
    }
}

public struct Welcome: Sendable, Codable, Equatable {
    public let protocolVersion: Int
    public let grantedScope: DeviceScope
    public let host: HostInfo
    public let capabilities: WireCapabilities
    /// The model/effort catalog the phone's pickers render (SYNC §5.2). Optional on the wire so
    /// a phone talking to a host that predates the field decodes it as an empty catalog and falls
    /// back to its built-in effort list.
    public let modelCatalog: WireModelCatalog

    public init(
        protocolVersion: Int = SyncProtocol.version,
        grantedScope: DeviceScope, host: HostInfo, capabilities: WireCapabilities,
        modelCatalog: WireModelCatalog = .empty
    ) {
        self.protocolVersion = protocolVersion
        self.grantedScope = grantedScope
        self.host = host
        self.capabilities = capabilities
        self.modelCatalog = modelCatalog
    }

    private enum CodingKeys: String, CodingKey {
        case protocolVersion, grantedScope, host, capabilities, modelCatalog
    }

    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.protocolVersion = try c.decode(Int.self, forKey: .protocolVersion)
        self.grantedScope = try c.decode(DeviceScope.self, forKey: .grantedScope)
        self.host = try c.decode(HostInfo.self, forKey: .host)
        self.capabilities = try c.decode(WireCapabilities.self, forKey: .capabilities)
        self.modelCatalog = try c.decodeIfPresent(WireModelCatalog.self, forKey: .modelCatalog) ?? .empty
    }
}

public struct Subscribe: Sendable, Codable, Equatable {
    public let sessionID: SessionID
    /// nil → host sends a snapshot + tail, not full history (SYNC §5.3).
    public let sinceSeq: UInt64?
    public let verbosity: Verbosity

    public init(sessionID: SessionID, sinceSeq: UInt64?, verbosity: Verbosity) {
        self.sessionID = sessionID
        self.sinceSeq = sinceSeq
        self.verbosity = verbosity
    }
}

// MARK: - Session projections

public struct SessionSummary: Sendable, Codable, Equatable {
    public let sessionID: SessionID
    public let projectID: String
    public let projectName: String
    public let backend: BackendID
    public let status: SessionStatus
    /// Refines `.awaitingInput` into "done" vs "needs you" (matches the Mac sidebar).
    public let disposition: TurnDisposition?
    public let title: String
    public let branch: String
    public let lastSeq: UInt64
    public let diffStat: DiffStat?
    public let pendingApprovalCount: Int
    public let favorite: Bool
    public let archived: Bool
    /// A follow-up the user submitted while the agent was mid-turn, held to send the moment
    /// the current turn finishes. `nil` = nothing queued. (Optional so older hosts/clients
    /// that omit the key decode it as "no queued message".)
    public let queuedMessage: String?
    /// The session's current model SKU / effort level — what the phone's header pickers show as
    /// selected and mutate via `setSessionModel` / `setSessionEffort`. `nil` = host/app default.
    public let model: String?
    public let effort: String?
    /// Auto-approval and autoship state (mirrors the Mac header toggles). Default `false` so
    /// summaries from a host that predates these fields decode cleanly.
    public let auto: Bool
    public let autoShip: Bool
    /// Per-session autoship destination override; `nil` inherits the project default.
    public let shipBranch: String?
    /// The latest turn's context-window occupancy (input tokens of the final model call), so the
    /// phone can show a context-usage badge without scanning the transcript. `nil` if unknown.
    public let contextInputTokens: Int?
    public let updatedAt: Date

    public init(
        sessionID: SessionID, projectID: String, projectName: String, backend: BackendID,
        status: SessionStatus, disposition: TurnDisposition? = nil, title: String, branch: String,
        lastSeq: UInt64, diffStat: DiffStat?, pendingApprovalCount: Int = 0,
        favorite: Bool = false, archived: Bool = false, queuedMessage: String? = nil,
        model: String? = nil, effort: String? = nil, auto: Bool = false, autoShip: Bool = false,
        shipBranch: String? = nil, contextInputTokens: Int? = nil,
        updatedAt: Date
    ) {
        self.sessionID = sessionID
        self.projectID = projectID
        self.projectName = projectName
        self.backend = backend
        self.status = status
        self.disposition = disposition
        self.title = title
        self.branch = branch
        self.lastSeq = lastSeq
        self.diffStat = diffStat
        self.pendingApprovalCount = pendingApprovalCount
        self.favorite = favorite
        self.archived = archived
        self.queuedMessage = queuedMessage
        self.model = model
        self.effort = effort
        self.auto = auto
        self.autoShip = autoShip
        self.shipBranch = shipBranch
        self.contextInputTokens = contextInputTokens
        self.updatedAt = updatedAt
    }

    private enum CodingKeys: String, CodingKey {
        case sessionID, projectID, projectName, backend, status, disposition, title, branch
        case lastSeq, diffStat, pendingApprovalCount, favorite, archived, queuedMessage
        case model, effort, auto, autoShip, shipBranch, contextInputTokens, updatedAt
    }

    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.sessionID = try c.decode(SessionID.self, forKey: .sessionID)
        self.projectID = try c.decode(String.self, forKey: .projectID)
        self.projectName = try c.decode(String.self, forKey: .projectName)
        self.backend = try c.decode(BackendID.self, forKey: .backend)
        self.status = try c.decode(SessionStatus.self, forKey: .status)
        self.disposition = try c.decodeIfPresent(TurnDisposition.self, forKey: .disposition)
        self.title = try c.decode(String.self, forKey: .title)
        self.branch = try c.decode(String.self, forKey: .branch)
        self.lastSeq = try c.decode(UInt64.self, forKey: .lastSeq)
        self.diffStat = try c.decodeIfPresent(DiffStat.self, forKey: .diffStat)
        self.pendingApprovalCount = try c.decodeIfPresent(Int.self, forKey: .pendingApprovalCount) ?? 0
        self.favorite = try c.decodeIfPresent(Bool.self, forKey: .favorite) ?? false
        self.archived = try c.decodeIfPresent(Bool.self, forKey: .archived) ?? false
        self.queuedMessage = try c.decodeIfPresent(String.self, forKey: .queuedMessage)
        // New fields — tolerate summaries from a host that predates them.
        self.model = try c.decodeIfPresent(String.self, forKey: .model)
        self.effort = try c.decodeIfPresent(String.self, forKey: .effort)
        self.auto = try c.decodeIfPresent(Bool.self, forKey: .auto) ?? false
        self.autoShip = try c.decodeIfPresent(Bool.self, forKey: .autoShip) ?? false
        self.shipBranch = try c.decodeIfPresent(String.self, forKey: .shipBranch)
        self.contextInputTokens = try c.decodeIfPresent(Int.self, forKey: .contextInputTokens)
        self.updatedAt = try c.decode(Date.self, forKey: .updatedAt)
    }
}

/// How the phone asks the host to land a session's branch (control scope). Mirrors the Mac's
/// integrate strategies.
public enum IntegrationMode: String, Sendable, Codable {
    case merge, squash, rebase
}

public struct SessionSnapshot: Sendable, Codable, Equatable {
    public let summary: SessionSummary
    /// Tail window, oldest→newest (not the whole transcript — SYNC §5.3).
    public let recentEvents: [AgentEvent]
    public let pendingApprovals: [ApprovalRequest]
    /// Client continues (`subscribe sinceSeq`) from here.
    public let cursor: UInt64

    public init(
        summary: SessionSummary, recentEvents: [AgentEvent],
        pendingApprovals: [ApprovalRequest], cursor: UInt64
    ) {
        self.summary = summary
        self.recentEvents = recentEvents
        self.pendingApprovals = pendingApprovals
        self.cursor = cursor
    }
}

/// An ordered batch of transcript events for one session (`HostMsg.events`). Carries its
/// seq range implicitly via the events; the client dedupes on `(sessionID, seq)`.
public struct EventBatch: Sendable, Codable, Equatable {
    public let sessionID: SessionID
    public let events: [AgentEvent]

    public init(sessionID: SessionID, events: [AgentEvent]) {
        self.sessionID = sessionID
        self.events = events
    }
}

// MARK: - Errors (SYNC §8)

public struct WireError: Sendable, Codable, Equatable, Error {
    public enum Code: String, Sendable, Codable {
        case unauthorized        // bad/revoked device or insufficient scope
        case unknownSession
        case alreadyResolved     // lost the approval race
        case unsupported         // e.g. interrupt on a non-control scope
        case backpressure        // host overloaded; client should back off
        case protocolVersion     // version mismatch; client must upgrade
        case malformed           // undecodable frame
    }

    public let code: Code
    public let message: String
    public let sessionID: SessionID?

    public init(code: Code, message: String, sessionID: SessionID? = nil) {
        self.code = code
        self.message = message
        self.sessionID = sessionID
    }
}
