//
//  UploadSampleViewController.swift
//  suixinkan
//

import AVFoundation
import PhotosUI
import SnapKit
import UniformTypeIdentifiers
import UIKit

/// 样片相册上传页，对齐 Android `UploadSampleScreen`。
final class UploadSampleViewController: SampleBaseViewController {
    private enum PickTarget {
        case material
        case cover
    }

    private let viewModel: UploadSampleViewModel
    private let api: any SampleManagementServing
    private let uploader: any SampleOSSUploading

    private let scrollView = UIScrollView()
    private let formCard = UIView()
    private let formStack = UIStackView()
    private let nameField = UITextField()
    private let nameSection = SampleFormFieldView(title: "相册名称")
    private let mediaTypeButton = SampleSelectButton()
    private let mediaTypeSection = SampleFormFieldView(title: "素材类型")
    private let mediaGridView = SampleUploadGridView()
    private let materialSection = SampleFormFieldView(title: "上传素材")
    private let materialHintLabel = UILabel()
    private let coverView = SampleCoverUploadView()
    private let coverSection = SampleFormFieldView(title: "上传封面")
    private let scenicButton = SampleSelectButton()
    private let scenicSection = SampleFormFieldView(title: "关联景区")
    private let spotButton = SampleSelectButton()
    private let spotSection = SampleFormFieldView(title: "关联打卡点")
    private let projectButton = SampleSelectButton()
    private let projectSection = SampleFormFieldView(title: "关联项目")
    private let bottomBar = UIView()
    private let cancelButton = UIButton(type: .system)
    private let confirmButton = UIButton(type: .system)
    private let progressOverlay = SampleUploadProgressOverlay()
    private let dropdownOverlay = SampleAnchoredDropdownView()

    private var pickTarget: PickTarget = .material

    var onUploadSuccess: (() -> Void)?

    /// 初始化样片上传页。
    init(
        viewModel: UploadSampleViewModel = UploadSampleViewModel(),
        api: (any SampleManagementServing)? = nil,
        uploader: (any SampleOSSUploading)? = nil
    ) {
        self.viewModel = viewModel
        self.api = api ?? NetworkServices.shared.sampleManagementAPI
        self.uploader = uploader ?? NetworkServices.shared.ossUploadService
        super.init(nibName: nil, bundle: nil)
    }

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

    override func setupNavigationBar() {
        super.setupNavigationBar()
        setSampleTitle("样片相册")
    }

