//
//  OrderLongTailViewModels.swift
//  suixinkan
//
//  Created by Codex on 2026/6/25.
//

import Foundation
import Combine

@MainActor
/// 押金订单列表 ViewModel，负责分页、核销、退款和操作后刷新。
final class DepositOrderListViewModel: ObservableObject {
    @Published private(set) var orders: [DepositOrderListItem] = []
    @Published private(set) var total = 0
    @Published private(set) var page = 1
    @Published private(set) var hasMore = false
    @Published private(set) var loading = false
    @Published private(set) var loadingMore = false
    @Published private(set) var operatingOrderNumber: String?
    @Published var errorMessage: String?

    private let pageSize = 10

    /// 加载押金订单列表，缺少景区时清空旧数据且不请求接口。
    func reload(api: OrderServing, scenicId: Int?, reset: Bool) async {
        guard let scenicId, scenicId > 0 else {
            resetState()
            return
        }

        if reset {
            page = 1
            total = 0
            orders = []
            loading = true
        } else {
            guard !loadingMore, hasMore else { return }
            loadingMore = true
        }

        defer {
            loading = false
            loadingMore = false
        }

        do {
            let result = try await api.depositOrderList(scenicId: scenicId, page: page, pageSize: pageSize)
            total = result.total
            if page == 1 {
                orders = result.list
            } else {
                orders.append(contentsOf: result.list)
            }
            page += 1
            hasMore = orders.count < result.total && !result.list.isEmpty
            errorMessage = nil
        } catch {
            if reset {
                orders = []
                total = 0
                page = 1
                hasMore = false
            }
            errorMessage = error.localizedDescription
        }
    }

    /// 核销押金订单，成功后刷新第一页。
    func writeOff(api: OrderServing, scenicId: Int?, orderNumber: String) async -> Bool {
        let normalized = orderNumber.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !normalized.isEmpty, operatingOrderNumber == nil else { return false }
        guard let scenicId, scenicId > 0 else {
            errorMessage = "缺少景区上下文"
            return false
        }

        operatingOrderNumber = normalized
        defer { operatingOrderNumber = nil }

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

    /// 申请押金订单退款，原因必填，成功后刷新第一页。
    func refund(api: OrderServing, scenicId: Int?, orderNumber: String, reason: String) async -> Bool {
        let normalizedOrderNumber = orderNumber.trimmingCharacters(in: .whitespacesAndNewlines)
        let normalizedReason = reason.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !normalizedOrderNumber.isEmpty, operatingOrderNumber == nil else { return false }
        guard !normalizedReason.isEmpty else {
            errorMessage = "请填写退款原因"
            return false
        }
        guard let scenicId, scenicId > 0 else {
            errorMessage = "缺少景区上下文"
            return false
        }

        operatingOrderNumber = normalizedOrderNumber
        defer { operatingOrderNumber = nil }

        do {
            try await api.depositOrderRefund(orderNumber: normalizedOrderNumber, refundReason: normalizedReason)
            await reload(api: api, scenicId: scenicId, reset: true)
            return true
        } catch {
            errorMessage = error.localizedDescription
            return false
        }
    }

    /// 清理错误文案。
    func clearError() {
        errorMessage = nil
    }

    private func resetState() {
        orders = []
        total = 0
        page = 1
        hasMore = false
        loading = false
        loadingMore = false
        operatingOrderNumber = nil
        errorMessage = nil
    }
}

@MainActor
/// 押金订单详情 ViewModel，负责按门店和订单号加载详情。
final class DepositOrderDetailViewModel: ObservableObject {
    @Published private(set) var detail: StoreOrderDetailResponse?
    @Published private(set) var loading = false
    @Published var errorMessage: String?

    /// 加载押金订单详情，缺少门店或订单号时不请求接口。
    func load(api: OrderServing, storeId: Int?, orderNumber: String) async {
        let normalized = orderNumber.trimmingCharacters(in: .whitespacesAndNewlines)
        guard let storeId, storeId > 0 else {
            reset(message: "当前账号缺少门店信息")
            return
        }
        guard !normalized.isEmpty else {
            reset(message: "订单号不能为空")
            return
        }

        detail = nil
        loading = true
        defer { loading = false }

        do {
            detail = try await api.storeOrderDetail(storeId: storeId, orderNumber: normalized)
            errorMessage = nil
        } catch {
            detail = nil
            errorMessage = error.localizedDescription
        }
    }

    /// 清理错误文案。
    func clearError() {
        errorMessage = nil
    }

