import SwiftUI
import UIKit

/// Equatable message cell — SwiftUI skips body re-evaluation when inputs haven't changed.
/// Extracted from ChatDetailView to create an Equatable boundary for `.equatable()` modifier.
struct MessageCellView: View, Equatable {
    let message: ChatMessage
    let maxBubbleWidth: CGFloat
    let position: BubblePosition
    let currentPublicKey: String
    let highlightedMessageId: String?
    let isSavedMessages: Bool
    let isSystemAccount: Bool
    let opponentPublicKey: String
    let opponentTitle: String
    let opponentUsername: String
    let actions: MessageCellActions

    static func == (lhs: MessageCellView, rhs: MessageCellView) -> Bool {
        lhs.message == rhs.message &&
        lhs.maxBubbleWidth == rhs.maxBubbleWidth &&
        lhs.position == rhs.position &&
        lhs.highlightedMessageId == rhs.highlightedMessageId
        // currentPublicKey, isSavedMessages, isSystemAccount, opponent* — stable per chat session
        // actions — class ref, excluded (pointer identity is unstable across re-renders)
    }

    var body: some View {
        #if DEBUG
        let _ = PerformanceLogger.shared.track("chatDetail.rowEval")
        #endif
        let outgoing = message.isFromMe(myPublicKey: currentPublicKey)
        let hasTail = position == .single || position == .bottom

        let visibleAttachments = message.attachments.filter {
            $0.type == .image || $0.type == .file || $0.type == .avatar || $0.type == .call
        }

        Group {
            if visibleAttachments.isEmpty {
                let hasReplyAttachment = message.attachments.contains(where: { $0.type == .messages })
                if hasReplyAttachment || !Self.isGarbageText(message.text) {
                    textOnlyBubble(
                        message: message,
                        outgoing: outgoing,
                        hasTail: hasTail,
                        maxBubbleWidth: maxBubbleWidth,
                        position: position
                    )
                }
            } else {
                attachmentBubble(
                    message: message,
                    attachments: visibleAttachments,
                    outgoing: outgoing,
                    hasTail: hasTail,
                    maxBubbleWidth: maxBubbleWidth,
                    position: position
                )
            }
        }
        .modifier(ConditionalSwipeToReply(
            enabled: !isSavedMessages && !isSystemAccount
                && !message.attachments.contains(where: { $0.type == .avatar })
                && !message.text.hasPrefix("#group:"),
            onReply: { actions.onReply(message) }
        ))
        .overlay {
            if highlightedMessageId == message.id {
                RoundedRectangle(cornerRadius: 16)
                    .fill(Color.white.opacity(0.12))
                    .allowsHitTesting(false)
            }
        }
        .frame(maxWidth: .infinity, alignment: outgoing ? .trailing : .leading)
        .padding(.trailing, outgoing ? (hasTail ? 2 : MessageBubbleShape.tailProtrusion + 2) : 0)
        .padding(.leading, !outgoing ? (hasTail ? 2 : MessageBubbleShape.tailProtrusion + 2) : 0)
        .padding(.top, (position == .single || position == .top) ? 6 : 2)
        .padding(.bottom, 0)
    }

    // MARK: - Text-Only Bubble

