//
//  MaterialFormViewController.swift
//  suixinkan
//

import AVFoundation
import Kingfisher
import PhotosUI
import SnapKit
import UniformTypeIdentifiers
import UIKit

/// 素材上传 / 编辑页，对齐 Android `UploadMaterialScreen` 与 `EditMaterialScreen`。
/// 素材上传与编辑页，负责字段录入、媒体选择和提交交互。
final class MaterialFormViewController: BaseViewController {
    private enum PickTarget {
        case material
        case cover
    }

    private let viewModel: MaterialFormViewModel
    private let api: any MaterialManagementServing
    private let uploader: any MaterialOSSUploading

    private let scrollView = UIScrollView()
    private let formCard = UIView()
    private let formStack = UIStackView()
    private let nameField = UITextField()
    private let nameSection = MaterialFormFieldView(title: "素材名称")
    private let spotButton = MaterialSelectButton()
    private let spotSection = MaterialFormFieldView(title: "关联打卡点")
    private let mediaTypeButton = MaterialSelectButton()
    private let mediaTypeSection = MaterialFormFieldView(title: "素材类型")
    private let mediaGridView = MaterialUploadGridView()
    private let materialSection = MaterialFormFieldView(title: "上传素材")
    private let coverView = MaterialCoverUploadView()
    private let coverSection = MaterialFormFieldView(title: "上传封面")
    private let tagView = MaterialTagEditorView()
    private let tagSection = MaterialFormFieldView(title: "标签")
    private let bottomBar = UIView()
    private let cancelButton = UIButton(type: .system)
    private let confirmButton = UIButton(type: .system)
    private let progressOverlay = MaterialUploadProgressOverlay()
    private let dropdownOverlay = MaterialAnchoredDropdownView()

    private var pickTarget: PickTarget = .material
    var onSubmitSuccess: (() -> Void)?

