import Foundation

// MARK: - Payload Sanitizer

/// Bereinigt freie Texte vor dem Versand an den KI-Dienst.
/// Erkannte PII-Muster werden durch neutrale Platzhalter ersetzt,
/// damit keine eindeutig identifizierende Information das Gerät verlässt.
/// Vornamen sind bewusst erlaubt – Nachnamen und andere eindeutige
/// Identifikatoren sollen vom Nutzer gar nicht erst eingetragen werden.
enum AIPayloadSanitizer {

    /// Wendet alle Anonymisierungsregeln der Reihe nach auf einen Text an.
    static func sanitize(_ text: String) -> String {
        var s = text
        s = maskEmails(s)
        s = maskURLs(s)
        s = maskIBANs(s)
        s = maskPhoneNumbers(s)
        s = maskLongDigitSequences(s)
        return s
    }

    // E-Mail-Adressen: name@domain.tld
    private static func maskEmails(_ text: String) -> String {
        apply("[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}", replacement: "[E-Mail]", to: text)
    }

    // URLs: https://... oder www....
    private static func maskURLs(_ text: String) -> String {
        apply("https?://\\S+|www\\.\\S+", replacement: "[Link]", to: text)
    }

    // IBANs: DE89 3704 0044 ... (2 Buchstaben + 2 Ziffern + mind. 11 alphanumerische Zeichen)
    private static func maskIBANs(_ text: String) -> String {
        apply("\\b[A-Z]{2}\\d{2}[A-Z0-9]{4}[\\dA-Z\\s]{4,28}\\b", replacement: "[IBAN]", to: text)
    }

    // Telefonnummern:
    //  - International: +49 30 12345678, +1-555-123-4567
    //  - Deutsche Mobil: 0151 12345678 (10–11 Ziffern am Stück)
    //  - Deutsche Festnetz mit Trenner: 030 12345678, 089/12345, 030-12345
    private static func maskPhoneNumbers(_ text: String) -> String {
        var s = text
        // Internationale Rufnummern mit +
        s = apply("\\+\\d{1,3}[\\s\\-]?[\\d\\s\\-\\.\\(\\)\\/]{7,20}", replacement: "[Telefon]", to: s)
        // Deutsche Mobilnummern (z.B. 0151, 0160, 0170): 10–11 Ziffern ohne Trenner
        s = apply("\\b0\\d{9,10}\\b", replacement: "[Telefon]", to: s)
        // Deutsche Festnetznummern mit Trenner (Leerzeichen, Slash oder Bindestrich)
        s = apply("\\b0\\d{2,4}[\\s\\/\\-]\\d{3,8}\\b", replacement: "[Telefon]", to: s)
        return s
    }

    // Lange Zahlenfolgen (≥ 8 Ziffern am Stück): Ausweisnummern, Kontonummern, etc.
    private static func maskLongDigitSequences(_ text: String) -> String {
        apply("\\b\\d{8,}\\b", replacement: "[Nummer]", to: text)
    }

    private static func apply(_ pattern: String, replacement: String, to text: String) -> String {
        guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else {
            return text
        }
        return regex.stringByReplacingMatches(
            in: text,
            options: [],
            range: NSRange(text.startIndex..., in: text),
            withTemplate: replacement
        )
    }
}

// MARK: - Configuration

struct AIConfig: Decodable {
    let baseURL:            String
    let completionsPath:    String  // z.B. "/v1/chat/completions" oder "/api/chat/completions"
    let apiKey:             String
    let model:              String
    let timeoutSeconds:     Double
    let rateLimitPerHour:   Int
    let systemPrompt:        String
    let userPromptTemplate:  String
    let giftPromptTemplate:  String

    var completionsURL: String { baseURL + completionsPath }

