//
//  MaterialManagementViewModels.swift
//  suixinkan
//

import Foundation

/// 素材 OSS 上传协议，便于上传和编辑 ViewModel 在测试中注入替身。
@MainActor
protocol MaterialOSSUploading {
    /// 上传素材文件并返回 OSS URL。
    func uploadMaterialFile(
        data: Data,
        fileName: String,
        fileType: Int,
        scenicId: Int,
        onProgress: @escaping (Int) -> Void
    ) async throws -> String
}

/// 素材列表页 ViewModel，负责搜索、筛选、分页和上下架操作。
final class MaterialListViewModel {
    private(set) var items: [MaterialItem] = []
    private(set) var isLoading = false
    private(set) var isRefreshing = false
    private(set) var canLoadMore = false
    private(set) var filterStatus: MaterialFilterStatus = .all
    private(set) var searchText = ""
    private(set) var orderInfo = MaterialOrderInfo()

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

    private var currentPage = 1
    private var totalCount = 0
    private let pageSize = 10

    /// 首次加载素材列表。
    func loadInitial(api: any MaterialManagementServing) async {
        await load(reset: true, showLoading: true, api: api)
    }

    /// 下拉刷新素材列表。
    func refresh(api: any MaterialManagementServing) async {
        isRefreshing = true
        notifyStateChange()
        await load(reset: true, showLoading: false, api: api)
    }

    /// 滚动到底部附近时加载更多。
    func loadMoreIfNeeded(lastVisibleIndex: Int, api: any MaterialManagementServing) async {
        guard lastVisibleIndex >= items.count - 3 else { return }
        await load(reset: false, showLoading: false, api: api)
    }

    /// 更新搜索文本并重新加载第一页。
    func updateSearchText(_ text: String, api: any MaterialManagementServing) async {
        searchText = text
        await load(reset: true, showLoading: false, api: api)
    }

    /// 选择筛选状态并重新加载第一页。
    func selectFilter(_ status: MaterialFilterStatus, api: any MaterialManagementServing) async {
        guard filterStatus != status else { return }
        filterStatus = status
        notifyStateChange()
        await load(reset: true, showLoading: false, api: api)
    }

    /// 切换素材上下架状态。
    func toggleListing(materialId: Int, checked: Bool, api: any MaterialManagementServing) async {
        guard let currentItem = items.first(where: { $0.id == materialId }) else { return }
        guard currentItem.status == 1 else {
            onShowMessage?("审核通过才可以操作上下架")
            notifyStateChange()
            return
        }

        let targetStatus = checked ? 1 : 0
        guard currentItem.listingStatus != targetStatus else { return }

        do {
            try await api.setListingStatus(id: materialId, listingStatus: targetStatus)
            items = items.map { item in
                guard item.id == materialId else { return item }
                return MaterialItem(
                    id: item.id,
                    name: item.name,
                    coverUrl: item.coverUrl,
                    createdAt: item.createdAt,
                    status: item.status,
                    downloadCount: item.downloadCount,
                    likesCount: item.likesCount,
                    collectCount: item.collectCount,
                    shareCount: item.shareCount,
                    scenicSpotName: item.scenicSpotName,
                    projectName: item.projectName,
                    listingStatus: targetStatus
                )
            }
            notifyStateChange()
        } catch is CancellationError {
            return
        } catch {
            onShowMessage?(error.localizedDescription.isEmpty ? "操作失败" : error.localizedDescription)
        }
    }

    /// 从列表中移除已删除素材。
    func removeMaterial(id: Int) {
        items.removeAll { $0.id == id }
        totalCount = max(0, totalCount - 1)
        canLoadMore = items.count < totalCount
        notifyStateChange()
    }

