package store

import (
	"encoding/json"
	"fmt"
	"net/http"
	"sync"
	"time"

	"github.com/doug-martin/goqu/v9"
	"github.com/doug-martin/goqu/v9/exp"
	"golang.org/x/sync/errgroup"

	"github.com/pingcap/test-infra/services/tcms/service"
	"github.com/pingcap/test-infra/services/tcms/utils/timing"

	"github.com/pingcap/test-infra/services/tcms/service/common"
	serror "github.com/pingcap/test-infra/services/tcms/service/common/errors"

	"github.com/pingcap/test-infra/common/model"
	respM "github.com/pingcap/test-infra/common/model/tcms/response"
	"github.com/pingcap/test-infra/services/tcms/db"
	"github.com/pingcap/test-infra/services/tcms/utils"
	"github.com/pingcap/test-infra/services/tcms/utils/sql"
)

type summaryService struct {
	db          *sql.Database
	triageSvc   service.TriageService
	caseSvc     service.CaseService
	caseExecSvc service.CaseExecutionService
	planExecSvc service.PlanExecutionService
	triggerSvc  service.TriggerService
}

func (s *summaryService) init(
	db *sql.Database,
	triageSvc service.TriageService,
	caseSvc service.CaseService,
	caseExecSvc service.CaseExecutionService,
	planExecSvc service.PlanExecutionService,
	triggerSvc service.TriggerService,
) {
	s.db = db
	s.triageSvc = triageSvc
	s.caseSvc = caseSvc
	s.caseExecSvc = caseExecSvc
	s.planExecSvc = planExecSvc
	s.triggerSvc = triggerSvc
}

type listExecitionsOpts struct {
	OnlyCount                  bool
	LastStatusSuccess          bool
	Status                     []model.ExecutionStatus
	FailReason                 *model.ExecFailReason
	FromStartedAt, ToStartedAt *int64
}

func (s *summaryService) listPlanExecutions(ctx utils.Context, opts listExecitionsOpts) ([]*model.ID, int, error) {
	c := ctx.Request().Context()

	where := []exp.Expression{}
	if len(opts.Status) > 0 {
		where = append(where, goqu.I("plan_executions.status").In(opts.Status))
	}
	subqueryCronTrigger := sql.Builder.
		Select("triggers.id").
		From("triggers").
		Where(goqu.L("plan_executions.plan_id=triggers.plan_id AND triggers.type='" + string(model.TriggerTypeCron) + "'"))
	if opts.LastStatusSuccess {
		subqueryCronExecHistory := sql.Builder.
			Select("started_at", "plan_id", "status").
			From(goqu.T("plan_executions").As("pexec")).
			Where(goqu.Func("EXISTS", subqueryCronTrigger))
		subqueryLastExec := sql.Builder.
			Select(goqu.MAX("started_at")).
			From(subqueryCronExecHistory).
			Where(goqu.Ex{
				"started_at": goqu.Op{"lt": goqu.I("plan_executions.started_at")},
			})
		subqueryLastStatusSuccess := sql.Builder.
			Select("id").
			From(subqueryCronExecHistory).
			Where(
				goqu.Ex{
					"started_at": subqueryLastExec,
					"status":     model.ExecutionStatusSuccess,
				})
		where = append(where, goqu.Func("EXISTS", subqueryLastStatusSuccess))
	} else {
		where = append(where, goqu.Func("EXISTS", subqueryCronTrigger))
	}

	if opts.FromStartedAt != nil {
		where = append(where, goqu.I("plan_executions.started_at").Gte(opts.FromStartedAt))
	}
	if opts.ToStartedAt != nil {
		where = append(where, goqu.I("plan_executions.started_at").Lt(opts.ToStartedAt))
	}
	b := sql.Builder.From(goqu.T("plan_executions")).Where(where...).Prepared(true)

	var total int
	query, args, _ := b.Select(goqu.COUNT("*")).ToSQL()
	if err := s.db.GetContext(c, &total, query, args...); err != nil {
		ctx.Log().Errorf(err, "select total failed")
		return nil, 0, err
	}
	if opts.OnlyCount {
		return nil, total, nil
	}

	var planExecIDs []*model.ID
	query, args, _ = b.Select("plan_executions.id").
		Order(goqu.I("created_at").Desc(), goqu.I("id").Asc()).
		ToSQL()
	if err := s.db.SelectContext(c, &planExecIDs, query, args...); err != nil {
		ctx.Log().Errorf(err, "select plan-execution failed")
		return nil, 0, err
	}
	return planExecIDs, total, nil
}

