//
//  PunchPointViewModels.swift
//  suixinkan
//

import CoreLocation
import Foundation

/// 打卡点图片上传协议，便于表单 ViewModel 注入替身。
@MainActor
protocol PunchPointImageUploading {
    /// 上传打卡点图片并返回 OSS URL。
    func uploadPunchPointImage(
        data: Data,
        fileName: String,
        scenicId: Int,
        onProgress: @escaping (Int) -> Void
    ) async throws -> String
}

/// 打卡点列表页 ViewModel，负责筛选、分页、二维码与删除。
final class PunchPointListViewModel {
    private(set) var items: [PunchPointItem] = []
    private(set) var filterType: PunchPointFilterType = .all
    private(set) var isLoading = false
    private(set) var isRefreshing = false
    private(set) var canLoadMore = false

    var onStateChange: (() -> Void)?
    var onShowMessage: ((String) -> Void)?
    var onShowQRCode: ((String) -> Void)?
    var onBeginDeleteAnimation: ((Int64) -> Void)?

    private var currentPage = 1
    private var totalCount = 0
    private let pageSize = 10
    private let currentScenicIdProvider: () -> Int
    private var pendingDeleteIDs = Set<Int64>()

    /// 初始化打卡点列表 ViewModel。
    init(currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId }) {
        self.currentScenicIdProvider = currentScenicIdProvider
    }

    /// 首次加载。
    func loadInitial(api: any PunchPointAPIProtocol) async {
        await load(reset: true, showLoading: true, api: api)
    }

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

    /// 选择筛选项并刷新第一页。
    func selectFilter(_ type: PunchPointFilterType, api: any PunchPointAPIProtocol) async {
        guard filterType != type else { return }
        filterType = type
        notifyStateChange()
        await load(reset: true, showLoading: false, api: api)
    }

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

    /// 展示二维码。
    func showQRCode(for item: PunchPointItem) {
        guard item.status == 1 || item.status == 2 else {
            onShowMessage?("未审核通过不可查看二维码")
            return
        }
        guard let url = item.mpQrcode?.trimmingCharacters(in: .whitespacesAndNewlines), !url.isEmpty else {
            onShowMessage?("暂无二维码")
            return
        }
        onShowQRCode?(url)
    }

    /// 删除打卡点。
    func delete(item: PunchPointItem, api: any PunchPointAPIProtocol) async {
        guard !pendingDeleteIDs.contains(item.id) else { return }
        do {
            try await api.delete(id: item.id)
            pendingDeleteIDs.insert(item.id)
            onBeginDeleteAnimation?(item.id)
        } catch is CancellationError {
            return
        } catch {
            onShowMessage?(error.localizedDescription.isEmpty ? "删除失败" : "删除失败:\(error.localizedDescription)")
        }
    }

    /// 删除动画完成后，从列表数据中正式移除条目。
    func completeDeleteAnimation(id: Int64) {
        guard pendingDeleteIDs.remove(id) != nil else { return }
        items.removeAll { $0.id == id }
        totalCount = max(0, totalCount - 1)
        canLoadMore = items.count < totalCount
        notifyStateChange()
    }

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

        let scenicId = currentScenicIdProvider()
        guard scenicId > 0 else {
            onShowMessage?("请先选择景区")
            if reset {
                items = []
                canLoadMore = false
                totalCount = 0
            }
            notifyStateChange()
            return
        }

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

        do {
            let response = try await api.list(
                scenicAreaId: String(scenicId),
                status: filterType.apiStatus,
                page: currentPage,
                pageSize: pageSize
            )
            totalCount = response.total
            items = reset ? response.list : items + response.list
            canLoadMore = response.list.count >= pageSize && items.count < totalCount
        } catch is CancellationError {
            return
        } catch {
            if !reset, currentPage > 1 {
                currentPage -= 1
            }
            if reset {
                items = []
                totalCount = 0
                canLoadMore = false
            }
            onShowMessage?(error.localizedDescription.isEmpty ? "获取打卡点列表失败" : "获取打卡点列表失败:\(error.localizedDescription)")
        }
    }

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

/// 打卡点详情页 ViewModel，负责详情加载。
final class PunchPointDetailViewModel {
    private(set) var detail: PunchPointDetail?
    private(set) var isLoading = false

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

