import ClawdbotChatUI
import ClawdbotKit
import OSLog
import SwiftUI

private struct SessionPreviewItem: Identifiable, Sendable {
    let id: String
    let role: PreviewRole
    let text: String
}

private enum PreviewRole: String, Sendable {
    case user
    case assistant
    case tool
    case system
    case other

    var label: String {
        switch self {
        case .user: "User"
        case .assistant: "Agent"
        case .tool: "Tool"
        case .system: "System"
        case .other: "Other"
        }
    }
}

private actor SessionPreviewCache {
    static let shared = SessionPreviewCache()

    private struct CacheEntry {
        let items: [SessionPreviewItem]
        let updatedAt: Date
    }

    private var entries: [String: CacheEntry] = [:]

    func cachedItems(for sessionKey: String, maxAge: TimeInterval) -> [SessionPreviewItem]? {
        guard let entry = self.entries[sessionKey] else { return nil }
        guard Date().timeIntervalSince(entry.updatedAt) < maxAge else { return nil }
        return entry.items
    }

    func store(items: [SessionPreviewItem], for sessionKey: String) {
        self.entries[sessionKey] = CacheEntry(items: items, updatedAt: Date())
    }

    func lastItems(for sessionKey: String) -> [SessionPreviewItem]? {
        self.entries[sessionKey]?.items
    }
}

struct SessionMenuPreviewView: View {
    private static let logger = Logger(subsystem: "com.clawdbot", category: "SessionPreview")
    private static let previewTimeoutSeconds: Double = 4

    let sessionKey: String
    let width: CGFloat
    let maxItems: Int
    let maxLines: Int
    let title: String

    @Environment(\.menuItemHighlighted) private var isHighlighted
    @State private var items: [SessionPreviewItem] = []
    @State private var status: LoadStatus = .loading

    private struct PreviewTimeoutError: LocalizedError {
        var errorDescription: String? { "preview timeout" }
    }

    private enum LoadStatus: Equatable {
        case loading
        case ready
        case empty
        case error(String)
    }

    private var primaryColor: Color {
        self.isHighlighted ? Color(nsColor: .selectedMenuItemTextColor) : .primary
    }

    private var secondaryColor: Color {
        self.isHighlighted ? Color(nsColor: .selectedMenuItemTextColor).opacity(0.85) : .secondary
    }

