//
//  WildPhotographerReportViewModels.swift
//  suixinkan
//

import Foundation
import MapKit
import UIKit

/// 举报摄影师附件上传协议，便于提交页注入 OSS 服务与单元测试替身。
@MainActor
protocol WildReportAttachmentUploading {
    /// 上传举报附件并返回 OSS 文件 URL。
    func uploadWildReportAttachment(
        data: Data,
        fileName: String,
        fileType: Int,
        scenicId: Int,
        onProgress: @escaping (Int) -> Void
    ) async throws -> String
}

extension OSSUploadService: WildReportAttachmentUploading {}

/// 举报摄影师模块通用日期格式化工具。
enum WildReportDateFormatters {
    static let displayDateTime: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        formatter.locale = Locale(identifier: "zh_CN")
        formatter.timeZone = .current
        return formatter
    }()
}

/// 举报摄影师模块通用联系方式解析工具。
enum WildReportContactParser {
    /// 将用户输入的联系方式拆分为手机号和微信号。
    static func parse(_ contact: String?) -> (phone: String?, wechat: String?) {
        let trimmed = contact?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        guard !trimmed.isEmpty else { return (nil, nil) }
        let digits = trimmed.filter(\.isNumber)
        if digits.count >= 7 {
            return (trimmed, nil)
        }
        return (nil, trimmed)
    }
}

/// 举报摄影师首页 ViewModel，维护首页远端文案和跨页面共享状态。
final class WildPhotographerReportHomeViewModel {
    private(set) var pageState: WildReportPageState = .normal
    private(set) var noticeItems: [String] = []
    private(set) var ruleGroups: [WildReportRuleGroup] = []
    private(set) var isLoadingCopy = false

    var onStateChange: (() -> Void)?
    var onShowMessage: ((String) -> Void)?

    var emptyTitle: String { "暂无举报服务" }
    var emptyMessage: String { "当前景区暂未开放举报摄影师入口。" }
    var shouldShowNoticeModule: Bool { !noticeItems.isEmpty || !ruleGroups.isEmpty }
    var shouldShowRuleButton: Bool { !ruleGroups.isEmpty }

    /// 加载首页举报须知与规则文案。
    func loadReportCopy(api: any WildPhotographerReportServing) async {
        guard !isLoadingCopy else { return }
        isLoadingCopy = true
        notifyStateChange()
        defer {
            isLoadingCopy = false
            notifyStateChange()
        }
        do {
            let response = try await api.reportCopy()
            noticeItems = response.notice
                .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
                .filter { !$0.isEmpty }
            ruleGroups = response.ruleGroups.compactMap { group in
                let title = group.title.trimmingCharacters(in: .whitespacesAndNewlines)
                let items = group.items
                    .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
                    .filter { !$0.isEmpty }
                guard !title.isEmpty || !items.isEmpty else { return nil }
                return WildReportRuleGroup(title: title, items: items)
            }
        } catch is CancellationError {
            return
        } catch {
            noticeItems = []
            ruleGroups = []
            onShowMessage?(error.localizedDescription)
        }
    }

    /// 切换页面状态。
    func setPageState(_ state: WildReportPageState) {
        pageState = state
        notifyStateChange()
    }

    private func notifyStateChange() {
        onStateChange?()
    }
}

/// 提交举报页 ViewModel，负责表单校验和前台定位。
final class WildPhotographerReportSubmitViewModel {
    private(set) var pageState: WildReportPageState = .normal
    private(set) var images: [WildReportAttachment]
    private(set) var videos: [WildReportAttachment]
    private(set) var paymentImages: [WildReportAttachment] = []
    private(set) var reportTypes: [WildReportType] = []
    private(set) var selectedReportType: WildReportType?
    private(set) var description = ""
    private(set) var contact = ""
    private(set) var reportLocation = ""
    private(set) var isLoadingReportTypes = false
    private(set) var isUpdatingLocation = false
    private(set) var isSubmitting = false
    private(set) var submitProgressText = ""
    private(set) var locationConfirmed = false
    private(set) var submitResult: WildReportSubmitResult?
    private(set) var validationMessage: String?

    var onStateChange: (() -> Void)?
    var onShowMessage: ((String) -> Void)?
    var onSubmitSuccess: ((WildReportSubmitResult) -> Void)?

    private let homeViewModel: WildPhotographerReportHomeViewModel
    private let locationProvider: any LocationProviding
    private var hasRequestedInitialLocation = false
    private var locationSnapshot: HomeLocationSnapshot?

    var locationTitleText: String {
        if reportLocation.isEmpty || isUpdatingLocation {
            return "正在获取当前位置..."
        }
        return "当前定位：\(reportLocation)"
    }

    init(
        homeViewModel: WildPhotographerReportHomeViewModel,
        locationProvider: any LocationProviding = LocationProvider.shared
    ) {
        self.homeViewModel = homeViewModel
        self.locationProvider = locationProvider
        self.images = []
        self.videos = []
    }

    /// 首次进入页面时自动请求定位。
    func loadInitialLocationIfNeeded() {
        guard !hasRequestedInitialLocation else { return }
        hasRequestedInitialLocation = true
        refreshCurrentLocation()
    }

    /// 从服务端加载举报类型。
    func loadReportTypes(api: any WildPhotographerReportServing) async {
        guard !isLoadingReportTypes else { return }
        isLoadingReportTypes = true
        notifyStateChange()
        defer {
            isLoadingReportTypes = false
            notifyStateChange()
        }

        do {
            let response = try await api.reportTypes()
            let loadedTypes = uniqueReportTypes(response.list)
            reportTypes = loadedTypes
            if let currentSelectedType = selectedReportType,
               let updatedType = reportTypes.first(where: { $0.value == currentSelectedType.value }) {
                selectedReportType = updatedType
            } else {
                selectedReportType = reportTypes.first
            }
        } catch is CancellationError {
            return
        } catch {
            reportTypes = []
            selectedReportType = nil
            onShowMessage?(error.localizedDescription)
        }
    }

    /// 选择举报类型。
    func selectReportType(_ type: WildReportType) {
        selectedReportType = type
        notifyStateChange()
    }

    /// 限制举报说明最多 500 字。
    func updateDescription(_ newValue: String) {
        description = String(newValue.prefix(500))
    }

    /// 更新联系方式。
    func updateContact(_ value: String) {
        contact = String(value.prefix(80))
    }

    /// 添加本地现场图片。
    func addImages(_ attachments: [WildReportAttachment]) {
        let remaining = max(0, 9 - evidenceCount)
        guard remaining > 0 else {
            onShowMessage?("现场证据最多上传9项")
            return
        }
        images.append(contentsOf: attachments.prefix(remaining))
        notifyStateChange()
    }

