import SwiftUI
import NucleicProtocol

/// Attention-first home (UX_IOS §3): NEEDS YOU pinned at top, then RUNNING, then DONE, then a
/// collapsible ARCHIVED group at the bottom (mirrors the Mac sidebar); disposition splits "needs
/// you" from "done".
struct SessionsView: View {
    @EnvironmentObject var store: RemoteStore
    @State private var archivedExpanded = false
    // Typed (not `NavigationPath`) so `consumeRoute` can see what's already pushed — a deep link
    // to the session on top must land on it, not stack a duplicate detail over it.
    @State private var path: [SessionID] = []

    private var grouped: [(title: String, rows: [WireSessionSummary])] {
        let pool = store.liveSessions
            .sorted(by: StatusStyle.attentionThenRecency)
        // One pass, first bucket wins — the old `done = pool.filter { !needs.contains($0) … }`
        // ran O(rows²) full-summary equality scans on every body evaluation.
        var needs: [WireSessionSummary] = [], running: [WireSessionSummary] = [], done: [WireSessionSummary] = []
        for summary in pool {
            if summary.status.needsYou(summary.disposition) { needs.append(summary) }
            else if summary.status == .running || summary.status == .provisioning || summary.status == .idle {
                running.append(summary)
            } else { done.append(summary) }
        }
        return [("Needs you", needs), ("Running", running), ("Done", done)].filter { !$0.rows.isEmpty }
    }

    /// Archived chats, shown in their own collapsible section below "Done". These are excluded
    /// from `liveSessions` (and therefore from `grouped`), so they can only appear here.
    private var archivedSessions: [WireSessionSummary] {
        store.sessions.filter { $0.archived }
            .sorted(by: StatusStyle.attentionThenRecency)
    }

    var body: some View {
        NavigationStack(path: $path) {
            Group {
                if store.liveSessions.isEmpty && archivedSessions.isEmpty {
                    ContentUnavailableView(
                        "No sessions", systemImage: "square.stack.3d.up",
                        description: Text(store.connectivity.isLive
                            ? "Start a session from Home or the Mac to see it here."
                            : "Waiting to connect to your mesh…"))
                } else {
                    List {
                        // Offline: the rows below are the saved history, not a live view — say so.
                        if !store.connectivity.isLive {
                            Section {
                                HStack(spacing: 8) {
                                    Image(systemName: "wifi.slash").foregroundStyle(.secondary)
                                    Text("Offline — showing saved chats")
                                        .font(.footnote).foregroundStyle(.secondary)
                                    Spacer(minLength: 8)
                                    if store.isPaired {
                                        Button("Reconnect") { store.reconnect() }
                                            .font(.caption.weight(.semibold))
                                            .buttonStyle(.borderless)
                                    }
                                }
                            }
                            .listRowBackground(Color.clear)
                        }
                        ForEach(grouped, id: \.title) { group in
                            Section(group.title.uppercased()) {
                                ForEach(group.rows, id: \.sessionID) { summary in
                                    sessionRow(summary)
                                }
                            }
                        }
                        // Archived chats live in their own collapsible section, pinned below
                        // "Done" (mirrors the Mac sidebar's "Archived Projects" group).
                        if !archivedSessions.isEmpty {
                            Section {
                                if archivedExpanded {
                                    ForEach(archivedSessions, id: \.sessionID) { summary in
                                        sessionRow(summary)
                                    }
                                }
                            } header: {
                                Button {
                                    withAnimation(.easeInOut(duration: 0.22)) { archivedExpanded.toggle() }
                                } label: {
                                    HStack(spacing: 6) {
                                        Text("Archived (\(archivedSessions.count))")
                                        Spacer(minLength: 0)
                                        Image(systemName: "chevron.right")
                                            .font(.caption2.weight(.semibold))
                                            .rotationEffect(.degrees(archivedExpanded ? 90 : 0))
                                    }
                                    .contentShape(Rectangle())
                                }
                                .buttonStyle(.plain)
                            }
                        }
                    }
                    .listStyle(.insetGrouped)
                    .refreshable { store.refreshSessions() }
                    .compactTabBarInset()
                }
            }
            .navigationTitle("Sessions")
            .navigationDestination(for: SessionID.self) { id in
                SessionDetailView(sessionID: id)
            }
            // Offline UI previews (with NUCLEIC_DEMO): jump straight into one session.
            .onAppear {
                if let raw = ProcessInfo.processInfo.environment["NUCLEIC_DEMO_SESSION"],
                    !raw.isEmpty, path.isEmpty {
                    path.append(SessionID(rawValue: raw))
                }
                consumeRoute()
                // Sync the shell's tab-bar visibility to the current push depth on (re)appear —
                // `onChange` alone won't fire for a path restored across a remount.
                store.compactDetailPresented = !path.isEmpty
            }
            // A notification tap while this tab is already up.
            .onChange(of: store.pendingRoute) { consumeRoute() }
            // The tab bar's visibility is owned by the session lifecycle (`RemoteStore.open`/
            // `closeOpen`), so it hides/shows no matter which tab a chat opens from. This handler is
            // just the responsiveness refinement for *this* tab: the path empties the instant a
            // back-swipe/tap begins, so clearing here slides the bar back in immediately instead of
            // waiting for `SessionDetailView.onDisappear` (which fires only at the end of the pop).
            .onChange(of: path) { _, newPath in
                if newPath.isEmpty { store.compactDetailPresented = false }
            }
        }
    }

