import SwiftUI
import NucleicCore

/// A compact, account-wide service-status pill with a rich popover. Calm and quiet (a check,
/// in `success` or muted) while every watched provider component is operational; when any has
/// an unresolved incident it turns amber and names the affected providers (e.g. "Claude,
/// OpenAI"). Clicking — or hovering briefly — opens an elegant panel listing the **current**
/// incidents (active only), grouped by provider, each linking to its status page.
///
/// Reads `AppStore.statusFeeds` (refreshed by `startStatusPolling()`, narrowed to
/// `enabledStatusProviders`); before the first fetch it shows a muted "Status" placeholder so
/// it's never blank, and it renders nothing when the user has disabled every provider. Mirrors
/// how `QuotaIndicator` sits beside the composer. Where it appears (home only vs everywhere) is
/// controlled by `StatusIndicatorVisibility`, applied at each call site.
struct StatusFeedIndicator: View {
    @Environment(AppStore.self) private var store
    /// Passed in rather than read from `\.appPalette` for the same reason `QuotaIndicator`
    /// takes it: the palette environment isn't reliably injected at every host.
    let palette: AppPalette
    /// When true, render the calm/operational state in `.secondary` to sit quietly beside the
    /// composer's branch label. Active incidents always render amber so a problem reads loudly.
    var monochrome: Bool = false

    @State private var showingPopover = false
    @State private var hovering = false

    var body: some View {
        if store.enabledStatusProviders.isEmpty {
            // Every provider disabled in Settings — nothing to report, so show nothing.
            EmptyView()
        } else {
            Button { showingPopover.toggle() } label: { pill }
                .buttonStyle(.plain)
                .font(.caption)
                .onHover { hovering = $0 }
                // Debounce hover-to-open so brushing past the pill doesn't pop the panel; the
                // task auto-cancels when `hovering` flips back, so leaving early won't open it.
                .task(id: hovering) {
                    guard hovering else { return }
                    try? await Task.sleep(for: .milliseconds(450))
                    if hovering { showingPopover = true }
                }
                .popover(isPresented: $showingPopover, arrowEdge: .top) { popover }
        }
    }

    // MARK: - Pill

    @ViewBuilder
    private var pill: some View {
        let feeds = store.statusFeeds
        let active = feeds.flatMap(\.activeIncidents)
        let providers = feeds.filter(\.hasActiveIncident).map(\.provider.displayName)
        if feeds.isEmpty {
            Label("Status", systemImage: "dot.radiowaves.up.forward")
                .foregroundStyle(.secondary)
        } else if active.isEmpty {
            Label("Operational", systemImage: "checkmark.circle.fill")
                .foregroundStyle(monochrome ? AnyShapeStyle(.secondary) : AnyShapeStyle(palette.success))
        } else {
            // Incident count, then which providers are affected, e.g. "2 incidents (Claude, OpenAI)".
            Label("\(active.count) incident\(active.count == 1 ? "" : "s") (\(providers.joined(separator: ", ")))",
                  systemImage: "exclamationmark.triangle.fill")
                .foregroundStyle(palette.attention)
        }
    }

    // MARK: - Popover (current events only)

    @ViewBuilder
    private var popover: some View {
        let feeds = store.statusFeeds
        let active = feeds.filter(\.hasActiveIncident)
        VStack(alignment: .leading, spacing: 14) {
            header(active: active, hasData: !feeds.isEmpty)
            if feeds.isEmpty {
                Text("Checking service status…")
                    .font(.callout).foregroundStyle(.secondary)
            } else if active.isEmpty {
                // The monitored (enabled) providers, in poll order.
                Text(feeds.map(\.provider.displayName).joined(separator: " · "))
                    .font(.callout).foregroundStyle(.secondary)
            } else {
                VStack(alignment: .leading, spacing: 18) {
                    ForEach(active, id: \.provider) { incidentGroup($0) }
                }
            }
        }
        .padding(18)
        .frame(width: 340, alignment: .leading)
    }

    private func header(active: [StatusFeed], hasData: Bool) -> some View {
        let ok = active.isEmpty
        let icon = !hasData ? "dot.radiowaves.up.forward" : (ok ? "checkmark.circle.fill" : "exclamationmark.triangle.fill")
        let tint: AnyShapeStyle = !hasData
            ? AnyShapeStyle(.secondary)
            : (ok ? AnyShapeStyle(palette.success) : AnyShapeStyle(palette.attention))
        let title = !hasData ? "Service status" : (ok ? "All systems operational" : "Active incidents")
        return HStack(spacing: 8) {
            Image(systemName: icon).foregroundStyle(tint).font(.title3)
            Text(title).font(.headline)
        }
    }