    /// 添加本地现场视频。
    func addVideos(_ attachments: [WildReportAttachment]) {
        let videoRemaining = max(0, 3 - videos.count)
        let evidenceRemaining = max(0, 9 - evidenceCount)
        let allowedCount = min(videoRemaining, evidenceRemaining)
        guard allowedCount > 0 else {
            onShowMessage?(videos.count >= 3 ? "现场视频最多上传3段" : "现场证据最多上传9项")
            return
        }
        videos.append(contentsOf: attachments.prefix(allowedCount))
        notifyStateChange()
    }

    /// 添加本地支付截图。
    func addPaymentImages(_ attachments: [WildReportAttachment]) {
        guard paymentImages.count < 3 else {
            onShowMessage?("支付截图最多上传3张")
            return
        }
        paymentImages.append(contentsOf: attachments.prefix(3 - paymentImages.count))
        notifyStateChange()
    }

    /// 删除指定附件。
    func removeAttachment(_ attachment: WildReportAttachment) {
        images.removeAll { $0.id == attachment.id }
        videos.removeAll { $0.id == attachment.id }
        paymentImages.removeAll { $0.id == attachment.id }
        notifyStateChange()
    }

    /// 使用前台定位 Provider 刷新当前位置。
    func refreshCurrentLocation() {
        guard !isUpdatingLocation else { return }
        isUpdatingLocation = true
        locationConfirmed = false
        reportLocation = "正在获取当前位置..."
        notifyStateChange()

        Task {
            do {
                let snapshot = try await locationProvider.requestSnapshot()
                applyResolvedLocation(snapshot)
            } catch {
                applyLocationFailure(error.localizedDescription)
            }
        }
    }

    /// 清除提交结果，用于导航返回。
    func clearSubmitResult() {
        submitResult = nil
        notifyStateChange()
    }

    /// 校验表单并提交举报到后端。
    func submitReport(
        api: any WildPhotographerReportServing,
        uploader: any WildReportAttachmentUploading,
        scenicId: Int,
        scenicName: String
    ) async {
        validationMessage = nil
        guard validateBeforeSubmit(scenicId: scenicId) else { return }
        guard !isSubmitting else { return }

        isSubmitting = true
        notifyStateChange()
        defer {
            isSubmitting = false
            submitProgressText = ""
            notifyStateChange()
        }

        do {
            let evidenceRequests = try await uploadAttachments(
                images + videos,
                uploader: uploader,
                scenicId: scenicId
            )
            let paymentRequests = try await uploadAttachments(
                paymentImages,
                uploader: uploader,
                scenicId: scenicId
            )

            let submitResponse = try await api.submitReport(buildSubmitRequest(
                scenicId: scenicId,
                scenicName: scenicName,
                evidences: evidenceRequests,
                paymentScreenshots: paymentRequests
            ))

            let record = buildSubmittedRecord(response: submitResponse)
            let result = WildReportSubmitResult(
                record: record,
                imageCount: images.count,
                videoCount: videos.count
            )
            submitResult = result
            notifyStateChange()
            onSubmitSuccess?(result)
        } catch is CancellationError {
            return
        } catch {
            setValidationMessage(error.localizedDescription)
        }
    }

    /// 重置表单到初始状态。
    func resetForm() {
        images = []
        videos = []
        paymentImages = []
        reportTypes = []
        selectedReportType = nil
        description = ""
        contact = ""
        reportLocation = ""
        locationConfirmed = false
        isUpdatingLocation = false
        isSubmitting = false
        submitProgressText = ""
        locationSnapshot = nil
        hasRequestedInitialLocation = false
        submitResult = nil
        validationMessage = nil
        notifyStateChange()
    }

    func applyResolvedLocation(_ snapshot: HomeLocationSnapshot) {
        let address = snapshot.address.isEmpty
            ? String(format: "%.5f, %.5f", snapshot.latitude, snapshot.longitude)
            : snapshot.address
        let reportAddress = Self.simplifiedReportLocation(address)
        locationSnapshot = HomeLocationSnapshot(
            latitude: snapshot.latitude,
            longitude: snapshot.longitude,
            address: reportAddress
        )
        reportLocation = reportAddress
        locationConfirmed = true
        isUpdatingLocation = false
        notifyStateChange()
    }

    private func applyLocationFailure(_ message: String) {
        reportLocation = ""
        locationSnapshot = nil
        locationConfirmed = false
        isUpdatingLocation = false
        notifyStateChange()
        onShowMessage?(message)
    }

    private func setValidationMessage(_ message: String) {
        validationMessage = message
        notifyStateChange()
        onShowMessage?(message)
    }

    private func notifyStateChange() {
        onStateChange?()
    }

    private func uniqueReportTypes(_ types: [WildReportType]) -> [WildReportType] {
        var seen: Set<Int> = []
        return types.filter { seen.insert($0.value).inserted }
    }

