//
//  WildPhotographerReportModels.swift
//  suixinkan
//

import CoreLocation
import Foundation
import MapKit
import UIKit

/// 举报摄影师页面状态，用于加载、空态和异常态展示。
enum WildReportPageState: String, CaseIterable, Hashable {
    case normal
    case loading
    case empty
    case error

    var title: String {
        switch self {
        case .normal: return "正常"
        case .loading: return "加载"
        case .empty: return "空状态"
        case .error: return "异常"
        }
    }
}

/// 举报附件类型，区分现场图片、视频和支付截图。
enum WildReportAttachmentKind: Hashable {
    case image
    case video
    case payment
}

/// 举报类型接口返回的单项数据，由后端动态配置。
struct WildReportType: Decodable, Hashable {
    let value: Int
    let label: String
}

/// 举报类型接口返回的列表包裹。
struct WildReportTypeListResponse: Decodable, Hashable {
    let list: [WildReportType]

    init(list: [WildReportType]) {
        self.list = list
    }
}

/// 举报首页须知与规则接口响应体。
struct WildReportCopyResponse: Decodable, Equatable, Hashable {
    let notice: [String]
    let ruleGroups: [WildReportRuleGroup]

    enum CodingKeys: String, CodingKey {
        case notice
        case ruleGroups = "rule_groups"
    }

    init(notice: [String] = [], ruleGroups: [WildReportRuleGroup] = []) {
        self.notice = notice
        self.ruleGroups = ruleGroups
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        notice = try container.decodeIfPresent([String].self, forKey: .notice) ?? []
        ruleGroups = try container.decodeIfPresent([WildReportRuleGroup].self, forKey: .ruleGroups) ?? []
    }
}

/// 举报规则分组，用于首页规则入口与举报规则页展示。
struct WildReportRuleGroup: Decodable, Equatable, Hashable {
    let title: String
    let items: [String]

    enum CodingKeys: String, CodingKey {
        case title
        case items
    }

    init(title: String = "", items: [String] = []) {
        self.title = title
        self.items = items
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decodeIfPresent(String.self, forKey: .title) ?? ""
        items = try container.decodeIfPresent([String].self, forKey: .items) ?? []
    }
}

/// 举报附件实体，表示一条本地待上传的附件。
struct WildReportAttachment: Hashable {
    let id: UUID
    let kind: WildReportAttachmentKind
    let title: String
    let localFileURL: URL?
    let fileName: String
    let fileType: Int
    let uploadedURL: String?

    init(
        id: UUID = UUID(),
        kind: WildReportAttachmentKind,
        title: String,
        localFileURL: URL? = nil,
        fileName: String? = nil,
        uploadedURL: String? = nil
    ) {
        self.id = id
        self.kind = kind
        self.title = title
        self.localFileURL = localFileURL
        self.fileName = fileName ?? "\(title).\(kind.defaultFileExtension)"
        self.fileType = kind.reportFileType
        self.uploadedURL = uploadedURL
    }
}

extension WildReportAttachment {
    /// 读取待上传附件的本地文件数据。
    func uploadData() throws -> Data {
        guard let localFileURL else { throw OSSUploadError.emptyFile }
        return try Data(contentsOf: localFileURL)
    }
}

extension WildReportAttachmentKind {
    /// 提交举报接口的文件类型：1 图片，2 视频。
    var reportFileType: Int {
        switch self {
        case .image, .payment: return 1
        case .video: return 2
        }
    }

    /// 本地附件默认扩展名。
    var defaultFileExtension: String {
        switch self {
        case .image, .payment: return "jpg"
        case .video: return "mp4"
        }
    }
}

/// 举报处理状态。
enum WildReportStatus: String, Hashable, CaseIterable {
    case pending = "待处理"
    case processing = "处理中"
    case processed = "已处理"
    case rejected = "驳回"

    var displayColor: UIColor {
        switch self {
        case .pending: return AppColor.warning
        case .processing: return AppColor.primary
        case .processed: return AppColor.success
        case .rejected: return AppColor.danger
        }
    }

    var backgroundColor: UIColor {
        switch self {
        case .pending: return AppColor.warningBackground
        case .processing: return AppColor.infoBackground
        case .processed: return AppColor.successBackground
        case .rejected: return AppColor.dangerBackground
        }
    }
}

extension WildReportStatus {
    /// 后端举报处理状态码：0 待处理、1 处理中、2 已处理、3 驳回。
    init(handleStatus: Int, fallbackText: String = "") {
        switch handleStatus {
        case 0:
            self = .pending
        case 1:
            self = .processing
        case 2:
            self = .processed
        case 3:
            self = .rejected
        default:
            switch fallbackText {
            case WildReportStatus.processing.rawValue:
                self = .processing
            case WildReportStatus.processed.rawValue:
                self = .processed
            case WildReportStatus.rejected.rawValue:
                self = .rejected
            default:
                self = .pending
            }
        }
    }
}

/// 我的举报列表筛选条件。
enum WildReportListFilter: String, CaseIterable, Hashable {
    case all = "全部"
    case pending = "待处理"
    case processing = "处理中"
    case processed = "已处理"
}

extension WildReportListFilter {
    /// 后端列表筛选状态码：全部不传，0 待处理、1 处理中、2 已处理。
    var handleStatusValue: Int? {
        switch self {
        case .all: return nil
        case .pending: return 0
        case .processing: return 1
        case .processed: return 2
        }
    }
}

/// 我的举报列表接口请求参数。
struct WildReportListRequest: Equatable {
    let scenicId: Int?
    let handleStatus: Int?
    let page: Int
    let pageSize: Int

    init(
        scenicId: Int?,
        handleStatus: Int?,
        page: Int = 1,
        pageSize: Int = 10
    ) {
        self.scenicId = scenicId
        self.handleStatus = handleStatus
        self.page = page
        self.pageSize = pageSize
    }

    var queryItems: [URLQueryItem] {
        var items: [URLQueryItem] = []
        if let scenicId, scenicId > 0 {
            items.append(URLQueryItem(name: "scenic_id", value: String(scenicId)))
        }
        if let handleStatus {
            items.append(URLQueryItem(name: "handle_status", value: String(handleStatus)))
        }
        items.append(URLQueryItem(name: "page", value: String(max(1, page))))
        items.append(URLQueryItem(name: "page_size", value: String(min(max(1, pageSize), 50))))
        return items
    }
}

/// 我的举报列表接口响应体。
struct WildReportListResponse: Decodable, Equatable {
    let list: [WildReportListItem]
    let total: Int
    let page: Int
    let pageSize: Int