    private func reset(message: String) {
        detail = nil
        loading = false
        errorMessage = message
    }
}

@MainActor
/// 押金拍摄信息 ViewModel，负责加载单个打卡点的媒体和评分。
final class DepositOrderShootingInfoViewModel: ObservableObject {
    @Published private(set) var detail: StoreOrderShootingDetailResponse?
    @Published private(set) var loading = false
    @Published var errorMessage: String?

    /// 加载押金订单拍摄信息，缺少门店或订单号时不请求接口。
    func load(api: OrderServing, storeId: Int?, orderNumber: String, scenicSpotId: Int, photogUid: Int) async {
        let normalized = orderNumber.trimmingCharacters(in: .whitespacesAndNewlines)
        guard let storeId, storeId > 0 else {
            reset(message: "当前账号缺少门店信息")
            return
        }
        guard !normalized.isEmpty else {
            reset(message: "订单号不能为空")
            return
        }

        detail = nil
        loading = true
        defer { loading = false }

        do {
            detail = try await api.storeOrderShootingDetail(
                storeId: storeId,
                orderNumber: normalized,
                scenicSpotId: scenicSpotId,
                photogUid: photogUid
            )
            errorMessage = nil
        } catch {
            detail = nil
            errorMessage = error.localizedDescription
        }
    }

    /// 清理错误文案。
    func clearError() {
        errorMessage = nil
    }

    private func reset(message: String) {
        detail = nil
        loading = false
        errorMessage = message
    }
}

@MainActor
/// 普通订单退款 ViewModel，负责退款入口判断、金额校验和提交保护。
final class OrderRefundViewModel: ObservableObject {
    @Published var mode: OrderRefundMode = .full
    @Published var amount = ""
    @Published var reason = ""
    @Published private(set) var submitting = false
    @Published var errorMessage: String?

    /// 判断订单是否允许展示普通退款入口。
    func canRefund(_ item: OrderEntity) -> Bool {
        (item.orderStatus == 18 || item.orderStatus == 30) &&
            item.orderType != 19 &&
            availableAmount(for: item) > 0
    }

    /// 打开退款表单时初始化默认值。
    func begin(item: OrderEntity) {
        mode = .full
        amount = availableAmountText(for: item)
        reason = ""
        errorMessage = nil
    }

    /// 提交普通订单退款。
    func submit(api: OrderServing, item: OrderEntity) async -> Bool {
        guard !submitting else { return false }
        guard canRefund(item) else {
            errorMessage = "当前订单不可退款"
            return false
        }
        let normalizedReason = reason.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !normalizedReason.isEmpty else {
            errorMessage = "请输入退款原因"
            return false
        }

        let refundAmount: String
        switch mode {
        case .full:
            refundAmount = availableAmountText(for: item)
        case .partial:
            guard let normalized = Self.normalizedMoney(amount) else {
                errorMessage = "请输入有效的退款金额"
                return false
            }
            guard (Decimal(string: normalized) ?? 0) <= availableAmount(for: item) else {
                errorMessage = "退款金额不能大于可退金额"
                return false
            }
            refundAmount = normalized
        }

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

        do {
            try await api.orderRefund(
                orderNumber: item.orderNumber,
                refundType: mode,
                refundAmount: refundAmount,
                refundReason: normalizedReason
            )
            return true
        } catch {
            errorMessage = error.localizedDescription
            return false
        }
    }

    /// 计算普通订单可退金额。
    func availableAmount(for item: OrderEntity) -> Decimal {
        let paid = Self.decimalValue(item.actualPayAmount)
        if paid > 0 { return paid }
        return Self.decimalValue(item.actualRefundAmount.isEmpty ? item.refundAmount : item.actualRefundAmount)
    }

    /// 返回可退金额展示和提交文案。
    func availableAmountText(for item: OrderEntity) -> String {
        Self.moneyText(availableAmount(for: item))
    }

    /// 将输入金额规范成两位小数，空值、非法值和超过两位小数返回 nil。
    static func normalizedMoney(_ rawValue: String) -> String? {
        let text = rawValue.trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(of: ",", with: "")
        guard !text.isEmpty else { return nil }
        let pattern = #"^\d+(\.\d{1,2})?$"#
        guard text.range(of: pattern, options: .regularExpression) != nil,
              let value = Decimal(string: text),
              value > 0 else {
            return nil
        }
        return moneyText(value)
    }

    private static func decimalValue(_ text: String) -> Decimal {
        Decimal(string: text.trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(of: ",", with: "")) ?? 0
    }