func (s *summaryService) listCaseExecutions(ctx utils.Context, opts listExecitionsOpts) ([]*model.ID, int, error) {
	c := ctx.Request().Context()

	where := []exp.Expression{}
	if len(opts.Status) > 0 {
		where = append(where, goqu.I("case_executions.status").In(opts.Status))
	}
	if opts.FailReason != nil {
		where = append(where, goqu.I("case_executions.reason").Eq(opts.FailReason))
	}
	subqueryCronTrigger := sql.Builder.
		Select("triggers.id").
		Distinct().
		From("triggers").
		Where(goqu.L("triggers.plan_id=(SELECT plan_id from plan_executions where id=case_executions.root_plan_exec_id) AND triggers.type='" + string(model.TriggerTypeCron) + "'"))
	if opts.LastStatusSuccess {
		subqueryCronExecHistory := sql.Builder.
			Select("started_at", "case_id", "status").
			From(goqu.T("case_executions").As("cexec")).
			Where(goqu.Func("EXISTS", subqueryCronTrigger))
		subqueryLastExec := sql.Builder.
			Select(goqu.MAX("started_at")).
			From(subqueryCronExecHistory).
			Where(goqu.Ex{
				"started_at": goqu.Op{"lt": goqu.I("case_executions.started_at")},
				"case_id":    goqu.I("case_executions.case_id"),
			})
		subqueryLastStatusSuccess := sql.Builder.
			Select("id").
			From(subqueryCronExecHistory).
			Where(
				goqu.Ex{
					"started_at": subqueryLastExec,
					"status":     "SUCCESS",
					"case_id":    goqu.I("case_executions.case_id"),
				})
		where = append(where, goqu.Func("EXISTS", subqueryLastStatusSuccess))
	} else {
		where = append(where, goqu.Func("EXISTS", subqueryCronTrigger))
	}
	if opts.FromStartedAt != nil {
		where = append(where, goqu.I("case_executions.started_at").Gte(opts.FromStartedAt))
	}
	if opts.ToStartedAt != nil {
		where = append(where, goqu.I("case_executions.started_at").Lt(opts.ToStartedAt))
	}
	b := sql.Builder.From(goqu.L("case_executions")).Where(where...).Prepared(true)

	var total int
	query, args, _ := b.Select(goqu.COUNT("*")).ToSQL()
	if err := s.db.GetContext(c, &total, query, args...); err != nil {
		ctx.Log().Errorf(err, "select total failed")
		return nil, 0, err
	}

	if opts.OnlyCount {
		return nil, total, nil
	}

	var caseExecIDs []*model.ID
	query, args, _ = b.Select("case_executions.id").
		Order(goqu.I("created_at").Desc(), goqu.I("id").Asc()).
		ToSQL()
	if err := s.db.SelectContext(c, &caseExecIDs, query, args...); err != nil {
		ctx.Log().Errorf(err, "select case-execution failed")
		return nil, 0, err
	}
	return caseExecIDs, total, nil
}