    let punchPointId: Int64

    /// 初始化详情 ViewModel。
    init(punchPointId: Int64) {
        self.punchPointId = punchPointId
    }

    /// 加载详情。
    func load(api: any PunchPointAPIProtocol) async {
        guard punchPointId > 0 else {
            onShowMessage?("打卡点信息异常")
            return
        }
        isLoading = true
        notifyStateChange()
        defer {
            isLoading = false
            notifyStateChange()
        }
        do {
            detail = try await api.info(id: punchPointId)
        } catch is CancellationError {
            return
        } catch {
            onShowMessage?(error.localizedDescription.isEmpty ? "获取打卡点详情失败" : error.localizedDescription)
        }
    }

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

/// 打卡点新建与编辑页 ViewModel，负责表单、定位、图片上传和提交。
final class PunchPointFormViewModel {
    /// 表单模式。
    enum Mode: Equatable {
        case create
        case edit(id: Int64)
    }

    private(set) var name = ""
    private(set) var address = ""
    private(set) var description = ""
    private(set) var coordinate: CLLocationCoordinate2D?
    private(set) var images: [PunchPointImageState] = []
    private(set) var uploadDialogState: PunchPointUploadDialogState?
    private(set) var isSubmitting = false
    private(set) var isLocating = false

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

    let mode: Mode
    private let currentScenicIdProvider: () -> Int
    private let locationProvider: any LocationProviding

    /// 初始化打卡点表单 ViewModel。
    init(
        mode: Mode = .create,
        currentScenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId },
        locationProvider: any LocationProviding = LocationProvider.shared
    ) {
        self.mode = mode
        self.currentScenicIdProvider = currentScenicIdProvider
        self.locationProvider = locationProvider
    }

    /// 坐标展示文本。
    var coordinatesText: String {
        guard let coordinate else { return "" }
        return "\(coordinate.latitude),\(coordinate.longitude)"
    }

    /// 使用详情初始化编辑表单。
    func initialize(with detail: PunchPointDetail) {
        name = detail.name
        address = detail.displayAddress
        description = detail.description ?? ""
        coordinate = detail.coordinate
        images = detail.guideImages.map(PunchPointImageState.remote(url:))
        if let coordinate {
            onCoordinateChange?(coordinate)
        }
        notifyStateChange()
    }

    /// 更新名称。
    func updateName(_ text: String) {
        name = text
        notifyStateChange()
    }

    /// 更新地址。
    func updateAddress(_ text: String) {
        address = text
        notifyStateChange()
    }

    /// 更新描述，最多 50 字。
    func updateDescription(_ text: String) {
        description = String(text.prefix(50))
        notifyStateChange()
    }

    /// 自动定位当前坐标并回填地址。
    func autoLocation() async {
        await locate(updateMap: true, missingPermissionMessage: "定位权限未授予，无法自动定位")
    }

    /// 点击定位按钮。
    func locateToCurrent() async {
        await locate(updateMap: true, missingPermissionMessage: "定位权限未授予，请在设置中开启")
    }