    enum CodingKeys: String, CodingKey {
        case list
        case total
        case page
        case pageSize = "page_size"
    }

    init(list: [WildReportListItem], total: Int, page: Int, pageSize: Int) {
        self.list = list
        self.total = total
        self.page = page
        self.pageSize = pageSize
    }
}

/// 举报微信小程序分享配置，由后端按具体举报记录生成。
struct WildReportMiniProgramShareData: Decodable, Equatable, Hashable {
    let userName: String
    let path: String
    let title: String
    let description: String
    let imagePath: String
    let webpageURL: String
    let withShareTicket: Bool
    let miniProgramTypeValue: Int
    let sceneValue: Int
    let disableForward: Bool

    enum CodingKeys: String, CodingKey {
        case userName
        case userNameSnake = "user_name"
        case path
        case title
        case description
        case imagePath
        case imagePathSnake = "image_path"
        case webpageURL = "webpageUrl"
        case webpageURLSnake = "webpage_url"
        case withShareTicket
        case withShareTicketSnake = "with_share_ticket"
        case miniProgramTypeValue = "miniprogramType"
        case miniProgramTypeValueSnake = "mini_program_type"
        case sceneValue = "scene"
        case disableForward
        case disableForwardSnake = "disable_forward"
    }

    init(
        userName: String = "",
        path: String = "",
        title: String = "",
        description: String = "",
        imagePath: String = "",
        webpageURL: String = "",
        withShareTicket: Bool = false,
        miniProgramTypeValue: Int = WeChatMiniProgramType.release.rawValue,
        sceneValue: Int = WeChatShareScene.session.rawValue,
        disableForward: Bool = false
    ) {
        self.userName = userName
        self.path = path
        self.title = title
        self.description = description
        self.imagePath = imagePath
        self.webpageURL = webpageURL
        self.withShareTicket = withShareTicket
        self.miniProgramTypeValue = miniProgramTypeValue
        self.sceneValue = sceneValue
        self.disableForward = disableForward
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        userName = try container.decodeString(primary: .userName, fallback: .userNameSnake)
        path = try container.decodeIfPresent(String.self, forKey: .path) ?? ""
        title = try container.decodeIfPresent(String.self, forKey: .title) ?? ""
        description = try container.decodeIfPresent(String.self, forKey: .description) ?? ""
        imagePath = try container.decodeString(primary: .imagePath, fallback: .imagePathSnake)
        webpageURL = try container.decodeString(primary: .webpageURL, fallback: .webpageURLSnake)
        withShareTicket = try container.decodeBool(primary: .withShareTicket, fallback: .withShareTicketSnake)
        miniProgramTypeValue = try container.decodeInt(
            primary: .miniProgramTypeValue,
            fallback: .miniProgramTypeValueSnake,
            defaultValue: WeChatMiniProgramType.release.rawValue
        )
        sceneValue = try container.decodeInt(
            primary: .sceneValue,
            fallback: nil,
            defaultValue: WeChatShareScene.session.rawValue
        )
        disableForward = try container.decodeBool(primary: .disableForward, fallback: .disableForwardSnake)
    }

    var miniProgramType: WeChatMiniProgramType {
        WeChatMiniProgramType(rawValue: miniProgramTypeValue) ?? .release
    }

    var scene: WeChatShareScene {
        WeChatShareScene(rawValue: sceneValue) ?? .session
    }

    var payload: WeChatMiniProgramSharePayload {
        WeChatMiniProgramSharePayload(
            title: title,
            description: description,
            webpageURL: webpageURL,
            userName: userName,
            path: path.nonEmpty,
            miniProgramType: miniProgramType,
            withShareTicket: withShareTicket,
            disableForward: disableForward
        )
    }
}

private extension KeyedDecodingContainer where Key == WildReportMiniProgramShareData.CodingKeys {
    func decodeString(primary: Key, fallback: Key?) throws -> String {
        if let value = try decodeIfPresent(String.self, forKey: primary) {
            return value
        }
        if let fallback, let value = try decodeIfPresent(String.self, forKey: fallback) {
            return value
        }
        return ""
    }

    func decodeBool(primary: Key, fallback: Key?) throws -> Bool {
        if let value = try decodeIfPresent(Bool.self, forKey: primary) {
            return value
        }
        if let fallback, let value = try decodeIfPresent(Bool.self, forKey: fallback) {
            return value
        }
        return false
    }

    func decodeInt(primary: Key, fallback: Key?, defaultValue: Int) throws -> Int {
        if let value = try decodeIfPresent(Int.self, forKey: primary) {
            return value
        }
        if let fallback, let value = try decodeIfPresent(Int.self, forKey: fallback) {
            return value
        }
        return defaultValue
    }
}

/// 我的举报列表接口单条举报数据。
struct WildReportListItem: Decodable, Equatable, Hashable {
    let id: Int
    let complaintNo: String
    let title: String
    let reportType: Int
    let reportTypeText: String
    let desc: String
    let scenicId: Int
    let scenicName: String
    let locationName: String
    let latitude: String
    let longitude: String
    let handleStatus: Int
    let handleStatusText: String
    let createdAt: String

    enum CodingKeys: String, CodingKey {
        case id
        case complaintNo = "complaint_no"
        case title
        case reportType = "report_type"
        case reportTypeText = "report_type_text"
        case desc
        case scenicId = "scenic_id"
        case scenicName = "scenic_name"
        case locationName = "location_name"
        case latitude
        case longitude
        case handleStatus = "handle_status"
        case handleStatusText = "handle_status_text"
        case createdAt = "created_at"
    }