    @ViewBuilder
    private func textOnlyBubble(
        message: ChatMessage, outgoing: Bool, hasTail: Bool,
        maxBubbleWidth: CGFloat, position: BubblePosition
    ) -> some View {
        let messageText = message.text.isEmpty ? " " : message.text
        let replyAttachment = message.attachments.first(where: { $0.type == .messages })
        let replyData = replyAttachment.flatMap {
            parseReplyBlob($0.blob) ?? parseReplyBlob($0.preview)
        }?.first
        let isForward = (message.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
            || Self.isGarbageText(message.text)) && replyData != nil

        if isForward, let reply = replyData {
            forwardedMessageBubble(
                message: message, reply: reply, outgoing: outgoing,
                hasTail: hasTail, maxBubbleWidth: maxBubbleWidth, position: position
            )
        } else if message.text.hasPrefix("#group:"),
                  let parsed = GroupRepository.parseInviteStringPure(message.text) {
            GroupInviteCardView(
                inviteString: message.text,
                title: parsed.title,
                groupId: parsed.groupId,
                isOutgoing: outgoing,
                actions: actions
            )
            .frame(width: min(220, maxBubbleWidth))
            .overlay(alignment: .bottomTrailing) {
                timestampOverlay(message: message, outgoing: outgoing)
            }
            .padding(.trailing, outgoing && hasTail ? MessageBubbleShape.tailProtrusion : 0)
            .padding(.leading, !outgoing && hasTail ? MessageBubbleShape.tailProtrusion : 0)
            .background { bubbleBackground(outgoing: outgoing, position: position) }
            .overlay {
                BubbleContextMenuOverlay(
                    items: contextMenuItems(for: message),
                    previewShape: MessageBubbleShape(position: position, outgoing: outgoing),
                    isOutgoing: outgoing,
                    replyQuoteHeight: 0,
                    onReplyQuoteTap: nil
                )
            }
            .frame(maxWidth: maxBubbleWidth, alignment: outgoing ? .trailing : .leading)
        } else {
            VStack(alignment: .leading, spacing: 0) {
                if let reply = replyData {
                    replyQuoteView(reply: reply, outgoing: outgoing)
                }

                Text(parsedMarkdown(messageText, outgoing: outgoing))
                    .font(.system(size: 17, weight: .regular))
                    .tracking(-0.43)
                    .foregroundStyle(outgoing ? Color.white : RosettaColors.Adaptive.text)
                    .multilineTextAlignment(.leading)
                    .lineSpacing(0)
                    .fixedSize(horizontal: false, vertical: true)
                    .padding(.leading, 11)
                    .padding(.trailing, outgoing ? 48 : 36)
                    .padding(.vertical, 5)
            }
            .frame(minWidth: outgoing ? 86 : 66, alignment: .leading)
            .overlay(alignment: .bottomTrailing) {
                timestampOverlay(message: message, outgoing: outgoing)
            }
            .padding(.trailing, outgoing && hasTail ? MessageBubbleShape.tailProtrusion : 0)
            .padding(.leading, !outgoing && hasTail ? MessageBubbleShape.tailProtrusion : 0)
            .background { bubbleBackground(outgoing: outgoing, position: position) }
            .overlay {
                BubbleContextMenuOverlay(
                    items: contextMenuItems(for: message),
                    previewShape: MessageBubbleShape(position: position, outgoing: outgoing),
                    isOutgoing: outgoing,
                    replyQuoteHeight: replyData != nil ? 49 : 0,
                    onReplyQuoteTap: replyData.map { reply in
                        { [reply] in actions.onScrollToMessage(reply.message_id) }
                    }
                )
            }
            .frame(maxWidth: maxBubbleWidth, alignment: outgoing ? .trailing : .leading)
        }
    }

    // MARK: - Forwarded Message Bubble

