//
//  SampleManagementViewModels.swift
//  suixinkan
//

import Foundation

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

/// 样片列表页 ViewModel，负责搜索、筛选、分页和上下架操作。
final class SampleListViewModel {
    private(set) var items: [SampleMaterialItem] = []
    private(set) var isLoading = false
    private(set) var isRefreshing = false
    private(set) var canLoadMore = false
    private(set) var filterStatus: SampleFilterStatus = .all
    private(set) var searchText = ""
    private(set) var operatingSampleIDs: Set<Int> = []

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

    private var currentPage = 1
    private var totalCount = 0
    private let pageSize = 10
    private var isRequestingList = false
    private var isLoadingMore = false

    /// 是否正在执行样片上下架操作。
    var isOperating: Bool { !operatingSampleIDs.isEmpty }

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

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

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

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

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

    /// 切换样片上下架状态。
    @discardableResult
    func toggleListing(sampleId: Int, checked: Bool, api: any SampleManagementServing) async -> Bool {
        guard let currentItem = items.first(where: { $0.id == sampleId }) else { return !checked }
        let previousState = currentItem.isListed
        guard currentItem.status == 1 else {
            onShowMessage?("审核通过才可以操作上下架")
            notifyStateChange()
            return previousState
        }

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

        guard !operatingSampleIDs.contains(sampleId) else { return previousState }
        operatingSampleIDs.insert(sampleId)
        notifyStateChange()
        defer {
            operatingSampleIDs.remove(sampleId)
            notifyStateChange()
        }

        do {
            try await api.setListingStatus(id: sampleId, listingStatus: targetStatus)
            items = items.map { item in
                guard item.id == sampleId else { return item }
                return SampleMaterialItem(
                    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()
            return checked
        } catch is CancellationError {
            return previousState
        } catch {
            onShowMessage?(error.localizedDescription.isEmpty ? "操作失败" : error.localizedDescription)
            return previousState
        }
    }

    private func load(reset: Bool, showLoading: Bool, api: any SampleManagementServing) async {
        guard !isRequestingList else {
            if isRefreshing {
                isRefreshing = false
                notifyStateChange()
            }
            return
        }
        if reset {
            currentPage = 1
            canLoadMore = false
        } else {
            guard canLoadMore, !isLoading, !isLoadingMore else { return }
            isLoadingMore = true
            currentPage += 1
        }

        isRequestingList = true
        isLoading = showLoading
        notifyStateChange()
        defer {
            isRequestingList = false
            if !reset { isLoadingMore = false }
            isLoading = false
            isRefreshing = false
            notifyStateChange()
        }

        do {
            let keyword = searchText.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty
            let response = try await api.list(
                status: filterStatus.apiStatus,
                keyword: keyword,
                page: currentPage,
                pageSize: pageSize
            )
            totalCount = response.total
            items = reset ? response.list : items + response.list
            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 SampleDetailViewModel {
    private(set) var detail: SampleDetail?
    private(set) var isLoading = false

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

    let sampleId: Int

    /// 初始化样片详情 ViewModel。
    init(sampleId: Int) {
        self.sampleId = sampleId
    }

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

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

/// 样片上传页 ViewModel，负责表单、选择数据、上传进度与提交。
final class UploadSampleViewModel {
    private(set) var albumName = ""
    private(set) var selectedScenic: ScenicInfo?
    private(set) var scenicList: [ScenicInfo] = []
    private(set) var selectedScenicSpot: ScenicSpotItem?
    private(set) var scenicSpotList: [ScenicSpotItem] = []
    private(set) var selectedProject: SampleProjectItem?
    private(set) var projectList: [SampleProjectItem] = []
    private(set) var projectCanLoadMore = false
    private(set) var mediaType: SampleMediaType = .image
    private(set) var uploadedMediaList: [SampleUploadedMediaItem] = []
    private(set) var coverImage: SampleUploadedMediaItem?
    private(set) var uploadDialogState: SampleUploadDialogState?
    private(set) var isSubmitting = false

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

    private let scenicListProvider: () -> [ScenicInfo]
    private let currentScenicIdProvider: () -> Int
    private var projectCurrentPage = 1
    private var projectTotal = 0
    private var projectIsLoading = false
    private let projectPageSize = 10

    /// 初始化样片上传 ViewModel。
    init(
        scenicListProvider: @escaping () -> [ScenicInfo] = { AppStore.shared.permissions.roleScenicList() },
        currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId }
    ) {
        self.scenicListProvider = scenicListProvider
        self.currentScenicIdProvider = currentScenicIdProvider
        scenicList = scenicListProvider()
    }

    /// 更新相册名称，最多保留 30 个字符。
    func updateAlbumName(_ name: String) {
        albumName = String(name.prefix(30))
        notifyStateChange()
    }

    /// 选择关联景区，并清空打卡点和项目选择。
    func selectScenic(_ scenic: ScenicInfo, api: any SampleManagementServing) async {
        selectedScenic = scenic
        selectedScenicSpot = nil
        scenicSpotList = []
        selectedProject = nil
        projectList = []
        projectCurrentPage = 1
        projectTotal = 0
        projectCanLoadMore = false
        notifyStateChange()
        await loadScenicSpots(api: api)
    }

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

    /// 选择关联项目。
    func selectProject(_ project: SampleProjectItem) {
        selectedProject = project
        notifyStateChange()
    }

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

    /// 加载项目列表。
    func loadProjects(refresh: Bool, api: any SampleManagementServing) async {
        guard let scenicId = selectedScenic?.id, scenicId > 0 else {
            onShowMessage?("请先选择景区")
            return
        }
        guard !projectIsLoading else { return }
        if refresh {
            projectCurrentPage = 1
            projectList = []
            projectCanLoadMore = false
            notifyStateChange()
        } else {
            guard projectCanLoadMore else { return }
        }

        projectIsLoading = true
        defer {
            projectIsLoading = false
            notifyStateChange()
        }

        do {
            let response = try await api.projectList(
                scenicId: String(scenicId),
                name: nil,
                page: projectCurrentPage,
                pageSize: projectPageSize
            )
            projectTotal = response.total
            projectList = refresh ? response.list : projectList + response.list
            projectCurrentPage += 1
            projectCanLoadMore = projectList.count < projectTotal
        } catch is CancellationError {
            return
        } catch {
            onShowMessage?(error.localizedDescription.isEmpty ? "获取项目列表失败" : error.localizedDescription)
        }
    }

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

        let acceptedItems = Array(mediaItems.prefix(remaining))
        if acceptedItems.count < mediaItems.count {
            onShowMessage?(mediaType == .video ? "视频只能上传1个" : "图片最多上传9张")
        }
        let startIndex = uploadedMediaList.count
        uploadedMediaList.append(contentsOf: acceptedItems)
        notifyStateChange()

        for offset in acceptedItems.indices {
            let index = startIndex + offset
            await uploadMedia(at: index, batchPosition: offset + 1, batchTotal: acceptedItems.count, uploader: uploader)
        }
        uploadDialogState = nil
        notifyStateChange()
    }

    /// 设置封面并立即上传。
    func setCoverImage(_ image: SampleUploadedMediaItem, uploader: any SampleOSSUploading) async {
        coverImage = image
        notifyStateChange()
        await uploadCover(uploader: uploader)
    }

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

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

    /// 校验并提交样片。
    func uploadSample(api: any SampleManagementServing) async {
        let trimmedName = albumName.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmedName.isEmpty else {
            onShowMessage?("请输入相册名称")
            return
        }
        guard trimmedName.count <= 30 else {
            onShowMessage?("相册名称最多30个字符")
            return
        }
        guard !uploadedMediaList.isEmpty else {
            onShowMessage?("请至少上传一个素材")
            return
        }
        guard let coverImage else {
            onShowMessage?("请上传封面图片")
            return
        }
        guard selectedScenic != nil else {
            onShowMessage?("请选择关联景区")
            return
        }
        guard let selectedScenicSpot else {
            onShowMessage?("请选择关联打卡点")
            return
        }
        guard let selectedProject else {
            onShowMessage?("请选择关联项目")
            return
        }
        guard uploadedMediaList.allSatisfy({ !$0.ossUrl.isEmpty }) else {
            onShowMessage?("请等待素材上传完成")
            return
        }
        guard !coverImage.ossUrl.isEmpty else {
            onShowMessage?("请等待封面图片上传完成")
            return
        }

        let mediaList = uploadedMediaList.map { item in
            SampleUploadMediaItem(
                originalName: item.fileName,
                ossUrl: item.ossUrl,
                size: item.size,
                fileWidthSize: SampleUploadFileSize(width: item.width, height: item.height)
            )
        }
        let request = SampleUploadMaterialRequest(
            name: trimmedName,
            mediaType: mediaType,
            mediaList: mediaList,
            coverUrl: coverImage.ossUrl,
            coverSize: String(coverImage.size),
            scenicSpotId: selectedScenicSpot.id,
            projectId: selectedProject.id
        )

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

        do {
            try await api.upload(request)
            onShowMessage?("上传成功")
            onUploadSuccess?()
        } catch is CancellationError {
            return
        } catch {
            onShowMessage?(error.localizedDescription.isEmpty ? "上传失败" : error.localizedDescription)
        }
    }

    private func loadScenicSpots(api: any SampleManagementServing) async {
        guard let scenicId = selectedScenic?.id, scenicId > 0 else { return }
        do {
            let response = try await api.scenicSpotListAll(scenicId: String(scenicId))
            scenicSpotList = response.list
            notifyStateChange()
        } catch is CancellationError {
            return
        } catch {
            onShowMessage?(error.localizedDescription.isEmpty ? "获取打卡点失败" : error.localizedDescription)
        }
    }

    private func uploadMedia(
        at index: Int,
        batchPosition: Int,
        batchTotal: Int,
        uploader: any SampleOSSUploading
    ) async {
        guard uploadedMediaList.indices.contains(index) else { return }
        let scenicId = currentScenicIdProvider()
        guard scenicId > 0 else {
            onShowMessage?("请先选择景区")
            return
        }

        uploadedMediaList[index].isUploading = true
        uploadedMediaList[index].uploadProgress = 0
        uploadDialogState = SampleUploadDialogState(title: "正在上传素材(\(batchPosition)/\(batchTotal))", progress: 0)
        notifyStateChange()

        let item = uploadedMediaList[index]
        do {
            let url = try await uploader.uploadSampleFile(
                data: item.data,
                fileName: item.fileName,
                fileType: mediaType.uploadFileType,
                scenicId: scenicId
            ) { [weak self] progress in
                guard let self else { return }
                Task { @MainActor in
                    guard self.uploadedMediaList.indices.contains(index) else { return }
                    guard self.uploadedMediaList[index].isUploading else { return }
                    self.uploadedMediaList[index].uploadProgress = progress
                    self.uploadedMediaList[index].isUploading = progress < 100
                    self.uploadDialogState = SampleUploadDialogState(
                        title: "正在上传素材(\(batchPosition)/\(batchTotal))",
                        progress: max(0, min(100, progress))
                    )
                    self.notifyStateChange()
                }
            }
            guard uploadedMediaList.indices.contains(index) else { return }
            uploadedMediaList[index].ossUrl = url
            uploadedMediaList[index].isUploading = false
            uploadedMediaList[index].uploadProgress = 100
            notifyStateChange()
        } catch is CancellationError {
            return
        } catch {
            guard uploadedMediaList.indices.contains(index) else { return }
            uploadedMediaList[index].isUploading = false
            uploadedMediaList[index].uploadProgress = 0
            onShowMessage?("上传失败: \(error.localizedDescription)")
            notifyStateChange()
        }
    }

    private func uploadCover(uploader: any SampleOSSUploading) async {
        guard var image = coverImage else { return }
        let scenicId = currentScenicIdProvider()
        guard scenicId > 0 else {
            onShowMessage?("请先选择景区")
            return
        }

        image.isUploading = true
        image.uploadProgress = 0
        coverImage = image
        uploadDialogState = SampleUploadDialogState(title: "正在上传封面", progress: 0)
        notifyStateChange()

        do {
            let url = try await uploader.uploadSampleFile(
                data: image.data,
                fileName: image.fileName,
                fileType: SampleMediaType.image.uploadFileType,
                scenicId: scenicId
            ) { [weak self] progress in
                guard let self else { return }
                Task { @MainActor in
                    guard var current = self.coverImage else { return }
                    guard current.isUploading else { return }
                    current.uploadProgress = progress
                    current.isUploading = progress < 100
                    self.coverImage = current
                    self.uploadDialogState = SampleUploadDialogState(
                        title: "正在上传封面",
                        progress: max(0, min(100, progress))
                    )
                    self.notifyStateChange()
                }
            }
            guard var current = coverImage else { return }
            current.ossUrl = url
            current.isUploading = false
            current.uploadProgress = 100
            coverImage = current
            uploadDialogState = nil
            notifyStateChange()
        } catch is CancellationError {
            return
        } catch {
            guard var current = coverImage else { return }
            current.isUploading = false
            current.uploadProgress = 0
            coverImage = current
            uploadDialogState = nil
            onShowMessage?("封面上传失败: \(error.localizedDescription)")
            notifyStateChange()
        }
    }

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

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