import Foundation

/// The immutable snapshot shape `BoardLoader` produces and every view/store reads
/// (02-architecture.md § Layering). `BoardModel`, `Lane`, and `Card` are plain value types —
/// no class, no shared mutable state — encoding the frontmatter tables from
/// 01-storage-format.md § Frontmatter as data. Level is position (root = board, depth 1 =
/// lane, depth 2 = card); there is deliberately no `type`/`level` discriminator field —
/// the three distinct Swift types encode it structurally.

/// The immutable identity of a lane or card folder: its exact name, byte-for-byte.
///
/// Folder names are lowercase UUIDv4, gated at load time (01-storage-format.md § Fractal
/// layout ▸ Rules, "Name shape gates level detection") — `BoardLoader` only ever promotes a
/// UUID-*shaped* folder (lowercase hex, `8-4-4-4-12`; version/variant nibbles unchecked) to a
/// `Lane`/`Card` in the first place, so every `ItemID` reaching this type already has that
/// shape. The model still stores whatever the folder is actually named rather than
/// re-validating or normalizing it: it must round-trip byte-perfect (it is the primary key) and
/// is the display-order tie-break (`Ranks.sortedForDisplay`). Deliberately **not** Foundation's
/// `UUID`, which normalizes to uppercase and would silently corrupt that round-trip.
///
/// Board roots don't get one of these: a board's folder name is a human/Finder-assigned
/// `.kanban` package name, not a UUID (01-storage-format.md § Board naming) — its identity is
/// `BoardModel.rootURL`, resolved elsewhere via a security-scoped bookmark
/// (02-architecture.md § Per-board app state), not a folder-name key.
public struct ItemID: Hashable, Sendable, RawRepresentable {
    public let rawValue: String

    public init(rawValue: String) {
        self.rawValue = rawValue
    }
}

extension ItemID: CustomStringConvertible {
    public var description: String { rawValue }
}

/// A board: `<root>/index.md` plus every lane beneath it. `<root>` is the `.kanban` package
/// (or an extension-less folder — both open).
public struct BoardModel: Sendable, Equatable {
    /// The board's identity — see `ItemID`'s doc comment for why boards don't have one of
    /// those instead.
    public let rootURL: URL

    public let schema: Int
    public let title: FieldValue<String>
    public let created: FieldValue<Date>
    public let modified: FieldValue<Date>
    public let modifiedBy: FieldValue<String>

    /// Legal per the common frontmatter table but meaningless at board level — a board can't
    /// tombstone itself out of its own window (01-storage-format.md § Deletion). `BoardLoader`
    /// ignores it (and emits a `LoadWarning`) rather than acting on it; the model keeps the
    /// field only so the value still round-trips. There is deliberately no `isDeleted` here —
    /// contrast `Lane`/`Card`.
    public let deleted: FieldValue<Date>

    public let background: FieldValue<String>
    public let icon: FieldValue<String>
    public let iconColor: FieldValue<String>

    /// `{order: N}` — picker position when this board lives in a template store
    /// (09-templates.md). Opaque by design: exposed as the engine's raw `YAMLValue`, never
    /// parsed into a dedicated Swift shape, so future subkeys need no model change.
    public let template: YAMLValue?

    /// Lanes in display order (`Ranks.sortedForDisplay`, folder-name tie-break) — **including
    /// tombstoned lanes**, which stay in the snapshot flagged (`Lane.isDeleted`) for the trash
    /// view (01-storage-format.md § Deletion).
    public let lanes: [Lane]

    /// The full parsed `index.md`. Unknown/reserved keys (`labels`, `assignees`, `due`,
    /// `remote`, …) ride along uninterpreted via `document.unknownFields` so a future writer
    /// can round-trip them without this model knowing what they mean.
    public let document: FrontmatterDocument

    /// The board description (free Markdown) — equivalent to `document.body`.
    public var body: String { document.body }
}

/// A lane: `<root>/<guid>/index.md` plus every card beneath it.
public struct Lane: Identifiable, Sendable, Equatable {
    public let id: ItemID

    public let schema: Int
    public let title: FieldValue<String>
    public let created: FieldValue<Date>
    public let modified: FieldValue<Date>
    public let modifiedBy: FieldValue<String>
    public let deleted: FieldValue<Date>
    public let background: FieldValue<String>
    public let icon: FieldValue<String>
    public let iconColor: FieldValue<String>

    /// Rank among lanes, ascending = left-to-right. Strict, per the frontmatter table's
    /// required field: `BoardLoader` fails the whole load (`missingOrder`/`malformedOrder`)
    /// rather than construct a `Lane` with a bad `order` — by the time one exists here it is
    /// always valid. Validity is the loader's job, not this type's; that is why it is a plain
    /// `Double` and not `FieldValue<Double>`.
    public let order: Double

    /// Width multiplier ≥ 1 (default 1 when missing or malformed). Lenient — it styles layout,
    /// it isn't structure — so a malformed value is preserved (`.malformed`) rather than
    /// failing the load (01-storage-format.md § Frontmatter).
    public let width: FieldValue<Int>

    /// Cards in this lane, in display order (`Ranks.sortedForDisplay`, folder-name tie-break)
    /// — **including tombstoned cards**, which stay in the snapshot flagged (`Card.isDeleted`)
    /// for the trash view.
    public let cards: [Card]

    /// The full parsed `index.md`; unknown/reserved keys ride along uninterpreted.
    public let document: FrontmatterDocument

    /// The lane description / WIP policy / notes — equivalent to `document.body`.
    public var body: String { document.body }

    /// A tombstoned lane: the `deleted` key is *present* (valid or malformed), not merely
    /// absent. Deliberate: a malformed timestamp still hides the lane from the board — the
    /// key's presence is what encodes deletion intent, a missing key is the only thing that
    /// means "not deleted" (`FieldValue.isMissing` already treats an explicit `deleted: null`
    /// as absent, matching the engine's own null-is-missing rule).
    public var isDeleted: Bool { !deleted.isMissing }
}

/// A card: `<root>/<guid>/<guid>/index.md`. Leaf of the fractal tree — `attachments/` and the
/// (future, out-of-scope) `comments/` live alongside `index.md` on disk but are not modeled
/// here.
public struct Card: Identifiable, Sendable, Equatable {
    public let id: ItemID

    public let schema: Int
    public let title: FieldValue<String>
    public let created: FieldValue<Date>
    public let modified: FieldValue<Date>
    public let modifiedBy: FieldValue<String>
    public let deleted: FieldValue<Date>
    public let background: FieldValue<String>
    public let icon: FieldValue<String>
    public let iconColor: FieldValue<String>

    /// Rank within its lane, ascending = top-to-bottom. Strict — see `Lane.order`'s doc
    /// comment; the same reasoning applies here.
    public let order: Double

    /// The full parsed `index.md`; unknown/reserved keys ride along uninterpreted.
    public let document: FrontmatterDocument

    /// The card's content — the whole point. Equivalent to `document.body`.
    public var body: String { document.body }

    /// A tombstoned card. See `Lane.isDeleted`'s doc comment — the same "presence, not
    /// validity" rule applies here.
    public var isDeleted: Bool { !deleted.isMissing }
}
