import UIKit
import QuartzCore
import os

// MARK: - RosettaListView

/// Custom high-performance list view for chat messages.
/// Telegram-parity replacement for UICollectionView.
///
/// Key differences from UICollectionView:
/// - Manual node management (addSubview/removeFromSuperview)
/// - Frame-based positioning (no autolayout)
/// - Async layout calculation (non-blocking main thread)
/// - Visibility culling (only ~30 nodes in memory)
/// - Inverted scroll (newest messages at bottom)
/// - CADisplayLink-driven animations
///
/// Telegram source: Display/Source/ListView.swift (5358 lines)
@MainActor
final class RosettaListView: UIView, UIScrollViewDelegate {

    private static let logger = Logger(subsystem: "com.rosetta.messenger", category: "ListView")

    // MARK: - Configuration

    /// Preload buffer above/below viewport. Nodes outside this range are culled.
    /// Telegram: `invisibleInset = 500.0` (ListView.swift:223).
    var invisibleInset: CGFloat = 500.0

    /// Stack items from bottom (chat mode). Newest items at visual bottom.
    /// Telegram: `stackFromBottom` (ListView.swift:240).
    var stackFromBottom: Bool = true

    /// Virtual scroll height. Large enough to scroll freely in both directions.
    /// Telegram: `infiniteScrollSize = 10000.0`.
    private let infiniteScrollSize: CGFloat = 10000.0

    // MARK: - Callbacks

    /// Called when visible item range changes (for pagination triggers).
    var displayedItemRangeChanged: ((_ range: ClosedRange<Int>?) -> Void)?

    /// Called when scroll position changes.
    var onScroll: ((_ offset: CGFloat, _ isDragging: Bool) -> Void)?

    /// Called when user taps empty area.
    var onTapBackground: (() -> Void)?

    // MARK: - State

    /// Data source items.
    private(set) var items: [RosettaListItem] = []

    /// Currently displayed nodes (ordered by visual position, bottom to top in inverted mode).
    private(set) var visibleNodes: [RosettaListNode] = []

    /// Node recycling pool keyed by reuse identifier.
    private var nodePool: [String: [RosettaListNode]] = [:]

    /// Maps item ID → index for O(1) lookup.
    private var itemIdToIndex: [String: Int] = [:]

    /// Transaction queue for serialized updates.
    let transactionQueue = RosettaTransactionQueue()

    /// Layout params (computed from view width).
    private var layoutParams: RosettaListLayoutParams {
        RosettaListLayoutParams(
            width: bounds.width,
            leftInset: safeAreaInsets.left,
            rightInset: safeAreaInsets.right
        )
    }

    // MARK: - Insets

    /// Content insets (keyboard, composer, etc.).
    var contentInsets: UIEdgeInsets = .zero {
        didSet {
            guard contentInsets != oldValue else { return }
            let delta = contentInsets.bottom - oldValue.bottom
            scroller.contentInset = contentInsets

            // Compensate offset for bottom inset changes (keyboard)
            if delta > 0, !scroller.isDragging, !scroller.isDecelerating {
                var offset = scroller.contentOffset
                offset.y += delta
                scroller.contentOffset = offset
            }
        }
    }

    // MARK: - Subviews

    /// The scroll view that handles touch/scroll physics.
    private let scroller: UIScrollView = {
        let sv = UIScrollView()
        sv.scrollsToTop = false
        sv.delaysContentTouches = false
        sv.canCancelContentTouches = true
        sv.showsVerticalScrollIndicator = true
        sv.showsHorizontalScrollIndicator = false
        sv.alwaysBounceVertical = true
        return sv
    }()

    /// Container inside scroller for node views.
    private let nodeContainer = UIView()

    // MARK: - Display Link

    private var displayLink: CADisplayLink?
    private var needsAnimationUpdate = false

    // MARK: - Scroll Tracking

    /// Anchor point for maintaining scroll position during layout changes.
    private var anchorItemId: String?
    private var anchorOffset: CGFloat = 0

