import Foundation

/// One board template — **a schema-valid board folder that loaded** (09-templates.md ▸ Definition
/// format: "A template is itself a board").
///
/// ### There is no template model, only a board model
///
/// Everything the chooser shows and everything instantiation reproduces is read off the template
/// board's own `index.md`, through the ordinary `BoardLoader`: `title` is the display name,
/// `icon`/`iconColor` are the picker icon and what the new board inherits, the **body** is the
/// blurb (which becomes the new board's description by simply being copied), and `template.order`
/// is the chooser position. This type holds the loaded `BoardModel` and derives those from it — it
/// stores no copies, so a template's identity can never drift from its file.
///
/// That is 09's "dogfood" clause taken literally: "the template format *is* the board format — no
/// second schema, no parallel Swift model to keep in sync". The pathfinder's Swift-struct catalog
/// (and the `laneTitles` stub that stood in for it here) is exactly what this replaces.
///
/// ### A value you can hold is a template that loaded
///
/// The initializer is the load (`TemplateEngine.load(templateAt:origin:)`), so there is no
/// "unloadable template" case in this type: an unloadable *user* template is a chooser row, not a
/// template — 09 says it is "still listed — by folder name, marked unloadable, carrying the loader's
/// fail-fast specifics — but can't be instantiated or previewed", and that listing is the chooser
/// card's, built from the loader's error rather than from a half-built value of this type. Anything
/// holding a `BoardTemplate` is therefore holding something instantiable.
struct BoardTemplate: Identifiable, Sendable, Equatable {

    /// Which store the template came from — 09's two tiers ("bundled templates by `template.order`,
    /// then keyed user templates by `template.order`, then keyless user boards last"). Carried
    /// rather than derived from the URL: the chooser's ordering and its Reveal in Finder affordance
    /// both turn on the tier, and re-deriving it from a path prefix would be a second answer to a
    /// question the discovery walk already answered.
    enum Origin: Sendable, Equatable {
        case bundled
        case user
    }

    /// The template folder itself — `<app bundle>/Templates/basic.kanban`, or a folder in the user
    /// store. The instantiation source, and the identity here.
    let url: URL

    let origin: Origin

    /// The template board as the ordinary loader read it. Instantiation does **not** use this — it
    /// copies the tree on disk — but the chooser's mini per-lane preview renders from it, and every
    /// derived property below reads it.
    let model: BoardModel

    /// A template is identified by where it lives: slugs are stable but not unique across the two
    /// stores (a user template may legitimately be called `basic.kanban` too), and the chooser's
    /// selection must never be ambiguous between tiers.
    var id: String { url.path }

    /// The stable slug — 09: "The bundle folder name (`basic.kanban`) is the template's stable slug
    /// (tests, a11y ids)". The extension is dropped because an extension-less board folder is
    /// equally legal (01-storage-format.md § Document packaging) and `basic` is the name the design
    /// uses.
    var slug: String { url.deletingPathExtension().lastPathComponent }

    /// The display name: the board's `title`, falling back to the folder name — 01-storage-format.md
    /// § Board naming's rule, unchanged, because a template is a board.
    var name: String { model.title.value ?? slug }

    /// The picker blurb — the template board's **body**, which is also what the instantiated board
    /// carries as its description (09: the body is "shown in the picker *and* becoming the new
    /// board's description"). Whitespace-trimmed for display only; the bytes on disk are copied
    /// verbatim by instantiation and never touched here.
    var blurb: String { model.document.body.trimmingCharacters(in: .whitespacesAndNewlines) }

    /// The board icon shown in the picker and inherited by the new board — the lenient `icon` rule
    /// (`ItemSymbol`), so a template naming a symbol this OS cannot draw shows the board default
    /// rather than an empty box.
    var icon: String { ItemSymbol.name(model.icon, fallback: ItemSymbol.board) }

    /// The icon's palette tint, or `nil` for the chrome default — read raw, since `Palette` is the
    /// one place a colour name is resolved.
    var iconColor: String? { model.iconColor.value }

