//
//  AssetsModels.swift
//  suixinkan
//
//  Created by Codex on 2026/6/23.
//

import Foundation

/// 云盘文件实体，表示相册云盘中的文件或文件夹。
struct CloudDriveFile: Decodable, Identifiable, Hashable {
    let id: Int
    let parentFolderId: Int
    let fileUrl: String
    let coverUrl: String
    let updatedAt: String
    let name: String
    let createdAt: String
    let childNum: Int
    let type: Int
    let fileSize: Int64

    /// 判断当前云盘项是否为文件夹。
    var isFolder: Bool { type == 99 }

    /// 判断当前云盘项是否为视频文件。
    var isVideo: Bool {
        type == 1 || Self.hasVideoExtension(name) || Self.hasVideoExtension(fileUrl)
    }

    /// 判断当前云盘项是否为图片文件。
    var isImage: Bool {
        type == 2 || Self.hasImageExtension(name) || Self.hasImageExtension(fileUrl)
    }

    /// 返回可用于预览的远程地址。
    var previewURLString: String {
        coverUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? fileUrl : coverUrl
    }

    /// 根据文件扩展名判断是否是常见视频格式。
    private static func hasVideoExtension(_ value: String) -> Bool {
        ["mp4", "mov", "m4v", "avi"].contains(pathExtension(for: value))
    }

    /// 根据文件扩展名判断是否是常见图片格式。
    private static func hasImageExtension(_ value: String) -> Bool {
        ["png", "jpg", "jpeg", "heic", "heif", "webp"].contains(pathExtension(for: value))
    }

    /// 提取 URL 或本地路径中的扩展名。
    private static func pathExtension(for value: String) -> String {
        URL(string: value)?.pathExtension.lowercased() ?? URL(fileURLWithPath: value).pathExtension.lowercased()
    }

    /// 云盘文件字段映射，兼容后端下划线命名。
    enum CodingKeys: String, CodingKey {
        case id
        case parentFolderId = "parent_folder_id"
        case fileUrl = "file_url"
        case coverUrl = "cover_url"
        case updatedAt = "updated_at"
        case name
        case createdAt = "created_at"
        case childNum = "child_num"
        case type
        case fileSize = "file_size"
    }

    /// 构造本地路径根节点，供云盘面包屑维护当前目录。
    init(id: Int, name: String) {
        self.id = id
        parentFolderId = 0
        fileUrl = ""
        coverUrl = ""
        updatedAt = ""
        self.name = name
        createdAt = ""
        childNum = 0
        type = 99
        fileSize = 0
    }

    /// 构造完整云盘文件实体，主要用于测试和本地状态拼装。
    init(
        id: Int,
        parentFolderId: Int,
        fileUrl: String,
        coverUrl: String = "",
        updatedAt: String = "",
        name: String,
        createdAt: String = "",
        childNum: Int = 0,
        type: Int,
        fileSize: Int64 = 0
    ) {
        self.id = id
        self.parentFolderId = parentFolderId
        self.fileUrl = fileUrl
        self.coverUrl = coverUrl
        self.updatedAt = updatedAt
        self.name = name
        self.createdAt = createdAt
        self.childNum = childNum
        self.type = type
        self.fileSize = fileSize
    }

    /// 宽松解码云盘文件字段。
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decodeLossyInt(forKey: .id) ?? 0
        parentFolderId = try container.decodeLossyInt(forKey: .parentFolderId) ?? 0
        fileUrl = try container.decodeLossyString(forKey: .fileUrl)
        coverUrl = try container.decodeLossyString(forKey: .coverUrl)
        updatedAt = try container.decodeLossyString(forKey: .updatedAt)
        name = try container.decodeLossyString(forKey: .name)
        createdAt = try container.decodeLossyString(forKey: .createdAt)
        childNum = try container.decodeLossyInt(forKey: .childNum) ?? 0
        type = try container.decodeLossyInt(forKey: .type) ?? 0
        fileSize = try container.decodeLossyInt64(forKey: .fileSize) ?? 0
    }
}

/// 云盘文件类型筛选实体，表示云盘页面的筛选范围。
enum CloudDriveFilter: Int, CaseIterable, Identifiable {
    case all = 0
    case video = 1
    case image = 2
    case folder = 99

    var id: Int { rawValue }

