//
//  WiredCameraTransferViewController.swift
//  suixinkan
//

import Kingfisher
import PhotosUI
import SnapKit
import UIKit
import UniformTypeIdentifiers

private enum WiredTransferMetrics {
    static let sidebarWidth: CGFloat = 76
    static let rowHeight: CGFloat = 66
    static let sectionHeaderHeight: CGFloat = 28
    static let selectAllBarHeight: CGFloat = 44
}

private struct WiredTransferListSection: Hashable {
    let id: String
    let title: String

    init(_ section: TravelAlbumOTGPhotoSection) {
        id = section.slotId
        title = section.headerTitle
    }
}

/// 有线相机传输页，对齐 Android OTG 页面并接入真实 ImageCaptureCore 传输。
final class WiredCameraTransferViewController: BaseViewController {
    private let viewModel: WiredCameraTransferViewModel

    private let titleLabel = UILabel()
    private let phoneLabel = UILabel()
    private let statusCard = UIView()
    private let storagePanel = UIView()
    private let storageTitleLabel = UILabel()
    private let storageValueLabel = UILabel()
    private let statusLabel = UILabel()
    private let refreshButton = UIButton(type: .system)
    private let helpLabel = UILabel()
    private let chipsStack = UIStackView()
    private let retouchButton = WiredTransferSettingChipButton()
    private let formatButton = WiredTransferSettingChipButton()
    private let modeButton = WiredTransferSettingChipButton(showsChevron: true)
    private let settingsStatsDivider = UIView()
    private let statsCard = UIStackView()
    private let firstStatsDivider = UIView()
    private let secondStatsDivider = UIView()
    private let photoPanelView = UIView()
    private let selectAllBar = UIControl()
    private let selectAllIconView = UIImageView()
    private let selectAllTitleLabel = UILabel()
    private let selectAllCountLabel = UILabel()
    private let photoContentView = UIView()
    private let sidebarView = WiredTransferTimeSidebarView()
    private let sidebarDivider = UIView()
    private let listContainerView = UIView()
    private let expandSidebarButton = UIButton(type: .system)
    private let emptyLabel = UILabel()
    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<WiredTransferListSection, TravelAlbumOTGPhotoItem>!
    private var currentSections: [WiredTransferListSection] = []
    private var pendingScrollSlotId: String?
    private var selectAllBarHeightConstraint: Constraint?
    private let bottomBar = UIView()
    private let bottomButtonsStack = UIStackView()
    private let batchButton = UIButton(type: .system)
    private let historyImportButton = UIButton(type: .system)
    private let albumImportButton = UIButton(type: .system)
    private let specifyButton = UIButton(type: .system)
    private var isHistoryImportButtonVisible = false
    private var previousNavigationBarStyle: (tintColor: UIColor?, barStyle: UIBarStyle, isTranslucent: Bool)?

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

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func setupNavigationBar() {
        navigationItem.largeTitleDisplayMode = .never
        configureBlueNavigationBarAppearance()

        let titleStack = UIStackView(arrangedSubviews: [titleLabel, phoneLabel])
        titleStack.axis = .vertical
        titleStack.alignment = .center
        titleStack.spacing = 1
        navigationItem.titleView = titleStack
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        applyViewModel()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        applyNavigationBarStyle()
        viewModel.start()
        navigationController?.setNavigationBarHidden(false, animated: animated)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        viewModel.stop()
        restoreNavigationBarStyle()
    }

    override func setupUI() {
        view.backgroundColor = AppColor.pageBackground
        titleLabel.text = viewModel.albumTitle
        titleLabel.font = .systemFont(ofSize: 17, weight: .semibold)
        titleLabel.textColor = .white
        titleLabel.textAlignment = .center
        phoneLabel.text = viewModel.headerPhone
        phoneLabel.font = .systemFont(ofSize: 11)
        phoneLabel.textColor = UIColor.white.withAlphaComponent(0.92)
        phoneLabel.textAlignment = .center

        statusCard.backgroundColor = .white
        statusCard.layer.cornerRadius = 12
        statusCard.layer.shadowColor = UIColor.black.withAlphaComponent(0.10).cgColor
        statusCard.layer.shadowOpacity = 1
        statusCard.layer.shadowRadius = 8
        statusCard.layer.shadowOffset = CGSize(width: 0, height: 3)

        storagePanel.layer.cornerRadius = 12
        storagePanel.layer.borderColor = AppColor.border.cgColor
        storagePanel.layer.borderWidth = 3
        storageTitleLabel.font = .systemFont(ofSize: 11, weight: .medium)
        storageTitleLabel.textColor = AppColor.textPrimary
        storageTitleLabel.textAlignment = .center
        storageTitleLabel.adjustsFontSizeToFitWidth = true
        storageTitleLabel.minimumScaleFactor = 0.72
        storageValueLabel.font = .systemFont(ofSize: 10)
        storageValueLabel.textColor = AppColor.primary
        storageValueLabel.textAlignment = .center
        storageValueLabel.adjustsFontSizeToFitWidth = true
        storageValueLabel.minimumScaleFactor = 0.72

        statusLabel.font = .systemFont(ofSize: 11, weight: .medium)
        statusLabel.layer.cornerRadius = 4
        statusLabel.clipsToBounds = true
        statusLabel.textAlignment = .center
        refreshButton.setTitleColor(.white, for: .normal)
        refreshButton.titleLabel?.font = .systemFont(ofSize: 12)
        refreshButton.backgroundColor = AppColor.primary
        refreshButton.layer.cornerRadius = 8
        helpLabel.font = .systemFont(ofSize: 10)
        helpLabel.numberOfLines = 3
        helpLabel.isUserInteractionEnabled = true

        chipsStack.axis = .horizontal
        chipsStack.spacing = 6
        chipsStack.distribution = .fillEqually
        [retouchButton, formatButton, modeButton].forEach {
            chipsStack.addArrangedSubview($0)
        }
        retouchButton.isUserInteractionEnabled = false
        formatButton.isUserInteractionEnabled = false
        settingsStatsDivider.backgroundColor = AppColor.border

        statsCard.axis = .horizontal
        statsCard.distribution = .fillEqually
        firstStatsDivider.backgroundColor = AppColor.border
        secondStatsDivider.backgroundColor = AppColor.border

        photoPanelView.backgroundColor = .white
        selectAllBar.backgroundColor = .white
        selectAllIconView.contentMode = .scaleAspectFit
        selectAllTitleLabel.text = "全选"
        selectAllTitleLabel.font = .systemFont(ofSize: 13)
        selectAllTitleLabel.textColor = AppColor.textPrimary
        selectAllCountLabel.font = .systemFont(ofSize: 12)
        selectAllCountLabel.textColor = AppColor.textSecondary
        selectAllCountLabel.textAlignment = .right
        sidebarDivider.backgroundColor = AppColor.border
        listContainerView.backgroundColor = .white
        expandSidebarButton.setImage(UIImage(systemName: "chevron.right"), for: .normal)
        expandSidebarButton.tintColor = AppColor.textSecondary
        expandSidebarButton.backgroundColor = UIColor(hex: 0xF5F6F8)
        expandSidebarButton.layer.cornerRadius = 8
        expandSidebarButton.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]

