//
//  WildReportRiskMapViewController.swift
//  suixinkan
//

import Kingfisher
import MapKit
import SnapKit
import UIKit

/// 附近风险地图页面，展示当前位置、疑似打野、处理中线索和内部摄影师标记。
final class WildReportRiskMapViewController: BaseViewController {
    private static let cachedNavigationAppKey = "wildReportRiskMap.cachedNavigationApp"

    private let viewModel: WildReportRiskMapViewModel
    private let api: any WildPhotographerReportServing
    private let locationProvider: any LocationProviding
    private let scenicIdProvider: () -> Int
    private let scenicNameProvider: () -> String

    private let rootStack = UIStackView()
    private let mapContainerView = UIView()
    private let mapView = MKMapView()
    private let scenicCapsule = UIView()
    private let scenicNameLabel = UILabel()
    private let mapControls = LocationReportMapControlStack()
    private let legendBar = UIStackView()
    private let detailContainerView = UIView()
    private let scrollView = UIScrollView()
    private let contentStack = UIStackView()
    private var annotationMap: [String: WildReportMapMarker] = [:]
    private var isShowingRiskMapLoading = false

    init(
        api: (any WildPhotographerReportServing)? = nil,
        locationProvider: any LocationProviding = LocationProvider.shared,
        scenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId },
        scenicNameProvider: @escaping () -> String = { AppStore.shared.session.currentScenicName }
    ) {
        self.viewModel = WildReportRiskMapViewModel()
        self.api = api ?? NetworkServices.shared.wildPhotographerReportAPI
        self.locationProvider = locationProvider
        self.scenicIdProvider = scenicIdProvider
        self.scenicNameProvider = scenicNameProvider
        super.init(nibName: nil, bundle: nil)
    }

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

    override func viewDidLoad() {
        super.viewDidLoad()
        Task {
            await viewModel.loadInitial(
                api: api,
                locationProvider: locationProvider,
                scenicId: scenicIdProvider(),
                scenicName: scenicNameProvider()
            )
        }
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        if isMovingFromParent || isBeingDismissed || navigationController?.isBeingDismissed == true {
            setRiskMapLoadingVisible(false)
        }
    }

    override func setupNavigationBar() {
        title = "附近风险地图"
    }

    override func setupUI() {
        view.backgroundColor = UIColor(hex: 0xF8FAFF)
        rootStack.axis = .vertical
        rootStack.spacing = 0
        view.addSubview(rootStack)

        mapContainerView.backgroundColor = UIColor(hex: 0xE8F3FF)
        mapContainerView.clipsToBounds = true
        rootStack.addArrangedSubview(mapContainerView)

        mapView.delegate = self
        mapView.showsCompass = false
        mapView.showsScale = true
        mapView.pointOfInterestFilter = .excludingAll
        mapContainerView.addSubview(mapView)

        configureScenicCapsule()
        configureMapControls()
        configureLegendBar()
        configureDetailArea()
        refreshContent(animated: false)
    }

    override func setupConstraints() {
        rootStack.snp.makeConstraints { make in
            make.edges.equalTo(view.safeAreaLayoutGuide)
        }
        mapContainerView.snp.makeConstraints { make in
            make.height.equalTo(340)
        }
        mapView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        scenicCapsule.snp.makeConstraints { make in
            make.top.leading.equalToSuperview().inset(12)
            make.height.equalTo(34)
        }
        mapControls.snp.makeConstraints { make in
            make.top.trailing.equalToSuperview().inset(12)
        }
        detailContainerView.snp.makeConstraints { make in
            make.height.greaterThanOrEqualTo(180)
        }
    }

    override func bindActions() {
        viewModel.onStateChange = { [weak self] in
            Task { @MainActor in
                self?.refreshContent(animated: true)
            }
        }
    }

    private func configureScenicCapsule() {
        scenicCapsule.backgroundColor = UIColor.white.withAlphaComponent(0.94)
        scenicCapsule.layer.cornerRadius = 17
        scenicCapsule.layer.shadowColor = UIColor.black.cgColor
        scenicCapsule.layer.shadowOpacity = 0.08
        scenicCapsule.layer.shadowRadius = 8
        scenicCapsule.layer.shadowOffset = CGSize(width: 0, height: 4)

        let icon = UIImageView(image: UIImage(systemName: "mountain.2.fill"))
        icon.tintColor = AppColor.primary
        icon.contentMode = .scaleAspectFit
        scenicNameLabel.font = .systemFont(ofSize: 13, weight: .semibold)
        scenicNameLabel.textColor = UIColor(hex: 0x195591)

        mapContainerView.addSubview(scenicCapsule)
        scenicCapsule.addSubview(icon)
        scenicCapsule.addSubview(scenicNameLabel)
        icon.snp.makeConstraints { make in
            make.leading.equalToSuperview().offset(12)
            make.centerY.equalToSuperview()
            make.size.equalTo(16)
        }
        scenicNameLabel.snp.makeConstraints { make in
            make.leading.equalTo(icon.snp.trailing).offset(6)
            make.trailing.equalToSuperview().inset(12)
            make.centerY.equalToSuperview()
        }
    }

    private func configureMapControls() {
        mapControls.layer.shadowColor = UIColor.black.cgColor
        mapControls.layer.shadowOpacity = 0.1
        mapControls.layer.shadowRadius = 8
        mapControls.layer.shadowOffset = CGSize(width: 0, height: 4)
        mapControls.onZoomIn = { [weak self] in
            self?.zoomMap(by: 0.5)
        }
        mapControls.onZoomOut = { [weak self] in
            self?.zoomMap(by: 2)
        }
        mapControls.onRelocate = { [weak self] in
            self?.focusCurrentLocation()
        }
        mapContainerView.addSubview(mapControls)
    }

    private func configureLegendBar() {
        legendBar.axis = .horizontal
        legendBar.alignment = .center
        legendBar.distribution = .fillEqually
        legendBar.spacing = 6
        legendBar.backgroundColor = .white
        legendBar.layoutMargins = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
        legendBar.isLayoutMarginsRelativeArrangement = true
        rootStack.addArrangedSubview(legendBar)
        rebuildLegend()

        let divider = UIView()
        divider.backgroundColor = AppColor.cardOutline
        rootStack.addArrangedSubview(divider)
        divider.snp.makeConstraints { make in
            make.height.equalTo(1 / UIScreen.main.scale)
        }
    }

    private func configureDetailArea() {
        detailContainerView.backgroundColor = UIColor(hex: 0xF8FAFF)
        rootStack.addArrangedSubview(detailContainerView)
        detailContainerView.addSubview(scrollView)
        scrollView.alwaysBounceVertical = true
        scrollView.addSubview(contentStack)
        contentStack.axis = .vertical
        contentStack.spacing = 14
        scrollView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        contentStack.snp.makeConstraints { make in
            make.edges.equalToSuperview().inset(16)
            make.width.equalTo(scrollView.snp.width).offset(-32)
        }
    }

    @MainActor
    private func refreshContent(animated: Bool) {
        setRiskMapLoadingVisible(viewModel.isLoading)
        scenicNameLabel.text = viewModel.scenicAreaName
        updateLocateButtonState()
        syncMapAnnotations()
        mapView.setRegion(viewModel.region, animated: animated)
        updateAnnotationSelection()
        rebuildLegend()
        rebuildPanel()
    }

    @MainActor
    private func setRiskMapLoadingVisible(_ isVisible: Bool) {
        guard isShowingRiskMapLoading != isVisible else { return }
        isShowingRiskMapLoading = isVisible
        isVisible ? showLoading() : hideLoading()
    }

    @MainActor
    private func updateLocateButtonState() {
        let hasCurrentLocation = viewModel.markers.contains { $0.kind == .selfLocation }
        mapControls.setRelocateEnabled(hasCurrentLocation)
    }

    private func syncMapAnnotations() {
        let oldAnnotations = mapView.annotations.compactMap { $0 as? WildReportMapAnnotation }
        mapView.removeAnnotations(oldAnnotations)
        annotationMap.removeAll()
        let annotations = viewModel.markers.map { marker in
            annotationMap[marker.id] = marker
            return WildReportMapAnnotation(marker: marker)
        }
        mapView.addAnnotations(annotations)
    }

    private func updateAnnotationSelection() {
        for annotation in mapView.annotations {
            guard let annotation = annotation as? WildReportMapAnnotation,
                  let view = mapView.view(for: annotation) as? WildReportPulseMarkerAnnotationView
            else { continue }
            view.apply(marker: annotation.marker, selected: annotation.marker.id == viewModel.selectedMarkerID)
        }
    }

    private func rebuildLegend() {
        legendBar.arrangedSubviews.forEach { view in
            legendBar.removeArrangedSubview(view)
            view.removeFromSuperview()
        }
        [
            WildReportMapMarkerKind.selfLocation,
            .wildPhotographer,
            .processingClue,
            .peerPhotographer
        ].forEach { kind in
            legendBar.addArrangedSubview(WildReportRiskLegendView(kind: kind))
        }
    }

    @MainActor
    private func rebuildPanel() {
        contentStack.arrangedSubviews.forEach { view in
            contentStack.removeArrangedSubview(view)
            view.removeFromSuperview()
        }

        if let message = viewModel.errorMessage, !viewModel.isLoading {
            contentStack.addArrangedSubview(makeStateCard(icon: "exclamationmark.triangle.fill", text: message))
        }

        guard let marker = viewModel.selectedMarker,
              case .activeClue(let clue) = marker.detail
        else {
            contentStack.addArrangedSubview(makeStateCard(icon: "hand.tap.fill", text: "点击地图标记查看详情"))
            return
        }

        let card = makeRadarResultCard(clue: clue)
        contentStack.addArrangedSubview(card)
    }

    private func makeRadarResultCard(clue: WildReportRadarActiveClue) -> UIView {
        let card = WildReportCardView(spacing: 14, inset: 16)
        card.layer.cornerRadius = 16
        card.stack.spacing = 14
        card.stack.addArrangedSubview(makeRadarHeader(clue: clue))

        if clue.type == .blue, let nearest = clue.nearestAbnormalText {
            card.stack.addArrangedSubview(makeCompareText(nearest))
        }

        if clue.type == .green {
            addPhotographerProfile(for: clue, to: card.stack)
        }

        if isReportClue(clue) {
            card.stack.addArrangedSubview(makeReporterInfo(clue: clue))
            addReportContents(for: clue, to: card.stack)
            addRadarImages(for: clue, to: card.stack)
            if clue.processingStarted, let record = clue.record {
                card.stack.addArrangedSubview(makeProcessingPanel(clue: clue, record: record))
            }
        }

        if clue.type == .green, let summary = clue.summary {
            card.stack.addArrangedSubview(makeCompareText(summary))
        }

        if clue.type == .green, let detail = clue.detail {
            card.stack.addArrangedSubview(makeDetailSection(detail))
        }

        if !clue.completed {
            card.stack.addArrangedSubview(makeRadarActionButtons(for: clue))
        }
        return card
    }

    private func makeRadarHeader(clue: WildReportRadarActiveClue) -> UIView {
        let container = UIView()

        let iconWrap = UIView()
        iconWrap.backgroundColor = headerIconColor(for: clue)
        iconWrap.layer.cornerRadius = 12
        let icon = UIImageView(image: UIImage(systemName: "location.fill"))
        icon.tintColor = .white
        icon.contentMode = .scaleAspectFit
        iconWrap.addSubview(icon)
        icon.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.size.equalTo(20)
        }

        let badge = WildReportRiskStatusBadge(text: clue.statusText, color: statusColor(for: clue))
        badge.setContentHuggingPriority(.required, for: .horizontal)
        badge.setContentCompressionResistancePriority(.required, for: .horizontal)

        let title: UILabel
        var metaText: String?
        if isReportClue(clue) {
            title = WildReportUI.label("线索 ID：\(clue.id)", font: .systemFont(ofSize: 14, weight: .bold), color: AppColor.textPrimary, lines: 1)
            var metaParts: [String] = []
            if let distance = clue.distanceText ?? clue.distance {
                metaParts.append("距离：距离您\(distance)")
            }
            if let updatedAt = clue.updatedAt {
                metaParts.append("时间：\(updatedAt)")
            }
            metaText = metaParts.isEmpty ? nil : metaParts.joined(separator: "  ")
        } else {
            title = WildReportUI.label(clue.displayTitle, font: .systemFont(ofSize: 16, weight: .bold), color: AppColor.textPrimary, lines: 2)
            if clue.type == .green {
                let text = [clue.distance.map { "距蓝点 \($0)" }, clue.direction, clue.updatedAt].compactMap { $0 }.joined(separator: "  ")
                metaText = text.isEmpty ? nil : text
            }
        }

        container.addSubview(iconWrap)
        container.addSubview(badge)
        container.addSubview(title)

        iconWrap.snp.makeConstraints { make in
            make.top.leading.equalToSuperview()
            make.size.equalTo(44)
        }
        badge.snp.makeConstraints { make in
            make.top.trailing.equalToSuperview()
        }
        title.snp.makeConstraints { make in
            make.top.equalToSuperview()
            make.leading.equalTo(iconWrap.snp.trailing).offset(12)
            make.trailing.lessThanOrEqualTo(badge.snp.leading).offset(-12)
        }

        if let metaText {
            let metaLabel = WildReportUI.label(metaText, font: .systemFont(ofSize: 12), color: AppColor.textSecondary, lines: 0)
            container.addSubview(metaLabel)
            metaLabel.snp.makeConstraints { make in
                make.top.equalTo(title.snp.bottom).offset(6)
                make.leading.equalTo(title)
                make.trailing.equalToSuperview()
                make.bottom.equalToSuperview()
            }
            iconWrap.snp.makeConstraints { make in
                make.bottom.lessThanOrEqualToSuperview()
            }
        } else {
            [iconWrap, title, badge].forEach { view in
                view.snp.makeConstraints { make in
                    make.bottom.lessThanOrEqualToSuperview()
                }
            }
            iconWrap.snp.makeConstraints { make in
                make.bottom.equalToSuperview().priority(.high)
            }
            title.snp.makeConstraints { make in
                make.bottom.equalToSuperview().priority(.high)
            }
        }

        return container
    }

    private func addPhotographerProfile(for clue: WildReportRadarActiveClue, to stack: UIStackView) {
        let row = UIStackView()
        row.axis = .horizontal
        row.alignment = .center
        row.spacing = 12

        let avatarWrap = UIView()
        avatarWrap.backgroundColor = AppColor.success.withAlphaComponent(0.12)
        avatarWrap.layer.cornerRadius = 28
        let placeholder = UIImage(systemName: "person.crop.circle.fill")
        let avatar = UIImageView(image: placeholder)
        avatar.tintColor = AppColor.success
        avatar.contentMode = .scaleAspectFill
        avatar.clipsToBounds = true
        avatar.layer.cornerRadius = 28
        let avatarURLString = clue.avatar?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        if let avatarURL = URL(string: avatarURLString), !avatarURLString.isEmpty {
            avatar.kf.setImage(with: avatarURL, placeholder: placeholder)
        }
        avatarWrap.addSubview(avatar)
        avatar.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        avatarWrap.snp.makeConstraints { make in
            make.size.equalTo(56)
        }

        let textStack = UIStackView()
        textStack.axis = .vertical
        textStack.spacing = 4
        if let name = clue.name {
            textStack.addArrangedSubview(WildReportUI.label(name, font: .systemFont(ofSize: 16, weight: .bold), color: AppColor.textPrimary))
        }
        if let storeName = clue.storeName {
            textStack.addArrangedSubview(WildReportUI.label(storeName, font: .systemFont(ofSize: 13, weight: .semibold), color: AppColor.success))
        }

        row.addArrangedSubview(avatarWrap)
        row.addArrangedSubview(textStack)
        stack.addArrangedSubview(row)
    }

    private func makeReporterInfo(clue: WildReportRadarActiveClue) -> UIView {
        let box = UIView()
        box.backgroundColor = UIColor(hex: 0xF9FAFB)
        box.layer.cornerRadius = 10

        let inner = UIStackView()
        inner.axis = .vertical
        inner.spacing = 10
        box.addSubview(inner)
        inner.snp.makeConstraints { make in
            make.edges.equalToSuperview().inset(12)
        }

        inner.addArrangedSubview(makeInfoLabelRow(label: "举报人", value: clue.reporterName ?? "-"))
        inner.addArrangedSubview(makePhoneRow(
            label: "举报人手机号",
            phone: clue.reporterPhone,
            displayPhone: clue.maskedReporterPhone ?? clue.reporterPhone
        ))
        return box
    }

    private func addReportContents(for clue: WildReportRadarActiveClue, to stack: UIStackView) {
        guard !clue.reportContents.isEmpty else { return }
        let section = UIStackView()
        section.axis = .vertical
        section.spacing = 10
        for item in clue.reportContents {
            let itemStack = UIStackView()
            itemStack.axis = .vertical
            itemStack.spacing = 4
            if !item.time.isEmpty {
                itemStack.addArrangedSubview(WildReportUI.label(item.time, font: .systemFont(ofSize: 11, weight: .semibold), color: AppColor.textSecondary))
            }
            itemStack.addArrangedSubview(makeLineSpacedLabel(item.content, font: .systemFont(ofSize: 13), color: AppColor.textPrimary))
            section.addArrangedSubview(itemStack)
        }
        stack.addArrangedSubview(section)
    }

    private func addRadarImages(for clue: WildReportRadarActiveClue, to stack: UIStackView) {
        guard !clue.images.isEmpty else { return }
        let section = UIStackView()
        section.axis = .vertical
        section.spacing = 10
        section.addArrangedSubview(WildReportUI.label("现场图片/视频", font: .systemFont(ofSize: 15, weight: .bold), color: AppColor.textPrimary))

        let scroll = UIScrollView()
        scroll.showsHorizontalScrollIndicator = false
        let row = UIStackView()
        row.axis = .horizontal
        row.spacing = 10
        scroll.addSubview(row)
        row.snp.makeConstraints { make in
            make.edges.equalToSuperview()
            make.height.equalTo(108)
        }
        for url in clue.images {
            let tile = WildReportRiskImageTile(url: url, size: 108)
            tile.onTap = { [weak self] in self?.presentImagePreview(url: url) }
            row.addArrangedSubview(tile)
        }
        section.addArrangedSubview(scroll)
        scroll.snp.makeConstraints { make in
            make.height.equalTo(108)
        }
        stack.addArrangedSubview(section)
    }

    private func makeProcessingPanel(clue: WildReportRadarActiveClue, record: WildReportRadarProcessRecord) -> UIView {
        let box = UIView()
        box.backgroundColor = UIColor(hex: 0xFFFBEB)
        box.layer.cornerRadius = 10

        let inner = UIStackView()
        inner.axis = .vertical
        inner.spacing = 10
        box.addSubview(inner)
        inner.snp.makeConstraints { make in
            make.edges.equalToSuperview().inset(12)
        }

        inner.addArrangedSubview(makeInfoLabelRow(label: "处理人", value: record.handlerName))
        inner.addArrangedSubview(makeInfoLabelRow(
            label: "状态",
            value: clue.completed ? "已处理" : "正在处理中",
            valueColor: clue.completed ? AppColor.success : UIColor(hex: 0xF59E0B)
        ))
        inner.addArrangedSubview(makePhoneRow(label: "联系电话", phone: record.phone, displayPhone: record.maskedPhone))

        let progressStack = UIStackView()
        progressStack.axis = .vertical
        progressStack.spacing = 10
        progressStack.addArrangedSubview(WildReportUI.label("处理进度", font: .systemFont(ofSize: 14, weight: .bold), color: AppColor.textPrimary))
        for item in clue.processingTimeline {
            progressStack.addArrangedSubview(makeTimelineRow(item))
        }
        inner.addArrangedSubview(progressStack)
        return box
    }

    private func makeTimelineRow(_ item: WildReportRadarProcessingTimelineItem) -> UIView {
        let row = UIStackView()
        row.axis = .horizontal
        row.alignment = .top
        row.spacing = 10

        let dot = UIView()
        dot.backgroundColor = timelineDotColor(item.status)
        dot.layer.cornerRadius = 5
        dot.snp.makeConstraints { make in
            make.size.equalTo(10)
        }
        let dotContainer = UIView()
        dotContainer.addSubview(dot)
        dot.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(4)
            make.centerX.equalToSuperview()
        }
        dotContainer.snp.makeConstraints { make in
            make.width.equalTo(10)
        }

        let textStack = UIStackView()
        textStack.axis = .vertical
        textStack.spacing = 2
        textStack.addArrangedSubview(WildReportUI.label(item.title, font: .systemFont(ofSize: 13, weight: .semibold), color: AppColor.textPrimary))
        textStack.addArrangedSubview(WildReportUI.label(item.time, font: .systemFont(ofSize: 12), color: AppColor.textSecondary))

        row.addArrangedSubview(dotContainer)
        row.addArrangedSubview(textStack)
        return row
    }

    private func makeRadarActionButtons(for clue: WildReportRadarActiveClue) -> UIView {
        let row = UIStackView()
        row.axis = .horizontal
        row.spacing = 10
        row.distribution = .fillEqually

        let navButton = makeRadarButton(title: clue.actionText, imageName: "map.fill", color: AppColor.primary)
        navButton.addTarget(self, action: #selector(navigateTapped), for: .touchUpInside)
        row.addArrangedSubview(navButton)

        if isReportClue(clue), !clue.completed {
            let shareButton = makeRadarButton(title: "分享", imageName: "square.and.arrow.up", color: AppColor.success)
            shareButton.addTarget(self, action: #selector(shareTapped), for: .touchUpInside)
            row.addArrangedSubview(shareButton)
        }

        [navButton].forEach { button in
            button.snp.makeConstraints { make in make.height.equalTo(44) }
        }
        return row
    }

    private func makeRadarButton(title: String, imageName: String, color: UIColor) -> UIButton {
        let button = UIButton(type: .system)
        button.titleLabel?.font = .systemFont(ofSize: 15, weight: .bold)
        button.setTitle(title, for: .normal)
        button.setImage(UIImage(systemName: imageName), for: .normal)
        button.tintColor = .white
        button.setTitleColor(.white, for: .normal)
        button.backgroundColor = color
        button.layer.cornerRadius = 12
        button.setConfigurationContentInsets(
            NSDirectionalEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12)
        )
        button.configuration?.imagePlacement = .leading
        button.configuration?.imagePadding = 3
        button.snp.makeConstraints { make in make.height.equalTo(44) }
        return button
    }

    private func makeCompareText(_ text: String) -> UIView {
        let label = makeLineSpacedLabel(text, font: .systemFont(ofSize: 13), color: AppColor.textSecondary)
        label.backgroundColor = UIColor(hex: 0xF3F4F6)
        label.layer.cornerRadius = 10
        label.clipsToBounds = true
        label.textInsets = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
        return label
    }

    private func makeDetailSection(_ detail: String) -> UIView {
        let row = UIStackView()
        row.axis = .horizontal
        row.alignment = .top
        row.spacing = 8
        let icon = UIImageView(image: UIImage(systemName: "doc.text.fill"))
        icon.tintColor = AppColor.primary
        icon.contentMode = .scaleAspectFit
        icon.snp.makeConstraints { make in
            make.size.equalTo(16)
        }
        row.addArrangedSubview(icon)
        row.addArrangedSubview(makeLineSpacedLabel(detail, font: .systemFont(ofSize: 13), color: AppColor.textPrimary))
        return row
    }

    private func makeInfoLabelRow(label: String, value: String, valueColor: UIColor = AppColor.textPrimary) -> UIView {
        let row = UIStackView()
        row.axis = .horizontal
        row.alignment = .center
        row.spacing = 12
        let titleLabel = WildReportUI.label(label, font: .systemFont(ofSize: 13), color: AppColor.textSecondary)
        let valueLabel = WildReportUI.label(value, font: .systemFont(ofSize: 14, weight: .semibold), color: valueColor, lines: 0)
        valueLabel.textAlignment = .right
        row.addArrangedSubview(titleLabel)
        row.addArrangedSubview(valueLabel)
        titleLabel.setContentHuggingPriority(.required, for: .horizontal)
        valueLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
        return row
    }

    private func makePhoneRow(label: String, phone: String?, displayPhone: String?) -> UIView {
        let row = UIStackView()
        row.axis = .horizontal
        row.alignment = .center
        row.spacing = 12
        let titleLabel = WildReportUI.label(label, font: .systemFont(ofSize: 13), color: AppColor.textSecondary)
        row.addArrangedSubview(titleLabel)
        row.addArrangedSubview(UIView())

        guard let displayPhone, !displayPhone.isEmpty else {
            row.addArrangedSubview(WildReportUI.label("-", font: .systemFont(ofSize: 14, weight: .semibold), color: AppColor.textPrimary))
            return row
        }

        let button = UIButton(type: .system)
        button.titleLabel?.font = .systemFont(ofSize: 14, weight: .semibold)
        button.setTitle(displayPhone, for: .normal)
        button.setImage(UIImage(systemName: "phone.fill"), for: .normal)
        button.tintColor = AppColor.primary
        button.setTitleColor(AppColor.primary, for: .normal)
        button.semanticContentAttribute = .forceRightToLeft
        button.setConfigurationImagePlacement(.trailing, padding: 4)
        if let phone {
            button.addAction(UIAction { [weak self] _ in
                self?.callPhone(phone)
            }, for: .touchUpInside)
        }
        row.addArrangedSubview(button)
        button.setContentHuggingPriority(.required, for: .horizontal)
        return row
    }

    private func makeLineSpacedLabel(_ text: String, font: UIFont, color: UIColor) -> WildReportRiskInsetLabel {
        let label = WildReportRiskInsetLabel()
        label.font = font
        label.textColor = color
        label.numberOfLines = 0
        let paragraph = NSMutableParagraphStyle()
        paragraph.lineSpacing = 4
        label.attributedText = NSAttributedString(
            string: text,
            attributes: [.paragraphStyle: paragraph, .font: font, .foregroundColor: color]
        )
        return label
    }

    private func isReportClue(_ clue: WildReportRadarActiveClue) -> Bool {
        clue.type == .red || clue.type == .yellow
    }

    private func headerIconColor(for clue: WildReportRadarActiveClue) -> UIColor {
        if isReportClue(clue), clue.processingStarted, !clue.completed {
            return UIColor(hex: 0xF59E0B)
        }
        switch clue.type {
        case .blue: return AppColor.primary
        case .green: return AppColor.success
        case .red: return AppColor.danger
        case .yellow: return UIColor(hex: 0xF59E0B)
        }
    }

    private func statusColor(for clue: WildReportRadarActiveClue) -> UIColor {
        if clue.completed { return AppColor.success }
        if clue.processingStarted, !clue.completed { return UIColor(hex: 0xF59E0B) }
        switch clue.type {
        case .blue: return AppColor.primary
        case .green: return AppColor.success
        case .red: return AppColor.danger
        case .yellow: return UIColor(hex: 0xF59E0B)
        }
    }

    private func timelineDotColor(_ status: String) -> UIColor {
        switch status {
        case "done": return AppColor.success
        case "current": return UIColor(hex: 0xF59E0B)
        default: return UIColor(hex: 0xD1D5DB)
        }
    }

    private func callPhone(_ phone: String) {
        guard phone.contains("*") == false,
              let url = URL(string: "tel://\(phone)")
        else { return }
        UIApplication.shared.open(url)
    }

    private func addInfoRows(for clue: WildReportRadarActiveClue, to stack: UIStackView) {
        if let name = clue.name {
            stack.addArrangedSubview(WildReportInfoRowView(title: clue.type == .green ? "摄影师" : "点位", value: name))
        }
        if let store = clue.storeName {
            stack.addArrangedSubview(WildReportInfoRowView(title: "门店", value: store))
        }
        if let distance = clue.distanceText ?? clue.distance {
            stack.addArrangedSubview(WildReportInfoRowView(title: "距离", value: distance))
        }
        if let updatedAt = clue.updatedAt {
            stack.addArrangedSubview(WildReportInfoRowView(title: "时间", value: updatedAt))
        }
        if let reporter = clue.reporterName {
            stack.addArrangedSubview(WildReportInfoRowView(title: "举报人", value: reporter))
        }
        if let phone = clue.maskedReporterPhone ?? clue.reporterPhone {
            stack.addArrangedSubview(WildReportInfoRowView(title: "联系电话", value: phone))
        }
    }

    private func addDescriptionBlocks(for clue: WildReportRadarActiveClue, to stack: UIStackView) {
        if let summary = clue.summary {
            stack.addArrangedSubview(makeBodyLabel(summary))
        }
        if let detail = clue.detail, detail != clue.summary {
            stack.addArrangedSubview(makeBodyLabel(detail))
        }
        for content in clue.reportContents.prefix(3) {
            let time = content.time.isEmpty ? "" : "\(content.time)  "
            stack.addArrangedSubview(makeBodyLabel("\(time)\(content.content)"))
        }
    }

    private func addMediaRow(for clue: WildReportRadarActiveClue, to stack: UIStackView) {
        guard !clue.images.isEmpty else { return }
        let scroll = UIScrollView()
        scroll.showsHorizontalScrollIndicator = false
        let row = UIStackView()
        row.axis = .horizontal
        row.spacing = 10
        scroll.addSubview(row)
        row.snp.makeConstraints { make in
            make.edges.equalToSuperview()
            make.height.equalTo(72)
        }
        for url in clue.images.prefix(9) {
            let tile = WildReportRiskImageTile(url: url)
            tile.onTap = { [weak self] in self?.presentImagePreview(url: url) }
            row.addArrangedSubview(tile)
        }
        stack.addArrangedSubview(scroll)
        scroll.snp.makeConstraints { make in
            make.height.equalTo(72)
        }
    }

    private func addProcessingBlock(for clue: WildReportRadarActiveClue, to stack: UIStackView) {
        guard let record = clue.record else { return }
        let box = UIView()
        box.backgroundColor = UIColor(hex: 0xF8FAFF)
        box.layer.cornerRadius = 12
        box.layer.borderWidth = 1
        box.layer.borderColor = AppColor.cardOutline.cgColor
        let inner = UIStackView()
        inner.axis = .vertical
        inner.spacing = 8
        box.addSubview(inner)
        inner.snp.makeConstraints { make in
            make.edges.equalToSuperview().inset(12)
        }
        inner.addArrangedSubview(WildReportInfoRowView(title: "处理人", value: record.handlerName))
        inner.addArrangedSubview(makeBodyLabel(record.processResult))
        stack.addArrangedSubview(box)
    }

    private func makeButtonRow(for clue: WildReportRadarActiveClue) -> UIView {
        let row = UIStackView()
        row.axis = .horizontal
        row.spacing = 10
        row.distribution = .fillEqually

        let navButton = WildReportUI.iconButton(
            title: clue.type == .blue ? "刷新定位" : "导航到此",
            imageName: clue.type == .blue ? "location.fill" : "map.fill",
            style: .primary
        )
        navButton.addTarget(self, action: #selector(navigateTapped), for: .touchUpInside)
        let shareButton = WildReportUI.iconButton(title: "分享", imageName: "square.and.arrow.up", style: .secondary)
        shareButton.addTarget(self, action: #selector(shareTapped), for: .touchUpInside)
        [navButton, shareButton].forEach { button in
            button.snp.makeConstraints { make in make.height.equalTo(44) }
            row.addArrangedSubview(button)
        }
        return row
    }

    private func makeInlineLoading(text: String) -> UIView {
        let row = UIStackView()
        row.axis = .horizontal
        row.alignment = .center
        row.spacing = 8
        let indicator = UIActivityIndicatorView(style: .medium)
        indicator.startAnimating()
        row.addArrangedSubview(indicator)
        row.addArrangedSubview(WildReportUI.label(text, font: .systemFont(ofSize: 13), color: AppColor.textSecondary))
        return row
    }

    private func makeStateCard(icon: String, text: String) -> UIView {
        let card = WildReportCardView(spacing: 12, inset: 18)
        card.stack.alignment = .center
        let image = UIImageView(image: UIImage(systemName: icon))
        image.tintColor = AppColor.primary
        image.contentMode = .scaleAspectFit
        image.snp.makeConstraints { make in
            make.size.equalTo(34)
        }
        let label = WildReportUI.label(text, font: .systemFont(ofSize: 15, weight: .semibold), color: AppColor.textSecondary, lines: 0)
        label.textAlignment = .center
        card.stack.addArrangedSubview(image)
        card.stack.addArrangedSubview(label)
        return card
    }

    private func makeHighlight(text: String, color: UIColor) -> UIView {
        let label = WildReportUI.label(text, font: .systemFont(ofSize: 14, weight: .semibold), color: color, lines: 0)
        label.backgroundColor = color.withAlphaComponent(0.08)
        label.layer.cornerRadius = 10
        label.clipsToBounds = true
        label.textAlignment = .center
        label.snp.makeConstraints { make in
            make.height.greaterThanOrEqualTo(34)
        }
        return label
    }

    private func makeBodyLabel(_ text: String) -> UILabel {
        WildReportUI.label(text, font: .systemFont(ofSize: 14), color: AppColor.textSecondary, lines: 0)
    }

    private func iconName(for kind: WildReportMapMarkerKind) -> String {
        switch kind {
        case .selfLocation: return "location.fill"
        case .wildPhotographer: return "exclamationmark"
        case .processingClue: return "clock.fill"
        case .peerPhotographer: return "camera.fill"
        }
    }

    private func presentImagePreview(url: String) {
        presentImagePreview(imageURLs: [url], startIndex: 0)
    }

    @objc private func navigateTapped() {
        guard let marker = viewModel.selectedMarker,
              case .activeClue(let clue) = marker.detail
        else { return }
        if clue.type == .blue {
            Task {
                await viewModel.refreshLocation(
                    api: api,
                    locationProvider: locationProvider,
                    scenicId: scenicIdProvider(),
                    scenicName: scenicNameProvider()
                )
            }
            return
        }
        guard let target = viewModel.navigateToSelectedMarker() else { return }
        if let cachedOption = cachedNavigationOption(for: target) {
            openNavigation(cachedOption, cacheSelection: false)
            return
        }
        presentNavigationOptions(for: target)
    }

    @objc private func shareTapped() {
        showLoading()
        Task { [weak self] in
            guard let self else { return }
            let payload = await self.viewModel.shareSelectedClue(api: self.api)
            await MainActor.run {
                self.hideLoading()
                guard let payload else { return }
                WeChatShareService.shareMiniProgram(payload, previewImage: UIImage(named: "report_share_cover")) { [weak self] result in
                    self?.showShareResult(result)
                }
            }
        }
    }

    private func presentNavigationOptions(for target: WildReportMapMarker) {
        let options = availableNavigationOptions(for: target)
        let alert = UIAlertController(title: "选择导航软件", message: target.title, preferredStyle: .actionSheet)
        options.forEach { option in
            alert.addAction(UIAlertAction(title: option.title, style: .default) { [weak self] _ in
                self?.openNavigation(option, cacheSelection: true)
            })
        }
        alert.addAction(UIAlertAction(title: "取消", style: .cancel))
        if let popover = alert.popoverPresentationController {
            popover.sourceView = view
            popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.maxY - 80, width: 1, height: 1)
        }
        present(alert, animated: true)
    }

    private func availableNavigationOptions(for target: WildReportMapMarker) -> [WildReportNavigationOption] {
        WildReportNavigationApp.allCases.compactMap { app in
            guard let option = navigationOption(for: app, target: target) else { return nil }
            guard isNavigationOptionAvailable(option) else { return nil }
            return option
        }
    }

    private func cachedNavigationOption(for target: WildReportMapMarker) -> WildReportNavigationOption? {
        guard let rawValue = UserDefaults.standard.string(forKey: Self.cachedNavigationAppKey),
              let app = WildReportNavigationApp(rawValue: rawValue),
              let option = navigationOption(for: app, target: target),
              isNavigationOptionAvailable(option)
        else {
            UserDefaults.standard.removeObject(forKey: Self.cachedNavigationAppKey)
            return nil
        }
        return option
    }

    private func navigationOption(for app: WildReportNavigationApp, target: WildReportMapMarker) -> WildReportNavigationOption? {
        WildReportNavigationOption(
            app: app,
            title: app.title,
            url: navigationURL(for: app, target: target),
            requiresAvailabilityCheck: app.requiresAvailabilityCheck
        )
    }

    private func isNavigationOptionAvailable(_ option: WildReportNavigationOption) -> Bool {
        guard let url = option.url else { return false }
        return !option.requiresAvailabilityCheck || UIApplication.shared.canOpenURL(url)
    }

    private func openNavigation(_ option: WildReportNavigationOption, cacheSelection: Bool) {
        guard let url = option.url else { return }
        UIApplication.shared.open(url) { [weak self] success in
            guard success else {
                UserDefaults.standard.removeObject(forKey: Self.cachedNavigationAppKey)
                Task { @MainActor in
                    self?.showToast("无法打开\(option.title)")
                }
                return
            }
            guard cacheSelection else { return }
            Task { @MainActor in
                UserDefaults.standard.set(option.app.rawValue, forKey: Self.cachedNavigationAppKey)
            }
        }
    }

    private func navigationURL(for app: WildReportNavigationApp, target: WildReportMapMarker) -> URL? {
        let destLat = target.coordinate.latitude
        let destLon = target.coordinate.longitude
        let name = encodedNavigationText(target.title)
        switch app {
        case .apple:
            return URL(string: "http://maps.apple.com/?daddr=\(destLat),\(destLon)&dirflg=d")
        case .amap:
            return URL(string: "iosamap://path?sourceApplication=suixinkan&dlat=\(destLat)&dlon=\(destLon)&dname=\(name)&dev=0&t=0")
        case .baidu:
            return URL(string: "baidumap://map/direction?destination=latlng:\(destLat),\(destLon)|name:\(name)&mode=driving&coord_type=wgs84")
        case .tencent:
            return URL(string: "qqmap://map/routeplan?type=drive&tocoord=\(destLat),\(destLon)&to=\(name)&referer=suixinkan")
        case .google:
            return URL(string: "comgooglemaps://?daddr=\(destLat),\(destLon)&directionsmode=driving")
        }
    }

    private func encodedNavigationText(_ text: String) -> String {
        var allowed = CharacterSet.urlQueryAllowed
        allowed.remove(charactersIn: "&=?")
        return text.addingPercentEncoding(withAllowedCharacters: allowed) ?? text
    }

    @MainActor
    private func showShareResult(_ result: WeChatShareResult) {
        switch result {
        case .success:
            showToast("分享成功")
        case .cancelled:
            showToast("已取消分享")
        case .notInstalled:
            showToast("请先安装微信")
        case .unsupported:
            showToast("当前微信版本不支持分享")
        case .invalidPayload(let message):
            showToast(message)
        case .sendFailed:
            showToast("微信分享发送失败")
        case .unknown:
            showToast("微信分享失败")
        }
    }

    private func zoomMap(by multiplier: CLLocationDegrees) {
        let visibleRegion = mapView.region
        let span = MKCoordinateSpan(
            latitudeDelta: min(max(visibleRegion.span.latitudeDelta * multiplier, 0.001), 180),
            longitudeDelta: min(max(visibleRegion.span.longitudeDelta * multiplier, 0.001), 180)
        )
        mapView.setRegion(MKCoordinateRegion(center: visibleRegion.center, span: span), animated: true)
    }

    private func focusCurrentLocation() {
        viewModel.focusCurrentLocation()
    }
}

/// 风险地图导航软件选项。
private struct WildReportNavigationOption {
    let app: WildReportNavigationApp
    let title: String
    let url: URL?
    let requiresAvailabilityCheck: Bool
}

/// 风险地图支持打开的导航软件。
private enum WildReportNavigationApp: String, CaseIterable {
    case apple
    case amap
    case baidu
    case tencent
    case google

    var title: String {
        switch self {
        case .apple:
            return "Apple 地图"
        case .amap:
            return "高德地图"
        case .baidu:
            return "百度地图"
        case .tencent:
            return "腾讯地图"
        case .google:
            return "Google Maps"
        }
    }

    var requiresAvailabilityCheck: Bool {
        self != .apple
    }
}

extension WildReportRiskMapViewController: MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        guard let annotation = annotation as? WildReportMapAnnotation else { return nil }
        let identifier = "WildReportPulseMarkerAnnotationView"
        let view = (mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? WildReportPulseMarkerAnnotationView)
            ?? WildReportPulseMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
        view.annotation = annotation
        view.apply(marker: annotation.marker, selected: annotation.marker.id == viewModel.selectedMarkerID)
        return view
    }

    func mapView(_ mapView: MKMapView, didSelect annotation: MKAnnotation) {
        guard let annotation = annotation as? WildReportMapAnnotation else { return }
        Task {
            await viewModel.selectMarker(
                annotation.marker,
                api: api,
                scenicId: scenicIdProvider()
            )
        }
    }
}