    @ViewBuilder
    private func forwardedMessageBubble(
        message: ChatMessage, reply: ReplyMessageData, outgoing: Bool,
        hasTail: Bool, maxBubbleWidth: CGFloat, position: BubblePosition
    ) -> some View {
        let senderName = senderDisplayName(for: reply.publicKey)
        let senderInitials = RosettaColors.initials(name: senderName, publicKey: reply.publicKey)
        let senderColorIndex = RosettaColors.avatarColorIndex(for: senderName, publicKey: reply.publicKey)
        let senderAvatar = AvatarRepository.shared.loadAvatar(publicKey: reply.publicKey)

        let imageAttachments = reply.attachments.filter { $0.type == 0 }
        let fileAttachments = reply.attachments.filter { $0.type == 2 }
        let hasVisualAttachments = !imageAttachments.isEmpty || !fileAttachments.isEmpty

        let hasCaption = !reply.message.trimmingCharacters(in: .whitespaces).isEmpty
            && !Self.isGarbageText(reply.message)

        let fallbackText: String = {
            if hasCaption { return reply.message }
            if !imageAttachments.isEmpty { return hasVisualAttachments ? "" : "Photo" }
            if let file = fileAttachments.first {
                let parsed = AttachmentPreviewCodec.parseFilePreview(file.preview)
                if !parsed.fileName.isEmpty { return parsed.fileName }
                return file.id.isEmpty ? "File" : file.id
            }
            if reply.attachments.contains(where: { $0.type == AttachmentType.avatar.rawValue }) { return "Avatar" }
            if reply.attachments.contains(where: { $0.type == AttachmentType.voice.rawValue }) { return "Voice message" }
            return "Message"
        }()

        let imageContentWidth = maxBubbleWidth - 22
            - (outgoing && hasTail ? MessageBubbleShape.tailProtrusion : 0)
            - (!outgoing && hasTail ? MessageBubbleShape.tailProtrusion : 0)

        VStack(alignment: .leading, spacing: 0) {
            Text("Forwarded from")
                .font(.system(size: 14, weight: .regular))
                .foregroundStyle(outgoing ? Color.white : RosettaColors.figmaBlue)
                .padding(.leading, 11)
                .padding(.top, 7)

            HStack(spacing: 4) {
                AvatarView(
                    initials: senderInitials,
                    colorIndex: senderColorIndex,
                    size: 16,
                    image: senderAvatar
                )
                Text(senderName)
                    .font(.system(size: 14, weight: .medium))
                    .foregroundStyle(outgoing ? Color.white : RosettaColors.figmaBlue)
                    .lineLimit(1)
            }
            .padding(.leading, 11)
            .padding(.top, 0)

            if !imageAttachments.isEmpty {
                ForwardedPhotoCollageView(
                    attachments: imageAttachments,
                    outgoing: outgoing,
                    maxWidth: imageContentWidth,
                    onImageTap: { attId in actions.onImageTap(attId, .zero, nil) }
                )
                .padding(.horizontal, 6)
                .padding(.top, 4)
            }

            ForEach(fileAttachments, id: \.id) { att in
                forwardedFilePreview(attachment: att, outgoing: outgoing)
                    .padding(.horizontal, 6)
                    .padding(.top, 4)
            }

            if hasCaption {
                Text(parsedMarkdown(reply.message, outgoing: outgoing))
                    .font(.system(size: 17, weight: .regular))
                    .tracking(-0.43)
                    .foregroundStyle(outgoing ? Color.white : RosettaColors.Adaptive.text)
                    .multilineTextAlignment(.leading)
                    .lineSpacing(0)
                    .fixedSize(horizontal: false, vertical: true)
                    .padding(.leading, 11)
                    .padding(.trailing, outgoing ? 48 : 36)
                    .padding(.top, 3)
                    .padding(.bottom, 5)
            } else if !hasVisualAttachments {
                Text(fallbackText)
                    .font(.system(size: 17, weight: .regular))
                    .tracking(-0.43)
                    .foregroundStyle(outgoing ? Color.white.opacity(0.7) : RosettaColors.Adaptive.textSecondary)
                    .padding(.leading, 11)
                    .padding(.trailing, outgoing ? 48 : 36)
                    .padding(.top, 3)
                    .padding(.bottom, 5)
            } else {
                Spacer().frame(height: 5)
            }
        }
        .frame(minWidth: 130, alignment: .leading)
        .overlay(alignment: .bottomTrailing) {
            if !imageAttachments.isEmpty && !hasCaption {
                mediaTimestampOverlay(message: message, outgoing: outgoing)
            } else {
                timestampOverlay(message: message, outgoing: outgoing)
            }
        }
        .padding(.trailing, outgoing && hasTail ? MessageBubbleShape.tailProtrusion : 0)
        .padding(.leading, !outgoing && hasTail ? MessageBubbleShape.tailProtrusion : 0)
        .background { bubbleBackground(outgoing: outgoing, position: position) }
        .overlay {
            BubbleContextMenuOverlay(
                items: contextMenuItems(for: message),
                previewShape: MessageBubbleShape(position: position, outgoing: outgoing),
                isOutgoing: outgoing,
                onTap: !imageAttachments.isEmpty ? { _, overlayView in
                    if let firstId = imageAttachments.first?.id {
                        let frame = overlayView?.convert(overlayView?.bounds ?? .zero, to: nil) ?? .zero
                        actions.onImageTap(firstId, frame, nil)
                    }
                } : nil
            )
        }
        .frame(maxWidth: maxBubbleWidth, alignment: outgoing ? .trailing : .leading)
    }