    private func load(reset: Bool, showLoading: Bool, api: any MaterialManagementServing) async {
        if reset {
            currentPage = 1
            canLoadMore = false
        } else {
            guard canLoadMore, !isLoading else { return }
            currentPage += 1
        }

        isLoading = showLoading
        notifyStateChange()
        defer {
            isLoading = false
            isRefreshing = false
            notifyStateChange()
        }

        do {
            let keyword = searchText.trimmingCharacters(in: .whitespacesAndNewlines).sxkNonEmpty
            let response = try await api.list(
                status: filterStatus.apiStatus,
                keyword: keyword,
                page: currentPage,
                pageSize: pageSize
            )
            totalCount = response.total
            orderInfo = response.order ?? MaterialOrderInfo()
            if reset {
                items = response.list
            } else {
                let existingIDs = Set(items.map(\.id))
                items.append(contentsOf: response.list.filter { !existingIDs.contains($0.id) })
            }
            canLoadMore = items.count < totalCount
        } catch is CancellationError {
            return
        } catch {
            if !reset, currentPage > 1 {
                currentPage -= 1
            }
            if reset {
                items = []
                totalCount = 0
                canLoadMore = false
            }
            onShowMessage?(error.localizedDescription)
        }
    }

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

/// 素材详情页 ViewModel，负责详情加载、删除和编辑跳转状态。
final class MaterialDetailViewModel {
    private(set) var detail: MaterialDetail?
    private(set) var isLoading = false
    private(set) var isDeleting = false

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

    let materialId: Int

    /// 初始化素材详情 ViewModel。
    init(materialId: Int) {
        self.materialId = materialId
    }

    /// 加载素材详情。
    func load(api: any MaterialManagementServing) async {
        guard materialId > 0 else {
            onShowMessage?("素材ID无效")
            return
        }
        isLoading = true
        notifyStateChange()
        defer {
            isLoading = false
            notifyStateChange()
        }
        do {
            detail = try await api.detail(id: materialId)
        } catch is CancellationError {
            return
        } catch {
            onShowMessage?(error.localizedDescription.isEmpty ? "获取素材详情失败" : error.localizedDescription)
        }
    }

    /// 删除当前素材。
    func delete(api: any MaterialManagementServing) async {
        guard materialId > 0 else {
            onShowMessage?("素材ID无效")
            return
        }
        isDeleting = true
        notifyStateChange()
        defer {
            isDeleting = false
            notifyStateChange()
        }
        do {
            try await api.delete(id: materialId)
            onShowMessage?("删除成功")
            onDeleted?(materialId)
        } catch is CancellationError {
            return
        } catch {
            onShowMessage?(error.localizedDescription.isEmpty ? "删除失败" : error.localizedDescription)
        }
    }

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

/// 素材上传与编辑页 ViewModel，负责表单、标签、选择数据、上传进度与提交。
final class MaterialFormViewModel {
    enum Mode: Equatable {
        case create
        case edit(id: Int)
    }

    private(set) var materialName = ""
    private(set) var selectedScenicSpot: ScenicSpotItem?
    private(set) var scenicSpotList: [ScenicSpotItem] = []
    private(set) var mediaType: MaterialMediaType = .image
    private(set) var uploadedMediaList: [MaterialUploadedMediaItem] = []
    private(set) var coverImage: MaterialUploadedMediaItem?
    private(set) var tagList: [MaterialTagItem] = []
    private(set) var tagInput = ""
    private(set) var pendingDeleteTag: MaterialTagItem?
    private(set) var uploadDialogState: MaterialUploadDialogState?
    private(set) var isSubmitting = false
    private(set) var isLoadingDetail = false

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

    let mode: Mode
    private let currentScenicIdProvider: () -> Int
    private var pendingScenicSpotId: Int64?

    /// 初始化素材表单 ViewModel。
    init(
        mode: Mode = .create,
        currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId }
    ) {
        self.mode = mode
        self.currentScenicIdProvider = currentScenicIdProvider
    }

    /// 初始加载，编辑模式会同时拉详情。
    func loadInitial(api: any MaterialManagementServing) async {
        await loadScenicSpots(api: api)
        if case .edit(let id) = mode {
            await loadDetail(id: id, api: api)
        }
    }