        collectionView = UICollectionView(frame: .zero, collectionViewLayout: makeLayout())
        collectionView.backgroundColor = .white
        collectionView.delegate = self
        collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 8, right: 0)
        collectionView.register(WiredTransferPhotoCell.self, forCellWithReuseIdentifier: WiredTransferPhotoCell.reuseIdentifier)
        collectionView.register(
            WiredTransferSectionHeaderView.self,
            forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
            withReuseIdentifier: WiredTransferSectionHeaderView.reuseIdentifier
        )
        dataSource = UICollectionViewDiffableDataSource<WiredTransferListSection, TravelAlbumOTGPhotoItem>(collectionView: collectionView) {
            [weak self] collectionView, indexPath, item in
            let cell = collectionView.dequeueReusableCell(
                withReuseIdentifier: WiredTransferPhotoCell.reuseIdentifier,
                for: indexPath
            ) as! WiredTransferPhotoCell
            self?.configurePhotoCell(cell, item: item)
            return cell
        }
        dataSource.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath in
            guard kind == UICollectionView.elementKindSectionHeader else { return nil }
            let header = collectionView.dequeueReusableSupplementaryView(
                ofKind: kind,
                withReuseIdentifier: WiredTransferSectionHeaderView.reuseIdentifier,
                for: indexPath
            ) as! WiredTransferSectionHeaderView
            header.apply(title: self?.currentSections[safe: indexPath.section]?.title ?? "")
            return header
        }

        emptyLabel.text = "暂无传输文件"
        emptyLabel.font = .systemFont(ofSize: 14)
        emptyLabel.textColor = AppColor.textTertiary
        emptyLabel.textAlignment = .center

        bottomBar.backgroundColor = .white
        bottomButtonsStack.axis = .horizontal
        bottomButtonsStack.spacing = 8
        bottomButtonsStack.distribution = .fillEqually
        configureBottomButton(batchButton, title: "批量上传", filled: true)
        configureBottomButton(historyImportButton, title: "历史导入", filled: false)
        configureBottomButton(albumImportButton, title: "相册导入", filled: false)
        configureBottomButton(specifyButton, title: "指定上传", filled: false)
        historyImportButton.isHidden = true
        historyImportButton.alpha = 0

        view.addSubview(statusCard)
        statusCard.addSubview(storagePanel)
        storagePanel.addSubview(storageTitleLabel)
        storagePanel.addSubview(storageValueLabel)
        statusCard.addSubview(statusLabel)
        statusCard.addSubview(refreshButton)
        statusCard.addSubview(helpLabel)
        statusCard.addSubview(chipsStack)
        statusCard.addSubview(settingsStatsDivider)
        statusCard.addSubview(statsCard)
        statsCard.addSubview(firstStatsDivider)
        statsCard.addSubview(secondStatsDivider)
        view.addSubview(photoPanelView)
        photoPanelView.addSubview(selectAllBar)
        selectAllBar.addSubview(selectAllIconView)
        selectAllBar.addSubview(selectAllTitleLabel)
        selectAllBar.addSubview(selectAllCountLabel)
        photoPanelView.addSubview(photoContentView)
        photoContentView.addSubview(sidebarView)
        photoContentView.addSubview(sidebarDivider)
        photoContentView.addSubview(listContainerView)
        listContainerView.addSubview(collectionView)
        listContainerView.addSubview(expandSidebarButton)
        view.addSubview(emptyLabel)
        view.addSubview(bottomBar)
        bottomBar.addSubview(bottomButtonsStack)
        bottomButtonsStack.addArrangedSubview(batchButton)
        bottomButtonsStack.addArrangedSubview(specifyButton)
        bottomButtonsStack.addArrangedSubview(albumImportButton)
        bottomButtonsStack.addArrangedSubview(historyImportButton)
    }

    override func setupConstraints() {
        statusCard.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide).offset(14)
            make.leading.trailing.equalToSuperview().inset(16)
        }
        storagePanel.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(14)
            make.leading.equalToSuperview().offset(14)
            make.width.equalTo(96)
            make.height.equalTo(76)
        }
        storageTitleLabel.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(10)
            make.leading.trailing.equalToSuperview().inset(8)
        }
        storageValueLabel.snp.makeConstraints { make in
            make.top.equalTo(storageTitleLabel.snp.bottom).offset(6)
            make.leading.trailing.equalTo(storageTitleLabel)
        }
        statusLabel.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(16)
            make.leading.equalTo(storagePanel.snp.trailing).offset(8)
            make.trailing.lessThanOrEqualTo(refreshButton.snp.leading).offset(-6)
            make.height.equalTo(22)
            make.width.greaterThanOrEqualTo(84)
        }
        refreshButton.snp.makeConstraints { make in
            make.centerY.equalTo(statusLabel)
            make.trailing.equalToSuperview().offset(-14)
            make.height.equalTo(34)
            make.width.greaterThanOrEqualTo(64)
        }
        helpLabel.snp.makeConstraints { make in
            make.top.equalTo(statusLabel.snp.bottom).offset(4)
            make.leading.equalTo(statusLabel)
            make.trailing.equalToSuperview().offset(-14)
        }
        chipsStack.snp.makeConstraints { make in
            make.top.equalTo(storagePanel.snp.bottom).offset(12)
            make.leading.trailing.equalToSuperview().inset(14)
            make.height.equalTo(34)
        }
        settingsStatsDivider.snp.makeConstraints { make in
            make.top.equalTo(chipsStack.snp.bottom).offset(12)
            make.leading.trailing.equalToSuperview()
            make.height.equalTo(1 / UIScreen.main.scale)
        }
        statsCard.snp.makeConstraints { make in
            make.top.equalTo(settingsStatsDivider.snp.bottom).offset(2)
            make.leading.trailing.bottom.equalToSuperview()
            make.height.equalTo(68)
        }
        firstStatsDivider.snp.makeConstraints { make in
            make.width.equalTo(1 / UIScreen.main.scale)
            make.top.bottom.equalToSuperview().inset(14)
            make.centerX.equalToSuperview().multipliedBy(2.0 / 3.0)
        }
        secondStatsDivider.snp.makeConstraints { make in
            make.width.equalTo(1 / UIScreen.main.scale)
            make.top.bottom.equalToSuperview().inset(14)
            make.centerX.equalToSuperview().multipliedBy(4.0 / 3.0)
        }
        bottomBar.snp.makeConstraints { make in
            make.leading.trailing.bottom.equalToSuperview()
        }
        bottomButtonsStack.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(12)
            make.leading.trailing.equalToSuperview().inset(16)
            make.height.equalTo(44)
            make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-12)
        }
        photoPanelView.snp.makeConstraints { make in
            make.top.equalTo(statusCard.snp.bottom).offset(14)
            make.leading.trailing.equalToSuperview()
            make.bottom.equalTo(bottomBar.snp.top)
        }
        selectAllBar.snp.makeConstraints { make in
            make.top.leading.trailing.equalToSuperview()
            selectAllBarHeightConstraint = make.height.equalTo(WiredTransferMetrics.selectAllBarHeight).constraint
        }
        selectAllIconView.snp.makeConstraints { make in
            make.leading.equalToSuperview().offset(10)
            make.centerY.equalToSuperview()
            make.size.equalTo(20)
        }
        selectAllTitleLabel.snp.makeConstraints { make in
            make.leading.equalTo(selectAllIconView.snp.trailing).offset(8)
            make.centerY.equalToSuperview()
        }
        selectAllCountLabel.snp.makeConstraints { make in
            make.leading.greaterThanOrEqualTo(selectAllTitleLabel.snp.trailing).offset(8)
            make.trailing.equalToSuperview().offset(-10)
            make.centerY.equalToSuperview()
        }
        photoContentView.snp.makeConstraints { make in
            make.top.equalTo(selectAllBar.snp.bottom)
            make.leading.trailing.bottom.equalToSuperview()
        }
        collectionView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        expandSidebarButton.snp.makeConstraints { make in
            make.leading.centerY.equalToSuperview()
            make.width.equalTo(24)
            make.height.equalTo(48)
        }
        emptyLabel.snp.makeConstraints { make in
            make.top.equalTo(statusCard.snp.bottom)
            make.leading.trailing.equalToSuperview()
            make.bottom.equalTo(bottomBar.snp.top)
        }
        updateSidebarLayout()
    }

    override func bindActions() {
        viewModel.onStateChange = { [weak self] in
            Task { @MainActor in self?.applyViewModel() }
        }
        viewModel.onShowMessage = { [weak self] message in
            Task { @MainActor in self?.showToast(message) }
        }
        refreshButton.addTarget(self, action: #selector(refreshTapped), for: .touchUpInside)
        batchButton.addTarget(self, action: #selector(batchTapped), for: .touchUpInside)
        historyImportButton.addTarget(self, action: #selector(historyImportTapped), for: .touchUpInside)
        albumImportButton.addTarget(self, action: #selector(albumImportTapped), for: .touchUpInside)
        specifyButton.addTarget(self, action: #selector(specifyTapped), for: .touchUpInside)
        selectAllBar.addTarget(self, action: #selector(selectAllTapped), for: .touchUpInside)
        expandSidebarButton.addTarget(self, action: #selector(toggleSidebarTapped), for: .touchUpInside)
        sidebarView.onToggleSidebar = { [weak self] in self?.viewModel.toggleSidebar() }
        sidebarView.onTimeSlotSelected = { [weak self] slotId in
            self?.pendingScrollSlotId = slotId
            self?.viewModel.selectTimeSlot(id: slotId)
        }
        modeButton.menu = UIMenu(children: TravelAlbumOTGTransferMode.allCases.map { mode in
            UIAction(title: mode.title) { [weak self] _ in self?.viewModel.selectTransferMode(mode.title) }
        })
        modeButton.showsMenuAsPrimaryAction = true
        helpLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(helpTapped)))
    }

    @MainActor
    private func applyViewModel() {
        storageTitleLabel.text = viewModel.deviceModelName
        storageValueLabel.text = viewModel.availableStorageText
        statusLabel.text = viewModel.cameraStatusText
        let isFailed = viewModel.cameraStatusText.contains("失败") || viewModel.cameraStatusText.contains("未连接")
        statusLabel.textColor = isFailed ? AppColor.danger : AppColor.primary
        statusLabel.backgroundColor = (isFailed ? AppColor.danger : AppColor.primary).withAlphaComponent(0.10)
        refreshButton.setTitle(viewModel.actionButtonText, for: .normal)

        retouchButton.apply(title: viewModel.retouchOption)
        formatButton.apply(title: "JPG")
        modeButton.apply(title: viewModel.transferModeOption)
        helpLabel.attributedText = helpText(viewModel.sonyMTPHint)

        rebuildStats()
        let sections = viewModel.photoSections
        currentSections = sections.map(WiredTransferListSection.init)
        var snapshot = NSDiffableDataSourceSnapshot<WiredTransferListSection, TravelAlbumOTGPhotoItem>()
        snapshot.appendSections(currentSections)
        for (index, section) in sections.enumerated() {
            snapshot.appendItems(section.photos, toSection: currentSections[index])
        }
        dataSource.apply(snapshot, animatingDifferences: true) { [weak self] in
            self?.reconfigureVisiblePhotoCells()
            self?.scrollToPendingTimeSlotIfNeeded()
        }

        sidebarView.apply(
            groups: viewModel.sidebarGroups,
            selectedSlotId: viewModel.selectedTimeSlotId
        )
        updateSelectAllBar()
        updateSidebarLayout()
        emptyLabel.isHidden = !sections.isEmpty
        photoPanelView.isHidden = sections.isEmpty
        if viewModel.selectUploadMode {
            let count = viewModel.selectedPhotoIds.count
            batchButton.setTitle(count > 0 ? "上传选中(\(count))" : "取消", for: .normal)
        } else {
            batchButton.setTitle("批量上传", for: .normal)
        }
        historyImportButton.setTitle("历史导入", for: .normal)
        albumImportButton.setTitle("相册导入", for: .normal)
        specifyButton.isEnabled = viewModel.canOpenSpecifyUploadChooser
        specifyButton.alpha = specifyButton.isEnabled ? 1 : 0.55
        updateHistoryImportButtonVisibility(viewModel.isCameraConnected, animated: view.window != nil)
    }

    private func updateHistoryImportButtonVisibility(_ visible: Bool, animated: Bool) {
        guard visible != isHistoryImportButtonVisible else { return }
        isHistoryImportButtonVisible = visible
        if visible {
            historyImportButton.isHidden = false
            historyImportButton.alpha = 0
        }
        let changes = {
            self.historyImportButton.alpha = visible ? 1 : 0
            if !visible {
                self.historyImportButton.isHidden = true
            }
            self.bottomBar.layoutIfNeeded()
            self.view.layoutIfNeeded()
        }
        guard animated else {
            changes()
            return
        }
        UIView.animate(
            withDuration: 0.25,
            delay: 0,
            options: [.beginFromCurrentState, .curveEaseInOut],
            animations: changes
        )
    }

    private func updateSelectAllBar() {
        let showSelectAll = viewModel.selectUploadMode && !viewModel.photoSections.isEmpty
        selectAllBar.isHidden = !showSelectAll
        selectAllBarHeightConstraint?.update(offset: showSelectAll ? WiredTransferMetrics.selectAllBarHeight : 0)
        let allSelected = viewModel.allVisibleSelectablePhotosSelected
        selectAllIconView.image = UIImage(systemName: allSelected ? "checkmark.circle.fill" : "circle")
        selectAllIconView.tintColor = allSelected ? AppColor.primary : AppColor.textTertiary
        selectAllCountLabel.text = "已选 \(viewModel.selectedVisibleUploadCount) / \(viewModel.selectableVisiblePhotoIds.count)"
        selectAllBar.isEnabled = !viewModel.selectableVisiblePhotoIds.isEmpty
        selectAllBar.alpha = selectAllBar.isEnabled ? 1 : 0.55
    }

    private func updateSidebarLayout() {
        let showSidebar = viewModel.sidebarExpanded && !viewModel.sidebarGroups.isEmpty
        sidebarView.isHidden = !showSidebar
        sidebarDivider.isHidden = !showSidebar
        expandSidebarButton.isHidden = viewModel.sidebarExpanded || viewModel.sidebarGroups.isEmpty
        sidebarView.snp.remakeConstraints { make in
            make.top.leading.bottom.equalToSuperview()
            make.width.equalTo(showSidebar ? WiredTransferMetrics.sidebarWidth : 0)
        }
        sidebarDivider.snp.remakeConstraints { make in
            make.top.bottom.equalToSuperview()
            make.leading.equalTo(sidebarView.snp.trailing)
            make.width.equalTo(showSidebar ? 1 : 0)
        }
        listContainerView.snp.remakeConstraints { make in
            make.top.bottom.trailing.equalToSuperview()
            make.leading.equalTo(sidebarDivider.snp.trailing)
        }
    }

    private func scrollToPendingTimeSlotIfNeeded() {
        guard let slotId = pendingScrollSlotId,
              let sectionIndex = currentSections.firstIndex(where: { $0.id == slotId }),
              collectionView.numberOfItems(inSection: sectionIndex) > 0 else {
            pendingScrollSlotId = nil
            return
        }
        collectionView.scrollToItem(
            at: IndexPath(item: 0, section: sectionIndex),
            at: .top,
            animated: true
        )
        pendingScrollSlotId = nil
    }

    private func configureBlueNavigationBarAppearance() {
        let appearance = UINavigationBarAppearance()
        appearance.configureWithOpaqueBackground()
        appearance.backgroundColor = AppColor.primary
        appearance.shadowColor = .clear
        appearance.titleTextAttributes = [
            .foregroundColor: UIColor.white,
            .font: UIFont.systemFont(ofSize: 17, weight: .semibold),
        ]

        navigationItem.standardAppearance = appearance
        navigationItem.scrollEdgeAppearance = appearance
        navigationItem.compactAppearance = appearance
    }

    private func applyNavigationBarStyle() {
        guard let navigationBar = navigationController?.navigationBar else { return }
        if previousNavigationBarStyle == nil {
            previousNavigationBarStyle = (navigationBar.tintColor, navigationBar.barStyle, navigationBar.isTranslucent)
        }
        navigationBar.tintColor = .white
        navigationBar.barStyle = .black
        navigationBar.isTranslucent = false
    }

    private func restoreNavigationBarStyle() {
        guard let previousNavigationBarStyle, let navigationBar = navigationController?.navigationBar else {
            return
        }
        navigationBar.tintColor = previousNavigationBarStyle.tintColor
        navigationBar.barStyle = previousNavigationBarStyle.barStyle
        navigationBar.isTranslucent = previousNavigationBarStyle.isTranslucent
        self.previousNavigationBarStyle = nil
    }

    private func configurePhotoCell(_ cell: WiredTransferPhotoCell, item: TravelAlbumOTGPhotoItem) {
        let selected = viewModel.selectedPhotoIds.contains(item.id)
        cell.apply(item: item, selectionMode: viewModel.selectUploadMode, selected: selected)
        cell.onRetry = { [weak self] in self?.viewModel.retryPhoto(photoId: item.id) }
        cell.onDelete = { [weak self] in self?.viewModel.deletePhoto(photoId: item.id) }
    }

    private func reconfigureVisiblePhotoCells() {
        for indexPath in collectionView.indexPathsForVisibleItems {
            guard let cell = collectionView.cellForItem(at: indexPath) as? WiredTransferPhotoCell,
                  let item = dataSource.itemIdentifier(for: indexPath) else {
                continue
            }
            configurePhotoCell(cell, item: item)
        }
    }

    private func syncSidebarSelectionWithCurrentScrollPosition() {
        guard !currentSections.isEmpty else { return }
        let visibleTopY = collectionView.contentOffset.y
            + collectionView.adjustedContentInset.top
            + WiredTransferMetrics.sectionHeaderHeight
        let visibleItems = collectionView.indexPathsForVisibleItems.compactMap { indexPath -> (IndexPath, CGRect)? in
            guard let attributes = collectionView.layoutAttributesForItem(at: indexPath) else { return nil }
            return (indexPath, attributes.frame)
        }
        guard let target = visibleItems
            .filter({ $0.1.maxY >= visibleTopY })
            .min(by: { lhs, rhs in
                abs(lhs.1.minY - visibleTopY) < abs(rhs.1.minY - visibleTopY)
            }),
            let slotId = currentSections[safe: target.0.section]?.id,
            slotId != viewModel.selectedTimeSlotId else {
            return
        }
        viewModel.syncVisibleTimeSlot(id: slotId)
        sidebarView.apply(
            groups: viewModel.sidebarGroups,
            selectedSlotId: viewModel.selectedTimeSlotId
        )
    }

    private func rebuildStats() {
        statsCard.arrangedSubviews.forEach { view in
            statsCard.removeArrangedSubview(view)
            view.removeFromSuperview()
        }
        for tab in TravelAlbumOTGTransferTab.allCases {
            let selected = viewModel.selectedTab == tab
            let button = makeStatButton(
                title: tab.title,
                count: viewModel.tabCounts[tab.rawValue],
                selected: selected,
                danger: tab == .failed
            )
            button.tag = tab.rawValue
            button.addTarget(self, action: #selector(tabTapped(_:)), for: .touchUpInside)
            statsCard.addArrangedSubview(button)
        }
        statsCard.bringSubviewToFront(firstStatsDivider)
        statsCard.bringSubviewToFront(secondStatsDivider)
    }

    private func makeStatButton(title: String, count: Int, selected: Bool, danger: Bool) -> UIButton {
        var config = UIButton.Configuration.plain()
        config.title = "\(count)"
        config.subtitle = title
        config.titleAlignment = .center
        config.titlePadding = 2
        config.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
        let foregroundColor = selected ? AppColor.primary : (danger ? AppColor.danger : AppColor.textPrimary)
        config.baseForegroundColor = foregroundColor
        config.background.backgroundColor = .white
        config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
            var outgoing = incoming
            outgoing.font = .systemFont(ofSize: 17, weight: selected ? .semibold : .medium)
            outgoing.foregroundColor = foregroundColor
            return outgoing
        }
        config.subtitleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
            var outgoing = incoming
            outgoing.font = .systemFont(ofSize: 12, weight: selected ? .semibold : .regular)
            outgoing.foregroundColor = foregroundColor
            return outgoing
        }
        let button = UIButton(configuration: config)
        button.contentVerticalAlignment = .center
        return button
    }

    private func configureBottomButton(_ button: UIButton, title: String, filled: Bool) {
        button.setTitle(title, for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 15)
        button.layer.cornerRadius = 10
        if filled {
            button.backgroundColor = AppColor.primary
            button.setTitleColor(.white, for: .normal)
        } else {
            button.backgroundColor = .white
            button.setTitleColor(AppColor.primary, for: .normal)
            button.layer.borderWidth = 1
            button.layer.borderColor = AppColor.primary.cgColor
        }
    }

    private func makeLayout() -> UICollectionViewCompositionalLayout {
        UICollectionViewCompositionalLayout { _, _ in
            let itemSize = NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1),
                heightDimension: .estimated(WiredTransferMetrics.rowHeight)
            )
            let item = NSCollectionLayoutItem(layoutSize: itemSize)
            let groupSize = NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1),
                heightDimension: .estimated(WiredTransferMetrics.rowHeight)
            )
            let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
            let section = NSCollectionLayoutSection(group: group)
            let headerSize = NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1),
                heightDimension: .absolute(WiredTransferMetrics.sectionHeaderHeight)
            )
            let header = NSCollectionLayoutBoundarySupplementaryItem(
                layoutSize: headerSize,
                elementKind: UICollectionView.elementKindSectionHeader,
                alignment: .top
            )
            header.pinToVisibleBounds = true
            section.boundarySupplementaryItems = [header]
            return section
        }
    }

    private func helpText(_ sonyHint: String?) -> NSAttributedString {
        let text = NSMutableAttributedString(
            string: sonyHint ?? "未连接，查看",
            attributes: [.foregroundColor: AppColor.textSecondary]
        )
        if sonyHint == nil {
            text.append(NSAttributedString(string: "《帮助文档》", attributes: [.foregroundColor: AppColor.primary]))
        }
        return text
    }

    @objc private func refreshTapped() {
        viewModel.refreshCameraFiles()
    }

    @objc private func batchTapped() {
        viewModel.onBatchUploadButtonClick()
    }

    @objc private func historyImportTapped() {
        guard let importViewModel = viewModel.makeCameraImportViewModel() else { return }
        viewModel.pauseLiveTransferForHistoricalImport()
        let importController = TravelAlbumCameraImportViewController(viewModel: importViewModel)
        importController.onImportFinished = { [weak self] importedCount, failedCount, importedPhotoIds in
            guard let self else { return }
            self.viewModel.handleNewlyImportedPhotoIds(importedPhotoIds)
            self.viewModel.start()
            if failedCount > 0 {
                self.showToast("已导入 \(importedCount) 张，\(failedCount) 张失败")
            } else {
                self.showToast("已导入 \(importedCount) 张照片")
            }
        }
        importController.onImportCancelled = { [weak self] in
            self?.viewModel.start()
        }
        let navigationController = UINavigationController(rootViewController: importController)
        navigationController.modalPresentationStyle = .pageSheet
        present(navigationController, animated: true)
    }

    @objc private func albumImportTapped() {
        var configuration = PHPickerConfiguration(photoLibrary: .shared())
        configuration.filter = .images
        configuration.selectionLimit = 0
        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = self
        present(picker, animated: true)
    }

    @objc private func specifyTapped() {
        guard viewModel.canOpenSpecifyUploadChooser else { return }
        let controller = WiredSpecifyUploadViewController(options: viewModel.specifyUploadOptions)
        controller.onConfirm = { [weak self] option in
            self?.viewModel.onSpecifyUploadOptionSelected(option)
        }
        controller.modalPresentationStyle = .pageSheet
        if let sheet = controller.sheetPresentationController {
            sheet.detents = [
                .custom(identifier: .init("wiredSpecifyUpload")) { _ in 260 }
            ]
            sheet.prefersGrabberVisible = false
            sheet.preferredCornerRadius = 16
        }
        present(controller, animated: true)
    }

    @objc private func selectAllTapped() {
        viewModel.toggleVisibleSelectablePhotos()
    }

    @objc private func toggleSidebarTapped() {
        viewModel.toggleSidebar()
    }

    @objc private func tabTapped(_ sender: UIButton) {
        guard let tab = TravelAlbumOTGTransferTab(rawValue: sender.tag) else { return }
        viewModel.selectTab(tab)
    }

    @objc private func helpTapped() {
        guard let url = WiredCameraTransferHelpDocument.bundledURL() else {
            showToast("帮助文档暂不可用")
            return
        }
        navigationController?.pushViewController(
            AgreementWebViewController(title: WiredCameraTransferHelpDocument.pageTitle, url: url),
            animated: true
        )
    }

    private func presentPhotoPreview(_ item: TravelAlbumOTGPhotoItem) {
        guard let url = item.thumbnailURL else {
            showToast("暂无可预览图片")
            return
        }
        let previewItem: MediaPreviewItem = url.isFileURL
            ? .localImage(url: url)
            : MediaPreviewItem(source: .remoteImage(url))
        MediaPreviewViewController.present(from: self, items: [previewItem], startIndex: 0)
    }
}

