//
//  ProjectViewModels.swift
//  suixinkan
//
//  Created by Codex on 2026/6/24.
//

import Foundation
import Combine

/// 项目表单模式，区分新建和编辑。
enum ProjectEditorMode: Equatable {
    case create
    case edit(PhotographerProjectDetailResponse)
}

/// 店铺项目表单模式，区分新建和编辑。
enum StoreProjectEditorMode: Equatable {
    case create
    case edit(PhotographerProjectDetailResponse)
}

/// 项目表单错误实体，表示校验、上传或提交失败。
enum ProjectEditorError: LocalizedError, Equatable {
    case missingScenic
    case missingUser
    case missingName
    case missingDescription
    case missingPrice
    case missingSpot
    case missingImage
    case missingStore

    /// 错误文案。
    var errorDescription: String? {
        switch self {
        case .missingScenic: return "当前缺少景区信息"
        case .missingUser: return "当前缺少用户信息"
        case .missingName: return "请输入项目名称"
        case .missingDescription: return "请输入项目简介"
        case .missingPrice: return "请填写有效价格"
        case .missingSpot: return "请至少选择一个打卡点"
        case .missingImage: return "请至少上传项目封面"
        case .missingStore: return "请选择所属门店"
        }
    }
}

/// 摄影师项目管理 ViewModel，负责项目列表、分页、详情和删除。
@MainActor
final class ProjectManagementViewModel: ObservableObject {
    @Published private(set) var items: [PhotographerProjectItem] = []
    @Published private(set) var loading = false
    @Published private(set) var loadingMore = false
    @Published private(set) var hasMore = false
    @Published private(set) var selectedDetail: PhotographerProjectDetailResponse?
    @Published var searchText = ""
    @Published var errorMessage: String?

    @Published private var page = 1
    @Published private var total = 0
    private let pageSize = 10

    /// 重新加载摄影师项目列表。
    func reload(api: any ProjectServing, scenicId: Int?) async {
        guard let scenicId else {
            resetList()
            return
        }
        loading = true
        errorMessage = nil
        defer { loading = false }
        do {
            page = 1
            let response = try await api.projectList(
                scenicId: scenicId,
                name: normalizedSearch,
                page: page,
                pageSize: pageSize
            )
            total = response.total
            items = sorted(response.list)
            hasMore = items.count < total
            page = 2
        } catch {
            resetList()
            errorMessage = error.localizedDescription
        }
    }

    /// 加载下一页摄影师项目。
    func loadMore(api: any ProjectServing, scenicId: Int?) async {
        guard let scenicId, hasMore, !loadingMore else { return }
        loadingMore = true
        defer { loadingMore = false }
        do {
            let response = try await api.projectList(scenicId: scenicId, name: normalizedSearch, page: page, pageSize: pageSize)
            total = response.total
            items = sorted(deduplicated(items + response.list))
            hasMore = items.count < total
            page += 1
        } catch {
            errorMessage = error.localizedDescription
        }
    }

    /// 加载项目详情。
    func loadDetail(id: Int, api: any ProjectServing) async {
        do {
            selectedDetail = try await api.projectDetail(id: id)
        } catch {
            errorMessage = error.localizedDescription
        }
    }

    /// 删除摄影师项目并从本地列表移除。
    func delete(id: Int, api: any ProjectServing) async {
        do {
            try await api.deleteProject(id: id)
            items.removeAll { $0.id == id }
            total = max(total - 1, 0)
            hasMore = items.count < total
        } catch {
            errorMessage = error.localizedDescription
        }
    }

    private var normalizedSearch: String? {
        let value = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
        return value.isEmpty ? nil : value
    }

    private func resetList() {
        items = []
        loading = false
        loadingMore = false
        hasMore = false
        page = 1
        total = 0
    }

    private func sorted(_ values: [PhotographerProjectItem]) -> [PhotographerProjectItem] {
        values.sorted { lhs, rhs in
            if lhs.status != rhs.status { return lhs.status < rhs.status }
            return lhs.id > rhs.id
        }
    }