    // MARK: - Attachment Bubble

    @ViewBuilder
    private func attachmentBubble(
        message: ChatMessage, attachments: [MessageAttachment], outgoing: Bool,
        hasTail: Bool, maxBubbleWidth: CGFloat, position: BubblePosition
    ) -> some View {
        let hasCaption = Self.isValidCaption(message.text)
        let partitioned = Self.partitionAttachments(attachments)
        let imageAttachments = partitioned.images
        let otherAttachments = partitioned.others
        let isImageOnly = !imageAttachments.isEmpty && otherAttachments.isEmpty && !hasCaption

        VStack(alignment: .leading, spacing: 0) {
            if !imageAttachments.isEmpty {
                PhotoCollageView(
                    attachments: imageAttachments,
                    message: message,
                    outgoing: outgoing,
                    maxWidth: maxBubbleWidth - (hasTail ? MessageBubbleShape.tailProtrusion : 0),
                    position: position
                )
            }

            ForEach(otherAttachments, id: \.id) { attachment in
                Group {
                    switch attachment.type {
                    case .file:
                        MessageFileView(
                            attachment: attachment,
                            message: message,
                            outgoing: outgoing
                        )
                        .padding(.horizontal, 4)
                        .padding(.top, 4)
                    case .avatar:
                        MessageAvatarView(
                            attachment: attachment,
                            message: message,
                            outgoing: outgoing
                        )
                        .padding(.horizontal, 6)
                        .padding(.top, 4)
                    case .call:
                        MessageCallView(
                            attachment: attachment,
                            message: message,
                            outgoing: outgoing,
                            currentPublicKey: currentPublicKey,
                            actions: actions
                        )
                        .padding(.horizontal, 4)
                        .padding(.top, 4)
                    default:
                        EmptyView()
                    }
                }
            }

            if hasCaption {
                Text(parsedMarkdown(message.text, outgoing: outgoing))
                    .font(.system(size: 17, weight: .regular))
                    .tracking(-0.43)
                    .foregroundStyle(outgoing ? Color.white : RosettaColors.Adaptive.text)
                    .multilineTextAlignment(.leading)
                    .lineSpacing(0)
                    .fixedSize(horizontal: false, vertical: true)
                    .padding(.leading, 11)
                    .padding(.trailing, outgoing ? 48 : 36)
                    .padding(.top, 6)
                    .padding(.bottom, 5)
            }
        }
        .frame(minWidth: outgoing ? 86 : 66, alignment: .leading)
        .overlay(alignment: .bottomTrailing) {
            if isImageOnly {
                mediaTimestampOverlay(message: message, outgoing: outgoing)
            } else {
                timestampOverlay(message: message, outgoing: outgoing)
            }
        }
        .padding(.trailing, outgoing && hasTail ? MessageBubbleShape.tailProtrusion : 0)
        .padding(.leading, !outgoing && hasTail ? MessageBubbleShape.tailProtrusion : 0)
        .background { bubbleBackground(outgoing: outgoing, position: position) }
        .clipShape(MessageBubbleShape(position: position, outgoing: outgoing))
        .overlay {
            BubbleContextMenuOverlay(
                items: contextMenuItems(for: message),
                previewShape: MessageBubbleShape(position: position, outgoing: outgoing),
                isOutgoing: outgoing,
                onTap: !attachments.isEmpty ? { tapLocation, overlayView in
                    if !imageAttachments.isEmpty {
                        let tappedId = imageAttachments.count == 1
                            ? imageAttachments[0].id
                            : Self.collageAttachmentId(
                                at: tapLocation,
                                attachments: imageAttachments,
                                maxWidth: maxBubbleWidth
                            )
                        if AttachmentCache.shared.loadImage(forAttachmentId: tappedId) != nil {
                            let frame = overlayView?.convert(overlayView?.bounds ?? .zero, to: nil) ?? .zero
                            actions.onImageTap(tappedId, frame, nil)
                        } else {
                            NotificationCenter.default.post(
                                name: .triggerAttachmentDownload, object: tappedId
                            )
                        }
                    } else {
                        for att in otherAttachments {
                            NotificationCenter.default.post(
                                name: .triggerAttachmentDownload, object: att.id
                            )
                        }
                    }
                } : nil
            )
        }
        .frame(maxWidth: maxBubbleWidth, alignment: outgoing ? .trailing : .leading)
    }

