import SwiftUI
import Combine

// MARK: - ChatRowView

/// Chat row matching Figma "Row - Chats" component spec (node 3994:38947):
///
/// Row:       height 78, pl-10, pr-16, items-center
/// Avatar:    62px circle, pr-10
/// Contents:  flex-col, h-full, items-start, justify-center, pb-px
///   Title and Trailing Accessories: flex-1, gap-6, items-center, w-full
///     Title and Detail:   flex-1, h-63, items-start, overflow-clip
///       Title:   gap-4, items-center — SF Pro Medium 17/22, tracking -0.43
///       Message: h-41 — SF Pro Regular 15/20, tracking -0.23, secondary
///     Accessories:        h-full, items-center, justify-end
///       Contents-Trailing: flex-col, h-full, items-end, justify-between, pt-8
///         Time:  SF Pro Regular 14/20, tracking -0.23, secondary
///         Other: flex-1, items-end, justify-end, pb-14
///           Badge: bg-#008BFF, min-w-20, max-w-37, px-4, rounded-full
///                  SF Pro Regular 15/20, black, tracking -0.23
struct ChatRowView: View {
    let dialog: Dialog
    /// Desktop parity: suppress unread badge during sync.
    var isSyncing: Bool = false
    /// Desktop parity: show "typing..." instead of last message.
    var isTyping: Bool = false
    /// Group typing: sender names for "Name typing..." / "Name and N typing..." display.
    var typingSenderNames: [String] = []


    var displayTitle: String {
        if dialog.isSavedMessages { return "Saved Messages" }
        if dialog.isGroup {
            let meta = GroupRepository.shared.groupMetadata(
                account: dialog.account,
                groupDialogKey: dialog.opponentKey
            )
            if let title = meta?.title, !title.isEmpty { return title }
        }
        if !dialog.opponentTitle.isEmpty { return dialog.opponentTitle }
        if !dialog.opponentUsername.isEmpty { return "@\(dialog.opponentUsername)" }
        return String(dialog.opponentKey.prefix(12))
    }

    var body: some View {
        let _ = PerformanceLogger.shared.track("chatRow.bodyEval")
        HStack(spacing: 0) {
            avatarSection
                .padding(.trailing, 10)

            contentSection
        }
        .padding(.leading, 10)
        .padding(.trailing, 16)
        .frame(height: 78)
        .contentShape(Rectangle())
    }
}

// MARK: - Avatar

/// Observation-isolated: reads `AvatarRepository.avatarVersion` in its own
/// scope so only the avatar re-renders when opponent avatar changes — not the
/// entire ChatRowView (title, message preview, badge, etc.).
private struct ChatRowAvatar: View {
    let dialog: Dialog

    var body: some View {
        if dialog.isGroup {
            groupAvatarView
        } else {
            directAvatarView
        }
    }

    private var directAvatarView: some View {
        // Establish @Observable tracking — re-renders this view on avatar save/remove.
        let _ = AvatarRepository.shared.avatarVersion
        return AvatarView(
            initials: dialog.initials,
            colorIndex: dialog.avatarColorIndex,
            size: 62,
            isOnline: dialog.isOnline,
            isSavedMessages: dialog.isSavedMessages,
            image: dialog.isSavedMessages ? nil : AvatarRepository.shared.loadAvatar(publicKey: dialog.opponentKey)
        )
    }

    private var groupAvatarView: some View {
        let _ = AvatarRepository.shared.avatarVersion
        let groupImage = AvatarRepository.shared.loadAvatar(publicKey: dialog.opponentKey)
        return ZStack {
            if let image = groupImage {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFill()
                    .frame(width: 62, height: 62)
                    .clipShape(Circle())
            } else {
                Circle()
                    .fill(RosettaColors.avatarColors[dialog.avatarColorIndex % RosettaColors.avatarColors.count].tint)
                    .frame(width: 62, height: 62)
                Image(systemName: "person.2.fill")
                    .font(.system(size: 24, weight: .medium))
                    .foregroundStyle(.white.opacity(0.9))
            }
        }
    }
}