    init(
        id: Int,
        complaintNo: String,
        title: String,
        reportType: Int,
        reportTypeText: String,
        desc: String,
        scenicId: Int,
        scenicName: String,
        locationName: String,
        latitude: String,
        longitude: String,
        handleStatus: Int,
        handleStatusText: String,
        createdAt: String
    ) {
        self.id = id
        self.complaintNo = complaintNo
        self.title = title
        self.reportType = reportType
        self.reportTypeText = reportTypeText
        self.desc = desc
        self.scenicId = scenicId
        self.scenicName = scenicName
        self.locationName = locationName
        self.latitude = latitude
        self.longitude = longitude
        self.handleStatus = handleStatus
        self.handleStatusText = handleStatusText
        self.createdAt = createdAt
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0
        complaintNo = try container.decodeIfPresent(String.self, forKey: .complaintNo) ?? ""
        title = try container.decodeIfPresent(String.self, forKey: .title) ?? ""
        reportType = try container.decodeIfPresent(Int.self, forKey: .reportType) ?? 0
        reportTypeText = try container.decodeIfPresent(String.self, forKey: .reportTypeText) ?? ""
        desc = try container.decodeIfPresent(String.self, forKey: .desc) ?? ""
        scenicId = try container.decodeIfPresent(Int.self, forKey: .scenicId) ?? 0
        scenicName = try container.decodeIfPresent(String.self, forKey: .scenicName) ?? ""
        locationName = try container.decodeIfPresent(String.self, forKey: .locationName) ?? ""
        latitude = try container.decodeIfPresent(String.self, forKey: .latitude) ?? ""
        longitude = try container.decodeIfPresent(String.self, forKey: .longitude) ?? ""
        handleStatus = try container.decodeIfPresent(Int.self, forKey: .handleStatus) ?? 0
        handleStatusText = try container.decodeIfPresent(String.self, forKey: .handleStatusText) ?? ""
        createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) ?? ""
    }

    /// 转换为当前 UIKit 列表和详情页复用的举报记录实体。
    func toRecord() -> WildReportRecord {
        let scenic = scenicName.trimmingCharacters(in: .whitespacesAndNewlines)
        let location = locationName.trimmingCharacters(in: .whitespacesAndNewlines)
        let displayLocation: String
        if !scenic.isEmpty, !location.isEmpty, scenic != location {
            displayLocation = "\(scenic) · \(location)"
        } else if !location.isEmpty {
            displayLocation = location
        } else {
            displayLocation = scenic
        }

        return WildReportRecord(
            serverID: id > 0 ? id : nil,
            id: complaintNo,
            title: title.isEmpty ? "\(reportTypeText)举报" : title,
            reportType: WildReportType(value: reportType, label: reportTypeText.isEmpty ? "其他" : reportTypeText),
            status: WildReportStatus(handleStatus: handleStatus, fallbackText: handleStatusText),
            location: displayLocation,
            submitTime: createdAt,
            description: desc,
            wechatID: "",
            phoneNumber: "",
            imageCount: 0,
            videoCount: 0,
            handlerInfo: nil
        )
    }
}

/// 举报详情接口返回的证据统计。
struct WildReportEvidenceSummary: Decodable, Equatable, Hashable {
    let imageCount: Int
    let videoCount: Int

    enum CodingKeys: String, CodingKey {
        case imageCount = "image_count"
        case videoCount = "video_count"
    }

    init(imageCount: Int = 0, videoCount: Int = 0) {
        self.imageCount = imageCount
        self.videoCount = videoCount
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        imageCount = try container.decodeIfPresent(Int.self, forKey: .imageCount) ?? 0
        videoCount = try container.decodeIfPresent(Int.self, forKey: .videoCount) ?? 0
    }
}

/// 举报详情接口返回的单个附件。
struct WildReportDetailEvidence: Decodable, Equatable, Hashable {
    let fileType: Int
    let fileTypeText: String
    let fileURL: String
    let fileName: String
    let evidenceKind: Int
    let evidenceKindText: String

    enum CodingKeys: String, CodingKey {
        case fileType = "file_type"
        case fileTypeText = "file_type_text"
        case fileURL = "file_url"
        case fileName = "file_name"
        case evidenceKind = "evidence_kind"
        case evidenceKindText = "evidence_kind_text"
    }

    init(
        fileType: Int = 0,
        fileTypeText: String = "",
        fileURL: String = "",
        fileName: String = "",
        evidenceKind: Int = 0,
        evidenceKindText: String = ""
    ) {
        self.fileType = fileType
        self.fileTypeText = fileTypeText
        self.fileURL = fileURL
        self.fileName = fileName
        self.evidenceKind = evidenceKind
        self.evidenceKindText = evidenceKindText
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        fileType = try container.decodeIfPresent(Int.self, forKey: .fileType) ?? 0
        fileTypeText = try container.decodeIfPresent(String.self, forKey: .fileTypeText) ?? ""
        fileURL = try container.decodeIfPresent(String.self, forKey: .fileURL) ?? ""
        fileName = try container.decodeIfPresent(String.self, forKey: .fileName) ?? ""
        evidenceKind = try container.decodeIfPresent(Int.self, forKey: .evidenceKind) ?? 0
        evidenceKindText = try container.decodeIfPresent(String.self, forKey: .evidenceKindText) ?? ""
    }

    var isVideo: Bool { fileType == 2 }
    var isImage: Bool { fileType == 1 || fileType == 5 }
}

/// 举报详情接口返回的处理人信息。
struct WildReportDetailHandler: Decodable, Equatable, Hashable {
    let id: Int
    let name: String
    let phone: String
    let startedAt: String
    let finishedAt: String
    let remark: String

    enum CodingKeys: String, CodingKey {
        case id
        case name
        case phone
        case startedAt = "started_at"
        case finishedAt = "finished_at"
        case remark
    }

    init(id: Int = 0, name: String = "", phone: String = "", startedAt: String = "", finishedAt: String = "", remark: String = "") {
        self.id = id
        self.name = name
        self.phone = phone
        self.startedAt = startedAt
        self.finishedAt = finishedAt
        self.remark = remark
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0
        name = try container.decodeIfPresent(String.self, forKey: .name) ?? ""
        phone = try container.decodeIfPresent(String.self, forKey: .phone) ?? ""
        startedAt = try container.decodeIfPresent(String.self, forKey: .startedAt) ?? ""
        finishedAt = try container.decodeIfPresent(String.self, forKey: .finishedAt) ?? ""
        remark = try container.decodeIfPresent(String.self, forKey: .remark) ?? ""
    }
}

/// 举报详情接口返回的补充内容或举报内容时间轴条目。
struct WildReportDetailContent: Decodable, Equatable, Hashable {
    let type: String
    let id: Int
    let desc: String
    let time: String
    let timeLabel: String
    let evidences: [WildReportDetailEvidence]

    enum CodingKeys: String, CodingKey {
        case type
        case id
        case desc
        case time
        case timeLabel = "time_label"
        case evidences
    }

    init(
        type: String = "",
        id: Int = 0,
        desc: String = "",
        time: String = "",
        timeLabel: String = "",
        evidences: [WildReportDetailEvidence] = []
    ) {
        self.type = type
        self.id = id
        self.desc = desc
        self.time = time
        self.timeLabel = timeLabel
        self.evidences = evidences
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        type = try container.decodeIfPresent(String.self, forKey: .type) ?? ""
        id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0
        desc = try container.decodeIfPresent(String.self, forKey: .desc) ?? ""
        time = try container.decodeIfPresent(String.self, forKey: .time) ?? ""
        timeLabel = try container.decodeIfPresent(String.self, forKey: .timeLabel) ?? ""
        evidences = try container.decodeIfPresent([WildReportDetailEvidence].self, forKey: .evidences) ?? []
    }