    /// One tappable session row with its mark-done / favorite / delete / archive swipe actions.
    /// Shared by the live sections (Needs you / Running / Done) and the collapsible Archived
    /// section.
    @ViewBuilder
    private func sessionRow(_ summary: WireSessionSummary) -> some View {
        NavigationLink(value: summary.sessionID) {
            SessionRow(summary: summary)
        }
        .listRowBackground(SessionRowWash(summary: summary))
        .swipeActions(edge: .leading) {
            Button {
                store.setFavorite(summary.sessionID, !summary.favorite)
            } label: {
                Label("Favorite", systemImage: summary.favorite ? "star.slash" : "star.fill")
            }.tint(.yellow)
            // "Mark Done" rides behind Favorite deliberately. The leading edge's *first* button is
            // also its full-swipe action, and marking done is the same declaration the classifier
            // makes — on an autoship-armed chat it enqueues the merge — so it must take a
            // deliberate tap, not a flick. Favorite keeps the full swipe it always had.
            if store.canMarkDone(summary) {
                Button {
                    store.markSessionDone(summary.sessionID)
                } label: {
                    Label("Mark Done", systemImage: "checkmark.circle")
                }.tint(Palette.success)
            }
        }
        .swipeActions(edge: .trailing) {
            Button(role: .destructive) {
                store.deleteSession(summary.sessionID)
            } label: { Label("Delete", systemImage: "trash") }
            Button {
                store.setArchived(summary.sessionID, !summary.archived)
            } label: {
                Label(summary.archived ? "Unarchive" : "Archive",
                      systemImage: summary.archived ? "tray.and.arrow.up" : "archivebox")
            }.tint(.gray)
        }
    }

    /// Push the session a notification tap asked for, then clear the request.
    private func consumeRoute() {
        guard let route = store.pendingRoute else { return }
        store.pendingRoute = nil
        // Already showing this session (the app was backgrounded with the chat open and a Live
        // Activity tap deep-linked back to it): pushing again would mount a duplicate detail whose
        // eventual teardown races the visible one's state. The open chat is the destination.
        guard path.last != route else { return }
        path.append(route)
    }
}

struct SessionRow: View {
    @EnvironmentObject var store: RemoteStore
    let summary: WireSessionSummary
    /// Show the project name as subtext. Suppressed when the row already lives inside
    /// its project's own section (e.g. `ProjectDetailView`), where the name is redundant.
    var showProjectName = true

    /// Whether Orchestra is latched on (the effort carries the sentinel).
    private var orchestra: Bool {
        guard let effort = summary.effort else { return false }
        return effort.caseInsensitiveCompare(store.modelCatalog.orchestraSentinelOrFallback) == .orderedSame
    }