    private func incidentGroup(_ feed: StatusFeed) -> some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(feed.provider.displayName)
                .font(.subheadline.weight(.semibold))
                .foregroundStyle(.secondary)
            ForEach(feed.activeIncidents) { incident in
                // Flush-left: title and meta start at the group's leading edge, and wrapped
                // title lines stay flush rather than hanging-indenting under the first line.
                VStack(alignment: .leading, spacing: 3) {
                    incidentTitle(incident)
                    Text(metaLine(incident))
                        .font(.caption).foregroundStyle(.secondary)
                        .multilineTextAlignment(.leading)
                        .frame(maxWidth: .infinity, alignment: .leading)
                }
                .frame(maxWidth: .infinity, alignment: .leading)
            }
        }
    }

    /// The incident title, linking to its status-page entry when the feed provides one.
    @ViewBuilder
    private func incidentTitle(_ incident: StatusIncident) -> some View {
        if let url = incident.url {
            // A Link is a control that expands to fill an `.infinity` frame and then *centers*
            // its label (button default), so a leading frame on the Link itself is ignored and
            // the title renders centered. Put the fill-and-align on the label Text inside the
            // Link so the text left-aligns and the Link merely wraps it. The else-branch Text
            // needs no such care — a Text hugs its content and the frame aligns it directly.
            Link(destination: url) {
                Text(incident.title)
                    .font(.callout)
                    .multilineTextAlignment(.leading)
                    .fixedSize(horizontal: false, vertical: true)
                    .frame(maxWidth: .infinity, alignment: .leading)
            }
        } else {
            Text(incident.title)
                .font(.callout)
                .multilineTextAlignment(.leading)
                .fixedSize(horizontal: false, vertical: true)
                .frame(maxWidth: .infinity, alignment: .leading)
        }
    }

    /// "Claude API · Investigating · 12m ago" — affected component(s), live state, and how
    /// long since the last update.
    private func metaLine(_ incident: StatusIncident) -> String {
        var parts = [incident.components.joined(separator: ", ")]
        if let state = incident.state { parts.append(state) }
        if let updatedAt = incident.updatedAt {
            parts.append(Self.relative.localizedString(for: updatedAt, relativeTo: Date()))
        }
        return parts.joined(separator: " · ")
    }

    private static let relative: RelativeDateTimeFormatter = {
        let formatter = RelativeDateTimeFormatter()
        formatter.unitsStyle = .abbreviated
        return formatter
    }()
}

// MARK: - Settings

/// Where the service-status indicator is shown. `everywhere` keeps it beside both the home
/// composer and each chat's composer; `homeOnly` confines it to the home dashboard.
enum StatusIndicatorVisibility: String, CaseIterable, Identifiable {
    case everywhere
    case homeOnly

    var id: String { rawValue }

    var label: String {
        switch self {
        case .everywhere: return "Everywhere"
        case .homeOnly: return "Only on home view"
        }
    }

    static let storageKey = "nucleic.status.visibility"

    /// The current choice (defaults to `everywhere`, preserving the prior behavior).
    static var stored: StatusIndicatorVisibility {
        UserDefaults.standard.string(forKey: storageKey).flatMap(Self.init(rawValue:)) ?? .everywhere
    }

    /// Storage key for the override that, under `homeOnly`, still surfaces the pill beside a
    /// chat's composer when that chat's model's provider has an active incident — so an outage
    /// on the model you're actually using is never hidden. Defaults to on; moot under
    /// `everywhere` (the pill already shows in chat). See `SessionDetailView`.
    static let chatIncidentOverrideKey = "nucleic.status.chatIncidentOverride"
}

/// Persistence for which providers the status indicator monitors. Each provider defaults to
/// enabled; the user disables the ones they don't use so their status isn't polled or shown.
enum StatusIndicatorSettings {
    static func providerKey(_ provider: StatusProvider) -> String {
        "nucleic.status.provider.\(provider.rawValue).enabled"
    }

    /// The enabled providers, read from `UserDefaults` (absent key → enabled).
    static var storedEnabledProviders: Set<StatusProvider> {
        Set(StatusProvider.allCases.filter { provider in
            UserDefaults.standard.object(forKey: providerKey(provider)) as? Bool ?? true
        })
    }
}
