import UIKit
import SwiftUI

// MARK: - ChatListCell

/// UICollectionViewCell with manual frame layout matching Telegram iOS ChatListItemNode.
/// All measurements taken from Telegram source: `ChatListItem.swift` asyncLayout().
///
/// No Auto Layout — all frames computed in `layoutSubviews()` for maximum scroll performance.
final class ChatListCell: UICollectionViewCell {

    // MARK: - Layout Constants (Telegram-exact)

    enum CellLayout {
        static let avatarDiameter: CGFloat = 60
        static let avatarLeftPadding: CGFloat = 10
        static let avatarToTextGap: CGFloat = 8  // visual gap after avatar
        static let contentLeftInset: CGFloat = 80 // avatarLeft(10) + avatar(60) + gap(10)
        static let contentRightInset: CGFloat = 10
        static let contentTopOffset: CGFloat = 8
        static let titleSpacing: CGFloat = -1  // negative, Telegram's titleSpacing
        static let dateYOffset: CGFloat = 2  // relative to contentTop
        static let badgeDiameter: CGFloat = 20
        static let badgeSpacing: CGFloat = 6
        static let badgeBottomInset: CGFloat = 2 // from content bottom
        static let separatorInset: CGFloat = 80
        static let onlineDotSize: CGFloat = 14  // 24% of 60
        static let onlineBorderWidth: CGFloat = 2.5
        static let statusIconSize: CGFloat = 16
        static let itemHeight: CGFloat = 76
    }

    // MARK: - Subviews

    // Avatar
    let avatarBackgroundView = UIView()
    let avatarInitialsLabel = UILabel()
    let avatarImageView = UIImageView()
    let onlineIndicator = UIView()
    private let onlineDotInner = UIView()

    // Group avatar fallback
    let groupIconView = UIImageView()

    // Title row
    let titleLabel = UILabel()
    let verifiedBadge = UIImageView()
    let mutedIconView = UIImageView()

    // Author (group sender name — separate line between title and message)
    let authorLabel = UILabel()

    // Message row
    let messageLabel = UILabel()

    // Typing indicator
    let typingDotsView = TypingDotsView()
    let typingLabel = UILabel()

    // Trailing column
    let dateLabel = UILabel()
    let statusImageView = UIImageView()
    let badgeContainer = UIView()
    let badgeLabel = UILabel()
    let mentionImageView = UIImageView()
    let pinnedIconView = UIImageView()

    // Separator
    let separatorView = UIView()

    // MARK: - State

    private var isPinned = false
    private var wasBadgeVisible = false
    private var wasMentionBadgeVisible = false
    private var isSystemChat = false
    private var isSeparatorFullWidth = false

    // MARK: - Init

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupSubviews()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: - Setup