    // MARK: - Timestamp Overlays

    @ViewBuilder
    private func timestampOverlay(message: ChatMessage, outgoing: Bool) -> some View {
        HStack(spacing: 3) {
            Text(messageTime(message.timestamp))
                .font(.system(size: 11, weight: .regular))
                .foregroundStyle(
                    outgoing
                        ? Color.white.opacity(0.55)
                        : RosettaColors.Adaptive.textSecondary.opacity(0.6)
                )

            if outgoing {
                if message.deliveryStatus == .error {
                    errorMenu(for: message)
                } else {
                    deliveryIndicator(message.deliveryStatus, read: message.isRead)
                }
            }
        }
        .padding(.trailing, 11)
        .padding(.bottom, 5)
    }

    @ViewBuilder
    private func mediaTimestampOverlay(message: ChatMessage, outgoing: Bool) -> some View {
        HStack(spacing: 3) {
            Text(messageTime(message.timestamp))
                .font(.system(size: 11, weight: .regular))
                .foregroundStyle(.white)

            if outgoing {
                if message.deliveryStatus == .error {
                    errorMenu(for: message)
                } else {
                    mediaDeliveryIndicator(message.deliveryStatus, read: message.isRead)
                }
            }
        }
        .padding(.horizontal, 7)
        .padding(.vertical, 3)
        .background(Color.black.opacity(0.3))
        .clipShape(RoundedRectangle(cornerRadius: 6))
        .padding(.trailing, 6)
        .padding(.bottom, 6)
    }

    // MARK: - Delivery Indicators

    private func deliveryTint(_ status: DeliveryStatus, read: Bool) -> Color {
        if status == .delivered && read { return Color(hex: 0xA4E2FF) }
        switch status {
        case .delivered: return Color.white.opacity(0.5)
        case .error: return RosettaColors.error
        default: return Color.white.opacity(0.78)
        }
    }

    @ViewBuilder
    private func deliveryIndicator(_ status: DeliveryStatus, read: Bool) -> some View {
        if status == .delivered && read {
            DoubleCheckmarkShape()
                .fill(deliveryTint(status, read: read))
                .frame(width: 16, height: 8.7)
        } else {
            switch status {
            case .delivered:
                SingleCheckmarkShape()
                    .fill(deliveryTint(status, read: read))
                    .frame(width: 12, height: 8.8)
            case .waiting:
                Image(systemName: "clock")
                    .font(.system(size: 10, weight: .semibold))
                    .foregroundStyle(deliveryTint(status, read: read))
            case .error:
                Image(systemName: "exclamationmark.circle.fill")
                    .font(.system(size: 10, weight: .semibold))
                    .foregroundStyle(deliveryTint(status, read: read))
            }
        }
    }