    private func deduplicated(_ values: [PhotographerProjectItem]) -> [PhotographerProjectItem] {
        var result: [Int: PhotographerProjectItem] = [:]
        values.forEach { result[$0.id] = $0 }
        return Array(result.values)
    }
}

/// 项目编辑 ViewModel，负责摄影师项目表单、图片上传和提交。
@MainActor
final class ProjectEditorViewModel: ObservableObject {
    @Published var name = ""
    @Published var descriptionText = ""
    @Published var price = ""
    @Published var otPrice = ""
    @Published var deposit = ""
    @Published var attrLabelText = ""
    @Published var materialNum = "1"
    @Published var photoNum = "1"
    @Published var videoNum = "1"
    @Published var selectedSpotIds: Set<Int> = []
    @Published var coverImage: ProjectLocalImage?
    @Published var carouselImages: [ProjectLocalImage] = []
    @Published var existingCoverURL = ""
    @Published var existingCarouselURLs: [String] = []
    @Published private(set) var submitting = false
    @Published private(set) var uploadProgress = 0
    @Published var errorMessage: String?

    private var mode: ProjectEditorMode

    /// 初始化摄影师项目表单，并在编辑模式下回填详情。
    init(mode: ProjectEditorMode) {
        self.mode = mode
        if case let .edit(detail) = mode {
            apply(detail)
        }
    }

    /// 回填编辑详情，保留同一个 ObservableObject 实例。
    func apply(_ detail: PhotographerProjectDetailResponse) {
        mode = .edit(detail)
        name = detail.name
        descriptionText = detail.description
        price = detail.price
        otPrice = detail.otPrice
        deposit = detail.priceDeposit
        attrLabelText = detail.attrLabel.joined(separator: "，")
        materialNum = "\(max(detail.materialNum, 0))"
        photoNum = "\(max(detail.photoNum, 0))"
        videoNum = "\(max(detail.videoNum, 0))"
        selectedSpotIds = Set(detail.scenicList.map(\.id))
        existingCoverURL = detail.coverProject
        existingCarouselURLs = detail.coverCarousel
        coverImage = nil
        carouselImages = []
    }

    /// 提交摄影师项目，必要时先上传封面和轮播图。
    func submit(
        scenicId: Int?,
        userId: Int?,
        api: any ProjectServing,
        uploadService: any OSSUploadServing
    ) async -> Bool {
        guard !submitting else { return false }
        guard let scenicId else { return fail(.missingScenic) }
        guard let userId, userId > 0 else { return fail(.missingUser) }
        let normalizedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
        let normalizedDescription = descriptionText.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !normalizedName.isEmpty else { return fail(.missingName) }
        guard !normalizedDescription.isEmpty else { return fail(.missingDescription) }
        guard Double(price) ?? 0 > 0 else { return fail(.missingPrice) }
        guard !selectedSpotIds.isEmpty else { return fail(.missingSpot) }

        submitting = true
        errorMessage = nil
        uploadProgress = 0
        defer { submitting = false }

        do {
            let coverURL = try await resolveCoverURL(scenicId: scenicId, uploadService: uploadService)
            guard !coverURL.isEmpty else { throw ProjectEditorError.missingImage }
            let carouselURLs = try await resolveCarouselURLs(scenicId: scenicId, uploadService: uploadService)
            let request = ProjectCreateRequest(
                id: editID,
                type: 11,
                scenicId: scenicId,
                name: normalizedName,
                price: normalizedMoney(price) ?? "0.00",
                otPrice: normalizedMoney(otPrice),
                coverProject: coverURL,
                coverCarousel: carouselURLs.isEmpty ? nil : carouselURLs,
                description: normalizedDescription,
                attrLabel: normalizedLabels,
                extra: ProjectCreateExtra(
                    priceDeposit: normalizedMoney(deposit) ?? "0.00",
                    materialNum: max(Int(materialNum) ?? 0, 0),
                    photoNum: max(Int(photoNum) ?? 0, 0),
                    videoNum: max(Int(videoNum) ?? 0, 0),
                    scenicSpotId: selectedSpotIds.sorted(),
                    photogUid: [userId]
                )
            )
            if editID == nil {
                try await api.createProject(request)
            } else {
                try await api.editProject(request)
            }
            uploadProgress = 100
            return true
        } catch {
            errorMessage = error.localizedDescription
            return false
        }
    }

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

