import Foundation

/// Wire projections of the Mac's account/status/diff surfaces so the phone can render the
/// same indicators (UX_MACOS §2's usage cards, the status pill, and the Diff tab's patch).
/// Deliberately plain value types with string-typed open sets (provider ids, file statuses)
/// so a newer host can add variants without breaking an older phone (SYNC §9).

// MARK: - Subscription usage (the Mac's QuotaIndicator source)

/// One subscription rate-limit window — mirrors `NucleicCore.UsageWindow`.
public struct WireUsageWindow: Sendable, Codable, Equatable {
    /// Percent of the window consumed, 0–100.
    public let utilization: Double
    /// When this window rolls over and frees capacity (nil if the server omits it).
    public let resetsAt: Date?

    public init(utilization: Double, resetsAt: Date?) {
        self.utilization = utilization
        self.resetsAt = resetsAt
    }

    /// The utilization to show at `now`: a window whose reset has already passed reads as
    /// empty rather than showing the stale pre-reset percentage (the host polls coarsely).
    public func utilization(at now: Date) -> Double {
        if let resetsAt, now >= resetsAt { return 0 }
        return utilization
    }
}

/// Account-wide subscription usage (the 5-hour / weekly windows the Mac's quota gauges
/// show). Rides in `DashboardSnapshot.usage`; nil when the host can't fetch it (logged
/// out, API-key auth, or a host that predates the field).
public struct WireSubscriptionUsage: Sendable, Codable, Equatable {
    public let fiveHour: WireUsageWindow?
    public let sevenDay: WireUsageWindow?
    public let sevenDayOpus: WireUsageWindow?
    public let sevenDaySonnet: WireUsageWindow?

    public init(
        fiveHour: WireUsageWindow?, sevenDay: WireUsageWindow?,
        sevenDayOpus: WireUsageWindow? = nil, sevenDaySonnet: WireUsageWindow? = nil
    ) {
        self.fiveHour = fiveHour
        self.sevenDay = sevenDay
        self.sevenDayOpus = sevenDayOpus
        self.sevenDaySonnet = sevenDaySonnet
    }

    /// The most-constrained window's utilization — the single headline number.
    public var peakUtilization: Double? {
        [fiveHour, sevenDay, sevenDayOpus, sevenDaySonnet].compactMap { $0?.utilization }.max()
    }
}

// MARK: - Provider status (the Mac's StatusFeedIndicator source)

/// One incident from a provider's status feed — mirrors `NucleicCore.StatusIncident`.
public struct WireStatusIncident: Sendable, Codable, Equatable, Identifiable {
    public let id: String
    public let title: String
    public let url: URL?
    public let updatedAt: Date?
    /// Current state word as published ("Investigating", "Monitoring", …), nil if absent.
    public let state: String?
    public let isResolved: Bool
    /// Watched components the incident touches ("Claude Code", "API", …).
    public let components: [String]

    public init(
        id: String, title: String, url: URL?, updatedAt: Date?,
        state: String?, isResolved: Bool, components: [String]
    ) {
        self.id = id
        self.title = title
        self.url = url
        self.updatedAt = updatedAt
        self.state = state
        self.isResolved = isResolved
        self.components = components
    }
}

/// A provider's feed reduced to watched-component incidents, newest first. The provider is
/// carried as its raw id plus a display name so an older phone renders a provider it's
/// never heard of without a code change.
public struct WireStatusFeed: Sendable, Codable, Equatable, Identifiable {
    /// Stable provider id ("claude", "openai", "xai", …).
    public let provider: String
    /// Human label ("Claude", "OpenAI", "xAI").
    public let providerName: String
    public let incidents: [WireStatusIncident]

    public var id: String { provider }

    public init(provider: String, providerName: String, incidents: [WireStatusIncident]) {
        self.provider = provider
        self.providerName = providerName
        self.incidents = incidents
    }

    public var activeIncidents: [WireStatusIncident] { incidents.filter { !$0.isResolved } }
    public var hasActiveIncident: Bool { incidents.contains { !$0.isResolved } }
}

// MARK: - Full session diff (the Mac Diff tab's patch, on demand)

/// One changed file in a session's diff — mirrors `NucleicCore.FileDiff` with the status as
/// a raw string so new statuses don't break older phones.
public struct WireFileDiff: Sendable, Codable, Equatable, Identifiable {
    public let path: String
    /// Original path for a rename, else nil.
    public let oldPath: String?
    /// Git name-status letter family: "added", "modified", "deleted", "renamed",
    /// "untracked", … (open set).
    public let status: String
    public let added: Int
    public let removed: Int

    public var id: String { path }

    public init(path: String, oldPath: String?, status: String, added: Int, removed: Int) {
        self.path = path
        self.oldPath = oldPath
        self.status = status
        self.added = added
        self.removed = removed
    }
}

/// A session's live worktree diff vs its base — the response to `ClientMsg.fetchDiff`,
/// fetched on demand (the patch is far too heavy to ride the dashboard). The host caps the
/// patch (see `truncated`) to protect the encrypted channel.
public struct WireSessionDiff: Sendable, Codable, Equatable {
    public let sessionID: SessionID
    public let stat: DiffStat
    public let files: [WireFileDiff]
    /// The unified patch text (possibly truncated).
    public let patch: String
    /// True when `patch` was cut at the host's size cap; the file list is always complete.
    public let truncated: Bool

    public init(
        sessionID: SessionID, stat: DiffStat, files: [WireFileDiff],
        patch: String, truncated: Bool = false
    ) {
        self.sessionID = sessionID
        self.stat = stat
        self.files = files
        self.patch = patch
        self.truncated = truncated
    }

    private enum CodingKeys: String, CodingKey { case sessionID, stat, files, patch, truncated }

    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.sessionID = try c.decode(SessionID.self, forKey: .sessionID)
        self.stat = try c.decode(DiffStat.self, forKey: .stat)
        self.files = try c.decodeIfPresent([WireFileDiff].self, forKey: .files) ?? []
        self.patch = try c.decodeIfPresent(String.self, forKey: .patch) ?? ""
        self.truncated = try c.decodeIfPresent(Bool.self, forKey: .truncated) ?? false
    }
}
