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

import Foundation
import Combine

/// 云盘传输记录仓库，保存本次 App 会话内的上传和下载进度。
@MainActor
final class CloudTransferStore: ObservableObject {
    static let shared = CloudTransferStore()

    @Published private(set) var records: [CloudTransferItem] = []

    /// 新增一条传输记录并返回记录 ID。
    @discardableResult
    func start(fileName: String, direction: CloudTransferDirection) -> UUID {
        let id = UUID()
        records.insert(CloudTransferItem(id: id, fileName: fileName, direction: direction), at: 0)
        return id
    }

    /// 更新指定传输记录的进度。
    func update(id: UUID, progress: Int, message: String = "") {
        guard let index = records.firstIndex(where: { $0.id == id }) else { return }
        records[index].progress = max(0, min(100, progress))
        if !message.isEmpty {
            records[index].message = message
        }
    }

    /// 标记指定传输记录成功。
    func succeed(id: UUID, message: String = "已完成") {
        guard let index = records.firstIndex(where: { $0.id == id }) else { return }
        records[index].progress = 100
        records[index].status = .success
        records[index].message = message
    }

    /// 标记指定传输记录失败。
    func fail(id: UUID, message: String) {
        guard let index = records.firstIndex(where: { $0.id == id }) else { return }
        records[index].status = .failed
        records[index].message = message
    }

    /// 清空当前会话传输记录。
    func clear() {
        records = []
    }
}

/// 云盘 ViewModel，负责文件列表、筛选、分页、文件操作和上传闭环。
@MainActor
final class CloudStorageViewModel: ObservableObject {
    @Published var path: [CloudDriveFile] = [CloudDriveFile(id: 0, name: "云盘")]
    @Published var files: [CloudDriveFile] = []
    @Published var total = 0
    @Published var page = 1
    @Published var searchText = ""
    @Published var selectedFilter: CloudDriveFilter = .all
    @Published var selectedSort: CloudDriveSort = .updatedDesc
    private var isLoading = false
    @Published var isLoadingMore = false
    @Published var isMutating = false
    @Published var errorMessage: String?

    private let pageSize = 20

    /// 当前目录 ID。
    var currentFolderId: Int {
        path.last?.id ?? 0
    }

    /// 判断当前目录是否还有下一页。
    var hasMore: Bool {
        files.count < total
    }

    /// 重新加载当前目录第一页。
    func reload(api: any AssetsServing) async {
        isLoading = true
        errorMessage = nil
        defer { isLoading = false }

        do {
            let payload = try await requestFiles(api: api, page: 1)
            page = 1
            files = payload.list
            total = payload.total
        } catch {
            files = []
            total = 0
            page = 1
            errorMessage = error.localizedDescription
        }
    }

    /// 加载当前目录下一页。
    func loadMore(api: any AssetsServing) async {
        guard hasMore, !isLoadingMore else { return }
        isLoadingMore = true
        defer { isLoadingMore = false }

        do {
            let nextPage = page + 1
            let payload = try await requestFiles(api: api, page: nextPage)
            page = nextPage
            total = payload.total
            files.append(contentsOf: payload.list)
        } catch {
            errorMessage = error.localizedDescription
        }
    }

    /// 进入指定文件夹并刷新列表。
    func enterFolder(_ folder: CloudDriveFile, api: any AssetsServing) async {
        guard folder.isFolder else { return }
        path.append(folder)
        await reload(api: api)
    }

    /// 回到指定面包屑目录并刷新列表。
    func popToFolder(at index: Int, api: any AssetsServing) async {
        guard path.indices.contains(index) else { return }
        path = Array(path.prefix(index + 1))
        await reload(api: api)
    }

    /// 创建文件夹并刷新当前目录。
    func createFolder(name: String, api: any AssetsServing) async -> Bool {
        let folderName = name.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !folderName.isEmpty else {
            errorMessage = "请输入文件夹名称"
            return false
        }
        return await mutate(api: api) {
            try await api.cloudFolderCreate(CloudFolderCreateRequest(parentFolderId: currentFolderId, name: folderName))
        }
    }

