package service

import (
	"fmt"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"go.uber.org/zap"

	"github.com/pingcap-inc/clinic/pkg/api/common"
	apierr "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/cache"
	"github.com/pingcap-inc/clinic/pkg/config"
	_const "github.com/pingcap-inc/clinic/pkg/const"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/db/model"
	"github.com/pingcap-inc/clinic/pkg/stream"
	_import "github.com/pingcap-inc/clinic/pkg/task_manager/task/import"
)

type TaskManagerService struct {
	cfg     *config.Config
	scanner *_import.ScannerService
}

func NewTaskManagerService(cfg *config.Config, scanner *_import.ScannerService) *TaskManagerService {
	return &TaskManagerService{
		cfg:     cfg,
		scanner: scanner,
	}
}

// rid
func (s *TaskManagerService) ReportStatus(c *gin.Context) {
	// todo@toto fetch rebuild status and report status
	uuid := c.Param("id")
	if uuid == "" {
		_ = c.Error(apierr.ErrBadRequest.New("id is empty"))
		return
	}
	report, err := model.GetHealthReportByID(uuid)
	if err != nil {
		log.Error("get health report failed", zap.Error(err))
		_ = c.Error(apierr.ErrNotFound.New("not found the item"))
	}

	reportTaskStatus := common.TaskStatus{
		ItemID:   uuid,
		TaskType: int(_const.ReportTask),
	}
	switch report.Status {
	case string(db.StatusPending):
		reportTaskStatus.Status = 0
	case string(db.StatusRunning):
		reportTaskStatus.Status = 50
	case string(db.StatusSuccess):
		reportTaskStatus.Status = 100
	}

	// FastPath: if the report has org type set to `cloud`, no need to check the rebuild status for metrics and log
	if report.OrgType == "cloud" {
		c.JSON(http.StatusOK, common.TaskStatusResp{
			Items: []common.TaskStatus{
				reportTaskStatus,
			},
		})
		return
	}

	// SlowPath: if the report has org type is not `cloud`, we need to check the rebuild status for metrics
	file, err := model.QueryUploadFile(uuid)
	if err != nil {
		log.Error("query upload file failed", zap.String("file", c.Param("id")), zap.Error(err))
		_ = c.Error(apierr.ErrInternal.New("query upload file failed"))
		return
	}

	if file == nil {
		_ = c.Error(apierr.ErrNotFound.New("not found related upload file"))
		return
	}

	c.JSON(http.StatusOK, common.TaskStatusResp{
		Items: []common.TaskStatus{
			getTaskStatus(file, _const.MetricTask),
			getTaskStatus(file, _const.LogTask),
			reportTaskStatus,
		},
	})
}

func (s *TaskManagerService) TouchComparison(c *gin.Context) {
	// todo@toto
	// fetch rebuild and report
	var val common.Comparison
	if err := c.BindJSON(&val); err != nil {
		_ = c.Error(apierr.ErrBadRequest.New("bad parameter"))
		return
	}
	fileUUIDs := append(val.Args.PackageID1, val.Args.PackageID2...)
	ufs, err := ValidateAndTrans(fileUUIDs...)
	if err != nil {
		_ = c.Error(apierr.ErrBadRequest.New("%s", err.Error()))
		return
	}
	hashKey, err := common.Hash(val.Args)
	if err != nil {
		_ = c.Error(apierr.ErrInternal.New("%s", err.Error()))
		return
	}
	cr := &db.ComparisonReport{
		ClusterID:     val.ClusterID,
		OrgID:         val.OrgID,
		OrgType:       val.OrgType,
		Hash:          hashKey,
		ComparisonArg: &val.Args,
		Creator:       val.Creator,
	}
	id, err := model.LandingComparisonTask(ufs, cr, ConvertToDBType(_const.AllTask))
	if err != nil {
		log.Error("landing comparison failed", zap.Error(err))
		_ = c.Error(apierr.ErrInternal.New("%s", err.Error()))
		return
	}
	c.JSON(http.StatusAccepted, common.TaskReponse{
		ID: id,
	})
}

// add health report
func (s *TaskManagerService) Rebuild(c *gin.Context) {
	var val common.Rebuild // datatype item
	if err := c.BindJSON(&val); err != nil {
		_ = c.Error(apierr.ErrBadRequest.New("body is required"))
		return
	}
	log.Info("rebuild", zap.Any("body", val))
	apiType := common.APIDataType(val.DataType)
	dtype := common.ConverToTaskType(apiType)
	if !dtype.Validate() {
		_ = c.Error(apierr.ErrBadRequest.New("bad parameter"))
		return
	}
	// validate parameter
	if val.ItemID == "" {
		_ = c.Error(apierr.ErrBadRequest.New("itemID is empty"))
		return
	}
	// FastPath: if the caller set DataType to ReportTask, we can skip all resource rebuild step
	// and land the health report task directly
	if dtype == _const.ReportTask {
		if err := model.LandingHealthReportTask(
			val.ItemID,
			val.HealthReportStartUnix,
			val.HealthReportEndUnix,
			val.OrgType,
			val.OrgID,
			val.ClusterID); err != nil {
			log.Error("landing health report task failed", zap.String("uuid", val.ItemID), zap.Error(err))
			_ = c.Error(apierr.ErrInternal.New("landing health report task failed"))
			return
		}
		c.JSON(http.StatusAccepted, common.SuccessResponse())
		return
	}
	// SlowPath: may need to rebuild metrics, log ahead.
	uf, err := model.QueryUploadFile(val.ItemID)
	if err != nil {
		log.Error("error on query file", zap.String("uuid", val.ItemID), zap.Error(err))
		_ = c.Error(apierr.ErrInternal.New("query file failed"))
		return
	}
	if uf == nil {
		_ = c.Error(apierr.ErrBadRequest.New("file is not existed"))
		return
	}

	tt, err := model.LandingTask(uf, ConvertToDBType(dtype), val.HealthReport, val.OrgType)
	if err != nil {
		_ = c.Error(apierr.ErrInternal.New("set db status failed"))
		return
	}
	// todo@toto skip success rebuild and touch a health report
	if err := s.scanner.WaitRebuild(&_import.ImporterEvent{File: uf, Task: tt}); err != nil {
		log.Error("register job failed", zap.Error(err))
		_ = c.Error(apierr.ErrInternal.New("%s", err.Error()))
		return
	}
	c.JSON(http.StatusAccepted, common.SuccessResponse())
}