    override func setupUI() {
        super.setupUI()
        view.backgroundColor = SampleManagementUITokens.pageBackground
        formCard.backgroundColor = .white
        formCard.layer.cornerRadius = 12
        formCard.clipsToBounds = true
        formStack.axis = .vertical
        formStack.spacing = 0

        nameField.placeholder = "请输入相册名称"
        nameField.font = .systemFont(ofSize: 14)
        nameField.textColor = .black
        nameField.layer.borderColor = UIColor(hex: 0xEEEEEE).cgColor
        nameField.layer.borderWidth = 1
        nameField.layer.cornerRadius = 8
        nameField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 44))
        nameField.leftViewMode = .always
        nameField.clearButtonMode = .whileEditing
        nameSection.setContent(nameField)

        mediaTypeSection.setContent(mediaTypeButton)
        materialSection.setContent(makeMaterialContent())
        coverSection.setContent(coverView)
        scenicSection.setContent(scenicButton)
        spotSection.setContent(spotButton)
        projectSection.setContent(projectButton)

        [nameSection, mediaTypeSection, materialSection, coverSection, scenicSection, spotSection, projectSection]
            .forEach(formStack.addArrangedSubview)
        formStack.setCustomSpacing(8, after: nameSection)
        formStack.setCustomSpacing(8, after: mediaTypeSection)
        formStack.setCustomSpacing(16, after: materialSection)
        formStack.setCustomSpacing(16, after: coverSection)
        formStack.setCustomSpacing(8, after: scenicSection)
        formStack.setCustomSpacing(8, after: spotSection)

        bottomBar.backgroundColor = .white
        configureBottomButton(cancelButton, title: "取消", backgroundColor: UIColor(hex: 0xEFF6FF), titleColor: AppColor.primary)
        configureBottomButton(confirmButton, title: "确认", backgroundColor: AppColor.primary, titleColor: .white)

        view.addSubview(scrollView)
        scrollView.addSubview(formCard)
        formCard.addSubview(formStack)
        view.addSubview(bottomBar)
        bottomBar.addSubview(cancelButton)
        bottomBar.addSubview(confirmButton)
        view.addSubview(dropdownOverlay)
        view.addSubview(progressOverlay)
        progressOverlay.isHidden = true
    }

    override func setupConstraints() {
        bottomBar.snp.makeConstraints { make in
            make.leading.trailing.bottom.equalToSuperview()
        }
        cancelButton.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(16)
            make.leading.equalToSuperview().offset(15)
            make.width.equalTo(100)
            make.height.equalTo(48)
            make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-16)
        }
        confirmButton.snp.makeConstraints { make in
            make.top.height.equalTo(cancelButton)
            make.leading.equalTo(cancelButton.snp.trailing).offset(12)
            make.trailing.equalToSuperview().inset(15)
        }
        scrollView.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide)
            make.leading.trailing.equalTo(view.safeAreaLayoutGuide)
            make.bottom.equalTo(bottomBar.snp.top)
        }
        formCard.snp.makeConstraints { make in
            make.edges.equalToSuperview().inset(16)
            make.width.equalTo(scrollView).offset(-32)
        }
        formStack.snp.makeConstraints { make in
            make.edges.equalToSuperview().inset(16)
        }
        nameField.snp.makeConstraints { make in
            make.height.equalTo(44)
        }
        [mediaTypeButton, scenicButton, spotButton, projectButton].forEach { button in
            button.snp.makeConstraints { make in
                make.height.equalTo(44)
            }
        }
        progressOverlay.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        dropdownOverlay.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }

    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) }
        }
        viewModel.onUploadSuccess = { [weak self] in
            Task { @MainActor in
                self?.onUploadSuccess?()
                self?.navigationController?.popViewController(animated: true)
            }
        }

        nameField.addTarget(self, action: #selector(nameChanged), for: .editingChanged)
        mediaTypeButton.addTarget(self, action: #selector(mediaTypeTapped), for: .touchUpInside)
        mediaGridView.onAdd = { [weak self] in self?.presentPickerSource(target: .material) }
        mediaGridView.onDelete = { [weak self] index in self?.viewModel.deleteMaterial(at: index) }
        mediaGridView.onPreview = { [weak self] index in self?.presentMediaPreview(startingAt: index) }
        coverView.onAdd = { [weak self] in self?.presentPickerSource(target: .cover) }
        coverView.onDelete = { [weak self] in self?.viewModel.deleteCoverImage() }
        scenicButton.addTarget(self, action: #selector(scenicTapped), for: .touchUpInside)
        spotButton.addTarget(self, action: #selector(spotTapped), for: .touchUpInside)
        projectButton.addTarget(self, action: #selector(projectTapped), for: .touchUpInside)
        cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
        confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
        applyViewModel()
    }

    private func makeMaterialContent() -> UIView {
        let container = UIView()
        materialHintLabel.text = "建议尺寸: 1920x1080px, 大小不超过20MB"
        materialHintLabel.font = .systemFont(ofSize: 12)
        materialHintLabel.textColor = UIColor(hex: 0xB6BECA)
        container.addSubview(mediaGridView)
        container.addSubview(materialHintLabel)
        mediaGridView.snp.makeConstraints { make in
            make.top.leading.trailing.equalToSuperview()
        }
        materialHintLabel.snp.makeConstraints { make in
            make.top.equalTo(mediaGridView.snp.bottom).offset(8)
            make.leading.trailing.bottom.equalToSuperview()
        }
        return container
    }

    private func configureBottomButton(_ button: UIButton, title: String, backgroundColor: UIColor, titleColor: UIColor) {
        button.setTitle(title, for: .normal)
        button.setTitleColor(titleColor, for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
        button.backgroundColor = backgroundColor
        button.layer.cornerRadius = 8
    }

    private func applyViewModel() {
        if nameField.text != viewModel.albumName {
            nameField.text = viewModel.albumName
        }
        mediaTypeButton.setTitle(viewModel.mediaType.title, placeholder: "")
        scenicButton.setTitle(viewModel.selectedScenic?.name, placeholder: "请选择景区")
        spotButton.setTitle(viewModel.selectedScenicSpot?.name, placeholder: "请选择关联打卡点")
        projectButton.setTitle(viewModel.selectedProject?.name, placeholder: "请选择关联项目")
        mediaGridView.apply(items: viewModel.uploadedMediaList, mediaType: viewModel.mediaType)
        coverView.apply(item: viewModel.coverImage)
        confirmButton.alpha = viewModel.isSubmitting ? 0.6 : 1
        confirmButton.isEnabled = !viewModel.isSubmitting
        if let dialog = viewModel.uploadDialogState {
            progressOverlay.apply(state: dialog)
            progressOverlay.isHidden = false
        } else {
            progressOverlay.isHidden = true
        }
    }

    @objc private func nameChanged() {
        viewModel.updateAlbumName(nameField.text ?? "")
    }

    @objc private func mediaTypeTapped() {
        let types = SampleMediaType.allCases
        dropdownOverlay.show(
            anchor: mediaTypeButton,
            options: types.map { .init(title: $0.title, isSelected: $0 == viewModel.mediaType) }
        ) { [weak self] index in
            guard types.indices.contains(index) else { return }
            self?.viewModel.selectMediaType(types[index])
        }
    }

    @objc private func scenicTapped() {
        let scenics = viewModel.scenicList
        dropdownOverlay.show(
            anchor: scenicButton,
            options: scenics.map { .init(title: $0.name, isSelected: $0.id == viewModel.selectedScenic?.id) }
        ) { [weak self] index in
            guard let self, scenics.indices.contains(index) else { return }
            Task { await self.viewModel.selectScenic(scenics[index], api: self.api) }
        }
    }

    @objc private func spotTapped() {
        guard viewModel.selectedScenic != nil else {
            showToast("请先选择关联景区")
            return
        }
        let spots = viewModel.scenicSpotList
        dropdownOverlay.show(
            anchor: spotButton,
            options: spots.map { .init(title: $0.name, isSelected: $0.id == viewModel.selectedScenicSpot?.id) }
        ) { [weak self] index in
            guard spots.indices.contains(index) else { return }
            self?.viewModel.selectScenicSpot(spots[index])
        }
    }

    @objc private func projectTapped() {
        guard viewModel.selectedScenic != nil else {
            showToast("请先选择景区")
            return
        }
        Task {
            if viewModel.projectList.isEmpty {
                showLoading()
                await viewModel.loadProjects(refresh: true, api: api)
                hideLoading()
            }
            await MainActor.run { self.presentProjectDropdown() }
        }
    }

    @MainActor
    private func presentProjectDropdown() {
        let projects = viewModel.projectList
        var options = projects.map { SampleAnchoredDropdownView.Option(title: $0.name, isSelected: $0.id == viewModel.selectedProject?.id) }
        if viewModel.projectCanLoadMore {
            options.append(.init(title: "加载更多...", isLoadMore: true))
        }
        dropdownOverlay.show(
            anchor: projectButton,
            options: options,
            onReachedBottom: { [weak self] in self?.loadMoreProjectsAndRefreshDropdown() }
        ) { [weak self] index in
            guard let self else { return }
            if projects.indices.contains(index) {
                self.viewModel.selectProject(projects[index])
            } else if self.viewModel.projectCanLoadMore {
                self.loadMoreProjectsAndRefreshDropdown()
            }
        }
    }

    private func loadMoreProjectsAndRefreshDropdown() {
        Task {
            await viewModel.loadProjects(refresh: false, api: api)
            await MainActor.run { self.presentProjectDropdown() }
        }
    }

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

    @objc private func confirmTapped() {
        Task {
            showLoading()
            defer { hideLoading() }
            await viewModel.uploadSample(api: api)
        }
    }

    private func presentPickerSource(target: PickTarget) {
        pickTarget = target
        let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
        if UIImagePickerController.isSourceTypeAvailable(.camera) {
            let cameraTitle = viewModel.mediaType == .video && target == .material ? "拍摄视频" : "拍照"
            sheet.addAction(UIAlertAction(title: cameraTitle, style: .default) { [weak self] _ in
                self?.presentCamera()
            })
        }
        sheet.addAction(UIAlertAction(title: "从相册选择", style: .default) { [weak self] _ in
            self?.presentPhotoPicker()
        })
        sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
        present(sheet, animated: true)
    }

    private func presentMediaPreview(startingAt sourceIndex: Int) {
        let completed = viewModel.uploadedMediaList.enumerated().filter { !$0.element.isUploading && !$0.element.ossUrl.isEmpty }
        guard let startIndex = completed.firstIndex(where: { $0.offset == sourceIndex }) else { return }
        let previewItems = completed.compactMap { entry -> MediaPreviewItem? in
            switch viewModel.mediaType {
            case .image: return .remoteImage(entry.element.ossUrl)
            case .video: return .remoteVideo(entry.element.ossUrl)
            }
        }
        guard !previewItems.isEmpty else { return }
        presentMediaPreview(items: previewItems, startIndex: min(startIndex, previewItems.count - 1))
    }

    private func presentPhotoPicker() {
        var configuration = PHPickerConfiguration(photoLibrary: .shared())
        if pickTarget == .cover {
            configuration.filter = .images
            configuration.selectionLimit = 1
        } else {
            configuration.filter = viewModel.mediaType == .image ? .images : .videos
            configuration.selectionLimit = viewModel.mediaType == .video
                ? 1
                : max(1, viewModel.mediaType.maxCount - viewModel.uploadedMediaList.count)
        }
        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = self
        present(picker, animated: true)
    }

    private func presentCamera() {
        let picker = UIImagePickerController()
        picker.sourceType = .camera
        picker.delegate = self
        if pickTarget == .material, viewModel.mediaType == .video {
            picker.mediaTypes = [UTType.movie.identifier]
            picker.cameraCaptureMode = .video
        } else {
            picker.mediaTypes = [UTType.image.identifier]
            picker.cameraCaptureMode = .photo
        }
        present(picker, animated: true)
    }

    private func uploadPickedItems(_ items: [SampleUploadedMediaItem]) {
        guard !items.isEmpty else { return }
        Task {
            switch pickTarget {
            case .material:
                await viewModel.addLocalMedia(items, uploader: uploader)
            case .cover:
                await viewModel.setCoverImage(items[0], uploader: uploader)
            }
        }
    }

    private func makeImageItem(image: UIImage, fileName: String) -> SampleUploadedMediaItem? {
        guard let data = image.jpegData(compressionQuality: 0.9) else { return nil }
        return SampleUploadedMediaItem(
            data: data,
            fileName: fileName,
            width: Int(image.size.width * image.scale),
            height: Int(image.size.height * image.scale)
        )
    }

    private func makeVideoItem(data: Data, fileName: String, sourceURL: URL) -> SampleUploadedMediaItem {
        let preview = videoPreview(url: sourceURL)
        return SampleUploadedMediaItem(
            data: data,
            thumbnailData: preview.thumbnailData,
            fileName: fileName,
            width: preview.width,
            height: preview.height
        )
    }

    private func videoPreview(url: URL) -> (thumbnailData: Data?, width: Int, height: Int) {
        let generator = AVAssetImageGenerator(asset: AVAsset(url: url))
        generator.appliesPreferredTrackTransform = true
        guard let imageRef = try? generator.copyCGImage(at: .zero, actualTime: nil) else {
            return (nil, 0, 0)
        }
        return (
            UIImage(cgImage: imageRef).jpegData(compressionQuality: 0.75),
            imageRef.width,
            imageRef.height
        )
    }
}

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

        var loadedItems = [SampleUploadedMediaItem?](repeating: nil, count: results.count)
        let group = DispatchGroup()

        for (index, result) in results.enumerated() {
            let provider = result.itemProvider
            if pickTarget == .material, viewModel.mediaType == .video {
                group.enter()
                provider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { [weak self] url, _ in
                    defer { group.leave() }
                    guard let self, let url else { return }
                    let data = (try? Data(contentsOf: url)) ?? Data()
                    guard !data.isEmpty else { return }
                    let fileName = self.resolvedFileName(provider: provider, url: url, fallbackExtension: "mp4")
                    loadedItems[index] = self.makeVideoItem(data: data, fileName: fileName, sourceURL: url)
                }
            } else {
                group.enter()
                let imageType = provider.registeredTypeIdentifiers.first {
                    UTType($0)?.conforms(to: .image) == true
                } ?? UTType.image.identifier
                provider.loadFileRepresentation(forTypeIdentifier: imageType) { [weak self] url, _ in
                    defer { group.leave() }
                    guard let self, let url, let data = try? Data(contentsOf: url), let image = UIImage(data: data) else { return }
                    let pathExtension = url.pathExtension
                    let ext = UTType(imageType)?.preferredFilenameExtension ?? (pathExtension.isEmpty ? "jpg" : pathExtension)
                    let fileName = self.resolvedFileName(provider: provider, url: url, fallbackExtension: ext)
                    loadedItems[index] = SampleUploadedMediaItem(
                        data: data,
                        thumbnailData: image.jpegData(compressionQuality: 0.75),
                        fileName: fileName,
                        size: Int64(data.count),
                        width: Int(image.size.width * image.scale),
                        height: Int(image.size.height * image.scale)
                    )
                }
            }
        }

        group.notify(queue: .main) { [weak self] in
            self?.uploadPickedItems(loadedItems.compactMap { $0 })
        }
    }

    private func resolvedFileName(provider: NSItemProvider, url: URL, fallbackExtension: String) -> String {
        let suggested = provider.suggestedName?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        if !suggested.isEmpty {
            return URL(fileURLWithPath: suggested).pathExtension.isEmpty
                ? "\(suggested).\(fallbackExtension)"
                : suggested
        }
        let urlName = url.lastPathComponent
        if !urlName.isEmpty, urlName != "." { return urlName }
        return "sample_\(Int(Date().timeIntervalSince1970)).\(fallbackExtension)"
    }
}

extension UploadSampleViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true)
    }

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
        picker.dismiss(animated: true)
        if pickTarget == .material, viewModel.mediaType == .video,
           let url = info[.mediaURL] as? URL,
           let data = try? Data(contentsOf: url) {
            uploadPickedItems([makeVideoItem(data: data, fileName: url.lastPathComponent, sourceURL: url)])
            return
        }
        if let image = info[.originalImage] as? UIImage,
           let item = makeImageItem(image: image, fileName: "sample_image_\(Int(Date().timeIntervalSince1970)).jpg") {
            uploadPickedItems([item])
        }
    }
}

