package store

import (
	"net/http"

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

	"github.com/pingcap/test-infra/common/model"
	reqM "github.com/pingcap/test-infra/common/model/tcms/request"
	"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/services/tcms/utils"
	"github.com/pingcap/test-infra/services/tcms/utils/sql"
	"github.com/pingcap/test-infra/services/tcms/utils/timing"
)

const (
	TABLE_CASE_EXECUTIONS_COLUMNS = "case_executions.id,case_executions.case_id,case_executions.root_plan_id,case_executions.root_plan_exec_id,case_executions.step_name,case_executions.step_seq,case_executions.created_at,case_executions.updated_at,case_executions.started_at,case_executions.completed_at,case_executions.status,case_executions.output,case_executions.note,case_executions.reason,case_executions.meta,case_executions.argo_url"
)

func (s *caseExecutionService) init(db *sql.Database, planExecSvc *planExecutionService) {
	s.db = db
	s.planExecSvc = planExecSvc
}

type caseExecutionService struct {
	db          *sql.Database
	planExecSvc *planExecutionService
}

func (s *caseExecutionService) GetStatus(ctx utils.Context, id model.ID) (*model.ExecutionStatus, error) {
	var status model.ExecutionStatus
	err := s.db.Get(&status, "SELECT status FROM case_executions WHERE id=?", id)
	if err != nil {
		if sql.IsErrNoRows(err) {
			return nil, nil
		}
		ctx.Log().Errorf(err, "GetCaseExecutionStatus failed")
		return nil, err
	}

	return &status, nil
}

func (s *caseExecutionService) List(ctx utils.Context, opts common.CaseExecQueryOpts) ([]*model.TestCaseExecution, int, error) {
	const MaxOffset = 1000
	if opts.Offset > MaxOffset {
		return nil, 0, serror.ErrMaxOffset(int(opts.Offset), MaxOffset)
	}
	c := ctx.Request().Context()
	defer timing.DB(c, "case-exec:list").End()

	where := []exp.Expression{}
	if len(opts.IDs) > 0 {
		where = append(where, goqu.I("case_executions.id").In(opts.IDs))
	}
	if opts.RootPlanID != nil {
		where = append(where, goqu.I("case_executions.root_plan_id").Eq(opts.RootPlanID))
	}
	if opts.RootPlanExecID != nil {
		where = append(where, goqu.I("case_executions.root_plan_exec_id").Eq(opts.RootPlanExecID))
	}
	if len(opts.CaseIDs) > 0 {
		where = append(where, goqu.I("case_executions.case_id").In(opts.CaseIDs))
	}
	if len(opts.Status) > 0 {
		where = append(where, goqu.I("case_executions.status").In(opts.Status))
	}
	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)

	g := errgroup.Group{}

	// Get total count.
	var total int64
	b_ := b // prevent race
	g.Go(func() (err error) {
		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 err
		}
		return nil
	})

	// Get items.
	var items []*model.TestCaseExecution
	orderBy := goqu.I("case_executions.created_at").Desc()
	if opts.RootPlanExecID != nil {
		orderBy = goqu.I("case_executions.step_seq").Asc()
	}
	query, args, _ := b.Select(
		goqu.L(TABLE_CASE_EXECUTIONS_COLUMNS),
		goqu.L("COALESCE(cases.name, '') AS case_name"),
	).
		LeftJoin(goqu.T("cases"), goqu.On(goqu.I("cases.id").Eq(goqu.I("case_executions.case_id")))).
		Order(orderBy, goqu.I("case_executions.id").Asc()).
		Limit(uint(opts.Count)).
		Offset(uint(opts.Offset)).
		ToSQL()
	if err := s.db.SelectContext(c, &items, query, args...); err != nil {
		ctx.Log().Errorf(err, "select case-execution failed")
		return nil, 0, err
	}
	if err := g.Wait(); err != nil {
		return nil, 0, err
	}
	return items, int(total), nil
}