    /// 返回筛选项展示文案。
    var title: String {
        switch self {
        case .all:
            "全部"
        case .video:
            "视频"
        case .image:
            "图片"
        case .folder:
            "文件夹"
        }
    }
}

/// 云盘排序实体，表示列表排序规则。
enum CloudDriveSort: Int, CaseIterable, Identifiable {
    case updatedDesc = 2
    case createdDesc = 1

    var id: Int { rawValue }

    /// 返回排序项展示文案。
    var title: String {
        switch self {
        case .updatedDesc:
            "最近更新"
        case .createdDesc:
            "创建时间"
        }
    }
}

/// 云盘新建文件夹请求实体。
struct CloudFolderCreateRequest: Encodable, Equatable {
    let parentFolderId: Int
    let name: String

    /// 请求字段映射，兼容后端下划线命名。
    enum CodingKeys: String, CodingKey {
        case parentFolderId = "parent_folder_id"
        case name
    }
}

/// 云盘文件上传登记请求实体。
struct CloudFileUploadRequest: Encodable, Equatable {
    let parentFolderId: Int
    let fileUrl: String
    let fileName: String

    /// 请求字段映射，兼容后端下划线命名。
    enum CodingKeys: String, CodingKey {
        case parentFolderId = "parent_folder_id"
        case fileUrl = "file_url"
        case fileName = "file_name"
    }
}

/// 云盘批量操作项实体，表示移动或删除时传给后端的文件/文件夹。
struct CloudFileActionItem: Encodable, Equatable {
    let id: Int
    let type: Int
}

/// 云盘删除请求实体。
struct CloudFileDeleteRequest: Encodable, Equatable {
    let list: [CloudFileActionItem]
}

/// 云盘移动请求实体。
struct CloudFileMoveRequest: Encodable, Equatable {
    let targetFolderId: Int
    let list: [CloudFileActionItem]

    /// 请求字段映射，兼容后端下划线命名。
    enum CodingKeys: String, CodingKey {
        case targetFolderId = "target_folder_id"
        case list
    }
}

/// 云盘文件夹重命名请求实体。
struct CloudFolderModifyRequest: Encodable, Equatable {
    let id: Int
    let name: String
}

/// 云盘文件重命名请求实体。
struct CloudFileModifyRequest: Encodable, Equatable {
    let id: Int
    let fileName: String

    /// 请求字段映射，兼容后端下划线命名。
    enum CodingKeys: String, CodingKey {
        case id
        case fileName = "file_name"
    }
}

/// 云盘上传权限响应实体，表示当前账号是否允许继续上传。
struct CheckCloudUploadResponse: Decodable, Equatable {
    let canUpload: Bool
    let reason: String

    /// 响应字段映射，兼容后端下划线命名。
    enum CodingKeys: String, CodingKey {
        case canUpload = "can_upload"
        case reason
    }

    /// 宽松解码上传权限字段。
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        canUpload = try container.decodeLossyBool(forKey: .canUpload) ?? true
        reason = try container.decodeLossyString(forKey: .reason)
    }
}

/// 云盘本地待上传文件实体，表示 PhotosPicker 读取后的内存数据。
struct CloudLocalUploadFile: Identifiable, Equatable {
    let id = UUID()
    let data: Data
    let fileName: String
    let fileType: Int
}

/// 云盘传输方向实体，区分上传和下载记录。
enum CloudTransferDirection: String, Codable, Equatable {
    case upload
    case download
}

/// 云盘传输状态实体，表示一次上传或下载的阶段。
enum CloudTransferStatus: String, Codable, Equatable {
    case running
    case success
    case failed
}

/// 云盘传输记录实体，仅保存在本次 App 会话内。
struct CloudTransferItem: Identifiable, Equatable {
    let id: UUID
    let fileName: String
    let direction: CloudTransferDirection
    var progress: Int
    var status: CloudTransferStatus
    var message: String
    let createdAt: Date

    /// 创建一条新的传输记录。
    init(
        id: UUID = UUID(),
        fileName: String,
        direction: CloudTransferDirection,
        progress: Int = 0,
        status: CloudTransferStatus = .running,
        message: String = "",
        createdAt: Date = Date()
    ) {
        self.id = id
        self.fileName = fileName
        self.direction = direction
        self.progress = progress
        self.status = status
        self.message = message
        self.createdAt = createdAt
    }
}

/// 素材库类型实体，表示素材或样片两类媒体资产。
enum MediaLibraryKind: Int, CaseIterable, Identifiable {
    case material = 1
    case sample = 2