private extension ChatRowView {
    var avatarSection: some View {
        ChatRowAvatar(dialog: dialog)
    }
}

// MARK: - Content Section
// Figma "Contents": flex-col, h-full, items-start, justify-center, pb-px
//   └─ "Title and Trailing Accessories": flex-1, gap-6, items-center

private extension ChatRowView {
    var contentSection: some View {
        HStack(alignment: .center, spacing: 6) {
            // "Title and Detail": flex-1, h-63, items-start, overflow-clip
            VStack(alignment: .leading, spacing: 0) {
                titleRow
                messageRow
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .frame(height: 63)
            .clipped()

            // "Accessories and Grabber": h-full, items-center, justify-end
            trailingColumn
                .frame(maxHeight: .infinity)
        }
        .frame(maxHeight: .infinity)
        .padding(.bottom, 1)
    }
}

// MARK: - Title Row (name + badges)
// Figma "Title": gap-4, items-center, w-full

private extension ChatRowView {
    var titleRow: some View {
        HStack(spacing: 4) {
            Text(displayTitle)
                .font(.system(size: 17, weight: .medium))
                .tracking(-0.43)
                .foregroundStyle(RosettaColors.Adaptive.text)
                .lineLimit(1)

            if !dialog.isSavedMessages && dialog.effectiveVerified > 0 {
                VerifiedBadge(
                    verified: dialog.effectiveVerified,
                    size: 16
                )
            }

            if dialog.isMuted {
                Image(systemName: "speaker.slash.fill")
                    .font(.system(size: 12))
                    .foregroundStyle(RosettaColors.Adaptive.textSecondary)
            }
        }
    }
}

// MARK: - Message Row
// Figma "Message": h-41, SF Pro Regular 15/20, tracking -0.23, secondary

private extension ChatRowView {
    var messageRow: some View {
        Text(messageText)
            .font(.system(size: 15))
            .tracking(-0.23)
            .foregroundStyle(
                isTyping && !dialog.isSavedMessages
                    ? RosettaColors.figmaBlue
                    : RosettaColors.Adaptive.textSecondary
            )
            .lineLimit(2)
            .frame(height: 41, alignment: .topLeading)
    }

    /// Static cache for emoji-parsed message text (avoids regex per row per render).
    private static var messageTextCache: [String: String] = [:]

    var messageText: String {
        // Desktop parity: show "typing..." in chat list row when opponent is typing.
        if isTyping && !dialog.isSavedMessages {
            if dialog.isGroup && !typingSenderNames.isEmpty {
                if typingSenderNames.count == 1 {
                    return "\(typingSenderNames[0]) typing..."
                } else {
                    return "\(typingSenderNames[0]) and \(typingSenderNames.count - 1) typing..."
                }
            }
            return "typing..."
        }
        let raw = dialog.lastMessage.trimmingCharacters(in: .whitespacesAndNewlines)
        if raw.isEmpty {
            return "No messages yet"
        }
        // Desktop parity: show "Group invite" for #group: invite messages.
        if raw.hasPrefix("#group:") {
            return "Group invite"
        }
        // Safety net: never show encrypted ciphertext (ivBase64:ctBase64) to user.
        // This catches stale data persisted before isGarbageText was improved.
        if Self.looksLikeCiphertext(raw) {
            return "No messages yet"
        }
        if let cached = Self.messageTextCache[dialog.lastMessage] {
            return cached
        }
        // Strip inline markdown markers and convert emoji shortcodes for clean preview.
        let cleaned = dialog.lastMessage.replacingOccurrences(of: "**", with: "")
        let result = EmojiParser.replaceShortcodes(in: cleaned)
        if Self.messageTextCache.count > 500 {
            let keysToRemove = Array(Self.messageTextCache.keys.prefix(250))
            for key in keysToRemove { Self.messageTextCache.removeValue(forKey: key) }
        }
        Self.messageTextCache[dialog.lastMessage] = result
        return result
    }