func (s *summaryService) DoDailySummary(ctx utils.Context, timeFromStartedAt time.Time, durationToStartedAt time.Duration) (*model.DailySummary, error) {
	dailysum := &model.DailySummary{}
	dailysum.PlanSummary.StatusSummary = make(map[model.ExecutionStatus]int)
	dailysum.PlanSummary.FailReasonSummary = make(map[model.ExecFailReason]int)
	dailysum.CaseSummary.StatusSummary = make(map[model.ExecutionStatus]int)
	dailysum.CaseSummary.FailReasonSummary = make(map[model.ExecFailReason]int)

	timestampFromStartedAt := timeFromStartedAt.Unix()
	timestampToStartedAt := timeFromStartedAt.Add(durationToStartedAt).Unix()

	for _, status := range []model.ExecutionStatus{
		model.ExecutionStatusPending,
		model.ExecutionStatusRunning,
		model.ExecutionStatusSuccess,
		model.ExecutionStatusFailure,
		model.ExecutionStatusSkipped,
		model.ExecutionStatusTimeout,
		model.ExecutionStatusCancelled,
		model.ExecutionStatusCancelling,
		model.ExecutionStatusError,
	} {
		planOpts := listExecitionsOpts{
			OnlyCount:     true,
			FromStartedAt: &timestampFromStartedAt,
			ToStartedAt:   &timestampToStartedAt,
			Status:        []model.ExecutionStatus{status},
		}
		_, plantotal, err := s.listPlanExecutions(ctx, planOpts)
		if err != nil {
			return nil, err
		}
		dailysum.PlanSummary.StatusSummary[status] = plantotal

		caseOpts := listExecitionsOpts{
			OnlyCount:     true,
			FromStartedAt: &timestampFromStartedAt,
			ToStartedAt:   &timestampToStartedAt,
			Status:        []model.ExecutionStatus{status},
		}
		_, casetotal, err := s.listCaseExecutions(ctx, caseOpts)
		if err != nil {
			return nil, err
		}
		dailysum.CaseSummary.StatusSummary[status] = casetotal
	}

	for _, failreason := range model.ReasonList {
		planOpts := listExecitionsOpts{
			OnlyCount:     true,
			FromStartedAt: &timestampFromStartedAt,
			ToStartedAt:   &timestampToStartedAt,
			Status: []model.ExecutionStatus{
				model.ExecutionStatusFailure,
				model.ExecutionStatusSkipped,
				model.ExecutionStatusTimeout,
				model.ExecutionStatusCancelled,
				model.ExecutionStatusCancelling,
				model.ExecutionStatusError,
			},
			FailReason: &failreason,
		}
		_, plantotal, err := s.listPlanExecutions(ctx, planOpts)
		if err != nil {
			return nil, err
		}
		dailysum.PlanSummary.FailReasonSummary[failreason] = plantotal

		caseOpts := listExecitionsOpts{
			OnlyCount:     true,
			FromStartedAt: &timestampFromStartedAt,
			ToStartedAt:   &timestampToStartedAt,
			Status: []model.ExecutionStatus{
				model.ExecutionStatusFailure,
				model.ExecutionStatusSkipped,
				model.ExecutionStatusTimeout,
				model.ExecutionStatusCancelled,
				model.ExecutionStatusCancelling,
				model.ExecutionStatusError,
			},
			FailReason: &failreason,
		}
		_, casetotal, err := s.listCaseExecutions(ctx, caseOpts)
		if err != nil {
			return nil, err
		}
		dailysum.CaseSummary.FailReasonSummary[failreason] = casetotal
	}

	{
		// calc FailIncreased
		planOpts := listExecitionsOpts{
			LastStatusSuccess: true,
			FromStartedAt:     &timestampFromStartedAt,
			ToStartedAt:       &timestampToStartedAt,
			Status:            []model.ExecutionStatus{model.ExecutionStatusFailure},
		}
		planExecIDs, plantotal, err := s.listPlanExecutions(ctx, planOpts)
		if err != nil {
			return nil, err
		}
		dailysum.PlanSummary.FailureExecIDIncreased = make([]model.ID, plantotal)
		for k, term := range planExecIDs {
			dailysum.PlanSummary.FailureExecIDIncreased[k] = *term
		}

		caseOpts := listExecitionsOpts{
			LastStatusSuccess: true,
			FromStartedAt:     &timestampFromStartedAt,
			ToStartedAt:       &timestampToStartedAt,
			Status:            []model.ExecutionStatus{model.ExecutionStatusFailure},
		}
		caseExecIDs, casetotal, err := s.listCaseExecutions(ctx, caseOpts)
		if err != nil {
			return nil, err
		}
		dailysum.CaseSummary.FailureExecIDIncreased = make([]model.ID, casetotal)
		for k, term := range caseExecIDs {
			dailysum.CaseSummary.FailureExecIDIncreased[k] = *term
		}
	}
	dailysum.Date = timeFromStartedAt.Format("2006-01-02")

	if err := s.update(ctx, dailysum); err != nil {
		return dailysum, err
	}

	return dailysum, nil
}