    var imageCount: Int { evidences.filter(\.isImage).count }
    var videoCount: Int { evidences.filter(\.isVideo).count }
}

/// 举报详情接口返回的处理日志时间轴。
struct WildReportDetailTimelineItem: Decodable, Equatable, Hashable {
    let time: String
    let content: String

    enum CodingKeys: String, CodingKey {
        case time
        case content
    }

    init(time: String = "", content: String = "") {
        self.time = time
        self.content = content
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        time = try container.decodeIfPresent(String.self, forKey: .time) ?? ""
        content = try container.decodeIfPresent(String.self, forKey: .content) ?? ""
    }
}

/// 举报详情接口业务数据。
struct WildReportDetailData: Decodable, Equatable, Hashable {
    let id: Int
    let complaintNo: String
    let title: String
    let reportType: Int
    let reportTypeText: String
    let desc: String
    let supplements: [WildReportDetailContent]
    let reportContents: [WildReportDetailContent]
    let scenicId: Int
    let scenicName: String
    let storeUserId: Int
    let contactPhone: String
    let locationName: String
    let locationAddress: String
    let latitude: String
    let longitude: String
    let handleStatus: Int
    let handleStatusText: String
    let handleRemark: String
    let handledAt: String
    let handler: WildReportDetailHandler?
    let handlerName: String
    let handlerPhone: String
    let evidenceSummary: WildReportEvidenceSummary?
    let evidences: [WildReportDetailEvidence]
    let handleEvidences: [WildReportDetailEvidence]
    let paymentScreenshots: [WildReportDetailEvidence]
    let timeline: [WildReportDetailTimelineItem]
    let createdAt: String

    enum CodingKeys: String, CodingKey {
        case id
        case complaintNo = "complaint_no"
        case title
        case reportType = "report_type"
        case reportTypeText = "report_type_text"
        case desc
        case supplements
        case reportContents = "report_contents"
        case scenicId = "scenic_id"
        case scenicName = "scenic_name"
        case storeUserId = "store_user_id"
        case contactPhone = "contact_phone"
        case locationName = "location_name"
        case locationAddress = "location_address"
        case latitude
        case longitude
        case handleStatus = "handle_status"
        case handleStatusText = "handle_status_text"
        case handleRemark = "handle_remark"
        case handledAt = "handled_at"
        case handler
        case handlerName = "handler_name"
        case handlerPhone = "handler_phone"
        case evidenceSummary = "evidence_summary"
        case evidences
        case handleEvidences = "handle_evidences"
        case paymentScreenshots = "payment_screenshots"
        case timeline
        case createdAt = "created_at"
    }

