import SwiftUI
import UIKit

@MainActor
@Observable
final class MoreNotificationsStore {
    var items: [UserNotification] = []
    var unread = 0
    var preferences: [String: MoreCategoryPref] = [:]
    var emailDigestMinutes = 30
    var loadedOnce = false
    var isLoading = false
    var errorMessage: String?
    var actionError: String?

    func load(_ api: APIClient) async {
        guard !isLoading else { return }
        isLoading = true
        errorMessage = nil
        do {
            let feed: MoreNotificationFeed = try await api.get(
                "auth/me/notifications",
                query: ["limit": "50"]
            )
            items = feed.notifications ?? []
            unread = feed.unread ?? 0
            PushManager.shared.setBadge(unread)
            let envelope: MoreNotificationPreferencesEnvelope = try await api.get("auth/me/notification-preferences")
            preferences = envelope.preferences?.categories ?? [:]
            emailDigestMinutes = envelope.preferences?.emailDigestMinutes ?? 30
            loadedOnce = true
        } catch {
            errorMessage = error.localizedDescription
        }
        isLoading = false
    }

    func markRead(_ id: String, _ api: APIClient) async {
        guard let index = items.firstIndex(where: { $0.id == id }), items[index].isUnread else { return }
        items[index].readAt = Date()
        unread = max(0, unread - 1)
        PushManager.shared.setBadge(unread)
        let _: MoreOkResponse? = try? await api.post("auth/me/notifications/\(id)/read")
    }

    func markAllRead(_ api: APIClient) async {
        let now = Date()
        for index in items.indices where items[index].readAt == nil {
            items[index].readAt = now
        }
        unread = 0
        PushManager.shared.setBadge(0)
        // PUT on the collection marks everything read; the handler ignores the body.
        let _: MoreOkResponse? = try? await api.put("auth/me/notifications", body: EmptyBody())
    }

    /// Autosaves the full preferences object, mirroring the web settings page.
    func setEnabled(_ key: String, _ enabled: Bool, _ api: APIClient) async {
        let previous = preferences
        var pref = preferences[key] ?? MoreCategoryPref(
            enabled: enabled,
            channels: MoreChannelPrefs(inApp: true, email: false, slack: false, push: true)
        )
        pref.enabled = enabled
        preferences[key] = pref
        await persist(previous: previous, api)
    }

    /// Email bundling window, saved through the same PUT as the toggles.
    func setEmailWindow(_ minutes: Int, _ api: APIClient) async {
        guard minutes != emailDigestMinutes else { return }
        let previous = preferences
        let previousWindow = emailDigestMinutes
        emailDigestMinutes = minutes
        await persist(previous: previous, previousWindow: previousWindow, api)
    }

    /// Global channel state, like the web settings page: a channel reads "on"
    /// only when every category delivers to it.
    func channelOn(_ channel: WritableKeyPath<MoreChannelPrefs, Bool?>) -> Bool {
        !preferences.isEmpty && preferences.values.allSatisfy { $0.channels?[keyPath: channel] == true }
    }

    /// Flips a delivery channel across every category at once (the web has the
    /// same all-or-nothing toggle; per-category channel splits stay web/API-only).
    func setChannel(_ channel: WritableKeyPath<MoreChannelPrefs, Bool?>, _ on: Bool, _ api: APIClient) async {
        let previous = preferences
        for key in preferences.keys {
            var pref = preferences[key] ?? MoreCategoryPref(enabled: false, channels: nil)
            var channels = pref.channels ?? MoreChannelPrefs(inApp: true, email: false, slack: false, push: true)
            channels[keyPath: channel] = on
            pref.channels = channels
            preferences[key] = pref
        }
        await persist(previous: previous, api)
    }