    /// The chooser position — `template.order`, 09's **one** picker key and its only subkey.
    ///
    /// Read out of the model's opaque `YAMLValue` rather than through a typed accessor, deliberately:
    /// 09 keeps the key's shape open ("future subkeys possible"), and `BoardModel.template` is
    /// carried raw for exactly that reason. `nil` covers every shape that is not a number under
    /// `order` — a missing key, a hand-dropped board that never had one, a malformed value — which
    /// is one case to the chooser: 09's keyless tier, sorted by display name.
    var order: Double? {
        guard case let .mapping(pairs) = model.template else { return nil }
        guard let value = pairs.last(where: { $0.key == .string("order") })?.value else { return nil }
        switch value {
        case let .int(number): return Double(number)
        case let .double(number): return number
        default: return nil
        }
    }

    /// The template's lanes in display order, for the chooser's mini per-lane preview — a real
    /// `BoardModel`'s lanes, not a list of strings the app maintains by hand.
    var lanes: [Lane] { model.lanes }
}

// MARK: - A chooser row

/// One row of the template chooser: **a template that loaded, or a folder that didn't**.
///
/// This is the either-shape 09-templates.md requires of the chooser and deliberately not of
/// `BoardTemplate`:
///
/// > **One bad template never fails the chooser** (the user store is hand-editable, so a malformed
/// > board there is one edit away): a user template the loader rejects is still listed — by folder
/// > name, marked unloadable, carrying the loader's fail-fast specifics — but can't be instantiated
/// > or previewed; fix the files and it comes back. (09 ▸ Why this format)
///
/// So the discovery walk answers with rows and `TemplateEngine.load` keeps answering with a
/// `Result`: anything holding a `BoardTemplate` is still holding something instantiable, and the one
/// place a failure is *rendered* is the one place that can carry it — the row.
///
/// **The unloadable half is a user-store shape only.** A bundled template that does not load is a
/// build defect, logged and skipped (`TemplateEngine.bundledTemplates()`), because nobody looking at
/// the chooser can fix an app's own resources.
enum TemplateRow: Identifiable, Sendable {

    case template(BoardTemplate)
    case unloadable(Unloadable)

    /// A store folder the loader rejected — the URL and the loader's error, whole and unreworded.
    struct Unloadable: Identifiable, Sendable, Equatable {

        let url: URL
        let error: BoardLoadError

        var id: String { url.path }

        /// **The folder name, sans extension** — "the failed load can supply neither
        /// `template.order` nor `title`, so the folder name is the only identity it has" (09
        /// ▸ Storage). Spelled the way every other board display name falls back
        /// (`AppModel.folderDisplayName(of:)`, `BoardTemplate.slug`), so a broken `Notes.kanban` and
        /// a working one sort and read alike rather than differing by four characters.
        var name: String { url.deletingPathExtension().lastPathComponent }
    }

    var id: String {
        switch self {
        case let .template(template): template.id
        case let .unloadable(unloadable): unloadable.id
        }
    }

    var url: URL {
        switch self {
        case let .template(template): template.url
        case let .unloadable(unloadable): unloadable.url
        }
    }

    /// The name the chooser shows and the keyless tier sorts by.
    var name: String {
        switch self {
        case let .template(template): template.name
        case let .unloadable(unloadable): unloadable.name
        }
    }

    /// `template.order`, and **always `nil` for an unloadable row** — 09 sorts it with the keyless
    /// tier for the reason that it has no key to read.
    var order: Double? {
        switch self {
        case let .template(template): template.order
        case .unloadable: nil
        }
    }

    /// The template behind the row, or `nil` — the whole of "can't be instantiated or previewed",
    /// expressed as the absence of the value both of those need.
    var template: BoardTemplate? {
        switch self {
        case let .template(template): template
        case .unloadable: nil
        }
    }

    /// The failure behind the row, or `nil` — the other half of the same either, so a caller holding
    /// an optional row can ask both questions without a nested `case .some(.unloadable(…))`.
    var unloadable: Unloadable? {
        switch self {
        case .template: nil
        case let .unloadable(unloadable): unloadable
        }
    }
}