    init(
        id: Int = 0,
        complaintNo: String = "",
        title: String = "",
        reportType: Int = 0,
        reportTypeText: String = "",
        desc: String = "",
        supplements: [WildReportDetailContent] = [],
        reportContents: [WildReportDetailContent] = [],
        scenicId: Int = 0,
        scenicName: String = "",
        storeUserId: Int = 0,
        contactPhone: String = "",
        locationName: String = "",
        locationAddress: String = "",
        latitude: String = "",
        longitude: String = "",
        handleStatus: Int = 0,
        handleStatusText: String = "",
        handleRemark: String = "",
        handledAt: String = "",
        handler: WildReportDetailHandler? = nil,
        handlerName: String = "",
        handlerPhone: String = "",
        evidenceSummary: WildReportEvidenceSummary? = nil,
        evidences: [WildReportDetailEvidence] = [],
        handleEvidences: [WildReportDetailEvidence] = [],
        paymentScreenshots: [WildReportDetailEvidence] = [],
        timeline: [WildReportDetailTimelineItem] = [],
        createdAt: String = ""
    ) {
        self.id = id
        self.complaintNo = complaintNo
        self.title = title
        self.reportType = reportType
        self.reportTypeText = reportTypeText
        self.desc = desc
        self.supplements = supplements
        self.reportContents = reportContents
        self.scenicId = scenicId
        self.scenicName = scenicName
        self.storeUserId = storeUserId
        self.contactPhone = contactPhone
        self.locationName = locationName
        self.locationAddress = locationAddress
        self.latitude = latitude
        self.longitude = longitude
        self.handleStatus = handleStatus
        self.handleStatusText = handleStatusText
        self.handleRemark = handleRemark
        self.handledAt = handledAt
        self.handler = handler
        self.handlerName = handlerName
        self.handlerPhone = handlerPhone
        self.evidenceSummary = evidenceSummary
        self.evidences = evidences
        self.handleEvidences = handleEvidences
        self.paymentScreenshots = paymentScreenshots
        self.timeline = timeline
        self.createdAt = createdAt
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0
        complaintNo = try container.decodeIfPresent(String.self, forKey: .complaintNo) ?? ""
        title = try container.decodeIfPresent(String.self, forKey: .title) ?? ""
        reportType = try container.decodeIfPresent(Int.self, forKey: .reportType) ?? 0
        reportTypeText = try container.decodeIfPresent(String.self, forKey: .reportTypeText) ?? ""
        desc = try container.decodeIfPresent(String.self, forKey: .desc) ?? ""
        supplements = try container.decodeIfPresent([WildReportDetailContent].self, forKey: .supplements) ?? []
        reportContents = try container.decodeIfPresent([WildReportDetailContent].self, forKey: .reportContents) ?? []
        scenicId = try container.decodeIfPresent(Int.self, forKey: .scenicId) ?? 0
        scenicName = try container.decodeIfPresent(String.self, forKey: .scenicName) ?? ""
        storeUserId = try container.decodeIfPresent(Int.self, forKey: .storeUserId) ?? 0
        contactPhone = try container.decodeIfPresent(String.self, forKey: .contactPhone) ?? ""
        locationName = try container.decodeIfPresent(String.self, forKey: .locationName) ?? ""
        locationAddress = try container.decodeIfPresent(String.self, forKey: .locationAddress) ?? ""
        latitude = try container.decodeIfPresent(String.self, forKey: .latitude) ?? ""
        longitude = try container.decodeIfPresent(String.self, forKey: .longitude) ?? ""
        handleStatus = try container.decodeIfPresent(Int.self, forKey: .handleStatus) ?? 0
        handleStatusText = try container.decodeIfPresent(String.self, forKey: .handleStatusText) ?? ""
        handleRemark = try container.decodeIfPresent(String.self, forKey: .handleRemark) ?? ""
        handledAt = try container.decodeIfPresent(String.self, forKey: .handledAt) ?? ""
        handler = try container.decodeIfPresent(WildReportDetailHandler.self, forKey: .handler)
        handlerName = try container.decodeIfPresent(String.self, forKey: .handlerName) ?? ""
        handlerPhone = try container.decodeIfPresent(String.self, forKey: .handlerPhone) ?? ""
        evidenceSummary = try container.decodeIfPresent(WildReportEvidenceSummary.self, forKey: .evidenceSummary)
        evidences = try container.decodeIfPresent([WildReportDetailEvidence].self, forKey: .evidences) ?? []
        handleEvidences = try container.decodeIfPresent([WildReportDetailEvidence].self, forKey: .handleEvidences) ?? []
        paymentScreenshots = try container.decodeIfPresent([WildReportDetailEvidence].self, forKey: .paymentScreenshots) ?? []
        timeline = try container.decodeIfPresent([WildReportDetailTimelineItem].self, forKey: .timeline) ?? []
        createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) ?? ""
    }

    var displayLocation: String {
        let scenic = scenicName.trimmingCharacters(in: .whitespacesAndNewlines)
        let detail = (locationAddress.nonEmpty ?? locationName).trimmingCharacters(in: .whitespacesAndNewlines)
        if !scenic.isEmpty, !detail.isEmpty, scenic != detail {
            return "\(scenic) · \(detail)"
        }
        return detail.nonEmpty ?? scenic
    }

    var displayStatusText: String {
        handleStatusText.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty
            ?? WildReportStatus(handleStatus: handleStatus).rawValue
    }

    var imageCount: Int {
        evidenceSummary?.imageCount ?? evidences.filter(\.isImage).count
    }

    var videoCount: Int {
        evidenceSummary?.videoCount ?? evidences.filter(\.isVideo).count
    }

    var supplementRecords: [WildReportSupplementaryEvidence] {
        let contents = supplements.isEmpty
            ? reportContents.filter { $0.type == "supplement" }
            : supplements
        return contents.map { content in
            WildReportSupplementaryEvidence(
                id: content.id > 0 ? String(content.id) : content.time,
                submitTime: content.time,
                text: content.desc,
                evidences: content.evidences
            )
        }
    }

    var detailHandlerInfo: WildReportHandlerInfo? {
        let resolvedName = handler?.name.nonEmpty ?? handlerName.nonEmpty ?? ""
        let resolvedPhone = handler?.phone.nonEmpty ?? handlerPhone.nonEmpty ?? ""
        let processingAt = handler?.startedAt.nonEmpty ?? ""
        let processedAt = handler?.finishedAt.nonEmpty ?? handledAt.nonEmpty ?? ""
        let processOpinion = handler?.remark.nonEmpty ?? handleRemark.nonEmpty ?? ""
        let media = handleEvidences.enumerated().compactMap { index, evidence -> WildReportCompletionMediaItem? in
            guard let url = evidence.fileURL.nonEmpty else { return nil }
            return WildReportCompletionMediaItem(
                id: "\(index)-\(url)",
                kind: evidence.isVideo ? .video : .image,
                coverURL: url,
                duration: evidence.isVideo ? evidence.fileTypeText.nonEmpty : nil
            )
        }
        guard !resolvedName.isEmpty
            || !resolvedPhone.isEmpty
            || !processingAt.isEmpty
            || !processedAt.isEmpty
            || !processOpinion.isEmpty
            || !media.isEmpty
        else {
            return nil
        }
        return WildReportHandlerInfo(
            handlerName: resolvedName,
            handlerPhone: resolvedPhone,
            processingAt: processingAt,
            processedAt: processedAt,
            processOpinion: processOpinion,
            completionMedia: media
        )
    }

    /// 转换为当前 UIKit 列表和详情页复用的举报记录实体。
    func toRecord(fallback: WildReportRecord? = nil) -> WildReportRecord {
        WildReportRecord(
            serverID: id > 0 ? id : fallback?.serverID,
            id: complaintNo.nonEmpty ?? fallback?.id ?? "",
            title: title.nonEmpty ?? fallback?.title ?? "摄影师举报",
            reportType: WildReportType(
                value: reportType > 0 ? reportType : fallback?.reportType.value ?? 4,
                label: reportTypeText.nonEmpty ?? fallback?.reportType.label ?? "其他"
            ),
            status: WildReportStatus(handleStatus: handleStatus, fallbackText: handleStatusText),
            location: displayLocation.nonEmpty ?? fallback?.location ?? "",
            submitTime: createdAt.nonEmpty ?? fallback?.submitTime ?? "",
            description: desc.nonEmpty ?? fallback?.description ?? "",
            wechatID: fallback?.wechatID ?? "",
            phoneNumber: contactPhone.nonEmpty ?? fallback?.phoneNumber ?? "",
            imageCount: imageCount,
            videoCount: videoCount,
            handlerInfo: detailHandlerInfo ?? fallback?.handlerInfo
        )
    }
}

/// 补充证据记录实体。
struct WildReportSupplementaryEvidence: Hashable {
    let id: String
    let submitTime: String
    let text: String
    let evidences: [WildReportDetailEvidence]

    var imageCount: Int { evidences.filter(\.isImage).count }
    var videoCount: Int { evidences.filter(\.isVideo).count }
    var hasMedia: Bool { imageCount > 0 || videoCount > 0 }

    var summaryText: String {
        var parts: [String] = []
        if !text.isEmpty { parts.append(text) }
        if imageCount > 0 { parts.append("\(imageCount)张图片") }
        if videoCount > 0 { parts.append("\(videoCount)段视频") }
        return parts.isEmpty ? "补充证据" : parts.joined(separator: " · ")
    }
}

/// 举报补充证据展示文案格式化工具。
enum WildReportSupplementDisplayFormatter {
    /// 返回按当前列表展示顺序从 1 起算的补充次数文案。
    static func sequenceTitle(forDisplayIndex index: Int) -> String {
        "第\(max(0, index) + 1)次补充"
    }
}

/// 举报位置解析工具。
enum WildReportLocationParser {
    /// 将「景区 · 详细地址」拆成两段展示文案。
    static func split(_ location: String) -> (scenic: String, detail: String) {
        let parts = location
            .components(separatedBy: " · ")
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty }
        if parts.count >= 2 {
            return (parts[0], parts[1])
        }
        return (location, location)
    }
}

