import SwiftUI

/// The card list for one lane — `BoardRoute.cards`'s destination.
///
/// Holds `boardRoot` and `laneID`, never a `Lane` value: the lane is re-read from
/// `session.snapshot` on every body evaluation, so a write this screen makes — or one relayed
/// from elsewhere through the metadata query — reaches the list the moment the session's reload
/// lands, and a lane deleted on another device is noticed rather than shown stale. Long-pressing
/// a card drags it to a new position, reordering the lane's own manual arrangement.
struct LaneScreen: View {
    let boardRoot: URL
    let laneID: ItemID

    @Environment(BoardIndexStore.self) private var index
    @Environment(\.dismiss) private var dismiss

    @State private var isPresentingNewCard = false
    @State private var cardPendingMove: ItemID?

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

    private var lane: Lane? {
        session.snapshot?.lanes.first { $0.id == laneID }
    }

    var body: some View {
        content
            .navigationTitle(navigationTitle)
            .toolbar {
                if lane != nil {
                    Button("New Card", systemImage: "plus") {
                        isPresentingNewCard = true
                    }
                }
            }
            .task { session.open() }
            .titlePromptAlert("New Card", isPresented: $isPresentingNewCard, placeholder: "Card Title") { title in
                createCard(titled: title.isEmpty ? nil : title)
            }
            .confirmationDialog(
                "Move Card",
                isPresented: Binding(get: { cardPendingMove != nil }, set: { if !$0 { cardPendingMove = nil } }),
                titleVisibility: .visible
            ) {
                ForEach(otherLanes) { destination in
                    Button(displayTitle(of: destination)) {
                        if let cardID = cardPendingMove {
                            move(cardID, to: destination.id)
                        }
                        cardPendingMove = nil
                    }
                }
                Button("Cancel", role: .cancel) { cardPendingMove = nil }
            }
    }

    private var navigationTitle: String {
        lane.map(displayTitle(of:)) ?? "Lane"
    }

    private var otherLanes: [Lane] {
        (session.snapshot?.lanes ?? []).filter { $0.id != laneID }
    }

    private func displayTitle(of lane: Lane) -> String {
        guard let title = lane.title.value, !title.isEmpty else { return "Untitled Lane" }
        return title
    }