/// 风险地图 MKAnnotation 包装。
final class WildReportMapAnnotation: NSObject, MKAnnotation {
    let marker: WildReportMapMarker
    var coordinate: CLLocationCoordinate2D { marker.coordinate }
    var title: String? { marker.title }

    init(marker: WildReportMapMarker) {
        self.marker = marker
        super.init()
    }
}

/// 风险地图自定义脉冲圆点标记。
private final class WildReportPulseMarkerAnnotationView: MKAnnotationView {
    private let titleBadge = WildReportMapMarkerTitleBadge()
    private let markerContainer = UIView()
    private let pulseRings = (0..<3).map { _ in UIView() }
    private let selectedRing = UIView()
    private let dot = UIView()
    private var showsTitleBadge = false

    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
        frame = CGRect(x: 0, y: 0, width: 72, height: 96)
        centerOffset = .zero
        canShowCallout = false
        setupUI()
    }

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

    func apply(marker: WildReportMapMarker, selected: Bool) {
        let color = marker.kind.color
        updateTitleBadgeVisibility(marker.kind == .selfLocation)
        titleBadge.text = marker.title
        pulseRings.forEach { ring in
            ring.layer.borderColor = color.withAlphaComponent(0.32).cgColor
        }
        selectedRing.layer.borderColor = color.withAlphaComponent(0.45).cgColor
        selectedRing.isHidden = !selected
        dot.backgroundColor = color
        startPulseIfNeeded()
    }

    private func setupUI() {
        backgroundColor = .clear
        titleBadge.isHidden = true
        addSubview(titleBadge)
        addSubview(markerContainer)
        pulseRings.forEach(markerContainer.addSubview)
        markerContainer.addSubview(selectedRing)
        markerContainer.addSubview(dot)

        pulseRings.forEach { view in
            view.backgroundColor = .clear
            view.layer.borderWidth = 2
            view.layer.cornerRadius = 10
            view.isUserInteractionEnabled = false
        }
        selectedRing.layer.cornerRadius = 14
        selectedRing.layer.borderWidth = 2
        dot.layer.cornerRadius = 7
        dot.layer.borderWidth = 2
        dot.layer.borderColor = UIColor.white.cgColor

        titleBadge.snp.makeConstraints { make in
            make.top.centerX.equalToSuperview()
            make.height.equalTo(24)
        }
        markerContainer.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.size.equalTo(56)
        }
        pulseRings.forEach { ring in
            ring.snp.makeConstraints { make in
                make.center.equalToSuperview()
                make.size.equalTo(20)
            }
        }
        selectedRing.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.size.equalTo(28)
        }
        dot.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.size.equalTo(14)
        }
    }

    private func updateTitleBadgeVisibility(_ isVisible: Bool) {
        guard showsTitleBadge != isVisible else { return }
        showsTitleBadge = isVisible
        titleBadge.isHidden = !isVisible
        markerContainer.snp.remakeConstraints { make in
            if isVisible {
                make.top.equalTo(titleBadge.snp.bottom).offset(4)
                make.centerX.equalToSuperview()
            } else {
                make.center.equalToSuperview()
            }
            make.size.equalTo(56)
        }
    }

    private func startPulseIfNeeded() {
        guard pulseRings.first?.layer.animation(forKey: "riskMapPulseOpacity") == nil else { return }
        let endSizes: [CGFloat] = [28, 40, 52]
        for (index, ring) in pulseRings.enumerated() {
            ring.layer.opacity = 0.72

            let scale = CABasicAnimation(keyPath: "transform.scale")
            scale.fromValue = 1
            scale.toValue = endSizes[index] / 20

            let opacity = CABasicAnimation(keyPath: "opacity")
            opacity.fromValue = 0.72
            opacity.toValue = 0

            let animation = CAAnimationGroup()
            animation.animations = [scale, opacity]
            animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
            animation.duration = 2
            animation.repeatCount = .infinity
            animation.isRemovedOnCompletion = false
            ring.layer.add(animation, forKey: "riskMapPulseOpacity")
        }
    }
}