    private func setupSubviews() {
        backgroundColor = .clear
        contentView.backgroundColor = .clear

        // Avatar background (colored circle for initials)
        avatarBackgroundView.clipsToBounds = true
        avatarBackgroundView.layer.cornerRadius = CellLayout.avatarDiameter / 2
        contentView.addSubview(avatarBackgroundView)

        // Initials label
        avatarInitialsLabel.textAlignment = .center
        avatarInitialsLabel.font = .systemFont(ofSize: CellLayout.avatarDiameter * 0.38, weight: .bold)
        contentView.addSubview(avatarInitialsLabel)

        // Group icon
        groupIconView.contentMode = .center
        groupIconView.tintColor = .white.withAlphaComponent(0.9)
        groupIconView.image = UIImage(systemName: "person.2.fill")?.withConfiguration(
            UIImage.SymbolConfiguration(pointSize: 24, weight: .medium)
        )
        groupIconView.isHidden = true
        contentView.addSubview(groupIconView)

        // Avatar image (photo, on top of background)
        avatarImageView.contentMode = .scaleAspectFill
        avatarImageView.clipsToBounds = true
        avatarImageView.layer.cornerRadius = CellLayout.avatarDiameter / 2
        contentView.addSubview(avatarImageView)

        // Online indicator
        onlineIndicator.isHidden = true
        onlineIndicator.layer.cornerRadius = CellLayout.onlineDotSize / 2
        contentView.addSubview(onlineIndicator)

        onlineDotInner.layer.cornerRadius = (CellLayout.onlineDotSize - CellLayout.onlineBorderWidth * 2) / 2
        onlineDotInner.backgroundColor = UIColor(RosettaColors.primaryBlue)
        onlineIndicator.addSubview(onlineDotInner)

        // Title
        titleLabel.font = .systemFont(ofSize: 16, weight: .medium)
        titleLabel.lineBreakMode = .byTruncatingTail
        contentView.addSubview(titleLabel)

        // Verified badge
        verifiedBadge.contentMode = .scaleAspectFit
        verifiedBadge.isHidden = true
        contentView.addSubview(verifiedBadge)

        // Muted icon
        mutedIconView.contentMode = .scaleAspectFit
        mutedIconView.image = UIImage(systemName: "speaker.slash.fill")?.withConfiguration(
            UIImage.SymbolConfiguration(pointSize: 12, weight: .regular)
        )
        mutedIconView.isHidden = true
        contentView.addSubview(mutedIconView)

        // Author (group sender name on own line)
        authorLabel.font = .systemFont(ofSize: 15, weight: .regular)
        authorLabel.lineBreakMode = .byTruncatingTail
        authorLabel.isHidden = true
        contentView.addSubview(authorLabel)

        // Message
        messageLabel.font = .systemFont(ofSize: 15, weight: .regular)
        messageLabel.numberOfLines = 1
        messageLabel.lineBreakMode = .byTruncatingTail
        contentView.addSubview(messageLabel)

        // Typing indicator (hidden by default)
        typingDotsView.isHidden = true
        contentView.addSubview(typingDotsView)

        typingLabel.font = .systemFont(ofSize: 15, weight: .regular)
        typingLabel.isHidden = true
        contentView.addSubview(typingLabel)

        // Date
        dateLabel.font = .systemFont(ofSize: 14, weight: .regular)
        dateLabel.textAlignment = .right
        contentView.addSubview(dateLabel)

        // Status icon (checkmarks)
        statusImageView.contentMode = .scaleAspectFit
        statusImageView.isHidden = true
        contentView.addSubview(statusImageView)

        // Badge container (capsule)
        badgeContainer.isHidden = true
        badgeContainer.layer.cornerRadius = CellLayout.badgeDiameter / 2
        contentView.addSubview(badgeContainer)

        // Badge label
        badgeLabel.font = .monospacedDigitSystemFont(ofSize: 12, weight: .semibold)
        badgeLabel.textColor = .white
        badgeLabel.textAlignment = .center
        badgeContainer.addSubview(badgeLabel)

        // Mention badge (Telegram-exact: tinted vector icon)
        mentionImageView.image = UIImage(named: "MentionBadgeIcon")?.withRenderingMode(.alwaysTemplate)
        mentionImageView.contentMode = .scaleAspectFit
        mentionImageView.isHidden = true
        contentView.addSubview(mentionImageView)

        // Pin icon
        pinnedIconView.contentMode = .scaleAspectFit
        let fallbackPinImage = UIImage(systemName: "pin.fill")?.withConfiguration(
            UIImage.SymbolConfiguration(pointSize: 13, weight: .regular)
        )
        pinnedIconView.image = (UIImage(named: "PeerPinnedIcon") ?? fallbackPinImage)?
            .withRenderingMode(.alwaysTemplate)
        pinnedIconView.isHidden = true
        contentView.addSubview(pinnedIconView)

        // Separator
        separatorView.isUserInteractionEnabled = false
        contentView.addSubview(separatorView)
    }

    // MARK: - Layout (manual frame calculation)