/// 样片上传表单字段容器。
private final class SampleFormFieldView: UIView {
    private let titleLabel = UILabel()
    private let contentHolder = UIView()

    init(title: String) {
        super.init(frame: .zero)
        titleLabel.text = title
        titleLabel.font = .systemFont(ofSize: 14, weight: .medium)
        titleLabel.textColor = .black
        addSubview(titleLabel)
        addSubview(contentHolder)
        titleLabel.snp.makeConstraints { make in
            make.top.leading.trailing.equalToSuperview()
        }
        contentHolder.snp.makeConstraints { make in
            make.top.equalTo(titleLabel.snp.bottom).offset(8)
            make.leading.trailing.bottom.equalToSuperview()
        }
    }

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

    func setContent(_ view: UIView) {
        contentHolder.subviews.forEach { $0.removeFromSuperview() }
        contentHolder.addSubview(view)
        view.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }
}

/// 样片上传下拉选择按钮。
private final class SampleSelectButton: UIControl {
    private let titleLabel = UILabel()
    private let chevronView = UIImageView(image: UIImage(systemName: "chevron.down"))

    override init(frame: CGRect) {
        super.init(frame: frame)
        layer.borderColor = UIColor(hex: 0xEEEEEE).cgColor
        layer.borderWidth = 1
        layer.cornerRadius = 8
        titleLabel.font = .systemFont(ofSize: 14)
        chevronView.tintColor = .black
        addSubview(titleLabel)
        addSubview(chevronView)
        titleLabel.snp.makeConstraints { make in
            make.leading.equalToSuperview().offset(12)
            make.centerY.equalToSuperview()
            make.trailing.lessThanOrEqualTo(chevronView.snp.leading).offset(-8)
        }
        chevronView.snp.makeConstraints { make in
            make.trailing.equalToSuperview().inset(12)
            make.centerY.equalToSuperview()
            make.size.equalTo(20)
        }
    }

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