    var body: some View {
        HStack(spacing: 12) {
            Image(systemName: StatusStyle.symbol(summary.status, disposition: summary.disposition))
                .foregroundStyle(Palette.status(summary.status, disposition: summary.disposition))
                .font(.title3)
                .frame(width: 24)
            VStack(alignment: .leading, spacing: 2) {
                HStack(spacing: 5) {
                    if summary.favorite {
                        Image(systemName: "star.fill").font(.caption2).foregroundStyle(.yellow)
                    }
                    Text(summary.title).font(.body.weight(.medium)).lineLimit(1)
                    // The same trailing markers the Mac sidebar row carries: auto-approval
                    // bolt, Orchestra note, autoship box. Suppressed on a moved-away tombstone.
                    if summary.movedTo == nil {
                        if summary.auto {
                            Image(systemName: "bolt.fill").font(.caption2).foregroundStyle(Palette.accent)
                        }
                        if orchestra {
                            Image(systemName: "music.note.list").font(.caption2).foregroundStyle(Palette.orchestra)
                        }
                        if summary.autoShip {
                            Image(systemName: "shippingbox.fill").font(.caption2).foregroundStyle(Palette.accent)
                        }
                    }
                }
                if let moved = summary.movedTo {
                    // Moved to another Mac (mesh P5): the name is baked by the host, so the phone
                    // renders it directly without needing to know that Mac itself.
                    Label("Moved to \(moved.deviceName)", systemImage: "arrow.up.forward")
                        .font(.caption).foregroundStyle(.secondary).lineLimit(1)
                } else if let arrived = summary.arrivedFrom {
                    // Arrived from another Mac (mesh P5) — subtle provenance, with the project name
                    // folded in when it would otherwise show.
                    Label(showProjectName ? "\(summary.projectName) · from \(arrived.deviceName)"
                                          : "from \(arrived.deviceName)",
                          systemImage: "arrow.down.forward")
                        .font(.caption).foregroundStyle(.secondary).lineLimit(1)
                } else if showProjectName {
                    // With more than one Mac connected, tag the owning Mac so the merged list stays
                    // distinguishable (mesh P3); a single-Mac view just shows the project.
                    if store.connectedHostCount > 1, let host = store.hostName(forSession: summary.sessionID) {
                        Text("\(summary.projectName) · \(host)").font(.caption).foregroundStyle(.secondary)
                    } else {
                        Text(summary.projectName).font(.caption).foregroundStyle(.secondary)
                    }
                }
            }
            Spacer()
            VStack(alignment: .trailing, spacing: 2) {
                if summary.pendingApprovalCount > 0 {
                    Text("\(summary.pendingApprovalCount)")
                        .font(.caption2.bold())
                        .padding(.horizontal, 6).padding(.vertical, 2)
                        .background(Palette.attention, in: Capsule())
                        .foregroundStyle(.white)
                }
                if let diff = summary.diffStat, diff.filesChanged > 0 {
                    Text("+\(diff.added)/−\(diff.removed)")
                        .font(.caption.monospacedDigit())
                        .foregroundStyle(.secondary)
                }
            }
        }
        .padding(.vertical, 2)
    }
}

/// The Mac sidebar's row washes, mirrored: a breathing amber fill on a chat that's blocked on
/// you, and a steady green fill on a chat that finished while you weren't looking. Applied as
/// a `listRowBackground` so the wash sits under the whole row.
struct SessionRowWash: View {
    @EnvironmentObject var store: RemoteStore
    let summary: WireSessionSummary
    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    @State private var pulsing = false

    var body: some View {
        ZStack {
            Color(.secondarySystemGroupedBackground)  // the list row's normal surface
            if summary.status.needsYou(summary.disposition) {
                // Breathing amber (0.10 → 0.30 on a 1.3s ease, matching the Mac); a steady
                // mid-tint under Reduce Motion.
                Palette.attention.opacity(reduceMotion ? 0.18 : (pulsing ? 0.30 : 0.10))
                    .animation(
                        reduceMotion ? nil
                            : .easeInOut(duration: 1.3).repeatForever(autoreverses: true),
                        value: pulsing)
                    .onAppear { pulsing = true }
            } else if store.unseenCompletion(summary) {
                Palette.success.opacity(0.18)
            }
        }
    }
}
