//
// OpenRocky — Voice-first AI Agent
// https://github.com/openrocky
//
// Developed by everettjf with the assistance of Claude Code and Codex.
// Date: 2026-05-04
// Copyright (c) 2026 everettjf. All rights reserved.
//

import Foundation
import LanguageModelChatUI

/// Distills a stored conversation into a compact list of items that can be
/// replayed into a fresh realtime session via `conversation.item.create` so the
/// model can continue where the user left off (instead of starting blind).
///
/// Tool calls and audio parts are deliberately dropped: the realtime model
/// can't act on a replayed tool result, and the costs of including them
/// (extra input tokens, confused turn boundaries) outweigh the value.
nonisolated enum OpenRockyVoicePriming {
    /// Caps tuned to keep priming under ~5–10s of latency on the
    /// session.update round-trip and well under the realtime input budget.
    /// These are local heuristics, not server limits.
    static let maxItemCount = 12
    static let maxTotalCharacters = 4000
    static let maxPerItemCharacters = 1500

    /// Build priming items from the most recent N messages of a conversation.
    /// Order is preserved (oldest → newest within the kept window) so the
    /// model sees the conversation in chronological order.
    nonisolated static func items(from messages: [ConversationMessage]) -> [OpenRockyVoicePrimingItem] {
        // Project each message to its concatenated text content. Empty results
        // (e.g., a message that was purely a tool-call burst) drop out here.
        let candidates: [OpenRockyVoicePrimingItem] = messages.compactMap { message in
            guard let role = primingRole(message.role) else { return nil }
            let text = textContent(of: message)
            guard !text.isEmpty else { return nil }
            return OpenRockyVoicePrimingItem(role: role, text: text)
        }
        return capped(candidates)
    }

    /// Apply the same count/character budgets to a pre-built item list. Used on
    /// reconnect to compress (initial priming + in-session turns) back into the
    /// budget — without it a long live conversation would balloon the replay.
    nonisolated static func capped(_ items: [OpenRockyVoicePrimingItem]) -> [OpenRockyVoicePrimingItem] {
        guard !items.isEmpty else { return [] }

        // Keep the tail (most-recent), preserving chronological order.
        let tail = items.suffix(maxItemCount)

        // Walk newest → oldest accumulating into the budget; reverse at the
        // end so the final list stays oldest → newest.
        var kept: [OpenRockyVoicePrimingItem] = []
        var remainingBudget = maxTotalCharacters
        for item in tail.reversed() {
            guard remainingBudget > 0 else { break }
            let truncated = truncate(item.text, max: min(maxPerItemCharacters, remainingBudget))
            guard !truncated.isEmpty else { break }
            kept.append(OpenRockyVoicePrimingItem(role: item.role, text: truncated))
            remainingBudget -= truncated.count
        }
        return kept.reversed()
    }

    // MARK: - Helpers

    private static func primingRole(_ role: MessageRole) -> OpenRockyVoicePrimingItem.Role? {
        switch role {
        case .user: return .user
        case .assistant: return .assistant
        default: return nil
        }
    }

    private static func textContent(of message: ConversationMessage) -> String {
        let pieces: [String] = message.parts.compactMap { part in
            if case .text(let t) = part {
                let trimmed = t.text.trimmingCharacters(in: .whitespacesAndNewlines)
                return trimmed.isEmpty ? nil : trimmed
            }
            return nil
        }
        return pieces.joined(separator: "\n")
    }

    private static func truncate(_ text: String, max: Int) -> String {
        guard text.count > max else { return text }
        guard max > 1 else { return "" }
        // Use the leading prefix and add an ellipsis to mark the cut so the
        // model knows the turn was clipped, not that the user trailed off.
        return String(text.prefix(max - 1)) + "…"
    }
}