    override func layoutSubviews() {
        super.layoutSubviews()
        let w = contentView.bounds.width
        let h = contentView.bounds.height
        let scale = UIScreen.main.scale

        // ── Avatar ──
        let avatarY = floor((h - CellLayout.avatarDiameter) / 2)
        let avatarFrame = CGRect(
            x: CellLayout.avatarLeftPadding,
            y: avatarY,
            width: CellLayout.avatarDiameter,
            height: CellLayout.avatarDiameter
        )
        avatarBackgroundView.frame = avatarFrame
        avatarInitialsLabel.frame = avatarFrame
        avatarImageView.frame = avatarFrame
        groupIconView.frame = avatarFrame

        // Online indicator (bottom-right of avatar)
        let onlineX = avatarFrame.maxX - CellLayout.onlineDotSize + 1
        let onlineY = avatarFrame.maxY - CellLayout.onlineDotSize + 1
        onlineIndicator.frame = CGRect(
            x: onlineX, y: onlineY,
            width: CellLayout.onlineDotSize, height: CellLayout.onlineDotSize
        )
        onlineDotInner.frame = onlineIndicator.bounds.insetBy(
            dx: CellLayout.onlineBorderWidth, dy: CellLayout.onlineBorderWidth
        )

        // ── Content area ──
        let contentLeft = CellLayout.contentLeftInset
        let contentRight = w - CellLayout.contentRightInset
        let contentTop = CellLayout.contentTopOffset

        // ── Top row: [title + icons ...] [status] [date] ──

        // Date — measure first (determines title max width)
        let dateSize = dateLabel.sizeThatFits(CGSize(width: 120, height: 20))
        let dateX = contentRight - dateSize.width
        let dateY = contentTop + CellLayout.dateYOffset
        dateLabel.frame = CGRect(x: dateX, y: dateY, width: ceil(dateSize.width), height: ceil(dateSize.height))

        // Status icon — left of date
        var titleRightBound = dateX - 6 // gap between title area and date
        if !statusImageView.isHidden {
            let statusW: CGFloat = CellLayout.statusIconSize
            let statusH: CGFloat = CellLayout.statusIconSize
            let statusX = dateX - statusW - 2
            let statusY = dateY + floor((dateSize.height - statusH) / 2)
            statusImageView.frame = CGRect(x: statusX, y: statusY, width: statusW, height: statusH)
            titleRightBound = statusX - 4
        }

        // Title — measure within available width
        let titleAvailableWidth = titleRightBound - contentLeft
        // Account for verified (16+3) and muted (14+3) if visible
        var titleIconsWidth: CGFloat = 0
        if !verifiedBadge.isHidden { titleIconsWidth += 16 + 3 }
        if !mutedIconView.isHidden { titleIconsWidth += 14 + 3 }
        let titleMaxWidth = max(0, titleAvailableWidth - titleIconsWidth)

        let titleSize = titleLabel.sizeThatFits(CGSize(width: titleMaxWidth, height: 22))
        let screenPixelTitle = 1.0 / scale
        titleLabel.frame = CGRect(
            x: contentLeft, y: contentTop + screenPixelTitle,
            width: min(ceil(titleSize.width), titleMaxWidth), height: ceil(titleSize.height)
        )

        // Verified badge — right of title text
        var iconX = titleLabel.frame.maxX + 3
        if !verifiedBadge.isHidden {
            let s: CGFloat = 16
            verifiedBadge.frame = CGRect(
                x: iconX,
                y: contentTop + floor((titleSize.height - s) / 2),
                width: s, height: s
            )
            iconX = verifiedBadge.frame.maxX + 3
        }

        // Muted icon
        if !mutedIconView.isHidden {
            let s: CGFloat = 14
            mutedIconView.frame = CGRect(
                x: iconX,
                y: contentTop + floor((titleSize.height - s) / 2),
                width: s, height: s
            )
        }

        // ── Bottom row: [message ...] [badge/pin] ──

        // Badges — positioned at bottom of content area
        var badgeRightEdge = contentRight
        // Telegram: badges aligned with content bottom - 2pt inset
        let badgeY = h - CellLayout.badgeDiameter - 10

        if !badgeContainer.isHidden {
            let textSize = badgeLabel.sizeThatFits(CGSize(width: 100, height: CellLayout.badgeDiameter))
            let badgeW = max(CellLayout.badgeDiameter, ceil(textSize.width) + 10)
            badgeContainer.frame = CGRect(
                x: badgeRightEdge - badgeW, y: badgeY,
                width: badgeW, height: CellLayout.badgeDiameter
            )
            badgeContainer.layer.cornerRadius = CellLayout.badgeDiameter / 2
            badgeLabel.frame = badgeContainer.bounds
            badgeRightEdge = badgeContainer.frame.minX - CellLayout.badgeSpacing
        }

        if !mentionImageView.isHidden {
            mentionImageView.frame = CGRect(
                x: badgeRightEdge - CellLayout.badgeDiameter, y: badgeY,
                width: CellLayout.badgeDiameter, height: CellLayout.badgeDiameter
            )
            badgeRightEdge = mentionImageView.frame.minX - CellLayout.badgeSpacing
        }

        if !pinnedIconView.isHidden {
            let pinSize = pinnedIconView.image?.size ?? CGSize(width: 20, height: 20)
            let contentRectMaxY = h - 13.0
            pinnedIconView.frame = CGRect(
                x: badgeRightEdge - pinSize.width,
                y: contentRectMaxY - pinSize.height - 2.0,
                width: pinSize.width,
                height: pinSize.height
            )
            badgeRightEdge = pinnedIconView.frame.minX - CellLayout.badgeSpacing
        }

        // ── Author + Message — fixed Y positions from Telegram screenshot ──
        // Measured from Telegram iOS at 76pt cell height, 16pt title, 15pt text:
        //   1:1:   title ~8pt, message ~27pt
        //   Group: title ~8pt, author ~27pt, message ~45pt
        let textLeft = contentLeft - 1
        let messageMaxW = badgeRightEdge - contentLeft

        if !authorLabel.isHidden {
            let authorSize = authorLabel.sizeThatFits(CGSize(width: messageMaxW, height: 22))
            authorLabel.frame = CGRect(
                x: textLeft, y: 30,
                width: min(ceil(authorSize.width), messageMaxW),
                height: 20
            )
            messageLabel.frame = CGRect(
                x: textLeft, y: 50,
                width: max(0, messageMaxW), height: 20
            )
        } else {
            authorLabel.frame = .zero
            // System chats (Updates/Safe): keep preview visually farther from title.
            let messageY: CGFloat = isSystemChat ? 29 : 21
            messageLabel.frame = CGRect(
                x: textLeft, y: messageY,
                width: max(0, messageMaxW), height: 38
            )
        }

        // ── Typing indicator ──
        // Y=30 so visual center matches 2-line message visual center (~40pt).
        // Dots (16pt) centered within text height (20pt) → Y+2.
        if !typingDotsView.isHidden {
            let dotsW: CGFloat = 24
            let dotsH: CGFloat = 16
            let typingY: CGFloat = 30
            typingLabel.frame = CGRect(x: textLeft + dotsW - 2, y: typingY, width: max(0, messageMaxW - dotsW + 2), height: 20)
            typingDotsView.frame = CGRect(x: textLeft, y: typingY + 2, width: dotsW, height: dotsH)
        }

        // ── Separator ──
        let separatorHeight = 1.0 / scale
        let separatorX = isSeparatorFullWidth ? 0 : CellLayout.separatorInset
        separatorView.frame = CGRect(
            x: separatorX,
            y: h - separatorHeight,
            width: w - separatorX,
            height: separatorHeight
        )
    }

