import {computed, ref, watch} from 'vue'
import {injectLocal, provideLocal} from "@vueuse/core";
import {fnPlaceholder} from "@/utils/uni-helper";
import _ from "lodash";
import {array, empty, number} from "@/uni_modules/uv-ui-tools/libs/function/test";

const key = Symbol('QUESTION_SERVICE')

// 从外部提供一个存储答题和提交的触发机制
// questionService依赖于paperService，主要用来分担一部分paperService的功能
// 与答案和打分相关的一些API集中在了这个服务
// 尽量保持只需要依靠paper/paper.questions中计算的相关API在paperService中
// 与userData计算相关的API集中的questionService中
export const useProvideQuestionService = function (
    paperService,
    chunkSize = 10,
    enableDuration = true,
    overrideService = {}) {
    const {
        allowAnswer,
        allowScore,
        index,
        questions,
        questionsMap,
        isCheckbox,
        commitQuestion,
        scoreQuestion,
        isObjective,
        isAnswerCorrect,
        goToQuestion,
        scorePaper
    } = paperService

    const container = ref({})
    const committing = ref([])
    const answerProps = ref(['answer', 'attachments', 'duration', 'questionId'])
    const scoreProps = ref(['answer', 'attachments', 'score', 'questionId']) // 这个answer/attachments是显示用的，不会提交
    const errorCount = ref(0)
    const chunk = computed(() => (errorCount.value + 1) * chunkSize)
    const timestampMap = new Map() // 另一个结构体存放时间，不污染container中的userData
    const errorLimit = 3 // 如果提交API连续超过3次失败，则不用再重试了

    const createUserDataFn = computed(() => {
        if (allowAnswer.value && allowScore.value)
            throw new Error('allowAnswer,allowScore can not be `true` at the same time!')
        if (!allowAnswer.value && !allowScore.value) return (q) => q // 原样返回，因为不会涉及到更改

        if (allowAnswer.value) return (question) => _.pick(question, answerProps.value)
        if (allowScore.value) return (question) => _.pick(question, scoreProps.value)
    })

    const getUserData = function (q) {
        let userData = container.value[q.questionId]
        if (userData) return userData
        userData = createUserDataFn.value(q)
        encodeToUserData(q, userData)
        container.value[q.questionId] = userData
        return userData
    }

    const cleanUserData = function () {
        container.value = {}
        committing.value = []
        timestampMap.clear()
    }

    const isAnswered = function (q) {
        const data = getUserData(q)
        return !empty(data.answer) || !empty(data.attachments)
    }

    const isScored = function (q) {
        const data = getUserData(q)
        return number(data.score)
    }

    const answeredPartition = computed(() => _.partition(questions.value, q => isAnswered(q)))
    const answeredQuestions = computed(() => answeredPartition.value[0])
    const unansweredQuestions = computed(() => answeredPartition.value[1])
    const isAllAnswered = computed(() => questions.value.length == answeredQuestions.value.length)

    const scoredPartition = computed(() => _.partition(questions.value, q => isScored(q)))
    const scoredQuestions = computed(() => scoredPartition.value[0])
    const unscoredQuestions = computed(() => scoredPartition.value[1])
    const isAllScored = computed(() => questions.value.length == scoredQuestions.value.length)

    const passedPartition = computed(() => _.partition(scoredQuestions.value, q => isPassed(q)))
    const passedQuestions = computed(() => passedPartition.value[0])
    const unpassedQuestions = computed(() => passedPartition.value[1])

    const isCompleteLocal = (q) =>
        (allowAnswer.value && isAnswered(q)) ||
        (allowScore.value && isScored(q))
    const isAllLocalComplete = computed(() =>
        (allowAnswer.value && isAllAnswered.value) ||
        (allowScore.value && isAllScored.value)
    )
    const allUncompleteLocalQuestions = computed(() => {
        if (allowAnswer.value) return unansweredQuestions.value
        if (allowScore.value) return unscoredQuestions.value
        return []
    })

    const isPassed = (q) => {
        const userData = getUserData(q)
        if (!number(userData.score)) return false
        return userData.score >= q.scoreTotal * 0.8
    }

    const encodeToUserData = (q, userData) => {
        // 与decodeFromUserData对应，有些字段，前后台需要转义
        // 多选兼容视图类型，要注意这里，转换answer是因为组件不支持string类型的传型，提交时会适配后台
        if (isCheckbox(q)) userData.answer = empty(userData.answer) ? [] : q.answer.split(',')
        // attachments做兼容性转换
        userData.attachments = empty(userData.attachments)
            ? []
            : array(userData.attachments)
                ? userData.attachments
                : q.attachments.split(',')
    }

    const decodeFromUserData = (userData) => {
        // 与encodeToUserData对应，提交或反向赋值时要使用
        if (allowAnswer.value) {
            return {
                ...userData,
                answer: userData.answer?.toString(), // 兼容多选
                attachments: userData.attachments // 图片提交时不用转
            }
        }
        if (allowScore.value) {
            const copy = {...userData}
            delete copy.answer // 打分时不需要提交答案
            delete copy.attachments
            return copy
        }
        return userData
    }

    /*@description 本地提交答案，达到chunkSize时自动向服务端同步
    * */
    const pushChunk = function (userData) {
        // 结算duration
        if (allowAnswer.value && enableDuration) {
            const duration = userData.duration || 0
            const diffSeconds = (new Date().getTime() - timestampMap.get(userData.questionId)) / 1000
            userData.duration = Math.round(duration + diffSeconds)
        }

        // chunk 机制只关心分段提交行为，并不关心提交的数据值
        // 因为userData在container中是唯一引用，所以用户的答题或者打分可能会反复保存
        // 但只要保持最后一次操作后，有提交行为，即可保证userData均与服务端同步了
        if (committing.value.includes(userData)) return
        committing.value.push(userData)

        // chunk = 0 不执行分段提交，要等forceChunk调用
        if (chunk.value
            && errorCount.value <= errorLimit
            && committing.value.length >= chunk.value) {
            return doChunk()
        }
    }

    const rollbackChunk = function (commits) {
        // 不触发，只是回加，doChunk等一下次自然机会
        _.remove(committing.value, item => commits.includes(item))
        committing.value.push(...commits)
    }

    /*
    * @description 同步本地数据到服务端
    * */
    const doChunk = function () {
        if (empty(committing.value)) return

        // prepare commit data, 需要兼容多选，所以重组了对象，后面使用的时候要小心引用变更
        const rawCommits = [...committing.value]
        const commits = rawCommits.map(decodeFromUserData)
        committing.value.length = 0

        const commitFn = allowAnswer.value
            ? commitQuestion
            : allowScore.value
                ? scoreQuestion
                : fnPlaceholder
        return commitFn(commits)
            .then(res => {
                errorCount.value = 0

                // sync props to realtime
                commits.forEach(userData => {
                    const raw = questionsMap.value[userData.questionId]
                    _.assign(raw, userData)
                })

            })
            .catch(e => {
                console.log('commitQuestion failed', e, rawCommits)
                errorCount.value += 1 // 扩容延缓提交

                // rollback to committing
                rollbackChunk(rawCommits)
                throw e // 原样抛出，使得外部调用await doChunk时能正常中断
            })
    }

    const getAllCommits = () => questions.value.map(q => userDataToCommit(getUserData(q)))

    // hooks
    // 完成答题计时功能
    // NOTE：如果页面上要显示计时，只需要在当前题上，从userData.duration按秒计数即可。
    watch([index, questions], ([newIndex], [oldIndex]) => {
        if (enableDuration && allowAnswer.value) { // 只有答题开放此功能
            if (oldIndex != undefined) {
                // 旧题清空时间戳
                const old = questions.value[oldIndex]
                if (old) timestampMap.delete(old.questionId)
            }
            // 新进入的题重新打时间戳
            // 如果在这期间，用户有pushChunk行为，则会结算duration
            const q = questions.value[newIndex]
            timestampMap.set(q.questionId, new Date().getTime())
        }
    })

    // 完成客观题自动评分
    // 如果全是客观题，直接自动完成阅卷
    watch([allowScore, questions], async () => {
        if (allowScore.value) {
            const targets = questions.value
                .filter(q => isObjective(q) && !isScored(q))
            if (targets.length) {
                // 提交所有客观题
                const commits = targets.map(q => {
                    const userData = getUserData(q)
                    userData.score = isAnswerCorrect(q) ? q.scoreTotal * 1 : 0
                    return userData
                })
                rollbackChunk(commits)
                await doChunk()
            }
            const nextTargets = unscoredQuestions.value
            if (nextTargets.length) {
                goToQuestion(_.first(nextTargets))
            } else if (isAllScored) {
                // 如果全是客观题，直接结束阅卷
                await scorePaper()
                index.value = 0 // 回到第1题
            }
        }
    })

    const options = {
        container,
        committing,
        createUserDataFn,
        cleanUserData,
        getUserData,
        getAllCommits,
        pushChunk,
        doChunk,
        isAnswered,
        isScored,
        isAllAnswered,
        isAllScored,
        isCompleteLocal,
        isAllLocalComplete,
        isPassed,
        answeredQuestions,
        unansweredQuestions,
        scoredQuestions,
        unscoredQuestions,
        passedQuestions,
        unpassedQuestions,
        allUncompleteLocalQuestions,
        encodeToUserData,
        decodeFromUserData,
        ...overrideService
    }
    provideLocal(key, options)
    return options
}

export const useInjectQuestionService = function () {
    return injectLocal(key)
}