    @ViewBuilder
    private func mediaDeliveryIndicator(_ status: DeliveryStatus, read: Bool) -> some View {
        if status == .delivered && read {
            DoubleCheckmarkShape()
                .fill(Color.white)
                .frame(width: 16, height: 8.7)
        } else {
            switch status {
            case .delivered:
                SingleCheckmarkShape()
                    .fill(Color.white.opacity(0.8))
                    .frame(width: 12, height: 8.8)
            case .waiting:
                Image(systemName: "clock")
                    .font(.system(size: 10, weight: .semibold))
                    .foregroundStyle(.white.opacity(0.8))
            case .error:
                Image(systemName: "exclamationmark.circle.fill")
                    .font(.system(size: 10, weight: .semibold))
                    .foregroundStyle(RosettaColors.error)
            }
        }
    }

    @ViewBuilder
    private func errorMenu(for message: ChatMessage) -> some View {
        Menu {
            Button {
                actions.onRetry(message)
            } label: {
                Label("Retry", systemImage: "arrow.clockwise")
            }
            Button(role: .destructive) {
                actions.onRemove(message)
            } label: {
                Label("Remove", systemImage: "trash")
            }
        } label: {
            Image(systemName: "exclamationmark.circle.fill")
                .font(.system(size: 10, weight: .semibold))
                .foregroundStyle(RosettaColors.error)
        }
    }

    // MARK: - Bubble Background

    private var incomingBubbleFill: Color {
        RosettaColors.Adaptive.messageBubble
    }

    @ViewBuilder
    private func bubbleBackground(outgoing: Bool, position: BubblePosition) -> some View {
        let fill = outgoing ? RosettaColors.figmaBlue : incomingBubbleFill
        MessageBubbleShape(position: position, outgoing: outgoing)
            .fill(fill)
    }

    // MARK: - Context Menu

    private func contextMenuItems(for message: ChatMessage) -> [TelegramContextMenuItem] {
        TelegramContextMenuBuilder.menuItems(
            for: message,
            actions: actions,
            isSavedMessages: isSavedMessages,
            isSystemAccount: isSystemAccount
        )
    }

    // MARK: - Reply Quote

    @ViewBuilder
    private func replyQuoteView(reply: ReplyMessageData, outgoing: Bool) -> some View {
        let senderName = senderDisplayName(for: reply.publicKey)
        let previewText: String = {
            let trimmed = reply.message.trimmingCharacters(in: .whitespaces)
            if !trimmed.isEmpty { return EmojiParser.replaceShortcodes(in: reply.message) }
            if reply.attachments.contains(where: { $0.type == AttachmentType.image.rawValue }) { return "Photo" }
            if reply.attachments.contains(where: { $0.type == AttachmentType.messages.rawValue }) { return "Forwarded message" }
            if reply.attachments.contains(where: { $0.type == AttachmentType.file.rawValue }) { return "File" }
            if reply.attachments.contains(where: { $0.type == AttachmentType.avatar.rawValue }) { return "Avatar" }
            if reply.attachments.contains(where: { $0.type == AttachmentType.voice.rawValue }) { return "Voice message" }
            // Android/Desktop parity: show "Call" for call attachments in reply quote
            if reply.attachments.contains(where: { $0.type == AttachmentType.call.rawValue }) { return "Call" }
            return "Attachment"
        }()
        let accentColor = outgoing ? Color.white : RosettaColors.figmaBlue
        let imageAttachment = reply.attachments.first(where: { $0.type == 0 })
        let blurHash: String? = {
            guard let att = imageAttachment, !att.preview.isEmpty else { return nil }
            let hash = AttachmentPreviewCodec.blurHash(from: att.preview)
            return hash.isEmpty ? nil : hash
        }()

        HStack(spacing: 0) {
            RoundedRectangle(cornerRadius: 4)
                .fill(accentColor)
                .frame(width: 3)

            if let att = imageAttachment {
                ReplyQuoteThumbnail(attachment: att, blurHash: blurHash)
                    .padding(.leading, 6)
            }

            VStack(alignment: .leading, spacing: 2) {
                Text(senderName)
                    .font(.system(size: 14, weight: .semibold))
                    .foregroundStyle(outgoing ? Color.white : RosettaColors.figmaBlue)
                    .lineLimit(1)
                Text(previewText)
                    .font(.system(size: 14, weight: .regular))
                    .foregroundStyle(outgoing ? Color.white : RosettaColors.Adaptive.text)
                    .lineLimit(1)
            }
            .padding(.leading, 8)

            Spacer(minLength: 0)
        }
        .padding(.vertical, 3)
        .background(
            RoundedRectangle(cornerRadius: 4)
                .fill(outgoing ? Color.white.opacity(0.12) : RosettaColors.figmaBlue.opacity(0.12))
        )
        .padding(.horizontal, 5)
        .padding(.top, 5)
        .padding(.bottom, 2)
    }

