import SwiftUI

/// The card editor — title, body, and attributes, all drafted and committed together — presented
/// full-screen from `CardDetailScreen`'s Edit button.
///
/// Holds board-relative IDs only, never a `Card` value, for the same reason every other screen in
/// this stack does: the card is re-read from `session.snapshot` on every body evaluation, so a
/// write landing from elsewhere while this sheet is open (a trash from another device, say) is
/// noticed rather than edited into thin air.
///
/// ### Transactional, not auto-saving
///
/// Every earlier screen in this app commits a field the moment it stops being edited — `onSubmit`,
/// `onDisappear`, a scene-phase change. This one commits nothing until `Save`, deliberately: a
/// `fullScreenCover` has no interactive swipe-to-dismiss, so the only way out mid-edit is a button
/// this screen controls, which makes "ask before discarding" both possible and the more honest
/// default for a screen with five drafted fields open across two panes at once. There is no
/// scenePhase or onDisappear commit anywhere below — backgrounding the app or losing focus leaves
/// the drafts exactly where they were, waiting for `Save` or `Cancel`.
///
/// `Cancel` on a clean sheet dismisses immediately; on a dirty one it raises a confirmation before
/// discarding. `Save` writes every changed field in one `session.perform` bracket — title through
/// `.rename`, the three style keys through one `.style` call, body through `writeBody` — writes
/// nothing at all when nothing changed, and dismisses only on success: a failed write keeps the
/// sheet and its drafts and raises an alert (see `save()`).
struct CardEditScreen: View {
    let boardRoot: URL
    let laneID: ItemID
    let cardID: ItemID

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

    private enum Pane: String, CaseIterable, Hashable {
        case details = "Details"
        case body = "Body"
    }

    @State private var pane: Pane = .details

    @State private var titleDraft = ""
    @State private var bodyDraft = ""
    @State private var iconDraft: String?
    @State private var iconColorDraft: String?
    @State private var backgroundDraft: String?
    @State private var isConfirmingDiscard = false
    @State private var saveError: BoardSessionError?
    @FocusState private var isBodyFocused: Bool

    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 {
        NavigationStack {
            content
                .navigationTitle("Card")
                .navigationBarTitleDisplayMode(.inline)
                .toolbar {
                    ToolbarItem(placement: .cancellationAction) {
                        Button("Cancel") { requestDismiss() }
                    }
                    if card != nil {
                        ToolbarItem(placement: .principal) {
                            Picker("Pane", selection: $pane) {
                                ForEach(Pane.allCases, id: \.self) { pane in
                                    Text(pane.rawValue).tag(pane)
                                }
                            }
                            .pickerStyle(.segmented)
                            .frame(maxWidth: 220)
                        }
                        ToolbarItem(placement: .confirmationAction) {
                            Button("Save") { save() }
                        }
                    }
                }
                .task { session.open() }
                // Seeds the drafts once per card. `.task(id:)` re-runs only when `card?.id`
                // changes — nil while the session is still opening, then the card's own id once
                // it lands — so a reload that lands *while this sheet is open* never clobbers a
                // draft the user is mid-typing. Unlike the old in-place editor, nothing this
                // screen does triggers such a reload before `Save` — but another device's write
                // can still land, and the same guard covers that case too.
                .task(id: card?.id) {
                    guard let card else { return }
                    titleDraft = card.title.value ?? ""
                    bodyDraft = card.body
                    iconDraft = card.icon.value
                    iconColorDraft = card.iconColor.value
                    backgroundDraft = card.background.value
                }
                .confirmationDialog(
                    "Discard Changes?",
                    isPresented: $isConfirmingDiscard,
                    titleVisibility: .visible
                ) {
                    Button("Discard", role: .destructive) { dismiss() }
                    Button("Cancel", role: .cancel) {}
                }
                .alert(
                    "Couldn't Save",
                    isPresented: Binding(get: { saveError != nil }, set: { if !$0 { saveError = nil } }),
                    presenting: saveError
                ) { _ in
                    Button("OK") { saveError = nil }
                } message: { error in
                    Text(error.description)
                }
        }
    }

    @ViewBuilder
    private var content: some View {
        if let card {
            switch pane {
            case .details:
                detailsForm(for: card)
            case .body:
                TextEditor(text: $bodyDraft)
                    .focused($isBodyFocused)
                    .padding(.horizontal, 12)
                    .padding(.top, 8)
            }
        } else if case .ready = session.phase {
            // The card is gone from a `.ready` snapshot — deleted elsewhere while this sheet was
            // open. There is nothing left to edit; dismiss the cover rather than leave drafts for
            // a card that no longer exists.
            ContentUnavailableView("Card Removed", systemImage: "trash", description: Text("This card was deleted."))
                .task { dismiss() }
        } else {
            ProgressView("Opening card")
        }
    }

