import Foundation

/// The model/effort catalog, projected over the wire so the phone's composer and session
/// header can offer the *same* model and reasoning choices the Mac does — with the same
/// per-backend effort caps (Codex tops out at `xhigh`, Grok exposes only `auto`) and the
/// same context-window sizes — without re-deriving any backend rules on-device.
///
/// The host builds this from its `ModelCatalog` (a single source of truth) and sends it once
/// in `Welcome`. Everything the phone needs to render a picker is precomputed here; the phone
/// treats it as opaque data (SYNC_PROTOCOL §5.2).
public struct WireModelCatalog: Sendable, Codable, Equatable {

    /// One selectable model SKU, fully described. The `sku` is the exact string the phone
    /// passes back in `StartChatRequest.model` / `setSessionModel`; it also selects the
    /// backend (`BackendID.forModel`), already resolved into `backend` here.
    public struct Model: Sendable, Codable, Equatable, Identifiable {
        public let sku: String
        public let displayName: String       // "Opus 4.8"
        public let backend: BackendID
        /// Context-window annotation that distinguishes same-named SKUs ("1M" vs "256K"); nil otherwise.
        public let contextBadge: String?
        public let contextWindow: Int        // approximate input window, tokens
        /// Supported effort levels, lowest→highest, with per-backend caps already applied
        /// (so the phone shows exactly these and never an unsupported one).
        public let efforts: [String]
        /// The reasoning control's noun for this model — "Effort" (Claude) or "Reasoning" (Codex/Grok).
        public let effortNoun: String

        public var id: String { sku }

        public init(
            sku: String, displayName: String, backend: BackendID, contextBadge: String?,
            contextWindow: Int, efforts: [String], effortNoun: String
        ) {
            self.sku = sku
            self.displayName = displayName
            self.backend = backend
            self.contextBadge = contextBadge
            self.contextWindow = contextWindow
            self.efforts = efforts
            self.effortNoun = effortNoun
        }
    }

    /// Models split into per-provider runs (Claude, then GPT, then Grok), preserving order so
    /// the phone's model menu can set each provider apart with a divider — mirrors the Mac.
    public let groups: [[Model]]
    /// Pretty labels for non-identity effort levels (e.g. `"auto" → "Auto"`, the orchestra
    /// sentinel → "Orchestra"); plain API levels fall back to themselves.
    public let effortDisplayNames: [String: String]
    /// The effort-menu sentinel that selects Orchestra (an orchestration mode, not an API level).
    public let orchestraSentinel: String
    /// Short grayed annotation shown when Orchestra is unavailable (non-Control project).
    public let orchestraRequiresControlNote: String
    public let fallbackModel: String
    public let fallbackEffort: String

    public init(
        groups: [[Model]], effortDisplayNames: [String: String], orchestraSentinel: String,
        orchestraRequiresControlNote: String, fallbackModel: String, fallbackEffort: String
    ) {
        self.groups = groups
        self.effortDisplayNames = effortDisplayNames
        self.orchestraSentinel = orchestraSentinel
        self.orchestraRequiresControlNote = orchestraRequiresControlNote
        self.fallbackModel = fallbackModel
        self.fallbackEffort = fallbackEffort
    }

    // MARK: - Convenience lookups (phone-side, pure reads)

    public var allModels: [Model] { groups.flatMap { $0 } }

    public func model(_ sku: String?) -> Model? {
        guard let sku else { return nil }
        return allModels.first { $0.sku == sku }
    }

    /// SKUs that run on `backend` — for the in-session model menu, where the backend is fixed
    /// at creation. Returns the SKUs grouped contiguously by provider, flattened.
    public func models(for backend: BackendID) -> [Model] {
        allModels.filter { model in
            switch backend {
            case .claudeCode: return model.backend == .claudeCode
            case .codex, .codexExec: return model.backend == .codex
            case .grok: return model.backend == .grok
            }
        }
    }

    /// Effort levels supported by `sku`, or the global fallback set if unknown.
    public func efforts(forModel sku: String?) -> [String] {
        model(sku)?.efforts ?? [fallbackEffort]
    }

    public func displayName(_ sku: String?) -> String {
        model(sku)?.displayName ?? sku ?? fallbackModel
    }

    public func contextBadge(_ sku: String?) -> String? { model(sku)?.contextBadge }

    public func contextWindow(_ sku: String?) -> Int { model(sku)?.contextWindow ?? 200_000 }

    public func effortNoun(forModel sku: String?) -> String { model(sku)?.effortNoun ?? "Effort" }

    /// Pretty label for an effort level — Orchestra/Auto get named labels; plain levels read as-is.
    public func effortDisplayName(_ effort: String) -> String {
        effortDisplayNames[effort] ?? effort
    }

    /// Whether `effort` selects Orchestra (case-insensitive against the sentinel).
    public func isOrchestra(_ effort: String?) -> Bool {
        guard let effort else { return false }
        return effort.caseInsensitiveCompare(orchestraSentinel) == .orderedSame
    }

    /// An empty catalog — the default before a `Welcome` arrives and the host-side fallback.
    public static let empty = WireModelCatalog(
        groups: [], effortDisplayNames: [:], orchestraSentinel: "orchestra",
        orchestraRequiresControlNote: "Requires Nucleic Control",
        fallbackModel: "", fallbackEffort: "high")
}