    // Fallback-Werte falls die JSON-Datei nicht geladen werden kann
    static let fallback = AIConfig(
        baseURL:            "https://api.mistral.ai",
        completionsPath:    "/v1/chat/completions",
        apiKey:             "zyXgjAmQl8WwVV0qoe8rSNajqrwK6QXQ",
        model:              "mistral-small-latest",
        timeoutSeconds:     60,
        rateLimitPerHour:   3,
        systemPrompt:       "Du bist ein einfühlsamer Assistent für persönliche Beziehungspflege. Antworte ausschließlich auf Deutsch. Sei prägnant, warm und direkt. Strukturiere deine Antwort exakt wie verlangt.",
        userPromptTemplate: "Person: {{personName}}\n{{birthday}}{{interests}}\n{{moments}}\n{{logEntries}}\nAnalysiere diese Beziehung und antworte exakt in diesem Format:\n\nMUSTER: [2-3 Sätze]\nBEZIEHUNG: [2-3 Sätze]\nEMPFEHLUNG: [1 konkreter Schritt]",
        giftPromptTemplate: "Person: {{personName}}\n{{birthday}}{{interests}}\n{{moments}}\nSchlage 3 Geschenkideen vor. Antworte exakt in diesem Format:\n\nIDEE 1: [Idee – Begründung]\nIDEE 2: [Idee – Begründung]\nIDEE 3: [Idee – Begründung]"
    )

    static func load() -> AIConfig {
        // Laufzeit-Overrides aus UserDefaults haben höchste Priorität
        let ud = UserDefaults.standard
        let base = loadFromBundle()
        return AIConfig(
            baseURL:            ud.string(forKey: "aiBaseURL") ?? base.baseURL,
            completionsPath:    base.completionsPath,
            apiKey:             ud.string(forKey: "aiAPIKey")  ?? base.apiKey,
            model:              ud.string(forKey: "aiModel")   ?? base.model,
            timeoutSeconds:     base.timeoutSeconds,
            rateLimitPerHour:   base.rateLimitPerHour,
            systemPrompt:       base.systemPrompt,
            userPromptTemplate: base.userPromptTemplate,
            giftPromptTemplate: base.giftPromptTemplate
        )
    }

    private static func loadFromBundle() -> AIConfig {
        guard
            let url  = Bundle.main.url(forResource: "AIConfiguration", withExtension: "json"),
            let data = try? Data(contentsOf: url)
        else { return .fallback }

        let decoder = JSONDecoder()
        return (try? decoder.decode(AIConfig.self, from: data)) ?? .fallback
    }
}

// MARK: - Analysis Result

struct AIAnalysisResult {
    let patterns:       String
    let relationship:   String
    let recommendation: String
}

// MARK: - Cached Analysis

struct CachedAnalysis: Codable {
    let patterns:       String
    let relationship:   String
    let recommendation: String
    let analyzedAt:     Date

    var asResult: AIAnalysisResult {
        AIAnalysisResult(patterns: patterns, relationship: relationship, recommendation: recommendation)
    }

    init(result: AIAnalysisResult, date: Date = Date()) {
        self.patterns       = result.patterns
        self.relationship   = result.relationship
        self.recommendation = result.recommendation
        self.analyzedAt     = date
    }
}

// MARK: - Cached Gift Suggestion

struct CachedGiftSuggestion: Codable {
    let text: String
    let generatedAt: Date
}

// MARK: - Conversation Suggestion Result

struct ConversationSuggestionResult {
    let topics: String   // THEMEN:
    let rescue: String   // GESPRAECHSRETTER:
    let depth:  String   // TIEFE:
}

// MARK: - Cached Conversation Suggestion

struct CachedConversationSuggestion: Codable {
    let topics: String
    let rescue: String
    let depth:  String
    let generatedAt: Date

    var asResult: ConversationSuggestionResult {
        ConversationSuggestionResult(topics: topics, rescue: rescue, depth: depth)
    }

    init(result: ConversationSuggestionResult, date: Date = Date()) {
        self.topics      = result.topics
        self.rescue      = result.rescue
        self.depth       = result.depth
        self.generatedAt = date
    }
}

// MARK: - Service

class AIAnalysisService {
    static let shared = AIAnalysisService()
    private init() {}

    // MARK: - Gratis-Abfragen (3 Lifetime für alle Nutzer)

    static let freeQueryLimit = 3
    private let freeQueryCountKey = "ai_free_queries_used"

    var freeQueriesUsed: Int {
        get { UserDefaults.standard.integer(forKey: freeQueryCountKey) }
        set { UserDefaults.standard.set(newValue, forKey: freeQueryCountKey) }
    }

    var hasFreeQueriesLeft: Bool { freeQueriesUsed < AIAnalysisService.freeQueryLimit }
    var freeQueriesRemaining: Int { max(0, AIAnalysisService.freeQueryLimit - freeQueriesUsed) }

