//
//  PunchPointDetailViewController.swift
//  suixinkan
//

import CoreLocation
import Kingfisher
import Photos
import SnapKit
import UIKit

#if targetEnvironment(simulator)
import MapKit
#endif

/// 打卡点详情页，对齐 Android `PunchPointDetailScreen`。
final class PunchPointDetailViewController: BaseViewController {
    private let viewModel: PunchPointDetailViewModel
    private let api: any PunchPointAPIProtocol

    private let mapView = PunchPointMapView()
    private let zoomStack = UIStackView()
    private let zoomInButton = PunchPointMapControlButton(assetName: "punch_point_zoom_in")
    private let zoomOutButton = PunchPointMapControlButton(assetName: "punch_point_zoom_out")
    private let locateButton = PunchPointMapControlButton(assetName: "punch_point_map_location")
    private let cardView = UIView()
    private let scrollView = UIScrollView()
    private let contentStack = UIStackView()
    private let bottomBar = UIView()
    private let editButton = UIButton(type: .system)
    private let emptyContainer = UIStackView()
    private let emptyLabel = UILabel()
    private let retryButton = UIButton(type: .system)

    var onChanged: (() -> Void)?

    /// 初始化打卡点详情页。
    init(
        punchPointId: Int64,
        viewModel: PunchPointDetailViewModel? = nil,
        api: (any PunchPointAPIProtocol)? = nil
    ) {
        self.viewModel = viewModel ?? PunchPointDetailViewModel(punchPointId: punchPointId)
        self.api = api ?? NetworkServices.shared.punchPointAPI
        super.init(nibName: nil, bundle: nil)
    }

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

    override func setupNavigationBar() {
        title = "打卡点详情"
    }

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

        zoomStack.axis = .vertical
        zoomStack.spacing = 0
        zoomStack.addArrangedSubview(zoomInButton)
        zoomStack.addArrangedSubview(zoomOutButton)
        cardView.backgroundColor = .white
        cardView.layer.cornerRadius = 12
        cardView.clipsToBounds = true
        scrollView.showsVerticalScrollIndicator = false
        contentStack.axis = .vertical
        contentStack.spacing = 12
        bottomBar.backgroundColor = .white
        editButton.setTitle("去编辑", for: .normal)
        editButton.setTitleColor(.white, for: .normal)
        editButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
        editButton.backgroundColor = AppColor.primary
        editButton.layer.cornerRadius = 12

        emptyContainer.axis = .vertical
        emptyContainer.alignment = .center
        emptyContainer.spacing = 12
        emptyLabel.text = "暂未获取到打卡点信息"
        emptyLabel.textColor = AppColor.textSecondary
        emptyLabel.font = .systemFont(ofSize: 15)
        retryButton.setTitle("重新加载", for: .normal)
        retryButton.setTitleColor(.white, for: .normal)
        retryButton.backgroundColor = AppColor.primary
        retryButton.layer.cornerRadius = 8
        emptyContainer.addArrangedSubview(emptyLabel)
        emptyContainer.addArrangedSubview(retryButton)
        emptyContainer.isHidden = true