    /// 重命名文件或文件夹并刷新当前目录。
    func rename(_ item: CloudDriveFile, newName: String, api: any AssetsServing) async -> Bool {
        let fileName = newName.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !fileName.isEmpty else {
            errorMessage = "请输入名称"
            return false
        }
        return await mutate(api: api) {
            if item.isFolder {
                try await api.cloudFolderEdit(CloudFolderModifyRequest(id: item.id, name: fileName))
            } else {
                try await api.cloudFileEdit(CloudFileModifyRequest(id: item.id, fileName: fileName))
            }
        }
    }

    /// 删除文件或文件夹并刷新当前目录。
    func delete(_ item: CloudDriveFile, api: any AssetsServing) async -> Bool {
        await mutate(api: api) {
            let action = CloudFileActionItem(id: item.id, type: item.type)
            try await api.cloudFileDelete(CloudFileDeleteRequest(list: [action]))
        }
    }

    /// 移动文件或文件夹并刷新当前目录。
    func move(_ item: CloudDriveFile, targetFolderId: Int, api: any AssetsServing) async -> Bool {
        await mutate(api: api) {
            let action = CloudFileActionItem(id: item.id, type: item.type)
            try await api.cloudFileMove(CloudFileMoveRequest(targetFolderId: targetFolderId, list: [action]))
        }
    }

    /// 上传本地文件到 OSS 后写入云盘。
    func upload(
        localFiles: [CloudLocalUploadFile],
        scenicId: Int?,
        api: any AssetsServing,
        uploadService: any OSSUploadServing,
        transferStore: CloudTransferStore
    ) async -> Bool {
        guard let scenicId else {
            errorMessage = "缺少当前景区，无法上传"
            return false
        }
        guard !localFiles.isEmpty else { return true }

        isMutating = true
        defer { isMutating = false }

        do {
            let permission = try await api.checkCloudUploadPermission()
            guard permission.canUpload else {
                let reason = permission.reason.trimmingCharacters(in: .whitespacesAndNewlines)
                errorMessage = reason.isEmpty ? "当前账号暂无上传权限" : reason
                return false
            }

            for file in localFiles {
                let transferId = transferStore.start(fileName: file.fileName, direction: .upload)
                do {
                    let url = try await uploadService.uploadCloudFile(
                        data: file.data,
                        fileName: file.fileName,
                        fileType: file.fileType,
                        scenicId: scenicId
                    ) { progress in
                        Task { @MainActor in
                            transferStore.update(id: transferId, progress: progress)
                        }
                    }
                    try await api.cloudFileUpload(
                        CloudFileUploadRequest(parentFolderId: currentFolderId, fileUrl: url, fileName: file.fileName)
                    )
                    transferStore.succeed(id: transferId)
                } catch {
                    transferStore.fail(id: transferId, message: error.localizedDescription)
                    throw error
                }
            }
            await reload(api: api)
            return true
        } catch {
            errorMessage = error.localizedDescription
            return false
        }
    }

    /// 执行一次云盘变更操作并刷新当前目录。
    private func mutate(api: any AssetsServing, operation: () async throws -> Void) async -> Bool {
        isMutating = true
        defer { isMutating = false }
        do {
            try await operation()
            await reload(api: api)
            return true
        } catch {
            errorMessage = error.localizedDescription
            return false
        }
    }

    /// 组装并发送云盘文件列表请求。
    private func requestFiles(api: any AssetsServing, page: Int) async throws -> ListPayload<CloudDriveFile> {
        try await api.cloudFileList(
            parentFolderId: currentFolderId,
            name: searchText.trimmingCharacters(in: .whitespacesAndNewlines),
            type: selectedFilter.rawValue,
            orderBy: selectedSort.rawValue,
            page: page,
            pageSize: pageSize
        )
    }
}

/// 媒体库 ViewModel，负责素材或样片列表、筛选、分页、详情和上下架。
@MainActor
final class MediaLibraryViewModel: ObservableObject {
    let kind: MediaLibraryKind
    @Published var items: [MediaLibraryItem] = []
    @Published var selectedDetail: MediaLibraryDetail?
    @Published var orderInfo: MediaLibraryOrderInfo?
    @Published var total = 0
    @Published var page = 1
    @Published var keyword = ""
    @Published var selectedAuditFilter: MediaLibraryAuditFilter = .all
    private var isLoading = false
    @Published var isLoadingMore = false
    private var isLoadingDetail = false
    @Published var errorMessage: String?

    private let pageSize = 10