    /// Whether initial content has been laid out.
    private var hasLaidOutInitialContent = false

    // MARK: - Init

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
        setupDisplayLink()
        setupGestures()
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError() }

    deinit {
        displayLink?.invalidate()
        transactionQueue.cancelAll()
    }

    // MARK: - Setup

    private func setupViews() {
        scroller.delegate = self
        scroller.frame = bounds
        scroller.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        addSubview(scroller)

        // Set initial content size for infinite scroll
        scroller.contentSize = CGSize(width: bounds.width, height: infiniteScrollSize * 2)

        nodeContainer.frame = CGRect(origin: .zero, size: scroller.contentSize)
        scroller.addSubview(nodeContainer)
    }

    private func setupDisplayLink() {
        displayLink = CADisplayLink(target: self, selector: #selector(displayLinkFired))
        displayLink?.add(to: .main, forMode: .common)
        displayLink?.isPaused = true
    }

    private func setupGestures() {
        let tap = UITapGestureRecognizer(target: self, action: #selector(backgroundTapped))
        tap.cancelsTouchesInView = false
        scroller.addGestureRecognizer(tap)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        let oldWidth = scroller.frame.width
        scroller.frame = bounds
        if bounds.width != oldWidth {
            scroller.contentSize.width = bounds.width
            nodeContainer.frame.size.width = bounds.width
            invalidateAllLayouts()
        }
    }

    // MARK: - Public API: Set Items (Full Replace)

    /// Replace all items with a new list. Calculates layouts async.
    /// Telegram: `transaction()` with deleteItems + insertItems.
    func setItems(_ newItems: [RosettaListItem], animated: Bool = false) {
        transactionQueue.enqueue { [weak self] completion in
            guard let self else { completion(); return }
            self.performSetItems(newItems, animated: animated, completion: completion)
        }
    }

    /// Insert items at specific indices.
    func insertItems(_ inserts: [RosettaListInsertItem], animated: Bool = true) {
        transactionQueue.enqueue { [weak self] completion in
            guard let self else { completion(); return }
            self.performInsertItems(inserts, animated: animated, completion: completion)
        }
    }

    // MARK: - Scroll API

    /// Scroll to bottom (newest messages in inverted mode).
    /// Telegram: `scrollToEndOfHistory()` (ChatHistoryListNode.swift:3494).
    func scrollToBottom(animated: Bool) {
        let targetY: CGFloat
        if stackFromBottom {
            targetY = infiniteScrollSize - bounds.height + contentInsets.bottom
        } else {
            targetY = -contentInsets.top
        }
        scroller.setContentOffset(CGPoint(x: 0, y: targetY), animated: animated)
    }

    /// Scroll to item with given ID.
    func scrollToItem(id: String, position: RosettaListScrollPosition = .center, animated: Bool = true) {
        guard let node = visibleNodes.first(where: { $0.index.flatMap({ items[$0].id }) == id }) else { return }
        let nodeFrame = node.frame
        let targetY: CGFloat
        switch position {
        case .top(let offset):
            targetY = nodeFrame.minY - contentInsets.top - offset
        case .center:
            targetY = nodeFrame.midY - bounds.height / 2
        case .bottom(let offset):
            targetY = nodeFrame.maxY - bounds.height + contentInsets.bottom + offset
        case .visible:
            let visibleRect = CGRect(
                x: 0, y: scroller.contentOffset.y + contentInsets.top,
                width: bounds.width,
                height: bounds.height - contentInsets.top - contentInsets.bottom
            )
            if visibleRect.contains(nodeFrame) { return }
            targetY = nodeFrame.midY - bounds.height / 2
        }
        scroller.setContentOffset(CGPoint(x: 0, y: targetY), animated: animated)
    }

    // MARK: - Node Pool

    private func dequeueNode(reuseId: String) -> RosettaListNode? {
        nodePool[reuseId]?.popLast()
    }

    private func enqueueNode(_ node: RosettaListNode) {
        node.prepareForReuse()
        node.removeFromSuperview()
        nodePool[node.reuseIdentifier, default: []].append(node)
    }

    // MARK: - Internal: Set Items

    private func performSetItems(
        _ newItems: [RosettaListItem],
        animated: Bool,
        completion: @escaping () -> Void
    ) {
        // Remove all existing nodes
        for node in visibleNodes {
            enqueueNode(node)
        }
        visibleNodes.removeAll()

        items = newItems
        rebuildIdIndex()

        guard !items.isEmpty else {
            completion()
            return
        }

        // Calculate layouts for visible window + buffer
        let params = layoutParams
        let visibleRange = estimateVisibleRange()

        var pendingLayouts = visibleRange.count
        guard pendingLayouts > 0 else { completion(); return }

        for i in visibleRange {
            let item = items[i]
            let prevItem = i > 0 ? items[i - 1] : nil
            let nextItem = i < items.count - 1 ? items[i + 1] : nil

            item.nodeConfiguredForParams(
                async: { f in
                    // For initial load: synchronous to avoid blank screen
                    f()
                },
                params: params,
                previousItem: prevItem,
                nextItem: nextItem
            ) { [weak self] node, layout in
                guard let self else { return }
                node.index = i
                node.reuseIdentifier = item.reuseIdentifier
                node.applyLayout(layout)
                self.visibleNodes.append(node)

                pendingLayouts -= 1
                if pendingLayouts == 0 {
                    self.visibleNodes.sort { ($0.index ?? 0) < ($1.index ?? 0) }
                    self.layoutVisibleNodes()
                    self.addVisibleNodesToView()
                    if !self.hasLaidOutInitialContent {
                        self.hasLaidOutInitialContent = true
                        if self.stackFromBottom {
                            self.scrollToBottom(animated: false)
                        }
                    }
                    completion()
                }
            }
        }
    }

    // MARK: - Internal: Insert Items

    private func performInsertItems(
        _ inserts: [RosettaListInsertItem],
        animated: Bool,
        completion: @escaping () -> Void
    ) {
        let timestamp = CACurrentMediaTime()

        // Update items array
        let sorted = inserts.sorted { $0.index < $1.index }
        for insert in sorted.reversed() {
            items.insert(insert.item, at: min(insert.index, items.count))
        }
        rebuildIdIndex()

        // Re-index existing nodes
        for node in visibleNodes {
            if let oldIndex = node.index,
               let id = findItemId(for: node),
               let newIndex = itemIdToIndex[id] {
                node.index = newIndex
            }
        }

        let params = layoutParams

        var pendingLayouts = sorted.count
        guard pendingLayouts > 0 else { completion(); return }

        for insert in sorted {
            let i = min(insert.index, items.count - 1)
            let item = items[i]
            let prevItem = i > 0 ? items[i - 1] : nil
            let nextItem = i < items.count - 1 ? items[i + 1] : nil

            item.nodeConfiguredForParams(
                async: { f in f() },
                params: params,
                previousItem: prevItem,
                nextItem: nextItem
            ) { [weak self] node, layout in
                guard let self else { return }
                node.index = i
                node.reuseIdentifier = item.reuseIdentifier
                node.applyLayout(layout)

                if animated {
                    let targetHeight = layout.size.height
                    node.addHeightAnimation(
                        from: 0,
                        to: targetHeight,
                        duration: 0.4,
                        beginAt: timestamp
                    )
                    node.addAlphaAnimation(from: 0, to: 1, duration: 0.2, beginAt: timestamp)
                    self.setNeedsAnimationUpdate()
                }

                self.visibleNodes.append(node)

                pendingLayouts -= 1
                if pendingLayouts == 0 {
                    self.visibleNodes.sort { ($0.index ?? 0) < ($1.index ?? 0) }
                    self.layoutVisibleNodes()
                    self.addVisibleNodesToView()
                    completion()
                }
            }
        }
    }

    // MARK: - Layout Engine

    /// Position all visible nodes based on their layout heights.
    /// Inverted mode: items stack from bottom of visible area upward.
    private func layoutVisibleNodes() {
        guard !visibleNodes.isEmpty else { return }

        if stackFromBottom {
            // Bottom-to-top: newest item at bottom, oldest at top.
            // Reference point: bottom of visible area = infiniteScrollSize
            var y = infiniteScrollSize
            for node in visibleNodes.reversed() {
                let h = node.effectiveHeight
                y -= h
                node.frame = CGRect(x: 0, y: y, width: bounds.width, height: h)
                node.contentView.frame = CGRect(x: 0, y: 0, width: bounds.width, height: node.layout?.size.height ?? h)
            }
        } else {
            var y: CGFloat = 0
            for node in visibleNodes {
                let h = node.effectiveHeight
                node.frame = CGRect(x: 0, y: y, width: bounds.width, height: h)
                node.contentView.frame = CGRect(x: 0, y: 0, width: bounds.width, height: h)
                y += h
            }
        }
    }

    /// Add visible nodes to the node container (if not already added).
    private func addVisibleNodesToView() {
        for node in visibleNodes {
            if node.superview !== nodeContainer {
                nodeContainer.addSubview(node)
            }
        }
    }

    // MARK: - Visibility Culling

    /// Remove nodes outside the visible area + buffer.
    /// Telegram: `removeInvisibleNodes()` (ListViewIntermediateState.swift:615-692).
    private func performVisibilityCulling() {
        let visibleTop = scroller.contentOffset.y - invisibleInset
        let visibleBottom = scroller.contentOffset.y + bounds.height + invisibleInset

        var i = visibleNodes.count - 1
        while i >= 0 {
            let node = visibleNodes[i]
            let frame = node.frame
            if frame.maxY < visibleTop || frame.minY > visibleBottom {
                enqueueNode(node)
                visibleNodes.remove(at: i)
            }
            i -= 1
        }

        // Load nodes that should be visible but aren't
        loadMissingVisibleNodes()
    }

    /// Check if there are items that should be visible but don't have nodes.
    private func loadMissingVisibleNodes() {
        guard !items.isEmpty else { return }

        let visibleTop = scroller.contentOffset.y - invisibleInset
        let visibleBottom = scroller.contentOffset.y + bounds.height + invisibleInset

        // Determine which indices should be visible based on approximate heights
        let existingIndices = Set(visibleNodes.compactMap(\.index))

        // Find range boundaries from existing nodes
        guard let minVisibleIndex = visibleNodes.compactMap(\.index).min(),
              let maxVisibleIndex = visibleNodes.compactMap(\.index).max() else { return }

        // Check items above current visible range
        if minVisibleIndex > 0 {
            let topNode = visibleNodes.first(where: { $0.index == minVisibleIndex })
            if let topNode, topNode.frame.minY > visibleTop {
                // Need to load items above
                let startIndex = max(0, minVisibleIndex - 5) // Load 5 at a time
                for i in startIndex..<minVisibleIndex {
                    if !existingIndices.contains(i) {
                        loadNodeForItem(at: i, synchronous: true)
                    }
                }
            }
        }

        // Check items below current visible range
        if maxVisibleIndex < items.count - 1 {
            let bottomNode = visibleNodes.first(where: { $0.index == maxVisibleIndex })
            if let bottomNode, bottomNode.frame.maxY < visibleBottom {
                let endIndex = min(items.count, maxVisibleIndex + 6)
                for i in (maxVisibleIndex + 1)..<endIndex {
                    if !existingIndices.contains(i) {
                        loadNodeForItem(at: i, synchronous: true)
                    }
                }
            }
        }
    }

    /// Load a single node for item at index.
    private func loadNodeForItem(at index: Int, synchronous: Bool) {
        guard index >= 0, index < items.count else { return }
        let item = items[index]
        let params = layoutParams
        let prevItem = index > 0 ? items[index - 1] : nil
        let nextItem = index < items.count - 1 ? items[index + 1] : nil

        item.nodeConfiguredForParams(
            async: { f in
                if synchronous { f() }
                else { DispatchQueue.global(qos: .userInitiated).async { f() } }
            },
            params: params,
            previousItem: prevItem,
            nextItem: nextItem
        ) { [weak self] node, layout in
            guard let self else { return }
            node.index = index
            node.reuseIdentifier = item.reuseIdentifier
            node.applyLayout(layout)
            self.visibleNodes.append(node)
            self.visibleNodes.sort { ($0.index ?? 0) < ($1.index ?? 0) }
            self.layoutVisibleNodes()
            self.addVisibleNodesToView()
        }
    }

    // MARK: - UIScrollViewDelegate

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        performVisibilityCulling()
        notifyVisibleRangeChanged()
        onScroll?(scrollView.contentOffset.y, scrollView.isDragging)
    }

    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        // Capture anchor for position restoration
        captureScrollAnchor()
    }

    // MARK: - Display Link

    @objc private func displayLinkFired() {
        let timestamp = CACurrentMediaTime()
        var anyActive = false

        for node in visibleNodes {
            if node.updateAnimations(at: timestamp) {
                anyActive = true
            }
        }

        if anyActive {
            layoutVisibleNodes()
        } else {
            displayLink?.isPaused = true
            needsAnimationUpdate = false
            // Clean up completed deletion animations
            visibleNodes.removeAll { $0.index == nil && !$0.hasActiveAnimations }
        }
    }

    private func setNeedsAnimationUpdate() {
        if !needsAnimationUpdate {
            needsAnimationUpdate = true
            displayLink?.isPaused = false
        }
    }

    // MARK: - Helpers

    private func rebuildIdIndex() {
        itemIdToIndex = Dictionary(uniqueKeysWithValues: items.enumerated().map { ($1.id, $0) })
    }

    private func findItemId(for node: RosettaListNode) -> String? {
        guard let index = node.index, index < items.count else { return nil }
        return items[index].id
    }

    private func estimateVisibleRange() -> Range<Int> {
        guard !items.isEmpty else { return 0..<0 }
        // For initial load, estimate based on approximate heights
        // Start from the end (newest messages) for stackFromBottom
        if stackFromBottom {
            let viewportHeight = bounds.height + invisibleInset * 2
            var totalHeight: CGFloat = 0
            var startIndex = items.count
            while startIndex > 0 && totalHeight < viewportHeight {
                startIndex -= 1
                totalHeight += items[startIndex].approximateHeight
            }
            return startIndex..<items.count
        } else {
            let viewportHeight = bounds.height + invisibleInset * 2
            var totalHeight: CGFloat = 0
            var endIndex = 0
            while endIndex < items.count && totalHeight < viewportHeight {
                totalHeight += items[endIndex].approximateHeight
                endIndex += 1
            }
            return 0..<endIndex
        }
    }

    private func captureScrollAnchor() {
        // Find the node closest to the top of the visible area
        let topY = scroller.contentOffset.y + contentInsets.top
        for node in visibleNodes {
            if node.frame.maxY >= topY {
                if let index = node.index, index < items.count {
                    anchorItemId = items[index].id
                    anchorOffset = node.frame.minY - topY
                }
                break
            }
        }
    }

    private func invalidateAllLayouts() {
        // TODO: Recalculate all visible node layouts for new width
    }

    private func notifyVisibleRangeChanged() {
        guard !visibleNodes.isEmpty else {
            displayedItemRangeChanged?(nil)
            return
        }
        let indices = visibleNodes.compactMap(\.index)
        guard let minIdx = indices.min(), let maxIdx = indices.max() else {
            displayedItemRangeChanged?(nil)
            return
        }
        displayedItemRangeChanged?(minIdx...maxIdx)
    }

    // MARK: - Gestures

    @objc private func backgroundTapped() {
        onTapBackground?()
    }
}