        view.addSubview(mapView)
        view.addSubview(zoomStack)
        view.addSubview(locateButton)
        view.addSubview(cardView)
        cardView.addSubview(scrollView)
        scrollView.addSubview(contentStack)
        view.addSubview(bottomBar)
        bottomBar.addSubview(editButton)
        view.addSubview(emptyContainer)
    }

    override func setupConstraints() {
        mapView.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide)
            make.leading.trailing.equalToSuperview()
            make.height.equalToSuperview().multipliedBy(0.5)
        }
        zoomStack.snp.makeConstraints { make in
            make.trailing.equalToSuperview().inset(16)
            make.bottom.equalTo(locateButton.snp.top).offset(-12)
        }
        locateButton.snp.makeConstraints { make in
            make.trailing.equalToSuperview().inset(16)
            make.bottom.equalTo(cardView.snp.top).offset(-16)
            make.size.equalTo(40)
        }
        zoomInButton.snp.makeConstraints { make in
            make.size.equalTo(40)
        }
        zoomOutButton.snp.makeConstraints { make in
            make.size.equalTo(40)
        }
        bottomBar.snp.makeConstraints { make in
            make.leading.trailing.bottom.equalToSuperview()
        }
        editButton.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(16)
            make.leading.trailing.equalToSuperview().inset(16)
            make.height.equalTo(48)
            make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-20)
        }
        cardView.snp.makeConstraints { make in
            make.leading.trailing.equalToSuperview().inset(16)
            make.bottom.equalTo(bottomBar.snp.top)
            make.height.lessThanOrEqualToSuperview().multipliedBy(0.45)
            make.top.greaterThanOrEqualTo(mapView.snp.top).offset(60)
        }
        scrollView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        contentStack.snp.makeConstraints { make in
            make.edges.equalToSuperview().inset(UIEdgeInsets(top: 16, left: 12, bottom: 12, right: 12))
            make.width.equalTo(scrollView.snp.width).offset(-24)
        }
        emptyContainer.snp.makeConstraints { make in
            make.center.equalToSuperview()
        }
        retryButton.snp.makeConstraints { make in
            make.width.equalTo(118)
            make.height.equalTo(40)
        }
    }

    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) }
        }
        zoomInButton.addTarget(self, action: #selector(zoomInTapped), for: .touchUpInside)
        zoomOutButton.addTarget(self, action: #selector(zoomOutTapped), for: .touchUpInside)
        locateButton.addTarget(self, action: #selector(locateTapped), for: .touchUpInside)
        retryButton.addTarget(self, action: #selector(retryTapped), for: .touchUpInside)
        editButton.addTarget(self, action: #selector(editTapped), for: .touchUpInside)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        Task { await viewModel.load(api: api) }
    }

    @MainActor
    private func applyViewModel() {
        viewModel.isLoading && viewModel.detail == nil ? showLoading() : hideLoading()
        emptyContainer.isHidden = viewModel.detail != nil || viewModel.isLoading
        guard let detail = viewModel.detail else {
            cardView.isHidden = true
            bottomBar.isHidden = true
            return
        }
        cardView.isHidden = false
        bottomBar.isHidden = false
        if let region = detail.region {
            let coordinate = CLLocationCoordinate2D(latitude: region.lat, longitude: region.lot)
            mapView.updatePunchPointMarker(
                coordinate: coordinate,
                imageURL: detail.guideImages.first,
                fitWithUserLocation: true
            )
        }
        rebuildContent(detail)
    }

    private func rebuildContent(_ detail: PunchPointDetail) {
        contentStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
        contentStack.addArrangedSubview(PunchPointReadOnlyField(title: "*打卡点名称", value: detail.name))
        let coordinateText = detail.region.map { "\($0.lat),\($0.lot)" } ?? "--"
        contentStack.addArrangedSubview(PunchPointReadOnlyField(
            title: "*打卡点坐标",
            value: coordinateText,
            showsLocationIcon: true
        ))
        contentStack.addArrangedSubview(PunchPointReadOnlyField(title: "*打卡点地址", value: detail.displayAddress.nonEmptyTrimmed ?? "--"))
        if let description = detail.description?.nonEmptyTrimmed {
            contentStack.addArrangedSubview(PunchPointReadOnlyField(title: "打卡点描述", value: description, multiline: true))
        }
        contentStack.addArrangedSubview(makeImageSection(detail.guideImages))
        contentStack.addArrangedSubview(PunchPointRowField(title: "负责人", value: detail.creator?.nonEmptyTrimmed ?? "--"))
        contentStack.addArrangedSubview(PunchPointDivider())
        contentStack.addArrangedSubview(PunchPointRowField(title: "负责人手机号", value: PunchPointDisplayFormatter.maskedPhone(detail.creatorPhone)))
        contentStack.addArrangedSubview(PunchPointDivider())
        contentStack.addArrangedSubview(makeStatusRow(detail))
        contentStack.addArrangedSubview(PunchPointDivider())
        contentStack.addArrangedSubview(PunchPointRowField(title: "审核人", value: detail.auditor?.nonEmptyTrimmed ?? "--"))
        contentStack.addArrangedSubview(PunchPointDivider())
        contentStack.addArrangedSubview(PunchPointRowField(title: "审核时间", value: detail.auditTime?.nonEmptyTrimmed ?? "--"))
        contentStack.addArrangedSubview(PunchPointDivider())
        contentStack.addArrangedSubview(PunchPointRowField(title: "审核备注", value: detail.auditRemark?.nonEmptyTrimmed ?? "--"))
        contentStack.addArrangedSubview(makeQRSection(detail.mpQrcode))
    }

    private func makeImageSection(_ urls: [String]) -> UIView {
        let section = UIStackView()
        section.axis = .vertical
        section.spacing = 8
        let title = UILabel()
        title.attributedText = requiredTitle("*上传图片 (最多9张)")
        section.addArrangedSubview(title)
        guard !urls.isEmpty else {
            let empty = UILabel()
            empty.text = "暂无图片"
            empty.font = .systemFont(ofSize: 12)
            empty.textColor = AppColor.textSecondary
            section.addArrangedSubview(empty)
            return section
        }
        let scroll = UIScrollView()
        scroll.showsHorizontalScrollIndicator = false
        let row = UIStackView()
        row.axis = .horizontal
        row.spacing = 12
        scroll.addSubview(row)
        row.snp.makeConstraints { make in
            make.edges.equalToSuperview()
            make.height.equalTo(90)
        }
        urls.enumerated().forEach { index, urlText in
            let imageView = UIImageView()
            imageView.contentMode = .scaleAspectFill
            imageView.clipsToBounds = true
            imageView.layer.cornerRadius = 10
            imageView.isUserInteractionEnabled = true
            imageView.tag = index
            if let url = URL(string: urlText) {
                imageView.kf.setImage(with: url, placeholder: UIImage(named: "SplashLogo"))
            } else {
                imageView.image = UIImage(named: "SplashLogo")
            }
            imageView.snp.makeConstraints { make in
                make.width.equalTo(120)
            }
            imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imageTapped(_:))))
            row.addArrangedSubview(imageView)
        }
        scroll.snp.makeConstraints { make in
            make.height.equalTo(90)
        }
        section.addArrangedSubview(scroll)
        return section
    }

    private func makeStatusRow(_ detail: PunchPointDetail) -> UIView {
        let row = UIStackView()
        row.axis = .horizontal
        row.alignment = .center
        row.distribution = .equalSpacing
        let label = UILabel()
        label.text = "审核状态"
        label.font = .systemFont(ofSize: 14)
        label.textColor = UIColor(hex: 0x2E3746)
        let chip = PunchPointStatusChip()
        chip.apply(PunchPointDisplayFormatter.auditStatus(detail.status), compact: false)
        row.addArrangedSubview(label)
        row.addArrangedSubview(chip)
        row.isLayoutMarginsRelativeArrangement = true
        row.layoutMargins = UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0)
        return row
    }

    private func makeQRSection(_ qrCode: String?) -> UIView {
        let section = UIStackView()
        section.axis = .vertical
        section.alignment = .center
        section.spacing = 12
        let title = UILabel()
        title.text = "打卡点二维码"
        title.font = .systemFont(ofSize: 14)
        title.textColor = UIColor(hex: 0x2E3746)
        title.textAlignment = .left
        section.addArrangedSubview(title)
        title.snp.makeConstraints { make in
            make.width.equalTo(section.snp.width)
        }
        guard let qrCode = qrCode?.nonEmptyTrimmed else {
            let empty = UILabel()
            empty.text = "暂无二维码"
            empty.font = .systemFont(ofSize: 12)
            empty.textColor = AppColor.textSecondary
            section.addArrangedSubview(empty)
            return section
        }
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        imageView.layer.cornerRadius = 16
        if let url = URL(string: qrCode) {
            imageView.kf.setImage(with: url, placeholder: UIImage(named: "SplashLogo"))
        } else {
            imageView.image = UIImage(named: "SplashLogo")
        }
        imageView.snp.makeConstraints { make in
            make.size.equalTo(180)
        }
        let download = UIButton(type: .system)
        download.setTitle("下载二维码", for: .normal)
        download.setTitleColor(AppColor.primary, for: .normal)
        download.titleLabel?.font = .systemFont(ofSize: 14)
        download.backgroundColor = AppColor.primaryLight
        download.layer.cornerRadius = 8
        download.snp.makeConstraints { make in
            make.height.equalTo(38)
            make.width.equalTo(128)
        }
        download.addAction(UIAction { [weak self] _ in
            self?.downloadQRCode(qrCode)
        }, for: .touchUpInside)
        section.addArrangedSubview(imageView)
        section.addArrangedSubview(download)
        return section
    }

    private func downloadQRCode(_ urlText: String) {
        guard let url = URL(string: urlText) else {
            showToast("二维码地址无效")
            return
        }
        Task {
            do {
                let authorization = await PHPhotoLibrary.requestAuthorization(for: .addOnly)
                guard authorization == .authorized || authorization == .limited else {
                    await MainActor.run { self.presentPhotoPermissionAlert() }
                    return
                }
                let (data, _) = try await URLSession.shared.data(from: url)
                guard let image = UIImage(data: data) else { throw APIError.decodeFailed("二维码图片无效") }
                try await PHPhotoLibrary.shared().performChanges {
                    PHAssetChangeRequest.creationRequestForAsset(from: image)
                }
                await MainActor.run { self.showToast("已保存到相册") }
            } catch {
                await MainActor.run { self.showToast(error.localizedDescription.isEmpty ? "保存失败" : error.localizedDescription) }
            }
        }
    }

    @MainActor
    private func presentPhotoPermissionAlert() {
        let alert = UIAlertController(
            title: "无法保存二维码",
            message: "相册权限未授予，请在设置中开启",
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "取消", style: .cancel))
        alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in
            guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
            UIApplication.shared.open(url)
        })
        present(alert, animated: true)
    }

    private func requiredTitle(_ text: String) -> NSAttributedString {
        let mutable = NSMutableAttributedString(string: text, attributes: [
            .font: UIFont.systemFont(ofSize: 14, weight: .medium),
            .foregroundColor: UIColor(hex: 0x2E3746),
        ])
        if text.hasPrefix("*") {
            mutable.addAttribute(.foregroundColor, value: AppColor.danger, range: NSRange(location: 0, length: 1))
        }
        return mutable
    }

    @objc private func imageTapped(_ gesture: UITapGestureRecognizer) {
        guard let imageView = gesture.view, let urls = viewModel.detail?.guideImages else { return }
        presentImagePreview(imageURLs: urls, startIndex: imageView.tag)
    }

    @objc private func zoomInTapped() {
        mapView.zoomIn()
    }

    @objc private func zoomOutTapped() {
        mapView.zoomOut()
    }

    @objc private func locateTapped() {
        Task {
            do {
                let snapshot = try await LocationProvider.shared.requestSnapshot()
                let coordinate = CLLocationCoordinate2D(
                    latitude: snapshot.latitude,
                    longitude: snapshot.longitude
                )
                await MainActor.run { self.mapView.setCenter(coordinate, zoomLevel: 16) }
            } catch LocationProviderError.permissionDenied {
                await MainActor.run { self.presentLocationPermissionAlert() }
            } catch {
                await MainActor.run { self.showToast("定位失败") }
            }
        }
    }

    @MainActor
    private func presentLocationPermissionAlert() {
        let alert = UIAlertController(
            title: "无法定位",
            message: "定位权限未授予，请在设置中开启",
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "取消", style: .cancel))
        alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in
            guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
            UIApplication.shared.open(url)
        })
        present(alert, animated: true)
    }

    @objc private func retryTapped() {
        Task { await viewModel.load(api: api) }
    }

    @objc private func editTapped() {
        guard let detail = viewModel.detail else { return }
        let controller = PunchPointFormViewController(mode: .edit(id: detail.id), initialDetail: detail)
        controller.onSubmitSuccess = { [weak self] in
            guard let self else { return }
            self.onChanged?()
            Task { await self.viewModel.load(api: self.api) }
        }
        navigationController?.pushViewController(controller, animated: true)
    }
}