    /// 初始化媒体库 ViewModel。
    init(kind: MediaLibraryKind = .material) {
        self.kind = kind
    }

    /// 判断当前列表是否还有下一页。
    var hasMore: Bool {
        items.count < total
    }

    /// 重新加载媒体库列表第一页。
    func reload(api: any AssetsServing) async {
        isLoading = true
        errorMessage = nil
        defer { isLoading = false }

        do {
            let response = try await requestList(api: api, page: 1)
            page = 1
            items = response.list
            total = response.total
            orderInfo = response.order
        } catch {
            items = []
            total = 0
            orderInfo = nil
            page = 1
            errorMessage = error.localizedDescription
        }
    }

    /// 加载媒体库列表下一页。
    func loadMore(api: any AssetsServing) async {
        guard hasMore, !isLoadingMore else { return }
        isLoadingMore = true
        defer { isLoadingMore = false }

        do {
            let nextPage = page + 1
            let response = try await requestList(api: api, page: nextPage)
            page = nextPage
            total = response.total
            orderInfo = response.order ?? orderInfo
            items.append(contentsOf: response.list)
        } catch {
            errorMessage = error.localizedDescription
        }
    }

    /// 加载媒体库详情，失败时保留已有详情或摘要。
    func loadDetail(id: Int, api: any AssetsServing) async {
        isLoadingDetail = true
        errorMessage = nil
        defer { isLoadingDetail = false }

        do {
            selectedDetail = try await api.mediaAlbumDetail(id: id, kind: kind)
        } catch {
            errorMessage = error.localizedDescription
        }
    }

    /// 更新素材或样片上下架状态，审核未通过时禁止操作。
    func toggleListing(_ item: MediaLibraryItem, api: any AssetsServing) async -> Bool {
        guard item.isApproved else {
            errorMessage = "审核通过后才能上下架"
            return false
        }
        return await operate(api: api) {
            let target = item.listing.toggled
            try await api.mediaAlbumOperation(MediaAlbumOperationRequest(id: item.id, type: kind.rawValue, listingStatus: target.rawValue))
        }
    }

    /// 更新素材或样片详情页上下架状态，审核未通过时禁止操作。
    func toggleListing(detail: MediaLibraryDetail, api: any AssetsServing) async -> Bool {
        guard detail.isApproved else {
            errorMessage = "审核通过后才能上下架"
            return false
        }
        return await operate(api: api) {
            let target = detail.listing.toggled
            try await api.mediaAlbumOperation(MediaAlbumOperationRequest(id: detail.id, type: kind.rawValue, listingStatus: target.rawValue))
        }
    }

    /// 删除素材或样片并刷新列表。
    func delete(id: Int, api: any AssetsServing) async -> Bool {
        await operate(api: api) {
            try await api.mediaAlbumDelete(MediaAlbumDeleteRequest(id: id, type: kind.rawValue))
        }
    }

    /// 执行媒体库变更操作并刷新列表。
    private func operate(api: any AssetsServing, operation: () async throws -> Void) async -> Bool {
        do {
            try await operation()
            await reload(api: api)
            return true
        } catch {
            errorMessage = error.localizedDescription
            return false
        }
    }

    /// 组装并发送媒体库列表请求。
    private func requestList(api: any AssetsServing, page: Int) async throws -> MediaLibraryListResponse {
        try await api.mediaAlbumList(
            kind: kind,
            keyword: keyword.trimmingCharacters(in: .whitespacesAndNewlines),
            status: selectedAuditFilter.rawValue >= 0 ? selectedAuditFilter.rawValue : nil,
            page: page,
            pageSize: pageSize
        )
    }
}

/// 媒体库上传编辑 ViewModel，负责表单校验、OSS 上传和提交素材或样片。
@MainActor
final class MediaLibraryEditorViewModel: ObservableObject {
    @Published var name = ""
    @Published var description = ""
    @Published var selectedSpotId: Int?
    @Published var selectedProjectId: Int?
    @Published var projects: [PhotographerProjectItem] = []
    @Published var tagsText = ""
    @Published var coverFile: MediaLocalUploadFile?
    @Published var mediaFiles: [MediaLocalUploadFile] = []
    @Published var isSubmitting = false
    @Published var isLoadingProjects = false
    @Published var errorMessage: String?
    @Published var didSubmitSuccessfully = false

    private let editingId: Int?