    private static func simplifiedReportLocation(_ address: String) -> String {
        let trimmed = address.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return trimmed }
        let detail = WildReportLocationParser.split(trimmed).detail
        let simplified = removeAdministrativePrefix(from: detail)
        return simplified.isEmpty ? trimmed : simplified
    }

    private static func removeAdministrativePrefix(from address: String) -> String {
        var text = address
        removeLeadingRegion(from: &text, matches: [
            "北京市", "天津市", "上海市", "重庆市",
            "内蒙古自治区", "广西壮族自治区", "西藏自治区", "宁夏回族自治区", "新疆维吾尔自治区"
        ])
        removeLeadingRegion(from: &text, suffixes: ["省"], maxLength: 8)
        removeLeadingRegion(from: &text, suffixes: ["自治州", "地区", "盟", "市"], maxLength: 12)
        removeLeadingRegion(from: &text, suffixes: ["自治县", "新区", "区", "县", "旗", "市"], maxLength: 12)
        return text.trimmingCharacters(in: .whitespacesAndNewlines)
    }

    private static func removeLeadingRegion(from text: inout String, matches: [String]) {
        guard let match = matches.first(where: { text.hasPrefix($0) }) else { return }
        text.removeFirst(match.count)
    }

    private static func removeLeadingRegion(from text: inout String, suffixes: [String], maxLength: Int) {
        guard let match = suffixes.compactMap({ suffix -> String? in
            guard let range = text.range(of: suffix) else { return nil }
            let prefix = String(text[..<range.upperBound])
            return prefix.count <= maxLength ? prefix : nil
        }).min(by: { $0.count < $1.count }) else { return }
        text.removeFirst(match.count)
    }

    private var evidenceCount: Int {
        images.count + videos.count
    }

    private func validateBeforeSubmit(scenicId: Int) -> Bool {
        guard scenicId > 0 else {
            setValidationMessage("请先选择景区后再提交举报。")
            return false
        }
        guard selectedReportType != nil else {
            setValidationMessage("请先选择举报类型。")
            return false
        }
        guard !images.isEmpty || !videos.isEmpty else {
            setValidationMessage("请至少上传一项现场证据。")
            return false
        }
        guard evidenceCount <= 9 else {
            setValidationMessage("现场证据最多上传9项。")
            return false
        }
        guard paymentImages.count <= 3 else {
            setValidationMessage("支付截图最多上传3张。")
            return false
        }
        guard !description.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
            setValidationMessage("请填写举报说明。")
            return false
        }
        guard !isUpdatingLocation else {
            setValidationMessage("正在获取定位，请稍后再提交。")
            return false
        }
        guard locationConfirmed, !reportLocation.isEmpty, reportLocation != "正在获取当前位置..." else {
            setValidationMessage("请先更新举报位置后再提交举报。")
            return false
        }
        guard locationSnapshot != nil else {
            setValidationMessage("缺少定位坐标，请重新更新举报位置。")
            return false
        }
        return true
    }

    private func uploadAttachments(
        _ attachments: [WildReportAttachment],
        uploader: any WildReportAttachmentUploading,
        scenicId: Int
    ) async throws -> [WildReportEvidenceRequest] {
        var requests: [WildReportEvidenceRequest] = []
        for attachment in attachments {
            let data = try attachment.uploadData()
            let url = try await uploader.uploadWildReportAttachment(
                data: data,
                fileName: attachment.fileName,
                fileType: attachment.fileType,
                scenicId: scenicId
            ) { _ in }
            requests.append(WildReportEvidenceRequest(
                fileURL: url,
                fileType: attachment.fileType,
                fileName: attachment.fileName
            ))
        }
        return requests
    }

    private func buildSubmitRequest(
        scenicId: Int,
        scenicName: String,
        evidences: [WildReportEvidenceRequest],
        paymentScreenshots: [WildReportEvidenceRequest]
    ) -> WildReportSubmitRequest {
        let locationParts = WildReportLocationParser.split(reportLocation)
        let trimmedScenicName = scenicName.trimmingCharacters(in: .whitespacesAndNewlines)
        let locationName = !trimmedScenicName.isEmpty ? trimmedScenicName : locationParts.scenic
        let snapshot = locationSnapshot ?? HomeLocationSnapshot(latitude: 0, longitude: 0, address: reportLocation)
        let trimmedContact = contact.trimmingCharacters(in: .whitespacesAndNewlines)
        return WildReportSubmitRequest(
            scenicId: scenicId,
            reportType: selectedReportType?.value ?? 0,
            desc: description.trimmingCharacters(in: .whitespacesAndNewlines),
            storeUserId: nil,
            contactPhone: trimmedContact.isEmpty ? nil : trimmedContact,
            locationName: locationName.isEmpty ? nil : locationName,
            locationAddress: locationParts.detail.isEmpty ? reportLocation : locationParts.detail,
            lat: snapshot.latitude,
            lng: snapshot.longitude,
            evidences: evidences,
            paymentScreenshots: paymentScreenshots
        )
    }

    private func buildSubmittedRecord(response: WildReportSubmitResponse) -> WildReportRecord {
        let parsedContact = WildReportContactParser.parse(contact)
        return WildReportRecord(
            serverID: response.id > 0 ? response.id : nil,
            id: response.complaintNo,
            title: "摄影师举报",
            reportType: selectedReportType ?? WildReportType(value: 0, label: ""),
            status: .pending,
            location: reportLocation,
            submitTime: WildReportDateFormatters.displayDateTime.string(from: Date()),
            description: description.trimmingCharacters(in: .whitespacesAndNewlines),
            wechatID: parsedContact.wechat ?? "",
            phoneNumber: parsedContact.phone ?? "",
            imageCount: images.count,
            videoCount: videos.count,
            handlerInfo: nil
        )
    }
}

/// 举报成功页 ViewModel。
final class WildPhotographerReportSuccessViewModel {
    let result: WildReportSubmitResult
    let progressSteps = ["已提交", "现场核查中", "处理结果反馈"]

    init(result: WildReportSubmitResult) {
        self.result = result
    }

    var uploadSummary: String {
        var parts: [String] = []
        if result.imageCount > 0 { parts.append("\(result.imageCount)张图片") }
        if result.videoCount > 0 { parts.append("\(result.videoCount)段视频") }
        return parts.isEmpty ? "暂无" : parts.joined(separator: " ")
    }

    /// 复制举报编号到剪贴板。
    @MainActor
    func copyReportID() -> String {
        UIPasteboard.general.string = result.record.id
        return result.record.id
    }
}

/// 举报详情页 ViewModel。
final class WildPhotographerReportDetailViewModel {
    private let initialRecord: WildReportRecord
    private let homeViewModel: WildPhotographerReportHomeViewModel?

    private(set) var detail: WildReportDetailData?
    private(set) var isLoadingDetail = false
    private(set) var detailErrorMessage: String?
    private(set) var actionMessage: String?
    var onStateChange: (() -> Void)?
    var onShowMessage: ((String) -> Void)?

    init(record: WildReportRecord, homeViewModel: WildPhotographerReportHomeViewModel? = nil) {
        self.initialRecord = record
        self.homeViewModel = homeViewModel
    }

    var record: WildReportRecord { detail?.toRecord(fallback: initialRecord) ?? initialRecord }
    var heroTitle: String { record.title.isEmpty ? "摄影师举报" : record.title }
    var scenicAreaName: String { WildReportLocationParser.split(record.location).scenic }
    var detailAddress: String {
        let detail = WildReportLocationParser.split(record.location).detail
        return detail.contains("附近") ? detail : "\(detail)附近"
    }
    var mapCoordinate: CLLocationCoordinate2D? {
        guard let latitudeText = detail?.latitude,
              let longitudeText = detail?.longitude,
              let latitude = Double(latitudeText),
              let longitude = Double(longitudeText) else {
            return nil
        }
        let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
        guard CLLocationCoordinate2DIsValid(coordinate), latitude != 0 || longitude != 0 else { return nil }
        return coordinate
    }