    // MARK: - Configuration

    /// Message text cache (shared across cells, avoids regex per configure).
    private static var messageTextCache: [String: String] = [:]

    // MARK: - Dynamic Theme Colors (auto-resolve on trait change)

    private static let dynamicTitleColor = UIColor { $0.userInterfaceStyle == .dark ? .white : .black }
    private static let dynamicSeparatorColor = UIColor { traits in
        traits.userInterfaceStyle == .dark
            ? UIColor(red: 0x54/255, green: 0x54/255, blue: 0x58/255, alpha: 0.55)
            : UIColor(red: 0xC8/255, green: 0xC7/255, blue: 0xCC/255, alpha: 1)
    }
    private static let dynamicMutedBadgeBg = UIColor { traits in
        traits.userInterfaceStyle == .dark
            ? UIColor(red: 0x66/255, green: 0x66/255, blue: 0x66/255, alpha: 1)
            : UIColor(red: 0xB6/255, green: 0xB6/255, blue: 0xBB/255, alpha: 1)
    }
    private static let dynamicPinnedBg = UIColor { traits in
        traits.userInterfaceStyle == .dark
            ? UIColor(red: 0x1C/255, green: 0x1C/255, blue: 0x1D/255, alpha: 1)
            : UIColor(red: 0xF7/255, green: 0xF7/255, blue: 0xF7/255, alpha: 1)
    }
    private static let dynamicCellBg = UIColor { $0.userInterfaceStyle == .dark ? .black : .white }

    func configure(with dialog: Dialog, isSyncing: Bool, typingUsers: Set<String>? = nil) {
        let isDark = traitCollection.userInterfaceStyle == .dark
        isPinned = dialog.isPinned
        isSystemChat = isSystemDialog(dialog)

        // Colors — use dynamic UIColors for automatic theme adaptation
        let titleColor = Self.dynamicTitleColor
        let secondaryColor = UIColor(red: 0x8E/255, green: 0x8E/255, blue: 0x93/255, alpha: 1)
        let accentBlue = UIColor(RosettaColors.figmaBlue)
        let mutedBadgeBg = Self.dynamicMutedBadgeBg
        let separatorColor = Self.dynamicSeparatorColor
        let pinnedBg = Self.dynamicPinnedBg

        // Background — pinned section uses decoration view, cells always clear
        contentView.backgroundColor = .clear

        // Online indicator background matches section bg
        let cellBg = dialog.isPinned ? pinnedBg : Self.dynamicCellBg
        onlineIndicator.backgroundColor = cellBg

        // Separator
        separatorView.backgroundColor = separatorColor

        // Avatar
        configureAvatar(dialog: dialog, isDark: isDark)

        // Online
        onlineIndicator.isHidden = !dialog.isOnline || dialog.isSavedMessages

        // Title
        titleLabel.text = displayTitle(for: dialog)
        titleLabel.textColor = titleColor

        // Verified
        configureVerified(dialog: dialog)

        // Muted
        mutedIconView.isHidden = !dialog.isMuted
        mutedIconView.tintColor = secondaryColor

        // Message text or typing indicator
        let activeTyping = typingUsers.flatMap { $0.isEmpty ? nil : $0 }
        if let typers = activeTyping {
            configureTypingIndicator(dialog: dialog, typingUsers: typers, color: secondaryColor)
        } else {
            hideTypingIndicator()
            configureMessageText(dialog: dialog, secondaryColor: secondaryColor, titleColor: titleColor)
        }

        // Date
        dateLabel.text = formatTime(dialog.lastMessageTimestamp)
        dateLabel.textColor = secondaryColor

        // Delivery status
        configureDeliveryStatus(dialog: dialog, secondaryColor: secondaryColor, accentBlue: accentBlue)

        // Badge
        configureBadge(dialog: dialog, isSyncing: isSyncing, accentBlue: accentBlue, mutedBadgeBg: mutedBadgeBg)

        // Pin
        pinnedIconView.isHidden = !(dialog.isPinned && dialog.unreadCount == 0)
        pinnedIconView.tintColor = UIColor { traits in
            traits.userInterfaceStyle == .dark
                ? UIColor(red: 0x76/255, green: 0x76/255, blue: 0x77/255, alpha: 1)
                : UIColor(red: 0xB6/255, green: 0xB6/255, blue: 0xBB/255, alpha: 1)
        }

        setNeedsLayout()
    }

    // MARK: - Avatar Configuration

    private static let mantineDarkBody = UIColor(red: 0x1A/255, green: 0x1B/255, blue: 0x1E/255, alpha: 1)