    @ViewBuilder
    private func detailsForm(for card: Card) -> some View {
        Form {
            if session.lastError != nil {
                Section {
                    Label("Showing the last saved version — a recent update didn't load.", systemImage: "exclamationmark.triangle")
                        .font(.footnote)
                        .foregroundStyle(.secondary)
                }
            }

            Section("Title") {
                TextField("Untitled Card", text: $titleDraft)
            }

            CardAttributesSection(icon: $iconDraft, iconColor: $iconColorDraft, background: $backgroundDraft)

            Section("Details") {
                LabeledContent("Created", value: card.created.value.map(Self.formatted) ?? "—")
                LabeledContent("Modified", value: card.modified.value.map(Self.formatted) ?? "—")
            }
        }
    }

    /// Any draft differing from the card's own current snapshot value — title compared trimmed,
    /// exactly as `Save` compares it, so this predicate and the write it gates never disagree.
    private var isDirty: Bool {
        guard let card else { return false }
        let trimmedTitle = titleDraft.trimmingCharacters(in: .whitespacesAndNewlines)
        return trimmedTitle != (card.title.value ?? "")
            || bodyDraft != card.body
            || iconDraft != card.icon.value
            || iconColorDraft != card.iconColor.value
            || backgroundDraft != card.background.value
    }

    private func requestDismiss() {
        if isDirty {
            isConfirmingDiscard = true
        } else {
            dismiss()
        }
    }

    /// One `session.perform` bracket, three independent writes inside it gated on whatever
    /// actually changed — a clean `Save` (nothing dirty) makes no write at all and just dismisses.
    /// Awaits the perform before dismissing: `perform` already awaits its own reload, so by the
    /// time this returns `CardDetailScreen`'s snapshot underneath already reflects the write, and
    /// the read screen shows the new values the instant the cover animates away.
    ///
    /// Dismissal is gated on the perform's own `Result` — the one caller of `perform` in this app
    /// that can't ignore it. The auto-committing screens could fire and forget because their
    /// drafts outlived the write attempt; here dismissing *is* draft destruction, so a failed
    /// write keeps the sheet (and every draft) exactly where it was and raises an alert instead.
    /// `perform` reloads even on failure, so a partially-applied multi-call write (title landed,
    /// body didn't) re-seeds nothing — the drafts keep the user's full intent and a retry rewrites
    /// only what still differs.
    private func save() {
        guard let card else {
            dismiss()
            return
        }

        let trimmedTitle = titleDraft.trimmingCharacters(in: .whitespacesAndNewlines)
        let titleChanged = trimmedTitle != (card.title.value ?? "")
        let newTitle: String? = trimmedTitle.isEmpty ? nil : trimmedTitle

        let iconChanged = iconDraft != card.icon.value
        let iconColorChanged = iconColorDraft != card.iconColor.value
        let backgroundChanged = backgroundDraft != card.background.value
        let styleChanged = iconChanged || iconColorChanged || backgroundChanged

        let bodyChanged = bodyDraft != card.body

        guard titleChanged || styleChanged || bodyChanged else {
            dismiss()
            return
        }

        let laneID = self.laneID
        let cardID = self.cardID
        let cardTitle = card.title.value
        let newIcon = iconDraft
        let newIconColor = iconColorDraft
        let newBackground = backgroundDraft
        let newBody = bodyDraft

        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.
            let outcome = await session.perform { (root: URL) throws(BoardWriteError) -> Void in
                let folder = root
                    .appendingPathComponent(laneID.rawValue, isDirectory: true)
                    .appendingPathComponent(cardID.rawValue, isDirectory: true)

                if titleChanged {
                    // The Mac's own rename path, verbatim (`BoardStore.setTitle`): plain
                    // `set`/`remove` — `setStyleValue` is the style gesture's helper, not the
                    // title's — and `.rename(title: nil)` so `updateIndex` enriches the operation
                    // from the document it reads rather than trusting a snapshot that may have
                    // aged.
                    try BoardWriter.updateIndex(inItemFolder: folder, operation: .rename(title: nil)) { document in
                        if let newTitle {
                            document.set(FrontmatterKeys.title, to: .string(newTitle))
                        } else {
                            document.remove(FrontmatterKeys.title)
                        }
                    }
                }

                if styleChanged {
                    // One `.style` call for all three keys — `operation: .style` is the
                    // vocabulary's own case for "`updateIndex` on behalf of styling flows"
                    // (`WriteOperation.style`), and each changed key writes through
                    // `setStyleValue` exactly as the old immediate-write attributes section did.
                    try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: cardTitle)) { document in
                        if iconChanged { document.setStyleValue(newIcon, for: FrontmatterKeys.icon) }
                        if iconColorChanged { document.setStyleValue(newIconColor, for: FrontmatterKeys.iconColor) }
                        if backgroundChanged { document.setStyleValue(newBackground, for: FrontmatterKeys.background) }
                    }
                }

                if bodyChanged {
                    _ = try BoardWriter.writeBody(inItemFolder: folder, body: newBody)
                }
            }
            switch outcome {
            case .success:
                dismiss()
            case let .failure(error):
                saveError = error
            }
        }
    }

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