    /// 初始化素材表单页。
    init(
        mode: MaterialFormViewModel.Mode,
        api: (any MaterialManagementServing)? = nil,
        uploader: (any MaterialOSSUploading)? = nil
    ) {
        viewModel = MaterialFormViewModel(mode: mode)
        self.api = api ?? NetworkServices.shared.materialManagementAPI
        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() {
        switch viewModel.mode {
        case .create: title = "上传素材"
        case .edit: title = "编辑素材"
        }
    }

    override func setupUI() {
        view.backgroundColor = UIColor(hex: 0xF4F4F4)
        formCard.backgroundColor = .white
        formCard.layer.cornerRadius = 12
        formCard.clipsToBounds = true
        formStack.axis = .vertical
        formStack.spacing = 16

        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)

        spotSection.setContent(spotButton)
        mediaTypeSection.setContent(mediaTypeButton)
        materialSection.setContent(mediaGridView)
        coverSection.setContent(coverView)
        tagSection.setContent(tagView)
        [nameSection, spotSection, mediaTypeSection, materialSection, coverSection, tagSection].forEach(formStack.addArrangedSubview)

        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(progressOverlay)
        view.addSubview(dropdownOverlay)
        progressOverlay.isHidden = true
        dropdownOverlay.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.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)
        }
        [spotButton, mediaTypeButton].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.onSubmitSuccess = { [weak self] in
            Task { @MainActor in
                self?.onSubmitSuccess?()
                self?.navigationController?.popViewController(animated: true)
            }
        }
        nameField.addTarget(self, action: #selector(nameChanged), for: .editingChanged)
        spotButton.addTarget(self, action: #selector(spotTapped), for: .touchUpInside)
        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() }
        tagView.onTextChanged = { [weak self] text in self?.viewModel.updateTagInput(text) }
        tagView.onAdd = { [weak self] in
            guard let self else { return }
            Task { await self.viewModel.addTag(api: self.api) }
        }
        tagView.onDelete = { [weak self] tag in self?.viewModel.requestDeleteTag(tag) }
        cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
        confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
        applyViewModel()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        Task {
            showLoading()
            await viewModel.loadInitial(api: api)
            hideLoading()
        }
    }

    @MainActor
    private func applyViewModel() {
        if nameField.text != viewModel.materialName {
            nameField.text = viewModel.materialName
        }
        spotButton.setTitle(viewModel.selectedScenicSpot?.name, placeholder: "请选择打卡点")
        mediaTypeButton.setTitle(viewModel.mediaType.title, placeholder: "")
        mediaGridView.apply(items: viewModel.uploadedMediaList, mediaType: viewModel.mediaType)
        coverView.apply(item: viewModel.coverImage)
        tagView.apply(tags: viewModel.tagList, input: viewModel.tagInput)
        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
        }
        presentTagDeleteDialogIfNeeded()
    }

    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
    }

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

    @objc private func spotTapped() {
        let spots = viewModel.scenicSpotList
        dropdownOverlay.show(
            in: view,
            anchor: spotButton,
            options: spots.map(\.name),
            selectedIndex: spots.firstIndex { $0.id == viewModel.selectedScenicSpot?.id },
            maximumHeight: 128
        ) { [weak self] index in
            guard spots.indices.contains(index) else { return }
            self?.viewModel.selectScenicSpot(spots[index])
        }
    }

    @objc private func mediaTypeTapped() {
        let types = MaterialMediaType.allCases
        dropdownOverlay.show(
            in: view,
            anchor: mediaTypeButton,
            options: types.map(\.title),
            selectedIndex: types.firstIndex(of: viewModel.mediaType)
        ) { [weak self] index in
            guard types.indices.contains(index) else { return }
            self?.viewModel.selectMediaType(types[index])
        }
    }

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

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

    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 items = completed.map {
            MaterialPreviewItem(url: $0.element.ossUrl, mediaType: viewModel.mediaType)
        }
        present(MaterialMediaPreviewViewController(items: items, startIndex: startIndex), animated: true)
    }

    private func presentTagDeleteDialogIfNeeded() {
        guard let tag = viewModel.pendingDeleteTag, presentedViewController == nil else { return }
        let dialog = MaterialDeleteDialogViewController(
            title: "删除标签",
            itemName: tag.name,
            itemSuffix: "标签吗?",
            onCancel: { [weak self] in self?.viewModel.cancelDeleteTag() },
            onConfirm: { [weak self] in self?.viewModel.confirmDeleteTag() }
        )
        present(dialog, animated: true)
    }

    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: [MaterialUploadedMediaItem]) {
        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) -> MaterialUploadedMediaItem? {
        guard let data = image.jpegData(compressionQuality: 0.9) else { return nil }
        return MaterialUploadedMediaItem(
            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) -> MaterialUploadedMediaItem {
        let preview = videoPreview(url: sourceURL)
        return MaterialUploadedMediaItem(
            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 MaterialFormViewController: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        picker.dismiss(animated: true)
        guard !results.isEmpty else { return }

        var loadedItems = [MaterialUploadedMediaItem?](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 = provider.suggestedName.map { "\($0).mp4" } ?? "material_video_\(Int(Date().timeIntervalSince1970)).mp4"
                    loadedItems[index] = self.makeVideoItem(data: data, fileName: fileName, sourceURL: url)
                }
            } else {
                group.enter()
                provider.loadObject(ofClass: UIImage.self) { [weak self] object, _ in
                    defer { group.leave() }
                    guard let self, let image = object as? UIImage else { return }
                    let fileName = "material_image_\(Int(Date().timeIntervalSince1970))_\(index).jpg"
                    loadedItems[index] = self.makeImageItem(image: image, fileName: fileName)
                }
            }
        }

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

extension MaterialFormViewController: 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: "material_image_\(Int(Date().timeIntervalSince1970)).jpg") {
            uploadPickedItems([item])
        }
    }
}

/// 素材表单字段容器。
/// 素材表单中的标题字段容器。
private final class MaterialFormFieldView: 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 MaterialSelectButton: UIControl {
    private let titleLabel = UILabel()
    private let chevronView = UIImageView(image: UIImage(named: "material_ic_arrow_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
    }
}

/// 锚定在表单选择框下方的 Android 风格下拉菜单。
private final class MaterialAnchoredDropdownView: UIControl {
    private let cardView = UIView()
    private let scrollView = UIScrollView()
    private let stackView = UIStackView()
    private var onSelect: ((Int) -> Void)?

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .clear
        cardView.backgroundColor = .white
        cardView.layer.cornerRadius = 4
        cardView.layer.shadowColor = UIColor.black.cgColor
        cardView.layer.shadowOpacity = 0.14
        cardView.layer.shadowRadius = 8
        cardView.layer.shadowOffset = CGSize(width: 0, height: 4)
        stackView.axis = .vertical
        addSubview(cardView)
        cardView.addSubview(scrollView)
        scrollView.addSubview(stackView)
        addTarget(self, action: #selector(dismissMenu), for: .touchUpInside)
    }

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

