import Foundation

// MARK: - To-do triage (AI-ranked inbox)

/// How pressing an idea is, after triage. Ordered by `severity` (critical = hottest).
/// Drives the row's colored badge and, via `TriageItem.rank`, the list order.
public enum TriageLevel: String, Sendable, Codable, CaseIterable {
    case critical
    case high
    case medium
    case low

    /// Upbeat label framed as opportunity, not alarm. The default badge text; the user
    /// can switch to `plainLabel` in Settings. Case names / model tokens stay technical.
    public var encouragingLabel: String {
        switch self {
        case .critical: "Max Impact"
        case .high: "High Impact"
        case .medium: "Worthwhile"
        case .low: "Someday"
        }
    }

    /// Classic severity words, for users who prefer the plain ranking over the
    /// encouraging framing (toggled in Settings).
    public var plainLabel: String {
        switch self {
        case .critical: "Critical"
        case .high: "High"
        case .medium: "Medium"
        case .low: "Low"
        }
    }

    /// The badge text for the user's chosen labeling style.
    public func label(encouraging: Bool) -> String {
        encouraging ? encouragingLabel : plainLabel
    }

    /// Higher = more pressing. For thresholding and ordering.
    public var severity: Int {
        switch self {
        case .critical: 3
        case .high: 2
        case .medium: 1
        case .low: 0
        }
    }

    /// Lenient parse of a model-emitted token ("CRITICAL", " high ", "Med") into a
    /// level, or nil if it isn't one — used when reading the model's triage lines.
    public init?(token: String) {
        switch token.lowercased().trimmingCharacters(in: .whitespaces) {
        case "critical", "crit": self = .critical
        case "high": self = .high
        case "medium", "med": self = .medium
        case "low": self = .low
        default: return nil
        }
    }
}

/// One idea handed to the triage provider: just its identity and text (the provider
/// needn't see the rest of the `Todo`).
public struct TriageInput: Sendable, Equatable {
    public let id: TodoID
    public let text: String
    public init(id: TodoID, text: String) {
        self.id = id
        self.text = text
    }
}

/// A triage verdict for one idea: where it lands in the ranked list (`rank`, 0 = top),
/// how hot it is (`level`, drives the color), and a short why (`reason`, shown on hover).
public struct TriageItem: Sendable, Equatable, Identifiable {
    public let id: TodoID
    public let level: TriageLevel
    public let rank: Int
    public let reason: String?
    public init(id: TodoID, level: TriageLevel, rank: Int, reason: String? = nil) {
        self.id = id
        self.level = level
        self.rank = rank
        self.reason = reason
    }
}

/// Model-free triage: scores each idea for IMPACT — how much lasting value finishing
/// it delivers — with a strong nudge for genuinely urgent/risky work, then returns the
/// ideas ranked most-pressing first, each bucketed into a level. Deterministic: the
/// fallback when Intelligence is off/unavailable, and the floor the model refines.
public enum HeuristicTriage {
    /// Signals that an idea is high-impact / strategic.
    private static let impactWords: [String] = [
        "refactor", "architecture", "redesign", "performance", "scale", "scalability",
        "revenue", "billing", "payment", "onboarding", "retention", "conversion",
        "core", "foundation", "infrastructure", "launch", "ship", "release",
        "users", "customer", "growth", "adoption", "accessibility", "reliability",
    ]
    /// Signals of genuine urgency — these nudge an idea up even when impact words are
    /// sparse (a blocker hurts more than a nice-to-have refactor).
    private static let urgentWords: [String] = [
        "urgent", "asap", "immediately", "deadline", "blocker", "blocking",
        "broken", "regression", "bug", "failing", "fails", "fix",
    ]
    /// The most severe signals — outages, data loss, security. Any of these pins the
    /// idea to the top regardless of impact, so extreme urgency always wins.
    private static let criticalWords: [String] = [
        "crash", "data loss", "corruption", "security", "vulnerability", "exploit",
        "outage", "production", "leak", "deadlock", "hang", "down",
    ]

    public static func triage(_ items: [TriageInput]) -> [TriageItem] {
        guard !items.isEmpty else { return [] }
        // Keep the input (FIFO) order as a stable tiebreak via `offset`.
        let scored = items.enumerated().map { offset, item -> (item: TriageInput, score: Double, critical: Bool, offset: Int) in
            let s = score(item.text)
            return (item, s.score, s.critical, offset)
        }
        let ordered = scored.sorted { a, b in
            // Extreme urgency always outranks mere impact.
            if a.critical != b.critical { return a.critical && !b.critical }
            if a.score != b.score { return a.score > b.score }
            return a.offset < b.offset
        }
        // Levels are RELATIVE to this batch: genuine urgency signals are pinned to
        // Critical, and everything else is spread across High→Medium→Low so there's
        // always a top and a bottom — even when every idea scores the same.
        let nonCritLevels = spreadLevels(ordered.filter { !$0.critical }.map(\.score))
        var nextLevel = nonCritLevels.makeIterator()
        return ordered.enumerated().map { rank, entry in
            TriageItem(id: entry.item.id,
                       level: entry.critical ? .critical : (nextLevel.next() ?? .low),
                       rank: rank)
        }
    }