func (s *summaryService) GetDailySummary(ctx utils.Context, date string) (*model.DailySummary, error) {
	log := ctx.Log().With("dailySummaryDate", date)

	dailySum := &db.DailySummary{}
	err := s.db.Get(dailySum, "SELECT * FROM daily_summaries WHERE date=?", date)
	if err != nil {
		if sql.IsErrNoRows(err) {
			return nil, nil
		}
		log.Errorf(err, "GetCaseExecution failed")
		return nil, err
	}
	dailySummary := &model.DailySummary{}
	if err = json.Unmarshal([]byte(*dailySum.Summary), dailySummary); err != nil {
		return nil, err
	}
	return dailySummary, nil
}

func (s *summaryService) update(ctx utils.Context, dailySummary *model.DailySummary) error {
	_, err := s.db.Exec(`INSERT INTO daily_summaries (date, summary) 
		VALUES (?, ?) ON DUPLICATE KEY UPDATE summary=VALUES(summary)`,
		dailySummary.Date,
		utils.ToJSON(dailySummary),
	)
	if err != nil {
		return err
	}
	return nil
}

func (s *summaryService) GetPlanExecSummary(ctx utils.Context, planExecID model.ID) (*respM.GetPlanExecSummaryResponse, error) {
	g := errgroup.Group{}

	var caseExecTotal int
	var caseExecFailedCnt int
	var planTriage *model.Triage
	var planExec *model.TestPlanExecution
	var caseTriageSummaries []*respM.CaseTriageSummary

	g.Go(func() (err error) {
		planTriages, err := s.triageSvc.ListPlanTriages(ctx,
			common.PlanTriageQueryOpts{
				PlanExecIDs: []model.ID{planExecID},
			})
		if err != nil {
			return err
		} else if len(planTriages) != 1 {
			return fmt.Errorf("len(PlanTriages) expected 1 but get %d", len(planTriages))
		}
		planTriage = planTriages[0]
		return nil
	})

	g.Go(func() (err error) {
		planExec, err = s.planExecSvc.Get(ctx, planExecID)
		return err
	})

	g.Go(func() error {
		defer timing.DB(ctx.Request().Context(), "case-exec: get total").End()
		if err := s.db.GetContext(
			ctx.Request().Context(),
			&caseExecTotal,
			`SELECT COUNT(*) FROM case_executions WHERE root_plan_exec_id=?`,
			planExecID,
		); err != nil {
			ctx.Log().Errorf(err, "select total failed")
			return err
		}
		return nil
	})

	g.Go(func() error {
		defer timing.DB(ctx.Request().Context(), "plan-exec: get total").End()
		if err := s.db.GetContext(
			ctx.Request().Context(),
			&caseExecFailedCnt,
			`SELECT COUNT(*) FROM case_executions WHERE root_plan_exec_id=? AND status IN (?, ?)`,
			planExecID, model.ExecutionStatusFailure, model.ExecutionStatusError); err != nil {
			ctx.Log().Errorf(err, "select caseExecFailedCnt failed")
			return err
		}
		return nil
	})

	if err := g.Wait(); err != nil {
		ctx.Log().Errorf(err, "failed to get plan execution summary")
		return nil, err
	}

	var lock = sync.RWMutex{}

	for _, item := range append(planTriage.TriagedCaseExecIDs, planTriage.ToTriageCaseExecIDs...) {
		localItem := item
		g.Go(func() (err error) {
			caseExec, err := s.caseExecSvc.Get(ctx, localItem)
			if err != nil {
				return err
			}
			case_, err := s.caseSvc.Get(ctx, caseExec.CaseIDAndName.ID, nil)
			if err != nil {
				return err
			}
			triage, err := s.triageSvc.GetCaseTriage(ctx, caseExec.ID)
			if err != nil {
				return err
			}
			lock.Lock()
			caseTriageSummaries = append(caseTriageSummaries, &respM.CaseTriageSummary{
				Triage:    triage,
				Case:      *case_,
				Execution: *caseExec,
			})
			lock.Unlock()
			return err
		})
	}

	if err := g.Wait(); err != nil {
		ctx.Log().Errorf(err, "failed to get case triage summaries")
	}

	return &respM.GetPlanExecSummaryResponse{
		ID:                  planExecID,
		CaseExecTotal:       caseExecTotal,
		CaseExecFailureCnt:  caseExecFailedCnt,
		PlanExec:            planExec,
		PlanTriage:          planTriage,
		CaseTriageSummaries: caseTriageSummaries,
	}, nil
}

