package cache

import (
	"fmt"

	gocache "github.com/patrickmn/go-cache"
	"github.com/pingcap/log"
	"go.uber.org/zap"

	_const "github.com/pingcap-inc/clinic/pkg/const"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/db/model"
)

type ITaskStatusCache interface {
	Init(id taskID)
	TotalIncr(id taskID, size int64)
	Done(id taskID, size int64)
	Remove(id taskID)
	Processed(id taskID) (int, bool)
}

var TaskStatusCache = newTaskStatusCache()

type taskStatusCache struct {
	cache *gocache.Cache
}

func newTaskStatusCache() *taskStatusCache {
	return &taskStatusCache{
		cache: gocache.New(gocache.NoExpiration, gocache.NoExpiration),
	}
}

func (tsc *taskStatusCache) Init(id taskID) {
	err := model.UpsertTask(&db.Task{
		TaskID:   string(id),
		Progress: 0,
	})
	if err != nil {
		log.Error("failed to upsert task progress", zap.String("taskID", string(id)))
	}

	tsc.cache.Set(string(id), int64(0), gocache.NoExpiration)
	tsc.cache.Set(taskIDTotalKey(id), int64(0), gocache.NoExpiration)
}

func (tsc *taskStatusCache) TotalIncr(id taskID, size int64) {
	if _, err := tsc.cache.IncrementInt64(taskIDTotalKey(id), size); err != nil {
		log.Warn("incr total failed", zap.String("id", string(id)), zap.Error(err))
	}
}

func (tsc *taskStatusCache) Done(id taskID, size int64) {
	oldProgress, _ := tsc.Processed(id)
	if _, err := tsc.cache.IncrementInt64(string(id), size); err != nil {
		log.Warn("incr task size failed", zap.String("id", string(id)), zap.Error(err))
	}
	newProgress, _ := tsc.Processed(id)
	if newProgress != oldProgress {
		err := model.UpdateTaskProgress(string(id), newProgress)
		if err != nil {
			log.Error("failed to update task progress", zap.String("taskID", string(id)))
		}
	}
}

func (tsc *taskStatusCache) Remove(id taskID) {
	tsc.cache.Delete(string(id))
	tsc.cache.Delete(taskIDTotalKey(id))
	err := model.DeleteTask(string(id))
	if err != nil {
		log.Error("failed to delete task progress", zap.String("taskID", string(id)))
	}
}

func (tsc *taskStatusCache) Processed(id taskID) (int, bool) {
	totalSize, ok := tsc.cache.Get(taskIDTotalKey(id))
	if !ok {
		return 0, false
	}

	total := getIntValue(totalSize)
	if total == 0 {
		log.Warn("empty total size", zap.String("task", string(id)))
		return 0, true
	}

	done, ok := tsc.cache.Get(string(id))
	if !ok {
		return 0, false
	}

	processed := float64(getIntValue(done)) / float64(total)

	r := int(processed*100) + 1
	if r >= 100 {
		return 100, true
	}

	return r, true
}

func getIntValue(v any) int64 {
	vv, ok := v.(int64)
	if !ok {
		return 0
	}

	return vv
}

func taskIDTotalKey(id taskID) string {
	return fmt.Sprintf("%v-total", id)
}

type taskID string

func TaskID(fileUUID string, dataType _const.TaskType) taskID {
	return taskID(fmt.Sprintf("%s-%d", fileUUID, dataType))
}