    func setTitle(_ title: String?, placeholder: String) {
        let text = title?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        titleLabel.text = text.isEmpty ? placeholder : text
        titleLabel.textColor = text.isEmpty ? UIColor(hex: 0x4B5563) : .black
    }
}

/// 样片素材上传网格。
private final class SampleUploadGridView: UIView {
    private let stack = UIStackView()
    private var items: [SampleUploadedMediaItem] = []
    private var mediaType: SampleMediaType = .image
    var onAdd: (() -> Void)?
    var onDelete: ((Int) -> Void)?
    var onPreview: ((Int) -> Void)?

    override init(frame: CGRect) {
        super.init(frame: frame)
        stack.axis = .vertical
        stack.spacing = 8
        addSubview(stack)
        stack.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }

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

    func apply(items: [SampleUploadedMediaItem], mediaType: SampleMediaType) {
        self.items = items
        self.mediaType = mediaType
        stack.arrangedSubviews.forEach { $0.removeFromSuperview() }
        let showAdd = items.count < mediaType.maxCount
        let cells = items.map(Optional.some) + (showAdd ? [nil] : [])
        let rows = Int(ceil(Double(cells.count) / 3.0))
        guard rows > 0 else { return }
        for row in 0 ..< rows {
            let rowStack = UIStackView()
            rowStack.axis = .horizontal
            rowStack.spacing = 8
            rowStack.distribution = .fillEqually
            stack.addArrangedSubview(rowStack)
            for column in 0 ..< 3 {
                let index = row * 3 + column
                if index < cells.count {
                    rowStack.addArrangedSubview(makeCell(item: cells[index], index: index))
                } else {
                    rowStack.addArrangedSubview(UIView())
                }
            }
        }
    }