    /// 初始化上传或编辑素材表单。
    init(detail: MediaLibraryDetail? = nil) {
        editingId = detail?.id
        name = detail?.name ?? ""
        description = detail?.description ?? ""
    }

    /// 判断当前是否处于编辑模式。
    var isEditing: Bool {
        editingId != nil
    }

    /// 加载样片上传可关联的项目列表。
    func loadProjects(scenicId: Int?, api: any AssetsServing) async {
        guard let scenicId else {
            projects = []
            errorMessage = "缺少当前景区，无法加载项目"
            return
        }
        isLoadingProjects = true
        errorMessage = nil
        defer { isLoadingProjects = false }

        do {
            let payload = try await api.projectList(scenicId: scenicId, name: nil, page: 1, pageSize: 50)
            projects = payload.list
            if let selectedProjectId, !projects.contains(where: { $0.id == selectedProjectId }) {
                self.selectedProjectId = nil
            }
        } catch {
            projects = []
            errorMessage = error.localizedDescription
        }
    }

    /// 添加封面本地文件。
    func setCover(_ file: MediaLocalUploadFile) {
        coverFile = file
    }

    /// 添加素材媒体文件。
    func addMediaFiles(_ files: [MediaLocalUploadFile]) {
        mediaFiles.append(contentsOf: files)
    }

    /// 移除指定媒体文件。
    func removeMediaFile(id: UUID) {
        mediaFiles.removeAll { $0.id == id }
    }

    /// 提交素材上传或编辑。
    func submit(
        kind: MediaLibraryKind = .material,
        scenicId: Int?,
        api: any AssetsServing,
        uploadService: any OSSUploadServing
    ) async -> Bool {
        guard validate(kind: kind, scenicId: scenicId) else { return false }
        guard !isSubmitting else { return false }
        isSubmitting = true
        defer { isSubmitting = false }

        do {
            let actualScenicId = scenicId ?? 0
            let coverUpload = try await uploadCover(scenicId: actualScenicId, uploadService: uploadService)
            let uploads = try await uploadMediaFiles(scenicId: actualScenicId, uploadService: uploadService)
            if let editingId {
                let request = MediaAlbumEditRequest(
                    id: editingId,
                    name: name.trimmingCharacters(in: .whitespacesAndNewlines),
                    type: kind.rawValue,
                    mediaType: resolvedMediaType(),
                    mediaList: uploads,
                    coverUrl: coverUpload.url,
                    coverSize: coverUpload.size,
                    scenicSpotId: selectedSpotId ?? 0,
                    description: description,
                    materialTag: normalizedTags()
                )
                try await api.mediaAlbumEdit(request)
            } else {
                let request = MediaAlbumUploadRequest(
                    name: name.trimmingCharacters(in: .whitespacesAndNewlines),
                    type: kind.rawValue,
                    mediaType: resolvedMediaType(),
                    mediaList: uploads,
                    coverUrl: coverUpload.url,
                    coverSize: coverUpload.size,
                    scenicSpotId: selectedSpotId ?? 0,
                    description: description,
                    materialTag: normalizedTags(),
                    projectId: kind == .sample ? (selectedProjectId ?? 0) : 0
                )
                try await api.mediaAlbumUpload(request)
            }
            didSubmitSuccessfully = true
            return true
        } catch {
            errorMessage = error.localizedDescription
            return false
        }
    }

    /// 校验素材或样片表单。
    private func validate(kind: MediaLibraryKind, scenicId: Int?) -> Bool {
        let title = name.trimmingCharacters(in: .whitespacesAndNewlines)
        guard scenicId != nil else {
            errorMessage = "缺少当前景区，无法提交\(kind == .sample ? "样片" : "素材")"
            return false
        }
        guard !title.isEmpty else {
            errorMessage = "请输入\(kind == .sample ? "样片" : "素材")名称"
            return false
        }
        guard selectedSpotId != nil else {
            errorMessage = "请选择打卡点"
            return false
        }
        guard kind != .sample || selectedProjectId != nil else {
            errorMessage = "请选择关联项目"
            return false
        }
        guard coverFile != nil else {
            errorMessage = "请选择封面"
            return false
        }
        guard !mediaFiles.isEmpty else {
            errorMessage = "请选择\(kind == .sample ? "样片" : "素材")文件"
            return false
        }
        guard normalizedTags().count <= 120 else {
            errorMessage = "标签内容过长"
            return false
        }
        return true
    }