    var id: Int { rawValue }

    /// 返回页面展示标题。
    var title: String {
        switch self {
        case .material:
            "素材管理"
        case .sample:
            "样片管理"
        }
    }

    /// 返回上传页面标题。
    var uploadTitle: String {
        switch self {
        case .material:
            "上传素材"
        case .sample:
            "上传样片"
        }
    }

    /// 返回详情页面标题。
    var detailTitle: String {
        switch self {
        case .material:
            "素材详情"
        case .sample:
            "样片详情"
        }
    }

    /// 返回名称输入占位文案。
    var namePlaceholder: String {
        switch self {
        case .material:
            "素材名称"
        case .sample:
            "样片名称"
        }
    }

    /// 返回搜索输入占位文案。
    var searchPlaceholder: String {
        switch self {
        case .material:
            "搜索素材名称"
        case .sample:
            "搜索样片名称"
        }
    }

    /// 返回空列表标题。
    var emptyTitle: String {
        switch self {
        case .material:
            "暂无素材"
        case .sample:
            "暂无样片"
        }
    }
}

/// 素材审核状态筛选实体。
enum MediaLibraryAuditFilter: Int, CaseIterable, Identifiable {
    case all = -1
    case pending = 0
    case approved = 1
    case rejected = 2

    var id: Int { rawValue }

    /// 返回筛选项展示文案。
    var title: String {
        switch self {
        case .all:
            "全部"
        case .pending:
            "待审核"
        case .approved:
            "已通过"
        case .rejected:
            "未通过"
        }
    }
}

/// 素材上下架状态实体。
enum MediaListingStatus: Int, Codable, Equatable {
    case offline = 0
    case online = 1

    /// 返回切换后的目标状态。
    var toggled: MediaListingStatus {
        self == .online ? .offline : .online
    }

    /// 返回展示文案。
    var title: String {
        self == .online ? "已上架" : "未上架"
    }
}

/// 素材库列表响应实体，包含分页列表和订单统计信息。
struct MediaLibraryListResponse: Decodable, Equatable {
    let total: Int
    let list: [MediaLibraryItem]
    let order: MediaLibraryOrderInfo?

    /// 响应字段映射。
    enum CodingKeys: String, CodingKey {
        case total
        case list
        case order
    }

    /// 宽松解码列表响应字段。
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        total = try container.decodeLossyInt(forKey: .total) ?? 0
        list = (try? container.decodeIfPresent([MediaLibraryItem].self, forKey: .list)) ?? []
        order = try? container.decodeIfPresent(MediaLibraryOrderInfo.self, forKey: .order)
    }
}

/// 素材订单统计实体，表示素材库顶部统计卡片。
struct MediaLibraryOrderInfo: Decodable, Equatable {
    let totalNum: Int
    let avgOrderAmount: Double
    let refundTotal: Double
    let avgChange: Double

    /// 响应字段映射，兼容后端下划线命名。
    enum CodingKeys: String, CodingKey {
        case totalNum = "total_num"
        case avgOrderAmount = "avg_order_amount"
        case refundTotal = "refund_total"
        case avgChange = "avg_change"
    }

    /// 宽松解码统计字段。
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        totalNum = try container.decodeLossyInt(forKey: .totalNum) ?? 0
        avgOrderAmount = try container.decodeLossyDouble(forKey: .avgOrderAmount) ?? 0
        refundTotal = try container.decodeLossyDouble(forKey: .refundTotal) ?? 0
        avgChange = try container.decodeLossyDouble(forKey: .avgChange) ?? 0
    }
}

/// 素材库列表项实体，表示素材列表中的单个素材。
struct MediaLibraryItem: Decodable, Identifiable, Hashable {
    let id: Int
    let name: String
    let coverUrl: String
    let createdAt: String
    let status: Int
    let downloadCount: Int
    let likesCount: Int
    let collectCount: Int
    let shareCount: Int
    let scenicSpotName: String
    let projectName: String
    let listingStatus: Int

    /// 判断素材是否通过审核。
    var isApproved: Bool { status == 1 }

    /// 当前素材上下架状态。
    var listing: MediaListingStatus {
        MediaListingStatus(rawValue: listingStatus) ?? .offline
    }