extension WiredCameraTransferViewController: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        picker.dismiss(animated: true)
        guard !results.isEmpty else { return }

        Task { @MainActor in
            showLoading()
            let items = await loadPhoneAlbumImportItems(from: results)
            let result = viewModel.importPhoneAlbumImages(items)
            hideLoading()
            if result.failedCount > 0 {
                showToast("已导入 \(result.importedCount) 张，\(result.failedCount) 张失败")
            } else if result.importedCount > 0 {
                showToast("已导入 \(result.importedCount) 张照片")
            } else {
                showToast("未能导入照片")
            }
        }
    }

    private func loadPhoneAlbumImportItems(from results: [PHPickerResult]) async -> [TravelAlbumPhoneAlbumImportItem] {
        var items: [TravelAlbumPhoneAlbumImportItem] = []
        for (index, result) in results.enumerated() {
            if let item = await Self.loadPhoneAlbumImportItem(from: result, index: index) {
                items.append(item)
            }
        }
        return items
    }

    private static func loadPhoneAlbumImportItem(
        from result: PHPickerResult,
        index: Int
    ) async -> TravelAlbumPhoneAlbumImportItem? {
        let provider = result.itemProvider
        let typeIdentifier = preferredImageTypeIdentifier(from: provider)
        let fileName = importFileName(
            suggestedName: provider.suggestedName,
            typeIdentifier: typeIdentifier,
            index: index
        )
        return await withCheckedContinuation { continuation in
            provider.loadDataRepresentation(forTypeIdentifier: typeIdentifier) { data, _ in
                guard let data, !data.isEmpty else {
                    continuation.resume(returning: nil)
                    return
                }
                continuation.resume(
                    returning: TravelAlbumPhoneAlbumImportItem(
                        data: data,
                        fileName: fileName,
                        createdAt: Date()
                    )
                )
            }
        }
    }

    private static func preferredImageTypeIdentifier(from provider: NSItemProvider) -> String {
        provider.registeredTypeIdentifiers.first { identifier in
            UTType(identifier)?.conforms(to: .image) == true
        } ?? UTType.image.identifier
    }

    private static func importFileName(suggestedName: String?, typeIdentifier: String, index: Int) -> String {
        let fallback = "album_image_\(Int(Date().timeIntervalSince1970))_\(index)"
        let rawName = suggestedName?.trimmingCharacters(in: .whitespacesAndNewlines)
        let baseName = rawName?.isEmpty == false ? rawName! : fallback
        guard URL(fileURLWithPath: baseName).pathExtension.isEmpty else {
            return baseName
        }
        let ext = UTType(typeIdentifier)?.preferredFilenameExtension ?? "jpg"
        return "\(baseName).\(ext)"
    }
}