    private func persist(previous: [String: MoreCategoryPref], previousWindow: Int? = nil, _ api: APIClient) async {
        do {
            let body = MorePreferencesBody(
                preferences: NotificationPreferences(categories: preferences, emailDigestMinutes: emailDigestMinutes)
            )
            let echoed: MoreNotificationPreferencesEnvelope = try await api.put(
                "auth/me/notification-preferences",
                body: body
            )
            if let echoedPrefs = echoed.preferences {
                preferences = echoedPrefs.categories
                emailDigestMinutes = echoedPrefs.emailDigestMinutes
            }
        } catch {
            preferences = previous
            if let previousWindow { emailDigestMinutes = previousWindow }
            actionError = error.localizedDescription
        }
    }
}

private enum MoreNotificationMeta {
    static func icon(_ category: String?) -> String {
        switch category {
        case "inbound_reply": return "arrowshape.turn.up.left.fill"
        case "inbound_out_of_office": return "moon.zzz.fill"
        case "health_bounce": return "arrow.uturn.left.circle.fill"
        case "health_complaint": return "flag.fill"
        case "health_worker_downtime": return "bolt.slash.fill"
        case "security_new_signin": return "lock.shield.fill"
        case "billing_alert": return "creditcard.fill"
        case "team_activity": return "person.2.fill"
        default: return "bell.fill"
        }
    }

    static func tone(_ category: String?) -> Tone {
        switch category {
        case "inbound_reply": return .emerald
        case "inbound_out_of_office": return .slate
        case "health_bounce": return .rose
        case "health_complaint": return .rose
        case "health_worker_downtime": return .amber
        case "security_new_signin": return .indigo
        case "billing_alert": return .orange
        case "team_activity": return .sky
        default: return .sky
        }
    }

    static func label(_ key: String) -> String {
        switch key {
        case "inbound_reply": return "Replies"
        case "inbound_out_of_office": return "Out of office"
        case "health_bounce": return "Bounces"
        case "health_complaint": return "Spam complaints"
        case "health_worker_downtime": return "Worker downtime"
        case "security_new_signin": return "New sign-ins"
        case "billing_alert": return "Billing"
        case "team_activity": return "Team"
        default:
            let pretty = key.replacingOccurrences(of: "_", with: " ")
            return pretty.prefix(1).uppercased() + pretty.dropFirst()
        }
    }

    static func caption(_ key: String) -> String? {
        switch key {
        case "inbound_reply": return "A contact replies to your outreach"
        case "inbound_out_of_office": return "Auto-replies detected on your sends"
        case "health_bounce": return "A campaign email hard bounces"
        case "health_complaint": return "A recipient marks your mail as spam"
        case "health_worker_downtime": return "A sending worker goes offline"
        case "security_new_signin": return "Your account signs in from a new device"
        case "billing_alert": return "Your trial is ending or a payment needs attention"
        case "team_activity": return "A teammate joined your workspace"
        default: return nil
        }
    }

    static let knownGroups: [(title: String, keys: [String])] = [
        ("Inbound", ["inbound_reply", "inbound_out_of_office"]),
        ("Mailbox health", ["health_bounce", "health_complaint", "health_worker_downtime"]),
        ("Security", ["security_new_signin"]),
        ("Account", ["billing_alert", "team_activity"]),
    ]

    // Bundling-window presets in minutes. The 30 minute floor mirrors the
    // backend: there is no per-event email mode.
    static let windowOptions: [(minutes: Int, label: String, caption: String)] = [
        (30, "Every 30 minutes", "The fastest option. Skips anything you already read."),
        (60, "Every hour", "At most one bundled email per hour."),
        (180, "Every 3 hours", "A few bundles across a working day."),
        (360, "Every 6 hours", "Morning, afternoon, evening."),
        (720, "Every 12 hours", "Twice a day at most."),
        (1440, "Once a day", "One daily summary of everything unread."),
    ]

    static func windowLabel(_ minutes: Int) -> String {
        if let match = windowOptions.first(where: { $0.minutes == minutes }) {
            return match.label
        }
        if minutes % 60 == 0 {
            return "Every \(minutes / 60) hours"
        }
        return "Every \(minutes) minutes"
    }

