import SwiftData
import SharedModels
import Foundation

@Model
final class UserProgress {
    var id: String = "main"
    var dailyGoal: Int = 50
    var todayCount: Int = 0
    var todayDate: String = ""
    var currentStreak: Int = 0
    var longestStreak: Int = 0
    var totalReviewed: Int = 0
    var selectedLevel: String = "basic"
    var showVosotros: Bool = true
    var autoFillStem: Bool = false
    var showReflexiveVerbsOnly: Bool = false

    // Legacy CloudKit array-backed fields retained for migration compatibility.
    var enabledTenses: [String] = []
    var unlockedBadges: [String] = []
    var enabledTensesBlob: String = ""
    var unlockedBadgesBlob: String = ""

    // Multi-select level + irregularity filters (Issue #26).
    var selectedLevelsBlob: String = ""
    var enabledIrregularCategoriesBlob: String = ""

    // Multi-select CEFR levels for the noun/adjective vocab catalog —
    // separate from the verb levels above so the two are independent.
    var selectedLexemeLevelsBlob: String = ""

    init() {}

    var selectedVerbLevel: VerbLevel {
        get { VerbLevel(rawValue: selectedLevel) ?? .basic }
        set { selectedLevel = newValue.rawValue }
    }

    var enabledTenseIDs: [String] {
        get { decodeStringArray(from: enabledTensesBlob, fallback: enabledTenses) }
        set {
            enabledTensesBlob = Self.encodeStringArray(newValue)
            enabledTenses = []
        }
    }

    var unlockedBadgeIDs: [String] {
        get { decodeStringArray(from: unlockedBadgesBlob, fallback: unlockedBadges) }
        set {
            unlockedBadgesBlob = Self.encodeStringArray(newValue)
            unlockedBadges = []
        }
    }

    /// Levels currently enabled for practice. Multi-select per Issue #26.
    /// Setting this also syncs `selectedLevel` to the highest-ranked selection so
    /// legacy single-level consumers (widget, AI scenarios, word-of-day) stay consistent.
    var selectedVerbLevels: Set<VerbLevel> {
        get {
            let raw = decodeStringArray(from: selectedLevelsBlob, fallback: [])
            let decoded = Set(raw.compactMap(VerbLevel.init(rawValue:)))
            if !decoded.isEmpty { return decoded }
            // Pre-migration users: treat the single selectedLevel as the set.
            if let legacy = VerbLevel(rawValue: selectedLevel) {
                return [legacy]
            }
            return []
        }
        set {
            let sorted = newValue.map(\.rawValue)
            selectedLevelsBlob = Self.encodeStringArray(sorted)
            selectedLevel = VerbLevel.highest(in: newValue)?.rawValue ?? VerbLevel.basic.rawValue
        }
    }

    /// The single representative level for callers that need one value
    /// (word-of-day widget, AI chat/story scenarios). Highest selected level.
    var primaryLevel: VerbLevel {
        VerbLevel.highest(in: selectedVerbLevels) ?? selectedVerbLevel
    }

    var enabledIrregularCategories: Set<IrregularSpan.SpanCategory> {
        get {
            let raw = decodeStringArray(from: enabledIrregularCategoriesBlob, fallback: [])
            return Set(raw.compactMap(IrregularSpan.SpanCategory.init(rawValue:)))
        }
        set {
            let sorted = newValue.map(\.rawValue)
            enabledIrregularCategoriesBlob = Self.encodeStringArray(sorted)
        }
    }

    func setTenseEnabled(_ tenseId: String, enabled: Bool) {
        var values = Set(enabledTenseIDs)
        if enabled {
            values.insert(tenseId)
        } else {
            values.remove(tenseId)
        }
        enabledTenseIDs = values.sorted()
    }

    func setLevelEnabled(_ level: VerbLevel, enabled: Bool) {
        var values = selectedVerbLevels
        if enabled {
            values.insert(level)
        } else {
            values.remove(level)
        }
        selectedVerbLevels = values
    }

    /// CEFR-style levels currently enabled for noun + adjective flashcards.
    /// First-ever read (blob empty) defaults to A1+A2 — a beginner-friendly
    /// starting point. Once the user touches any toggle, the blob is no
    /// longer empty and exactly reflects their selection (including the
    /// "all off" state, which shows the empty-state message).
    var selectedLexemeLevels: Set<LexemeLevel> {
        get {
            if selectedLexemeLevelsBlob.isEmpty {
                return [.a1, .a2]
            }
            let raw = decodeStringArray(from: selectedLexemeLevelsBlob, fallback: [])
            return Set(raw.compactMap(LexemeLevel.init(rawValue:)))
        }
        set {
            let sorted = newValue.map(\.rawValue)
            selectedLexemeLevelsBlob = Self.encodeStringArray(sorted)
        }
    }

    func setLexemeLevelEnabled(_ level: LexemeLevel, enabled: Bool) {
        var values = selectedLexemeLevels
        if enabled {
            values.insert(level)
        } else {
            values.remove(level)
        }
        selectedLexemeLevels = values
    }

    func setIrregularCategoryEnabled(_ category: IrregularSpan.SpanCategory, enabled: Bool) {
        var values = enabledIrregularCategories
        if enabled {
            values.insert(category)
        } else {
            values.remove(category)
        }
        enabledIrregularCategories = values
    }

    func unlockBadge(_ badgeId: String) {
        var values = Set(unlockedBadgeIDs)
        values.insert(badgeId)
        unlockedBadgeIDs = values.sorted()
    }

    /// Resets `currentStreak` to zero if more than one day has passed since
    /// the last recorded activity. Without this check the dashboard keeps
    /// displaying a stale streak number for days after the user actually
    /// stops practicing — the underlying counter only updates on the *next*
    /// practice action. Call from app launch and the dashboard's `.task`.
    @MainActor
    func validateStreakIfStale(today: Date = Date(), context: ModelContext) {
        guard !todayDate.isEmpty else { return }
        let todayString = DailyLog.dateString(from: today)
        if todayDate == todayString { return }
        guard let prevDate = DailyLog.date(from: todayDate) else { return }
        let diff = Calendar.current.dateComponents([.day], from: prevDate, to: today)
        if (diff.day ?? Int.max) > 1 && currentStreak != 0 {
            currentStreak = 0
            try? context.save()
        }
    }

    func migrateLegacyStorageIfNeeded() {
        if enabledTensesBlob.isEmpty && !enabledTenses.isEmpty {
            enabledTenseIDs = enabledTenses
        }
        if unlockedBadgesBlob.isEmpty && !unlockedBadges.isEmpty {
            unlockedBadgeIDs = unlockedBadges
        }
        if selectedLevelsBlob.isEmpty, let legacy = VerbLevel(rawValue: selectedLevel) {
            selectedVerbLevels = [legacy]
        }
    }

    private func decodeStringArray(from blob: String, fallback: [String]) -> [String] {
        guard !blob.isEmpty,
              let data = blob.data(using: .utf8),
              let values = try? JSONDecoder().decode([String].self, from: data) else {
            return fallback
        }
        return values
    }

    private static func encodeStringArray(_ values: [String]) -> String {
        let normalized = Array(Set(values)).sorted()
        guard let data = try? JSONEncoder().encode(normalized),
              let string = String(data: data, encoding: .utf8) else {
            return "[]"
        }
        return string
    }

}