/// OTG 指定上传选项弹窗。
private final class WiredSpecifyUploadViewController: UIViewController {
    var onConfirm: ((TravelAlbumOTGSpecifyUploadOption) -> Void)?

    private let options: [TravelAlbumOTGSpecifyUploadOption]
    private var selectedOption: TravelAlbumOTGSpecifyUploadOption
    private var optionRows: [WiredSpecifyUploadOptionRow] = []
    private let headerView = UIView()
    private let cancelTitleButton = UIButton(type: .system)
    private let titleLabel = UILabel()
    private let optionsStack = UIStackView()
    private let bottomButtonsStack = UIStackView()
    private let cancelButton = UIButton(type: .system)
    private let confirmButton = UIButton(type: .system)

    init(options: [TravelAlbumOTGSpecifyUploadOption]) {
        let resolvedOptions = options.isEmpty ? TravelAlbumOTGSpecifyUploadOption.allCases : options
        self.options = resolvedOptions
        selectedOption = resolvedOptions[0]
        super.init(nibName: nil, bundle: nil)
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        setupConstraints()
    }

    private func setupUI() {
        view.backgroundColor = AppColor.pageBackground

        cancelTitleButton.setTitle("取消", for: .normal)
        cancelTitleButton.setTitleColor(AppColor.primary, for: .normal)
        cancelTitleButton.titleLabel?.font = .systemFont(ofSize: 16)
        cancelTitleButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)