/// 高德地图容器，供打卡点详情与表单复用。
/// 打卡点地图视图，模拟器使用 MapKit 提供 marker、缩放、定位与点击选点行为。
#if targetEnvironment(simulator)
final class PunchPointMapView: UIView, MKMapViewDelegate {
    var onMapTap: ((CLLocationCoordinate2D) -> Void)?

    private let mapView: MKMapView
    private var punchPointAnnotation: MKPointAnnotation?
    private var selectedAnnotation: MKPointAnnotation?
    private var punchPointImageURL: URL?
    private var fitWithUserLocation = false

    override init(frame: CGRect) {
        mapView = MKMapView(frame: .zero)
        super.init(frame: frame)
        mapView.delegate = self
        mapView.showsUserLocation = true
        mapView.showsCompass = false
        mapView.showsScale = false
        mapView.pointOfInterestFilter = .includingAll
        addSubview(mapView)
        mapView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }

        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:)))
        tapGesture.cancelsTouchesInView = false
        mapView.addGestureRecognizer(tapGesture)
    }

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

    /// 更新原打卡点标记，可使用首张图片作为圆角 Marker。
    func updatePunchPointMarker(
        coordinate: CLLocationCoordinate2D?,
        imageURL: String? = nil,
        fitWithUserLocation: Bool = false
    ) {
        if let punchPointAnnotation {
            mapView.removeAnnotation(punchPointAnnotation)
            self.punchPointAnnotation = nil
        }
        punchPointImageURL = imageURL.flatMap(URL.init(string:))
        self.fitWithUserLocation = fitWithUserLocation
        guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return }
        let annotation = MKPointAnnotation()
        annotation.coordinate = coordinate
        annotation.title = "punch_point_original"
        punchPointAnnotation = annotation
        mapView.addAnnotation(annotation)
        if fitWithUserLocation {
            fitPunchPointAndUserLocation()
        }
    }

    /// 兼容仅需更新普通 Marker 的调用。
    func updateMarker(coordinate: CLLocationCoordinate2D?) {
        updatePunchPointMarker(coordinate: coordinate)
    }

    /// 更新地图点击产生的新位置 Marker。
    func updateSelectedMarker(coordinate: CLLocationCoordinate2D?) {
        if let selectedAnnotation {
            mapView.removeAnnotation(selectedAnnotation)
            self.selectedAnnotation = nil
        }
        guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return }
        let annotation = MKPointAnnotation()
        annotation.coordinate = coordinate
        annotation.title = "punch_point_selected"
        selectedAnnotation = annotation
        mapView.addAnnotation(annotation)
    }

    /// 设置地图中心点。
    func setCenter(_ coordinate: CLLocationCoordinate2D, zoomLevel: Double = 15, animated: Bool = true) {
        let delta = min(max(0.012 * pow(2, 15 - zoomLevel), 0.0005), 120)
        let region = MKCoordinateRegion(
            center: coordinate,
            span: MKCoordinateSpan(latitudeDelta: delta, longitudeDelta: delta)
        )
        mapView.setRegion(region, animated: animated)
    }

    /// 放大地图。
    func zoomIn() {
        updateZoom(scale: 0.5)
    }

    /// 缩小地图。
    func zoomOut() {
        updateZoom(scale: 2)
    }

    /// 居中到用户当前位置。
    func centerOnUserLocation() {
        guard let coordinate = mapView.userLocation.location?.coordinate,
              CLLocationCoordinate2DIsValid(coordinate),
              coordinate.latitude != 0 || coordinate.longitude != 0 else { return }
        setCenter(coordinate)
    }

    /// 同时展示原打卡点、用户位置和新选位置。
    func fitAllMarkers(edgePadding: UIEdgeInsets = UIEdgeInsets(top: 80, left: 56, bottom: 80, right: 56)) {
        var annotations: [any MKAnnotation] = []
        if let punchPointAnnotation { annotations.append(punchPointAnnotation) }
        if let selectedAnnotation { annotations.append(selectedAnnotation) }
        if mapView.userLocation.location != nil { annotations.append(mapView.userLocation) }
        guard !annotations.isEmpty else { return }
        if annotations.count == 1, let annotation = annotations.first {
            setCenter(annotation.coordinate, zoomLevel: 16)
        } else {
            var mapRect = MKMapRect.null
            for annotation in annotations {
                let point = MKMapPoint(annotation.coordinate)
                mapRect = mapRect.union(MKMapRect(
                    origin: point,
                    size: MKMapSize(width: 1, height: 1)
                ))
            }
            mapView.setVisibleMapRect(mapRect, edgePadding: edgePadding, animated: true)
        }
    }

    func mapView(_ mapView: MKMapView, viewFor annotation: any MKAnnotation) -> MKAnnotationView? {
        if annotation is MKUserLocation { return nil }
        let isOriginal = (annotation as AnyObject) === punchPointAnnotation
        let reuseIdentifier = isOriginal ? "PunchPointImageMarker" : "PunchPointSelectedMarker"
        let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier)
            ?? MKAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
        annotationView.annotation = annotation
        annotationView.canShowCallout = false
        annotationView.centerOffset = CGPoint(x: 0, y: -12)

        if isOriginal, let punchPointImageURL {
            annotationView.image = UIImage(named: "punch_point_marker")
            KingfisherManager.shared.retrieveImage(with: punchPointImageURL) { result in
                guard case let .success(value) = result else { return }
                Task { @MainActor in
                    guard annotationView.annotation === annotation else { return }
                    annotationView.image = Self.roundedMarkerImage(value.image)
                }
            }
        } else {
            annotationView.image = UIImage(named: "punch_point_marker")
        }
        return annotationView
    }

    func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
        guard userLocation.location != nil else { return }
        fitPunchPointAndUserLocation()
    }

    private func fitPunchPointAndUserLocation() {
        guard fitWithUserLocation else { return }
        fitAllMarkers(edgePadding: UIEdgeInsets(top: 80, left: 50, bottom: 80, right: 50))
    }

    private static func roundedMarkerImage(_ source: UIImage) -> UIImage {
        let size = CGSize(width: 40, height: 40)
        return UIGraphicsImageRenderer(size: size).image { _ in
            UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 6).addClip()
            source.draw(in: CGRect(origin: .zero, size: size))
        }
    }

    @objc private func mapTapped(_ gesture: UITapGestureRecognizer) {
        guard gesture.state == .ended else { return }
        let point = gesture.location(in: mapView)
        onMapTap?(mapView.convert(point, toCoordinateFrom: mapView))
    }

    private func updateZoom(scale: Double) {
        let current = mapView.region
        let span = MKCoordinateSpan(
            latitudeDelta: min(max(current.span.latitudeDelta * scale, 0.0005), 120),
            longitudeDelta: min(max(current.span.longitudeDelta * scale, 0.0005), 120)
        )
        mapView.setRegion(MKCoordinateRegion(center: current.center, span: span), animated: true)
    }
}
#else
/// 打卡点地图视图，真机封装高德地图 marker、缩放、定位与点击选点行为。
final class PunchPointMapView: UIView, MAMapViewDelegate {
    var onMapTap: ((CLLocationCoordinate2D) -> Void)?

