//
//  ContextSelectionService.swift
//  Confab
//
//  Smart context selection for AI conversations
//  Selects relevant messages instead of sending entire history
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of Confab.
//
// Confab is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling Confab or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://confab.no>.


import Foundation
import os

// MARK: - Context Window

struct ContextWindow {
    let messages: [Message]
    let summaries: [String]
    let totalTokens: Int
    let excludedCount: Int
}

// MARK: - Selection Strategy

enum SelectionStrategy {
    case allMessages      // Memory ON (old behavior): send all messages
    case lastMessageOnly  // Memory OFF: send only last message
    case smart            // NEW: intelligent selection
}

// MARK: - Context Selection Service

final class ContextSelectionService {
    static let shared = ContextSelectionService()

    private let db: DatabaseService

    /// `db` defaults to the shared on-disk instance; tests inject an in-memory `DatabaseService`.
    init(db: DatabaseService = .shared) {
        self.db = db
    }

    /// Select context messages using the specified strategy
    func selectContext(
        allMessages: [Message],
        strategy: SelectionStrategy,
        maxTokens: Int?,
        currentQuery: String? = nil,
        conversationId: UUID? = nil
    ) -> ContextWindow {
        switch strategy {
        case .allMessages:
            return allMessagesContext(allMessages)

        case .lastMessageOnly:
            return lastMessageOnlyContext(allMessages)

        case .smart:
            guard let maxTokens = maxTokens else {
                // Fallback to all messages if no token limit
                return allMessagesContext(allMessages)
            }
            return smartSelection(allMessages: allMessages, maxTokens: maxTokens, conversationId: conversationId)
        }
    }

    // MARK: - Simple Strategies

    private func allMessagesContext(_ messages: [Message]) -> ContextWindow {
        ContextWindow(
            messages: messages,
            summaries: [],
            totalTokens: estimateTokens(messages),
            excludedCount: 0
        )
    }

    private func lastMessageOnlyContext(_ messages: [Message]) -> ContextWindow {
        guard let last = messages.last else {
            return ContextWindow(messages: [], summaries: [], totalTokens: 0, excludedCount: 0)
        }
        return ContextWindow(
            messages: [last],
            summaries: [],
            totalTokens: estimateTokens([last]),
            excludedCount: messages.count - 1
        )
    }

    // MARK: - Smart Selection Algorithm

    func smartSelection(allMessages: [Message], maxTokens: Int, conversationId: UUID? = nil) -> ContextWindow {
        guard !allMessages.isEmpty else {
            return ContextWindow(messages: [], summaries: [], totalTokens: 0, excludedCount: 0)
        }

        // Filter out system messages (tools)
        let chatMessages = allMessages.filter { $0.role == .user || $0.role == .assistant }

        // Step 1: Always include last N messages (recent context)
        let recentCount = min(10, chatMessages.count)
        let recentMessages = Array(chatMessages.suffix(recentCount))
        var selectedMessages = recentMessages
        var currentTokens = estimateTokens(recentMessages)

        Log.ui.debug("Smart selection: starting with last \(recentCount) messages (\(currentTokens) tokens)")

        // Step 2: Add starred messages from earlier in conversation
        let olderMessages = chatMessages.dropLast(recentCount)
        var starredMessages: [Message] = []

        for message in olderMessages {
            // Check if message is starred
            if isMessageStarred(message) {
                let msgTokens = estimateTokens([message])
                if currentTokens + msgTokens <= maxTokens {
                    starredMessages.append(message)
                    currentTokens += msgTokens
                } else {
                    Log.ui.debug("Smart selection: token budget exceeded, stopping at \(selectedMessages.count + starredMessages.count) messages")
                    break
                }
            }
        }

        // Step 3: Add important messages (high cost, long content)
        var importantMessages: [Message] = []
        if currentTokens < maxTokens {
            for message in olderMessages {
                // Skip if already starred
                if starredMessages.contains(where: { $0.id == message.id }) {
                    continue
                }

                let importance = getImportanceScore(message)
                if importance > 0.5 { // Threshold for "important"
                    let msgTokens = estimateTokens([message])
                    if currentTokens + msgTokens <= maxTokens {
                        importantMessages.append(message)
                        currentTokens += msgTokens
                    } else {
                        break
                    }
                }
            }
        }

        // Combine: starred + important + recent (in chronological order)
        let allSelected = (starredMessages + importantMessages + recentMessages)
            .sorted { $0.timestamp < $1.timestamp }

        // Remove duplicates while preserving order
        var seen = Set<UUID>()
        selectedMessages = allSelected.filter { message in
            if seen.contains(message.id) {
                return false
            }
            seen.insert(message.id)
            return true
        }

        let excludedCount = chatMessages.count - selectedMessages.count

        // Get summaries for excluded message ranges
        var summaries: [String] = []
        if excludedCount > 0, let conversationId = conversationId {
            summaries = getSummariesForExcludedRange(
                conversationId: conversationId,
                totalMessages: chatMessages.count,
                selectedCount: selectedMessages.count
            )
        }

        Log.ui.info("Smart selection: selected \(selectedMessages.count)/\(chatMessages.count) messages (\(currentTokens) tokens, excluded: \(excludedCount), summaries: \(summaries.count))")

        return ContextWindow(
            messages: selectedMessages,
            summaries: summaries,
            totalTokens: currentTokens,
            excludedCount: excludedCount
        )
    }

    /// Get summaries for excluded message ranges
    func getSummariesForExcludedRange(
        conversationId: UUID,
        totalMessages: Int,
        selectedCount: Int
    ) -> [String] {
        guard let summaryRecords = try? db.getConversationSummaries(conversationId: conversationId) else {
            return []
        }

        var summaries: [String] = []
        for record in summaryRecords {
            // Only include summaries for messages that were excluded
            if record.end_message_index < (totalMessages - selectedCount) {
                summaries.append(record.summary)
            }
        }

        return summaries
    }

    // MARK: - Importance Scoring

    /// Calculate importance score (0.0 - 1.0) for a message
    func getImportanceScore(_ message: Message) -> Double {
        var score = 0.0

        // Factor 1: Cost (expensive calls are important)
        if let cost = message.cost {
            let costScore = min(1.0, cost / 0.01) // $0.01+ = max score
            score += costScore * 0.5
        }

        // Factor 2: Length (detailed messages are important)
        let contentLength = Double(message.content.count)
        let lengthScore = min(1.0, contentLength / 2000.0) // 2000+ chars = max score
        score += lengthScore * 0.3

        // Factor 3: Token count (if available)
        if let tokens = message.tokens {
            let tokenScore = min(1.0, Double(tokens) / 1000.0) // 1000+ tokens = max score
            score += tokenScore * 0.2
        }

        return min(1.0, score)
    }

    /// Check if a message is starred by the user
    func isMessageStarred(_ message: Message) -> Bool {
        guard let metadata = try? db.getMessageMetadata(messageId: message.id) else {
            return false
        }
        return metadata.user_starred == 1
    }

    // MARK: - Token Estimation

    /// Estimate token count for messages (rough approximation)
    func estimateTokens(_ messages: [Message]) -> Int {
        var total = 0
        for message in messages {
            if let tokens = message.tokens {
                total += tokens
            } else {
                // Rough estimate: 1 token ≈ 4 characters
                total += message.content.count / 4
            }
        }
        return total
    }
}
