import Combine
import Observation
import SwiftUI
import UIKit

struct ChatListUIKitView: View {
    @Binding var isSearchActive: Bool
    @Binding var isDetailPresented: Bool

    var body: some View {
        ChatListUIKitContainer(
            isSearchActive: $isSearchActive,
            isDetailPresented: $isDetailPresented
        )
    }
}

private struct ChatListUIKitContainer: UIViewControllerRepresentable {
    @Binding var isSearchActive: Bool
    @Binding var isDetailPresented: Bool

    func makeCoordinator() -> ChatListUIKitCoordinator {
        ChatListUIKitCoordinator(
            isSearchActive: $isSearchActive,
            isDetailPresented: $isDetailPresented
        )
    }

    func makeUIViewController(context: Context) -> UINavigationController {
        let root = ChatListRootViewController()
        root.onSearchActiveChanged = { [weak coordinator = context.coordinator] active in
            coordinator?.setSearchActive(active)
        }
        root.onDetailPresentedChanged = { [weak coordinator = context.coordinator] presented in
            coordinator?.setDetailPresented(presented)
        }

        let nav = UINavigationController(rootViewController: root)
        nav.view.backgroundColor = UIColor { $0.userInterfaceStyle == .dark ? .black : .white }
        nav.navigationBar.tintColor = UIColor { $0.userInterfaceStyle == .dark ? .white : .black }
        nav.navigationBar.prefersLargeTitles = false
        return nav
    }

    func updateUIViewController(_ navigationController: UINavigationController, context: Context) {
        context.coordinator.isSearchActive = $isSearchActive
        context.coordinator.isDetailPresented = $isDetailPresented
    }
}

private final class ChatListUIKitCoordinator {
    var isSearchActive: Binding<Bool>
    var isDetailPresented: Binding<Bool>

    init(isSearchActive: Binding<Bool>, isDetailPresented: Binding<Bool>) {
        self.isSearchActive = isSearchActive
        self.isDetailPresented = isDetailPresented
    }

    func setSearchActive(_ active: Bool) {
        DispatchQueue.main.async {
            if self.isSearchActive.wrappedValue != active {
                self.isSearchActive.wrappedValue = active
            }
        }
    }

    func setDetailPresented(_ presented: Bool) {
        if self.isDetailPresented.wrappedValue != presented {
            self.isDetailPresented.wrappedValue = presented
        }
    }
}

@MainActor
final class ChatListRootViewController: UIViewController, UINavigationControllerDelegate {

    var onSearchActiveChanged: ((Bool) -> Void)?
    var onDetailPresentedChanged: ((Bool) -> Void)?

    private let viewModel = ChatListViewModel()
    private let listController = ChatListCollectionController()
    private let searchHeaderView = ChatListSearchHeaderView()
    private let toolbarTitleView = ChatListToolbarTitleView()
    private let navigationBlurView = ChatListHeaderBlurView()
    private var typingDialogs: [String: Set<String>] = [:]
    private var currentSearchQuery = ""
    private var searchResultUsersByKey: [String: SearchUser] = [:]
    private let searchTopSpacing: CGFloat = 10
    private let searchBottomSpacing: CGFloat = 5

    // Search overlay (shown when search is active)
    private var searchOverlayView: UIView?
    private var searchContentHosting: UIHostingController<AnyView>?
    private let searchHeaderHeight: CGFloat = 44
    private var searchChromeHeight: CGFloat {
        searchTopSpacing + searchHeaderHeight + searchBottomSpacing
    }

    private var searchHeaderTopConstraint: NSLayoutConstraint?
    private var searchHeaderHeightConstraint: NSLayoutConstraint?
    private var navigationBlurHeightConstraint: NSLayoutConstraint?
    private var lastSearchExpansion: CGFloat = 1.0
    private var lastNavigationBlurProgress: CGFloat = -1.0
    private var lastNavigationBlurSearchActive = false
    private var lastNavigationBlurPinnedFraction: CGFloat = -1.0
    private var hasPinnedDialogs = false
    private var pinnedHeaderFraction: CGFloat = 0.0

    private var typingDialogsCancellable: AnyCancellable?
    private var searchResultsCancellable: AnyCancellable?
    private var observationTask: Task<Void, Never>?
    private var openChatObserver: NSObjectProtocol?
    private var didBecomeActiveObserver: NSObjectProtocol?

    // Device alert banner (shown when new device tries to login)
    private var deviceAlertBanner: DeviceAlertBannerView?
    private var lastPendingDeviceId: String?

    // PERF: Throttle render to prevent rapid-fire UI updates (blue flash bug).
    private var pendingRenderWork: DispatchWorkItem?
    private var lastRenderTime: CFAbsoluteTime = 0
    private let renderThrottleInterval: TimeInterval = 0.1

    private lazy var editButtonControl: ChatListToolbarEditButton = {
        let button = ChatListToolbarEditButton()
        button.addTarget(self, action: #selector(editTapped), for: .touchUpInside)
        return button
    }()

    private lazy var rightButtonsControl: ChatListToolbarDualActionButton = {
        let button = ChatListToolbarDualActionButton()
        button.onAddPressed = { [weak self] in
            self?.addPressed()
        }
        button.onComposePressed = { [weak self] in
            self?.composeTapped()
        }
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor { $0.userInterfaceStyle == .dark ? .black : .white }

        setupNavigationChrome()
        setupSearchHeader()
        setupListController()
        setupObservers()
        startObservationLoop()
        render()
        consumePendingRouteIfFresh()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        if navigationController?.delegate !== self {
            navigationController?.delegate = self
        }
        // Hide nav bar on chat list; pushed VCs show it via willShow delegate
        navigationController?.setNavigationBarHidden(true, animated: animated)
        let blurProgress = searchHeaderView.isSearchActive ? 1.0 : (1.0 - lastSearchExpansion)
        updateNavigationBarBlur(progress: blurProgress)
        // Don't update tab bar visibility during interactive swipe-back —
        // willShow delegate handles it with proper interactive transition checks.
        if navigationController?.transitionCoordinator?.isInteractive != true {
            onDetailPresentedChanged?(navigationController?.viewControllers.count ?? 1 > 1)
        }
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        layoutCustomHeader()
        updateNavigationBlurHeight()
    }

    private func layoutCustomHeader() {
        let statusBarHeight = view.safeAreaInsets.top
        let centerY = statusBarHeight + headerBarHeight * 0.5
        let sideInset: CGFloat = 16

        // Edit button (left)
        let editSize = editButtonControl.intrinsicContentSize
        editButtonControl.frame = CGRect(
            x: sideInset,
            y: centerY - editSize.height * 0.5,
            width: editSize.width,
            height: editSize.height
        )

        // Right buttons (right)
        let rightSize = rightButtonsControl.intrinsicContentSize
        rightButtonsControl.frame = CGRect(
            x: view.bounds.width - sideInset - rightSize.width,
            y: centerY - rightSize.height * 0.5,
            width: rightSize.width,
            height: rightSize.height
        )

        // Title (centered)
        let titleLeft = editButtonControl.frame.maxX + 8
        let titleRight = rightButtonsControl.frame.minX - 8
        let titleWidth = max(0, titleRight - titleLeft)
        let titleSize = toolbarTitleView.intrinsicContentSize
        let actualWidth = min(titleSize.width, titleWidth)
        toolbarTitleView.frame = CGRect(
            x: titleLeft + (titleWidth - actualWidth) * 0.5,
            y: centerY - 15,
            width: actualWidth,
            height: 30
        )
    }

    private func updateNavigationBlurHeight() {
        navigationBlurHeightConstraint?.constant = max(0, headerTotalHeight + 14.0)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        // Keep tab bar state in sync when leaving the screen while search is active.
        if searchHeaderView.isSearchActive {
            searchHeaderView.endSearch(animated: false, clearText: true)
            // Restore toolbar immediately (no animation since we're leaving)
            editButtonControl.alpha = 1.0
            rightButtonsControl.alpha = 1.0
            toolbarTitleView.alpha = 1.0
            editButtonControl.isUserInteractionEnabled = true
            rightButtonsControl.isUserInteractionEnabled = true
            toolbarTitleView.isUserInteractionEnabled = true
            // Restore search bar position, blur, list, and tear down overlay
            searchHeaderTopConstraint?.constant = headerBarHeight + searchTopSpacing
            navigationBlurView.isHidden = false
            listController.view.isHidden = false
            teardownSearchOverlayImmediately()
        }
    }

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        guard traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle else { return }
        // Force blur view to re-resolve colors and re-apply opacity with current progress
        updateNavigationBarBlur(progress: lastNavigationBlurProgress, force: true)
    }

    deinit {
        observationTask?.cancel()
        if let openChatObserver {
            NotificationCenter.default.removeObserver(openChatObserver)
        }
        if let didBecomeActiveObserver {
            NotificationCenter.default.removeObserver(didBecomeActiveObserver)
        }
    }

