import Foundation

// MARK: - CommentPath

/// **What a changed path under a card's `comments/` is** — the pure classification the announcer and
/// the commit-message composer read (01-storage-format.md § Enhanced schema: "foreign comment changes
/// are described by **path shape** — the 'Update agent guide (vN)' mechanism: a changed path under
/// `…/comments/<uuid>/` composes 'Comment on ⟨card title⟩' / 'Edit comment on…' / 'Delete comment
/// on…', and the announcer speaks arrivals the same way").
///
/// ### Why a path, and not the snapshot
///
/// Comments are window-scoped and the board snapshot never carries their content, so the two
/// consumers that describe *change* have no diff to read: the composer is "a pure snapshot diff" and
/// the snapshot has nothing to say here, exactly as it has nothing to say about `CLAUDE.md`. Path
/// shape is what is left, and it is enough — the verb family is a function of *where* a file sits,
/// not of what it contains.
///
/// ### It classifies, it does not name
///
/// The shape says comment / draft / trashed and which card. Which *verb* that composes needs one more
/// fact the path cannot carry — whether the folder is an arrival or a change to one already there —
/// and that belongs to the caller with the before-and-after in hand. This type stays a pure function
/// of a string so both consumers can share it without sharing anything else.
///
/// Homed beside `EchoLedger` and `BoardDiff`, which are the two things that turn observed paths into
/// described events.
public struct CommentPath: Sendable, Equatable {

    /// The **card**'s path relative to the board root — `<lane>/<card>`, or `.trash/<card>` for a
    /// trashed card, which carries its thread like any other content.
    public let cardPath: String

    /// Which of the thread's three homes the path is in.
    public let kind: Kind

    public enum Kind: Sendable, Equatable {
        /// A posted comment — `<card>/comments/<uuid>/…`. The thread's own content.
        case comment(ItemID)
        /// The card's single draft — `<card>/comments/.draft/…`. Composes the quiet
        /// "Draft comment on '⟨card⟩'".
        case draft
        /// A deleted comment waiting for the close purge — `<card>/comments/.trash/<uuid>/…`.
        case trashed(ItemID)
    }

    /// The comment's identity, or `nil` for the draft — which has none, and is the one member of the
    /// thread that is a name rather than an id.
    public var id: ItemID? {
        switch kind {
        case let .comment(id), let .trashed(id): id
        case .draft: nil
        }
    }

    /// Classifies one **root-relative, `/`-separated** path, or `nil` when it is not inside a thread.
    ///
    /// The rule is one index: a thread lives at `<lane>/<card>/comments/` and a trashed card's at
    /// `.trash/<card>/comments/`, so `comments` is always the third component and the card is always
    /// the second — one check covers both containers without either being spelled twice.
    ///
    /// Everything else answers `nil`, including the paths that are *nearly* one: `comments/` itself
    /// (a container, never an event), a stray folder inside it, `comments/.trash` with no entry under
    /// it. A `nil` is not a defect — it is this function saying the path is somebody else's to
    /// describe.
    public static func classify(_ relativePath: String) -> CommentPath? {
        let components = relativePath.split(separator: "/", omittingEmptySubsequences: true).map(String.init)
        guard components.count >= 4,
              components[2].lowercased() == IntegrityRules.commentsFolderName,
              IntegrityRules.isIdentityShaped(components[1])
        else {
            return nil
        }
        let cardPath = components[0] + "/" + components[1]

        let entry = components[3]
        if entry.lowercased() == IntegrityRules.commentDraftFolderName {
            return CommentPath(cardPath: cardPath, kind: .draft)
        }
        if entry.lowercased() == IntegrityRules.commentTrashFolderName {
            guard components.count >= 5, IntegrityRules.isIdentityShaped(components[4]) else { return nil }
            return CommentPath(cardPath: cardPath, kind: .trashed(ItemID(rawValue: components[4])))
        }
        guard IntegrityRules.isIdentityShaped(entry) else { return nil }
        return CommentPath(cardPath: cardPath, kind: .comment(ItemID(rawValue: entry)))
    }
}