        titleLabel.text = "指定上传"
        titleLabel.font = .systemFont(ofSize: 17, weight: .semibold)
        titleLabel.textColor = AppColor.textPrimary
        titleLabel.textAlignment = .center

        optionsStack.axis = .vertical
        optionsStack.spacing = 12
        for option in options {
            let row = WiredSpecifyUploadOptionRow(option: option)
            row.apply(selected: option == selectedOption)
            row.onTap = { [weak self] option in self?.selectOption(option) }
            optionsStack.addArrangedSubview(row)
            row.snp.makeConstraints { make in
                make.height.equalTo(52)
            }
            optionRows.append(row)
        }

        bottomButtonsStack.axis = .horizontal
        bottomButtonsStack.spacing = 12
        bottomButtonsStack.distribution = .fillEqually
        configureActionButton(cancelButton, title: "取消", filled: false)
        configureActionButton(confirmButton, title: "确认上传", filled: true)
        cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
        confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)

        view.addSubview(headerView)
        headerView.addSubview(cancelTitleButton)
        headerView.addSubview(titleLabel)
        view.addSubview(optionsStack)
        view.addSubview(bottomButtonsStack)
        bottomButtonsStack.addArrangedSubview(cancelButton)
        bottomButtonsStack.addArrangedSubview(confirmButton)
    }

    private func setupConstraints() {
        headerView.snp.makeConstraints { make in
            make.top.leading.trailing.equalToSuperview()
            make.height.equalTo(52)
        }
        cancelTitleButton.snp.makeConstraints { make in
            make.leading.equalToSuperview().offset(16)
            make.centerY.equalToSuperview()
            make.height.equalTo(40)
        }
        titleLabel.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.leading.greaterThanOrEqualTo(cancelTitleButton.snp.trailing).offset(12)
        }
        optionsStack.snp.makeConstraints { make in
            make.top.equalTo(headerView.snp.bottom)
            make.leading.trailing.equalToSuperview().inset(16)
        }
        bottomButtonsStack.snp.makeConstraints { make in
            make.top.equalTo(optionsStack.snp.bottom).offset(20)
            make.leading.trailing.equalToSuperview().inset(16)
            make.height.equalTo(48)
            make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-12)
        }
    }

    private func configureActionButton(_ button: UIButton, title: String, filled: Bool) {
        button.setTitle(title, for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
        button.layer.cornerRadius = 10
        if filled {
            button.backgroundColor = AppColor.primary
            button.setTitleColor(.white, for: .normal)
        } else {
            button.backgroundColor = .white
            button.setTitleColor(AppColor.primary, for: .normal)
            button.layer.borderWidth = 1
            button.layer.borderColor = AppColor.primary.cgColor
        }
    }

    private func selectOption(_ option: TravelAlbumOTGSpecifyUploadOption) {
        selectedOption = option
        optionRows.forEach { row in
            row.apply(selected: row.option == option)
        }
    }

    @objc private func cancelTapped() {
        dismiss(animated: true)
    }

    @objc private func confirmTapped() {
        let option = selectedOption
        dismiss(animated: true) { [onConfirm] in
            onConfirm?(option)
        }
    }
}