/// 附近风险点实体。
struct WildReportRiskPoint: Hashable {
    let id: Int
    let title: String
    let location: String
    let distance: String
    let riskLevel: String
    let evidenceCount: Int
    let latestTime: String
    let description: String
}

/// 举报记录实体。
struct WildReportRecord: Hashable {
    var serverID: Int? = nil
    let id: String
    let title: String
    let reportType: WildReportType
    let status: WildReportStatus
    let location: String
    let submitTime: String
    let description: String
    let wechatID: String
    let phoneNumber: String
    let imageCount: Int
    let videoCount: Int
    let handlerInfo: WildReportHandlerInfo?

    var contactDisplayText: String {
        switch (wechatID.isEmpty, phoneNumber.isEmpty) {
        case (true, true): return "未填写"
        case (false, false): return "\(wechatID) / \(phoneNumber)"
        case (false, true): return wechatID
        case (true, false): return phoneNumber
        }
    }
}

/// 处理完成后的媒体项。
struct WildReportCompletionMediaItem: Hashable {
    enum Kind: Hashable {
        case image
        case video
    }

    let id: String
    let kind: Kind
    let coverURL: String
    let duration: String?
}

/// 举报处理人信息实体。
struct WildReportHandlerInfo: Hashable {
    let handlerName: String
    let handlerPhone: String
    let processingAt: String
    let processedAt: String
    let processOpinion: String
    let completionMedia: [WildReportCompletionMediaItem]

    var maskedHandlerPhone: String {
        guard handlerPhone.count == 11, handlerPhone.allSatisfy(\.isNumber) else { return handlerPhone }
        return "\(handlerPhone.prefix(3))****\(handlerPhone.suffix(4))"
    }

    var completionMediaSummary: String {
        let imageCount = completionMedia.filter { $0.kind == .image }.count
        let videoCount = completionMedia.filter { $0.kind == .video }.count
        switch (imageCount, videoCount) {
        case (0, 0): return "0项"
        case (_, 0): return "\(imageCount)张"
        case (0, _): return "\(videoCount)段"
        default: return "\(imageCount)张 \(videoCount)段"
        }
    }
}

/// 举报进度节点状态。
enum WildReportProgressStepState: Hashable {
    case completed
    case current
    case pending
}

/// 举报进度时间轴节点。
struct WildReportProgressStep: Hashable {
    let id: String
    let title: String
    let timeText: String
    let state: WildReportProgressStepState
}

/// 举报人材料预览项。
enum WildReportReporterMediaItem: Hashable {
    case image(index: Int, url: String?, fileName: String?, fileTypeText: String?)
    case video(index: Int, url: String?, fileName: String?, fileTypeText: String?)

    var id: String {
        switch self {
        case .image(let index, let url, _, _): return "reporter-image-\(index)-\(url ?? "")"
        case .video(let index, let url, _, _): return "reporter-video-\(index)-\(url ?? "")"
        }
    }

    var fileURL: String? {
        switch self {
        case .image(_, let url, _, _), .video(_, let url, _, _):
            return url
        }
    }

    var fileName: String? {
        switch self {
        case .image(_, _, let fileName, _), .video(_, _, let fileName, _):
            return fileName
        }
    }

    var fileTypeText: String? {
        switch self {
        case .image(_, _, _, let fileTypeText), .video(_, _, _, let fileTypeText):
            return fileTypeText
        }
    }
}

/// 举报提交结果，用于成功页展示。
struct WildReportSubmitResult: Hashable {
    let record: WildReportRecord
    let imageCount: Int
    let videoCount: Int
}

/// 风险地图标记类型。
enum WildReportMapMarkerKind: Hashable {
    case selfLocation
    case wildPhotographer
    case processingClue
    case peerPhotographer

    var color: UIColor {
        switch self {
        case .selfLocation: return AppColor.primary
        case .wildPhotographer: return AppColor.danger
        case .processingClue: return AppColor.warning
        case .peerPhotographer: return AppColor.success
        }
    }

    var title: String {
        switch self {
        case .selfLocation: return "我的位置"
        case .wildPhotographer: return "疑似打野"
        case .processingClue: return "处理中线索"
        case .peerPhotographer: return "内部摄影师"
        }
    }
}

/// 雷达地图标记颜色类型。
enum WildReportRadarMarkerType: String, Hashable {
    case blue
    case green
    case red
    case yellow
}

/// 风险地图接口标记类型，兼容后端新增枚举。
enum WildReportRiskMarkerType: Hashable {
    case blue
    case green
    case red
    case yellow
    case unknown(String)

    init(rawValue: String) {
        switch rawValue.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
        case "blue", "self", "self_location":
            self = .blue
        case "green":
            self = .green
        case "red":
            self = .red
        case "yellow":
            self = .yellow
        default:
            self = .unknown(rawValue)
        }
    }

    var rawValue: String {
        switch self {
        case .blue: return "blue"
        case .green: return "green"
        case .red: return "red"
        case .yellow: return "yellow"
        case .unknown(let value): return value
        }
    }

    var markerKind: WildReportMapMarkerKind {
        switch self {
        case .blue:
            return .selfLocation
        case .green:
            return .peerPhotographer
        case .yellow:
            return .processingClue
        case .red, .unknown:
            return .wildPhotographer
        }
    }

    var radarType: WildReportRadarMarkerType {
        switch self {
        case .blue:
            return .blue
        case .green:
            return .green
        case .yellow:
            return .yellow
        case .red, .unknown:
            return .red
        }
    }
}

/// 风险地图后端状态字段，兼容数字和字符串两种形态。
struct WildReportDecodedHandleStatus: Decodable, Equatable, Hashable {
    let intValue: Int?
    let textValue: String?

    init(intValue: Int? = nil, textValue: String? = nil) {
        self.intValue = intValue
        self.textValue = textValue
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let value = try? container.decode(Int.self) {
            intValue = value
            textValue = nil
        } else if let value = try? container.decode(String.self) {
            intValue = Int(value)
            textValue = value
        } else {
            intValue = nil
            textValue = nil
        }
    }

    var displayText: String {
        if let text = textValue?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty {
            switch text {
            case "0": return WildReportStatus.pending.rawValue
            case "1": return WildReportStatus.processing.rawValue
            case "2": return WildReportStatus.processed.rawValue
            case "3": return WildReportStatus.rejected.rawValue
            default: return text
            }
        }
        return WildReportStatus(handleStatus: intValue ?? 0).rawValue
    }
}

/// 附近风险地图接口返回体。
struct WildReportRiskMapResponse: Decodable, Equatable {
    let scenicId: Int
    let scenicName: String
    let legend: [WildReportRiskLegendItem]
    let greenMarkerTip: String
    let markers: [WildReportRiskMapMarkerDTO]