    var displayStatusText: String { detail?.displayStatusText ?? record.status.rawValue }
    var descriptionText: String { record.description }
    var contactText: String { record.contactDisplayText }
    var handleRemarkText: String { detail?.handleRemark.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" }
    var supplementaryEvidences: [WildReportSupplementaryEvidence] {
        if let detail {
            return detail.supplementRecords
        }
        return []
    }
    var reporterMediaItems: [WildReportReporterMediaItem] {
        if let detail {
            return detail.evidences.enumerated().map { index, evidence in
                evidence.isVideo
                    ? .video(index: index, url: evidence.fileURL, fileName: evidence.fileName, fileTypeText: evidence.fileTypeText)
                    : .image(index: index, url: evidence.fileURL, fileName: evidence.fileName, fileTypeText: evidence.fileTypeText)
            }
        }
        let images = (0..<record.imageCount).map { WildReportReporterMediaItem.image(index: $0, url: nil, fileName: nil, fileTypeText: nil) }
        let videos = (0..<record.videoCount).map { WildReportReporterMediaItem.video(index: $0, url: nil, fileName: nil, fileTypeText: nil) }
        return images + videos
    }
    var paymentScreenshotItems: [WildReportReporterMediaItem] {
        detail?.paymentScreenshots.enumerated().map { index, evidence in
            let fileTypeText = evidence.fileTypeText.trimmingCharacters(in: .whitespacesAndNewlines)
            return .image(
                index: index,
                url: evidence.fileURL,
                fileName: evidence.fileName,
                fileTypeText: fileTypeText.isEmpty ? "支付截图" : fileTypeText
            )
        } ?? []
    }
    var imageCountText: String { "\(record.imageCount)张" }
    var videoCountText: String { "\(record.videoCount)段" }
    var paymentScreenshotSummary: String { "\(paymentScreenshotItems.count)张" }
    var reporterMediaSummary: String {
        switch (record.imageCount, record.videoCount) {
        case (0, 0): return "0项"
        case (_, 0): return "\(record.imageCount)张"
        case (0, _): return "\(record.videoCount)段"
        default: return "\(record.imageCount)张 \(record.videoCount)段"
        }
    }
    var progressSteps: [WildReportProgressStep] {
        if let detail, !detail.timeline.isEmpty {
            return Self.buildProgressSteps(from: detail.timeline, status: record.status)
        }
        return Self.buildProgressSteps(for: record)
    }
    var showHandlerInfo: Bool { (record.status == .processed || record.status == .rejected) && record.handlerInfo != nil }
    var handlerInfo: WildReportHandlerInfo? { record.handlerInfo }

    /// 有服务端数值 ID 时拉取举报详情；本地临时记录继续展示已有内容。
    func loadDetailIfNeeded(api: any WildPhotographerReportServing, force: Bool = false) async {
        guard let serverID = initialRecord.serverID, serverID > 0, (force || detail == nil), !isLoadingDetail else { return }
        isLoadingDetail = true
        detailErrorMessage = nil
        notifyStateChange()
        defer {
            isLoadingDetail = false
            notifyStateChange()
        }
        do {
            detail = try await api.reportDetail(id: serverID)
        } catch is CancellationError {
            return
        } catch {
            detailErrorMessage = error.localizedDescription
            onShowMessage?(error.localizedDescription)
        }
    }

    /// 展示媒体预览提示。
    func showMediaPreview(kind: String) {
        showActionMessage("暂不支持预览\(kind)")
    }

    /// 展示位置预览提示。
    func showLocationPreview() {
        showActionMessage("举报位置：\(record.location)")
    }

    /// 拉取当前举报的小程序分享载荷。
    func shareReport(api: any WildPhotographerReportServing) async -> WeChatMiniProgramSharePayload? {
        guard let serverID = record.serverID, serverID > 0 else {
            showActionMessage("当前举报暂未同步到服务器，无法分享")
            return nil
        }
        do {
            let shareData = try await api.reportShare(id: String(serverID))
            return shareData.payload
        } catch is CancellationError {
            return nil
        } catch {
            showActionMessage(error.localizedDescription)
            return nil
        }
    }

    private func showActionMessage(_ message: String) {
        actionMessage = message
        onShowMessage?(message)
        notifyStateChange()
    }

    private static func buildProgressSteps(for record: WildReportRecord) -> [WildReportProgressStep] {
        [
            WildReportProgressStep(
                id: "current-status",
                title: record.status.rawValue,
                timeText: record.submitTime,
                state: record.status == .pending || record.status == .processing ? .current : .completed
            )
        ]
    }

    private static func buildProgressSteps(
        from timeline: [WildReportDetailTimelineItem],
        status: WildReportStatus
    ) -> [WildReportProgressStep] {
        timeline.enumerated().map { index, item in
            let isLast = index == timeline.count - 1
            let lastIsOpen = isLast && (status == .pending || status == .processing)
            return WildReportProgressStep(
                id: "\(index)-\(item.time)",
                title: item.content.isEmpty ? "处理进度" : item.content,
                timeText: item.time,
                state: lastIsOpen ? .current : .completed
            )
        }
    }

    private func notifyStateChange() {
        onStateChange?()
    }
}

/// 我的举报列表 ViewModel。
final class WildPhotographerReportListViewModel {
    private static let pageSize = 10

    private(set) var selectedFilter: WildReportListFilter = .all
    private(set) var shareMessage: String?
    private(set) var reports: [WildReportRecord] = []
    private(set) var isLoading = false
    private(set) var isLoadingMore = false
    private(set) var hasMore = true
    private(set) var page = 1
    private(set) var total = 0
    private(set) var errorMessage: String?

    let homeViewModel: WildPhotographerReportHomeViewModel
    var onStateChange: (() -> Void)?
    var onShowMessage: ((String) -> Void)?

    init(homeViewModel: WildPhotographerReportHomeViewModel) {
        self.homeViewModel = homeViewModel
    }

    var filteredReports: [WildReportRecord] {
        reports
    }

    var footerTip: String { "温馨提示：为保障处理效率，请如实举报并提供有效证据" }

    var footerDisplayText: String {
        if isLoadingMore { return "正在加载更多..." }
        if isLoading && reports.isEmpty { return "正在加载举报记录..." }
        if !hasMore && !reports.isEmpty { return footerTip }
        return footerTip
    }

    /// 首次加载我的举报列表。
    func loadInitial(api: any WildPhotographerReportServing, scenicId: Int?) async {
        await reload(api: api, scenicId: scenicId)
    }

    /// 切换列表筛选标签并重新拉取第一页。
    func selectFilter(_ filter: WildReportListFilter, api: any WildPhotographerReportServing, scenicId: Int?) async {
        guard selectedFilter != filter else { return }
        selectedFilter = filter
        await reload(api: api, scenicId: scenicId)
    }