    /// Height of the custom header bar content (matches UINavigationBar standard)
    private let headerBarHeight: CGFloat = 44
    /// Computed total header height: status bar + header bar
    private var headerTotalHeight: CGFloat {
        view.safeAreaInsets.top + headerBarHeight
    }

    private func setupNavigationChrome() {
        definesPresentationContext = true

        // Blur background layer (below buttons)
        navigationBlurView.translatesAutoresizingMaskIntoConstraints = false
        navigationBlurView.isUserInteractionEnabled = false
        navigationBlurView.layer.zPosition = 40
        view.addSubview(navigationBlurView)

        let blurHeight = navigationBlurView.heightAnchor.constraint(equalToConstant: 0)
        navigationBlurHeightConstraint = blurHeight
        NSLayoutConstraint.activate([
            navigationBlurView.topAnchor.constraint(equalTo: view.topAnchor),
            navigationBlurView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            navigationBlurView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            blurHeight,
        ])

        // Header elements as direct subviews (z=55, above blur at z=40 and search at z=50)
        for v: UIView in [editButtonControl, rightButtonsControl, toolbarTitleView] {
            v.layer.zPosition = 55
            view.addSubview(v)
        }

        toolbarTitleView.addTarget(self, action: #selector(handleTitleTapped), for: .touchUpInside)
        updateNavigationBarBlur(progress: 0)
    }

    @objc private func handleTitleTapped() {
        NotificationCenter.default.post(name: .chatListScrollToTop, object: nil)
    }

    private func setupSearchHeader() {
        searchHeaderView.translatesAutoresizingMaskIntoConstraints = false
        searchHeaderView.clipsToBounds = true
        searchHeaderView.layer.zPosition = 50
        view.addSubview(searchHeaderView)

        // With nav bar hidden, safeArea.top = status bar only.
        // Search bar goes below our custom 44pt header bar.
        let top = searchHeaderView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: headerBarHeight + searchTopSpacing)
        let height = searchHeaderView.heightAnchor.constraint(equalToConstant: searchHeaderHeight)
        searchHeaderTopConstraint = top
        searchHeaderHeightConstraint = height

        NSLayoutConstraint.activate([
            top,
            searchHeaderView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
            searchHeaderView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
            height,
        ])

        searchHeaderView.onQueryChanged = { [weak self] query in
            guard let self else { return }
            self.currentSearchQuery = query
            self.viewModel.setSearchQuery(query)
            self.renderList()
            self.updateSearchOverlayContent()
        }

        searchHeaderView.onActiveChanged = { [weak self] active in
            guard let self else { return }
            self.applySearchExpansion(1.0, animated: true)
            self.navigationBlurView.isHidden = active
            self.listController.view.isHidden = active
            self.updateNavigationBarBlur(progress: active ? 0.0 : (1.0 - self.lastSearchExpansion))
            self.animateToolbarForSearch(active: active)
            self.animateSearchBarPosition(active: active)
            if active {
                self.showSearchOverlay()
            } else {
                self.hideSearchOverlay()
            }
            self.onSearchActiveChanged?(active)
        }
    }