    /// 地图点击选择坐标。
    func selectCoordinate(_ coordinate: CLLocationCoordinate2D) async {
        self.coordinate = coordinate
        notifyStateChange()
        onCoordinateChange?(coordinate)
        let resolved = await locationProvider.reverseGeocode(
            latitude: coordinate.latitude,
            longitude: coordinate.longitude
        )
        if !resolved.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            address = resolved
            notifyStateChange()
        }
    }

    /// 添加本地图片并立即顺序上传。
    func addLocalImages(_ newImages: [PunchPointImageState], uploader: any PunchPointImageUploading) async {
        let remaining = 9 - images.count
        guard remaining > 0 else {
            onShowMessage?("最多只能选择9张图片")
            return
        }
        let accepted = Array(newImages.prefix(remaining))
        guard !accepted.isEmpty else { return }
        let start = images.count
        images.append(contentsOf: accepted)
        notifyStateChange()
        for offset in accepted.indices {
            await uploadImage(at: start + offset, title: "正在上传(\(offset + 1)/\(accepted.count))", uploader: uploader)
        }
        uploadDialogState = nil
        notifyStateChange()
    }

    /// 删除图片。
    func deleteImage(at index: Int) {
        guard images.indices.contains(index), !images[index].isUploading else { return }
        images.remove(at: index)
        notifyStateChange()
    }

    /// 重新上传失败图片。
    func retryUpload(at index: Int, uploader: any PunchPointImageUploading) async {
        guard images.indices.contains(index) else { return }
        images[index].errorMessage = nil
        notifyStateChange()
        await uploadImage(at: index, title: "正在上传(1/1)", uploader: uploader)
        uploadDialogState = nil
        notifyStateChange()
    }

    /// 提交审核。
    func submit(api: any PunchPointAPIProtocol) async {
        guard !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
            onShowMessage?("请输入打卡点名称")
            return
        }
        guard let coordinate else {
            onShowMessage?("请选择打卡点坐标")
            return
        }
        guard !address.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
            onShowMessage?("请输入打卡点地址")
            return
        }
        guard !images.isEmpty else {
            onShowMessage?("请上传至少一张图片")
            return
        }
        guard !images.contains(where: { $0.isUploading }) else {
            onShowMessage?("图片正在上传，请稍后提交")
            return
        }
        guard !images.contains(where: { !$0.isUploaded }) else {
            onShowMessage?("请先完成图片上传")
            return
        }
        let scenicId = currentScenicIdProvider()
        guard scenicId > 0 else {
            onShowMessage?("请先选择景区")
            return
        }

        let request = PunchPointSaveRequest(
            id: editId,
            scenicAreaId: String(scenicId),
            name: name,
            description: description.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : description,
            region: PunchPointSaveRegion(
                lot: coordinate.longitude,
                lat: coordinate.latitude,
                address: address
            ),
            scenicSpotString: address,
            guideImages: images.map(\.uploadedURL)
        )

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

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

    private func locate(updateMap: Bool, missingPermissionMessage: String) async {
        isLocating = true
        notifyStateChange()
        defer {
            isLocating = false
            notifyStateChange()
        }
        do {
            let snapshot = try await locationProvider.requestSnapshot()
            let coordinate = CLLocationCoordinate2D(latitude: snapshot.latitude, longitude: snapshot.longitude)
            self.coordinate = coordinate
            if !snapshot.address.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
                address = snapshot.address
            }
            if updateMap {
                onCoordinateChange?(coordinate)
            }
        } catch LocationProviderError.permissionDenied {
            onShowMessage?(missingPermissionMessage)
        } catch is CancellationError {
            return
        } catch {
            onShowMessage?(error.localizedDescription.isEmpty ? "定位失败" : error.localizedDescription)
        }
    }

    private func uploadImage(at index: Int, title: String, uploader: any PunchPointImageUploading) async {
        guard images.indices.contains(index) else { return }
        let scenicId = currentScenicIdProvider()
        guard scenicId > 0 else {
            onShowMessage?("请先选择景区")
            return
        }
        guard !images[index].isUploaded else { return }
        images[index].isUploading = true
        images[index].uploadProgress = 0
        images[index].errorMessage = nil
        uploadDialogState = PunchPointUploadDialogState(title: title, progress: 0)
        notifyStateChange()

        do {
            let item = images[index]
            let url = try await uploader.uploadPunchPointImage(
                data: item.data,
                fileName: item.fileName,
                scenicId: scenicId,
                onProgress: { [weak self] progress in
                    guard let self else { return }
                    Task { @MainActor in
                        guard self.images.indices.contains(index) else { return }
                        self.images[index].uploadProgress = progress
                        self.images[index].isUploading = progress < 100
                        self.uploadDialogState = PunchPointUploadDialogState(title: title, progress: progress)
                        self.notifyStateChange()
                    }
                }
            )
            guard images.indices.contains(index) else { return }
            images[index].uploadedURL = url
            images[index].previewURL = url
            images[index].isUploading = false
            images[index].uploadProgress = 100
        } catch is CancellationError {
            return
        } catch {
            guard images.indices.contains(index) else { return }
            images[index].isUploading = false
            images[index].uploadProgress = 0
            images[index].errorMessage = error.localizedDescription.isEmpty ? "上传失败" : error.localizedDescription
            onShowMessage?(error.localizedDescription.isEmpty ? "图片上传失败，请重试" : error.localizedDescription)
        }
    }

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