    private let mapView: MAMapView
    private var punchPointAnnotation: MAPointAnnotation?
    private var selectedAnnotation: MAPointAnnotation?
    private var punchPointImageURL: URL?
    private var fitWithUserLocation = false

    override init(frame: CGRect) {
        _ = AMapBootstrap.configureIfNeeded()
        mapView = MAMapView(frame: .zero)
        super.init(frame: frame)
        mapView.delegate = self
        mapView.showsUserLocation = true
        mapView.showsCompass = false
        mapView.showsScale = false
        mapView.touchPOIEnabled = true
        mapView.zoomLevel = 15
        addSubview(mapView)
        mapView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }

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

    /// 更新原打卡点标记，可使用首张图片作为 Android 同款圆角 Marker。
    func updatePunchPointMarker(
        coordinate: CLLocationCoordinate2D?,
        imageURL: String? = nil,
        fitWithUserLocation: Bool = false
    ) {
        if let punchPointAnnotation {
            mapView.removeAnnotation(punchPointAnnotation)
            self.punchPointAnnotation = nil
        }
        punchPointImageURL = imageURL.flatMap(URL.init(string:))
        self.fitWithUserLocation = fitWithUserLocation
        guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return }
        let annotation = MAPointAnnotation()
        annotation.coordinate = coordinate
        annotation.title = "punch_point_original"
        punchPointAnnotation = annotation
        mapView.addAnnotation(annotation)
        if fitWithUserLocation {
            fitPunchPointAndUserLocation()
        }
    }

    /// 兼容仅需更新普通 Marker 的调用。
    func updateMarker(coordinate: CLLocationCoordinate2D?) {
        updatePunchPointMarker(coordinate: coordinate)
    }

    /// 更新地图点击产生的新位置 Marker。
    func updateSelectedMarker(coordinate: CLLocationCoordinate2D?) {
        if let selectedAnnotation {
            mapView.removeAnnotation(selectedAnnotation)
            self.selectedAnnotation = nil
        }
        guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return }
        let annotation = MAPointAnnotation()
        annotation.coordinate = coordinate
        annotation.title = "punch_point_selected"
        selectedAnnotation = annotation
        mapView.addAnnotation(annotation)
    }

    /// 设置地图中心点。
    func setCenter(_ coordinate: CLLocationCoordinate2D, zoomLevel: Double = 15, animated: Bool = true) {
        mapView.setCenter(coordinate, animated: animated)
        mapView.setZoomLevel(zoomLevel, animated: animated)
    }

    /// 放大地图。
    func zoomIn() {
        mapView.setZoomLevel(mapView.zoomLevel + 1, animated: true)
    }

    /// 缩小地图。
    func zoomOut() {
        mapView.setZoomLevel(mapView.zoomLevel - 1, animated: true)
    }

    /// 居中到用户当前位置。
    func centerOnUserLocation() {
        guard let coordinate = mapView.userLocation.location?.coordinate,
              CLLocationCoordinate2DIsValid(coordinate),
              coordinate.latitude != 0 || coordinate.longitude != 0 else { return }
        setCenter(coordinate)
    }

    /// 同时展示原打卡点、用户位置和新选位置，并留出 Android 同款边距。
    func fitAllMarkers(edgePadding: UIEdgeInsets = UIEdgeInsets(top: 80, left: 56, bottom: 80, right: 56)) {
        var annotations: [MAAnnotation] = []
        if let punchPointAnnotation { annotations.append(punchPointAnnotation) }
        if let selectedAnnotation { annotations.append(selectedAnnotation) }
        if let userLocation = mapView.userLocation,
           let location = userLocation.location,
           CLLocationCoordinate2DIsValid(location.coordinate) {
            annotations.append(userLocation)
        }
        guard !annotations.isEmpty else { return }
        if annotations.count == 1, let annotation = annotations.first {
            setCenter(annotation.coordinate, zoomLevel: 16)
        } else {
            mapView.showAnnotations(annotations, edgePadding: edgePadding, animated: true)
        }
    }

    private func fitPunchPointAndUserLocation() {
        guard fitWithUserLocation else { return }
        fitAllMarkers(edgePadding: UIEdgeInsets(top: 80, left: 50, bottom: 80, right: 50))
    }

    func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! {
        if annotation is MAUserLocation { return nil }
        let isOriginal = annotation === punchPointAnnotation
        let reuseIdentifier = isOriginal ? "PunchPointImageMarker" : "PunchPointSelectedMarker"
        let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier)
            ?? MAAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
        annotationView?.annotation = annotation
        annotationView?.canShowCallout = false
        annotationView?.centerOffset = CGPoint(x: 0, y: -12)

        if isOriginal, let punchPointImageURL {
            annotationView?.image = UIImage(named: "punch_point_marker")
            KingfisherManager.shared.retrieveImage(with: punchPointImageURL) { result in
                guard case let .success(value) = result else { return }
                Task { @MainActor in
                    guard annotationView?.annotation === annotation else { return }
                    annotationView?.image = Self.roundedMarkerImage(value.image)
                }
            }
        } else {
            annotationView?.image = UIImage(named: "punch_point_marker")
        }
        return annotationView
    }

    func mapView(_ mapView: MAMapView!, didUpdate userLocation: MAUserLocation!, updatingLocation: Bool) {
        guard updatingLocation, userLocation.location != nil else { return }
        fitPunchPointAndUserLocation()
    }

    private static func roundedMarkerImage(_ source: UIImage) -> UIImage {
        let size = CGSize(width: 40, height: 40)
        return UIGraphicsImageRenderer(size: size).image { _ in
            UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 6).addClip()
            source.draw(in: CGRect(origin: .zero, size: size))
        }
    }

    func mapView(_ mapView: MAMapView!, didSingleTappedAt coordinate: CLLocationCoordinate2D) {
        onMapTap?(coordinate)
    }

    func mapView(_ mapView: MAMapView!, didTouchPois pois: [Any]!) {
        guard let poi = pois.first as? MATouchPoi else { return }
        onMapTap?(poi.coordinate)
    }
}
#endif