    /// 字段映射，兼容后端下划线命名。
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case coverUrl = "cover_url"
        case createdAt = "created_at"
        case status
        case downloadCount = "download_count"
        case likesCount = "likes_count"
        case collectCount = "collect_count"
        case shareCount = "share_count"
        case scenicSpotName = "scenic_spot_name"
        case projectName = "project_name"
        case listingStatus = "listing_status"
    }

    /// 宽松解码素材列表项。
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decodeLossyInt(forKey: .id) ?? 0
        name = try container.decodeLossyString(forKey: .name)
        coverUrl = try container.decodeLossyString(forKey: .coverUrl)
        createdAt = try container.decodeLossyString(forKey: .createdAt)
        status = try container.decodeLossyInt(forKey: .status) ?? 0
        downloadCount = try container.decodeLossyInt(forKey: .downloadCount) ?? 0
        likesCount = try container.decodeLossyInt(forKey: .likesCount) ?? 0
        collectCount = try container.decodeLossyInt(forKey: .collectCount) ?? 0
        shareCount = try container.decodeLossyInt(forKey: .shareCount) ?? 0
        scenicSpotName = try container.decodeLossyString(forKey: .scenicSpotName)
        projectName = try container.decodeLossyString(forKey: .projectName)
        listingStatus = try container.decodeLossyInt(forKey: .listingStatus) ?? 0
    }
}

/// 素材详情实体，表示素材详情页展示的数据。
struct MediaLibraryDetail: Decodable, Equatable {
    let id: Int
    let name: String
    let coverUrl: String
    let description: String
    let status: Int
    let listingStatus: Int
    let likesCount: Int
    let downloadCount: Int
    let collectCount: Int
    let shareCount: Int
    let uploaderName: String
    let createdAt: String
    let mediaList: [MediaLibraryMediaItem]
    let scenicName: String
    let projectName: String

    /// 判断素材是否通过审核。
    var isApproved: Bool { status == 1 }

    /// 当前素材上下架状态。
    var listing: MediaListingStatus {
        MediaListingStatus(rawValue: listingStatus) ?? .offline
    }

    /// 字段映射，兼容后端下划线命名。
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case coverUrl = "cover_url"
        case description
        case status
        case listingStatus = "listing_status"
        case likesCount = "likes_count"
        case downloadCount = "download_count"
        case collectCount = "collect_count"
        case shareCount = "share_count"
        case uploaderName = "uploader_name"
        case createdAt = "created_at"
        case mediaList = "media_list"
        case scenicName = "scenic_name"
        case projectName = "project_name"
    }

    /// 宽松解码素材详情字段。
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decodeLossyInt(forKey: .id) ?? 0
        name = try container.decodeLossyString(forKey: .name)
        coverUrl = try container.decodeLossyString(forKey: .coverUrl)
        description = try container.decodeLossyString(forKey: .description)
        status = try container.decodeLossyInt(forKey: .status) ?? 0
        listingStatus = try container.decodeLossyInt(forKey: .listingStatus) ?? 0
        likesCount = try container.decodeLossyInt(forKey: .likesCount) ?? 0
        downloadCount = try container.decodeLossyInt(forKey: .downloadCount) ?? 0
        collectCount = try container.decodeLossyInt(forKey: .collectCount) ?? 0
        shareCount = try container.decodeLossyInt(forKey: .shareCount) ?? 0
        uploaderName = try container.decodeLossyString(forKey: .uploaderName)
        createdAt = try container.decodeLossyString(forKey: .createdAt)
        mediaList = (try? container.decodeIfPresent([MediaLibraryMediaItem].self, forKey: .mediaList)) ?? []
        scenicName = try container.decodeLossyString(forKey: .scenicName)
        projectName = try container.decodeLossyString(forKey: .projectName)
    }
}

/// 素材媒体文件实体，表示详情页中的图片或视频资源。
struct MediaLibraryMediaItem: Decodable, Identifiable, Hashable {
    let id: Int
    let originalName: String
    let ossUrl: String
    let thumbnailUrl: String
    let type: Int
    let size: Int64
    let showUrl: String

    /// 当前媒体是否为视频。
    var isVideo: Bool {
        type == 1 || ["mp4", "mov", "m4v", "avi"].contains(URL(string: ossUrl)?.pathExtension.lowercased() ?? "")
    }

