import SwiftUI

/// The lane list for one board — `BoardRoute.lanes`'s destination.
///
/// Holds only `boardRoot`, never a `BoardSummary`/`BoardModel` value: the session and its
/// snapshot are pulled from the environment's index store fresh on every body evaluation, so a
/// write from anywhere in the stack (a lane created, a card moved) reaches this screen the moment
/// the session's reload lands. Long-pressing a row drags it to a new position, reordering the
/// board's own manual arrangement.
struct BoardScreen: View {
    let boardRoot: URL

    @Environment(BoardIndexStore.self) private var index
    @State private var isPresentingNewLane = false

    private var session: BoardSession { index.session(forBoardAt: boardRoot) }

    var body: some View {
        content
            .navigationTitle(title)
            .toolbar {
                if case .ready = session.phase {
                    Button("New Lane", systemImage: "plus") {
                        isPresentingNewLane = true
                    }
                }
            }
            .task { session.open() }
            .onDisappear {
                // Stops the materializing/downloading retry timer. In practice a no-op here: a
                // lane row (the only thing on this screen that pushes forward) only renders in
                // `.ready`, the one phase where `close()`'s `cancelRetry()` has nothing to cancel —
                // so pushing deeper into the board costs nothing, and popping back out to the
                // board list is where this actually stops the timer.
                session.close()
            }
            .titlePromptAlert("New Lane", isPresented: $isPresentingNewLane, placeholder: "Lane Title") { title in
                let newTitle: String? = title.isEmpty ? nil : title
                Task {
                    // The closure's throws type must be spelled out — a trailing closure literal
                    // does not pick up `perform`'s `throws(BoardWriteError)` from context alone.
                    await session.perform { (root: URL) throws(BoardWriteError) -> ItemID in
                        try BoardWriter.createLane(inBoard: root, title: newTitle)
                    }
                }
            }
    }

    private var title: String {
        // The same fallback `BoardSummaryScanner` uses — the folder name minus `.kanban` — so the
        // title never blanks out while the session is still opening.
        session.snapshot?.title.value ?? boardRoot.deletingPathExtension().lastPathComponent
    }

    @ViewBuilder
    private var content: some View {
        switch session.phase {
        case .idle, .loading:
            ProgressView("Opening board")

        case let .materializing(progress):
            ProgressView(value: progress.fractionMaterialized) {
                Text("Downloading board")
            }
            .padding()

        case let .failed(error):
            ContentUnavailableView {
                Label("Can't Open Board", systemImage: "exclamationmark.triangle")
            } description: {
                Text(error.description)
            } actions: {
                Button("Try Again") { session.reload() }
            }

        case .ready:
            if let snapshot = session.snapshot {
                laneList(snapshot)
            } else {
                // Unreachable in practice — `.ready` is only ever set alongside a landed load,
                // which sets `snapshot` in the same step (`BoardSession.apply`). Kept so the
                // switch is exhaustive without a force-unwrap.
                ProgressView("Opening board")
            }
        }
    }

    @ViewBuilder
    private func laneList(_ snapshot: BoardModel) -> some View {
        List {
            if session.lastError != nil {
                Section {
                    Label("Showing the last saved version — a recent update didn't load.", systemImage: "exclamationmark.triangle")
                        .font(.footnote)
                        .foregroundStyle(.secondary)
                }
            }
            if snapshot.lanes.isEmpty {
                ContentUnavailableView(
                    "No Lanes Yet",
                    systemImage: "rectangle.split.3x1",
                    description: Text("Add a lane to start organizing cards.")
                )
            } else {
                // `snapshot.lanes` is already in display order (the loader's `Ranks.sortedForDisplay`),
                // so it's trusted here rather than re-sorted.
                ForEach(snapshot.lanes) { lane in
                    NavigationLink(value: BoardRoute.cards(boardRoot: boardRoot, laneID: lane.id)) {
                        LaneSummaryRow(lane: lane)
                    }
                }
                .onMove(perform: { moveRows(from: $0, to: $1) })
            }
        }
        .refreshable { session.reload() }
    }

    /// Commits a lane drag's release.
    ///
    /// `manualOrder` is `snapshot.lanes` with the drag already applied locally (`Array.move`), so the
    /// dragged lane's post-drag index is both the row SwiftUI just drew and the position
    /// `Ranks.insertionRank` mints a rank against — the same index convention `BoardStore.moveLane`
    /// uses on the Mac. The happy path asks for the midpoint between the two neighbours the lane
    /// lands between; on the rare board where that gap is already exhausted
    /// (01-storage-format.md § Ordering), there is no `HealScheduler` to reach for — that engine
    /// lives in `Kanban/LiveStore`, which this target does not compile — so the fallback renumbers
    /// every lane to a fresh whole-multiple-of-1024 ladder in `manualOrder`'s own order, one
    /// `BoardWriter.moveItem` reorder call per lane inside the same `session.perform` bracket. Both
    /// paths are same-parent reorders, so neither stamps `modified` (the reorders-don't-stamp rule).
    private func moveRows(from source: IndexSet, to destination: Int) {
        guard let sourceIndex = source.first,
              let snapshot = session.snapshot,
              snapshot.lanes.indices.contains(sourceIndex)
        else { return }

        var manualOrder = snapshot.lanes
        let moved = manualOrder[sourceIndex]
        manualOrder.move(fromOffsets: IndexSet(integer: sourceIndex), toOffset: destination)
        guard let landingIndex = manualOrder.firstIndex(where: { $0.id == moved.id }) else { return }

        let siblingOrders = manualOrder.filter { $0.id != moved.id }.map(\.order)
        // A `let` copy — the mutating `Array.move` above needs `manualOrder` as a `var`, but a
        // `@Sendable` closure cannot capture a mutable var, and the fallback loop below needs the
        // finished (post-move) arrangement, not the pre-move one.
        let finalOrder = manualOrder

        Task {
            await session.perform { (root: URL) throws(BoardWriteError) -> MoveResult in
                if let rank = Ranks.insertionRank(amongVisible: siblingOrders, at: landingIndex) {
                    return try BoardWriter.moveItem(
                        at: root.appendingPathComponent(moved.id.rawValue, isDirectory: true),
                        toParent: root,
                        sourceBoardRoot: root,
                        destinationBoardRoot: root,
                        order: rank
                    )
                }
                for (lane, freshRank) in zip(finalOrder, Ranks.renumbered(count: finalOrder.count)) {
                    _ = try BoardWriter.moveItem(
                        at: root.appendingPathComponent(lane.id.rawValue, isDirectory: true),
                        toParent: root,
                        sourceBoardRoot: root,
                        destinationBoardRoot: root,
                        order: freshRank
                    )
                }
                return MoveResult(id: moved.id, reminted: [])
            }
        }
    }
}

/// One lane row: its icon when it has one, title, and its card count.
private struct LaneSummaryRow: View {
    let lane: Lane

    var body: some View {
        HStack {
            ItemIconView(icon: lane.icon.value, iconColor: lane.iconColor.value)
            VStack(alignment: .leading, spacing: 2) {
                Text(displayTitle)
                Text("\(lane.cards.count) card\(lane.cards.count == 1 ? "" : "s")")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
        }
    }

    private var displayTitle: String {
        guard let title = lane.title.value, !title.isEmpty else { return "Untitled Lane" }
        return title
    }
}