    private func setupListController() {
        addChild(listController)
        view.insertSubview(listController.view, belowSubview: searchHeaderView)
        listController.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            listController.view.topAnchor.constraint(equalTo: view.topAnchor),
            listController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            listController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            listController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
        listController.didMove(toParent: self)
        listController.setSearchHeaderExpansion(lastSearchExpansion)

        // zPosition only affects rendering, NOT hit-testing — bring header
        // buttons to front so they receive touches above the list view.
        view.bringSubviewToFront(navigationBlurView)
        view.bringSubviewToFront(searchHeaderView)
        view.bringSubviewToFront(editButtonControl)
        view.bringSubviewToFront(rightButtonsControl)
        view.bringSubviewToFront(toolbarTitleView)

        listController.onSelectDialog = { [weak self] dialog in
            guard let self else { return }

            let selectedUser = self.searchResultUsersByKey[dialog.opponentKey]
            let shouldOpenUserRoute = selectedUser != nil && !self.currentSearchQuery.isEmpty
            let routeToOpen: ChatRoute
            if let user = selectedUser, shouldOpenUserRoute {
                self.viewModel.addToRecent(user)
                routeToOpen = ChatRoute(user: user)
            } else {
                routeToOpen = ChatRoute(dialog: dialog)
            }

            if self.searchHeaderView.isSearchActive {
                self.searchHeaderView.endSearch(animated: false, clearText: true)
                // Let keyboard dismissal complete before starting the push animation.
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in
                    self?.openChat(route: routeToOpen)
                }
                return
            }

            self.openChat(route: routeToOpen)
        }
        listController.onDeleteDialog = { [weak self] dialog in
            self?.viewModel.deleteDialog(dialog)
        }
        listController.onTogglePin = { [weak self] dialog in
            guard let self else { return }
            self.viewModel.togglePin(dialog)
            self.renderList()
            let progress = self.searchHeaderView.isSearchActive ? 1.0 : (1.0 - self.lastSearchExpansion)
            self.updateNavigationBarBlur(progress: progress, force: true)
        }
        listController.onToggleMute = { [weak self] dialog in
            self?.viewModel.toggleMute(dialog)
        }
        listController.onPinnedStateChange = { [weak self] hasPinned in
            guard let self else { return }
            self.hasPinnedDialogs = hasPinned
            if !hasPinned {
                self.pinnedHeaderFraction = 0.0
            }
            let progress = self.searchHeaderView.isSearchActive ? 1.0 : (1.0 - self.lastSearchExpansion)
            self.updateNavigationBarBlur(progress: progress, force: true)
        }
        listController.onPinnedHeaderFractionChange = { [weak self] fraction in
            guard let self else { return }
            self.pinnedHeaderFraction = self.hasPinnedDialogs ? fraction : 0.0
            let progress = self.searchHeaderView.isSearchActive ? 1.0 : (1.0 - self.lastSearchExpansion)
            self.updateNavigationBarBlur(progress: progress)
        }
        listController.onMarkAsRead = { [weak self] dialog in
            self?.viewModel.markAsRead(dialog)
        }
        listController.onShowRequests = { [weak self] in
            self?.openRequests()
        }
        listController.onScrollOffsetChange = { [weak self] expansion in
            self?.applySearchExpansion(expansion, animated: false)
        }
    }

    private func setupObservers() {
        typingDialogsCancellable = MessageRepository.shared.$typingDialogs
            .receive(on: DispatchQueue.main)
            .sink { [weak self] typing in
                guard let self else { return }
                self.typingDialogs = typing
                self.renderList()
            }

        searchResultsCancellable = viewModel.$serverSearchResults
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                self?.renderList()
            }

        openChatObserver = NotificationCenter.default.addObserver(
            forName: .openChatFromNotification,
            object: nil,
            queue: .main
        ) { [weak self] notification in
            guard let route = notification.object as? ChatRoute else { return }
            AppDelegate.pendingChatRoute = nil
            AppDelegate.pendingChatRouteTimestamp = nil
            Task { @MainActor [weak self] in
                self?.openChat(route: route)
            }
        }

        didBecomeActiveObserver = NotificationCenter.default.addObserver(
            forName: UIApplication.didBecomeActiveNotification,
            object: nil,
            queue: .main
        ) { [weak self] _ in
            Task { @MainActor [weak self] in
                self?.consumePendingRouteIfFresh()
            }
        }
    }

    private func startObservationLoop() {
        observationTask?.cancel()
        observationTask = Task { @MainActor [weak self] in
            self?.observeState()
        }
    }

    private func observeState() {
        withObservationTracking {
            _ = DialogRepository.shared.dialogs
            _ = SessionManager.shared.syncBatchInProgress
            _ = ProtocolManager.shared.connectionState
            _ = ProtocolManager.shared.pendingDeviceVerification
            _ = AvatarRepository.shared.avatarVersion
        } onChange: { [weak self] in
            Task { @MainActor [weak self] in
                guard let self else { return }
                self.scheduleRender()
                self.observeState()
            }
        }
    }

    /// PERF: Coalesce rapid observation triggers into a single render.
    /// First render in a burst fires immediately; subsequent within 100ms are batched.
    private func scheduleRender() {
        pendingRenderWork?.cancel()

        let now = CFAbsoluteTimeGetCurrent()
        let elapsed = now - lastRenderTime

        if elapsed >= renderThrottleInterval {
            lastRenderTime = now
            render()
        } else {
            let delay = renderThrottleInterval - elapsed
            let work = DispatchWorkItem { [weak self] in
                guard let self else { return }
                self.lastRenderTime = CFAbsoluteTimeGetCurrent()
                self.render()
            }
            pendingRenderWork = work
            DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: work)
        }
    }

    private func render() {
        updateNavigationTitle()
        updateDeviceAlertBanner()
        renderList()
    }

    private func updateDeviceAlertBanner() {
        let pending = ProtocolManager.shared.pendingDeviceVerification
        let newId = pending?.deviceId

        // No change — skip
        guard newId != lastPendingDeviceId else { return }
        lastPendingDeviceId = newId

        if let device = pending {
            showDeviceAlertBanner(device: device)
        } else {
            hideDeviceAlertBanner()
        }
    }

    private func showDeviceAlertBanner(device: DeviceEntry) {
        guard deviceAlertBanner == nil else {
            deviceAlertBanner?.configure(deviceName: device.deviceName, deviceOs: device.deviceOs)
            return
        }

        let banner = DeviceAlertBannerView()
        banner.configure(deviceName: device.deviceName, deviceOs: device.deviceOs)
        banner.onAccept = { [weak self] in
            ProtocolManager.shared.acceptDevice(device.deviceId)
            self?.hideDeviceAlertBanner()
        }
        banner.onDecline = { [weak self] in
            ProtocolManager.shared.declineDevice(device.deviceId)
            self?.hideDeviceAlertBanner()
        }

        let inset: CGFloat = 10
        let bannerY = headerTotalHeight + searchChromeHeight
        banner.frame = CGRect(
            x: inset,
            y: bannerY,
            width: view.bounds.width - inset * 2,
            height: DeviceAlertBannerView.bannerHeight
        )
        banner.alpha = 0
        banner.transform = CGAffineTransform(translationX: 0, y: -20)
        view.insertSubview(banner, belowSubview: searchHeaderView)
        deviceAlertBanner = banner

        // Push list content down via the collection view inside listController
        let extraInset = DeviceAlertBannerView.bannerHeight + inset
        if let cv = listController.view.subviews.first(where: { $0 is UICollectionView }) as? UICollectionView {
            cv.contentInset.top += extraInset
        }

        UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) {
            banner.alpha = 1
            banner.transform = .identity
        }
    }

    private func hideDeviceAlertBanner() {
        guard let banner = deviceAlertBanner else { return }
        deviceAlertBanner = nil
        lastPendingDeviceId = nil

        let inset: CGFloat = 10
        let extraInset = DeviceAlertBannerView.bannerHeight + inset

        UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseIn, animations: {
            banner.alpha = 0
            banner.transform = CGAffineTransform(translationX: 0, y: -20)
        }) { _ in
            banner.removeFromSuperview()
        }

        // Restore list inset
        if let cv = listController.view.subviews.first(where: { $0 is UICollectionView }) as? UICollectionView {
            cv.contentInset.top = max(0, cv.contentInset.top - extraInset)
        }
    }

    /// Telegram-style toolbar fade: 0.14s linear out, 0.3s linear in
    private func animateToolbarForSearch(active: Bool) {
        let targetAlpha: CGFloat = active ? 0.0 : 1.0
        let duration: TimeInterval = active ? 0.14 : 0.3

        UIView.animate(
            withDuration: duration,
            delay: 0,
            options: [.curveLinear, .beginFromCurrentState],
            animations: {
                self.editButtonControl.alpha = targetAlpha
                self.rightButtonsControl.alpha = targetAlpha
                self.toolbarTitleView.alpha = targetAlpha
            }
        )

        editButtonControl.isUserInteractionEnabled = !active
        rightButtonsControl.isUserInteractionEnabled = !active
        toolbarTitleView.isUserInteractionEnabled = !active
    }

    // MARK: - Search Bar Position Animation

    /// Telegram: search bar moves to just below status bar when active
    private func animateSearchBarPosition(active: Bool) {
        let targetTop: CGFloat = active
            ? searchTopSpacing                          // just below status bar
            : headerBarHeight + searchTopSpacing         // below custom toolbar
        searchHeaderTopConstraint?.constant = targetTop

        UIView.animate(
            withDuration: 0.5,
            delay: 0,
            usingSpringWithDamping: 0.78,
            initialSpringVelocity: 0,
            options: [.beginFromCurrentState],
            animations: { self.view.layoutIfNeeded() }
        )
    }

    // MARK: - Search Overlay

    private func showSearchOverlay() {
        guard searchOverlayView == nil else { return }

        let bg = UIView()
        bg.backgroundColor = UIColor { $0.userInterfaceStyle == .dark ? .black : .white }
        bg.alpha = 0
        bg.translatesAutoresizingMaskIntoConstraints = false
        bg.layer.zPosition = 45
        view.insertSubview(bg, belowSubview: searchHeaderView)

        NSLayoutConstraint.activate([
            bg.topAnchor.constraint(equalTo: searchHeaderView.bottomAnchor, constant: searchBottomSpacing),
            bg.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            bg.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            bg.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])

        let content = makeSearchContentView()
        let hosting = UIHostingController(rootView: AnyView(content))
        hosting.view.backgroundColor = .clear
        hosting.view.translatesAutoresizingMaskIntoConstraints = false

        addChild(hosting)
        bg.addSubview(hosting.view)
        hosting.didMove(toParent: self)

        NSLayoutConstraint.activate([
            hosting.view.topAnchor.constraint(equalTo: bg.topAnchor),
            hosting.view.leadingAnchor.constraint(equalTo: bg.leadingAnchor),
            hosting.view.trailingAnchor.constraint(equalTo: bg.trailingAnchor),
            hosting.view.bottomAnchor.constraint(equalTo: bg.bottomAnchor),
        ])

        searchOverlayView = bg
        searchContentHosting = hosting

        UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) {
            bg.alpha = 1
        }
    }

    private func hideSearchOverlay() {
        guard let overlay = searchOverlayView else { return }

        UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut, animations: {
            overlay.alpha = 0
        }) { [weak self] _ in
            guard let self else { return }
            self.searchContentHosting?.willMove(toParent: nil)
            self.searchContentHosting?.view.removeFromSuperview()
            self.searchContentHosting?.removeFromParent()
            self.searchContentHosting = nil
            overlay.removeFromSuperview()
            self.searchOverlayView = nil
        }
    }

    private func updateSearchOverlayContent() {
        guard let hosting = searchContentHosting else { return }
        hosting.rootView = AnyView(makeSearchContentView())
    }

    private func makeSearchContentView() -> some View {
        ChatListSearchContent(
            searchText: currentSearchQuery,
            viewModel: viewModel,
            onSelectRecent: { [weak self] query in
                guard let self else { return }
                self.searchHeaderView.setQueryFromOutside(query)
            },
            onOpenDialog: { [weak self] route in
                guard let self else { return }
                self.searchHeaderView.endSearch(animated: false, clearText: true)
                self.animateSearchBarPosition(active: false)
                self.hideSearchOverlay()
                self.animateToolbarForSearch(active: false)
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in
                    self?.openChat(route: route)
                }
            }
        )
    }

    private func teardownSearchOverlayImmediately() {
        searchContentHosting?.willMove(toParent: nil)
        searchContentHosting?.view.removeFromSuperview()
        searchContentHosting?.removeFromParent()
        searchContentHosting = nil
        searchOverlayView?.removeFromSuperview()
        searchOverlayView = nil
    }

    private func applySearchExpansion(_ expansion: CGFloat, animated: Bool) {
        let clamped = max(0.0, min(1.0, expansion))
        if searchHeaderView.isSearchActive && clamped < 0.999 {
            return
        }
        guard abs(clamped - lastSearchExpansion) > 0.003 else { return }
        lastSearchExpansion = clamped

        // Structural: top stays fixed, height collapses (Telegram: 60*progress, we use 44)
        searchHeaderTopConstraint?.constant = headerBarHeight + searchTopSpacing
        searchHeaderHeightConstraint?.constant = searchHeaderHeight * clamped
        listController.setSearchHeaderExpansion(clamped)

        // Telegram animation: NO scale transform, NO whole-view alpha.
        // Height reduction + clipping handles the collapse.
        // Only content (text/icon) fades separately.
        searchHeaderView.transform = .identity
        searchHeaderView.alpha = 1.0
        searchHeaderView.isUserInteractionEnabled = clamped > 0.2

        // Update internal content alpha + corner radius (Telegram behavior)
        searchHeaderView.updateExpansionProgress(clamped)

        updateNavigationBlurHeight()
        updateNavigationBarBlur(progress: 1.0 - clamped)

        if animated {
            let updates = { self.view.layoutIfNeeded() }
            UIView.animate(
                withDuration: 0.5,
                delay: 0,
                usingSpringWithDamping: 0.78,
                initialSpringVelocity: 0,
                options: [.beginFromCurrentState],
                animations: updates
            )
        } else {
            // Force layout so capsule resizes with header (corner radius tracks height)
            searchHeaderView.setNeedsLayout()
            searchHeaderView.layoutIfNeeded()
        }
    }

    private func updateNavigationBarBlur(progress: CGFloat, force: Bool = false) {
        let clamped = max(0.0, min(1.0, progress))
        let isSearchActive = searchHeaderView.isSearchActive
        let effectivePinnedFraction = isSearchActive ? 0.0 : pinnedHeaderFraction

        let hasChanged = abs(clamped - lastNavigationBlurProgress) > 0.01
            || isSearchActive != lastNavigationBlurSearchActive
            || abs(effectivePinnedFraction - lastNavigationBlurPinnedFraction) > 0.01
        guard force || hasChanged else { return }

        lastNavigationBlurProgress = clamped
        lastNavigationBlurSearchActive = isSearchActive
        lastNavigationBlurPinnedFraction = effectivePinnedFraction

        navigationBlurView.setProgress(
            clamped,
            pinnedFraction: effectivePinnedFraction,
            isSearchActive: isSearchActive
        )
    }

    private func updateNavigationTitle() {
        let state = ProtocolManager.shared.connectionState
        let isSyncing = SessionManager.shared.syncBatchInProgress

        let publicKey = AccountManager.shared.currentAccount?.publicKey ?? SessionManager.shared.currentPublicKey
        let displayName = SessionManager.shared.displayName
        let initials = RosettaColors.initials(name: displayName, publicKey: publicKey)
        let avatarIndex = RosettaColors.avatarColorIndex(for: displayName, publicKey: publicKey)
        let avatarImage = AvatarRepository.shared.loadAvatar(publicKey: publicKey)

        if state == .authenticated && isSyncing {
            toolbarTitleView.configure(
                title: "Updating...",
                mode: .loading,
                initials: initials,
                avatarIndex: avatarIndex,
                avatarImage: avatarImage
            )
        } else if state == .authenticated {
            toolbarTitleView.configure(
                title: "Chats",
                mode: .avatar,
                initials: initials,
                avatarIndex: avatarIndex,
                avatarImage: avatarImage
            )
        } else {
            toolbarTitleView.configure(
                title: "Connecting...",
                mode: .loading,
                initials: initials,
                avatarIndex: avatarIndex,
                avatarImage: avatarImage
            )
        }
    }

    private func renderList() {
        let dialogs = dialogsForCurrentSearchState()
        let pinned = dialogs.filter(\.isPinned)
        let unpinned = dialogs.filter { !$0.isPinned }
        let requestsCount = currentSearchQuery.isEmpty ? viewModel.requestsCount : 0

        hasPinnedDialogs = !pinned.isEmpty
        if !hasPinnedDialogs {
            pinnedHeaderFraction = 0.0
        }

        let syncing = SessionManager.shared.syncBatchInProgress
        listController.updateDialogs(
            pinned: pinned,
            unpinned: unpinned,
            requestsCount: requestsCount,
            typingDialogs: typingDialogs,
            isSyncing: syncing
        )
    }

    private func dialogsForCurrentSearchState() -> [Dialog] {
        searchResultUsersByKey = [:]

        let all = viewModel.allModeDialogs
        let query = currentSearchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !query.isEmpty else { return all }

        if !viewModel.serverSearchResults.isEmpty {
            let account = SessionManager.shared.currentPublicKey
            let dialogs = viewModel.serverSearchResults.map { user in
                searchResultUsersByKey[user.publicKey] = user
                return Dialog(
                    id: user.publicKey,
                    account: account,
                    opponentKey: user.publicKey,
                    opponentTitle: user.title,
                    opponentUsername: user.username,
                    lastMessage: "",
                    lastMessageTimestamp: 0,
                    unreadCount: 0,
                    isOnline: user.online == 0,
                    lastSeen: 0,
                    verified: user.verified,
                    iHaveSent: true,
                    isPinned: false,
                    isMuted: false,
                    lastMessageFromMe: false,
                    lastMessageDelivered: .delivered,
                    lastMessageRead: false
                )
            }
            return dialogs
        }

        let normalized = query.lowercased()
        return all.filter { dialog in
            dialog.opponentTitle.lowercased().contains(normalized)
                || dialog.opponentUsername.lowercased().contains(normalized)
                || dialog.opponentKey.lowercased().contains(normalized)
        }
    }

    private func consumePendingRouteIfFresh() {
        guard let route = AppDelegate.consumeFreshPendingRoute() else { return }
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
            self?.openChat(route: route)
        }
    }

    private func openChat(route: ChatRoute) {
        onDetailPresentedChanged?(true) // hide tab bar BEFORE push animation
        let detail = ChatDetailViewController(route: route)
        navigationController?.pushViewController(detail, animated: true)
    }

    private func openRequests() {
        let vc = RequestChatsUIKitShellController(viewModel: viewModel)
        vc.onOpenRoute = { [weak self] route in
            self?.openChat(route: route)
        }
        navigationController?.pushViewController(vc, animated: true)
    }

    @objc private func editTapped() {
        // UIKit shell parity: keep action reserved for future editing mode.
    }

    @objc private func addPressed() {
        composeTapped()
    }

    @objc private func composeTapped() {
        let composeVC = ComposeViewController()
        composeVC.onOpenChat = { [weak self] route in
            self?.openChat(route: route)
        }
        navigationController?.pushViewController(composeVC, animated: true)
    }

    // MARK: - UINavigationControllerDelegate

    func navigationController(
        _ navigationController: UINavigationController,
        willShow viewController: UIViewController,
        animated: Bool
    ) {
        let hideNavBar = viewController === self
            || viewController is ChatDetailViewController
            || viewController is OpponentProfileViewController
            || viewController is RequestChatsUIKitShellController
            || viewController is ComposeViewController
            || viewController is GroupContactPickerViewController
            || viewController is GroupSetupViewController
        navigationController.setNavigationBarHidden(hideNavBar, animated: animated)

        if let coordinator = navigationController.transitionCoordinator, coordinator.isInteractive {
            // Interactive swipe-back: defer tab bar show until transition COMPLETES.
            // If cancelled (user swipes back but doesn't complete), don't show tab bar.
            coordinator.notifyWhenInteractionChanges { [weak self, weak navigationController] context in
                if context.isCancelled {
                    // Swipe cancelled — stay on chat detail, keep tab bar hidden
                    return
                }
                // Swipe completed — show tab bar after completion animation
                guard let navigationController else { return }
                let isPresented = navigationController.viewControllers.count > 1
                self?.onDetailPresentedChanged?(isPresented)
            }
        } else {
            // Non-interactive (programmatic push/pop): update immediately
            let isPresented = navigationController.viewControllers.count > 1
            onDetailPresentedChanged?(isPresented)
        }
    }

    func navigationController(
        _ navigationController: UINavigationController,
        didShow viewController: UIViewController,
        animated: Bool
    ) {
        // Safety fallback: ensure state is correct after any transition
        let isPresented = navigationController.viewControllers.count > 1
        onDetailPresentedChanged?(isPresented)
    }
}