    private static func moneyText(_ value: Decimal) -> String {
        let number = NSDecimalNumber(decimal: value)
        let handler = NSDecimalNumberHandler(
            roundingMode: .plain,
            scale: 2,
            raiseOnExactness: false,
            raiseOnOverflow: false,
            raiseOnUnderflow: false,
            raiseOnDivideByZero: false
        )
        return String(format: "%.2f", number.rounding(accordingToBehavior: handler).doubleValue)
    }
}

@MainActor
/// 历史拍摄 ViewModel，负责按订单号加载多点位历史拍摄媒体。
final class HistoricalShootingInfoViewModel: ObservableObject {
    @Published private(set) var projectName = ""
    @Published private(set) var projectTypeName = ""
    @Published private(set) var spots: [PhotogSpotItem] = []
    @Published private(set) var loading = false
    @Published var errorMessage: String?

    /// 加载历史拍摄信息，订单号为空时清空旧数据且不请求接口。
    func load(api: OrderServing, orderNumber: String) async {
        let normalized = orderNumber.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !normalized.isEmpty else {
            reset(message: "订单号不能为空")
            return
        }

        loading = true
        defer { loading = false }

        do {
            let result = try await api.multiTravelShootHistory(orderNumber: normalized)
            projectName = result.projectName
            projectTypeName = result.projectTypeName
            spots = result.photogSpotList
            errorMessage = nil
        } catch {
            reset(message: error.localizedDescription)
        }
    }

    /// 清理错误文案。
    func clearError() {
        errorMessage = nil
    }

    private func reset(message: String) {
        projectName = ""
        projectTypeName = ""
        spots = []
        loading = false
        errorMessage = message
    }
}

@MainActor
/// 多点旅拍任务上传 ViewModel，负责订单号、打卡点、云盘附件、本地附件和素材提交。
final class MultiTravelTaskUploadViewModel: ObservableObject {
    @Published var orderNumber = ""
    @Published var selectedSpotId: Int?
    @Published private(set) var spots: [MultiTravelVerifiedScenicSpotItem] = []
    @Published var selectedCloudFiles: [TaskCloudSelectionItem] = []
    @Published var selectedLocalFiles: [TaskLocalUploadItem] = []
    @Published private(set) var isLoadingSpots = false
    @Published private(set) var isSubmitting = false
    @Published private(set) var didSubmitSuccessfully = false
    @Published var errorMessage: String?

    @Published private var loadedSpotOrderNumber = ""

    /// 当前选中打卡点展示名。
    var selectedSpotName: String {
        spots.first(where: { $0.id == selectedSpotId })?.name ?? "请选择打卡点"
    }

    /// 判断提交按钮是否满足基础启用条件。
    var canSubmit: Bool {
        !isSubmitting &&
            selectedSpotId != nil &&
            (!selectedCloudFiles.isEmpty || !selectedLocalFiles.isEmpty) &&
            !selectedLocalFiles.contains(where: \.isUploading)
    }

    /// 初始化任务上传表单订单号。
    init(initialOrderNumber: String = "") {
        orderNumber = initialOrderNumber
    }

    /// 加载当前订单已核销打卡点，订单号为空时不请求接口。
    func loadSpots(api: OrderServing) async {
        let normalized = orderNumber.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !normalized.isEmpty else {
            resetSpotSelection()
            return
        }

        isLoadingSpots = true
        defer { isLoadingSpots = false }

        do {
            let result = try await api.multiTravelVerifiedScenicSpotList(orderNumber: normalized)
            spots = result
            loadedSpotOrderNumber = normalized
            if !spots.contains(where: { $0.id == selectedSpotId }) {
                selectedSpotId = spots.first?.id
            }
            errorMessage = nil
        } catch {
            resetSpotSelection()
            errorMessage = error.localizedDescription
        }
    }

    /// 切换选中的打卡点。
    func selectSpot(id: Int?) {
        selectedSpotId = id
    }

    /// 合并云盘附件，按文件 ID 去重。
    func mergeCloudFiles(_ files: [TaskCloudSelectionItem]) {
        var next = selectedCloudFiles
        for file in files where !next.contains(where: { $0.id == file.id }) {
            next.append(file)
        }
        selectedCloudFiles = next
    }

    /// 移除指定云盘附件。
    func removeCloudFile(id: Int) {
        selectedCloudFiles.removeAll { $0.id == id }
    }

    /// 添加本地图片或视频附件。
    func addLocalFile(data: Data, fileName: String) {
        selectedLocalFiles.append(
            TaskLocalUploadItem(
                id: UUID(),
                data: data,
                fileName: fileName,
                fileType: Self.fileType(for: fileName),
                remark: "",
                uploadedURL: nil,
                progress: 0,
                errorMessage: nil
            )
        )
    }