    /// 上传封面并返回 URL 和尺寸。
    private func uploadCover(
        scenicId: Int,
        uploadService: any OSSUploadServing
    ) async throws -> (url: String, size: MediaAlbumFileSize) {
        guard let coverFile else {
            throw APIError.emptyData
        }
        let url = try await uploadService.uploadCloudFile(
            data: coverFile.data,
            fileName: coverFile.fileName,
            fileType: coverFile.fileType,
            scenicId: scenicId,
            onProgress: { _ in }
        )
        return (url, MediaAlbumFileSize(width: coverFile.width, height: coverFile.height))
    }

    /// 上传素材媒体文件并组装服务端请求项。
    private func uploadMediaFiles(
        scenicId: Int,
        uploadService: any OSSUploadServing
    ) async throws -> [MediaAlbumUploadItem] {
        var result: [MediaAlbumUploadItem] = []
        for file in mediaFiles {
            let url = try await uploadService.uploadCloudFile(
                data: file.data,
                fileName: file.fileName,
                fileType: file.fileType,
                scenicId: scenicId,
                onProgress: { _ in }
            )
            result.append(
                MediaAlbumUploadItem(
                    originalName: file.fileName,
                    ossUrl: url,
                    size: Int64(file.data.count),
                    fileWidthSize: MediaAlbumFileSize(width: file.width, height: file.height)
                )
            )
        }
        return result
    }

    /// 根据已选文件推断素材媒体类型。
    private func resolvedMediaType() -> Int {
        mediaFiles.contains { $0.fileType == 1 } ? 1 : 2
    }

    /// 规范化用户输入的标签文本。
    private func normalizedTags() -> String {
        tagsText
            .split { $0 == "," || $0 == "，" || $0 == " " || $0 == "\n" }
            .map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty }
            .joined(separator: ",")
    }
}

/// 相册列表 ViewModel，负责相册文件夹搜索、日期筛选、分页和新建相册。
@MainActor
final class AlbumListViewModel: ObservableObject {
    @Published var folders: [AlbumFolderItem] = []
    @Published var total = 0
    @Published var page = 1
    @Published var searchText = ""
    @Published var startTime = ""
    @Published var endTime = ""
    private var isLoading = false
    @Published var isLoadingMore = false
    @Published var isMutating = false
    @Published var errorMessage: String?

    private let pageSize = 10

    /// 判断当前相册列表是否还有下一页。
    var hasMore: Bool {
        folders.count < total
    }

    /// 重新加载相册第一页。
    func reload(api: any AssetsServing, scenicId: Int?) async {
        guard let scenicId else {
            folders = []
            total = 0
            page = 1
            errorMessage = "缺少当前景区，无法加载相册"
            return
        }

        isLoading = true
        errorMessage = nil
        defer { isLoading = false }

        do {
            let payload = try await requestFolders(api: api, scenicId: scenicId, page: 1)
            self.page = 1
            folders = payload.list
            total = payload.total
        } catch {
            folders = []
            total = 0
            page = 1
            errorMessage = error.localizedDescription
        }
    }

    /// 加载相册下一页。
    func loadMore(api: any AssetsServing, scenicId: Int?) async {
        guard let scenicId, hasMore, !isLoadingMore else { return }
        isLoadingMore = true
        defer { isLoadingMore = false }

        do {
            let nextPage = page + 1
            let payload = try await requestFolders(api: api, scenicId: scenicId, page: nextPage)
            page = nextPage
            total = payload.total
            folders.append(contentsOf: payload.list)
        } catch {
            errorMessage = error.localizedDescription
        }
    }

    /// 新建相册并刷新列表。
    func createFolder(name: String, remark: String, scenicId: Int?, api: any AssetsServing) async -> Bool {
        let folderName = name.trimmingCharacters(in: .whitespacesAndNewlines)
        guard let scenicId else {
            errorMessage = "缺少当前景区，无法创建相册"
            return false
        }
        guard !folderName.isEmpty else {
            errorMessage = "请输入相册名称"
            return false
        }
        guard !isMutating else { return false }

        isMutating = true
        defer { isMutating = false }

        do {
            try await api.addAlbumFolder(scenicId: scenicId, name: folderName, remark: remark.trimmingCharacters(in: .whitespacesAndNewlines))
            await reload(api: api, scenicId: scenicId)
            return true
        } catch {
            errorMessage = error.localizedDescription
            return false
        }
    }