    /// Detects encrypted payload formats that should never be shown in UI.
    private static func looksLikeCiphertext(_ text: String) -> Bool {
        // CHNK: chunked format
        if text.hasPrefix("CHNK:") { return true }
        // ivBase64:ctBase64 or hex-encoded XChaCha20 ciphertext
        let parts = text.components(separatedBy: ":")
        if parts.count == 2 {
            let base64Chars = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "+/="))
            let bothBase64 = parts.allSatisfy { part in
                part.count >= 16 && part.unicodeScalars.allSatisfy { base64Chars.contains($0) }
            }
            if bothBase64 { return true }
        }
        // Pure hex string (≥40 chars, only hex digits) — XChaCha20 wire format
        if text.count >= 40 {
            let hexChars = CharacterSet(charactersIn: "0123456789abcdefABCDEF")
            if text.unicodeScalars.allSatisfy({ hexChars.contains($0) }) { return true }
        }
        return false
    }
}

// MARK: - Trailing Column
// Figma "Contents - Trailing": flex-col, h-full, items-end, justify-between, pt-8
//   ├─ "Read Status and Time": gap-2, items-center
//   └─ "Other": flex-1, items-end, justify-end, pb-14

private extension ChatRowView {
    var trailingColumn: some View {
        VStack(alignment: .trailing, spacing: 0) {
            // Top: read status + time
            HStack(spacing: 2) {
                if dialog.lastMessageFromMe && !dialog.isSavedMessages {
                    deliveryIcon
                }

                Text(formattedTime)
                    .font(.system(size: 14))
                    .tracking(-0.23)
                    .foregroundStyle(
                        dialog.unreadCount > 0 && !dialog.isMuted
                            ? RosettaColors.figmaBlue
                            : RosettaColors.Adaptive.textSecondary
                    )
            }
            .padding(.top, 8)

            Spacer(minLength: 0)

            // Bottom: pin or unread badge
            HStack(spacing: 8) {
                if dialog.isPinned && dialog.unreadCount == 0 {
                    Image(systemName: "pin.fill")
                        .font(.system(size: 15))
                        .foregroundStyle(RosettaColors.Adaptive.textSecondary)
                        .rotationEffect(.degrees(45))
                }

                // Show unread badge whenever there are unread messages.
                // Previously hidden when lastMessageFromMe (desktop parity),
                // but this caused invisible unreads when user sent a reply
                // without reading prior incoming messages first.
                if dialog.unreadCount > 0 && !isSyncing {
                    unreadBadge
                }
            }
            .padding(.bottom, 14)
        }
    }

    @ViewBuilder
    var deliveryIcon: some View {
        if dialog.lastMessageDelivered == .delivered && dialog.lastMessageRead {
            DoubleCheckmarkShape()
                .fill(RosettaColors.figmaBlue)
                .frame(width: 17, height: 9.3)
        } else {
            switch dialog.lastMessageDelivered {
            case .waiting:
                // Timer isolated to sub-view — only .waiting rows create a timer.
                DeliveryWaitingIcon(sentTimestamp: dialog.lastMessageTimestamp)
            case .delivered:
                SingleCheckmarkShape()
                    .fill(RosettaColors.Adaptive.textSecondary)
                    .frame(width: 14, height: 10.3)
            case .error:
                Image(systemName: "exclamationmark.circle.fill")
                    .font(.system(size: 14))
                    .foregroundStyle(RosettaColors.error)
            }
        }
    }

    var unreadBadge: some View {
        let count = dialog.unreadCount
        let text = count > 999 ? "\(count / 1000)K" : (count > 99 ? "99+" : "\(count)")
        let isMuted = dialog.isMuted
        let isSmall = count < 10

        return Text(text)
            .font(.system(size: 15))
            .tracking(-0.23)
            .foregroundStyle(.white)
            .padding(.horizontal, isSmall ? 0 : 4)
            .frame(
                minWidth: 20,
                maxWidth: isSmall ? 20 : 37,
                minHeight: 20
            )
            .background {
                Capsule()
                    .fill(isMuted ? Color(hex: 0x787880) : RosettaColors.figmaBlue)
            }
    }
}

