package globalservice

import (
	"context"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"io"

	"github.com/tidbcloud/observability/pkg/common/constants"
	"github.com/tidbcloud/observability/pkg/common/message"
	"github.com/tidbcloud/observability/pkg/common/structs"
	"github.com/tidbcloud/observability/pkg/common/utils"
	"github.com/tidbcloud/observability/pkg/metamanager/s3"
	"github.com/tidbcloud/observability/pkg/resourcemanager"
	batchv1 "k8s.io/api/batch/v1"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"sync"
)

type EvaluationHandler struct {
}

var handler *EvaluationHandler

var evaluationOnce sync.Once

// GetEvaluationHandler returns the singleton of EvaluationHandler.
func GetEvaluationHandler() *EvaluationHandler {
	evaluationOnce.Do(func() {
		if handler == nil {
			handler = new(EvaluationHandler)
		}
	})

	return handler
}

func (p *EvaluationHandler) CreateEvaluation(ctx context.Context, req message.CreateEvaluationReq) (resp message.CreateEvaluationResp, err error) {
	if len(req.Name) == 0 {
		req.Name = utils.ShortID()
	}
	evaluation := &structs.Evaluation{
		ID:   req.Name,
		User: req.User,
		Name: req.Name,

		Model:        req.Model,
		ModelVersion: req.ModelVersion,
		Dataset:      req.Dataset,
		Template:     req.Template,

		DatasetCategory: req.DatasetCategory,
		AddedQuestion:   req.AddedQuestion,
		StartDate:       req.StartDate,
		EndDate:         req.EndDate,

		ModelMetaFilePath:   fmt.Sprintf("s3://%s/%s", constants.EvaluationBucketName(), fmt.Sprintf(constants.ModelMetaFileKey, req.Model, req.ModelVersion)),
		TemplateFilePath:    fmt.Sprintf("s3://%s/%s", constants.EvaluationBucketName(), fmt.Sprintf(constants.TemplateFileKey, req.Template)),
		DatasetMetaFilePath: fmt.Sprintf("s3://%s/%s", constants.EvaluationBucketName(), fmt.Sprintf(constants.DatasetMetaFileKey, req.Dataset)),
		BenchmarkFilePath:   fmt.Sprintf("s3://%s/%s", constants.EvaluationBucketName(), fmt.Sprintf(constants.DatasetBenchmarkFileKey, req.Dataset)),
		TablesFilePath:      fmt.Sprintf("s3://%s/%s", constants.EvaluationBucketName(), fmt.Sprintf(constants.DatasetTablesFileKey, req.Dataset)),

		ResultFilePath: fmt.Sprintf("s3://%s/%s", constants.EvaluationBucketName(), fmt.Sprintf(constants.ResultFileKey, req.User, req.Name)),
	}

	err = s3.GetEvaluationMetaService().SaveMeta(ctx, *evaluation)
	if err != nil {
		return resp, err
	}

	job := buildJob(*evaluation)
	err = resourcemanager.GetJobReaderWriter().Create(ctx, job)
	if err != nil {
		return resp, err
	}

	resp.Evaluation = *evaluation

	return resp, nil
}

func buildJob(evaluation structs.Evaluation) *batchv1.Job {
	image := GetGlobalManager().GetGlobalMetaCache().EvaluationImage
	var token = false
	job := &batchv1.Job{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: constants.EvaluationNamespace,
			Name:      evaluation.User + "-" + evaluation.Name,
		},
		Spec: batchv1.JobSpec{
			TTLSecondsAfterFinished: &constants.EvaluationTTLSecondsAfterFinished,
			Completions:             &constants.EvaluationCompletions,
			BackoffLimit:            &constants.EvaluationBackoffLimit,
			Template: v1.PodTemplateSpec{
				Spec: v1.PodSpec{
					RestartPolicy:                v1.RestartPolicyNever,
					AutomountServiceAccountToken: &token,
					Containers: []v1.Container{
						{
							Image: image,
							Args: []string{
								"cmds",
								fmt.Sprintf("--evaluation_id=%s", evaluation.ID),
								fmt.Sprintf("--evaluation_name=%s", evaluation.Name),
								fmt.Sprintf("--model_meta=%s", evaluation.ModelMetaFilePath),
								fmt.Sprintf("--template=%s", evaluation.TemplateFilePath),
								fmt.Sprintf("--output=%s", evaluation.ResultFilePath),
								fmt.Sprintf("--dataset_meta=%s", evaluation.DatasetMetaFilePath),
							},
							Name: "evaluation",
						},
					},
					NodeSelector: map[string]string{
						"node_type": "worker",
					},
				},
			},
		},
	}

	for _, cate := range evaluation.DatasetCategory {
		switch cate {
		case constants.Benchmark:
			datasetArgs := []string{
				"dataset",
				fmt.Sprintf("--benchmark=%s", evaluation.BenchmarkFilePath),
				fmt.Sprintf("--spider.table=%s", evaluation.TablesFilePath),
				"--spider.etype=all",
			}
			job.Spec.Template.Spec.Containers[0].Args = append(job.Spec.Template.Spec.Containers[0].Args, datasetArgs...)

		case constants.Baseline:
			questionArgs := []string{
				"baseline",
				fmt.Sprintf("--start_date=%s", evaluation.StartDate),
				fmt.Sprintf("--end_date=%s", evaluation.EndDate),
			}
			job.Spec.Template.Spec.Containers[0].Args = append(job.Spec.Template.Spec.Containers[0].Args, questionArgs...)
		case constants.Question:
			questionArgs := []string{
				"question",
				fmt.Sprintf("--question=%s", evaluation.AddedQuestion),
			}
			job.Spec.Template.Spec.Containers[0].Args = append(job.Spec.Template.Spec.Containers[0].Args, questionArgs...)
		}
	}
	return job
}