    static func windowCaption(_ minutes: Int) -> String {
        windowOptions.first(where: { $0.minutes == minutes })?.caption
            ?? "Bundles everything unread into one email per window."
    }
}

/// In-app notification feed plus per-category preferences, rendered as one
/// flat surface: dense tile-led feed rows with hairline separators, eyebrow
/// captions instead of grouped bands.
struct NotificationsView: View {
    @Environment(AppEnvironment.self) private var env
    @State private var store = MoreNotificationsStore()
    @State private var tab = 0

    var body: some View {
        VStack(spacing: 0) {
            Picker("View", selection: $tab) {
                Text("Feed").tag(0)
                Text("Preferences").tag(1)
            }
            .pickerStyle(.segmented)
            .padding(.horizontal, 16)
            .padding(.bottom, 8)
            content
        }
        .background(Color(.systemBackground))
        .navigationTitle("Notifications")
        .navigationBarTitleDisplayMode(.inline)
        .toolbar {
            ToolbarItem(placement: .topBarTrailing) {
                if tab == 0, store.unread > 0 {
                    Button("Mark all read") {
                        Task { await store.markAllRead(env.api) }
                    }
                }
            }
        }
        .task {
            await store.load(env.api)
            await PushManager.shared.refreshAuthorization()
        }
        .onChange(of: env.realtime.pulse(for: .notifications)) {
            Task { await store.load(env.api) }
        }
        .alert(
            "Couldn't save preferences",
            isPresented: Binding(
                get: { store.actionError != nil },
                set: { if !$0 { store.actionError = nil } }
            )
        ) {
            Button("OK", role: .cancel) {}
        } message: {
            Text(store.actionError ?? "")
        }
    }

    @ViewBuilder
    private var content: some View {
        if !store.loadedOnce {
            if let message = store.errorMessage {
                ErrorStateView(title: "Couldn't load notifications", message: message) {
                    await store.load(env.api)
                }
            } else {
                SkeletonRows()
            }
        } else if tab == 0 {
            feedList
        } else {
            preferencesList
        }
    }

    // MARK: Feed

    @ViewBuilder
    private var feedList: some View {
        if store.items.isEmpty {
            List {
                EmptyStateView(
                    title: "All caught up",
                    message: "Replies, deliverability alerts, and sign-in notices land here."
                )
                .listRowSeparator(.hidden)
                .listRowBackground(Color(.systemBackground))
            }
            .listStyle(.plain)
            .scrollContentBackground(.hidden)
            .refreshable { await store.load(env.api) }
        } else {
            List {
                ForEach(store.items) { item in
                    feedRow(item)
                        .moreFlatRow(textLeading: MoreFlatMetrics.tileTextLeading)
                }
            }
            .listStyle(.plain)
            .scrollContentBackground(.hidden)
            .environment(\.defaultMinListRowHeight, 0)
            .refreshable { await store.load(env.api) }
        }
    }

    private func feedRow(_ item: UserNotification) -> some View {
        let tone = MoreNotificationMeta.tone(item.category)
        return Button {
            Task { await store.markRead(item.id, env.api) }
        } label: {
            HStack(alignment: .top, spacing: 12) {
                IconTile(symbol: MoreNotificationMeta.icon(item.category), tone: tone, size: 34)
                VStack(alignment: .leading, spacing: 2) {
                    Text(item.title ?? "Notification")
                        .font(.subheadline.weight(item.isUnread ? .semibold : .regular))
                        .foregroundStyle(.primary)
                        .lineLimit(2)
                    if let body = item.body, !body.isEmpty {
                        Text(body)
                            .font(.footnote)
                            .foregroundStyle(.secondary)
                            .lineLimit(2)
                    }
                }
                Spacer(minLength: 8)
                VStack(alignment: .trailing, spacing: 6) {
                    if let created = item.createdAt {
                        Text(WFormat.relative(created))
                            .font(.footnote)
                            .monospacedDigit()
                            .foregroundStyle(item.isUnread ? AnyShapeStyle(WTheme.accent) : AnyShapeStyle(Color(.tertiaryLabel)))
                    }
                    if item.isUnread {
                        Circle()
                            .fill(WTheme.accent)
                            .frame(width: 7, height: 7)
                    }
                }
                .padding(.top, 2)
            }
            .padding(.vertical, 10)
            .contentShape(Rectangle())
        }
        .buttonStyle(TapScaleStyle())
    }