    /// 在指定控件下方展示菜单。
    func show(
        in container: UIView,
        anchor: UIView,
        options: [String],
        selectedIndex: Int?,
        maximumHeight: CGFloat = 128,
        onSelect: @escaping (Int) -> Void
    ) {
        guard !options.isEmpty else { return }
        self.onSelect = onSelect
        stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
        for (index, option) in options.enumerated() {
            let button = UIButton(type: .system)
            button.tag = index
            button.contentHorizontalAlignment = .leading
            button.setConfigurationContentInsets(
                NSDirectionalEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12)
            )
            button.setTitle(option, for: .normal)
            button.setTitleColor(index == selectedIndex ? AppColor.primary : .black, for: .normal)
            button.titleLabel?.font = .systemFont(ofSize: 14, weight: index == selectedIndex ? .medium : .regular)
            button.backgroundColor = .white
            button.addTarget(self, action: #selector(optionTapped(_:)), for: .touchUpInside)
            button.snp.makeConstraints { make in make.height.equalTo(28) }
            stackView.addArrangedSubview(button)
        }

        isHidden = false
        container.bringSubviewToFront(self)
        layoutIfNeeded()
        let anchorFrame = anchor.convert(anchor.bounds, to: self)
        let menuHeight = min(maximumHeight, CGFloat(options.count) * 28)
        cardView.frame = CGRect(x: anchorFrame.minX, y: anchorFrame.maxY, width: anchorFrame.width, height: menuHeight)
        scrollView.frame = cardView.bounds
        stackView.frame = CGRect(x: 0, y: 0, width: cardView.bounds.width, height: CGFloat(options.count) * 28)
        scrollView.contentSize = stackView.frame.size
    }

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        isHidden ? false : true
    }

    @objc private func optionTapped(_ sender: UIButton) {
        isHidden = true
        onSelect?(sender.tag)
    }

    @objc private func dismissMenu() {
        isHidden = true
    }
}