    private func configureAvatar(dialog: Dialog, isDark: Bool) {
        let colorPair = RosettaColors.avatarColors[dialog.avatarColorIndex % RosettaColors.avatarColors.count]
        let image = dialog.isSavedMessages ? nil : AvatarRepository.shared.loadAvatar(publicKey: dialog.opponentKey)

        // Reset visibility
        avatarBackgroundView.isHidden = false
        avatarImageView.isHidden = true
        avatarInitialsLabel.isHidden = true
        groupIconView.isHidden = true

        if dialog.isSavedMessages {
            avatarBackgroundView.backgroundColor = UIColor(RosettaColors.primaryBlue)
            groupIconView.isHidden = false
            groupIconView.image = UIImage(systemName: "bookmark.fill")?.withConfiguration(
                UIImage.SymbolConfiguration(pointSize: CellLayout.avatarDiameter * 0.38, weight: .semibold)
            )
            groupIconView.tintColor = .white
        } else if let image {
            avatarImageView.image = image
            avatarImageView.isHidden = false
            avatarBackgroundView.isHidden = true
        } else {
            // Initials — Mantine "light" variant (dynamic for instant theme switch)
            let tintUIColor = UIColor(colorPair.tint)
            let textUIColor = UIColor(colorPair.text)
            avatarBackgroundView.backgroundColor = UIColor { [weak self] traits in
                let dark = traits.userInterfaceStyle == .dark
                let base: UIColor = dark ? Self.mantineDarkBody : .white
                return base.blended(with: tintUIColor, alpha: dark ? 0.15 : 0.10)
            }
            avatarInitialsLabel.isHidden = false
            avatarInitialsLabel.text = dialog.initials
            avatarInitialsLabel.font = .systemFont(
                ofSize: CellLayout.avatarDiameter * 0.38, weight: .bold
            ).rounded()
            avatarInitialsLabel.textColor = UIColor { traits in
                traits.userInterfaceStyle == .dark ? textUIColor : tintUIColor
            }
            if dialog.isGroup {
                avatarInitialsLabel.text = dialog.initials
            }
        }
    }

    // MARK: - Verified Badge

    /// Cached verified badge images (rendered once from SVG paths).
    private static var verifiedImageCache: [Int: UIImage] = [:]

    private static func verifiedImage(level: Int, size: CGFloat) -> UIImage {
        if let cached = verifiedImageCache[level] { return cached }
        let pathData: String
        switch level {
        case 2: pathData = TablerIconPath.shieldCheckFilled
        case 3...: pathData = TablerIconPath.arrowBadgeDownFilled
        default: pathData = TablerIconPath.rosetteDiscountCheckFilled
        }
        let image = renderSVGPath(pathData, viewBox: CGSize(width: 24, height: 24), size: CGSize(width: size, height: size))
        verifiedImageCache[level] = image
        return image
    }

    private func configureVerified(dialog: Dialog) {
        let level = dialog.effectiveVerified
        guard level > 0 && !dialog.isSavedMessages else {
            verifiedBadge.isHidden = true
            return
        }
        verifiedBadge.isHidden = false
        verifiedBadge.image = Self.verifiedImage(level: level, size: 16)
            .withRenderingMode(.alwaysTemplate)
        switch level {
        case 1:
            verifiedBadge.tintColor = UIColor(RosettaColors.primaryBlue)
        case 2:
            verifiedBadge.tintColor = UIColor(RosettaColors.success)
        default:
            verifiedBadge.tintColor = UIColor(red: 1, green: 215/255, blue: 0, alpha: 1)
        }
    }

    // MARK: - Delivery Status

    /// Cached checkmark images (rendered once from SwiftUI Shapes).
    private static let singleCheckImage: UIImage = {
        // Match double checkmark canvas (17×9.3) so both scale
        // identically in the 16×16 statusImageView (scaleAspectFit).
        let canvas = CGSize(width: 17, height: 9.3)
        let checkW = 16.4 * (9.3 / 12.0)
        let renderer = UIGraphicsImageRenderer(size: canvas)
        return renderer.image { ctx in
            ctx.cgContext.translateBy(x: canvas.width - checkW, y: 0)
            let path = SingleCheckmarkShape().path(
                in: CGRect(x: 0, y: 0, width: checkW, height: canvas.height)
            )
            ctx.cgContext.addPath(path.cgPath)
            ctx.cgContext.setFillColor(UIColor.black.cgColor)
            ctx.cgContext.fillPath()
        }
    }()
    private static let doubleCheckImage: UIImage = StatusIconRenderer.renderShape(DoubleCheckmarkShape(), size: CGSize(width: 17, height: 9.3))