    /// 更新素材名称，最多保留 30 个字符。
    func updateMaterialName(_ name: String) {
        materialName = String(name.prefix(30))
        notifyStateChange()
    }

    /// 选择关联打卡点。
    func selectScenicSpot(_ spot: ScenicSpotItem) {
        selectedScenicSpot = spot
        notifyStateChange()
    }

    /// 切换素材类型，类型变化时清空已选素材。
    func selectMediaType(_ type: MaterialMediaType) {
        guard mediaType != type else { return }
        mediaType = type
        uploadedMediaList = []
        notifyStateChange()
    }

    /// 更新标签输入。
    func updateTagInput(_ text: String) {
        tagInput = String(text.prefix(15))
        notifyStateChange()
    }

    /// 新增标签。
    func addTag(api: any MaterialManagementServing) async {
        let name = tagInput.trimmingCharacters(in: .whitespacesAndNewlines)
        guard name.count >= 5, name.count <= 15 else {
            onShowMessage?("标签长度必须在5-15个字符之间")
            return
        }
        guard tagList.count < 30 else {
            onShowMessage?("最多只能添加30个标签")
            return
        }
        guard !tagList.contains(where: { $0.name == name }) else {
            onShowMessage?("标签已存在")
            return
        }
        do {
            let id = try await api.addTag(name: name)
            tagList.append(MaterialTagItem(id: id, name: name))
            tagInput = ""
            onShowMessage?("添加标签成功")
            notifyStateChange()
        } catch is CancellationError {
            return
        } catch {
            onShowMessage?(error.localizedDescription.isEmpty ? "添加标签失败" : error.localizedDescription)
        }
    }

    /// 请求删除标签，等待用户确认。
    func requestDeleteTag(_ tag: MaterialTagItem) {
        pendingDeleteTag = tag
        notifyStateChange()
    }

    /// 取消删除标签。
    func cancelDeleteTag() {
        pendingDeleteTag = nil
        notifyStateChange()
    }

    /// 确认删除当前待删除标签。
    func confirmDeleteTag() {
        guard let tag = pendingDeleteTag else { return }
        tagList.removeAll { $0.id == tag.id }
        pendingDeleteTag = nil
        notifyStateChange()
    }

    /// 增加并立即顺序上传素材。
    func addLocalMedia(_ mediaItems: [MaterialUploadedMediaItem], uploader: any MaterialOSSUploading) async {
        guard !mediaItems.isEmpty else { return }
        let remaining = mediaType.maxCount - uploadedMediaList.count
        guard remaining > 0 else {
            onShowMessage?(mediaType == .video ? "视频只能上传1个" : "图片最多上传9张")
            return
        }

        let accepted = Array(mediaItems.prefix(remaining))
        uploadedMediaList.append(contentsOf: accepted)
        notifyStateChange()

        for offset in accepted.indices {
            let index = uploadedMediaList.count - accepted.count + offset
            await uploadMedia(at: index, title: "正在上传素材(\(offset + 1)/\(accepted.count))", uploader: uploader)
        }
        uploadDialogState = nil
        notifyStateChange()
    }

    /// 设置封面并立即上传。
    func setCoverImage(_ item: MaterialUploadedMediaItem, uploader: any MaterialOSSUploading) async {
        coverImage = item
        notifyStateChange()
        await uploadCover(uploader: uploader)
        uploadDialogState = nil
        notifyStateChange()
    }

    /// 删除素材。
    func deleteMaterial(at index: Int) {
        guard uploadedMediaList.indices.contains(index) else { return }
        uploadedMediaList.remove(at: index)
        notifyStateChange()
    }

    /// 删除封面。
    func deleteCoverImage() {
        coverImage = nil
        notifyStateChange()
    }