/// 素材上传网格。
/// 素材媒体上传网格视图。
private final class MaterialUploadGridView: UIView {
    private let stack = UIStackView()
    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: [MaterialUploadedMediaItem], mediaType: MaterialMediaType) {
        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, mediaType: mediaType))
                } else {
                    rowStack.addArrangedSubview(UIView())
                }
            }
        }
    }

    private func makeCell(item: MaterialUploadedMediaItem?, index: Int, mediaType: MaterialMediaType) -> UIView {
        let cell = MaterialUploadThumbView(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 MaterialUploadThumbView: 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 = UIButton(type: .system)
    private let playIcon = UIImageView(image: UIImage(named: "material_ic_play"))
    private let mediaType: MaterialMediaType
    private let dashedLayer = CAShapeLayer()
    private var previewEnabled = false

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

    init(mediaType: MaterialMediaType) {
        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
        addCircle.isUserInteractionEnabled = false
        addIcon.isUserInteractionEnabled = false
        textLabel.font = .systemFont(ofSize: 12)
        textLabel.textColor = UIColor(hex: 0x4B5563)
        textLabel.textAlignment = .center
        textLabel.isUserInteractionEnabled = false
        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
        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
        imageView.kf.cancelDownloadTask()
        backgroundColor = .white
        layer.borderWidth = 0
        dashedLayer.isHidden = false
        previewEnabled = false
        addCircle.isHidden = false
        textLabel.isHidden = false
        textLabel.text = mediaType == .image ? "点击上传图片" : "点击上传视频"
        textLabel.numberOfLines = 1
        deleteButton.isHidden = true
        playIcon.isHidden = true
    }

    func apply(item: MaterialUploadedMediaItem) {
        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
            if mediaType == .video {
                if let thumbnailData = item.thumbnailData, let image = UIImage(data: thumbnailData) {
                    imageView.image = image
                } else if let urlText = (item.previewURL?.isEmpty == false ? item.previewURL : item.ossUrl),
                          !urlText.isEmpty, let url = URL(string: urlText) {
                    loadVideoThumbnail(url: url)
                }
            } else if let urlText = item.previewURL, let url = URL(string: urlText), !urlText.isEmpty {
                imageView.kf.setImage(with: url)
            } else {
                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 func loadVideoThumbnail(url: URL) {
        imageView.image = nil
        let generator = AVAssetImageGenerator(asset: AVURLAsset(url: url))
        generator.appliesPreferredTrackTransform = true
        generator.generateCGImagesAsynchronously(forTimes: [NSValue(time: .zero)]) { [weak self] _, image, _, _, _ in
            guard let image else { return }
            DispatchQueue.main.async { self?.imageView.image = UIImage(cgImage: image) }
        }
    }
}

/// 素材封面上传视图。
/// 素材封面上传视图。
private final class MaterialCoverUploadView: 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 = UIButton(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
        imageView.isUserInteractionEnabled = false
        addCircle.backgroundColor = AppColor.primary
        addCircle.layer.cornerRadius = 16
        addIcon.tintColor = .white
        addCircle.isUserInteractionEnabled = false
        addIcon.isUserInteractionEnabled = false
        titleLabel.text = "点击上传图片"
        titleLabel.font = .systemFont(ofSize: 14)
        titleLabel.textColor = UIColor(hex: 0x4B5563)
        titleLabel.textAlignment = .center
        titleLabel.isUserInteractionEnabled = false
        hintLabel.text = "建议尺寸: 1920x1080px, 大小不超过5MB"
        hintLabel.font = .systemFont(ofSize: 12)
        hintLabel.textColor = UIColor(hex: 0xB6BECA)
        hintLabel.textAlignment = .center
        hintLabel.isUserInteractionEnabled = false
        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: MaterialUploadedMediaItem?) {
        guard let item else {
            imageView.image = nil
            imageView.kf.cancelDownloadTask()
            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 {
            if let urlText = item.previewURL, let url = URL(string: urlText), !urlText.isEmpty {
                imageView.kf.setImage(with: url)
            } 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 MaterialTagEditorView: UIView {
    private let stack = UIStackView()
    private let tagsStack = UIStackView()
    private let inputRow = UIStackView()
    private let textField = UITextField()
    private let addButton = UIButton(type: .system)
    private var tags: [MaterialTagItem] = []

    var onTextChanged: ((String) -> Void)?
    var onAdd: (() -> Void)?
    var onDelete: ((MaterialTagItem) -> Void)?

    override init(frame: CGRect) {
        super.init(frame: frame)
        stack.axis = .vertical
        stack.spacing = 8
        tagsStack.axis = .horizontal
        tagsStack.spacing = 8
        tagsStack.alignment = .center
        inputRow.axis = .horizontal
        inputRow.spacing = 8
        textField.font = .systemFont(ofSize: 14)
        textField.layer.borderColor = UIColor(hex: 0xEEEEEE).cgColor
        textField.layer.borderWidth = 1
        textField.layer.cornerRadius = 4
        textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 44))
        textField.leftViewMode = .always
        textField.placeholder = "请输入标签,最低5个字符"
        addButton.setTitle("添加标签", for: .normal)
        addButton.setTitleColor(.white, for: .normal)
        addButton.backgroundColor = AppColor.primary
        addButton.layer.cornerRadius = 4
        addSubview(stack)
        stack.addArrangedSubview(tagsStack)
        stack.addArrangedSubview(inputRow)
        inputRow.addArrangedSubview(textField)
        inputRow.addArrangedSubview(addButton)
        stack.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        textField.snp.makeConstraints { make in
            make.height.equalTo(36)
        }
        addButton.snp.makeConstraints { make in
            make.width.equalTo(80)
            make.height.equalTo(36)
        }
        textField.addTarget(self, action: #selector(textChanged), for: .editingChanged)
        addButton.addTarget(self, action: #selector(addTapped), for: .touchUpInside)
    }

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

    func apply(tags: [MaterialTagItem], input: String) {
        self.tags = tags
        if textField.text != input {
            textField.text = input
        }
        let canAdd = (5 ... 15).contains(input.count)
        addButton.isEnabled = canAdd
        addButton.backgroundColor = canAdd ? AppColor.primary : AppColor.buttonDisabled
        tagsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
        tags.forEach { tag in
            let button = UIButton(type: .system)
            button.setTitle(tag.name, for: .normal)
            button.setTitleColor(AppColor.primary, for: .normal)
            button.titleLabel?.font = .systemFont(ofSize: 12)
            button.backgroundColor = UIColor(hex: 0xEFF6FF)
            button.layer.cornerRadius = 4
            button.setConfigurationContentInsets(
                NSDirectionalEdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8)
            )
            button.setContentHuggingPriority(.required, for: .horizontal)
            button.setContentCompressionResistancePriority(.required, for: .horizontal)
            button.snp.makeConstraints { make in
                make.height.equalTo(28)
            }
            button.addAction(UIAction { [weak self] _ in
                self?.onDelete?(tag)
            }, for: .touchUpInside)
            tagsStack.addArrangedSubview(button)
        }
        if !tags.isEmpty {
            // Android Row 中标签按内容宽度排列；尾部弹性占位吸收剩余空间。
            tagsStack.addArrangedSubview(UIView())
        }
        tagsStack.isHidden = tags.isEmpty
    }

    @objc private func textChanged() {
        onTextChanged?(textField.text ?? "")
    }

    @objc private func addTapped() {
        onAdd?()
    }
}

/// 素材上传进度蒙层。
/// 素材上传进度遮罩视图。
private final class MaterialUploadProgressOverlay: 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: MaterialUploadDialogState) {
        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)))%"
    }
}