func (s *caseExecutionService) Get(ctx utils.Context, id model.ID) (*model.TestCaseExecution, error) {
	log := ctx.Log().With("caseExecutionID", id)
	defer timing.DB(ctx.Request().Context(), "case-exec:get").End()

	exec := &model.TestCaseExecution{}
	err := s.db.Get(exec, `
	SELECT 
		`+TABLE_CASE_EXECUTIONS_COLUMNS+`,
		COALESCE(cases.name, '') AS case_name
	FROM case_executions 
	LEFT JOIN cases ON cases.id=case_executions.case_id
	WHERE case_executions.id=?`,
		id,
	)
	if err != nil {
		if sql.IsErrNoRows(err) {
			return nil, nil
		}
		log.Errorf(err, "GetCaseExecution failed")
		return nil, err
	}
	return exec, nil
}

func (s *caseExecutionService) Create(ctx utils.Context, exec *reqM.CaseExecutionCreateRequest) (*model.TestCaseExecution, error) {
	log := ctx.Log()

	// root-plan-execution must exist
	rootPlanID, err := s.planExecSvc.getPlanID(ctx, exec.RootPlanExecID)
	if err != nil {
		log.Errorf(err, "check root-plan-execution exist failed")
		return nil, err
	} else if rootPlanID == nil {
		log.Warnf("root-plan-execution not found")
		return nil, serror.NewMsg(http.StatusBadRequest, "root-plan-execution not found")
	}

	var id int64
	r, err := s.db.Exec(`INSERT INTO case_executions (case_id, root_plan_id, root_plan_exec_id, step_name, step_seq, started_at, completed_at, status, output, note, meta, argo_url)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?, ?)`,
		exec.CaseID,
		*rootPlanID,
		exec.RootPlanExecID,
		exec.StepName,
		exec.StepSeq,
		exec.StartedAt,
		exec.CompletedAt,
		exec.Status,
		exec.Output,
		exec.Meta,
		exec.ArgoURL,
	)
	if err != nil {
		log.Errorf(err, "CreateCaseExecution failed")
		return nil, err
	}

	id, err = r.LastInsertId()
	if err != nil {
		log.Errorf(err, "get last insert id failed")
		return nil, err
	}

	return s.Get(ctx, model.ID(id))
}

func (s *caseExecutionService) Update(ctx utils.Context, id model.ID, exec *reqM.CaseExecutionUpdateRequest) (*model.TestCaseExecution, error) {
	log := ctx.Log().With("caseExecutionID", id)

	// can not update if the execution's status is already not PENDING or RUNNING
	oldStatus, err := s.GetStatus(ctx, id)
	if err != nil {
		return nil, err
	} else if oldStatus == nil {
		return nil, serror.NewMsg(http.StatusNotFound, "case-exec #%d not found", id)
	} else if oldStatus.IsCompleted() && *oldStatus != exec.Status {
		log.With("oldStatus", oldStatus, "newStatus", exec.Status).Warnf("can not update status")
		return nil, serror.NewMsg(http.StatusBadRequest, "can not update execution from %s to %s", *oldStatus, exec.Status)
	}

	// update case-execution
	_, err = s.db.Exec(`UPDATE case_executions SET started_at=?, completed_at=?, status=?, output=?, meta=?, argo_url=? WHERE id=?`,
		exec.StartedAt,
		exec.CompletedAt,
		exec.Status,
		exec.Output,
		exec.Meta,
		exec.ArgoURL,
		id,
	)
	if err != nil {
		log.Errorf(err, "UpdateCaseExecution failed")
		return nil, err
	}
	if err != nil {
		return nil, err
	}

	return s.Get(ctx, id)
}

func (s *caseExecutionService) UpdateNote(ctx utils.Context, id model.ID, note string) error {
	_, err := s.db.Exec(`UPDATE case_executions SET note=? WHERE id=?`, note, id)
	if err != nil {
		ctx.Log().Errorf(err, "update case-execution note failed")
		return err
	}
	return nil
}

func (s *caseExecutionService) UpdateReason(ctx utils.Context, id model.ID, reason model.ExecFailReason) error {
	_, err := s.db.Exec(`UPDATE case_executions SET reason=? WHERE id=?`, reason, id)
	if err != nil {
		ctx.Log().Errorf(err, "update case-execution reason failed")
		return err
	}
	return nil
}