    /// 字段映射，兼容后端下划线命名。
    enum CodingKeys: String, CodingKey {
        case id
        case originalName = "original_name"
        case ossUrl = "oss_url"
        case thumbnailUrl = "thumbnail_url"
        case type
        case size
        case showUrl = "show_url"
    }

    /// 宽松解码媒体文件字段。
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decodeLossyInt(forKey: .id) ?? 0
        originalName = try container.decodeLossyString(forKey: .originalName)
        ossUrl = try container.decodeLossyString(forKey: .ossUrl)
        thumbnailUrl = try container.decodeLossyString(forKey: .thumbnailUrl)
        type = try container.decodeLossyInt(forKey: .type) ?? 0
        size = try container.decodeLossyInt64(forKey: .size) ?? 0
        showUrl = try container.decodeLossyString(forKey: .showUrl)
    }
}

/// 素材上传文件尺寸实体。
struct MediaAlbumFileSize: Codable, Equatable {
    let width: Int
    let height: Int
}

/// 素材上传文件请求实体，表示上传到 OSS 后提交给服务端的媒体文件。
struct MediaAlbumUploadItem: Codable, Equatable {
    let originalName: String
    let ossUrl: String
    let size: Int64
    let fileWidthSize: MediaAlbumFileSize

    /// 请求字段映射，兼容后端下划线命名。
    enum CodingKeys: String, CodingKey {
        case originalName = "original_name"
        case ossUrl = "oss_url"
        case size
        case fileWidthSize = "file_width_size"
    }
}

/// 素材上传请求实体。
struct MediaAlbumUploadRequest: Encodable, Equatable {
    let name: String
    let type: Int
    let mediaType: Int
    let mediaList: [MediaAlbumUploadItem]
    let coverUrl: String
    let coverSize: MediaAlbumFileSize
    let scenicSpotId: Int
    let description: String
    let materialTag: String
    let projectId: Int

    /// 请求字段映射，兼容后端下划线命名。
    enum CodingKeys: String, CodingKey {
        case name
        case type
        case mediaType = "media_type"
        case mediaList = "media_list"
        case coverUrl = "cover_url"
        case coverSize = "cover_size"
        case scenicSpotId = "scenic_spot_id"
        case description
        case materialTag = "material_tag"
        case projectId = "project_id"
    }
}

/// 素材编辑请求实体。
struct MediaAlbumEditRequest: Encodable, Equatable {
    let id: Int
    let name: String
    let type: Int
    let mediaType: Int
    let mediaList: [MediaAlbumUploadItem]
    let coverUrl: String
    let coverSize: MediaAlbumFileSize
    let scenicSpotId: Int
    let description: String
    let materialTag: String

    /// 请求字段映射，兼容后端下划线命名。
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case type
        case mediaType = "media_type"
        case mediaList = "media_list"
        case coverUrl = "cover_url"
        case coverSize = "cover_size"
        case scenicSpotId = "scenic_spot_id"
        case description
        case materialTag = "material_tag"
    }
}

/// 素材操作请求实体，用于素材或样片上下架。
struct MediaAlbumOperationRequest: Encodable, Equatable {
    let id: Int
    let type: Int
    let listingStatus: Int

    /// 请求字段映射，兼容后端下划线命名。
    enum CodingKeys: String, CodingKey {
        case id
        case type
        case listingStatus = "listing_status"
    }
}

/// 素材删除请求实体，用于删除素材或样片。
struct MediaAlbumDeleteRequest: Encodable, Equatable {
    let id: Int
    let type: Int
}

/// 素材标签新增请求实体。
struct MediaAlbumAddTagRequest: Encodable, Equatable {
    let name: String
}

/// 素材本地待上传文件实体，表示封面或媒体文件的内存数据。
struct MediaLocalUploadFile: Identifiable, Equatable {
    let id = UUID()
    let data: Data
    let fileName: String
    let fileType: Int
    let width: Int
    let height: Int
}

/// 相册文件夹实体，表示相册管理列表中的一个相册。
struct AlbumFolderItem: Decodable, Identifiable, Hashable {
    let id: Int
    let name: String
    let coverFileId: Int
    let countVideo: Int
    let countImage: Int
    let coverFile: AlbumFileItem?
    let createTime: String
    let remark: String

    /// 相册封面地址，优先使用封面文件的预览地址。
    var coverURLString: String {
        coverFile?.previewURLString ?? ""
    }

    /// 相册内文件总数。
    var totalCount: Int {
        countVideo + countImage
    }