// MARK: - Delivery Waiting Icon (timer-isolated)

/// Desktop parity: clock → error after 80s. Timer only exists on rows with
/// `.waiting` delivery status — all other rows have zero timer overhead.
private struct DeliveryWaitingIcon: View {
    let sentTimestamp: Int64
    @State private var now = Date()
    private let recheckTimer = Timer.publish(every: 40, on: .main, in: .common).autoconnect()

    private var isWithinWindow: Bool {
        guard sentTimestamp > 0 else { return true }
        let sentDate = Date(timeIntervalSince1970: Double(sentTimestamp) / 1000)
        return now.timeIntervalSince(sentDate) < 80
    }

    var body: some View {
        Group {
            if isWithinWindow {
                Image(systemName: "clock")
                    .font(.system(size: 13))
                    .foregroundStyle(RosettaColors.Adaptive.textSecondary)
            } else {
                Image(systemName: "exclamationmark.circle.fill")
                    .font(.system(size: 14))
                    .foregroundStyle(RosettaColors.error)
            }
        }
        .onReceive(recheckTimer) { now = $0 }
    }
}

// MARK: - Time Formatting

private extension ChatRowView {
    private static let timeFormatter: DateFormatter = {
        let f = DateFormatter(); f.dateFormat = "h:mm a"; return f
    }()
    private static let dayFormatter: DateFormatter = {
        let f = DateFormatter(); f.dateFormat = "EEE"; return f
    }()
    private static let dateFormatter: DateFormatter = {
        let f = DateFormatter(); f.dateFormat = "dd.MM.yy"; return f
    }()

    /// Static cache for formatted time strings (avoids Date/Calendar per row per render).
    private static var timeStringCache: [Int64: String] = [:]

    var formattedTime: String {
        guard dialog.lastMessageTimestamp > 0 else { return "" }

        if let cached = Self.timeStringCache[dialog.lastMessageTimestamp] {
            return cached
        }

        let date = Date(timeIntervalSince1970: Double(dialog.lastMessageTimestamp) / 1000)
        let now = Date()
        let calendar = Calendar.current

        let result: String
        if calendar.isDateInToday(date) {
            result = Self.timeFormatter.string(from: date)
        } else if calendar.isDateInYesterday(date) {
            result = "Yesterday"
        } else if let days = calendar.dateComponents([.day], from: date, to: now).day, days < 7 {
            result = Self.dayFormatter.string(from: date)
        } else {
            result = Self.dateFormatter.string(from: date)
        }

        if Self.timeStringCache.count > 500 {
            let keysToRemove = Array(Self.timeStringCache.keys.prefix(250))
            for key in keysToRemove { Self.timeStringCache.removeValue(forKey: key) }
        }
        Self.timeStringCache[dialog.lastMessageTimestamp] = result
        return result
    }
}

// MARK: - Preview

#Preview {
    let sampleDialog = Dialog(
        id: "preview", account: "mykey", opponentKey: "abc001",
        opponentTitle: "Alice Johnson",
        opponentUsername: "alice",
        lastMessage: "Hey, how are you?",
        lastMessageTimestamp: Int64(Date().timeIntervalSince1970 * 1000),
        unreadCount: 3, isOnline: true, lastSeen: 0,
        verified: 1, iHaveSent: true,
        isPinned: false, isMuted: false,
        lastMessageFromMe: true, lastMessageDelivered: .delivered,
        lastMessageRead: true
    )

    VStack(spacing: 0) {
        ChatRowView(dialog: sampleDialog)
        ChatRowView(dialog: sampleDialog, isTyping: true)
    }
    .background(RosettaColors.Adaptive.background)
}