    /// 上传或编辑素材。
    func submit(api: any MaterialManagementServing) async {
        guard !materialName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
            onShowMessage?("请输入素材名称")
            return
        }
        guard materialName.count <= 30 else {
            onShowMessage?("素材名称最多30个字符")
            return
        }
        guard !uploadedMediaList.isEmpty else {
            onShowMessage?("请至少上传一个素材")
            return
        }
        guard let coverImage else {
            onShowMessage?("请上传封面图片")
            return
        }
        guard let selectedScenicSpot else {
            onShowMessage?("请选择关联打卡点")
            return
        }
        guard !uploadedMediaList.contains(where: { $0.ossUrl.isEmpty }) else {
            onShowMessage?("请等待素材上传完成")
            return
        }
        guard !coverImage.ossUrl.isEmpty else {
            onShowMessage?("请等待封面图片上传完成")
            return
        }

        let request = MaterialUploadRequest(
            id: editID,
            name: materialName,
            mediaType: mediaType,
            mediaList: uploadedMediaList.map {
                MaterialUploadMediaItem(
                    originalName: $0.fileName,
                    ossUrl: $0.ossUrl,
                    size: $0.size,
                    fileWidthSize: MaterialUploadFileSize(width: $0.width, height: $0.height)
                )
            },
            coverUrl: coverImage.ossUrl,
            coverSize: String(coverImage.size),
            scenicSpotId: selectedScenicSpot.id,
            materialTag: tagList.filter { $0.id > 0 }.map { String($0.id) }.joined(separator: ",")
        )

        isSubmitting = true
        notifyStateChange()
        defer {
            isSubmitting = false
            notifyStateChange()
        }
        do {
            switch mode {
            case .create:
                try await api.upload(request)
                onShowMessage?("上传成功")
            case .edit:
                try await api.edit(request)
                onShowMessage?("编辑成功")
            }
            onSubmitSuccess?()
        } catch is CancellationError {
            return
        } catch {
            onShowMessage?(error.localizedDescription.isEmpty ? "提交失败" : error.localizedDescription)
        }
    }

    private var editID: Int? {
        if case .edit(let id) = mode { return id }
        return nil
    }

    private func loadScenicSpots(api: any MaterialManagementServing) async {
        let scenicId = currentScenicIdProvider()
        guard scenicId > 0 else {
            onShowMessage?("请先选择景区")
            return
        }
        do {
            let response = try await api.scenicSpotListAll(scenicId: String(scenicId))
            scenicSpotList = response.list
            if let pendingScenicSpotId {
                selectedScenicSpot = scenicSpotList.first { $0.id == pendingScenicSpotId }
            }
            notifyStateChange()
        } catch is CancellationError {
            return
        } catch {
            onShowMessage?(error.localizedDescription.isEmpty ? "获取打卡点失败" : error.localizedDescription)
        }
    }

    private func loadDetail(id: Int, api: any MaterialManagementServing) async {
        isLoadingDetail = true
        notifyStateChange()
        defer {
            isLoadingDetail = false
            notifyStateChange()
        }
        do {
            let detail = try await api.detail(id: id)
            materialName = detail.name ?? ""
            mediaType = detail.mediaList?.first?.type == 2 ? .video : .image
            pendingScenicSpotId = detail.scenicSpotId
            selectedScenicSpot = scenicSpotList.first { $0.id == detail.scenicSpotId }
            tagList = detail.materialTag ?? []
            uploadedMediaList = detail.mediaList?.map { media in
                MaterialUploadedMediaItem(
                    data: Data(),
                    thumbnailData: nil,
                    previewURL: media.displayURL,
                    fileName: media.originalName ?? "material",
                    size: media.size ?? 0,
                    width: media.width ?? 0,
                    height: media.height ?? 0,
                    ossUrl: media.ossUrl ?? media.showUrl ?? "",
                    isUploading: false,
                    uploadProgress: 100
                )
            } ?? []
            if let coverUrl = detail.coverUrl, !coverUrl.isEmpty {
                coverImage = MaterialUploadedMediaItem(
                    data: Data(),
                    previewURL: coverUrl,
                    fileName: "cover",
                    size: Int64(detail.coverSize ?? 0),
                    ossUrl: coverUrl,
                    isUploading: false,
                    uploadProgress: 100
                )
            }
        } catch is CancellationError {
            return
        } catch {
            onShowMessage?(error.localizedDescription.isEmpty ? "加载素材详情失败" : error.localizedDescription)
        }
    }

