import Foundation
import SwiftData

enum DictionaryService {

    // MARK: - Vocabulary

    /// Adds one or more comma-separated words to vocabulary.
    /// Returns an error message string if something went wrong, nil on success.
    @discardableResult
    static func addVocabularyWords(
        _ input: String,
        existing: [VocabularyWord],
        context: ModelContext
    ) -> String? {
        let parts = input
            .split(separator: ",")
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty }

        guard !parts.isEmpty else { return nil }

        if parts.count == 1, let word = parts.first {
            if existing.contains(where: { $0.word.lowercased() == word.lowercased() }) {
                return "'\(word)' is already in the vocabulary"
            }
            return insertVocabularyWord(word, context: context)
        }

        var addedWords = Set(existing.map { $0.word.lowercased() })
        var errors = [String]()
        for word in parts {
            let lower = word.lowercased()
            if !addedWords.contains(lower) {
                if let error = insertVocabularyWord(word, context: context) {
                    errors.append(error)
                }
                addedWords.insert(lower)
            }
        }
        return errors.isEmpty ? nil : errors.joined(separator: "; ")
    }

    @discardableResult
    private static func insertVocabularyWord(_ word: String, context: ModelContext) -> String? {
        let entry = VocabularyWord(word: word)
        context.insert(entry)
        do {
            try context.save()
            return nil
        } catch {
            context.delete(entry)
            return "Failed to add '\(word)': \(error.localizedDescription)"
        }
    }

    // MARK: - Default seeding

    /// Vaste merknaam-vangregel die altijd moet bestaan. Eén regex vangt alle
    /// Voxtral-verbasteringen van "noaber.ai" (Nobel.ai, Norbet.ai, Nalber.ai …).
    /// `[ae]i` + verplichte scheiding `[.\s]`: vangt ook de "Norbert.ei"-uitgang
    /// die Voxtral produceert, zonder NL-woorden als "notarei/boerderei" te raken.
    static let noaberRegexTrigger = DictionaryPack.noaberRegexTrigger

    /// Te Kloeze fonetische vangregel: vangt Voxtral-varianten als "te clouger",
    /// "de klusje", "te kloes", "de kloeze". Vereist 2-5 chars na [u/o] zodat
    /// korte NL-woorden ("de klus", "te kop") niet falsch matchen. Accepteert
    /// zowel "te" als "de" prefix want Voxtral verwart die. Eén regex vervangt
    /// een onbeperkt aantal komma-list-varianten.
    static let teKloezeRegexTrigger = DictionaryPack.teKloezeRegexTrigger

    /// Gecureerde standaard-vervangpack: alleen eigennamen/merken die Voxtral
    /// structureel verhaspelt of verkeerd schrijft. Bewust GEEN gewone woorden
    /// (die hoeven niet vervangen). Linkerkant = komma-gescheiden verbasteringen,
    /// rechterkant = canonieke schrijfwijze.
    ///
    /// Pack en woordenlijst komen uit `dictionary/proat-dictionary.json`, de
    /// gedeelde bron met iOS en Windows. Uitbreiden = die JSON aanpassen en
    /// `python3 dictionary/generate.py` draaien; de packversie zit in de JSON.
    static let seedPackVersion = DictionaryPack.packVersion

    static var defaultReplacementPack: [(original: String, replacement: String)] {
        DictionaryPack.replacements
    }

    /// De woordenlijst voor biasing en de Gemini-prompt, zie `DictionaryPack`.
    static var defaultVocabularyPack: [String] { DictionaryPack.vocabulary }

    /// Seedt het gecureerde vervangpack als geldige SwiftData-objecten
    /// (idempotent, per regel op originalText). Eén keer per packversie via
    /// een UserDefaults-vlag, zodat door de gebruiker verwijderde regels niet
    /// elke start terugkeren. Handmatig in de store prikken werkt niet —
    /// SwiftData met history/CloudKit-metadata negeert zulke rijen.
    /// `defaults` is injecteerbaar zodat tests niet in de echte app-defaults
    /// schrijven; de app gebruikt gewoon `.standard`.
    static func seedDefaultReplacements(context: ModelContext, defaults: UserDefaults = .standard) {
        let flagKey = "DidSeedDictionaryPackV\(seedPackVersion)"
        guard !defaults.bool(forKey: flagKey) else { return }

        let descriptor = FetchDescriptor<WordReplacement>()
        let existing = (try? context.fetch(descriptor)) ?? []
        let existingOriginals = Set(existing.map { $0.originalText })

        // Oude/verouderde Noaber-regex-regels opruimen (bv. de pre-v2 `…ai`-
        // variant) zodat het verbeterde patroon niet naast het oude blijft
        // staan. Via SwiftData context.delete → betrouwbaar, geen dubbele regel.
        var removed = 0
        for rule in existing
        where rule.replacementText == "Noaber.ai"
            && rule.originalText.hasPrefix("/")
            && rule.originalText != noaberRegexTrigger {
            context.delete(rule)
            removed += 1
        }

        // Regels opruimen die volledig opgaan in een pack-regel naar hetzelfde
        // doel. Sinds pack v7 is er per doel één regel: twee regels naar
        // hetzelfde doel vechten om de sorteer-orde (langste origineel eerst),
        // waardoor een korte oude regel het resultaat van een langere weer kon
        // terugdraaien, bijvoorbeeld "Te Kloeze" → "te Kloeze".
        for rule in existing where !rule.originalText.hasPrefix("/") {
            let ruleVariants = variantSet(rule.originalText)
            guard !ruleVariants.isEmpty else { continue }
            let coveringPackRule = defaultReplacementPack.first { packRule in
                packRule.replacement.lowercased() == rule.replacementText.lowercased()
                    && packRule.original != rule.originalText
                    && ruleVariants.isSubset(of: variantSet(packRule.original))
            }
            if coveringPackRule != nil {
                context.delete(rule)
                removed += 1
            }
        }

        var added = 0
        for rule in defaultReplacementPack where !existingOriginals.contains(rule.original) {
            context.insert(WordReplacement(originalText: rule.original, replacementText: rule.replacement))
            added += 1
        }
        do {
            if added > 0 || removed > 0 { try context.save() }
            defaults.set(true, forKey: flagKey)
            ProatDebugLog.write("SEED | pack v\(seedPackVersion): +\(added) / -\(removed) regels")
        } catch {
            ProatDebugLog.write("SEED FOUT | \(error.localizedDescription)")
        }
    }

    /// De komma-varianten van een trigger, hoofdletter-ongevoelig. Regex-triggers
    /// horen hier niet in (die splitsen niet op komma, zie WordReplacementService).
    private static func variantSet(_ trigger: String) -> Set<String> {
        Set(trigger
            .split(separator: ",")
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() }
            .filter { !$0.isEmpty })
    }

    /// Seedt de gecureerde woordenlijst (eigennamen, merken, vaktermen) in de
    /// SwiftData-store. Die woorden voeden de Voxtral-biasingprompt en het
    /// <CUSTOM_VOCABULARY>-blok van de verfijning; ze vervangen niets.
    ///
    /// Eén keer per packversie via een UserDefaults-vlag, zodat woorden die de
    /// gebruiker weggooit niet elke start terugkomen. Dedupe is
    /// hoofdletter-ongevoelig, dus eigen toevoegingen blijven staan.
    static func seedDefaultVocabulary(context: ModelContext, defaults: UserDefaults = .standard) {
        let flagKey = "DidSeedVocabularyPackV\(seedPackVersion)"
        guard !defaults.bool(forKey: flagKey) else { return }

        let descriptor = FetchDescriptor<VocabularyWord>()
        let existing = (try? context.fetch(descriptor)) ?? []
        var known = Set(existing.map { $0.word.lowercased() })

        var added = 0
        for word in defaultVocabularyPack {
            let trimmed = word.trimmingCharacters(in: .whitespacesAndNewlines)
            let key = trimmed.lowercased()
            guard !trimmed.isEmpty, !known.contains(key) else { continue }
            context.insert(VocabularyWord(word: trimmed))
            known.insert(key)
            added += 1
        }

        do {
            if added > 0 { try context.save() }
            defaults.set(true, forKey: flagKey)
            ProatDebugLog.write("SEED VOCAB | pack v\(seedPackVersion): +\(added) woorden (totaal \(known.count))")
        } catch {
            ProatDebugLog.write("SEED VOCAB FOUT | \(error.localizedDescription)")
        }
    }

    // MARK: - Word Replacement

    /// Adds a word replacement entry (original may be comma-separated).
    /// Returns an error message string if something went wrong, nil on success.
    @discardableResult
    static func addWordReplacement(
        original: String,
        replacement: String,
        existing: [WordReplacement],
        context: ModelContext
    ) -> String? {
        let tokens = original
            .split(separator: ",")
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty }

        guard !tokens.isEmpty, !replacement.isEmpty else { return nil }

        for existingEntry in existing {
            let existingTokens = existingEntry.originalText
                .split(separator: ",")
                .map { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() }
                .filter { !$0.isEmpty }

            for token in tokens {
                if existingTokens.contains(token.lowercased()) {
                    return "'\(token)' already exists in word replacements"
                }
            }
        }

        let entry = WordReplacement(originalText: original, replacementText: replacement)
        context.insert(entry)
        do {
            try context.save()
            return nil
        } catch {
            context.delete(entry)
            return "Failed to add replacement: \(error.localizedDescription)"
        }
    }
}