    private func makeCell(item: SampleUploadedMediaItem?, index: Int) -> UIView {
        let cell = SampleUploadThumbView(mediaType: mediaType)
        cell.snp.makeConstraints { make in
            make.height.equalTo(cell.snp.width).dividedBy(1.3)
        }
        if let item {
            cell.apply(item: item)
            cell.onDelete = { [weak self] in self?.onDelete?(index) }
            cell.onPreview = { [weak self] in self?.onPreview?(index) }
        } else {
            cell.applyAdd()
            cell.onAdd = { [weak self] in self?.onAdd?() }
        }
        return cell
    }
}

/// 样片上传缩略图格子。
private final class SampleUploadThumbView: UIControl {
    private let imageView = UIImageView()
    private let addCircle = UIView()
    private let addIcon = UIImageView(image: UIImage(named: "material_ic_add"))
    private let textLabel = UILabel()
    private let deleteButton = SampleExpandedTouchButton(type: .system)
    private let playIcon = UIImageView(image: UIImage(named: "material_ic_play"))
    private let mediaType: SampleMediaType
    private let dashedLayer = CAShapeLayer()
    private var previewEnabled = false

    var onAdd: (() -> Void)?
    var onDelete: (() -> Void)?
    var onPreview: (() -> Void)?

    init(mediaType: SampleMediaType) {
        self.mediaType = mediaType
        super.init(frame: .zero)
        layer.cornerRadius = 8
        clipsToBounds = true
        backgroundColor = .white
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        addCircle.backgroundColor = AppColor.primary
        addCircle.layer.cornerRadius = 14
        addIcon.tintColor = .white
        textLabel.font = .systemFont(ofSize: 12)
        textLabel.textColor = UIColor(hex: 0x4B5563)
        textLabel.textAlignment = .center
        deleteButton.setImage(UIImage(named: "material_ic_clear"), for: .normal)
        deleteButton.tintColor = .white
        deleteButton.backgroundColor = UIColor.black.withAlphaComponent(0.6)
        deleteButton.layer.cornerRadius = 10
        playIcon.tintColor = .white
        playIcon.isHidden = true
        [imageView, addCircle, addIcon, textLabel, playIcon].forEach {
            $0.isUserInteractionEnabled = false
        }
        accessibilityLabel = mediaType == .image ? "上传图片" : "上传视频"

        addSubview(imageView)
        addSubview(addCircle)
        addCircle.addSubview(addIcon)
        addSubview(textLabel)
        addSubview(deleteButton)
        addSubview(playIcon)
        imageView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        addCircle.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.centerY.equalToSuperview().offset(-10)
            make.size.equalTo(28)
        }
        addIcon.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.size.equalTo(20)
        }
        textLabel.snp.makeConstraints { make in
            make.top.equalTo(addCircle.snp.bottom).offset(4)
            make.leading.trailing.equalToSuperview().inset(4)
        }
        deleteButton.snp.makeConstraints { make in
            make.top.trailing.equalToSuperview().inset(4)
            make.size.equalTo(20)
        }
        playIcon.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.size.equalTo(32)
        }
        dashedLayer.strokeColor = UIColor(hex: 0xE5E7EB).cgColor
        dashedLayer.fillColor = UIColor.clear.cgColor
        dashedLayer.lineWidth = 1
        dashedLayer.lineDashPattern = [6, 6]
        layer.addSublayer(dashedLayer)
        addTarget(self, action: #selector(cellTapped), for: .touchUpInside)
        deleteButton.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside)
    }

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

    override func layoutSubviews() {
        super.layoutSubviews()
        dashedLayer.frame = bounds
        dashedLayer.path = UIBezierPath(roundedRect: bounds.insetBy(dx: 0.5, dy: 0.5), cornerRadius: 8).cgPath
    }

    func applyAdd() {
        imageView.image = nil
        backgroundColor = .white
        layer.borderWidth = 0
        dashedLayer.isHidden = false
        previewEnabled = false
        addCircle.isHidden = false
        textLabel.isHidden = false
        textLabel.text = mediaType == .image ? "点击上传图片" : "点击上传视频"
        deleteButton.isHidden = true
        playIcon.isHidden = true
        isUserInteractionEnabled = true
    }

    func apply(item: SampleUploadedMediaItem) {
        dashedLayer.isHidden = true
        previewEnabled = !item.ossUrl.isEmpty && !item.isUploading
        backgroundColor = UIColor(hex: 0xF5F5F5)
        addCircle.isHidden = true
        deleteButton.isHidden = item.ossUrl.isEmpty || item.isUploading
        playIcon.isHidden = mediaType != .video || item.ossUrl.isEmpty || item.isUploading
        if item.isUploading {
            imageView.image = nil
            textLabel.isHidden = false
            textLabel.text = "上传中...\n\(item.uploadProgress)%"
            textLabel.numberOfLines = 2
        } else {
            textLabel.isHidden = true
            imageView.image = UIImage(data: item.thumbnailData ?? item.data)
        }
    }

    @objc private func cellTapped() {
        if !addCircle.isHidden {
            onAdd?()
        } else if previewEnabled {
            onPreview?()
        }
    }

    @objc private func deleteTapped() {
        onDelete?()
    }
}