@MainActor
final class RequestChatsUIKitShellController: UIViewController {

    var onOpenRoute: ((ChatRoute) -> Void)?

    private let viewModel: ChatListViewModel
    private let requestsController = RequestChatsController()
    private var observationTask: Task<Void, Never>?
    private var pendingRenderWork: DispatchWorkItem?
    private var lastRenderTime: CFAbsoluteTime = 0

    // Custom header elements (direct subviews — glass needs this)
    private let headerBarHeight: CGFloat = 44
    private let backButton = UIControl()
    private let backGlassBackground = ChatListToolbarGlassCapsuleView()
    private let chevronLayer = CAShapeLayer()
    private let titleLabel = UILabel()
    private var addedFullWidthGesture = false

    init(viewModel: ChatListViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }

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

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor { $0.userInterfaceStyle == .dark ? .black : .white }

        setupCustomHeader()

        addChild(requestsController)
        view.addSubview(requestsController.view)
        requestsController.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            requestsController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: headerBarHeight),
            requestsController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            requestsController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            requestsController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
        requestsController.didMove(toParent: self)

        requestsController.onSelectDialog = { [weak self] dialog in
            self?.onOpenRoute?(ChatRoute(dialog: dialog))
        }
        requestsController.onDeleteDialog = { [weak self] dialog in
            self?.viewModel.deleteDialog(dialog)
        }

        startObservationLoop()
        render()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        setupFullWidthSwipeBack()
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        layoutCustomHeader()
    }

    deinit {
        observationTask?.cancel()
    }

    // MARK: - Custom Header

    private func setupCustomHeader() {
        // Back button — glass capsule + SVG chevron (TelegramIconPath.backChevron)
        backGlassBackground.translatesAutoresizingMaskIntoConstraints = false
        backButton.addSubview(backGlassBackground)
        NSLayoutConstraint.activate([
            backGlassBackground.topAnchor.constraint(equalTo: backButton.topAnchor),
            backGlassBackground.leadingAnchor.constraint(equalTo: backButton.leadingAnchor),
            backGlassBackground.trailingAnchor.constraint(equalTo: backButton.trailingAnchor),
            backGlassBackground.bottomAnchor.constraint(equalTo: backButton.bottomAnchor),
        ])

        // Chevron from SVG path (viewBox 11×20, same as ChatDetailView)
        let viewBox = CGSize(width: 11, height: 20)
        let iconSize = CGSize(width: 11, height: 20)
        var parser = SVGPathParser(pathData: TelegramIconPath.backChevron)
        let rawPath = parser.parse()
        let buttonSize: CGFloat = 44
        var transform = CGAffineTransform(
            scaleX: iconSize.width / viewBox.width,
            y: iconSize.height / viewBox.height
        ).translatedBy(
            x: (buttonSize - iconSize.width) / 2 * (viewBox.width / iconSize.width),
            y: (buttonSize - iconSize.height) / 2 * (viewBox.height / iconSize.height)
        )
        chevronLayer.path = rawPath.copy(using: &transform)
        chevronLayer.fillColor = UIColor { $0.userInterfaceStyle == .dark ? .white : .black }.cgColor
        chevronLayer.frame = CGRect(x: 0, y: 0, width: buttonSize, height: buttonSize)
        backButton.layer.addSublayer(chevronLayer)

        backButton.addTarget(self, action: #selector(backTapped), for: .touchUpInside)
        backButton.layer.zPosition = 55
        view.addSubview(backButton)

        // Title
        titleLabel.text = "Request Chats"
        titleLabel.font = .systemFont(ofSize: 17, weight: .semibold)
        titleLabel.textColor = UIColor { $0.userInterfaceStyle == .dark ? .white : .black }
        titleLabel.textAlignment = .center
        titleLabel.layer.zPosition = 55
        view.addSubview(titleLabel)
    }

    private func layoutCustomHeader() {
        let statusBarHeight = view.safeAreaInsets.top
        let centerY = statusBarHeight + headerBarHeight * 0.5
        let sideInset: CGFloat = 16
        let buttonSize: CGFloat = 44

        backButton.frame = CGRect(
            x: sideInset,
            y: centerY - buttonSize * 0.5,
            width: buttonSize,
            height: buttonSize
        )

        let titleSize = titleLabel.intrinsicContentSize
        titleLabel.frame = CGRect(
            x: (view.bounds.width - titleSize.width) * 0.5,
            y: centerY - titleSize.height * 0.5,
            width: titleSize.width,
            height: titleSize.height
        )
    }

    @objc private func backTapped() {
        navigationController?.popViewController(animated: true)
    }

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        chevronLayer.fillColor = UIColor { $0.userInterfaceStyle == .dark ? .white : .black }.cgColor
    }

    // MARK: - Full-Width Swipe Back

    private func setupFullWidthSwipeBack() {
        guard !addedFullWidthGesture else { return }
        addedFullWidthGesture = true

        guard let nav = navigationController,
              let edgeGesture = nav.interactivePopGestureRecognizer,
              let targets = edgeGesture.value(forKey: "targets") as? NSArray,
              targets.count > 0
        else { return }

        edgeGesture.isEnabled = true

        let fullWidthGesture = UIPanGestureRecognizer()
        fullWidthGesture.setValue(targets, forKey: "targets")
        fullWidthGesture.delegate = self
        nav.view.addGestureRecognizer(fullWidthGesture)
    }

    // MARK: - Observation

    private func startObservationLoop() {
        observationTask?.cancel()
        observationTask = Task { @MainActor [weak self] in
            self?.observeState()
        }
    }

    private func observeState() {
        withObservationTracking {
            _ = DialogRepository.shared.dialogs
            _ = SessionManager.shared.syncBatchInProgress
        } onChange: { [weak self] in
            Task { @MainActor [weak self] in
                guard let self else { return }
                self.scheduleRender()
                self.observeState()
            }
        }
    }

    private func scheduleRender() {
        pendingRenderWork?.cancel()
        let now = CFAbsoluteTimeGetCurrent()
        if now - lastRenderTime >= 0.1 {
            lastRenderTime = now
            render()
        } else {
            let delay = 0.1 - (now - lastRenderTime)
            let work = DispatchWorkItem { [weak self] in
                guard let self else { return }
                self.lastRenderTime = CFAbsoluteTimeGetCurrent()
                self.render()
            }
            pendingRenderWork = work
            DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: work)
        }
    }

    private func render() {
        let requests = viewModel.requestsModeDialogs
        requestsController.updateDialogs(
            requests,
            isSyncing: SessionManager.shared.syncBatchInProgress
        )
        // When requests become empty (user replied → dialog moved to main list),
        // silently remove self from nav stack so back goes directly to ChatList.
        if requests.isEmpty,
           let nav = navigationController,
           nav.topViewController !== self {
            var vcs = nav.viewControllers
            vcs.removeAll { $0 === self }
            nav.viewControllers = vcs
        }
    }
}