    private var previewLimit: Int {
        min(max(self.maxItems * 3, 20), 120)
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            HStack(alignment: .firstTextBaseline, spacing: 4) {
                Text(self.title)
                    .font(.caption.weight(.semibold))
                    .foregroundStyle(self.secondaryColor)
                Spacer(minLength: 8)
            }

            switch self.status {
            case .loading:
                Text("Loading preview…")
                    .font(.caption)
                    .foregroundStyle(self.secondaryColor)
            case .empty:
                Text("No recent messages")
                    .font(.caption)
                    .foregroundStyle(self.secondaryColor)
            case let .error(message):
                Text(message)
                    .font(.caption)
                    .foregroundStyle(self.secondaryColor)
            case .ready:
                VStack(alignment: .leading, spacing: 6) {
                    ForEach(self.items) { item in
                        self.previewRow(item)
                    }
                }
            }
        }
        .padding(.vertical, 6)
        .padding(.leading, 16)
        .padding(.trailing, 11)
        .frame(width: max(1, self.width), alignment: .leading)
        .task(id: self.sessionKey) {
            await self.loadPreview()
        }
    }

    @ViewBuilder
    private func previewRow(_ item: SessionPreviewItem) -> some View {
        HStack(alignment: .top, spacing: 4) {
            Text(item.role.label)
                .font(.caption2.monospacedDigit())
                .foregroundStyle(self.roleColor(item.role))
                .frame(width: 50, alignment: .leading)

            Text(item.text)
                .font(.caption)
                .foregroundStyle(self.primaryColor)
                .multilineTextAlignment(.leading)
                .lineLimit(self.maxLines)
                .truncationMode(.tail)
                .fixedSize(horizontal: false, vertical: true)
        }
    }

    private func roleColor(_ role: PreviewRole) -> Color {
        if self.isHighlighted { return Color(nsColor: .selectedMenuItemTextColor).opacity(0.9) }
        switch role {
        case .user: return .accentColor
        case .assistant: return .secondary
        case .tool: return .orange
        case .system: return .gray
        case .other: return .secondary
        }
    }

    private func loadPreview() async {
        if let cached = await SessionPreviewCache.shared.cachedItems(for: self.sessionKey, maxAge: 12) {
            await MainActor.run {
                self.items = cached
                self.status = cached.isEmpty ? .empty : .ready
            }
            return
        }

        await MainActor.run {
            self.status = .loading
        }

        do {
            let timeoutMs = Int(Self.previewTimeoutSeconds * 1000)
            let payload = try await AsyncTimeout.withTimeout(
                seconds: Self.previewTimeoutSeconds,
                onTimeout: { PreviewTimeoutError() },
                operation: {
                    try await GatewayConnection.shared.chatHistory(
                        sessionKey: self.sessionKey,
                        limit: self.previewLimit,
                        timeoutMs: timeoutMs)
                })
            let built = Self.previewItems(from: payload, maxItems: self.maxItems)
            await SessionPreviewCache.shared.store(items: built, for: self.sessionKey)
            await MainActor.run {
                self.items = built
                self.status = built.isEmpty ? .empty : .ready
            }
        } catch is CancellationError {
            return
        } catch {
            let fallback = await SessionPreviewCache.shared.lastItems(for: self.sessionKey)
            await MainActor.run {
                if let fallback {
                    self.items = fallback
                    self.status = fallback.isEmpty ? .empty : .ready
                } else {
                    self.status = .error("Preview unavailable")
                }
            }
            let errorDescription = String(describing: error)
            Self.logger.warning(
                "Session preview failed session=\(self.sessionKey, privacy: .public) " +
                    "error=\(errorDescription, privacy: .public)")
        }
    }

    private static func previewItems(
        from payload: ClawdbotChatHistoryPayload,
        maxItems: Int) -> [SessionPreviewItem]
    {
        let raw: [ClawdbotKit.AnyCodable] = payload.messages ?? []
        let messages = self.decodeMessages(raw)
        let built = messages.compactMap { message -> SessionPreviewItem? in
            guard let text = self.previewText(for: message) else { return nil }
            let isTool = self.isToolCall(message)
            let role = self.previewRole(message.role, isTool: isTool)
            let id = "\(message.timestamp ?? 0)-\(UUID().uuidString)"
            return SessionPreviewItem(id: id, role: role, text: text)
        }

        let trimmed = built.suffix(maxItems)
        return Array(trimmed.reversed())
    }

    private static func decodeMessages(_ raw: [ClawdbotKit.AnyCodable]) -> [ClawdbotChatMessage] {
        raw.compactMap { item in
            guard let data = try? JSONEncoder().encode(item) else { return nil }
            return try? JSONDecoder().decode(ClawdbotChatMessage.self, from: data)
        }
    }

    private static func previewRole(_ raw: String, isTool: Bool) -> PreviewRole {
        if isTool { return .tool }
        switch raw.lowercased() {
        case "user": return .user
        case "assistant": return .assistant
        case "system": return .system
        case "tool": return .tool
        default: return .other
        }
    }

    private static func previewText(for message: ClawdbotChatMessage) -> String? {
        let text = message.content.compactMap(\.text).joined(separator: "\n")
            .trimmingCharacters(in: .whitespacesAndNewlines)
        if !text.isEmpty { return text }

        let toolNames = self.toolNames(for: message)
        if !toolNames.isEmpty {
            let shown = toolNames.prefix(2)
            let overflow = toolNames.count - shown.count
            var label = "call \(shown.joined(separator: ", "))"
            if overflow > 0 { label += " +\(overflow)" }
            return label
        }

        if let media = self.mediaSummary(for: message) {
            return media
        }

        return nil
    }

    private static func isToolCall(_ message: ClawdbotChatMessage) -> Bool {
        if message.toolName?.nonEmpty != nil { return true }
        return message.content.contains { $0.name?.nonEmpty != nil || $0.type?.lowercased() == "toolcall" }
    }

    private static func toolNames(for message: ClawdbotChatMessage) -> [String] {
        var names: [String] = []
        for content in message.content {
            if let name = content.name?.nonEmpty {
                names.append(name)
            }
        }
        if let toolName = message.toolName?.nonEmpty {
            names.append(toolName)
        }
        return Self.dedupePreservingOrder(names)
    }

    private static func mediaSummary(for message: ClawdbotChatMessage) -> String? {
        let types = message.content.compactMap { content -> String? in
            let raw = content.type?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
            guard let raw, !raw.isEmpty else { return nil }
            if raw == "text" || raw == "toolcall" { return nil }
            return raw
        }
        guard let first = types.first else { return nil }
        return "[\(first)]"
    }

    private static func dedupePreservingOrder(_ values: [String]) -> [String] {
        var seen = Set<String>()
        var result: [String] = []
        for value in values where !seen.contains(value) {
            seen.insert(value)
            result.append(value)
        }
        return result
    }
}
