import Foundation

/// Pure practice-pool filtering (Issue #26).
///
/// Takes plain value snapshots of the verb + irregular-span data and computes
/// the set of verb IDs eligible for practice under the user's selected filters.
/// Deliberately decoupled from SwiftData so the same logic is directly testable
/// without a ModelContainer.
public enum PracticeFilter {

    /// Minimal verb snapshot for filtering.
    public struct VerbSlot: Sendable, Hashable {
        public let id: Int
        public let level: String
        public init(id: Int, level: String) {
            self.id = id
            self.level = level
        }
    }

    /// Minimal irregular-span snapshot for filtering.
    public struct IrregularSlot: Sendable, Hashable {
        public let verbId: Int
        public let category: IrregularSpan.SpanCategory
        public init(verbId: Int, category: IrregularSpan.SpanCategory) {
            self.verbId = verbId
            self.category = category
        }
    }

    /// Union of `VerbLevelGroup.dataLevels(for:)` across every user-facing level.
    /// An empty input produces an empty result; callers decide the empty semantics.
    public static func dataLevels(forSelectedLevels levels: Set<String>) -> Set<String> {
        levels.reduce(into: Set<String>()) { acc, level in
            acc.formUnion(VerbLevelGroup.dataLevels(for: level))
        }
    }

    /// Verb IDs whose `level` falls inside any of the selected level groups.
    public static func verbIDs(
        matchingLevels selectedLevels: Set<String>,
        in verbs: [VerbSlot]
    ) -> Set<Int> {
        guard !selectedLevels.isEmpty else { return [] }
        let expanded = dataLevels(forSelectedLevels: selectedLevels)
        return Set(verbs.filter { expanded.contains($0.level) }.map(\.id))
    }

    /// Verb IDs that have at least one irregular span in the requested categories.
    /// Returns an empty set when `categories` is empty — caller decides whether
    /// that means "no constraint" or "no matches".
    public static func verbIDs(
        matchingIrregularCategories categories: Set<IrregularSpan.SpanCategory>,
        in spans: [IrregularSlot]
    ) -> Set<Int> {
        guard !categories.isEmpty else { return [] }
        var ids = Set<Int>()
        for slot in spans where categories.contains(slot.category) {
            ids.insert(slot.verbId)
        }
        return ids
    }

    /// Practice pool: verbs at the selected levels, intersected with irregular
    /// categories when that filter is active.
    ///
    /// Semantics (Issue #26):
    /// - `selectedLevels` empty → empty pool (literal).
    /// - `irregularCategories` empty → no irregular constraint (all verbs at level).
    public static func allowedVerbIDs(
        verbs: [VerbSlot],
        spans: [IrregularSlot],
        selectedLevels: Set<String>,
        irregularCategories: Set<IrregularSpan.SpanCategory>
    ) -> Set<Int> {
        let levelIDs = verbIDs(matchingLevels: selectedLevels, in: verbs)
        guard !irregularCategories.isEmpty else { return levelIDs }
        let irregularIDs = verbIDs(matchingIrregularCategories: irregularCategories, in: spans)
        return levelIDs.intersection(irregularIDs)
    }
}