    @ViewBuilder
    private var content: some View {
        if let lane {
            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 lane.cards.isEmpty {
                    ContentUnavailableView(
                        "No Cards Yet",
                        systemImage: "rectangle.on.rectangle",
                        description: Text("Add a card to this lane.")
                    )
                } else {
                    // `lane.cards` is already in display order, exactly as the lane list trusts
                    // `BoardModel.lanes` — so it's trusted here rather than re-sorted.
                    ForEach(lane.cards) { card in
                        NavigationLink(value: BoardRoute.card(boardRoot: boardRoot, laneID: laneID, cardID: card.id)) {
                            CardSummaryRow(card: card)
                        }
                        .swipeActions(edge: .trailing) {
                            Button(role: .destructive) {
                                trash(card.id)
                            } label: {
                                Label("Trash", systemImage: "trash")
                            }
                        }
                        .swipeActions(edge: .leading) {
                            if !otherLanes.isEmpty {
                                Button {
                                    cardPendingMove = card.id
                                } label: {
                                    Label("Move", systemImage: "arrow.right.arrow.left")
                                }
                                .tint(.blue)
                            }
                        }
                    }
                    .onMove(perform: { moveRows(from: $0, to: $1) })
                }
            }
            .refreshable { session.reload() }
        } else if case .ready = session.phase {
            // The lane is gone from a `.ready` snapshot — deleted elsewhere while this screen was
            // open. Nothing left to list; back out rather than leave a dead screen on top of the
            // stack.
            ContentUnavailableView("Lane Removed", systemImage: "trash", description: Text("This lane was deleted."))
                .task { dismiss() }
        } else {
            ProgressView("Opening lane")
        }
    }

    private func createCard(titled title: String?) {
        let laneID = self.laneID
        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
                let laneFolder = root.appendingPathComponent(laneID.rawValue, isDirectory: true)
                return try BoardWriter.createCard(inLane: laneFolder, title: title)
            }
        }
    }

    private func trash(_ cardID: ItemID) {
        let laneID = self.laneID
        Task {
            await session.perform { (root: URL) throws(BoardWriteError) -> ItemID in
                let cardFolder = root
                    .appendingPathComponent(laneID.rawValue, isDirectory: true)
                    .appendingPathComponent(cardID.rawValue, isDirectory: true)
                return try BoardWriter.deleteCardToTrash(at: cardFolder, inBoard: root)
            }
        }
    }

    private func move(_ cardID: ItemID, to destinationLaneID: ItemID) {
        let laneID = self.laneID
        Task {
            await session.perform { (root: URL) throws(BoardWriteError) -> MoveResult in
                let sourceFolder = root
                    .appendingPathComponent(laneID.rawValue, isDirectory: true)
                    .appendingPathComponent(cardID.rawValue, isDirectory: true)
                let destinationParent = root.appendingPathComponent(destinationLaneID.rawValue, isDirectory: true)
                // `order: nil` — append at the end of the destination lane's visible cards, the
                // same placement a hand-created card gets.
                return try BoardWriter.moveItem(
                    at: sourceFolder,
                    toParent: destinationParent,
                    sourceBoardRoot: root,
                    destinationBoardRoot: root,
                    order: nil
                )
            }
        }
    }

    /// Commits a card drag's release.
    ///
    /// `manualOrder` is `lane.cards` with the drag already applied locally (`Array.move`), so the
    /// dragged card'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 for lanes. The happy path asks for the midpoint between the two neighbours the
    /// card lands between; on the rare lane 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 card in the lane to a fresh whole-multiple-of-1024 ladder in `manualOrder`'s own order,
    /// one `BoardWriter.moveItem` reorder call per card 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 lane,
              lane.cards.indices.contains(sourceIndex)
        else { return }

        var manualOrder = lane.cards
        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)
        let laneID = self.laneID
        // 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
                let laneFolder = root.appendingPathComponent(laneID.rawValue, isDirectory: true)
                if let rank = Ranks.insertionRank(amongVisible: siblingOrders, at: landingIndex) {
                    return try BoardWriter.moveItem(
                        at: laneFolder.appendingPathComponent(moved.id.rawValue, isDirectory: true),
                        toParent: laneFolder,
                        sourceBoardRoot: root,
                        destinationBoardRoot: root,
                        order: rank
                    )
                }
                for (card, freshRank) in zip(finalOrder, Ranks.renumbered(count: finalOrder.count)) {
                    _ = try BoardWriter.moveItem(
                        at: laneFolder.appendingPathComponent(card.id.rawValue, isDirectory: true),
                        toParent: laneFolder,
                        sourceBoardRoot: root,
                        destinationBoardRoot: root,
                        order: freshRank
                    )
                }
                return MoveResult(id: moved.id, reminted: [])
            }
        }
    }
}

/// One card row: its icon when it has one, title, and an attachment-count hint.
///
/// **No label chips.** `labels` is not a field `BoardModel`/`FrontmatterFields` expose — see
/// `CardAttributesSection`'s doc comment for why — so `attachments` (a field the model already
/// carries cheaply) stands in as the row's "cheap material" instead.
private struct CardSummaryRow: View {
    let card: Card

    var body: some View {
        HStack {
            ItemIconView(icon: card.icon.value, iconColor: card.iconColor.value)
            VStack(alignment: .leading, spacing: 2) {
                Text(displayTitle)
                if !card.attachments.isEmpty {
                    Label("\(card.attachments.count)", systemImage: "paperclip")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
            }
        }
    }

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