/// OTG 指定上传单选行。
private final class WiredSpecifyUploadOptionRow: UIControl {
    let option: TravelAlbumOTGSpecifyUploadOption
    var onTap: ((TravelAlbumOTGSpecifyUploadOption) -> Void)?

    private let titleLabel = UILabel()
    private let radioView = UIImageView()

    init(option: TravelAlbumOTGSpecifyUploadOption) {
        self.option = option
        super.init(frame: .zero)
        setupUI()
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func apply(selected: Bool) {
        radioView.image = UIImage(systemName: selected ? "largecircle.fill.circle" : "circle")
        radioView.tintColor = selected ? AppColor.textPrimary : AppColor.border
    }

    private func setupUI() {
        backgroundColor = .white
        layer.cornerRadius = 10
        layer.borderWidth = 1
        layer.borderColor = AppColor.border.cgColor

        titleLabel.text = option.title
        titleLabel.font = .systemFont(ofSize: 15)
        titleLabel.textColor = AppColor.textPrimary
        radioView.contentMode = .scaleAspectFit
        addTarget(self, action: #selector(rowTapped), for: .touchUpInside)

        addSubview(titleLabel)
        addSubview(radioView)
        titleLabel.snp.makeConstraints { make in
            make.leading.equalToSuperview().offset(14)
            make.centerY.equalToSuperview()
            make.trailing.lessThanOrEqualTo(radioView.snp.leading).offset(-10)
        }
        radioView.snp.makeConstraints { make in
            make.trailing.equalToSuperview().offset(-14)
            make.centerY.equalToSuperview()
            make.size.equalTo(24)
        }
    }

    @objc private func rowTapped() {
        onTap?(option)
    }
}

extension WiredCameraTransferViewController: UICollectionViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        guard scrollView === collectionView else { return }
        syncSidebarSelectionWithCurrentScrollPosition()
    }

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
        if viewModel.selectUploadMode {
            viewModel.toggleTransferPhotoSelection(photoId: item.id)
        } else {
            presentPhotoPreview(item)
        }
    }
}

