import Foundation
import Observation

/// The labels this user applied lately, most-recent-first — the **tie-break** half of "most
/// frequently and recently used" (`LabelRanking`; `FrontmatterKeys.labels`, activated 2026-08-09).
///
/// `StyleRecents`' shape exactly, for its reasons: the list *rule* is a static pure function, this
/// object is its persistence and its observability, and `defaults` is injectable so a test drives a
/// suite of its own rather than the user's.
///
/// ### App-wide, not per board — and it does not matter much
///
/// A board's label *universe* is board data, derived from its own cards (`LabelIndex`). This is not
/// that. It is a memory of what this **user** reached for, and it exists only to order labels that are
/// already tied on frequency within one board's universe — so an entry naming a label another board
/// uses simply never matches anything here and is inert. App-wide is `StyleRecents`' own posture ("no
/// board owns the list"), it needs no per-board storage key, and it survives a board being closed and
/// reopened, which a window-lived list would not.
///
/// ### What enters it
///
/// **Applications, not removals.** Taking `bug` off a card is not evidence the user is reaching for
/// `bug`; recording it would push a label the user is actively getting rid of to the front of the very
/// menu they are trying to leave. So `LabelCommand` records on the add half of a toggle and on a
/// create, and never on a remove — the same "the None well is not a colour" carve-out `StyleRecents`
/// makes one field over.
@MainActor
@Observable
public final class LabelRecents {

    /// How many the list keeps. **Twice the menu's twelve**, deliberately: unlike `StyleRecents`' six,
    /// this list is never *displayed* — it only breaks ties inside a twelve-row menu — so its useful
    /// depth is "enough to order a tie group that could fill the menu", and a cap equal to the menu
    /// size would leave the thirteenth-most-recent label indistinguishable from one never used at all.
    public static let cap = 24

    /// The labels, most-recent-first. Display spellings, exactly as written to frontmatter.
    public private(set) var labels: [String]

    @ObservationIgnored
    private let defaults: UserDefaults

    /// - Parameter defaults: the domain to persist in. Injected for `StyleRecents`' reason — a test
    ///   must be able to hold its own without touching the user's.
    public init(defaults: UserDefaults = .standard) {
        self.defaults = defaults
        // Anything but an array of strings reads as an empty list rather than as an error —
        // `StyleRecents`' rule and its reason: a hand-edited or truncated preference must never be why
        // a context menu cannot open.
        labels = (defaults.array(forKey: AppPreferences.labelRecentsKey) as? [String]) ?? []
    }

    /// Records `name` as the most recently applied label and persists the new list.
    public func record(_ name: String) {
        let updated = Self.updated(labels, with: name)
        guard updated != labels else { return }
        labels = updated
        defaults.set(labels, forKey: AppPreferences.labelRecentsKey)
    }

    /// The list rule, as a pure function: `name` to the front, its **case-insensitive** earlier
    /// occurrence removed, the tail truncated to `cap`.
    ///
    /// Case-insensitive because that is what a label's identity is (`CardLabels.canonical`), and a list
    /// holding both `Bug` and `bug` would spend two of its slots ordering one label against itself. The
    /// spelling kept is the one just applied, which is the board's own by construction — every write
    /// path resolves a typed name against `LabelIndex.canonicalSpelling(of:)` before it lands.
    ///
    /// A name that trims to nothing returns the list unchanged: it is not a label anybody applied.
    public static func updated(_ list: [String], with name: String, cap: Int = cap) -> [String] {
        guard let name = CardLabels.normalized(name) else { return list }
        let key = CardLabels.canonical(name)
        var updated = list.filter { CardLabels.canonical($0) != key }
        updated.insert(name, at: 0)
        return Array(updated.prefix(cap))
    }
}