    // MARK: Preferences

    private var extraKeys: [String] {
        let known = Set(MoreNotificationMeta.knownGroups.flatMap(\.keys))
        return store.preferences.keys.filter { !known.contains($0) }.sorted()
    }

    private var preferencesList: some View {
        List {
            ForEach(MoreNotificationMeta.knownGroups, id: \.title) { group in
                let present = group.keys.filter { store.preferences[$0] != nil }
                if !present.isEmpty {
                    MoreFlatSectionHeader(group.title, top: group.title == MoreNotificationMeta.knownGroups.first?.title ? 8 : 20)
                    ForEach(present, id: \.self) { key in
                        preferenceRow(key)
                    }
                }
            }
            if !extraKeys.isEmpty {
                MoreFlatSectionHeader("Other")
                ForEach(extraKeys, id: \.self) { key in
                    preferenceRow(key)
                }
            }
            MoreFlatSectionHeader("Channels")
            channelRow(
                symbol: "app.badge.fill",
                tone: .sky,
                label: "In-app",
                caption: "The feed above; always on for enabled categories."
            ) {
                Text("On")
                    .font(.caption.weight(.semibold))
                    .foregroundStyle(Tone.emerald.color)
            }
            channelRow(
                symbol: "iphone.gen3.radiowaves.left.and.right",
                tone: .rose,
                label: "Push",
                caption: "Alerts on this device. The first event pushes right away; bursts arrive as one summary instead of a ping per event."
            ) {
                Toggle("", isOn: channelBinding(\.push))
                    .labelsHidden()
                    .tint(WTheme.accent)
                    .disabled(PushManager.shared.authorization == .denied)
            }
            if PushManager.shared.authorization == .denied {
                HStack(spacing: 6) {
                    Image(systemName: "exclamationmark.triangle.fill")
                        .font(.caption)
                        .foregroundStyle(Tone.amber.color)
                    Text("Notifications are off for Warmbly in iOS Settings.")
                        .font(.footnote)
                        .foregroundStyle(.secondary)
                    Spacer(minLength: 8)
                    Button("Open Settings") {
                        if let url = URL(string: UIApplication.openNotificationSettingsURLString) {
                            UIApplication.shared.open(url)
                        }
                    }
                    .font(.footnote.weight(.medium))
                }
                .padding(.vertical, 6)
                .moreFlatRow(textLeading: MoreFlatMetrics.tileTextLeading)
            }
            channelRow(
                symbol: "envelope.fill",
                tone: .indigo,
                label: "Email",
                caption: "Delivery to your account email."
            ) {
                Toggle("", isOn: channelBinding(\.email))
                    .labelsHidden()
                    .tint(WTheme.accent)
            }
            channelRow(
                symbol: "bubble.left.and.bubble.right.fill",
                tone: .emerald,
                label: "Slack",
                caption: "Posts to the Slack channel set up in Integrations on the web."
            ) {
                Toggle("", isOn: channelBinding(\.slack))
                    .labelsHidden()
                    .tint(WTheme.accent)
            }
            Text("Channels apply across every enabled category above.")
                .font(.footnote)
                .foregroundStyle(.secondary)
                .padding(.top, 16)
                .padding(.bottom, 4)
                .moreFlatRow(separator: .hidden)
            MoreFlatSectionHeader("Email delivery")
            channelRow(
                symbol: "tray.full.fill",
                tone: .indigo,
                label: "Bundling window",
                caption: MoreNotificationMeta.windowCaption(store.emailDigestMinutes)
            ) {
                Menu {
                    Picker("Bundling window", selection: windowBinding) {
                        ForEach(windowChoices, id: \.self) { minutes in
                            Text(MoreNotificationMeta.windowLabel(minutes)).tag(minutes)
                        }
                    }
                } label: {
                    HStack(spacing: 4) {
                        Text(MoreNotificationMeta.windowLabel(store.emailDigestMinutes))
                            .font(.subheadline.weight(.medium))
                        Image(systemName: "chevron.up.chevron.down")
                            .font(.caption2.weight(.semibold))
                    }
                    .foregroundStyle(WTheme.accent)
                    .contentShape(Rectangle())
                }
            }
            Text("Alerts you read in the app are never emailed. Security sign-in alerts always send immediately.")
                .font(.footnote)
                .foregroundStyle(.secondary)
                .padding(.top, 16)
                .padding(.bottom, 28)
                .moreFlatRow(separator: .hidden)
        }
        .listStyle(.plain)
        .scrollContentBackground(.hidden)
        .environment(\.defaultMinListRowHeight, 0)
        .refreshable { await store.load(env.api) }
    }