    /// Cached error indicator (Telegram: red circle with white exclamation).
    private static let errorImage: UIImage = {
        let size = CGSize(width: 16, height: 16)
        let renderer = UIGraphicsImageRenderer(size: size)
        return renderer.image { ctx in
            // Red circle
            let circlePath = UIBezierPath(ovalIn: CGRect(origin: .zero, size: size))
            UIColor(red: 0xFF/255, green: 0x3B/255, blue: 0x30/255, alpha: 1).setFill()
            circlePath.fill()
            // White exclamation mark
            let lineWidth: CGFloat = 1.8
            let topY: CGFloat = 3.5
            let bottomY: CGFloat = 10
            let dotY: CGFloat = 12.5
            let centerX = size.width / 2
            // Stem
            let stem = UIBezierPath()
            stem.move(to: CGPoint(x: centerX, y: topY))
            stem.addLine(to: CGPoint(x: centerX, y: bottomY))
            stem.lineWidth = lineWidth
            stem.lineCapStyle = .round
            UIColor.white.setStroke()
            stem.stroke()
            // Dot
            let dotSize: CGFloat = 2.0
            let dotRect = CGRect(x: centerX - dotSize/2, y: dotY, width: dotSize, height: dotSize)
            let dot = UIBezierPath(ovalIn: dotRect)
            UIColor.white.setFill()
            dot.fill()
        }
    }()

    private func configureDeliveryStatus(dialog: Dialog, secondaryColor: UIColor, accentBlue: UIColor) {
        guard dialog.lastMessageFromMe && !dialog.isSavedMessages else {
            statusImageView.isHidden = true
            return
        }

        if dialog.lastMessageDelivered == .delivered && dialog.lastMessageRead {
            // Read — blue double checkmarks
            statusImageView.isHidden = false
            statusImageView.image = Self.doubleCheckImage.withRenderingMode(.alwaysTemplate)
            statusImageView.tintColor = accentBlue
        } else if dialog.lastMessageDelivered == .error {
            // Error — red indicator
            statusImageView.isHidden = false
            statusImageView.image = Self.errorImage
            statusImageView.tintColor = nil
        } else if dialog.lastMessageDelivered == .delivered {
            // Delivered but not read — gray single checkmark
            statusImageView.isHidden = false
            statusImageView.image = Self.singleCheckImage.withRenderingMode(.alwaysTemplate)
            statusImageView.tintColor = secondaryColor
        } else {
            // Waiting — hide (no server ACK yet)
            statusImageView.isHidden = true
        }
    }

    // MARK: - Badge

    private func configureBadge(dialog: Dialog, isSyncing: Bool, accentBlue: UIColor, mutedBadgeBg: UIColor) {
        let count = dialog.unreadCount
        // Telegram: when mention + only 1 unread → show only @ badge, no count
        let showMention = dialog.hasMention && count > 0 && !isSyncing
        let showBadge = count > 0 && !isSyncing && !(showMention && count == 1)

        if showBadge {
            let text: String
            if count > 999 { text = "\(count / 1000)K" }
            else if count > 99 { text = "99+" }
            else { text = "\(count)" }
            badgeLabel.text = text
            badgeContainer.backgroundColor = dialog.isMuted ? mutedBadgeBg : accentBlue
        }

        // Animate badge appear/disappear (Telegram: scale spring)
        animateBadgeTransition(view: badgeContainer, shouldShow: showBadge, wasVisible: &wasBadgeVisible)

        // Mention badge (Telegram: tinted vector icon)
        if showMention {
            mentionImageView.tintColor = dialog.isMuted ? mutedBadgeBg : accentBlue
        }
        animateBadgeTransition(view: mentionImageView, shouldShow: showMention, wasVisible: &wasMentionBadgeVisible)
    }

    /// Telegram badge animation: appear = CASpringAnimation pop-in; disappear = scale down.
    /// Uses Core Animation (not UIView.animate) so animations work inside performWithoutAnimation.
    private func animateBadgeTransition(view: UIView, shouldShow: Bool, wasVisible: inout Bool) {
        let wasShowing = wasVisible
        wasVisible = shouldShow

        // PERF: Cancel any in-flight animation to prevent stacking during rapid reconfigure
        view.layer.removeAllAnimations()

        if shouldShow && !wasShowing {
            // Appear: spring pop-in (CASpringAnimation bypasses performWithoutAnimation)
            view.isHidden = false
            view.transform = .identity

            let spring = CASpringAnimation(keyPath: "transform.scale")
            spring.fromValue = 0.0001
            spring.toValue = 1.0
            spring.mass = 1.0
            spring.stiffness = 400
            spring.damping = 22
            spring.duration = spring.settlingDuration
            view.layer.add(spring, forKey: "badgePop")
        } else if !shouldShow && wasShowing {
            // Disappear: quick scale down
            let anim = CABasicAnimation(keyPath: "transform.scale")
            anim.fromValue = 1.0
            anim.toValue = 0.0001
            anim.duration = 0.12
            anim.timingFunction = CAMediaTimingFunction(name: .easeIn)
            anim.fillMode = .forwards
            anim.isRemovedOnCompletion = false

            CATransaction.begin()
            CATransaction.setCompletionBlock { [weak view] in
                view?.isHidden = true
                view?.transform = .identity
                view?.layer.removeAnimation(forKey: "badgeHide")
            }
            view.layer.add(anim, forKey: "badgeHide")
            CATransaction.commit()
        } else {
            // No transition — just set visibility
            view.isHidden = !shouldShow
            view.transform = .identity
        }
    }

    // MARK: - Message Text