    // MARK: - Forwarded File Preview

    @ViewBuilder
    private func forwardedFilePreview(attachment: ReplyAttachmentData, outgoing: Bool) -> some View {
        let filename: String = {
            let parsed = AttachmentPreviewCodec.parseFilePreview(attachment.preview)
            if !parsed.fileName.isEmpty { return parsed.fileName }
            return attachment.id.isEmpty ? "File" : attachment.id
        }()
        HStack(spacing: 8) {
            Image(systemName: "doc.fill")
                .font(.system(size: 20))
                .foregroundStyle(outgoing ? Color.white.opacity(0.7) : RosettaColors.figmaBlue)
            Text(filename)
                .font(.system(size: 14, weight: .medium))
                .foregroundStyle(outgoing ? Color.white : RosettaColors.Adaptive.text)
                .lineLimit(1)
        }
        .padding(.horizontal, 8)
        .padding(.vertical, 6)
        .background(
            RoundedRectangle(cornerRadius: 8)
                .fill(outgoing ? Color.white.opacity(0.08) : Color.white.opacity(0.06))
        )
    }

    // MARK: - Collage Hit Test

    static func collageAttachmentId(
        at point: CGPoint, attachments: [MessageAttachment], maxWidth: CGFloat
    ) -> String {
        let spacing: CGFloat = 2
        let count = attachments.count
        let x = point.x
        let y = point.y

        switch count {
        case 2:
            let half = (maxWidth - spacing) / 2
            return attachments[x < half ? 0 : 1].id

        case 3:
            let rightWidth = maxWidth * 0.34
            let leftWidth = maxWidth - spacing - rightWidth
            let totalHeight = min(leftWidth * 1.1, 300)
            let rightCellHeight = (totalHeight - spacing) / 2
            if x < leftWidth {
                return attachments[0].id
            } else {
                return attachments[y < rightCellHeight ? 1 : 2].id
            }

        case 4:
            let half = (maxWidth - spacing) / 2
            let cellHeight = min(half * 0.85, 150)
            let row = y < cellHeight ? 0 : 1
            let col = x < half ? 0 : 1
            return attachments[row * 2 + col].id

        case 5:
            let topCellWidth = (maxWidth - spacing) / 2
            let bottomCellWidth = (maxWidth - spacing * 2) / 3
            let topHeight = min(topCellWidth * 0.85, 165)
            if y < topHeight {
                return attachments[x < topCellWidth ? 0 : 1].id
            } else {
                let col = min(Int(x / (bottomCellWidth + spacing)), 2)
                return attachments[2 + col].id
            }

        default:
            return attachments[0].id
        }
    }

    // MARK: - Sender Display Name

    @MainActor private static var senderNameCache: [String: String] = [:]

    private func senderDisplayName(for publicKey: String) -> String {
        if publicKey == currentPublicKey {
            return "You"
        }
        if let cached = Self.senderNameCache[publicKey] {
            return cached
        }
        if publicKey == opponentPublicKey && !opponentTitle.isEmpty {
            Self.senderNameCache[publicKey] = opponentTitle
            return opponentTitle
        }
        if let dialog = DialogRepository.shared.dialogs[publicKey],
           !dialog.opponentTitle.isEmpty {
            Self.senderNameCache[publicKey] = dialog.opponentTitle
            return dialog.opponentTitle
        }
        if publicKey == opponentPublicKey && !opponentUsername.isEmpty {
            let name = "@\(opponentUsername)"
            Self.senderNameCache[publicKey] = name
            return name
        }
        return String(publicKey.prefix(8)) + "…"
    }

