import SwiftUI
import NucleicCore

/// A status-driven prompt to ride out a provider outage on another provider. When the default
/// model's provider is reporting an incident on a watched component, this offers to switch the
/// default model to a comparable model on a healthy provider (Opus ↔ GPT-5.5, etc.); once the
/// provider recovers the original is restored automatically (handled in `AppStore`), and while
/// failed over it offers an early "switch back".
///
/// Lives on the home dashboard, where new chats pick up `defaultModel`. Renders nothing when
/// the default model's provider is healthy and no failover is in effect, so it's invisible in
/// the common case. Reads `AppStore.statusFeeds` / `defaultModel` / `modelFailover`.
struct ModelFailoverBanner: View {
    @Environment(AppStore.self) private var store
    @Environment(\.appPalette) private var palette
    /// The offer the user dismissed, keyed by provider + active incidents, so a declined
    /// prompt doesn't nag — but reappears if the incident set changes.
    @State private var dismissedKey: String?

    var body: some View {
        if let failover = store.modelFailover {
            activeBanner(failover)
        } else if let plan = offer, dismissedKey != plan.key {
            offerBanner(plan)
        }
    }

    // MARK: - Banners

    private func offerBanner(_ plan: FailoverPlan) -> some View {
        let altName = ModelCatalog.displayName(plan.alternative)
        return banner(
            icon: "exclamationmark.triangle.fill",
            tint: palette.attention,
            message: "\(plan.downProvider.displayName) is reporting an incident"
                + summarySuffix(plan.downProvider)
                + ". Switch the default model to \(altName) on \(plan.altProvider.displayName) "
                + "until it recovers?",
            primaryTitle: "Switch to \(altName)",
            primary: { store.engageModelFailover(original: plan.original, alternative: plan.alternative) },
            secondaryTitle: "Dismiss",
            secondary: { dismissedKey = plan.key })
    }

    private func activeBanner(_ failover: ModelFailover) -> some View {
        let provider = StatusProvider.forModel(failover.original)
        let providerName = provider?.displayName ?? "the provider"
        let originalName = ModelCatalog.displayName(failover.original)
        let altName = ModelCatalog.displayName(failover.alternative)
        return banner(
            icon: "arrow.triangle.2.circlepath",
            tint: palette.accent,
            message: "Defaulting to \(altName) while \(providerName) recovers — new chats will "
                + "switch back to \(originalName) automatically once the incident is resolved.",
            primaryTitle: "Switch back now",
            primary: { store.cancelModelFailover() },
            secondaryTitle: nil,
            secondary: nil)
    }

    /// Shared banner chrome: an icon, a wrapping message, and one or two trailing actions.
    private func banner(
        icon: String, tint: Color, message: String,
        primaryTitle: String, primary: @escaping () -> Void,
        secondaryTitle: String?, secondary: (() -> Void)?
    ) -> some View {
        HStack(alignment: .top, spacing: 10) {
            Image(systemName: icon).foregroundStyle(tint)
            Text(message)
                .font(.callout)
                .fixedSize(horizontal: false, vertical: true)
                .frame(maxWidth: .infinity, alignment: .leading)
            VStack(alignment: .trailing, spacing: 6) {
                Button(primaryTitle, action: primary).buttonStyle(.borderedProminent)
                if let secondaryTitle, let secondary {
                    Button(secondaryTitle, action: secondary)
                        .buttonStyle(.plain)
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
            }
            .fixedSize()
        }
        .padding(12)
        .background(tint.opacity(0.10), in: .rect(cornerRadius: 12))
        .overlay(RoundedRectangle(cornerRadius: 12).strokeBorder(tint.opacity(0.35), lineWidth: 1))
    }

    // MARK: - Offer computation

    /// The failover currently worth offering, or nil. We offer only when: no failover is
    /// already active, the default model's provider has an active incident, a comparable model
    /// exists on another provider, and that provider is itself healthy.
    private var offer: FailoverPlan? {
        guard store.modelFailover == nil, !store.statusFeeds.isEmpty else { return nil }
        let current = store.defaultModel ?? ModelCatalog.fallbackModel
        guard let provider = StatusProvider.forModel(current),
              let feed = store.statusFeed(for: provider), feed.hasActiveIncident,
              let alternative = ModelCatalog.crossProviderAlternative(for: current),
              let altProvider = StatusProvider.forModel(alternative),
              !store.providerHasActiveIncident(altProvider)
        else { return nil }
        let incidentKey = feed.activeIncidents.map(\.id).sorted().joined(separator: ",")
        return FailoverPlan(
            downProvider: provider, altProvider: altProvider,
            original: current, alternative: alternative,
            key: "\(provider.rawValue)|\(incidentKey)")
    }

    /// " (Component: title)" for the provider's first active incident, or "" if none — keeps
    /// the offer message specific without assuming an incident is present.
    private func summarySuffix(_ provider: StatusProvider) -> String {
        guard let incident = store.statusFeed(for: provider)?.activeIncidents.first else { return "" }
        let component = incident.components.first ?? provider.displayName
        return " (\(component): \(incident.title))"
    }

    private struct FailoverPlan {
        let downProvider: StatusProvider
        let altProvider: StatusProvider
        let original: String
        let alternative: String
        /// Identity of the current offer, for dismissal — changes when the incident set does.
        let key: String
    }
}