/// 有线传输照片列表时间段 Header。
private final class WiredTransferSectionHeaderView: UICollectionReusableView {
    static let reuseIdentifier = "WiredTransferSectionHeaderView"

    private let titleLabel = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = AppColor.pageBackground
        titleLabel.font = .systemFont(ofSize: 11)
        titleLabel.textColor = AppColor.textSecondary
        addSubview(titleLabel)
        titleLabel.snp.makeConstraints { make in
            make.leading.trailing.equalToSuperview().inset(10)
            make.centerY.equalToSuperview()
        }
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func apply(title: String) {
        titleLabel.text = title
    }
}

/// 有线传输照片列表 Cell。
private final class WiredTransferPhotoCell: UICollectionViewCell {
    static let reuseIdentifier = "WiredTransferPhotoCell"
    private static let previewImageSize = CGSize(width: 96, height: 96)

    private let selectionIconView = UIImageView()
    private let imageView = UIImageView()
    private let statusLabel = UILabel()
    private let titleLabel = UILabel()
    private let sizeLabel = UILabel()
    private let progressView = UIProgressView(progressViewStyle: .default)
    private let menuButton = UIButton(type: .system)
    private let separatorView = UIView()

    var onRetry: (() -> Void)?
    var onDelete: (() -> Void)?

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

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        imageView.kf.cancelDownloadTask()
        imageView.image = nil
        onRetry = nil
        onDelete = nil
        menuButton.menu = nil
    }

    func apply(item: TravelAlbumOTGPhotoItem, selectionMode: Bool, selected: Bool) {
        let canSelect = item.canSelectForUpload
        let rowAlpha: CGFloat = selectionMode && !canSelect ? 0.45 : 1
        if let url = item.thumbnailURL {
            let processor = DownsamplingImageProcessor(size: Self.previewImageSize)
            imageView.kf.setImage(
                with: url,
                placeholder: UIImage(systemName: "photo"),
                options: [
                    .processor(processor),
                    .scaleFactor(UIScreen.main.scale),
                    .backgroundDecode
                ]
            )
        } else {
            imageView.image = UIImage(systemName: "photo")
            imageView.tintColor = AppColor.textTertiary
        }
        imageView.alpha = rowAlpha
        titleLabel.text = item.fileName
        titleLabel.alpha = rowAlpha
        sizeLabel.text = item.fileSizeText
        sizeLabel.alpha = rowAlpha
        statusLabel.text = statusText(item.status)
        statusLabel.textColor = statusTextColor(item.status)
        statusLabel.backgroundColor = statusBackgroundColor(item.status)
        progressView.isHidden = item.status != .uploading && item.status != .transferring
        progressView.progress = Float(item.progress) / 100.0
        selectionIconView.isHidden = !selectionMode
        selectionIconView.image = UIImage(systemName: selected ? "checkmark.circle.fill" : "circle")
        selectionIconView.tintColor = canSelect ? (selected ? AppColor.primary : AppColor.textTertiary) : AppColor.textTertiary.withAlphaComponent(0.5)
        menuButton.isHidden = selectionMode
        menuButton.menu = makeMenu(canRetry: item.status == .failed || item.status == .pending)
        updateImageConstraints(selectionMode: selectionMode)
    }

    private func setupUI() {
        contentView.backgroundColor = .white
        selectionIconView.contentMode = .scaleAspectFit
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        imageView.layer.cornerRadius = 6
        statusLabel.font = .systemFont(ofSize: 9)
        statusLabel.textAlignment = .center
        statusLabel.layer.cornerRadius = 3
        statusLabel.clipsToBounds = true
        statusLabel.setContentHuggingPriority(.required, for: .horizontal)
        statusLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
        titleLabel.font = .systemFont(ofSize: 12, weight: .medium)
        titleLabel.textColor = AppColor.textPrimary
        titleLabel.lineBreakMode = .byTruncatingMiddle
        titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
        titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        sizeLabel.font = .systemFont(ofSize: 10)
        sizeLabel.textColor = AppColor.textTertiary
        progressView.progressTintColor = AppColor.primary
        progressView.trackTintColor = AppColor.border
        menuButton.setImage(UIImage(systemName: "ellipsis"), for: .normal)
        menuButton.tintColor = AppColor.textSecondary
        menuButton.showsMenuAsPrimaryAction = true
        separatorView.backgroundColor = AppColor.border

        contentView.addSubview(selectionIconView)
        contentView.addSubview(imageView)
        contentView.addSubview(titleLabel)
        contentView.addSubview(statusLabel)
        contentView.addSubview(sizeLabel)
        contentView.addSubview(progressView)
        contentView.addSubview(menuButton)
        contentView.addSubview(separatorView)

        selectionIconView.snp.makeConstraints { make in
            make.leading.equalToSuperview().offset(10)
            make.centerY.equalToSuperview()
            make.size.equalTo(20)
        }
        updateImageConstraints(selectionMode: true)
        titleLabel.snp.makeConstraints { make in
            make.top.equalTo(imageView).offset(1)
            make.leading.equalTo(imageView.snp.trailing).offset(8)
            make.trailing.equalTo(statusLabel.snp.leading).offset(-4)
        }
        statusLabel.snp.makeConstraints { make in
            make.centerY.equalTo(titleLabel)
            make.trailing.equalTo(menuButton.snp.leading).offset(-4)
            make.height.equalTo(17)
            make.width.greaterThanOrEqualTo(44)
        }
        sizeLabel.snp.makeConstraints { make in
            make.top.equalTo(titleLabel.snp.bottom).offset(5)
            make.leading.equalTo(titleLabel)
            make.trailing.equalToSuperview().offset(-10)
        }
        progressView.snp.makeConstraints { make in
            make.leading.equalTo(titleLabel)
            make.trailing.equalToSuperview().offset(-10)
            make.top.equalTo(sizeLabel.snp.bottom).offset(7)
            make.height.equalTo(2)
        }
        menuButton.snp.makeConstraints { make in
            make.centerY.equalTo(titleLabel)
            make.trailing.equalToSuperview().offset(-4)
            make.size.equalTo(32)
        }
        separatorView.snp.makeConstraints { make in
            make.leading.trailing.equalToSuperview().inset(10)
            make.bottom.equalToSuperview()
            make.height.equalTo(1 / UIScreen.main.scale)
        }
    }

    private func makeMenu(canRetry: Bool) -> UIMenu {
        let retry = UIAction(title: "重传", image: UIImage(systemName: "arrow.clockwise")) { [weak self] _ in
            self?.onRetry?()
        }
        retry.attributes = canRetry ? [] : [.disabled]
        let delete = UIAction(title: "删除", image: UIImage(systemName: "trash"), attributes: .destructive) { [weak self] _ in
            self?.onDelete?()
        }
        return UIMenu(children: [retry, delete])
    }

    private func updateImageConstraints(selectionMode: Bool) {
        imageView.snp.remakeConstraints { make in
            if selectionMode {
                make.leading.equalTo(selectionIconView.snp.trailing).offset(8)
            } else {
                make.leading.equalToSuperview().offset(10)
            }
            make.top.equalToSuperview().offset(8)
            make.size.equalTo(48)
            make.bottom.lessThanOrEqualToSuperview().offset(-8)
        }
    }

    private func statusText(_ status: TravelAlbumOTGUploadStatus) -> String {
        switch status {
        case .pending: return "待上传"
        case .transferring: return "传输中"
        case .uploading: return "上传中"
        case .uploaded: return "已上传"
        case .failed: return "上传失败"
        }
    }

    private func statusTextColor(_ status: TravelAlbumOTGUploadStatus) -> UIColor {
        switch status {
        case .pending: return AppColor.textSecondary
        case .transferring, .uploading: return AppColor.primary
        case .uploaded: return UIColor(hex: 0x16A34A)
        case .failed: return AppColor.danger
        }
    }

    private func statusBackgroundColor(_ status: TravelAlbumOTGUploadStatus) -> UIColor {
        switch status {
        case .pending:
            return AppColor.pageBackground
        case .transferring, .uploading:
            return AppColor.primary.withAlphaComponent(0.12)
        case .uploaded:
            return UIColor(hex: 0x16A34A).withAlphaComponent(0.12)
        case .failed:
            return AppColor.danger.withAlphaComponent(0.12)
        }
    }
}