    enum CodingKeys: String, CodingKey {
        case scenicId = "scenic_id"
        case scenicName = "scenic_name"
        case legend
        case greenMarkerTip = "green_marker_tip"
        case markers
    }

    init(
        scenicId: Int,
        scenicName: String,
        legend: [WildReportRiskLegendItem] = [],
        greenMarkerTip: String = "",
        markers: [WildReportRiskMapMarkerDTO] = []
    ) {
        self.scenicId = scenicId
        self.scenicName = scenicName
        self.legend = legend
        self.greenMarkerTip = greenMarkerTip
        self.markers = markers
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        scenicId = try container.decodeIfPresent(Int.self, forKey: .scenicId) ?? 0
        scenicName = try container.decodeIfPresent(String.self, forKey: .scenicName) ?? ""
        legend = try container.decodeIfPresent([WildReportRiskLegendItem].self, forKey: .legend) ?? []
        greenMarkerTip = try container.decodeIfPresent(String.self, forKey: .greenMarkerTip) ?? ""
        markers = try container.decodeIfPresent([WildReportRiskMapMarkerDTO].self, forKey: .markers) ?? []
    }
}

/// 风险地图图例项，兼容后端不同字段命名。
struct WildReportRiskLegendItem: Decodable, Equatable, Hashable {
    let markerType: String?
    let title: String?
    let color: String?

    enum CodingKeys: String, CodingKey {
        case markerType = "marker_type"
        case type
        case title
        case text
        case name
        case color
    }

    init(markerType: String? = nil, title: String? = nil, color: String? = nil) {
        self.markerType = markerType
        self.title = title
        self.color = color
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        markerType = try container.decodeIfPresent(String.self, forKey: .markerType)
            ?? (try container.decodeIfPresent(String.self, forKey: .type))
        title = try container.decodeIfPresent(String.self, forKey: .title)
            ?? (try container.decodeIfPresent(String.self, forKey: .text))
            ?? (try container.decodeIfPresent(String.self, forKey: .name))
        color = try container.decodeIfPresent(String.self, forKey: .color)
    }
}

/// 风险地图列表接口的标记摘要。
struct WildReportRiskMapMarkerDTO: Decodable, Equatable, Hashable {
    let markerType: String
    let id: Int
    let latitude: String
    let longitude: String
    let locationName: String
    let title: String
    let reportTypeText: String
    let handleStatus: WildReportDecodedHandleStatus?
    let handleStatusText: String?
    let distanceM: Double
    let createdAt: String
    let tag: String?
    let name: String?
    let avatar: String?
    let storeName: String?
    let online: Bool?
    let greenMarkerTip: String?
    let activityText: String?

    enum CodingKeys: String, CodingKey {
        case markerType = "marker_type"
        case id
        case latitude
        case longitude
        case locationName = "location_name"
        case title
        case reportTypeText = "report_type_text"
        case handleStatus = "handle_status"
        case handleStatusText = "handle_status_text"
        case distanceM = "distance_m"
        case createdAt = "created_at"
        case tag
        case name
        case avatar
        case storeName = "store_name"
        case online
        case greenMarkerTip = "green_marker_tip"
        case activityText = "activity_text"
    }

    init(
        markerType: String,
        id: Int,
        latitude: String,
        longitude: String,
        locationName: String,
        title: String,
        reportTypeText: String = "",
        handleStatus: WildReportDecodedHandleStatus? = nil,
        handleStatusText: String? = nil,
        distanceM: Double = 0,
        createdAt: String = "",
        tag: String? = nil,
        name: String? = nil,
        avatar: String? = nil,
        storeName: String? = nil,
        online: Bool? = nil,
        greenMarkerTip: String? = nil,
        activityText: String? = nil
    ) {
        self.markerType = markerType
        self.id = id
        self.latitude = latitude
        self.longitude = longitude
        self.locationName = locationName
        self.title = title
        self.reportTypeText = reportTypeText
        self.handleStatus = handleStatus
        self.handleStatusText = handleStatusText
        self.distanceM = distanceM
        self.createdAt = createdAt
        self.tag = tag
        self.name = name
        self.avatar = avatar
        self.storeName = storeName
        self.online = online
        self.greenMarkerTip = greenMarkerTip
        self.activityText = activityText
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        markerType = try container.decodeIfPresent(String.self, forKey: .markerType) ?? ""
        id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0
        latitude = try container.decodeIfPresent(String.self, forKey: .latitude) ?? ""
        longitude = try container.decodeIfPresent(String.self, forKey: .longitude) ?? ""
        locationName = try container.decodeIfPresent(String.self, forKey: .locationName) ?? ""
        title = try container.decodeIfPresent(String.self, forKey: .title) ?? ""
        reportTypeText = try container.decodeIfPresent(String.self, forKey: .reportTypeText) ?? ""
        handleStatus = try container.decodeIfPresent(WildReportDecodedHandleStatus.self, forKey: .handleStatus)
        handleStatusText = try container.decodeIfPresent(String.self, forKey: .handleStatusText)
        distanceM = try container.decodeIfPresent(Double.self, forKey: .distanceM) ?? 0
        createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) ?? ""
        tag = try container.decodeIfPresent(String.self, forKey: .tag)
        name = try container.decodeIfPresent(String.self, forKey: .name)
        avatar = try container.decodeIfPresent(String.self, forKey: .avatar)
        storeName = try container.decodeIfPresent(String.self, forKey: .storeName)
        online = try container.decodeIfPresent(Bool.self, forKey: .online)
        greenMarkerTip = try container.decodeIfPresent(String.self, forKey: .greenMarkerTip)
        activityText = try container.decodeIfPresent(String.self, forKey: .activityText)
    }
}

/// 风险地图标记详情接口返回体。
struct WildReportMarkerDetailResponse: Decodable, Equatable, Hashable {
    let markerType: String
    let id: Int
    let clueId: String
    let title: String
    let statusTag: String
    let tag: String
    let name: String
    let avatar: String
    let storeName: String
    let online: Bool
    let locationName: String
    let latitude: String
    let longitude: String
    let distanceM: Double
    let greenMarkerTip: String
    let activityText: String
    let reportTypeText: String
    let handleStatus: WildReportDecodedHandleStatus?
    let handleStatusText: String
    let desc: String
    let reporterName: String
    let reporterPhone: String
    let createdAt: String
    let reportContents: [WildReportDetailContent]
    let evidences: [WildReportDetailEvidence]