    private func configureMessageText(dialog: Dialog, secondaryColor: UIColor, titleColor: UIColor) {
        let text = resolveMessageText(dialog: dialog)

        // Group chats: sender name on SEPARATE LINE (Telegram layout)
        // authorNameColor = white (dark) / black (light)
        let showAuthor = dialog.isGroup && !dialog.isSavedMessages
            && !dialog.lastMessageSenderKey.isEmpty && !text.isEmpty
        if showAuthor {
            let senderName: String
            if dialog.lastMessageFromMe {
                senderName = "You"
            } else {
                let lookup = DialogRepository.shared.dialogs[dialog.lastMessageSenderKey]
                senderName = lookup?.opponentTitle.isEmpty == false
                    ? lookup!.opponentTitle
                    : String(dialog.lastMessageSenderKey.prefix(8))
            }
            authorLabel.isHidden = false
            authorLabel.text = senderName
            authorLabel.textColor = titleColor
            messageLabel.numberOfLines = 1 // always single-line subtitle
        } else {
            authorLabel.isHidden = true
            messageLabel.numberOfLines = 1
        }

        messageLabel.attributedText = nil
        messageLabel.text = text
        messageLabel.textColor = secondaryColor
    }

    private func isSystemDialog(_ dialog: Dialog) -> Bool {
        if SystemAccounts.isSystemAccount(dialog.opponentKey) { return true }
        if dialog.opponentTitle.caseInsensitiveCompare(SystemAccounts.updatesTitle) == .orderedSame { return true }
        if dialog.opponentTitle.caseInsensitiveCompare(SystemAccounts.safeTitle) == .orderedSame { return true }
        if dialog.opponentUsername.caseInsensitiveCompare("updates") == .orderedSame { return true }
        if dialog.opponentUsername.caseInsensitiveCompare("safe") == .orderedSame { return true }
        return false
    }

    // MARK: - Typing Indicator

    private func configureTypingIndicator(dialog: Dialog, typingUsers: Set<String>, color: UIColor) {
        // Hide normal message content
        messageLabel.isHidden = true
        authorLabel.isHidden = true

        // Show typing
        typingDotsView.isHidden = false
        typingDotsView.dotColor = color
        typingDotsView.startAnimating()

        typingLabel.isHidden = false
        typingLabel.textColor = color

        if dialog.isGroup {
            let names = typingUsers.prefix(2).map { key -> String in
                if let d = DialogRepository.shared.dialogs[key], !d.opponentTitle.isEmpty {
                    return d.opponentTitle
                }
                return String(key.prefix(8))
            }
            if typingUsers.count == 1 {
                typingLabel.text = "\(names[0]) typing"
            } else if typingUsers.count == 2 {
                typingLabel.text = "\(names[0]), \(names[1]) typing"
            } else {
                typingLabel.text = "\(names[0]) and \(typingUsers.count - 1) others typing"
            }
        } else {
            typingLabel.text = "typing"
        }
    }

    private func hideTypingIndicator() {
        typingDotsView.stopAnimating()
        typingDotsView.isHidden = true
        typingLabel.isHidden = true
        messageLabel.isHidden = false
    }

    // MARK: - Message Text Resolve

    private func resolveMessageText(dialog: Dialog) -> String {
        let raw = dialog.lastMessage.trimmingCharacters(in: .whitespacesAndNewlines)
        if raw.isEmpty { return "No messages yet" }
        if raw.hasPrefix("#group:") { return "Group invite" }
        if Self.looksLikeCiphertext(raw) { return "No messages yet" }

        if let cached = Self.messageTextCache[dialog.lastMessage] { return cached }

        let cleaned = dialog.lastMessage.replacingOccurrences(of: "**", with: "")
        let result = EmojiParser.replaceShortcodes(in: cleaned)
        if Self.messageTextCache.count > 500 {
            let keys = Array(Self.messageTextCache.keys.prefix(250))
            for key in keys { Self.messageTextCache.removeValue(forKey: key) }
        }
        Self.messageTextCache[dialog.lastMessage] = result
        return result
    }