// MARK: - UIGestureRecognizerDelegate (full-width swipe back)

extension RequestChatsUIKitShellController: UIGestureRecognizerDelegate {
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        guard let pan = gestureRecognizer as? UIPanGestureRecognizer else { return false }
        let velocity = pan.velocity(in: pan.view)
        return velocity.x > 0 && abs(velocity.x) > abs(velocity.y)
    }

    func gestureRecognizer(
        _ gestureRecognizer: UIGestureRecognizer,
        shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
    ) -> Bool {
        false
    }
}

private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate {

    var onQueryChanged: ((String) -> Void)?
    var onActiveChanged: ((Bool) -> Void)?

    private(set) var isSearchActive = false

    private let capsuleView = UIView()
    private let capsuleGlass = TelegramGlassUIView(frame: .zero)
    private let placeholderStack = UIStackView()
    private let placeholderIcon = UIImageView(image: UIImage(systemName: "magnifyingglass"))
    private let placeholderLabel = UILabel()

    private let activeStack = UIStackView()
    private let activeIcon = UIImageView(image: UIImage(systemName: "magnifyingglass"))
    private let textField = UITextField()
    private let inlineClearButton = UIButton(type: .system)
    private let cancelButton = UIButton(type: .custom)
    private let cancelGlass = TelegramGlassUIView(frame: .zero)

    private var cancelWidthConstraint: NSLayoutConstraint!
    private var capsuleTrailingConstraint: NSLayoutConstraint!
    private var suppressQueryCallback = false

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupUI()
        applyColors()
        updateVisualState(animated: false)
    }

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

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        applyColors()
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        // Sync glass frame with capsule bounds
        capsuleGlass.frame = capsuleView.bounds
        // Keep corner radius proportional to height (Telegram pill style)
        let radius = capsuleView.bounds.height * 0.5
        capsuleView.layer.cornerRadius = radius
        capsuleGlass.fixedCornerRadius = radius
        capsuleGlass.updateGlass()
        // cancelGlass uses constraints (sibling of cancelButton), just update glass
        cancelGlass.fixedCornerRadius = cancelButton.bounds.height * 0.5
        cancelGlass.updateGlass()
    }

    func endSearch(animated: Bool, clearText: Bool) {
        setSearchActive(false, animated: animated, clearText: clearText)
    }

    /// Called from search overlay when user selects a recent search query
    func setQueryFromOutside(_ text: String) {
        setQueryText(text)
    }

    /// Telegram-style expansion animation:
    /// - Corner radius scales with height (pill shape maintained)
    /// - Text/icon alpha: invisible until 77% expanded, then ramps to 1.0
    /// - Background: always visible (controlled by height clipping)
    func updateExpansionProgress(_ progress: CGFloat) {
        let currentHeight = searchBarHeight * progress
        // Telegram: corner radius = height * 0.5 (always pill-shaped)
        let radius = max(0, currentHeight * 0.5)
        capsuleView.layer.cornerRadius = radius
        capsuleGlass.fixedCornerRadius = radius
        capsuleGlass.updateGlass()

        // Telegram background alpha: fades out quickly (0→0.3 range)
        let outerAlpha = min(0.3, progress) / 0.3
        capsuleView.alpha = outerAlpha

        // Telegram inner content alpha: 0 until 77%, then ramps to 1
        let innerAlpha = max(0.0, min(1.0, (progress - 0.77) / 0.23))
        placeholderStack.alpha = isSearchActive ? 0 : innerAlpha
        placeholderIcon.alpha = innerAlpha
        placeholderLabel.alpha = innerAlpha
    }

    private let searchBarHeight: CGFloat = 44

    private func setupUI() {
        translatesAutoresizingMaskIntoConstraints = false

        capsuleView.translatesAutoresizingMaskIntoConstraints = false
        capsuleView.layer.cornerRadius = searchBarHeight * 0.5  // Telegram: height * 0.5
        capsuleView.clipsToBounds = true
        // Glass background for capsule (Telegram active search material)
        capsuleGlass.fixedCornerRadius = searchBarHeight * 0.5
        capsuleGlass.isHidden = true
        capsuleGlass.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        capsuleView.insertSubview(capsuleGlass, at: 0)
        addSubview(capsuleView)

        placeholderStack.translatesAutoresizingMaskIntoConstraints = false
        placeholderStack.axis = .horizontal
        placeholderStack.alignment = .center
        placeholderStack.spacing = 4
        placeholderIcon.contentMode = .scaleAspectFit
        placeholderIcon.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)
        placeholderLabel.text = "Search"
        placeholderLabel.font = .systemFont(ofSize: 17)
        placeholderStack.addArrangedSubview(placeholderIcon)
        placeholderStack.addArrangedSubview(placeholderLabel)
        capsuleView.addSubview(placeholderStack)

        activeStack.translatesAutoresizingMaskIntoConstraints = false
        activeStack.axis = .horizontal
        activeStack.alignment = .center
        activeStack.spacing = 2
        activeIcon.contentMode = .scaleAspectFit
        activeIcon.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)

        textField.translatesAutoresizingMaskIntoConstraints = false
        textField.placeholder = "Search"
        textField.font = .systemFont(ofSize: 17)
        textField.autocapitalizationType = .none
        textField.autocorrectionType = .no
        textField.returnKeyType = .search
        textField.clearButtonMode = .never
        textField.delegate = self
        textField.addTarget(self, action: #selector(handleTextChanged), for: .editingChanged)

        inlineClearButton.translatesAutoresizingMaskIntoConstraints = false
        inlineClearButton.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
        inlineClearButton.addTarget(self, action: #selector(handleInlineClearTapped), for: .touchUpInside)

        activeStack.addArrangedSubview(activeIcon)
        activeStack.addArrangedSubview(textField)
        activeStack.addArrangedSubview(inlineClearButton)
        capsuleView.addSubview(activeStack)

        // Glass background for X button (sibling, positioned behind)
        cancelGlass.translatesAutoresizingMaskIntoConstraints = false
        cancelGlass.fixedCornerRadius = 22
        cancelGlass.isUserInteractionEnabled = false
        cancelGlass.isHidden = true
        addSubview(cancelGlass)

        // Telegram-style circular X button: 44pt, glass material
        cancelButton.translatesAutoresizingMaskIntoConstraints = false
        // Telegram exact: custom drawn X — 2pt lineWidth, round cap, 40×40 canvas
        cancelButton.setImage(Self.telegramCloseIcon(size: 40, color: UIColor { $0.userInterfaceStyle == .dark ? .white : .black }), for: .normal)
        cancelButton.setTitle(nil, for: .normal)
        cancelButton.clipsToBounds = false
        cancelButton.backgroundColor = .clear
        cancelButton.addTarget(self, action: #selector(handleCancelTapped), for: .touchUpInside)
        addSubview(cancelButton)

        let tap = UITapGestureRecognizer(target: self, action: #selector(handleCapsuleTapped))
        capsuleView.addGestureRecognizer(tap)

        cancelWidthConstraint = cancelButton.widthAnchor.constraint(equalToConstant: 0)
        // Fix 1: animate gap — 0 when inactive (symmetric), -8 when active
        let trailing = capsuleView.trailingAnchor.constraint(equalTo: cancelButton.leadingAnchor, constant: 0)
        capsuleTrailingConstraint = trailing

        NSLayoutConstraint.activate([
            capsuleView.leadingAnchor.constraint(equalTo: leadingAnchor),
            capsuleView.topAnchor.constraint(equalTo: topAnchor),
            capsuleView.bottomAnchor.constraint(equalTo: bottomAnchor),
            trailing,

            cancelButton.trailingAnchor.constraint(equalTo: trailingAnchor),
            cancelButton.centerYAnchor.constraint(equalTo: centerYAnchor),
            cancelButton.heightAnchor.constraint(equalToConstant: 44),
            cancelWidthConstraint,

            // Glass behind X button (same frame)
            cancelGlass.topAnchor.constraint(equalTo: cancelButton.topAnchor),
            cancelGlass.leadingAnchor.constraint(equalTo: cancelButton.leadingAnchor),
            cancelGlass.trailingAnchor.constraint(equalTo: cancelButton.trailingAnchor),
            cancelGlass.bottomAnchor.constraint(equalTo: cancelButton.bottomAnchor),

            placeholderStack.centerXAnchor.constraint(equalTo: capsuleView.centerXAnchor),
            placeholderStack.centerYAnchor.constraint(equalTo: capsuleView.centerYAnchor),

            // Fix 4: leading 8pt (Telegram parity)
            activeStack.leadingAnchor.constraint(equalTo: capsuleView.leadingAnchor, constant: 8),
            activeStack.trailingAnchor.constraint(equalTo: capsuleView.trailingAnchor, constant: -10),
            activeStack.topAnchor.constraint(equalTo: capsuleView.topAnchor),
            activeStack.bottomAnchor.constraint(equalTo: capsuleView.bottomAnchor),

            // Fix 2: fixed width prevents stretching to 44pt height
            activeIcon.widthAnchor.constraint(equalToConstant: 20),
            inlineClearButton.widthAnchor.constraint(equalToConstant: 24),
            inlineClearButton.heightAnchor.constraint(equalToConstant: 24),
        ])
    }

    private func applyColors() {
        let isDark = traitCollection.userInterfaceStyle == .dark
        // inputPlaceholderTextColor: dark #8f8f8f, light #8e8e93
        let placeholderColor = isDark
            ? UIColor(red: 0x8f/255.0, green: 0x8f/255.0, blue: 0x8f/255.0, alpha: 1.0)
            : UIColor(red: 0x8e/255.0, green: 0x8e/255.0, blue: 0x93/255.0, alpha: 1.0)
        placeholderLabel.textColor = placeholderColor
        placeholderIcon.tintColor = placeholderColor
        activeIcon.tintColor = placeholderColor
        // inputTextColor: dark #ffffff, light #000000
        textField.textColor = isDark ? .white : .black
        inlineClearButton.tintColor = placeholderColor
        // X button icon: white on dark, black on light
        cancelButton.tintColor = isDark ? .white : .black
        // Apply correct background for current state
        applyCapsuleBackground()
    }

    /// Inactive: solid #272728. Active: TelegramGlassUIView (glass material).
    private func applyCapsuleBackground() {
        let isDark = traitCollection.userInterfaceStyle == .dark
        let solidFill = isDark
            ? UIColor(red: 0x27/255.0, green: 0x27/255.0, blue: 0x28/255.0, alpha: 1.0)
            : UIColor(red: 0xe9/255.0, green: 0xe9/255.0, blue: 0xe9/255.0, alpha: 1.0)

        if isSearchActive {
            // Glass material (Telegram active search)
            capsuleView.backgroundColor = .clear
            capsuleGlass.isHidden = false
            capsuleGlass.updateGlass()
            cancelGlass.isHidden = false
            cancelGlass.updateGlass()
        } else {
            // Solid color (Telegram inactive search)
            capsuleView.backgroundColor = solidFill
            capsuleGlass.isHidden = true
            cancelGlass.isHidden = true
        }
    }

    private func updateVisualState(animated: Bool) {
        // Switch capsule background: solid ↔ glass
        applyCapsuleBackground()

        let updates = {
            self.placeholderStack.alpha = self.isSearchActive ? 0 : 1
            self.activeStack.alpha = self.isSearchActive ? 1 : 0
            self.cancelButton.alpha = self.isSearchActive ? 1 : 0
            self.cancelWidthConstraint.constant = self.isSearchActive ? 44 : 0
            self.capsuleTrailingConstraint.constant = self.isSearchActive ? -8 : 0
            self.layoutIfNeeded()
        }

        if animated {
            UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.78, initialSpringVelocity: 0, options: [.beginFromCurrentState], animations: updates)
        } else {
            updates()
        }
    }

    private func setSearchActive(_ active: Bool, animated: Bool, clearText: Bool = false) {
        let oldValue = isSearchActive
        isSearchActive = active

        if !active {
            textField.resignFirstResponder()
            if clearText {
                setQueryText("")
            }
        } else {
            DispatchQueue.main.async { [weak self] in
                self?.textField.becomeFirstResponder()
            }
        }

        if oldValue != active {
            onActiveChanged?(active)
        }

        updateClearButtonVisibility()
        updateVisualState(animated: animated)
    }

    private func setQueryText(_ text: String) {
        let old = textField.text ?? ""
        guard old != text else { return }
        suppressQueryCallback = true
        textField.text = text
        suppressQueryCallback = false
        updateClearButtonVisibility()
        onQueryChanged?(text)
    }

    private func updateClearButtonVisibility() {
        inlineClearButton.alpha = (textField.text?.isEmpty == false && isSearchActive) ? 1 : 0
        inlineClearButton.isUserInteractionEnabled = inlineClearButton.alpha > 0
    }

    /// Telegram exact close icon: two diagonal lines, 2pt width, round cap.
    /// Source: SearchBarPlaceholderNode.swift — lines from (12,12)→(28,28) in 40×40 canvas.
    private static func telegramCloseIcon(size: CGFloat, color: UIColor) -> UIImage {
        let renderer = UIGraphicsImageRenderer(size: CGSize(width: size, height: size))
        return renderer.image { ctx in
            let cg = ctx.cgContext
            cg.setStrokeColor(color.cgColor)
            cg.setLineWidth(2.0)
            cg.setLineCap(.round)
            let inset: CGFloat = size * 0.3  // 12/40 = 0.3
            cg.move(to: CGPoint(x: inset, y: inset))
            cg.addLine(to: CGPoint(x: size - inset, y: size - inset))
            cg.move(to: CGPoint(x: size - inset, y: inset))
            cg.addLine(to: CGPoint(x: inset, y: size - inset))
            cg.strokePath()
        }.withRenderingMode(.alwaysTemplate)
    }

    @objc private func handleCapsuleTapped() {
        guard !isSearchActive else { return }
        setSearchActive(true, animated: true)
    }

    @objc private func handleTextChanged() {
        updateClearButtonVisibility()
        guard !suppressQueryCallback else { return }
        onQueryChanged?(textField.text ?? "")
    }

    @objc private func handleInlineClearTapped() {
        setQueryText("")
    }

    @objc private func handleCancelTapped() {
        endSearch(animated: true, clearText: true)
    }

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
}

private final class ChatListHeaderBlurView: UIView {

    // Telegram EdgeEffectView port:
    // 1. CABackdropLayer — variable blur of content behind (radius 1.0)
    // 2. Tint overlay — color changes with pinnedFraction, alpha 0.85, gradient-masked
    private let tintView = UIView()
    private let tintMaskView = UIImageView()
    private var backdropLayer: CALayer?
    private var plainBackgroundColor: UIColor = .black
    private var pinnedBackgroundColor: UIColor = .black
    private var currentProgress: CGFloat = 0.0
    private var currentPinnedFraction: CGFloat = 0.0
    private var isSearchCurrentlyActive = false

    override init(frame: CGRect) {
        super.init(frame: frame)
        isUserInteractionEnabled = false

        // Backdrop blur layer (Telegram: VariableBlurView maxRadius 1.0)
        if let backdrop = BackdropLayerHelper.createBackdropLayer() {
            backdrop.delegate = BackdropLayerDelegate.shared
            BackdropLayerHelper.setScale(backdrop, scale: 0.5)
            if let blur = CALayer.blurFilter() {
                blur.setValue(1.0 as NSNumber, forKey: "inputRadius")
                backdrop.filters = [blur]
            }
            layer.addSublayer(backdrop)
            self.backdropLayer = backdrop
        }

        // Tint overlay with gradient mask (Telegram: contentView at alpha 0.85)
        tintView.mask = tintMaskView
        tintView.alpha = 0.85
        addSubview(tintView)

        applyAdaptiveColors()
    }

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

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        guard traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle else { return }
        applyAdaptiveColors()
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        backdropLayer?.frame = bounds
        tintView.frame = bounds
        tintMaskView.frame = bounds
        updateTintMask()
    }

    private func applyAdaptiveColors() {
        let isDark = traitCollection.userInterfaceStyle == .dark
        plainBackgroundColor = isDark ? .black : .white
        pinnedBackgroundColor = isDark
            ? UIColor(red: 0x1C/255, green: 0x1C/255, blue: 0x1D/255, alpha: 1)
            : UIColor(red: 0xF7/255, green: 0xF7/255, blue: 0xF7/255, alpha: 1)
        updateEdgeEffectColor()
        updateChromeOpacity()
        setNeedsLayout()
    }

    // Telegram: plainBackgroundColor.mixedWith(pinnedItemBackgroundColor, alpha: pinnedFraction)
    private func updateEdgeEffectColor() {
        let effectivePinnedFraction = isSearchCurrentlyActive ? 0.0 : currentPinnedFraction
        let resolved = plainBackgroundColor.mixedWith(pinnedBackgroundColor, alpha: effectivePinnedFraction)
        tintView.backgroundColor = resolved
    }

    private func updateChromeOpacity() {
        let clamped = max(0.0, min(1.0, currentProgress))
        // Telegram: content alpha is always 0.85; we modulate by scroll progress
        tintView.alpha = 0.85 * clamped
    }

    private func updateTintMask() {
        let height = max(1, bounds.height)
        let edgeSize = min(54.0, height)
        tintMaskView.image = VariableBlurEdgeView.generateEdgeGradient(baseHeight: edgeSize)
    }

    func setProgress(_ progress: CGFloat, pinnedFraction: CGFloat, isSearchActive: Bool) {
        currentProgress = max(0.0, min(1.0, progress))
        currentPinnedFraction = max(0.0, min(1.0, pinnedFraction))
        isSearchCurrentlyActive = isSearchActive
        updateEdgeEffectColor()
        updateChromeOpacity()
    }
}