func (s *summaryService) GetReleaseSummary(ctx utils.Context, releaseTriggerID model.ID, triagePhase *model.TriagePhase) (*respM.GetReleaseSummaryResponse, error) {
	g := errgroup.Group{}
	var planExectotal, planExecFailureCnt, planExecErrorCnt int
	var failedPlanExecs []*model.TestPlanExecution
	var errorPlanExecs []*model.TestPlanExecution

	g.Go(func() (err error) {
		trigger, err := s.triggerSvc.Get(ctx, releaseTriggerID)
		if err != nil {
			return err
		} else if trigger.Type != model.TriggerTypeRelease {
			return serror.New(http.StatusBadRequest, fmt.Errorf("this is not release trigger"))
		}
		return nil
	})

	g.Go(func() (err error) {
		failedPlanExecs, planExecFailureCnt, err = s.planExecSvc.List(ctx,
			common.PlanExecQueryOpts{
				TriggerIDs:  []model.ID{releaseTriggerID},
				Status:      []model.ExecutionStatus{model.ExecutionStatusFailure},
				TriagePhase: triagePhase,
				Count:       999,
			})
		return err
	})

	g.Go(func() (err error) {
		errorPlanExecs, planExecErrorCnt, err = s.planExecSvc.List(ctx,
			common.PlanExecQueryOpts{
				TriggerIDs:  []model.ID{releaseTriggerID},
				Status:      []model.ExecutionStatus{model.ExecutionStatusError},
				TriagePhase: triagePhase,
				Count:       999,
			})
		return err
	})

	g.Go(func() (err error) {
		_, planExectotal, err = s.planExecSvc.List(ctx,
			common.PlanExecQueryOpts{
				TriggerIDs:  []model.ID{releaseTriggerID},
				TriagePhase: triagePhase,
				Count:       999,
			})
		return err
	})

	if err := g.Wait(); err != nil {
		ctx.Log().Errorf(err, "failed to get trigger/plan executions")
	}

	var caseExectotal, caseExecFailureCnt int
	var lock = sync.RWMutex{}
	var planExecsFailureSummary []*respM.GetPlanExecSummaryResponse
	var planExecsErrorSummary []*respM.GetPlanExecSummaryResponse

	for _, item := range failedPlanExecs {
		localItem := item
		g.Go(func() error {
			tmpPlanExecSummary, err := s.GetPlanExecSummary(ctx, localItem.ID)
			if err != nil {
				return err
			}
			lock.Lock()
			planExecsFailureSummary = append(planExecsFailureSummary, tmpPlanExecSummary)
			caseExectotal += tmpPlanExecSummary.CaseExecTotal
			caseExecFailureCnt += tmpPlanExecSummary.CaseExecFailureCnt
			lock.Unlock()
			return nil
		})
	}

	for _, item := range errorPlanExecs {
		localItem := item
		g.Go(func() error {
			tmpPlanExecSummary, err := s.GetPlanExecSummary(ctx, localItem.ID)
			if err != nil {
				return err
			}
			lock.Lock()
			planExecsErrorSummary = append(planExecsErrorSummary, tmpPlanExecSummary)
			caseExectotal += tmpPlanExecSummary.CaseExecTotal
			caseExecFailureCnt += tmpPlanExecSummary.CaseExecFailureCnt
			lock.Unlock()
			return nil
		})
	}

	if err := g.Wait(); err != nil {
		ctx.Log().Errorf(err, "failed to get case executions by trigger id")
	}

	return &respM.GetReleaseSummaryResponse{
		PlanExecTotal:          planExectotal,
		PlanExecFailureCnt:     planExecFailureCnt,
		PlanExecErrorCnt:       planExecErrorCnt,
		CaseExecTotal:          caseExectotal,
		CaseExecFailureCnt:     caseExecFailureCnt,
		FailedPlanExecsSummary: planExecsFailureSummary,
		ErrorPlanExecsSummary:  planExecsErrorSummary,
	}, nil
}