    private var normalizedLabels: [String]? {
        let labels = attrLabelText
            .components(separatedBy: CharacterSet(charactersIn: ",，"))
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty }
        return labels.isEmpty ? nil : labels
    }

    private func resolveCoverURL(scenicId: Int, uploadService: any OSSUploadServing) async throws -> String {
        if let coverImage {
            return try await uploadService.uploadProjectImage(data: coverImage.data, fileName: coverImage.fileName, scenicId: scenicId) { [weak self] progress in
                self?.uploadProgress = progress
            }
        }
        return existingCoverURL
    }

    private func resolveCarouselURLs(scenicId: Int, uploadService: any OSSUploadServing) async throws -> [String] {
        var urls = existingCarouselURLs
        for image in carouselImages {
            let url = try await uploadService.uploadProjectImage(data: image.data, fileName: image.fileName, scenicId: scenicId) { [weak self] progress in
                self?.uploadProgress = progress
            }
            urls.append(url)
        }
        return urls
    }

    private func normalizedMoney(_ value: String) -> String? {
        let amount = Double(value.trimmingCharacters(in: .whitespacesAndNewlines)) ?? 0
        return amount > 0 ? String(format: "%.2f", amount) : nil
    }

    private func fail(_ error: ProjectEditorError) -> Bool {
        errorMessage = error.localizedDescription
        return false
    }
}

/// 店铺项目管理 ViewModel，负责店铺项目列表、筛选、详情和删除。
@MainActor
final class StoreProjectManagementViewModel: ObservableObject {
    @Published private(set) var items: [PhotographerProjectItem] = []
    @Published private(set) var loading = false
    @Published private(set) var selectedDetail: PhotographerProjectDetailResponse?
    @Published var searchText = ""
    @Published var selectedType = 0
    @Published var errorMessage: String?

    /// 根据本地筛选条件返回展示项目。
    var filteredItems: [PhotographerProjectItem] {
        let keyword = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
        return items.filter { item in
            let typeMatched = selectedType == 0 || item.type == selectedType
            let keywordMatched = keyword.isEmpty || item.name.localizedCaseInsensitiveContains(keyword)
            return typeMatched && keywordMatched
        }
    }

    /// 重新加载店铺项目列表。
    func reload(api: any ProjectServing, userId: String?) async {
        let normalizedUserId = userId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        guard !normalizedUserId.isEmpty else {
            items = []
            return
        }
        loading = true
        errorMessage = nil
        defer { loading = false }
        do {
            let response = try await api.storeManagerProjectList(userId: normalizedUserId, page: 1, pageSize: 100)
            items = response.list
        } catch {
            items = []
            errorMessage = error.localizedDescription
        }
    }

    /// 加载店铺项目详情。
    func loadDetail(id: Int, api: any ProjectServing) async {
        do {
            selectedDetail = try await api.storeManagerProjectDetail(id: id)
        } catch {
            errorMessage = error.localizedDescription
        }
    }