private final class ChatListToolbarGlassCapsuleView: UIView {

    private let glassView = TelegramGlassUIView(frame: .zero)

    override init(frame: CGRect) {
        super.init(frame: frame)
        isUserInteractionEnabled = false
        clipsToBounds = false
        addSubview(glassView)
    }

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

    override func layoutSubviews() {
        super.layoutSubviews()
        glassView.frame = bounds
        glassView.fixedCornerRadius = bounds.height * 0.5
        glassView.updateGlass()
    }
}

private final class ChatListToolbarEditButton: UIControl {

    private let backgroundView = ChatListToolbarGlassCapsuleView()
    private let titleLabel = UILabel()

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

        isAccessibilityElement = true
        accessibilityLabel = "Edit"
        accessibilityTraits = .button

        addSubview(backgroundView)
        backgroundView.translatesAutoresizingMaskIntoConstraints = false

        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        titleLabel.text = "Edit"
        titleLabel.font = .systemFont(ofSize: 17, weight: .medium)
        titleLabel.textColor = UIColor { $0.userInterfaceStyle == .dark ? .white : .black }
        titleLabel.textAlignment = .center
        addSubview(titleLabel)

        NSLayoutConstraint.activate([
            backgroundView.topAnchor.constraint(equalTo: topAnchor),
            backgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
            backgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
            backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor),

            titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
            titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
            heightAnchor.constraint(equalToConstant: 44),
        ])

        self.frame = CGRect(origin: .zero, size: intrinsicContentSize)
    }

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

    override var intrinsicContentSize: CGSize {
        let textWidth = (titleLabel.text as NSString?)?.size(withAttributes: [.font: titleLabel.font!]).width ?? 0
        let width = max(44.0, ceil(textWidth) + 24.0)
        return CGSize(width: width, height: 44.0)
    }

    override var isHighlighted: Bool {
        didSet {
            if isHighlighted {
                alpha = 0.6
            } else {
                UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) {
                    self.alpha = 1.0
                }
            }
        }
    }
}

