import SwiftUI

/// The card's read-only view — `BoardRoute.card`'s destination.
///
/// Holds board-relative IDs only, never a `Card` value: every body evaluation re-reads the card
/// from `session.snapshot`, so a write this screen's own edit sheet makes (or one that lands from
/// elsewhere while it's open) is reflected the moment the session's reload lands, and a card
/// trashed on another device is noticed rather than shown stale.
///
/// **Read-only, by design.** There is no draft here and nothing this screen ever writes — editing
/// lives entirely in `CardEditScreen`, presented full-screen from the Edit button. That split is
/// the whole point of the transactional model: a screen that only ever renders `session.snapshot`
/// cannot itself go stale relative to disk, and there is no auto-save timing to reason about
/// because there is no save at all on this side of the Edit button.
///
/// **Comments are the one thing here the snapshot does not carry.** The thread lives outside the
/// board walk (the Mac's arrangement, kept), so this screen reads it itself — re-read whenever
/// `session.snapshotGeneration` moves, which covers both a comment sheet's own write (whose
/// `perform` awaits the reload) and a foreign change landing. The read-only posture survives
/// intact: the thread renders read-only, and the two comment writes live behind their own
/// transactional sheets (`CommentEditorScreen`), exactly as card edits live behind Edit.
struct CardDetailScreen: View {
    let boardRoot: URL
    let laneID: ItemID
    let cardID: ItemID

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

    @State private var isPresentingEdit = false
    @State private var thread: CommentThread?
    @State private var isComposingComment = false
    @State private var editingComment: Comment?

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

    private var card: Card? {
        session.snapshot?.lanes.first { $0.id == laneID }?.cards.first { $0.id == cardID }
    }

    var body: some View {
        content
            .navigationTitle("Card")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                if card != nil {
                    ToolbarItem(placement: .primaryAction) {
                        Button("Edit") { isPresentingEdit = true }
                    }
                }
            }
            .task { session.open() }
            // The thread's read, keyed on the walk counter: runs at appearance (generation 0 or
            // whatever the session has reached), then again every time a walk lands — which is
            // every landing point a thread on this screen can have gone stale at.
            .task(id: session.snapshotGeneration) {
                thread = await session.loadCommentThread(laneID: laneID, cardID: cardID)
            }
            .fullScreenCover(isPresented: $isPresentingEdit) {
                CardEditScreen(boardRoot: boardRoot, laneID: laneID, cardID: cardID)
            }
            .sheet(isPresented: $isComposingComment) {
                CommentEditorScreen(boardRoot: boardRoot, laneID: laneID, cardID: cardID, mode: .compose)
            }
            .sheet(item: $editingComment) { comment in
                CommentEditorScreen(boardRoot: boardRoot, laneID: laneID, cardID: cardID, mode: .edit(comment))
            }
    }

    @ViewBuilder
    private var content: some View {
        if let card {
            ScrollView {
                VStack(alignment: .leading, spacing: 20) {
                    if session.lastError != nil {
                        Label("Showing the last saved version — a recent update didn't load.", systemImage: "exclamationmark.triangle")
                            .font(.footnote)
                            .foregroundStyle(.secondary)
                    }

                    titleBlock(for: card)
                    bodyBlock(for: card)
                    detailsFooter(for: card)
                    CardCommentsSection(
                        thread: thread,
                        onAdd: { isComposingComment = true },
                        onEdit: { editingComment = $0 }
                    )
                }
                .padding()
                .frame(maxWidth: .infinity, alignment: .leading)
            }
        } else if case .ready = session.phase {
            ContentUnavailableView("Card Removed", systemImage: "trash", description: Text("This card was deleted."))
                .task { dismiss() }
        } else {
            ProgressView("Opening card")
        }
    }

    /// The title, its icon when the card has one, and a subtle background tint lifted from the
    /// card's own `background` swatch — passive display only, no swatches to tap here. An
    /// unrecognized colour name (the same leniency `ItemIconView`/`CardPalette` document for
    /// themselves) just skips the tint rather than guessing at one.
    ///
    /// **Alignment constraint.** The title's leading edge has to land exactly where the body's
    /// paragraphs land, one block below — both sit at nothing but the `ScrollView`'s own outer
    /// `.padding()`, so this view carries no padding of its own around the icon/text content. The
    /// tint still wants breathing room rather than painting flush against the letterforms, but
    /// that room can't come from padding the content — that's exactly what would reintroduce the
    /// indent this fixes. It comes from the tint shape growing *outward* past the content's own
    /// bounds instead, via a negative inset in `.background`, which is invisible to layout: the
    /// row's frame — and so the title's leading edge — is unaffected by how far its background
    /// paints past it. A card with no tint (most of them) renders with zero indent, flush with the
    /// body below; a tinted card's title sits in exactly the same place, just with a soft wash
    /// bleeding past its edges.
    @ViewBuilder
    private func titleBlock(for card: Card) -> some View {
        HStack(alignment: .firstTextBaseline, spacing: 10) {
            ItemIconView(icon: card.icon.value, iconColor: card.iconColor.value)
            titleText(for: card)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .background {
            if let tintColor = tintColor(for: card) {
                RoundedRectangle(cornerRadius: 12)
                    .fill(tintColor.opacity(0.15))
                    .padding(-10)
            }
        }
    }

    @ViewBuilder
    private func titleText(for card: Card) -> some View {
        if let title = card.title.value, !title.isEmpty {
            Text(title)
                .font(.title.bold())
        } else {
            Text("Untitled Card")
                .font(.title.bold())
                .foregroundStyle(.secondary)
        }
    }

    private func tintColor(for card: Card) -> Color? {
        card.background.value.flatMap { CardPalette.color(named: $0, in: CardPalette.backgrounds) }
    }

    /// The body, rendered as Markdown — see `CardBodyView` for the block-by-block renderer this
    /// delegates to, built on the same `BodyMarkup` model the Mac's Preview surface parses with.
    @ViewBuilder
    private func bodyBlock(for card: Card) -> some View {
        CardBodyView(body: card.body)
    }

    @ViewBuilder
    private func detailsFooter(for card: Card) -> some View {
        VStack(alignment: .leading, spacing: 4) {
            Text("Created \(card.created.value.map(Self.formatted) ?? "—")")
            Text("Modified \(card.modified.value.map(Self.formatted) ?? "—")")
        }
        .font(.footnote)
        .foregroundStyle(.secondary)
        .padding(.top, 8)
    }

    private static func formatted(_ date: Date) -> String {
        date.formatted(date: .abbreviated, time: .shortened)
    }
}