    /// 组装相册列表请求参数。
    private func requestFolders(api: any AssetsServing, scenicId: Int, page: Int) async throws -> ListPayload<AlbumFolderItem> {
        try await api.albumFolderList(
            scenicId: scenicId,
            page: page,
            pageSize: pageSize,
            name: searchText.trimmingCharacters(in: .whitespacesAndNewlines),
            startTime: startTime.trimmingCharacters(in: .whitespacesAndNewlines).assetsNilIfEmpty,
            endTime: endTime.trimmingCharacters(in: .whitespacesAndNewlines).assetsNilIfEmpty
        )
    }
}

/// 相册详情 ViewModel，负责相册信息、图片/视频列表和文件操作。
@MainActor
final class AlbumDetailViewModel: ObservableObject {
    let folderId: Int
    @Published var folder: AlbumFolderItem?
    @Published var files: [AlbumFileItem] = []
    @Published var selectedTab: AlbumFileTab = .image
    @Published var total = 0
    @Published var page = 1
    private var isLoading = false
    @Published var isLoadingMore = false
    @Published var isMutating = false
    @Published var errorMessage: String?

    private let pageSize = 20

    /// 初始化相册详情 ViewModel。
    init(folderId: Int, summary: AlbumFolderItem? = nil) {
        self.folderId = folderId
        folder = summary
    }

    /// 判断当前文件列表是否还有下一页。
    var hasMore: Bool {
        files.count < total
    }

    /// 重新加载相册信息和当前 Tab 文件列表。
    func reload(api: any AssetsServing, scenicId: Int?) async {
        guard let scenicId else {
            files = []
            total = 0
            page = 1
            errorMessage = "缺少当前景区，无法加载相册内容"
            return
        }

        isLoading = true
        errorMessage = nil
        defer { isLoading = false }

        do {
            folder = try await api.albumFolderInfo(id: folderId)
            let listPayload = try await api.albumFileList(
                scenicId: scenicId,
                folderId: folderId,
                fileType: selectedTab.rawValue,
                page: 1,
                pageSize: pageSize
            )
            files = listPayload.list
            total = listPayload.total
            page = 1
        } catch {
            page = 1
            errorMessage = error.localizedDescription
        }
    }

    /// 切换图片或视频列表并重新加载第一页。
    func selectTab(_ tab: AlbumFileTab, api: any AssetsServing, scenicId: Int?) async {
        guard selectedTab != tab else { return }
        selectedTab = tab
        files = []
        total = 0
        page = 1
        await reload(api: api, scenicId: scenicId)
    }

    /// 加载当前 Tab 下一页文件。
    func loadMore(api: any AssetsServing, scenicId: Int?) async {
        guard let scenicId, hasMore, !isLoadingMore else { return }
        isLoadingMore = true
        defer { isLoadingMore = false }

        do {
            let nextPage = page + 1
            let payload = try await api.albumFileList(
                scenicId: scenicId,
                folderId: folderId,
                fileType: selectedTab.rawValue,
                page: nextPage,
                pageSize: pageSize
            )
            page = nextPage
            total = payload.total
            files.append(contentsOf: payload.list)
        } catch {
            errorMessage = error.localizedDescription
        }
    }

    /// 删除相册文件并刷新当前列表。
    func delete(file: AlbumFileItem, scenicId: Int?, api: any AssetsServing) async -> Bool {
        await mutate(api: api, scenicId: scenicId) {
            try await api.deleteAlbumFiles(folderId: folderId, idList: [file.id])
        }
    }

    /// 将指定相册文件设为封面。
    func setCover(file: AlbumFileItem, scenicId: Int?, api: any AssetsServing) async -> Bool {
        await mutate(api: api, scenicId: scenicId) {
            try await api.editAlbumFolder(folderId: folderId, coverFileId: file.id, name: nil, remark: nil)
        }
    }

    /// 更新相册名称。
    func rename(name: String, scenicId: Int?, api: any AssetsServing) async -> Bool {
        let folderName = name.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !folderName.isEmpty else {
            errorMessage = "请输入相册名称"
            return false
        }
        return await mutate(api: api, scenicId: scenicId) {
            try await api.editAlbumFolder(folderId: folderId, coverFileId: nil, name: folderName, remark: nil)
        }
    }