    /// 滚动到底部时加载下一页。
    func loadMoreIfNeeded(api: any WildPhotographerReportServing, scenicId: Int?) async {
        guard hasMore, !isLoading, !isLoadingMore else { return }
        let nextPage = page + 1
        isLoadingMore = true
        errorMessage = nil
        notifyStateChange()
        defer {
            isLoadingMore = false
            notifyStateChange()
        }
        do {
            let response = try await api.reportList(makeRequest(page: nextPage, scenicId: scenicId))
            let newRecords = response.list.map { $0.toRecord() }
            reports.append(contentsOf: newRecords)
            page = response.page
            total = response.total
            hasMore = reports.count < response.total
        } catch is CancellationError {
            return
        } catch {
            errorMessage = error.localizedDescription
            onShowMessage?(error.localizedDescription)
        }
    }

    /// 拉取指定举报的小程序分享载荷。
    func shareReport(_ record: WildReportRecord, api: any WildPhotographerReportServing) async -> WeChatMiniProgramSharePayload? {
        guard let serverID = record.serverID, serverID > 0 else {
            shareMessage = "当前举报暂未同步到服务器，无法分享"
            onShowMessage?(shareMessage ?? "")
            notifyStateChange()
            return nil
        }
        do {
            let shareData = try await api.reportShare(id: String(serverID))
            shareMessage = nil
            notifyStateChange()
            return shareData.payload
        } catch is CancellationError {
            return nil
        } catch {
            shareMessage = error.localizedDescription
            onShowMessage?(shareMessage ?? "")
            notifyStateChange()
            return nil
        }
    }

    private func reload(api: any WildPhotographerReportServing, scenicId: Int?) async {
        let hadReportsBeforeReload = !reports.isEmpty
        isLoading = true
        isLoadingMore = false
        errorMessage = nil
        page = 1
        hasMore = true
        notifyStateChange()
        defer {
            isLoading = false
            notifyStateChange()
        }
        do {
            let response = try await api.reportList(makeRequest(page: 1, scenicId: scenicId))
            reports = response.list.map { $0.toRecord() }
            page = response.page
            total = response.total
            hasMore = reports.count < response.total
        } catch is CancellationError {
            return
        } catch {
            errorMessage = error.localizedDescription
            hasMore = false
            if !hadReportsBeforeReload {
                reports = []
                total = 0
            }
            onShowMessage?(error.localizedDescription)
        }
    }

    private func makeRequest(page: Int, scenicId: Int?) -> WildReportListRequest {
        WildReportListRequest(
            scenicId: scenicId,
            handleStatus: selectedFilter.handleStatusValue,
            page: page,
            pageSize: Self.pageSize
        )
    }

    private func notifyStateChange() {
        onStateChange?()
    }
}

/// 补充证据页 ViewModel。
final class WildReportSupplementEvidenceViewModel {
    private(set) var text = ""
    private(set) var images: [WildReportAttachment] = []
    private(set) var videos: [WildReportAttachment] = []
    private(set) var validationMessage: String?
    private(set) var didSubmit = false
    private(set) var isSubmitting = false
    private(set) var submitProgressText = ""

    let reportID: String
    let serverID: Int?
    var onStateChange: (() -> Void)?
    var onShowMessage: ((String) -> Void)?
    var onSubmitSuccess: (() -> Void)?

    private let homeViewModel: WildPhotographerReportHomeViewModel

    init(record: WildReportRecord, homeViewModel: WildPhotographerReportHomeViewModel) {
        self.reportID = record.id
        self.serverID = record.serverID
        self.homeViewModel = homeViewModel
    }

    /// 限制补充说明最多 500 字。
    func updateText(_ newValue: String) {
        text = String(newValue.prefix(500))
        notifyStateChange()
    }

    /// 添加本地补充图片。
    func addImages(_ attachments: [WildReportAttachment]) {
        let remaining = max(0, 9 - evidenceCount)
        guard remaining > 0 else {
            onShowMessage?("补充证据最多上传9项")
            return
        }
        images.append(contentsOf: attachments.prefix(remaining))
        notifyStateChange()
    }

    /// 添加本地补充视频。
    func addVideos(_ attachments: [WildReportAttachment]) {
        let videoRemaining = max(0, 3 - videos.count)
        let evidenceRemaining = max(0, 9 - evidenceCount)
        let allowedCount = min(videoRemaining, evidenceRemaining)
        guard allowedCount > 0 else {
            onShowMessage?(videos.count >= 3 ? "补充视频最多上传3段" : "补充证据最多上传9项")
            return
        }
        videos.append(contentsOf: attachments.prefix(allowedCount))
        notifyStateChange()
    }

    /// 删除指定附件。
    func removeAttachment(_ attachment: WildReportAttachment) {
        images.removeAll { $0.id == attachment.id }
        videos.removeAll { $0.id == attachment.id }
        notifyStateChange()
    }

    /// 校验并提交补充证据。
    func submit(
        api: any WildPhotographerReportServing,
        uploader: any WildReportAttachmentUploading,
        scenicId: Int
    ) async {
        validationMessage = nil
        let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines)
        guard validateBeforeSubmit(trimmedText: trimmedText, scenicId: scenicId) else { return }
        guard !isSubmitting else { return }

        isSubmitting = true
        notifyStateChange()
        defer {
            isSubmitting = false
            submitProgressText = ""
            notifyStateChange()
        }

        do {
            let evidenceRequests = try await uploadAttachments(
                images + videos,
                uploader: uploader,
                scenicId: scenicId
            )

            try await api.supplementReport(WildReportSupplementRequest(
                id: serverID ?? 0,
                desc: trimmedText.isEmpty ? nil : trimmedText,
                evidences: evidenceRequests.isEmpty ? nil : evidenceRequests
            ))

            didSubmit = true
            notifyStateChange()
            onSubmitSuccess?()
        } catch is CancellationError {
            return
        } catch {
            setValidationMessage(error.localizedDescription)
        }
    }

    private var evidenceCount: Int {
        images.count + videos.count
    }

    private func validateBeforeSubmit(trimmedText: String, scenicId: Int) -> Bool {
        guard let serverID, serverID >= 1 else {
            setValidationMessage("当前举报暂未同步到服务器，无法补充证据")
            return false
        }
        guard !trimmedText.isEmpty || evidenceCount > 0 else {
            setValidationMessage("请至少填写补充说明或上传一项证据。")
            return false
        }
        guard evidenceCount <= 9 else {
            setValidationMessage("补充证据最多上传9项。")
            return false
        }
        guard videos.count <= 3 else {
            setValidationMessage("补充视频最多上传3段。")
            return false
        }
        if evidenceCount > 0, scenicId <= 0 {
            setValidationMessage("请先选择景区后再提交补充证据。")
            return false
        }
        return true
    }