    /// 删除店铺项目。
    func delete(id: Int, api: any ProjectServing) async {
        do {
            try await api.storeManagerDeleteProject(id: id)
            items.removeAll { $0.id == id }
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}

/// 店铺项目编辑 ViewModel，负责多点位和押金项目表单提交。
@MainActor
final class StoreProjectEditorViewModel: ObservableObject {
    @Published var projectType = 19
    @Published var scenicList: [StoreManagerScenicItem] = []
    @Published var storeList: [StoreItem] = []
    @Published var selectedScenicIds: Set<Int> = []
    @Published var selectedStoreId: Int?
    @Published var name = ""
    @Published var descriptionText = ""
    @Published var coverImage: ProjectLocalImage?
    @Published var carouselImages: [ProjectLocalImage] = []
    @Published var existingCoverURL = ""
    @Published var existingCarouselURLs: [String] = []
    @Published var settleSpotNum = "1"
    @Published var projectPrice = ""
    @Published var priceMaterial = ""
    @Published var pricePhoto = ""
    @Published var priceVideo = ""
    @Published var priceDeposit = ""
    @Published var materialNum = "1"
    @Published var photoNum = "1"
    @Published var videoNum = "1"
    @Published private(set) var submitting = false
    @Published var errorMessage: String?

    private var mode: StoreProjectEditorMode

    /// 初始化店铺项目表单，并在编辑模式下回填详情。
    init(mode: StoreProjectEditorMode) {
        self.mode = mode
        if case let .edit(detail) = mode {
            apply(detail)
        }
    }

    /// 回填编辑详情，保留同一个 ObservableObject 实例。
    func apply(_ detail: PhotographerProjectDetailResponse) {
        mode = .edit(detail)
        projectType = detail.type == 4 ? 4 : 19
        name = detail.name
        descriptionText = detail.description
        existingCoverURL = detail.coverProject
        existingCarouselURLs = detail.coverCarousel
        selectedScenicIds = Set(detail.scenicList.map(\.id))
        projectPrice = detail.price
        priceDeposit = detail.priceDeposit
        materialNum = "\(max(detail.materialNum, 0))"
        photoNum = "\(max(detail.photoNum, 0))"
        videoNum = "\(max(detail.videoNum, 0))"
        coverImage = nil
        carouselImages = []
    }

    /// 加载店铺项目可管理景区。
    func loadScenicList(api: any ProjectServing, userId: Int) async {
        guard scenicList.isEmpty, userId > 0 else { return }
        do {
            scenicList = try await api.storeManagerScenicList(userId: "\(userId)").list
        } catch {
            errorMessage = error.localizedDescription
        }
    }

    /// 加载当前景区下门店列表。
    func loadStoreList(api: any ProjectServing) async {
        guard projectType == 4, let scenicId = selectedScenicIds.first else { return }
        do {
            storeList = try await api.storeAll().list.filter { $0.scenicId == scenicId }
            if selectedStoreId == nil {
                selectedStoreId = storeList.first?.id
            }
        } catch {
            errorMessage = error.localizedDescription
        }
    }

    /// 切换店铺项目景区选择。
    func toggleScenic(_ id: Int) {
        if projectType == 4 {
            selectedScenicIds = [id]
        } else if selectedScenicIds.contains(id) {
            selectedScenicIds.remove(id)
        } else {
            selectedScenicIds.insert(id)
        }
    }

    /// 提交店铺项目。
    func submit(userId: Int, api: any ProjectServing, uploadService: any OSSUploadServing) async -> Bool {
        guard !submitting else { return false }
        guard userId > 0 else { return fail(.missingUser) }
        let normalizedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
        let normalizedDescription = descriptionText.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !normalizedName.isEmpty else { return fail(.missingName) }
        guard !normalizedDescription.isEmpty else { return fail(.missingDescription) }
        guard !selectedScenicIds.isEmpty else { return fail(.missingScenic) }
        guard Double(projectType == 4 ? priceDeposit : projectPrice) ?? 0 > 0 else { return fail(.missingPrice) }
        if projectType == 4, selectedStoreId == nil { return fail(.missingStore) }

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

        do {
            let scenicIdForUpload = selectedScenicIds.first ?? 0
            let coverURL = try await resolveCoverURL(scenicId: scenicIdForUpload, uploadService: uploadService)
            guard !coverURL.isEmpty else { throw ProjectEditorError.missingImage }
            let carouselURLs = try await resolveCarouselURLs(scenicId: scenicIdForUpload, uploadService: uploadService)
            if projectType == 4 {
                try await submitOfflineProject(
                    api: api,
                    name: normalizedName,
                    description: normalizedDescription,
                    coverURL: coverURL,
                    carouselURLs: carouselURLs
                )
            } else {
                try await submitMultiPointProject(
                    userId: userId,
                    api: api,
                    name: normalizedName,
                    description: normalizedDescription,
                    coverURL: coverURL,
                    carouselURLs: carouselURLs
                )
            }
            return true
        } catch {
            errorMessage = error.localizedDescription
            return false
        }
    }

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

    private func resolveCoverURL(scenicId: Int, uploadService: any OSSUploadServing) async throws -> String {
        if let coverImage {
            return try await uploadService.uploadProjectImage(data: coverImage.data, fileName: coverImage.fileName, scenicId: scenicId) { _ in }
        }
        return existingCoverURL
    }

    private func resolveCarouselURLs(scenicId: Int, uploadService: any OSSUploadServing) async throws -> [String] {
        var urls = existingCarouselURLs
        for image in carouselImages {
            let url = try await uploadService.uploadProjectImage(data: image.data, fileName: image.fileName, scenicId: scenicId) { _ in }
            urls.append(url)
        }
        return urls
    }

    private func submitMultiPointProject(
        userId: Int,
        api: any ProjectServing,
        name: String,
        description: String,
        coverURL: String,
        carouselURLs: [String]
    ) async throws {
        if let editID {
            try await api.storeManagerUpdate(
                StoreManagerUpdateRequest(
                    id: editID,
                    name: name,
                    type: 19,
                    description: description,
                    coverProject: coverURL,
                    coverCarousel: carouselURLs,
                    projectRule: nil,
                    scenicId: selectedScenicIds.sorted(),
                    settleSpotNum: max(Int(settleSpotNum) ?? 1, 1),
                    price: Double(projectPrice) ?? 0,
                    priceMaterial: Double(priceMaterial) ?? 0,
                    pricePhoto: Double(pricePhoto) ?? 0,
                    priceVideo: Double(priceVideo) ?? 0,
                    priceMaterialAll: nil,
                    packageList: nil,
                    userId: userId,
                    singleSpotMaterialNum: max(Int(materialNum) ?? 0, 0),
                    singleSpotPhotoNum: max(Int(photoNum) ?? 0, 0),
                    singleSpotVideoNum: max(Int(videoNum) ?? 0, 0)
                )
            )
        } else {
            try await api.storeManagerCreate(
                StoreManagerCreateRequest(
                    name: name,
                    storeId: nil,
                    type: 19,
                    description: description,
                    coverProject: coverURL,
                    coverCarousel: carouselURLs,
                    projectRule: nil,
                    scenicId: selectedScenicIds.sorted(),
                    settleSpotNum: max(Int(settleSpotNum) ?? 1, 1),
                    price: Double(projectPrice) ?? 0,
                    priceMaterial: Double(priceMaterial) ?? 0,
                    pricePhoto: Double(pricePhoto) ?? 0,
                    priceVideo: Double(priceVideo) ?? 0,
                    priceMaterialAll: nil,
                    packageList: nil,
                    userId: userId,
                    singleSpotMaterialNum: max(Int(materialNum) ?? 0, 0),
                    singleSpotPhotoNum: max(Int(photoNum) ?? 0, 0),
                    singleSpotVideoNum: max(Int(videoNum) ?? 0, 0)
                )
            )
        }
    }

    private func submitOfflineProject(api: any ProjectServing, name: String, description: String, coverURL: String, carouselURLs: [String]) async throws {
        let scenicId = selectedScenicIds.first ?? 0
        let storeId = selectedStoreId ?? 0
        if let editID {
            try await api.storeManagerOfflineUpdate(
                StoreManagerOfflineUpdateRequest(
                    id: editID,
                    name: name,
                    scenicId: scenicId,
                    description: description,
                    price: Double(priceDeposit) ?? 0,
                    coverProject: coverURL,
                    coverCarousel: carouselURLs,
                    storeId: storeId
                )
            )
        } else {
            try await api.storeManagerOfflineCreate(
                StoreManagerOfflineCreateRequest(
                    name: name,
                    scenicId: scenicId,
                    storeId: storeId,
                    description: description,
                    price: Double(priceDeposit) ?? 0,
                    coverProject: coverURL,
                    coverCarousel: carouselURLs
                )
            )
        }
    }

    private func fail(_ error: ProjectEditorError) -> Bool {
        errorMessage = error.localizedDescription
        return false
    }
}