    /// 字段映射，兼容旧接口下划线命名。
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case coverFileId = "cover_file_id"
        case countVideo = "count_video"
        case countImage = "count_image"
        case coverFile = "cover"
        case createTime = "created_at"
        case remark
    }

    /// 宽松解码相册文件夹字段。
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decodeLossyInt(forKey: .id) ?? 0
        name = try container.decodeLossyString(forKey: .name)
        coverFileId = try container.decodeLossyInt(forKey: .coverFileId) ?? 0
        countVideo = try container.decodeLossyInt(forKey: .countVideo) ?? 0
        countImage = try container.decodeLossyInt(forKey: .countImage) ?? 0
        coverFile = try? container.decodeIfPresent(AlbumFileItem.self, forKey: .coverFile)
        createTime = try container.decodeLossyString(forKey: .createTime)
        remark = try container.decodeLossyString(forKey: .remark)
    }
}

/// 相册文件实体，表示相册详情中的图片或视频。
struct AlbumFileItem: Decodable, Identifiable, Hashable {
    let id: Int
    let fileName: String
    let fileType: Int
    let fileUrl: String
    let coverUrl: String
    let fileSize: Int64
    let fileSizeHuman: String
    let remark: String

    /// 判断当前文件是否为视频。
    var isVideo: Bool {
        fileType == 1 || Self.hasVideoExtension(fileName) || Self.hasVideoExtension(fileUrl)
    }

    /// 判断当前文件是否为图片。
    var isImage: Bool {
        fileType == 2 || Self.hasImageExtension(fileName) || Self.hasImageExtension(fileUrl)
    }

    /// 返回图片或视频封面的预览地址。
    var previewURLString: String {
        coverUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? fileUrl : coverUrl
    }

    /// 返回文件大小展示文案。
    var displayFileSize: String {
        if !fileSizeHuman.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            return fileSizeHuman
        }
        return fileSize.assetsFormattedFileSize
    }

    /// 根据文件扩展名判断是否为视频。
    private static func hasVideoExtension(_ value: String) -> Bool {
        ["mp4", "mov", "m4v", "avi"].contains(pathExtension(for: value))
    }

    /// 根据文件扩展名判断是否为图片。
    private static func hasImageExtension(_ value: String) -> Bool {
        ["png", "jpg", "jpeg", "heic", "heif", "webp"].contains(pathExtension(for: value))
    }

    /// 提取 URL 或文件路径中的扩展名。
    private static func pathExtension(for value: String) -> String {
        URL(string: value)?.pathExtension.lowercased() ?? URL(fileURLWithPath: value).pathExtension.lowercased()
    }

    /// 字段映射，兼容旧接口下划线命名。
    enum CodingKeys: String, CodingKey {
        case id
        case fileName = "file_name"
        case fileType = "file_type"
        case fileUrl = "file_url"
        case coverUrl = "cover_url"
        case fileSize = "file_size"
        case fileSizeHuman = "file_size_human"
        case remark
    }

    /// 宽松解码相册文件字段。
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decodeLossyInt(forKey: .id) ?? 0
        fileName = try container.decodeLossyString(forKey: .fileName)
        fileType = try container.decodeLossyInt(forKey: .fileType) ?? 0
        fileUrl = try container.decodeLossyString(forKey: .fileUrl)
        coverUrl = try container.decodeLossyString(forKey: .coverUrl)
        fileSize = try container.decodeLossyInt64(forKey: .fileSize) ?? 0
        fileSizeHuman = try container.decodeLossyString(forKey: .fileSizeHuman)
        remark = try container.decodeLossyString(forKey: .remark)
    }
}

/// 相册文件类型筛选实体，表示相册详情中的图片或视频 Tab。
enum AlbumFileTab: Int, CaseIterable, Identifiable {
    case image = 2
    case video = 1

    var id: Int { rawValue }

    /// 返回页面展示标题。
    var title: String {
        switch self {
        case .image:
            "图片"
        case .video:
            "视频"
        }
    }
}

/// 新建相册请求实体。
struct AlbumFolderAddRequest: Encodable, Equatable {
    let scenicId: String
    let name: String
    let remark: String
    let cloudFolderType: Int

    /// 请求字段映射，兼容旧接口下划线命名。
    enum CodingKeys: String, CodingKey {
        case scenicId = "scenic_id"
        case name
        case remark
        case cloudFolderType = "cloud_folder_type"
    }
}