    /// 更新相册备注。
    func updateRemark(_ remark: String, scenicId: Int?, api: any AssetsServing) async -> Bool {
        await mutate(api: api, scenicId: scenicId) {
            try await api.editAlbumFolder(folderId: folderId, coverFileId: nil, name: nil, remark: remark.trimmingCharacters(in: .whitespacesAndNewlines))
        }
    }

    /// 执行相册变更操作并刷新详情。
    private func mutate(api: any AssetsServing, scenicId: Int?, operation: () async throws -> Void) async -> Bool {
        guard !isMutating else { return false }
        guard scenicId != nil else {
            errorMessage = "缺少当前景区，无法操作相册"
            return false
        }
        isMutating = true
        defer { isMutating = false }

        do {
            try await operation()
            await reload(api: api, scenicId: scenicId)
            return true
        } catch {
            errorMessage = error.localizedDescription
            return false
        }
    }
}

/// 相册预览上传 ViewModel，负责选择相册、本地文件上传和相册入库。
@MainActor
final class AlbumTrailerViewModel: ObservableObject {
    @Published var folders: [AlbumFolderItem] = []
    @Published var selectedFolderId: Int?
    @Published var localFiles: [AlbumLocalUploadFile] = []
    @Published var uploadProgress = 0
    private var isLoadingFolders = false
    @Published var isSubmitting = false
    @Published var errorMessage: String?
    @Published var didUploadSuccessfully = false

    /// 加载可上传的相册列表。
    func loadFolders(api: any AssetsServing, scenicId: Int?) async {
        guard let scenicId else {
            folders = []
            selectedFolderId = nil
            errorMessage = "缺少当前景区，无法加载相册"
            return
        }

        isLoadingFolders = true
        errorMessage = nil
        defer { isLoadingFolders = false }

        do {
            let payload = try await api.albumFolderList(
                scenicId: scenicId,
                page: 1,
                pageSize: 100,
                name: "",
                startTime: nil,
                endTime: nil
            )
            folders = payload.list
            if selectedFolderId == nil {
                selectedFolderId = folders.first?.id
            }
        } catch {
            folders = []
            selectedFolderId = nil
            errorMessage = error.localizedDescription
        }
    }

    /// 添加本地待上传文件。
    func addLocalFiles(_ files: [AlbumLocalUploadFile]) {
        localFiles.append(contentsOf: files)
    }

    /// 移除一个本地待上传文件。
    func removeLocalFile(id: UUID) {
        localFiles.removeAll { $0.id == id }
    }

    /// 上传本地文件到 OSS，并登记到指定相册。
    func submit(scenicId: Int?, api: any AssetsServing, uploadService: any OSSUploadServing) async -> Bool {
        guard let scenicId else {
            errorMessage = "缺少当前景区，无法上传"
            return false
        }
        guard let folderId = selectedFolderId else {
            errorMessage = "请选择相册"
            return false
        }
        guard !localFiles.isEmpty else {
            errorMessage = "请选择要上传的图片或视频"
            return false
        }
        guard !isSubmitting else { return false }

        isSubmitting = true
        didUploadSuccessfully = false
        uploadProgress = 0
        defer { isSubmitting = false }

        do {
            let fileCount = max(localFiles.count, 1)
            for (index, file) in localFiles.enumerated() {
                let url = try await uploadService.uploadAlbumFile(
                    data: file.data,
                    fileName: file.fileName,
                    fileType: file.fileType,
                    scenicId: scenicId
                ) { progress in
                    Task { @MainActor in
                        let base = Double(index) / Double(fileCount)
                        let step = Double(progress) / Double(fileCount)
                        self.uploadProgress = min(99, Int((base + step / 100) * 100))
                    }
                }
                try await api.albumFileUploadURL(scenicId: scenicId, fileURL: url, folderId: folderId)
            }
            uploadProgress = 100
            didUploadSuccessfully = true
            localFiles = []
            return true
        } catch {
            errorMessage = error.localizedDescription
            return false
        }
    }
}

private extension String {
    /// 空字符串转 nil，方便可选筛选参数传递。
    var assetsNilIfEmpty: String? {
        isEmpty ? nil : self
    }
}