    /// 移除指定本地附件。
    func removeLocalFile(id: UUID) {
        selectedLocalFiles.removeAll { $0.id == id }
    }

    /// 提交多点旅拍任务素材；本地附件先上传 OSS，再提交最终 URL。
    @discardableResult
    func submit(api: OrderServing, uploadService: any OSSUploadServing, scenicId: Int?) async -> Bool {
        guard !isSubmitting else { return false }
        guard let requestContext = validateBeforeSubmit(scenicId: scenicId) else { return false }

        isSubmitting = true
        didSubmitSuccessfully = false
        errorMessage = nil
        defer { isSubmitting = false }

        do {
            let uploadFiles = try await uploadLocalFiles(uploadService: uploadService, scenicId: requestContext.scenicId)
            try await api.multiTravelUploadMaterial(
                MultiTravelUploadMaterialRequest(
                    orderNumber: requestContext.orderNumber,
                    scenicSpotId: requestContext.scenicSpotId,
                    cloudFile: selectedCloudFiles.map { MultiTravelCloudFileItem(fileId: $0.id) },
                    uploadFile: uploadFiles
                )
            )
            didSubmitSuccessfully = true
            return true
        } catch {
            errorMessage = error.localizedDescription
            return false
        }
    }

    /// 清理错误文案。
    func clearError() {
        errorMessage = nil
    }

    private func validateBeforeSubmit(scenicId: Int?) -> SubmitContext? {
        let normalizedOrderNumber = orderNumber.trimmingCharacters(in: .whitespacesAndNewlines)
        guard let scenicId, scenicId > 0 else {
            errorMessage = "请先选择景区"
            return nil
        }
        guard !normalizedOrderNumber.isEmpty else {
            errorMessage = "请先填写订单号"
            return nil
        }
        guard loadedSpotOrderNumber == normalizedOrderNumber else {
            errorMessage = "请先刷新打卡点"
            return nil
        }
        guard let selectedSpotId else {
            errorMessage = "请选择打卡点"
            return nil
        }
        guard !selectedCloudFiles.isEmpty || !selectedLocalFiles.isEmpty else {
            errorMessage = "请至少选择一个素材文件"
            return nil
        }
        guard !selectedLocalFiles.contains(where: \.isUploading) else {
            errorMessage = "文件上传中，请稍后提交"
            return nil
        }
        return SubmitContext(scenicId: scenicId, orderNumber: normalizedOrderNumber, scenicSpotId: selectedSpotId)
    }

    private func uploadLocalFiles(uploadService: any OSSUploadServing, scenicId: Int) async throws -> [MultiTravelUploadFileItem] {
        var uploadFiles: [MultiTravelUploadFileItem] = []
        for index in selectedLocalFiles.indices {
            if let uploadedURL = selectedLocalFiles[index].uploadedURL {
                uploadFiles.append(MultiTravelUploadFileItem(fileName: selectedLocalFiles[index].fileName, fileUrl: uploadedURL))
                continue
            }

            selectedLocalFiles[index].errorMessage = nil
            let fileID = selectedLocalFiles[index].id
            do {
                let url = try await uploadService.uploadTaskFile(
                    data: selectedLocalFiles[index].data,
                    fileName: selectedLocalFiles[index].fileName,
                    fileType: selectedLocalFiles[index].fileType,
                    scenicId: scenicId,
                    onProgress: { [weak self] progress in
                        Task { @MainActor in
                            self?.updateLocalFileProgress(id: fileID, progress: progress)
                        }
                    }
                )
                selectedLocalFiles[index].uploadedURL = url
                selectedLocalFiles[index].progress = 100
                uploadFiles.append(MultiTravelUploadFileItem(fileName: selectedLocalFiles[index].fileName, fileUrl: url))
            } catch {
                selectedLocalFiles[index].progress = 0
                selectedLocalFiles[index].errorMessage = error.localizedDescription
                throw error
            }
        }
        return uploadFiles
    }

    private func updateLocalFileProgress(id: UUID, progress: Int) {
        guard let index = selectedLocalFiles.firstIndex(where: { $0.id == id }) else { return }
        selectedLocalFiles[index].progress = progress
    }

    private func resetSpotSelection() {
        spots = []
        selectedSpotId = nil
        loadedSpotOrderNumber = ""
        isLoadingSpots = false
    }

    private static func fileType(for fileName: String) -> Int {
        let ext = URL(fileURLWithPath: fileName).pathExtension.lowercased()
        return ["mp4", "mov", "m4v", "avi"].contains(ext) ? 1 : 2
    }

    private struct SubmitContext {
        let scenicId: Int
        let orderNumber: String
        let scenicSpotId: Int
    }
}
