import SwiftUI

// MARK: - One row

/// One **Details** row: a frontmatter key the app does not own, and the text it carries
/// (05-card-window.md ▸ Details).
///
/// `id` is the key because the effective frontmatter view has one entry per key — a key written
/// twice reads once, at its winning occurrence (`FrontmatterDocument.parse`, the last-wins rule) —
/// so there is nothing here for a duplicate to collide with.
struct CardDetailRow: Identifiable, Equatable, Sendable {
    let key: String
    let value: String

    var id: String { key }
}

// MARK: - The seam

/// What the Details section shows, as a pure function of a parsed `index.md`
/// (05-card-window.md ▸ Details).
///
/// ### Which keys
///
/// **Every unknown key, and only unknown keys, in file order.** The set is
/// `FrontmatterDocument.unknownFields` unchanged — the schema's eleven names are out, and everything
/// else is in, *including* the reserved enhanced-schema names (`labels`, `assignees`, `due`,
/// `remote`, …), which "are ordinary unknown keys in this version and appear here like any other —
/// no special rendering". Order is the document's, which is the file's: 01-storage-format.md
/// preserves key order verbatim and the sidebar honors it, so a hand-written key sits where the hand
/// that wrote it put it.
///
/// Nothing here reaches for a card's folder or re-reads a file: the snapshot's `Card` already
/// carries its whole parsed document, so this section shows exactly what the last reload read, like
/// every other surface in the window.
///
/// ### Which text
///
/// **The rawest honest form the parse can offer, and never an error.** A value written on one line
/// renders as the author's own bytes — `rawValue`, which is the span minus the `key:` header, the
/// surrounding whitespace and any trailing comment (a comment belongs to the line, not to the
/// value). Quotes, hex, an ISO timestamp, a flow collection all read exactly as typed, which is the
/// whole point of a section that exists to show what the app did *not* interpret.
///
/// Two shapes have no single-line source text to show, and both fall back to the engine's own
/// reading of the value (`YAMLValue.description`):
///
/// - **A value spanning several lines** — a block scalar, a multi-line flow collection. Its raw span
///   carries YAML syntax the value does not (`|`, `>-`, the continuation indent), so the bytes would
///   be a worse answer than the text they encode.
/// - **A value with no text at all** — `project:` with nothing after it. The rawest form is the empty
///   string, and a row with a key and a blank beside it reads as a bug rather than as YAML's null.
///
/// Neither branch can throw and neither can fail: the document was parsed before a `Card` existed at
/// all, and every reading below is total over `YAMLValue`. "Exotic YAML shapes display best-effort,
/// never error" is that, exactly.
enum CardDetails {

    /// The section's rows, or `[]` when the card carries no unknown keys — which is also the whole
    /// of the section's visibility rule ("shown only when any exist"), stated where a test can hold
    /// it rather than as an `if` in a view.
    nonisolated static func rows(of document: FrontmatterDocument) -> [CardDetailRow] {
        document.unknownFields.map { CardDetailRow(key: $0.key, value: display(of: $0)) }
    }

    /// One field's display text — see the type's doc comment for the rule and its two fallbacks.
    nonisolated static func display(of field: FrontmatterField) -> String {
        let raw = field.rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
        if !raw.isEmpty, !raw.contains(where: \.isNewline) { return raw }
        return field.value.description.trimmingCharacters(in: .whitespacesAndNewlines)
    }
}

// MARK: - The section

/// The sidebar's **Details** section: read-only key/value rows for every unknown frontmatter key
/// (05-card-window.md ▸ Details).
///
/// ### There is no edit affordance, and that is the design
///
/// "Editing is deliberately not offered: the **raw source outlet** is the write path for frontmatter
/// the app doesn't own." So these rows are text — selectable, copyable, and nothing else. A field
/// here would have to guess a YAML shape for whatever the user typed, into a key whose meaning the
/// app has no opinion about; ⌥⌘E edits the file itself, where the guess is the user's.
///
/// ### Absent, not empty
///
/// The section disappears entirely on a card with no unknown keys — which is most cards. Contrast
/// Attachments, which keeps a hint when empty because it advertises a drop surface the user has to
/// be able to find: there is nothing to teach here, and a permanent empty "Details" header would
/// imply the card has details it is failing to show.
struct CardDetailsSection: View {

    let rows: [CardDetailRow]

    private var pointSize: CGFloat { CardWindowMetrics.bodyPointSize }

    var body: some View {
        if !rows.isEmpty {
            VStack(alignment: .leading, spacing: CardWindowMetrics.sidebarRowSpacing(bodyPointSize: pointSize)) {
                CardSidebarSectionHeader(title: "Details")
                ForEach(rows) { row in
                    self.row(row)
                }
            }
            .frame(maxWidth: .infinity, alignment: .leading)
        }
    }

    /// Key over value rather than key beside value: the sidebar is 26 characters wide, and a
    /// two-column row would give a `project:` overlay four characters to say `lanework` in. The value
    /// wraps to as many lines as it needs — the sidebar scrolls, and a truncated value in a section
    /// whose only job is to show values would be the one thing worse than no section at all.
    private func row(_ row: CardDetailRow) -> some View {
        VStack(alignment: .leading, spacing: 1) {
            Text(row.key)
                .font(.caption)
                .foregroundStyle(.secondary)
                .lineLimit(1)
                .truncationMode(.middle)
            Text(row.value)
                .font(.callout)
                .textSelection(.enabled)
                .fixedSize(horizontal: false, vertical: true)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .accessibilityElement(children: .combine)
        .accessibilityLabel("\(row.key), \(row.value)")
    }
}