/// 样片封面上传视图。
private final class SampleCoverUploadView: UIControl {
    private let imageView = UIImageView()
    private let addCircle = UIView()
    private let addIcon = UIImageView(image: UIImage(named: "material_ic_add"))
    private let titleLabel = UILabel()
    private let hintLabel = UILabel()
    private let deleteButton = SampleExpandedTouchButton(type: .system)
    private let dashedLayer = CAShapeLayer()
    var onAdd: (() -> Void)?
    var onDelete: (() -> Void)?

    override init(frame: CGRect) {
        super.init(frame: frame)
        layer.cornerRadius = 8
        dashedLayer.strokeColor = UIColor(hex: 0xE5E7EB).cgColor
        dashedLayer.fillColor = UIColor.clear.cgColor
        dashedLayer.lineWidth = 1
        dashedLayer.lineDashPattern = [6, 6]
        layer.addSublayer(dashedLayer)
        clipsToBounds = true
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        addCircle.backgroundColor = AppColor.primary
        addCircle.layer.cornerRadius = 16
        addIcon.tintColor = .white
        titleLabel.text = "点击上传图片"
        titleLabel.font = .systemFont(ofSize: 14)
        titleLabel.textColor = UIColor(hex: 0x4B5563)
        titleLabel.textAlignment = .center
        hintLabel.text = "建议尺寸: 1920x1080px, 大小不超过5MB"
        hintLabel.font = .systemFont(ofSize: 12)
        hintLabel.textColor = UIColor(hex: 0xB6BECA)
        hintLabel.textAlignment = .center
        [imageView, addCircle, addIcon, titleLabel, hintLabel].forEach {
            $0.isUserInteractionEnabled = false
        }
        accessibilityLabel = "上传封面图片"
        deleteButton.setImage(UIImage(named: "material_ic_clear"), for: .normal)
        deleteButton.tintColor = .white
        deleteButton.backgroundColor = UIColor.black.withAlphaComponent(0.6)
        deleteButton.layer.cornerRadius = 12

        addSubview(imageView)
        addSubview(addCircle)
        addCircle.addSubview(addIcon)
        addSubview(titleLabel)
        addSubview(hintLabel)
        addSubview(deleteButton)
        snp.makeConstraints { make in
            make.height.equalTo(snp.width).dividedBy(2.59)
        }
        imageView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        addCircle.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.centerY.equalToSuperview().offset(-22)
            make.size.equalTo(32)
        }
        addIcon.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.size.equalTo(24)
        }
        titleLabel.snp.makeConstraints { make in
            make.top.equalTo(addCircle.snp.bottom).offset(12)
            make.leading.trailing.equalToSuperview().inset(12)
        }
        hintLabel.snp.makeConstraints { make in
            make.top.equalTo(titleLabel.snp.bottom).offset(8)
            make.leading.trailing.equalToSuperview().inset(12)
        }
        deleteButton.snp.makeConstraints { make in
            make.top.trailing.equalToSuperview().inset(8)
            make.size.equalTo(24)
        }
        addTarget(self, action: #selector(addTapped), for: .touchUpInside)
        deleteButton.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside)
    }

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

    override func layoutSubviews() {
        super.layoutSubviews()
        dashedLayer.frame = bounds
        dashedLayer.path = UIBezierPath(roundedRect: bounds.insetBy(dx: 0.5, dy: 0.5), cornerRadius: 8).cgPath
    }

    func apply(item: SampleUploadedMediaItem?) {
        guard let item else {
            imageView.image = nil
            addCircle.isHidden = false
            titleLabel.isHidden = false
            titleLabel.text = "点击上传图片"
            titleLabel.numberOfLines = 1
            hintLabel.isHidden = false
            deleteButton.isHidden = true
            dashedLayer.isHidden = false
            return
        }
        if item.isUploading {
            imageView.image = nil
            addCircle.isHidden = true
            titleLabel.isHidden = false
            titleLabel.text = "上传中...\n\(item.uploadProgress)%"
            titleLabel.numberOfLines = 2
            hintLabel.isHidden = true
            deleteButton.isHidden = true
            dashedLayer.isHidden = true
        } else {
            imageView.image = UIImage(data: item.data)
            addCircle.isHidden = true
            titleLabel.isHidden = true
            hintLabel.isHidden = true
            deleteButton.isHidden = item.ossUrl.isEmpty
            dashedLayer.isHidden = true
        }
    }

    @objc private func addTapped() {
        guard addCircle.isHidden == false else { return }
        onAdd?()
    }

    @objc private func deleteTapped() {
        onDelete?()
    }
}