    private func setValidationMessage(_ message: String) {
        validationMessage = message
        notifyStateChange()
        onShowMessage?(message)
    }

    private func uploadAttachments(
        _ attachments: [WildReportAttachment],
        uploader: any WildReportAttachmentUploading,
        scenicId: Int
    ) async throws -> [WildReportEvidenceRequest] {
        var requests: [WildReportEvidenceRequest] = []
        for attachment in attachments {
            let data = try attachment.uploadData()
            let url = try await uploader.uploadWildReportAttachment(
                data: data,
                fileName: attachment.fileName,
                fileType: attachment.fileType,
                scenicId: scenicId
            ) { _ in }
            requests.append(WildReportEvidenceRequest(
                fileURL: url,
                fileType: attachment.fileType,
                fileName: attachment.fileName
            ))
        }
        return requests
    }

    private func notifyStateChange() {
        onStateChange?()
    }
}

/// 附近风险地图 ViewModel。
final class WildReportRiskMapViewModel {
    private static let riskMapLocationAccuracy = kCLLocationAccuracyHundredMeters

    private var loadedDetailMarkerIDs: Set<String> = []

    private(set) var region: MKCoordinateRegion
    private(set) var markers: [WildReportMapMarker]
    private(set) var scenicAreaName: String
    private(set) var selectedMarkerID: String?
    private(set) var isLoading = false
    private(set) var isLoadingDetail = false
    private(set) var errorMessage: String?
    private(set) var greenMarkerTip = ""
    private(set) var legendItems: [WildReportRiskLegendItem] = []
    private(set) var currentCoordinate: CLLocationCoordinate2D?

    var onStateChange: (() -> Void)?

    init() {
        self.markers = []
        self.region = Self.region(for: [])
        let scenicName = AppStore.shared.session.currentScenicName.trimmingCharacters(in: .whitespacesAndNewlines)
        self.scenicAreaName = scenicName.isEmpty ? "当前景区" : scenicName
        self.selectedMarkerID = nil
    }

    var selectedMarker: WildReportMapMarker? {
        guard let selectedMarkerID else { return nil }
        return markers.first { $0.id == selectedMarkerID }
    }

    var selectedClue: WildReportRadarActiveClue? {
        guard let marker = selectedMarker,
              case .activeClue(let clue) = marker.detail else { return nil }
        return clue
    }

    /// 首次加载附近风险地图数据。
    func loadInitial(
        api: any WildPhotographerReportServing,
        locationProvider: any LocationProviding,
        scenicId: Int? = nil,
        scenicName: String? = nil
    ) async {
        await loadRiskMap(
            api: api,
            locationProvider: locationProvider,
            scenicId: scenicId,
            scenicName: scenicName,
            selectCurrentLocation: selectedMarkerID == nil || selectedMarker?.kind == .selfLocation
        )
    }

    /// 选中地图标记。
    func selectMarker(
        _ marker: WildReportMapMarker,
        api: (any WildPhotographerReportServing)? = nil,
        scenicId: Int? = nil
    ) async {
        selectedMarkerID = marker.id
        region = MKCoordinateRegion(
            center: marker.coordinate,
            span: MKCoordinateSpan(latitudeDelta: 0.018, longitudeDelta: 0.018)
        )
        notifyStateChange()

        guard marker.kind != .selfLocation,
              !loadedDetailMarkerIDs.contains(marker.id),
              let api,
              let request = markerDetailRequest(for: marker, scenicId: scenicId)
        else { return }

        isLoadingDetail = true
        notifyStateChange()
        do {
            let detail = try await api.markerDetail(request)
            mergeDetail(detail, intoMarkerID: marker.id)
            loadedDetailMarkerIDs.insert(marker.id)
        } catch {
            errorMessage = error.localizedDescription
        }
        isLoadingDetail = false
        notifyStateChange()
    }

    /// 导航到选中标记或刷新蓝点定位。
    @discardableResult
    func navigateToSelectedMarker() -> WildReportMapMarker? {
        guard let marker = selectedMarker,
              case .activeClue(let clue) = marker.detail else { return nil }

        if clue.type == .blue {
            return nil
        }

        region = MKCoordinateRegion(
            center: marker.coordinate,
            span: MKCoordinateSpan(latitudeDelta: 0.018, longitudeDelta: 0.018)
        )
        notifyStateChange()
        return marker
    }

    /// 将地图快速移动到已加载的当前位置标记。
    @discardableResult
    func focusCurrentLocation() -> Bool {
        guard let marker = markers.first(where: { $0.kind == .selfLocation }) else { return false }
        selectedMarkerID = marker.id
        region = MKCoordinateRegion(
            center: marker.coordinate,
            span: MKCoordinateSpan(latitudeDelta: 0.018, longitudeDelta: 0.018)
        )
        notifyStateChange()
        return true
    }

    /// 将地图居中到当前位置标记。
    func refreshLocation(
        api: any WildPhotographerReportServing,
        locationProvider: any LocationProviding,
        scenicId: Int? = nil,
        scenicName: String? = nil
    ) async {
        await loadRiskMap(
            api: api,
            locationProvider: locationProvider,
            scenicId: scenicId,
            scenicName: scenicName,
            selectCurrentLocation: true
        )
    }

    /// 拉取当前选中举报线索的小程序分享载荷。
    func shareSelectedClue(api: any WildPhotographerReportServing) async -> WeChatMiniProgramSharePayload? {
        guard let marker = selectedMarker,
              marker.kind == .wildPhotographer || marker.kind == .processingClue,
              let reportID = reportID(fromMarkerID: marker.id)
        else {
            errorMessage = "请选择可分享的举报线索"
            notifyStateChange()
            return nil
        }

        do {
            let shareData = try await api.reportShare(id: String(reportID))
            return shareData.payload
        } catch is CancellationError {
            return nil
        } catch {
            errorMessage = error.localizedDescription
            notifyStateChange()
            return nil
        }
    }

    /// 生成图片预览提示。
    func showImagePreview(_ url: String) {
        _ = url
    }