    private func uploadMedia(at index: Int, title: String, uploader: any MaterialOSSUploading) async {
        guard uploadedMediaList.indices.contains(index) else { return }
        guard currentScenicIdProvider() > 0 else {
            onShowMessage?("请先选择景区")
            return
        }
        uploadedMediaList[index].isUploading = true
        uploadedMediaList[index].uploadProgress = 0
        uploadDialogState = MaterialUploadDialogState(title: title, progress: 0)
        notifyStateChange()

        do {
            let item = uploadedMediaList[index]
            let url = try await uploader.uploadMaterialFile(
                data: item.data,
                fileName: item.fileName,
                fileType: mediaType.uploadFileType,
                scenicId: currentScenicIdProvider(),
                onProgress: { [weak self] progress in
                    guard let self else { return }
                    Task { @MainActor in
                        guard self.uploadedMediaList.indices.contains(index) else { return }
                        let clampedProgress = max(0, min(100, progress))
                        self.uploadedMediaList[index].uploadProgress = clampedProgress
                        self.uploadedMediaList[index].isUploading = clampedProgress < 100
                        if clampedProgress < 100 {
                            self.uploadDialogState = MaterialUploadDialogState(title: title, progress: clampedProgress)
                        }
                        self.notifyStateChange()
                    }
                }
            )
            guard uploadedMediaList.indices.contains(index) else { return }
            uploadedMediaList[index].ossUrl = url
            uploadedMediaList[index].isUploading = false
            uploadedMediaList[index].uploadProgress = 100
        } catch is CancellationError {
            return
        } catch {
            guard uploadedMediaList.indices.contains(index) else { return }
            uploadedMediaList[index].isUploading = false
            uploadedMediaList[index].uploadProgress = 0
            onShowMessage?(error.localizedDescription.isEmpty ? "上传失败" : error.localizedDescription)
        }
        notifyStateChange()
    }

    private func uploadCover(uploader: any MaterialOSSUploading) async {
        guard let coverImage else { return }
        guard currentScenicIdProvider() > 0 else {
            onShowMessage?("请先选择景区")
            return
        }
        self.coverImage?.isUploading = true
        self.coverImage?.uploadProgress = 0
        uploadDialogState = MaterialUploadDialogState(title: "正在上传封面", progress: 0)
        notifyStateChange()

        do {
            let url = try await uploader.uploadMaterialFile(
                data: coverImage.data,
                fileName: coverImage.fileName,
                fileType: 2,
                scenicId: currentScenicIdProvider(),
                onProgress: { [weak self] progress in
                    guard let self else { return }
                    Task { @MainActor in
                        let clampedProgress = max(0, min(100, progress))
                        self.coverImage?.uploadProgress = clampedProgress
                        self.coverImage?.isUploading = clampedProgress < 100
                        if clampedProgress < 100 {
                            self.uploadDialogState = MaterialUploadDialogState(title: "正在上传封面", progress: clampedProgress)
                        }
                        self.notifyStateChange()
                    }
                }
            )
            self.coverImage?.ossUrl = url
            self.coverImage?.isUploading = false
            self.coverImage?.uploadProgress = 100
        } catch is CancellationError {
            return
        } catch {
            self.coverImage?.isUploading = false
            self.coverImage?.uploadProgress = 0
            onShowMessage?(error.localizedDescription.isEmpty ? "封面上传失败" : error.localizedDescription)
        }
    }

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

private extension String {
    var sxkNonEmpty: String? { isEmpty ? nil : self }
}