    private func preferenceRow(_ key: String) -> some View {
        Toggle(isOn: preferenceBinding(key)) {
            HStack(spacing: 12) {
                IconTile(
                    symbol: MoreNotificationMeta.icon(key),
                    tone: store.preferences[key]?.enabled == true ? MoreNotificationMeta.tone(key) : .slate,
                    size: 34
                )
                VStack(alignment: .leading, spacing: 2) {
                    Text(MoreNotificationMeta.label(key))
                        .font(.body.weight(.medium))
                    if let caption = MoreNotificationMeta.caption(key) {
                        Text(caption)
                            .font(.footnote)
                            .foregroundStyle(.secondary)
                    }
                }
            }
        }
        .tint(WTheme.accent)
        .padding(.vertical, 9)
        .moreFlatRow(textLeading: MoreFlatMetrics.tileTextLeading)
    }

    private func preferenceBinding(_ key: String) -> Binding<Bool> {
        Binding(
            get: { store.preferences[key]?.enabled ?? false },
            set: { newValue in
                Task { await store.setEnabled(key, newValue, env.api) }
            }
        )
    }

    // Preset windows, plus the server value when it matches none (set to a
    // custom window on the web) so the picker never lies about the state.
    private var windowChoices: [Int] {
        var choices = MoreNotificationMeta.windowOptions.map(\.minutes)
        if !choices.contains(store.emailDigestMinutes) {
            choices.append(store.emailDigestMinutes)
            choices.sort()
        }
        return choices
    }

    private var windowBinding: Binding<Int> {
        Binding(
            get: { store.emailDigestMinutes },
            set: { newValue in
                Task { await store.setEmailWindow(newValue, env.api) }
            }
        )
    }

    private func channelBinding(_ channel: WritableKeyPath<MoreChannelPrefs, Bool?>) -> Binding<Bool> {
        Binding(
            get: { store.channelOn(channel) },
            set: { newValue in
                Task { await store.setChannel(channel, newValue, env.api) }
            }
        )
    }

    private func channelRow(
        symbol: String,
        tone: Tone,
        label: String,
        caption: String,
        @ViewBuilder trailing: () -> some View
    ) -> some View {
        HStack(spacing: 12) {
            IconTile(symbol: symbol, tone: tone, size: 34)
            VStack(alignment: .leading, spacing: 2) {
                Text(label)
                    .font(.body.weight(.medium))
                Text(caption)
                    .font(.footnote)
                    .foregroundStyle(.secondary)
            }
            Spacer(minLength: 8)
            trailing()
        }
        .padding(.vertical, 9)
        .moreFlatRow(textLeading: MoreFlatMetrics.tileTextLeading)
    }
}