/// 打卡点地图控制按钮。
/// 地图悬浮控制按钮。
final class PunchPointMapControlButton: UIButton {
    init(assetName: String) {
        super.init(frame: .zero)
        setImage(UIImage(named: assetName), for: .normal)
        backgroundColor = .white
        layer.cornerRadius = 12
        clipsToBounds = true
        imageView?.contentMode = .scaleAspectFit
        accessibilityLabel = switch assetName {
        case "punch_point_zoom_in": "放大地图"
        case "punch_point_zoom_out": "缩小地图"
        default: "定位当前位置"
        }
    }

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

/// 打卡点只读字段。
/// 详情页只读字段块。
final class PunchPointReadOnlyField: UIView {
    init(title: String, value: String, multiline: Bool = false, showsLocationIcon: Bool = false) {
        super.init(frame: .zero)
        let stack = UIStackView()
        stack.axis = .vertical
        stack.spacing = 8
        let titleLabel = UILabel()
        titleLabel.attributedText = Self.requiredTitle(title)
        let valueContainer = UIView()
        valueContainer.backgroundColor = UIColor(hex: 0xF4F4F4)
        valueContainer.layer.cornerRadius = 8
        let valueLabel = UILabel()
        valueLabel.text = value.nonEmptyTrimmed ?? "--"
        valueLabel.font = .systemFont(ofSize: 14)
        valueLabel.textColor = UIColor(hex: 0x4B5563)
        valueLabel.numberOfLines = multiline ? 0 : 2
        stack.addArrangedSubview(titleLabel)
        stack.addArrangedSubview(valueContainer)
        valueContainer.addSubview(valueLabel)
        if showsLocationIcon {
            let icon = UIImageView(image: UIImage(named: "punch_point_location"))
            icon.contentMode = .scaleAspectFit
            valueContainer.addSubview(icon)
            icon.snp.makeConstraints { make in
                make.trailing.equalToSuperview().inset(12)
                make.centerY.equalToSuperview()
                make.size.equalTo(20)
            }
            valueLabel.snp.makeConstraints { make in
                make.top.bottom.equalToSuperview().inset(12)
                make.leading.equalToSuperview().offset(12)
                make.trailing.lessThanOrEqualTo(icon.snp.leading).offset(-8)
            }
        } else {
            valueLabel.snp.makeConstraints { make in
                make.edges.equalToSuperview().inset(UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12))
            }
        }
        valueContainer.snp.makeConstraints { make in
            make.height.greaterThanOrEqualTo(multiline ? 72 : 46)
        }
        addSubview(stack)
        stack.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }

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