/// 有线传输页左侧时间快速导航。
private final class WiredTransferTimeSidebarView: UIView {
    var onToggleSidebar: (() -> Void)?
    var onTimeSlotSelected: ((String) -> Void)?

    private let headerButton = UIButton(type: .system)
    private let scrollView = UIScrollView()
    private let stackView = UIStackView()
    private weak var selectedRowView: TimeSlotRowView?

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

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func apply(groups: [TravelAlbumOTGDateGroup], selectedSlotId: String?) {
        selectedRowView = nil
        stackView.arrangedSubviews.forEach { view in
            stackView.removeArrangedSubview(view)
            view.removeFromSuperview()
        }

        for group in groups {
            let dateContainer = UIView()
            let dateLabel = UILabel()
            dateLabel.text = group.dateLabel
            dateLabel.font = .systemFont(ofSize: 12)
            dateLabel.textColor = AppColor.textTertiary
            dateContainer.addSubview(dateLabel)
            stackView.addArrangedSubview(dateContainer)
            dateContainer.snp.makeConstraints { make in
                make.height.equalTo(24)
            }
            dateLabel.snp.makeConstraints { make in
                make.leading.equalToSuperview().offset(10)
                make.trailing.equalToSuperview()
                make.bottom.equalToSuperview().offset(-4)
            }

            for slot in group.slots {
                let row = TimeSlotRowView()
                let selected = slot.id == selectedSlotId
                row.apply(slot: slot, selected: selected)
                if selected {
                    selectedRowView = row
                }
                row.onTap = { [weak self] in self?.onTimeSlotSelected?(slot.id) }
                stackView.addArrangedSubview(row)
                row.snp.makeConstraints { make in
                    make.height.equalTo(52)
                }
            }
        }
        scrollSelectedRowIntoVisible()
    }

    private func setupUI() {
        backgroundColor = UIColor(hex: 0xF5F6F8)
        headerButton.setTitle("收起", for: .normal)
        headerButton.setImage(UIImage(systemName: "chevron.left"), for: .normal)
        headerButton.tintColor = AppColor.textSecondary
        headerButton.setTitleColor(AppColor.textSecondary, for: .normal)
        headerButton.titleLabel?.font = .systemFont(ofSize: 11)
        headerButton.semanticContentAttribute = .forceLeftToRight
        headerButton.addTarget(self, action: #selector(toggleTapped), for: .touchUpInside)
        scrollView.showsVerticalScrollIndicator = false
        stackView.axis = .vertical

        addSubview(headerButton)
        addSubview(scrollView)
        scrollView.addSubview(stackView)

        headerButton.snp.makeConstraints { make in
            make.top.leading.trailing.equalToSuperview()
            make.height.equalTo(36)
        }
        scrollView.snp.makeConstraints { make in
            make.top.equalTo(headerButton.snp.bottom)
            make.leading.trailing.bottom.equalToSuperview()
        }
        stackView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
            make.width.equalToSuperview()
        }
    }

    @objc private func toggleTapped() {
        onToggleSidebar?()
    }

    private func scrollSelectedRowIntoVisible() {
        guard let selectedRowView else { return }
        layoutIfNeeded()
        let rowFrame = selectedRowView.convert(selectedRowView.bounds, to: stackView)
        scrollView.scrollRectToVisible(rowFrame.insetBy(dx: 0, dy: -8), animated: false)
    }
}

private final class TimeSlotRowView: UIControl {
    private let timeLabel = UILabel()
    private let countLabel = UILabel()
    private let arrowView = UIImageView(image: UIImage(systemName: "play.fill"))

    var onTap: (() -> Void)?

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

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func apply(slot: TravelAlbumOTGTimeSlot, selected: Bool) {
        backgroundColor = selected ? .white : .clear
        timeLabel.text = slot.slotStartLabel
        timeLabel.font = .systemFont(ofSize: 13, weight: selected ? .semibold : .regular)
        timeLabel.textColor = selected ? AppColor.primary : AppColor.textPrimary
        arrowView.tintColor = selected ? AppColor.primary : AppColor.textPrimary
        countLabel.text = "\(slot.photoCount)张"
    }

    private func setupUI() {
        arrowView.contentMode = .scaleAspectFit
        timeLabel.textAlignment = .center
        countLabel.font = .systemFont(ofSize: 10)
        countLabel.textColor = AppColor.textTertiary
        countLabel.textAlignment = .center
        addTarget(self, action: #selector(rowTapped), for: .touchUpInside)

        addSubview(arrowView)
        addSubview(timeLabel)
        addSubview(countLabel)

        arrowView.snp.makeConstraints { make in
            make.centerY.equalTo(timeLabel)
            make.trailing.equalTo(timeLabel.snp.leading).offset(-2)
            make.size.equalTo(10)
        }
        timeLabel.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(8)
            make.centerX.equalToSuperview().offset(6)
        }
        countLabel.snp.makeConstraints { make in
            make.top.equalTo(timeLabel.snp.bottom).offset(2)
            make.centerX.equalToSuperview()
        }
    }

    @objc private func rowTapped() {
        onTap?()
    }
}

private extension Array {
    subscript(safe index: Index) -> Element? {
        indices.contains(index) ? self[index] : nil
    }
}