func (s *TaskManagerService) PackageStatus(c *gin.Context) {
	file, err := model.QueryUploadFile(c.Param("id"))
	if err != nil {
		log.Error("query upload file failed", zap.String("file", c.Param("id")), zap.Error(err))
		_ = c.Error(apierr.ErrInternal.New("query cluster status failed"))
		return
	}

	if file == nil {
		_ = c.Error(apierr.ErrNotFound.New("not found the item"))
		return
	}

	c.JSON(http.StatusOK, common.TaskStatusResp{
		Items: []common.TaskStatus{
			getTaskStatus(file, _const.MetricTask),
			getTaskStatus(file, _const.LogTask),
		},
	})
}

func (s *TaskManagerService) TaskStatus(c *gin.Context) {
	param := &GetTaskStatusParam{}
	if err := param.Init(c); err != nil {
		_ = c.Error(err)
		return
	}
	if err := param.validate(s.cfg.GetAllowMaxDataRange()); err != nil {
		_ = c.Error(err)
		return
	}

	files, _, err := model.ListUploadFiles(uint(param.OrgID), param.ClusterID, 0, 0, param.StartTime, param.EndTime)
	if err != nil {
		log.Error("query files failed", zap.String("cluster", c.Query("cid")), zap.Error(err))
		_ = c.Error(apierr.ErrInternal.New("query cluster status failed"))
		return
	}

	results := make([]common.TaskStatus, 0)

	stream.OfSlice(files).Map(func(f db.UploadFile) common.TaskStatus {
		return getTaskStatus(&f, param.TaskType)
	}).CollectEmptySlice(&results)

	c.JSON(http.StatusOK, common.TaskStatusResp{
		Items: results,
	})
}

func getTaskStatus(f *db.UploadFile, dtype _const.TaskType) common.TaskStatus {
	status := common.TaskStatus{
		ItemID:    f.FileUUID,
		StartTime: getTime(f.BeginTime),
		EndTime:   getTime(f.EndTime),
		TaskType:  int(dtype),
	}

	// check status from db first, then calculate the processed percentage from cache if db status is pending.
	statusFromDB, realStatus := convertDBStatus(dtype, f)
	// if task status is `success`, `failed` or `unscheduled`, not need to check status in cache.
	if realStatus == db.StatusSuccess || realStatus == db.StatusUnscheduled || realStatus == db.StatusFailed {
		status.Status = statusFromDB
		return status
	}
	// calculate processed percentage from cache and fallback to `statusFromDB` if `statusFromCache` is not ok.
	t, err := model.GetTask(string(cache.TaskID(f.FileUUID, dtype)))
	if err != nil {
		log.Error("cannot get task progress from db", zap.String("taskID", string(cache.TaskID(f.FileUUID, dtype))))
		status.Status = statusFromDB
	} else {
		status.Status = t.Progress
	}
	return status
}

func getTime(t *time.Time) int64 {
	if t == nil {
		return 0
	}
	return t.Unix()
}

func convertDBStatus(dtype _const.TaskType, file *db.UploadFile) (int, db.StatusType) {
	status := common.GetTaskStatusByDataType(dtype, file)
	switch status {
	case db.StatusSuccess:
		return 100, status
	case db.StatusPending:
		return 0, status
	case db.StatusRunning:
		return 1, status
	case db.StatusFailed:
		return -2, status
	default:
		return -1, status
	}
}

/*
	func filtersJob(fileuuid string, rebuildType _const.TaskType) ([]string, error) {
		task, err := model.SetStatus(fileuuid, ConvertToDBType(rebuildType))
		if err != nil {
			return nil, err
		}
		return task, nil
	}
*/
type rebuildField string

var (
	rebuildMetricsField rebuildField = "rebuildMetric"
	rebuildLogField     rebuildField = "rebuildLog"
	rebuildConfField    rebuildField = "rebuildConfiguration"
)

func ConvertToDBType(dt _const.TaskType) map[string]struct{} {
	switch dt {
	case _const.MetricTask:
		return map[string]struct{}{string(rebuildMetricsField): {}}
	case _const.LogTask:
		return map[string]struct{}{string(rebuildLogField): {}}
	case _const.RebuildConfTask:
		return map[string]struct{}{string(rebuildConfField): {}}
	case _const.AllTask:
		return map[string]struct{}{string(rebuildMetricsField): {}, string(rebuildLogField): {}, string(rebuildConfField): {}}
	case _const.MetricsAndConfTask:
		return map[string]struct{}{string(rebuildMetricsField): {}, string(rebuildConfField): {}}
	}
	return map[string]struct{}{}
}

func ValidateAndTrans(packages ...string) ([]*db.UploadFile, error) {
	ufs := make([]*db.UploadFile, 0)
	for _, p := range packages {
		if p == "" {
			return nil, fmt.Errorf("have empty itemID")
		}
		uf, err := model.QueryUploadFile(p)
		if err != nil {
			return nil, err
		}
		ufs = append(ufs, uf)
	}
	return ufs, nil
}