    private static func looksLikeCiphertext(_ text: String) -> Bool {
        if text.hasPrefix("CHNK:") { return true }
        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 }
        }
        if text.count >= 40 {
            let hex = CharacterSet(charactersIn: "0123456789abcdefABCDEF")
            if text.unicodeScalars.allSatisfy({ hex.contains($0) }) { return true }
        }
        return false
    }

    // MARK: - Display Title

    private func displayTitle(for dialog: Dialog) -> 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))
    }

    // MARK: - Time Formatting

    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
    }()
    private static var timeStringCache: [Int64: String] = [:]

    private func formatTime(_ timestamp: Int64) -> String {
        guard timestamp > 0 else { return "" }
        if let cached = Self.timeStringCache[timestamp] { return cached }

        let date = Date(timeIntervalSince1970: Double(timestamp) / 1000)
        let now = Date()
        let cal = Calendar.current

        let result: String
        if cal.isDateInToday(date) {
            result = Self.timeFormatter.string(from: date)
        } else if cal.isDateInYesterday(date) {
            result = "Yesterday"
        } else if let days = cal.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 keys = Array(Self.timeStringCache.keys.prefix(250))
            for key in keys { Self.timeStringCache.removeValue(forKey: key) }
        }
        Self.timeStringCache[timestamp] = result
        return result
    }

    // MARK: - Reuse

    override func prepareForReuse() {
        super.prepareForReuse()
        avatarImageView.image = nil
        avatarImageView.isHidden = true
        avatarBackgroundView.isHidden = false
        avatarInitialsLabel.isHidden = false
        groupIconView.isHidden = true
        verifiedBadge.isHidden = true
        mutedIconView.isHidden = true
        statusImageView.isHidden = true
        badgeContainer.isHidden = true
        mentionImageView.isHidden = true
        pinnedIconView.isHidden = true
        onlineIndicator.isHidden = true
        contentView.backgroundColor = .clear
        messageLabel.attributedText = nil
        messageLabel.numberOfLines = 1
        authorLabel.isHidden = true
        // Typing indicator
        typingDotsView.stopAnimating()
        typingDotsView.isHidden = true
        typingLabel.isHidden = true
        // PERF: Cancel in-flight badge animations to prevent blue flash during rapid reuse
        badgeContainer.layer.removeAllAnimations()
        mentionImageView.layer.removeAllAnimations()
        statusImageView.layer.removeAllAnimations()
        // Badge animation state
        wasBadgeVisible = false
        wasMentionBadgeVisible = false
        badgeContainer.transform = .identity
        mentionImageView.transform = .identity
        isSystemChat = false
        isSeparatorFullWidth = false
    }

    // MARK: - Highlight

    override var isHighlighted: Bool {
        didSet {
            let isDark = traitCollection.userInterfaceStyle == .dark
            // Telegram-exact: #E5E5EA light, #121212 dark (normal); #E5E5EA/#2B2B2C (pinned)
            let highlightBg: UIColor
            if isPinned {
                highlightBg = isDark
                    ? UIColor(red: 0x2B/255, green: 0x2B/255, blue: 0x2C/255, alpha: 1)
                    : UIColor(red: 0xE5/255, green: 0xE5/255, blue: 0xEA/255, alpha: 1)
            } else {
                highlightBg = isDark
                    ? UIColor(red: 0x12/255, green: 0x12/255, blue: 0x12/255, alpha: 1)
                    : UIColor(red: 0xE5/255, green: 0xE5/255, blue: 0xEA/255, alpha: 1)
            }
            if isHighlighted {
                contentView.backgroundColor = highlightBg
            } else {
                // Telegram: 0.3s delay + 0.7s ease-out fade
                // Pinned section background is handled by decoration view, so always fade to clear
                UIView.animate(withDuration: 0.7, delay: 0.3, options: .curveEaseOut) {
                    self.contentView.backgroundColor = .clear
                }
            }
        }
    }

    // MARK: - Separator Visibility

    func setSeparatorHidden(_ hidden: Bool) {
        setSeparatorStyle(hidden: hidden, fullWidth: false)
    }

    func setSeparatorStyle(hidden: Bool, fullWidth: Bool) {
        separatorView.isHidden = hidden
        isSeparatorFullWidth = fullWidth
        setNeedsLayout()
    }
}

// MARK: - UIFont Rounded Helper

private extension UIFont {
    func rounded() -> UIFont {
        guard let descriptor = fontDescriptor.withDesign(.rounded) else { return self }
        return UIFont(descriptor: descriptor, size: 0)
    }
}

// MARK: - UIColor Blending Helper

extension UIColor {
    func blended(with color: UIColor, alpha: CGFloat) -> UIColor {
        var r1: CGFloat = 0, g1: CGFloat = 0, b1: CGFloat = 0, a1: CGFloat = 0
        var r2: CGFloat = 0, g2: CGFloat = 0, b2: CGFloat = 0, a2: CGFloat = 0
        getRed(&r1, green: &g1, blue: &b1, alpha: &a1)
        color.getRed(&r2, green: &g2, blue: &b2, alpha: &a2)
        return UIColor(
            red: r1 * (1 - alpha) + r2 * alpha,
            green: g1 * (1 - alpha) + g2 * alpha,
            blue: b1 * (1 - alpha) + b2 * alpha,
            alpha: 1
        )
    }
}

// MARK: - Shape → UIImage Rendering

/// Renders an SVG path string into a UIImage (used for verified badges).
private func renderSVGPath(_ pathData: String, viewBox: CGSize, size: CGSize) -> UIImage {
    let renderer = UIGraphicsImageRenderer(size: size)
    return renderer.image { ctx in
        let scale = CGAffineTransform(
            scaleX: size.width / viewBox.width,
            y: size.height / viewBox.height
        )
        let svgPath = SVGPathShape(pathData: pathData, viewBox: viewBox)
        let swiftUIPath = svgPath.path(in: CGRect(origin: .zero, size: size))
        ctx.cgContext.addPath(swiftUIPath.cgPath)
        ctx.cgContext.setFillColor(UIColor.black.cgColor)
        ctx.cgContext.fillPath()
    }
}