func (p *EvaluationHandler) GetEvaluation(ctx context.Context, req message.GetEvaluationReq) (resp message.GetEvaluationResp, err error) {
	evaluation, err := s3.GetEvaluationMetaService().GetMeta(ctx, req.User, req.Name)
	if err != nil {
		return resp, err
	}

	job, err := resourcemanager.GetJobReaderWriter().Get(ctx, constants.EvaluationNamespace, req.User+"-"+req.Name)
	if err != nil {
		return
	}

	if job.Status.Succeeded > 0 {
		evaluation.Phase = constants.EvaluationDone
		result, resultSummary, resultError := s3.GetEvaluationMetaService().GetResult(ctx, req.User, req.Name)
		if resultError != nil {
			return resp, resultError
		} else {
			evaluation.Result = result
			if len(resultSummary) > 0 {
				summary := map[string]interface{}{}
				summaryErr := json.Unmarshal([]byte(resultSummary), &summary)
				if summaryErr == nil {
					evaluation.ResultSummary = summary
				}
			}
		}
	} else if job.Status.Failed > 0 {
		evaluation.Phase = constants.EvaluationFailed
	} else {
		evaluation.Phase = constants.EvaluationEvaluating
	}

	resp.Evaluation = evaluation

	return resp, nil
}

func (p *EvaluationHandler) QueryEvaluations(ctx context.Context, req message.QueryEvaluationReq) (resp message.QueryClustersResp, err error) {
	// todo
	return resp, nil
}

func (p *EvaluationHandler) QueryDataset(ctx context.Context, req message.QueryDatasetReq) (resp message.QueryDatasetResp, err error) {
	output, err := s3.GetEvaluationMetaService().GetDatasetRecords(ctx, req.Dataset, req.Date)
	if err != nil {
		return
	}
	for _, o := range output {
		if o.Body != nil {
			reader := csv.NewReader(o.Body)
			title, err := reader.Read()
			if err != nil {
				return resp, err
			}
			indexMap := getIndex(title)
			records := make([][]string, 0)
			for {
				row, err := reader.Read()
				if err != nil && err != io.EOF {
					return resp, err
				}
				if err == io.EOF {
					break
				}
				records = append(records, row)
			}

			if len(req.ResultType) > 0 {
				records = recordsFilter(records, indexMap, "result_type", req.ResultType)
			}
			if len(req.Status) > 0 {
				records = recordsFilter(records, indexMap, "status", req.Status)
			}
			if len(req.FeedbackType) > 0 {
				records = recordsFilter(records, indexMap, "feedback_type", req.FeedbackType)
			}
			for _, row := range records {
				record := structs.DatasetRecord{
					QuestionID:      row[indexMap["question_id"]],
					UserID:          row[indexMap["user_id"]],
					Title:           row[indexMap["title"]],
					QuerySql:        row[indexMap["query_sql"]],
					Result:          row[indexMap["result"]],
					Status:          row[indexMap["status"]],
					FeedbackType:    row[indexMap["feedback_type"]],
					FeedbackContent: row[indexMap["feedback_content"]],
					CreatedAt:       row[indexMap["created_at"]],
				}
				resp.Records = append(resp.Records, record)
			}
		}
	}
	return resp, nil
}

func getIndex(title []string) map[string]int {
	indexMap := make(map[string]int)
	for i, j := range title {
		indexMap[j] = i
	}
	return indexMap
}

func recordsFilter(records [][]string, indexMap map[string]int, filter string, filterValue []string) [][]string {
	result := make([][]string, 0)
	for _, r := range records {
		if len(filterValue) > 0 {
			for _, v := range filterValue {
				if r[indexMap[filter]] == v {
					result = append(result, r)
				}
			}
		} else {
			result = append(result, r)
		}
	}
	return result
}