    private static func requiredTitle(_ text: String) -> NSAttributedString {
        let mutable = NSMutableAttributedString(string: text, attributes: [
            .font: UIFont.systemFont(ofSize: 14, weight: .medium),
            .foregroundColor: UIColor(hex: 0x2E3746),
        ])
        if text.hasPrefix("*") {
            mutable.addAttribute(.foregroundColor, value: AppColor.danger, range: NSRange(location: 0, length: 1))
        }
        return mutable
    }
}

/// 打卡点左右字段。
/// 详情页横向键值字段。
final class PunchPointRowField: UIView {
    init(title: String, value: String) {
        super.init(frame: .zero)
        let titleLabel = UILabel()
        titleLabel.text = title
        titleLabel.font = .systemFont(ofSize: 14)
        titleLabel.textColor = UIColor(hex: 0x2E3746)
        let valueLabel = UILabel()
        valueLabel.text = value.nonEmptyTrimmed ?? "--"
        valueLabel.font = .systemFont(ofSize: 14, weight: .medium)
        valueLabel.textColor = .black
        valueLabel.textAlignment = .right
        valueLabel.numberOfLines = 2
        addSubview(titleLabel)
        addSubview(valueLabel)
        titleLabel.snp.makeConstraints { make in
            make.leading.top.bottom.equalToSuperview().inset(UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0))
        }
        valueLabel.snp.makeConstraints { make in
            make.leading.greaterThanOrEqualTo(titleLabel.snp.trailing).offset(12)
            make.trailing.centerY.equalToSuperview()
        }
    }

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

/// 打卡点详情分割线。
/// 详情卡片内分割线。
final class PunchPointDivider: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor(hex: 0xF0F0F0)
        snp.makeConstraints { make in
            make.height.equalTo(1)
        }
    }

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

private extension String {
    var nonEmptyTrimmed: String? {
        let text = trimmingCharacters(in: .whitespacesAndNewlines)
        return text.isEmpty ? nil : text
    }
}