/// 风险地图当前位置标记标题胶囊。
private final class WildReportMapMarkerTitleBadge: UIView {
    var text: String? {
        get { label.text }
        set { label.text = newValue }
    }

    private let label = UILabel()

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

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

    private func setupUI() {
        backgroundColor = .white
        layer.cornerRadius = 12
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOpacity = 0.08
        layer.shadowRadius = 4
        layer.shadowOffset = CGSize(width: 0, height: 2)

        label.font = .systemFont(ofSize: 11, weight: .semibold)
        label.textColor = AppColor.textPrimary
        addSubview(label)
        label.snp.makeConstraints { make in
            make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8))
        }
    }
}

/// 风险地图图例项视图。
private final class WildReportRiskLegendView: UIView {
    init(kind: WildReportMapMarkerKind) {
        super.init(frame: .zero)
        let row = UIStackView()
        row.axis = .horizontal
        row.alignment = .center
        row.spacing = 5
        let dot = UIView()
        dot.backgroundColor = kind.color
        dot.layer.cornerRadius = 4
        dot.snp.makeConstraints { make in
            make.size.equalTo(8)
        }
        let label = WildReportUI.label(kind.title, font: .systemFont(ofSize: 11, weight: .medium), color: AppColor.textSecondary, lines: 1)
        label.adjustsFontSizeToFitWidth = true
        label.minimumScaleFactor = 0.82
        row.addArrangedSubview(dot)
        row.addArrangedSubview(label)
        addSubview(row)
        row.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.leading.greaterThanOrEqualToSuperview()
            make.trailing.lessThanOrEqualToSuperview()
        }
    }

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