    private func loadRiskMap(
        api: any WildPhotographerReportServing,
        locationProvider: any LocationProviding,
        scenicId: Int?,
        scenicName: String?,
        selectCurrentLocation: Bool
    ) async {
        let resolvedScenicId = scenicId ?? AppStore.shared.session.currentScenicId
        guard resolvedScenicId >= 1 else {
            errorMessage = "请先选择景区后再查看风险地图"
            notifyStateChange()
            return
        }

        let coordinate = await requestRiskMapCoordinate(locationProvider: locationProvider)
        currentCoordinate = coordinate
        await requestRiskMap(
            api: api,
            scenicId: resolvedScenicId,
            scenicName: scenicName,
            coordinate: coordinate,
            selectCurrentLocation: selectCurrentLocation
        )
    }

    private func requestRiskMapCoordinate(locationProvider: any LocationProviding) async -> CLLocationCoordinate2D? {
        try? await locationProvider.requestCoordinate(desiredAccuracy: Self.riskMapLocationAccuracy)
    }

    private func requestRiskMap(
        api: any WildPhotographerReportServing,
        scenicId: Int,
        scenicName: String?,
        coordinate: CLLocationCoordinate2D?,
        selectCurrentLocation: Bool
    ) async {
        isLoading = true
        errorMessage = nil
        notifyStateChange()

        do {
            let response = try await api.riskMap(WildReportRiskMapRequest(
                scenicId: scenicId,
                latitude: coordinate?.latitude,
                longitude: coordinate?.longitude
            ))
            scenicAreaName = response.scenicName.nonEmpty
                ?? scenicName?.nonEmpty
                ?? AppStore.shared.session.currentScenicName.nonEmpty
                ?? scenicAreaName
            greenMarkerTip = response.greenMarkerTip
            legendItems = response.legend
            markers = buildMarkers(response: response, currentCoordinate: coordinate)
            region = Self.region(for: markers)
            if selectCurrentLocation, let current = markers.first(where: { $0.kind == .selfLocation }) {
                selectedMarkerID = current.id
            } else if selectedMarker == nil {
                selectedMarkerID = markers.first?.id
            }
        } catch {
            errorMessage = error.localizedDescription
        }

        isLoading = false
        notifyStateChange()
    }

    private func buildMarkers(
        response: WildReportRiskMapResponse,
        currentCoordinate: CLLocationCoordinate2D?
    ) -> [WildReportMapMarker] {
        let remoteMarkers = response.markers.compactMap {
            marker(from: $0, greenMarkerTip: response.greenMarkerTip)
        }
        var result: [WildReportMapMarker] = []
        if let currentCoordinate {
            result.append(currentLocationMarker(
                coordinate: currentCoordinate,
                nearestAbnormalText: nearestAbnormalText(for: remoteMarkers)
            ))
        }
        result.append(contentsOf: remoteMarkers)
        return result
    }

    private func currentLocationMarker(
        coordinate: CLLocationCoordinate2D,
        nearestAbnormalText: String
    ) -> WildReportMapMarker {
        WildReportMapMarker(
            id: "blue-current",
            title: "我的位置",
            coordinate: coordinate,
            kind: .selfLocation,
            detail: .activeClue(WildReportRadarActiveClue(
                id: "blue-current",
                type: .blue,
                displayTitle: "当前位置",
                statusText: "当前位置",
                actionText: "刷新定位",
                nearestAbnormalText: nearestAbnormalText,
                distance: "0米",
                direction: "当前位置",
                updatedAt: "实时定位",
                name: scenicAreaName,
                storeName: nil,
                avatar: nil,
                summary: "蓝色标记为当前位置，可对比周边摄影师风险点位距离。",
                detail: "点击刷新定位可重新计算周边风险点位距离。",
                distanceText: "0米",
                reporterName: nil,
                reporterPhone: nil,
                maskedReporterPhone: nil,
                reportContents: [],
                images: [],
                processingStarted: false,
                completed: false,
                record: nil,
                processingTimeline: [],
                completionPhoto: nil
            ))
        )
    }

    private func marker(
        from dto: WildReportRiskMapMarkerDTO,
        greenMarkerTip: String
    ) -> WildReportMapMarker? {
        guard let latitude = Double(dto.latitude),
              let longitude = Double(dto.longitude),
              dto.id > 0
        else { return nil }

        let markerType = WildReportRiskMarkerType(rawValue: dto.markerType)
        let statusText = dto.handleStatusText?.nonEmpty
            ?? dto.handleStatus?.displayText
            ?? defaultStatusText(for: markerType, dto: dto)
        let title = dto.title.nonEmpty ?? dto.locationName.nonEmpty ?? defaultTitle(for: markerType, id: dto.id)
        let clue = WildReportRadarActiveClue(
            id: String(dto.id),
            type: markerType.radarType,
            displayTitle: title,
            statusText: statusText,
            actionText: markerType == .blue ? "刷新定位" : "导航到此",
            nearestAbnormalText: nil,
            distance: formattedDistance(dto.distanceM),
            direction: nil,
            updatedAt: dto.createdAt.nonEmpty,
            name: dto.name?.nonEmpty ?? dto.locationName.nonEmpty,
            storeName: dto.storeName?.nonEmpty,
            avatar: dto.avatar?.nonEmpty,
            summary: markerType == .green ? (dto.greenMarkerTip?.nonEmpty ?? greenMarkerTip.nonEmpty) : dto.reportTypeText.nonEmpty,
            detail: dto.activityText?.nonEmpty,
            distanceText: formattedDistance(dto.distanceM),
            reporterName: nil,
            reporterPhone: nil,
            maskedReporterPhone: nil,
            reportContents: dto.reportTypeText.nonEmpty.map {
                [WildReportRadarReportContent(time: dto.createdAt.nonEmpty ?? "", content: $0)]
            } ?? [],
            images: [],
            processingStarted: markerType == .yellow || statusText == WildReportStatus.processing.rawValue,
            completed: statusText == WildReportStatus.processed.rawValue,
            record: nil,
            processingTimeline: [],
            completionPhoto: nil
        )
        return WildReportMapMarker(
            id: "\(markerType.rawValue)-\(dto.id)",
            title: title,
            coordinate: CLLocationCoordinate2D(latitude: latitude, longitude: longitude),
            kind: markerType.markerKind,
            detail: .activeClue(clue)
        )
    }

    private func markerDetailRequest(
        for marker: WildReportMapMarker,
        scenicId: Int?
    ) -> WildReportMarkerDetailRequest? {
        let resolvedScenicId = scenicId ?? AppStore.shared.session.currentScenicId
        guard resolvedScenicId >= 1 else { return nil }
        let parts = marker.id.split(separator: "-", maxSplits: 1).map(String.init)
        guard parts.count == 2, let id = Int(parts[1]), id >= 1 else { return nil }
        return WildReportMarkerDetailRequest(
            scenicId: resolvedScenicId,
            markerType: parts[0],
            id: id,
            latitude: currentCoordinate?.latitude,
            longitude: currentCoordinate?.longitude
        )
    }