    /// Impact-weighted score with an urgency nudge; `critical` flags a top-pinning
    /// signal (outage / data loss / security).
    static func score(_ text: String) -> (score: Double, critical: Bool) {
        let lower = text.lowercased()
        func hits(_ words: [String]) -> Int { words.reduce(0) { $0 + (lower.contains($1) ? 1 : 0) } }
        let impact = hits(impactWords)
        let urgent = hits(urgentWords)
        let crit = hits(criticalWords)
        // Impact leads; urgency is a secondary nudge; a critical signal dominates.
        let score = Double(impact) * 1.0 + Double(urgent) * 0.6 + Double(crit) * 2.0
        return (score, crit > 0)
    }

    /// Maps an already-ordered (most→least impactful) run of scores onto a relative
    /// High→Medium→Low gradient. When the scores genuinely differ they're normalized
    /// to the batch's own range; when they don't (e.g. no keywords matched), the band
    /// is taken from rank position so the spectrum never collapses to a single level.
    static func spreadLevels(_ scoresDesc: [Double]) -> [TriageLevel] {
        guard let maxS = scoresDesc.first, let minS = scoresDesc.last else { return [] }
        guard maxS > minS else { return gradient(count: scoresDesc.count) }
        return scoresDesc.map { band(($0 - minS) / (maxS - minS)) }
    }

    /// A purely positional High→Low gradient for `count` items already in priority
    /// order — the floor that guarantees a relative spectrum when scores don't differ
    /// (and the provider's fallback when a model bunches everything at one level).
    public static func gradient(count: Int) -> [TriageLevel] {
        guard count > 0 else { return [] }
        guard count > 1 else { return [.medium] }  // one idea has no spectrum — neutral
        return (0..<count).map { i in band(Double(count - 1 - i) / Double(count - 1)) }
    }

    /// Buckets a 0…1 position into the three non-critical bands.
    private static func band(_ t: Double) -> TriageLevel {
        if t >= 2.0 / 3.0 { return .high }
        if t >= 1.0 / 3.0 { return .medium }
        return .low
    }

    /// One idea's verdict as returned by the model: the idea's number (1-based, as
    /// labeled in the prompt), a level token ("HIGH"…), and a short reason.
    public struct ModelVerdict: Sendable, Equatable {
        public let number: Int
        public let level: String
        public let reason: String
        public init(number: Int, level: String, reason: String) {
            self.number = number
            self.level = level
            self.reason = reason
        }
    }

    /// Turns a model's ranked verdicts (most-impactful first) into ordered
    /// `TriageItem`s: validates and de-dupes the numbers (rank = line order), parses
    /// the level tokens, appends any ideas the model dropped in heuristic order, and —
    /// as a safety net — re-spreads the non-critical levels by rank if the model
    /// bunched them all at one band. Falls back to a full heuristic pass when the model
    /// returned nothing usable.
    public static func fromModelRanking(_ ranked: [ModelVerdict], items: [TriageInput]) -> [TriageItem] {
        var seen = Set<Int>()
        var result: [TriageItem] = []
        for verdict in ranked {
            let n = verdict.number
            guard n >= 1, n <= items.count, !seen.contains(n),
                  let level = TriageLevel(token: verdict.level) else { continue }
            seen.insert(n)
            let reason = verdict.reason.trimmingCharacters(in: .whitespacesAndNewlines)
            result.append(TriageItem(id: items[n - 1].id, level: level, rank: result.count,
                                     reason: reason.isEmpty ? nil : reason))
        }
        guard !result.isEmpty else { return triage(items) }
        // Append ideas the model skipped, in heuristic order, after the ranked ones.
        let missing = items.enumerated().filter { !seen.contains($0.offset + 1) }.map(\.element)
        for extra in triage(missing) {
            result.append(TriageItem(id: extra.id, level: extra.level, rank: result.count, reason: extra.reason))
        }
        // If the model bunched every non-critical idea at one level, re-spread by rank
        // so the badges still show a relative spectrum.
        let nonCrit = result.indices.filter { result[$0].level != .critical }
        if nonCrit.count >= 2, Set(nonCrit.map { result[$0].level }).count <= 1 {
            let levels = gradient(count: nonCrit.count)
            for (k, idx) in nonCrit.enumerated() {
                let r = result[idx]
                result[idx] = TriageItem(id: r.id, level: levels[k], rank: r.rank, reason: r.reason)
            }
        }
        return result
    }
}