/// 编辑相册请求实体，用于改名、备注和封面。
struct AlbumFolderEditRequest: Encodable, Equatable {
    let folderId: Int
    let coverFileId: Int?
    let name: String?
    let remark: String?

    /// 请求字段映射，兼容旧接口下划线命名。
    enum CodingKeys: String, CodingKey {
        case folderId = "id"
        case coverFileId = "cover_file_id"
        case name
        case remark
    }
}

/// 删除相册文件请求实体。
struct AlbumFileDeleteRequest: Encodable, Equatable {
    let idList: [Int]
    let folderId: Int

    /// 请求字段映射，兼容旧接口下划线命名。
    enum CodingKeys: String, CodingKey {
        case idList = "id_list"
        case folderId = "folder_id"
    }
}

/// 相册本地待上传文件实体，表示 PhotosPicker 读取后的图片或视频。
struct AlbumLocalUploadFile: Identifiable, Equatable {
    let id = UUID()
    let data: Data
    let fileName: String
    let fileType: Int
}

private extension Int64 {
    /// 返回资产模块通用文件大小展示文案。
    var assetsFormattedFileSize: String {
        let value = Double(self)
        if value >= 1_073_741_824 {
            return String(format: "%.1fGB", value / 1_073_741_824)
        }
        if value >= 1_048_576 {
            return String(format: "%.1fMB", value / 1_048_576)
        }
        if value >= 1024 {
            return String(format: "%.1fKB", value / 1024)
        }
        return "\(self)B"
    }
}

private extension KeyedDecodingContainer {
    /// 将 String、数字和 Bool 宽松解码为字符串。
    func decodeLossyString(forKey key: Key) throws -> String {
        if let value = try? decodeIfPresent(String.self, forKey: key) {
            return value
        }
        if let value = try? decodeIfPresent(Int.self, forKey: key) {
            return String(value)
        }
        if let value = try? decodeIfPresent(Double.self, forKey: key) {
            return String(value)
        }
        if let value = try? decodeIfPresent(Bool.self, forKey: key) {
            return value ? "1" : "0"
        }
        return ""
    }

    /// 将 String、Double 和 Int 宽松解码为 Int。
    func decodeLossyInt(forKey key: Key) throws -> Int? {
        if let value = try? decodeIfPresent(Int.self, forKey: key) {
            return value
        }
        if let value = try? decodeIfPresent(Double.self, forKey: key) {
            return Int(value)
        }
        if let value = try? decodeIfPresent(String.self, forKey: key) {
            return Int(value.trimmingCharacters(in: .whitespacesAndNewlines))
        }
        return nil
    }

    /// 将 String、Double 和 Int 宽松解码为 Int64。
    func decodeLossyInt64(forKey key: Key) throws -> Int64? {
        if let value = try? decodeIfPresent(Int64.self, forKey: key) {
            return value
        }
        if let value = try? decodeIfPresent(Int.self, forKey: key) {
            return Int64(value)
        }
        if let value = try? decodeIfPresent(Double.self, forKey: key) {
            return Int64(value)
        }
        if let value = try? decodeIfPresent(String.self, forKey: key) {
            return Int64(value.trimmingCharacters(in: .whitespacesAndNewlines))
        }
        return nil
    }

    /// 将 String、Int 和 Double 宽松解码为 Double。
    func decodeLossyDouble(forKey key: Key) throws -> Double? {
        if let value = try? decodeIfPresent(Double.self, forKey: key) {
            return value
        }
        if let value = try? decodeIfPresent(Int.self, forKey: key) {
            return Double(value)
        }
        if let value = try? decodeIfPresent(String.self, forKey: key) {
            return Double(value.trimmingCharacters(in: .whitespacesAndNewlines))
        }
        return nil
    }

    /// 将 String、Int 和 Bool 宽松解码为 Bool。
    func decodeLossyBool(forKey key: Key) throws -> Bool? {
        if let value = try? decodeIfPresent(Bool.self, forKey: key) {
            return value
        }
        if let value = try? decodeIfPresent(Int.self, forKey: key) {
            return value != 0
        }
        if let value = try? decodeIfPresent(String.self, forKey: key) {
            let text = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
            if ["1", "true", "yes"].contains(text) { return true }
            if ["0", "false", "no"].contains(text) { return false }
        }
        return nil
    }
}