    private func reportID(fromMarkerID markerID: String) -> Int? {
        let parts = markerID.split(separator: "-", maxSplits: 1).map(String.init)
        guard parts.count == 2, let id = Int(parts[1]), id >= 1 else { return nil }
        return id
    }

    private func mergeDetail(_ detail: WildReportMarkerDetailResponse, intoMarkerID markerID: String) {
        guard let index = markers.firstIndex(where: { $0.id == markerID }),
              case .activeClue(let oldClue) = markers[index].detail
        else { return }

        let markerType = WildReportRiskMarkerType(rawValue: detail.markerType.nonEmpty ?? oldClue.type.rawValue)
        let statusText = detail.statusTag.nonEmpty
            ?? detail.handleStatusText.nonEmpty
            ?? detail.handleStatus?.displayText
            ?? oldClue.statusText
        let reportContents = !detail.reportContents.isEmpty
            ? detail.reportContents.map { WildReportRadarReportContent(time: $0.time, content: $0.desc) }
            : (detail.desc.nonEmpty.map { [WildReportRadarReportContent(time: detail.createdAt, content: $0)] } ?? oldClue.reportContents)
        let imageURLs = detail.evidences.filter(\.isImage).map(\.fileURL)
        let clue = WildReportRadarActiveClue(
            id: detail.clueId.nonEmpty ?? String(detail.id > 0 ? detail.id : Int(oldClue.id) ?? 0),
            type: markerType.radarType,
            displayTitle: detail.title.nonEmpty ?? oldClue.displayTitle,
            statusText: statusText,
            actionText: oldClue.actionText,
            nearestAbnormalText: oldClue.nearestAbnormalText,
            distance: formattedDistance(detail.distanceM),
            direction: oldClue.direction,
            updatedAt: detail.createdAt.nonEmpty ?? oldClue.updatedAt,
            name: detail.name.nonEmpty ?? oldClue.name,
            storeName: detail.storeName.nonEmpty ?? oldClue.storeName,
            avatar: detail.avatar.nonEmpty ?? oldClue.avatar,
            summary: detail.greenMarkerTip.nonEmpty ?? detail.reportTypeText.nonEmpty ?? oldClue.summary,
            detail: detail.activityText.nonEmpty ?? detail.desc.nonEmpty ?? oldClue.detail,
            distanceText: formattedDistance(detail.distanceM) ?? oldClue.distanceText,
            reporterName: detail.reporterName.nonEmpty ?? oldClue.reporterName,
            reporterPhone: detail.reporterPhone.nonEmpty ?? oldClue.reporterPhone,
            maskedReporterPhone: detail.reporterPhone.nonEmpty.map(Self.maskPhone) ?? oldClue.maskedReporterPhone,
            reportContents: reportContents,
            images: imageURLs.isEmpty ? oldClue.images : imageURLs,
            processingStarted: markerType == .yellow || statusText == WildReportStatus.processing.rawValue,
            completed: statusText == WildReportStatus.processed.rawValue,
            record: oldClue.record,
            processingTimeline: oldClue.processingTimeline,
            completionPhoto: oldClue.completionPhoto
        )
        markers[index] = WildReportMapMarker(
            id: markers[index].id,
            title: detail.title.nonEmpty ?? markers[index].title,
            coordinate: markers[index].coordinate,
            kind: markerType.markerKind,
            detail: .activeClue(clue)
        )
    }

    private func nearestAbnormalText(for markers: [WildReportMapMarker]) -> String {
        let distances = markers.compactMap { marker -> Double? in
            guard case .activeClue(let clue) = marker.detail,
                  clue.type == .red || clue.type == .yellow,
                  let distance = clue.distanceText ?? clue.distance
            else { return nil }
            if distance.hasSuffix("公里") {
                return Double(distance.replacingOccurrences(of: "公里", with: "")).map { $0 * 1000 }
            }
            return Double(distance.replacingOccurrences(of: "米", with: ""))
        }
        guard let minDistance = distances.min() else { return "周边暂无异常点位" }
        return "距离最近的异常点位\(formattedDistance(minDistance) ?? "0米")"
    }

    private func defaultStatusText(for markerType: WildReportRiskMarkerType, dto: WildReportRiskMapMarkerDTO) -> String {
        switch markerType {
        case .green:
            return dto.online == false ? "离线" : "在线"
        case .yellow:
            return "处理中线索"
        case .red, .unknown:
            return dto.reportTypeText.nonEmpty ?? "异常线索"
        case .blue:
            return "当前位置"
        }
    }

    private func defaultTitle(for markerType: WildReportRiskMarkerType, id: Int) -> String {
        switch markerType {
        case .green:
            return "景区内摄影师"
        case .yellow, .red, .unknown:
            return "线索 ID：\(id)"
        case .blue:
            return "我的位置"
        }
    }

    private func formattedDistance(_ meters: Double) -> String? {
        guard meters > 0 else { return nil }
        if meters < 1000 {
            return "\(Int(meters.rounded()))米"
        }
        return String(format: "%.1f公里", meters / 1000)
    }

    private static func region(for markers: [WildReportMapMarker]) -> MKCoordinateRegion {
        guard !markers.isEmpty else {
            return MKCoordinateRegion(
                center: CLLocationCoordinate2D(latitude: 0, longitude: 0),
                span: MKCoordinateSpan(latitudeDelta: 0.08, longitudeDelta: 0.08)
            )
        }
        let latitudes = markers.map(\.coordinate.latitude)
        let longitudes = markers.map(\.coordinate.longitude)
        let minLat = latitudes.min() ?? markers[0].coordinate.latitude
        let maxLat = latitudes.max() ?? markers[0].coordinate.latitude
        let minLon = longitudes.min() ?? markers[0].coordinate.longitude
        let maxLon = longitudes.max() ?? markers[0].coordinate.longitude
        let center = CLLocationCoordinate2D(
            latitude: (minLat + maxLat) / 2,
            longitude: (minLon + maxLon) / 2
        )
        return MKCoordinateRegion(
            center: center,
            span: MKCoordinateSpan(
                latitudeDelta: max(0.018, (maxLat - minLat) * 1.6),
                longitudeDelta: max(0.018, (maxLon - minLon) * 1.6)
            )
        )
    }

    private static func maskPhone(_ phone: String) -> String {
        guard phone.count == 11, phone.allSatisfy(\.isNumber) else { return phone }
        return "\(phone.prefix(3))****\(phone.suffix(4))"
    }

    private func notifyStateChange() {
        onStateChange?()
    }
}

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