import Foundation

// MARK: - BoardWindowRef

/// What a board window *is*, as a value: the board's root path.
///
/// `WindowGroup(id:for:)` keys its windows on the presented value, so this type is simultaneously
/// the window's identity and the argument its host opens with — which is what makes
/// "**one board window per root**" (02-architecture.md § Windows) a property of the scene rather
/// than bookkeeping somebody has to remember: `openWindow(value:)` with a ref that already has a
/// window focuses that window instead of opening a second one.
///
/// **A path, not a bookmark or a file identity**, even though the app keys boards by identity
/// everywhere else (`BoardStoreRegistry`, `BoardRegistry`). Two reasons, both about what a window
/// value has to be: it must be `Codable` into a scene-restoration archive, and it must be cheap to
/// compare — a `FileIdentity` is neither. The identity keying is not lost, only moved: `AppModel`
/// asks `BoardStoreRegistry.liveStore(for:)` — which *is* identity-keyed — before opening anything,
/// so a board reached through two spellings of its path still lands on the window it already has.
public struct BoardWindowRef: Codable, Hashable, Sendable {

    /// The board root's filesystem path.
    public let path: String

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

    public init(url: URL) {
        self.init(path: url.path)
    }

    /// The root as a URL again. Carries **no** security-scoped access — the scope belongs to the
    /// URL object the bookmark resolved to, which the session holds for its whole life (`AppModel`),
    /// never to a URL rebuilt from a string.
    public var url: URL {
        URL(fileURLWithPath: path, isDirectory: true)
    }
}

// MARK: - CardWindowRef

/// What a card window is: **board root path plus card GUID** (05-card-window.md ▸ Deletion &
/// lifecycle).
///
/// That compound key is the whole lifecycle rule in one value. Within its board the window *follows*
/// its card — the key names the card, not the lane, so a move between lanes is invisible to the
/// window. Across boards it does not: "a cross-board move dismisses it exactly like a delete, since
/// the board half of the key no longer names it once the card has left" — the card's UUID travels
/// with the move, but `(oldBoard, uuid)` names nothing afterwards, so the window that was keyed on
/// it has no card and dismisses.
///
/// ### The card id is a string, compared like an `ItemID`
///
/// `rawValue` is stored, because the folder's exact spelling is what builds URLs and what must
/// round-trip byte-perfect (`ItemID`'s doc comment in `BoardModel.swift`). But equality and hashing
/// **case-fold** it, because `ItemID` does: two case-spellings of one UUID are one identity
/// everywhere in this app, and a window key that disagreed would open a *second* window for a card
/// whose folder was spelled `ABC…` where the first was spelled `abc…` — the exact duplicate the
/// identity rule exists to prevent. The board half is compared verbatim: it is a path, and paths are
/// the filesystem's business, not this type's.
public struct CardWindowRef: Codable, Hashable, Sendable {

    /// The owning board root's path — the same string a `BoardWindowRef` carries, which is what lets
    /// a card window find its board's session.
    public let boardPath: String

    /// The card's `ItemID.rawValue`: the folder name exactly as it is spelled on disk.
    public let cardID: String

    public init(boardPath: String, cardID: String) {
        self.boardPath = boardPath
        self.cardID = cardID
    }

    public init(board: BoardWindowRef, cardID: ItemID) {
        self.init(boardPath: board.path, cardID: cardID.rawValue)
    }

    /// The board half, as the board window's own key — how a card window reaches its session.
    public var board: BoardWindowRef {
        BoardWindowRef(path: boardPath)
    }

    public var boardURL: URL {
        board.url
    }

    /// The card id under `ItemID`'s comparison rule. Computed, so `cardID` stays the single source of
    /// truth for what is on disk.
    public var cardIdentity: ItemID {
        ItemID(rawValue: cardID)
    }

    public static func == (lhs: CardWindowRef, rhs: CardWindowRef) -> Bool {
        lhs.boardPath == rhs.boardPath && lhs.cardIdentity == rhs.cardIdentity
    }

    public func hash(into hasher: inout Hasher) {
        hasher.combine(boardPath)
        hasher.combine(cardIdentity)
    }
}