    /// Verbraucht eine Gratis-Abfrage. Nur aufrufen nach erfolgreicher API-Antwort.
    func consumeFreeQuery() {
        freeQueriesUsed += 1
    }

    // MARK: - Rate Limiting (max 3 pro Stunde, serverseitiger Schutz)

    private var maxRequestsPerHour: Int { AIConfig.load().rateLimitPerHour }
    private let rateLimitKey = "ai_rate_timestamps"

    var remainingRequests: Int {
        let recent = recentTimestamps()
        return max(0, maxRequestsPerHour - recent.count)
    }

    var isRateLimited: Bool { remainingRequests == 0 }

    var rateLimitResetsAt: Date? {
        let timestamps = recentTimestamps()
        guard timestamps.count >= maxRequestsPerHour,
              let oldest = timestamps.min()
        else { return nil }
        return Date(timeIntervalSince1970: oldest + 3600)
    }

    private func recentTimestamps() -> [Double] {
        let all = UserDefaults.standard.array(forKey: rateLimitKey) as? [Double] ?? []
        let cutoff = Date().timeIntervalSince1970 - 3600
        return all.filter { $0 > cutoff }
    }

    private func recordRequest() {
        var recent = recentTimestamps()
        recent.append(Date().timeIntervalSince1970)
        UserDefaults.standard.set(recent, forKey: rateLimitKey)
    }

    // MARK: - Cache

    private func cacheKey(for person: Person) -> String { "ai_cache_\(person.id.uuidString)" }

    func loadCached(for person: Person) -> CachedAnalysis? {
        guard let data = UserDefaults.standard.data(forKey: cacheKey(for: person)),
              let cached = try? JSONDecoder().decode(CachedAnalysis.self, from: data)
        else { return nil }
        return cached
    }

    private func saveCache(_ result: AIAnalysisResult, for person: Person) {
        let cached = CachedAnalysis(result: result)
        if let data = try? JSONEncoder().encode(cached) {
            UserDefaults.standard.set(data, forKey: cacheKey(for: person))
        }
    }

    // MARK: - Analyze

    func analyze(person: Person) async throws -> AIAnalysisResult {
        let config = AIConfig.load()

        guard let url = URL(string: config.completionsURL) else {
            throw URLError(.badURL)
        }

        let prompt = buildPrompt(for: person)

        let body: [String: Any] = [
            "model":   config.model,
            "stream":  false,
            "messages": [
                ["role": "system", "content": AppLanguage.current.systemPrompt],
                ["role": "user",   "content": prompt]
            ]
        ]

        var request = URLRequest(url: url, timeoutInterval: config.timeoutSeconds)
        request.httpMethod = "POST"
        request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json",         forHTTPHeaderField: "Content-Type")
        request.httpBody = try JSONSerialization.data(withJSONObject: body)

        let (data, response) = try await URLSession.shared.data(for: request)

        guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }

        guard
            let json    = try JSONSerialization.jsonObject(with: data) as? [String: Any],
            let choices = json["choices"] as? [[String: Any]],
            let first   = choices.first,
            let message = first["message"] as? [String: Any],
            let content = message["content"] as? String
        else {
            throw URLError(.cannotParseResponse)
        }