/// 风险地图状态胶囊。
private final class WildReportRiskStatusBadge: UILabel {
    private let contentInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)

    init(text: String, color: UIColor) {
        super.init(frame: .zero)
        self.text = text
        textColor = color
        font = .systemFont(ofSize: 12, weight: .semibold)
        textAlignment = .center
        backgroundColor = color.withAlphaComponent(0.12)
        layer.cornerRadius = 12
        clipsToBounds = true
        numberOfLines = 1
        adjustsFontSizeToFitWidth = true
        minimumScaleFactor = 0.8
        setContentHuggingPriority(.required, for: .horizontal)
        setContentCompressionResistancePriority(.required, for: .horizontal)
        snp.makeConstraints { make in
            make.height.equalTo(24)
            make.width.greaterThanOrEqualTo(52)
        }
    }

    override var intrinsicContentSize: CGSize {
        let size = super.intrinsicContentSize
        return CGSize(
            width: size.width + contentInsets.left + contentInsets.right,
            height: max(24, size.height + contentInsets.top + contentInsets.bottom)
        )
    }

    override func drawText(in rect: CGRect) {
        super.drawText(in: rect.inset(by: contentInsets))
    }

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

/// 风险地图底部卡片带内边距文本。
private final class WildReportRiskInsetLabel: UILabel {
    var textInsets = UIEdgeInsets.zero {
        didSet { invalidateIntrinsicContentSize() }
    }

    override func drawText(in rect: CGRect) {
        super.drawText(in: rect.inset(by: textInsets))
    }

    override var intrinsicContentSize: CGSize {
        let size = super.intrinsicContentSize
        return CGSize(
            width: size.width + textInsets.left + textInsets.right,
            height: size.height + textInsets.top + textInsets.bottom
        )
    }
}

/// 风险地图举报图片缩略图。
private final class WildReportRiskImageTile: UIView {
    private let imageView = UIImageView()
    var onTap: (() -> Void)?

    init(url: String, size: CGFloat = 72) {
        super.init(frame: .zero)
        backgroundColor = AppColor.pageBackgroundSoft
        layer.cornerRadius = 10
        layer.masksToBounds = true
        imageView.contentMode = .scaleAspectFill
        addSubview(imageView)
        imageView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        if let imageURL = URL(string: url) {
            imageView.kf.setImage(with: imageURL, placeholder: UIImage(systemName: "photo.fill"))
        } else {
            imageView.image = UIImage(systemName: "photo.fill")
            imageView.tintColor = AppColor.textTertiary
        }
        addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapped)))
        snp.makeConstraints { make in
            make.size.equalTo(size)
        }
    }

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

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