    // MARK: - Static Caches

    @MainActor private static var replyBlobCache: [String: [ReplyMessageData]] = [:]

    private func parseReplyBlob(_ blob: String) -> [ReplyMessageData]? {
        guard !blob.isEmpty else { return nil }
        if let cached = Self.replyBlobCache[blob] { return cached }
        guard let data = blob.data(using: .utf8) else { return nil }
        guard let result = try? JSONDecoder().decode([ReplyMessageData].self, from: data) else { return nil }
        if Self.replyBlobCache.count > 300 {
            let keysToRemove = Array(Self.replyBlobCache.keys.prefix(150))
            for key in keysToRemove { Self.replyBlobCache.removeValue(forKey: key) }
        }
        Self.replyBlobCache[blob] = result
        return result
    }

    @MainActor private static var markdownCache: [String: AttributedString] = [:]

    private func parsedMarkdown(_ text: String, outgoing: Bool = false) -> AttributedString {
        let cacheKey = outgoing ? "out:\(text)" : text
        if let cached = Self.markdownCache[cacheKey] {
            PerformanceLogger.shared.track("markdown.cacheHit")
            return cached
        }
        PerformanceLogger.shared.track("markdown.cacheMiss")

        let withEmoji = EmojiParser.replaceShortcodes(in: text)
        var result: AttributedString
        if let parsed = try? AttributedString(
            markdown: withEmoji,
            options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
        ) {
            result = parsed
        } else {
            result = AttributedString(withEmoji)
        }

        // Outgoing links: white on blue bubble (Telegram parity)
        if outgoing {
            for run in result.runs {
                if result[run.range].link != nil {
                    result[run.range].foregroundColor = .white
                }
            }
        }

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

    @MainActor private static var blurHashCache: [String: UIImage] = [:]

    @MainActor
    static func cachedBlurHash(_ hash: String, width: Int, height: Int) -> UIImage? {
        let key = "\(hash)_\(width)x\(height)"
        if let cached = blurHashCache[key] { return cached }
        guard let image = UIImage.fromBlurHash(hash, width: width, height: height) else { return nil }
        if blurHashCache.count > 300 {
            let keysToRemove = Array(blurHashCache.keys.prefix(150))
            for key in keysToRemove { blurHashCache.removeValue(forKey: key) }
        }
        blurHashCache[key] = image
        return image
    }

    @MainActor private static var timeCache: [Int64: String] = [:]

    private func messageTime(_ timestamp: Int64) -> String {
        if let cached = Self.timeCache[timestamp] { return cached }
        let result = Self.timeFormatter.string(
            from: Date(timeIntervalSince1970: Double(timestamp) / 1000)
        )
        if Self.timeCache.count > 500 {
            let keysToRemove = Array(Self.timeCache.keys.prefix(250))
            for key in keysToRemove { Self.timeCache.removeValue(forKey: key) }
        }
        Self.timeCache[timestamp] = result
        return result
    }

    static let timeFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.dateFormat = "HH:mm"
        return formatter
    }()

    // MARK: - Static Helpers

    static func isGarbageText(_ text: String) -> Bool {
        MessageCellLayout.isGarbageOrEncrypted(text)
    }

    static func isValidCaption(_ text: String) -> Bool {
        let cleaned = text.trimmingCharacters(in: .whitespacesAndNewlines)
        if cleaned.isEmpty { return false }
        if text == " " { return false }
        if isGarbageText(text) { return false }
        return true
    }

    static func partitionAttachments(
        _ attachments: [MessageAttachment]
    ) -> (images: [MessageAttachment], others: [MessageAttachment]) {
        var images: [MessageAttachment] = []
        var others: [MessageAttachment] = []
        for att in attachments {
            if att.type == .image { images.append(att) }
            else { others.append(att) }
        }
        return (images, others)
    }
}