private final class ChatListToolbarDualActionButton: UIView {

    var onAddPressed: (() -> Void)?
    var onComposePressed: (() -> Void)?

    private let backgroundView = ChatListToolbarGlassCapsuleView()
    private let addButton = UIButton(type: .system)
    private let composeButton = UIButton(type: .system)
    // dividerView removed per Telegram parity

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

        isAccessibilityElement = false

        backgroundView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(backgroundView)

        addButton.translatesAutoresizingMaskIntoConstraints = false
        composeButton.translatesAutoresizingMaskIntoConstraints = false

        addButton.tintColor = UIColor { $0.userInterfaceStyle == .dark ? .white : .black }
        composeButton.tintColor = UIColor { $0.userInterfaceStyle == .dark ? .white : .black }
        addButton.accessibilityLabel = "Add"
        composeButton.accessibilityLabel = "Compose"

        let iconConfig = UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)
        let targetIconSize = CGSize(width: 19, height: 19)
        let addIcon = (UIImage(named: "toolbar-add-chat")?.withRenderingMode(.alwaysTemplate)
            ?? UIImage(systemName: "plus", withConfiguration: iconConfig))?.scaledTo(targetIconSize)
        let composeIcon = (UIImage(named: "toolbar-compose")?.withRenderingMode(.alwaysTemplate)
            ?? UIImage(systemName: "square.and.pencil", withConfiguration: iconConfig))?.scaledTo(targetIconSize)
        addButton.setImage(addIcon, for: .normal)
        composeButton.setImage(composeIcon, for: .normal)

        addButton.addTarget(self, action: #selector(handleAddTapped), for: .touchUpInside)
        composeButton.addTarget(self, action: #selector(handleComposeTapped), for: .touchUpInside)
        addButton.addTarget(self, action: #selector(handleTouchDown(_:)), for: .touchDown)
        composeButton.addTarget(self, action: #selector(handleTouchDown(_:)), for: .touchDown)
        addButton.addTarget(self, action: #selector(handleTouchUp(_:)), for: [.touchUpInside, .touchCancel, .touchDragExit])
        composeButton.addTarget(self, action: #selector(handleTouchUp(_:)), for: [.touchUpInside, .touchCancel, .touchDragExit])

        addSubview(addButton)
        addSubview(composeButton)

        NSLayoutConstraint.activate([
            backgroundView.topAnchor.constraint(equalTo: topAnchor),
            backgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
            backgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
            backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor),

            addButton.leadingAnchor.constraint(equalTo: leadingAnchor),
            addButton.topAnchor.constraint(equalTo: topAnchor),
            addButton.bottomAnchor.constraint(equalTo: bottomAnchor),
            addButton.widthAnchor.constraint(equalToConstant: 36),

            composeButton.trailingAnchor.constraint(equalTo: trailingAnchor),
            composeButton.topAnchor.constraint(equalTo: topAnchor),
            composeButton.bottomAnchor.constraint(equalTo: bottomAnchor),
            composeButton.widthAnchor.constraint(equalToConstant: 36),

            heightAnchor.constraint(equalToConstant: 44),
            widthAnchor.constraint(equalToConstant: 72),
        ])

        self.frame = CGRect(origin: .zero, size: intrinsicContentSize)
    }

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

    override var intrinsicContentSize: CGSize {
        CGSize(width: 72, height: 44)
    }

    @objc private func handleAddTapped() {
        onAddPressed?()
    }

    @objc private func handleComposeTapped() {
        onComposePressed?()
    }

    @objc private func handleTouchDown(_ sender: UIButton) {
        sender.alpha = 0.6
    }

    @objc private func handleTouchUp(_ sender: UIButton) {
        UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) {
            sender.alpha = 1.0
        }
    }
}

private final class ChatListToolbarTitleView: UIControl {

    enum Mode {
        case loading
        case avatar
    }

    private let stackView = UIStackView()
    private let titleLabel = UILabel()
    private let avatarContainer = UIView()
    private let avatarImageView = UIImageView()
    private let avatarInitialsLabel = UILabel()
    private let spinner = ChatListToolbarArcSpinnerView()

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

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

    override var intrinsicContentSize: CGSize {
        let fitting = stackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
        return CGSize(width: fitting.width, height: 30)
    }

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

    func configure(title: String, mode: Mode, initials: String, avatarIndex: Int, avatarImage: UIImage?) {
        titleLabel.text = title
        avatarInitialsLabel.text = initials

        // Mantine "light" variant — same as ChatListCell (NOT solid tint)
        let colorPair = RosettaColors.avatarColors[avatarIndex % RosettaColors.avatarColors.count]
        let tintUIColor = UIColor(colorPair.tint)
        avatarContainer.backgroundColor = UIColor { 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.textColor = UIColor { traits in
            traits.userInterfaceStyle == .dark ? UIColor(colorPair.text) : tintUIColor
        }

        if let avatarImage {
            avatarImageView.image = avatarImage
            avatarImageView.isHidden = false
            avatarInitialsLabel.isHidden = true
        } else {
            avatarImageView.image = nil
            avatarImageView.isHidden = true
            avatarInitialsLabel.isHidden = false
        }

        switch mode {
        case .loading:
            spinner.isHidden = false
            spinner.startAnimating()
            avatarContainer.isHidden = true
        case .avatar:
            spinner.stopAnimating()
            spinner.isHidden = true
            avatarContainer.isHidden = false
        }

        invalidateIntrinsicContentSize()
    }

    private func setupUI() {
        isAccessibilityElement = true
        accessibilityTraits = .button

        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .horizontal
        stackView.alignment = .center
        stackView.spacing = 6
        addSubview(stackView)

        spinner.translatesAutoresizingMaskIntoConstraints = false
        spinner.setColor(UIColor(RosettaColors.primaryBlue))

        avatarContainer.translatesAutoresizingMaskIntoConstraints = false
        avatarContainer.clipsToBounds = true
        avatarContainer.layer.cornerRadius = 14

        avatarImageView.translatesAutoresizingMaskIntoConstraints = false
        avatarImageView.contentMode = .scaleAspectFill
        avatarImageView.clipsToBounds = true

        avatarInitialsLabel.translatesAutoresizingMaskIntoConstraints = false
        avatarInitialsLabel.font = .systemFont(ofSize: 12, weight: .semibold)
        avatarInitialsLabel.textColor = UIColor { $0.userInterfaceStyle == .dark ? .white : .black }
        avatarInitialsLabel.textAlignment = .center

        avatarContainer.addSubview(avatarImageView)
        avatarContainer.addSubview(avatarInitialsLabel)

        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        titleLabel.font = .systemFont(ofSize: 17, weight: .semibold)
        titleLabel.textColor = UIColor { $0.userInterfaceStyle == .dark ? .white : .black }
        titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)

        stackView.addArrangedSubview(spinner)
        stackView.addArrangedSubview(avatarContainer)
        stackView.addArrangedSubview(titleLabel)

        NSLayoutConstraint.activate([
            stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
            stackView.topAnchor.constraint(equalTo: topAnchor),
            stackView.bottomAnchor.constraint(equalTo: bottomAnchor),

            spinner.widthAnchor.constraint(equalToConstant: 20),
            spinner.heightAnchor.constraint(equalToConstant: 20),

            avatarContainer.widthAnchor.constraint(equalToConstant: 28),
            avatarContainer.heightAnchor.constraint(equalToConstant: 28),

            avatarImageView.leadingAnchor.constraint(equalTo: avatarContainer.leadingAnchor),
            avatarImageView.trailingAnchor.constraint(equalTo: avatarContainer.trailingAnchor),
            avatarImageView.topAnchor.constraint(equalTo: avatarContainer.topAnchor),
            avatarImageView.bottomAnchor.constraint(equalTo: avatarContainer.bottomAnchor),

            avatarInitialsLabel.leadingAnchor.constraint(equalTo: avatarContainer.leadingAnchor),
            avatarInitialsLabel.trailingAnchor.constraint(equalTo: avatarContainer.trailingAnchor),
            avatarInitialsLabel.topAnchor.constraint(equalTo: avatarContainer.topAnchor),
            avatarInitialsLabel.bottomAnchor.constraint(equalTo: avatarContainer.bottomAnchor),
        ])
    }
}

private final class ChatListToolbarArcSpinnerView: UIView {

    private let arcLayer = CAShapeLayer()
    private let animationKey = "chatlist.toolbar.arcSpinner.rotation"

    override init(frame: CGRect) {
        super.init(frame: frame)
        isUserInteractionEnabled = false
        backgroundColor = .clear

        arcLayer.fillColor = UIColor.clear.cgColor
        arcLayer.lineWidth = 2
        arcLayer.lineCap = .round
        layer.addSublayer(arcLayer)
    }

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

    deinit {
        stopAnimating()
    }

    override func willMove(toSuperview newSuperview: UIView?) {
        super.willMove(toSuperview: newSuperview)
        if newSuperview == nil {
            stopAnimating()
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        arcLayer.frame = bounds

        let radius = max(0, min(bounds.width, bounds.height) * 0.5 - arcLayer.lineWidth)
        let center = CGPoint(x: bounds.midX, y: bounds.midY)
        let start = -CGFloat.pi / 2 + (2 * CGFloat.pi * 0.05)
        let end = -CGFloat.pi / 2 + (2 * CGFloat.pi * 0.78)
        arcLayer.path = UIBezierPath(
            arcCenter: center,
            radius: radius,
            startAngle: start,
            endAngle: end,
            clockwise: true
        ).cgPath
    }

    func setColor(_ color: UIColor) {
        arcLayer.strokeColor = color.cgColor
    }

    func startAnimating() {
        guard layer.animation(forKey: animationKey) == nil else { return }
        let animation = CABasicAnimation(keyPath: "transform.rotation.z")
        animation.fromValue = 0
        animation.toValue = Double.pi * 2
        animation.duration = 1.0
        animation.repeatCount = .infinity
        animation.timingFunction = CAMediaTimingFunction(name: .linear)
        layer.add(animation, forKey: animationKey)
    }

    func stopAnimating() {
        layer.removeAnimation(forKey: animationKey)
    }
}

private extension UIImage {
    func scaledTo(_ size: CGSize) -> UIImage {
        let renderer = UIGraphicsImageRenderer(size: size)
        return renderer.image { _ in
            self.draw(in: CGRect(origin: .zero, size: size))
        }.withRenderingMode(renderingMode)
    }
}