/// 样片上传进度蒙层。
private final class SampleUploadProgressOverlay: UIView {
    private let card = UIView()
    private let titleLabel = UILabel()
    private let progressView = UIProgressView(progressViewStyle: .default)
    private let percentLabel = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor.black.withAlphaComponent(0.2)
        card.backgroundColor = .white
        card.layer.cornerRadius = 12
        titleLabel.font = .systemFont(ofSize: 16, weight: .medium)
        titleLabel.textColor = AppColor.textPrimary
        progressView.progressTintColor = AppColor.primary
        progressView.trackTintColor = UIColor(hex: 0xF3F4F6)
        percentLabel.font = .systemFont(ofSize: 14)
        percentLabel.textColor = AppColor.textSecondary
        percentLabel.textAlignment = .center
        addSubview(card)
        card.addSubview(titleLabel)
        card.addSubview(progressView)
        card.addSubview(percentLabel)
        card.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.leading.trailing.equalToSuperview().inset(44)
            make.height.equalTo(card.snp.width).dividedBy(1.7)
        }
        titleLabel.snp.makeConstraints { make in
            make.top.leading.trailing.equalToSuperview().inset(24)
        }
        progressView.snp.makeConstraints { make in
            make.top.equalTo(titleLabel.snp.bottom).offset(32)
            make.leading.trailing.equalToSuperview().inset(24)
            make.height.equalTo(6)
        }
        percentLabel.snp.makeConstraints { make in
            make.top.equalTo(progressView.snp.bottom).offset(12)
            make.leading.trailing.equalToSuperview().inset(24)
        }
    }

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

    func apply(state: SampleUploadDialogState) {
        titleLabel.text = state.title
        let progress = Float(max(0, min(100, state.progress))) / 100
        progressView.setProgress(progress, animated: true)
        percentLabel.text = "\(max(0, min(100, state.progress)))%"
    }
}

/// 在不改变 Android 视觉尺寸的前提下，将小图标按钮触控区域扩展到至少 44pt。
private final class SampleExpandedTouchButton: UIButton {
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        let horizontalInset = max(0, (44 - bounds.width) / 2)
        let verticalInset = max(0, (44 - bounds.height) / 2)
        return bounds.insetBy(dx: -horizontalInset, dy: -verticalInset).contains(point)
    }
}