    enum CodingKeys: String, CodingKey {
        case markerType = "marker_type"
        case id
        case clueId = "clue_id"
        case title
        case statusTag = "status_tag"
        case tag
        case name
        case avatar
        case storeName = "store_name"
        case online
        case locationName = "location_name"
        case latitude
        case longitude
        case distanceM = "distance_m"
        case greenMarkerTip = "green_marker_tip"
        case activityText = "activity_text"
        case reportTypeText = "report_type_text"
        case handleStatus = "handle_status"
        case handleStatusText = "handle_status_text"
        case desc
        case reporterName = "reporter_name"
        case reporterPhone = "reporter_phone"
        case contactPhone = "contact_phone"
        case createdAt = "created_at"
        case reportContents = "report_contents"
        case evidences
    }

    init(
        markerType: String,
        id: Int,
        clueId: String = "",
        title: String = "",
        statusTag: String = "",
        tag: String = "",
        name: String = "",
        avatar: String = "",
        storeName: String = "",
        online: Bool = false,
        locationName: String = "",
        latitude: String = "",
        longitude: String = "",
        distanceM: Double = 0,
        greenMarkerTip: String = "",
        activityText: String = "",
        reportTypeText: String = "",
        handleStatus: WildReportDecodedHandleStatus? = nil,
        handleStatusText: String = "",
        desc: String = "",
        reporterName: String = "",
        reporterPhone: String = "",
        createdAt: String = "",
        reportContents: [WildReportDetailContent] = [],
        evidences: [WildReportDetailEvidence] = []
    ) {
        self.markerType = markerType
        self.id = id
        self.clueId = clueId
        self.title = title
        self.statusTag = statusTag
        self.tag = tag
        self.name = name
        self.avatar = avatar
        self.storeName = storeName
        self.online = online
        self.locationName = locationName
        self.latitude = latitude
        self.longitude = longitude
        self.distanceM = distanceM
        self.greenMarkerTip = greenMarkerTip
        self.activityText = activityText
        self.reportTypeText = reportTypeText
        self.handleStatus = handleStatus
        self.handleStatusText = handleStatusText
        self.desc = desc
        self.reporterName = reporterName
        self.reporterPhone = reporterPhone
        self.createdAt = createdAt
        self.reportContents = reportContents
        self.evidences = evidences
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        markerType = try container.decodeIfPresent(String.self, forKey: .markerType) ?? ""
        id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0
        clueId = try container.decodeIfPresent(String.self, forKey: .clueId) ?? ""
        title = try container.decodeIfPresent(String.self, forKey: .title) ?? ""
        statusTag = try container.decodeIfPresent(String.self, forKey: .statusTag) ?? ""
        tag = try container.decodeIfPresent(String.self, forKey: .tag) ?? ""
        name = try container.decodeIfPresent(String.self, forKey: .name) ?? ""
        avatar = try container.decodeIfPresent(String.self, forKey: .avatar) ?? ""
        storeName = try container.decodeIfPresent(String.self, forKey: .storeName) ?? ""
        online = try container.decodeIfPresent(Bool.self, forKey: .online) ?? false
        locationName = try container.decodeIfPresent(String.self, forKey: .locationName) ?? ""
        latitude = try container.decodeIfPresent(String.self, forKey: .latitude) ?? ""
        longitude = try container.decodeIfPresent(String.self, forKey: .longitude) ?? ""
        distanceM = try container.decodeIfPresent(Double.self, forKey: .distanceM) ?? 0
        greenMarkerTip = try container.decodeIfPresent(String.self, forKey: .greenMarkerTip) ?? ""
        activityText = try container.decodeIfPresent(String.self, forKey: .activityText) ?? ""
        reportTypeText = try container.decodeIfPresent(String.self, forKey: .reportTypeText) ?? ""
        handleStatus = try container.decodeIfPresent(WildReportDecodedHandleStatus.self, forKey: .handleStatus)
        handleStatusText = try container.decodeIfPresent(String.self, forKey: .handleStatusText) ?? ""
        desc = try container.decodeIfPresent(String.self, forKey: .desc) ?? ""
        reporterName = try container.decodeIfPresent(String.self, forKey: .reporterName) ?? ""
        reporterPhone = try container.decodeIfPresent(String.self, forKey: .reporterPhone)
            ?? (try container.decodeIfPresent(String.self, forKey: .contactPhone))
            ?? ""
        createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) ?? ""
        reportContents = try container.decodeIfPresent([WildReportDetailContent].self, forKey: .reportContents) ?? []
        evidences = try container.decodeIfPresent([WildReportDetailEvidence].self, forKey: .evidences) ?? []
    }
}

/// 雷达线索举报内容条目。
struct WildReportRadarReportContent: Hashable {
    let time: String
    let content: String
}

/// 雷达线索处理记录。
struct WildReportRadarProcessRecord: Hashable {
    let handlerName: String
    let phone: String
    let maskedPhone: String
    let statusText: String
    let processResult: String
    let processedAt: String?
    let processingAt: String?
}

/// 雷达线索处理时间轴节点。
struct WildReportRadarProcessingTimelineItem: Hashable {
    let title: String
    let time: String
    let status: String
}

/// 雷达地图活动线索详情。
struct WildReportRadarActiveClue: Hashable {
    let id: String
    let type: WildReportRadarMarkerType
    let displayTitle: String
    let statusText: String
    let actionText: String
    let nearestAbnormalText: String?
    let distance: String?
    let direction: String?
    let updatedAt: String?
    let name: String?
    let storeName: String?
    let avatar: String?
    let summary: String?
    let detail: String?
    let distanceText: String?
    let reporterName: String?
    let reporterPhone: String?
    let maskedReporterPhone: String?
    let reportContents: [WildReportRadarReportContent]
    let images: [String]
    let processingStarted: Bool
    let completed: Bool
    let record: WildReportRadarProcessRecord?
    let processingTimeline: [WildReportRadarProcessingTimelineItem]
    let completionPhoto: String?
}

/// 风险地图标记详情。
enum WildReportMapMarkerDetail: Hashable {
    case activeClue(WildReportRadarActiveClue)
}

/// 风险地图标记实体。
struct WildReportMapMarker: Hashable {
    let id: String
    let title: String
    let coordinate: CLLocationCoordinate2D
    let kind: WildReportMapMarkerKind
    let detail: WildReportMapMarkerDetail

    static func == (lhs: WildReportMapMarker, rhs: WildReportMapMarker) -> Bool {
        lhs.id == rhs.id
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

extension CLLocationCoordinate2D: @retroactive Equatable {
    public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool {
        lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
    }
}

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