        let result = parseResult(content)
        recordRequest()
        saveCache(result, for: person)
        return result
    }

    // MARK: - Prompt Builder

    private enum PromptType {
        case analysis, gift, conversation
    }

    /// Baut den vollständigen User-Prompt sprachabhängig auf.
    private func buildPrompt(for person: Person, promptType: PromptType = .analysis) -> String {
        let lang = AppLanguage.current

        let formatter = DateFormatter()
        formatter.dateFormat = "dd.MM.yyyy"
        formatter.locale = Locale.current

        let momentLines = person.sortedMoments.prefix(30).map {
            "- \(formatter.string(from: $0.createdAt)) [\($0.type.rawValue)]: \(AIPayloadSanitizer.sanitize($0.text))"
        }.joined(separator: "\n")

        let logLines = person.sortedLogEntries.prefix(20).map {
            "- \(formatter.string(from: $0.loggedAt)) [\($0.type.rawValue)]: \(AIPayloadSanitizer.sanitize($0.title))"
        }.joined(separator: "\n")

        let moments    = momentLines.isEmpty ? "" : "\(lang.momentsLabel) (\(person.sortedMoments.count)):\n\(momentLines)\n"
        let logEntries = logLines.isEmpty    ? "" : "\(lang.logEntriesLabel) (\(person.sortedLogEntries.count)):\n\(logLines)\n"
        let interests  = person.interests.map { "\(lang.interestsLabel): \(AIPayloadSanitizer.sanitize($0))\n" } ?? ""
        let culturalBg = person.culturalBackground.map { "\(lang.culturalBackgroundLabel): \($0)\n" } ?? ""

        let instruction: String
        switch promptType {
        case .analysis:     instruction = lang.analysisInstruction
        case .gift:         instruction = lang.giftInstruction
        case .conversation: instruction = lang.conversationInstruction
        }

        return "Person: \(person.firstName)\n"
            + birthYearContext(for: person, language: lang)
            + interests
            + culturalBg
            + "\n"
            + moments
            + "\n"
            + logEntries
            + "\n"
            + instruction
    }

    private func birthYearContext(for person: Person, language: AppLanguage) -> String {
        guard let birthday = person.birthday else { return "" }
        let year = Calendar.current.component(.year, from: birthday)
        return "\(language.birthYearLabel): \(year)\n"
    }

    // MARK: - Gift Cache

    private func giftCacheKey(for person: Person) -> String { "ai_gift_\(person.id.uuidString)" }

    func loadCachedGift(for person: Person) -> CachedGiftSuggestion? {
        guard let data = UserDefaults.standard.data(forKey: giftCacheKey(for: person)),
              let cached = try? JSONDecoder().decode(CachedGiftSuggestion.self, from: data)
        else { return nil }
        return cached
    }

    private func saveGiftCache(_ text: String, for person: Person) {
        let cached = CachedGiftSuggestion(text: text, generatedAt: Date())
        if let data = try? JSONEncoder().encode(cached) {
            UserDefaults.standard.set(data, forKey: giftCacheKey(for: person))
        }
    }

    // MARK: - Gift Suggestion

    func suggestGift(person: Person) async throws -> String {
        let config = AIConfig.load()

        guard let url = URL(string: config.completionsURL) else {
            throw URLError(.badURL)
        }

        let prompt = buildPrompt(for: person, promptType: .gift)

        let body: [String: Any] = [
            "model":   config.model,
            "stream":  false,
            "messages": [
                ["role": "system", "content": AppLanguage.current.systemPrompt],
                ["role": "user",   "content": prompt]
            ]
        ]

        var request = URLRequest(url: url, timeoutInterval: config.timeoutSeconds)
        request.httpMethod = "POST"
        request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json",         forHTTPHeaderField: "Content-Type")
        request.httpBody = try JSONSerialization.data(withJSONObject: body)

        let (data, response) = try await URLSession.shared.data(for: request)

        guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }

        guard
            let json    = try JSONSerialization.jsonObject(with: data) as? [String: Any],
            let choices = json["choices"] as? [[String: Any]],
            let first   = choices.first,
            let message = first["message"] as? [String: Any],
            let content = message["content"] as? String
        else {
            throw URLError(.cannotParseResponse)
        }

        // Normalize **IDEE X:** → IDEE X:
        var normalized = content
        for i in 1...3 {
            normalized = normalized
                .replacingOccurrences(of: "**IDEE \(i):**", with: "IDEE \(i):")
                .replacingOccurrences(of: "**IDEE \(i)**:",  with: "IDEE \(i):")
        }

        recordRequest()
        saveGiftCache(normalized, for: person)
        return normalized
    }

    // MARK: - Conversation Cache

    private func conversationCacheKey(for person: Person) -> String { "ai_conversation_\(person.id.uuidString)" }

    func loadCachedConversation(for person: Person) -> CachedConversationSuggestion? {
        guard let data = UserDefaults.standard.data(forKey: conversationCacheKey(for: person)),
              let cached = try? JSONDecoder().decode(CachedConversationSuggestion.self, from: data)
        else { return nil }
        return cached
    }

    private func saveConversationCache(_ result: ConversationSuggestionResult, for person: Person) {
        let cached = CachedConversationSuggestion(result: result)
        if let data = try? JSONEncoder().encode(cached) {
            UserDefaults.standard.set(data, forKey: conversationCacheKey(for: person))
        }
    }

    // MARK: - Conversation Suggestion

    func suggestConversation(person: Person) async throws -> ConversationSuggestionResult {
        let config = AIConfig.load()

        guard let url = URL(string: config.completionsURL) else {
            throw URLError(.badURL)
        }

        let prompt = buildPrompt(for: person, promptType: .conversation)

        let body: [String: Any] = [
            "model":   config.model,
            "stream":  false,
            "messages": [
                ["role": "system", "content": AppLanguage.current.systemPrompt],
                ["role": "user",   "content": prompt]
            ]
        ]

        var request = URLRequest(url: url, timeoutInterval: config.timeoutSeconds)
        request.httpMethod = "POST"
        request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json",         forHTTPHeaderField: "Content-Type")
        request.httpBody = try JSONSerialization.data(withJSONObject: body)

        let (data, response) = try await URLSession.shared.data(for: request)

        guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }

        guard
            let json    = try JSONSerialization.jsonObject(with: data) as? [String: Any],
            let choices = json["choices"] as? [[String: Any]],
            let first   = choices.first,
            let message = first["message"] as? [String: Any],
            let content = message["content"] as? String
        else {
            throw URLError(.cannotParseResponse)
        }

        let result = parseConversationResult(content)
        recordRequest()
        saveConversationCache(result, for: person)
        return result
    }

    // MARK: - Result Parser

    private func parseResult(_ text: String) -> AIAnalysisResult {
        // Labels können als "MUSTER:" oder "**MUSTER:**" erscheinen — beides normalisieren
        var normalized = text
        for label in ["MUSTER", "BEZIEHUNG", "EMPFEHLUNG"] {
            normalized = normalized
                .replacingOccurrences(of: "**\(label):**", with: "\(label):")
                .replacingOccurrences(of: "**\(label)**:",  with: "\(label):")
                .replacingOccurrences(of: "**\(label)** :", with: "\(label):")
        }

        func extract(_ label: String) -> String {
            // Lookahead nur auf bekannte Labels, nicht auf beliebige Großbuchstaben-Sequenzen
            let pattern = "\(label):\\s*(.+?)(?=\\n(?:MUSTER|BEZIEHUNG|EMPFEHLUNG):|\\z)"
            guard
                let regex = try? NSRegularExpression(pattern: pattern, options: [.dotMatchesLineSeparators]),
                let match = regex.firstMatch(in: normalized, range: NSRange(normalized.startIndex..., in: normalized)),
                let range = Range(match.range(at: 1), in: normalized)
            else { return "" }
            return String(normalized[range]).trimmingCharacters(in: .whitespacesAndNewlines)
        }

        return AIAnalysisResult(
            patterns:       extract("MUSTER"),
            relationship:   extract("BEZIEHUNG"),
            recommendation: extract("EMPFEHLUNG")
        )
    }

    /// Extrahiert die drei Gesprächsvorschlag-Sektionen aus der KI-Antwort.
    /// Internal (nicht private) damit Unit-Tests direkten Zugriff haben.
    func parseConversationResult(_ text: String) -> ConversationSuggestionResult {
        var normalized = text
        for label in ["THEMEN", "GESPRAECHSRETTER", "TIEFE"] {
            normalized = normalized
                .replacingOccurrences(of: "**\(label):**", with: "\(label):")
                .replacingOccurrences(of: "**\(label)**:",  with: "\(label):")
                .replacingOccurrences(of: "**\(label)** :", with: "\(label):")
        }

        func extract(_ label: String) -> String {
            let pattern = "\(label):\\s*(.+?)(?=\\n(?:THEMEN|GESPRAECHSRETTER|TIEFE):|\\z)"
            guard
                let regex = try? NSRegularExpression(pattern: pattern, options: [.dotMatchesLineSeparators]),
                let match = regex.firstMatch(in: normalized, range: NSRange(normalized.startIndex..., in: normalized)),
                let range = Range(match.range(at: 1), in: normalized)
            else { return "" }
            // Verbleibende ** im Fließtext entfernen (KI-Markdown in Plain Text umwandeln)
            return String(normalized[range])
                .replacingOccurrences(of: "**", with: "")
                .trimmingCharacters(in: .whitespacesAndNewlines)
        }

        return ConversationSuggestionResult(
            topics: extract("THEMEN"),
            rescue: extract("GESPRAECHSRETTER"),
            depth:  extract("TIEFE")
        )
    }
}
