package store

import (
	"net/http"

	"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"
	"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/service/store/v2/ent"
	"github.com/pingcap/test-infra/services/tcms/service/store/v2/ent/casetriage"
	"github.com/pingcap/test-infra/services/tcms/service/store/v2/ent/triage"
	"github.com/pingcap/test-infra/services/tcms/utils"
)

type triageService struct {
	ent         *ent.Client
	planExecSvc service.PlanExecutionService
	caseExecSvc service.CaseExecutionService
	testbedSvc  service.TestbedService
}

func (s *triageService) init(ent *ent.Client, planExecSvc service.PlanExecutionService, caseExecSvc service.CaseExecutionService, testbedSvc service.TestbedService) {
	s.ent = ent
	s.planExecSvc = planExecSvc
	s.caseExecSvc = caseExecSvc
	s.testbedSvc = testbedSvc
}

func (s *triageService) ListPlanTriages(ctx utils.Context, opts common.PlanTriageQueryOpts) ([]*model.Triage, error) {
	query := s.ent.Triage.Query().Offset(int(opts.Offset))
	if opts.Solutions != nil {
		query = query.Where(triage.SolutionIn(opts.Solutions...))
	}
	if opts.PlanIDs != nil {
		query = query.Where(triage.PlanIDIn(opts.PlanIDs...))
	}
	if opts.PlanExecIDs != nil {
		query = query.Where(triage.PlanExecutionIDIn(opts.PlanExecIDs...))
	}
	res, err := query.All(ctx.Request().Context())
	if err != nil {
		return nil, err
	}
	return toModelPlanTriage(res), nil
}

func (s *triageService) CreatePlanTriage(ctx utils.Context, triage *model.Triage) (*model.Triage, error) {
	log := ctx.Log()

	planExec, err := s.planExecSvc.Get(ctx, triage.PlanExecutionID)
	if err != nil {
		return nil, err
	}

	res, err := s.ent.Triage.Create().
		SetPlanExecutionID(triage.PlanExecutionID).
		SetPlanID(planExec.PlanIDAndName.ID).
		SetTriagingUsers(triage.TriagingUsers).
		SetSolution(model.ExecFailSolutionKEEP).
		SetToTriageCaseExecIDs(triage.ToTriageCaseExecIDs).
		Save(ctx.Request().Context())
	if err != nil {
		if ent.IsConstraintError(err) {
			return nil, serror.New(http.StatusBadRequest, err)
		}
		log.Errorf(err, "CreateTriage: Triage.Create() failed")
		return nil, err
	} else if res == nil {
		return nil, nil
	}

	return toModelPlanTriage([]*ent.Triage{res})[0], nil
}

func (s *triageService) UpdatePlanTriage(ctx utils.Context, id model.ID, req *reqM.PlanTriageUpdateRequest) error {
	log := ctx.Log()

	triage, err := s.ent.Triage.Query().
		Where(triage.PlanExecutionIDEQ(id)).
		Only(ctx.Request().Context())
	if err != nil {
		log.Errorf(err, "UpdateTriage: get Triage failed")
		return err
	}

	var isTriagingUser bool
	for _, triagingUser := range triage.TriagingUsers {
		if triagingUser == ctx.User().ID {
			isTriagingUser = true
		}
	}
	// if triagingUsers is empty, skip permission control
	if !isTriagingUser && len(triage.TriagingUsers) != 0 {
		return serror.NewMsg(http.StatusBadRequest, "you are not in the triaging user list, please ask the plan maintainers")
	}

	update := triage.Update()
	var triaging bool

	// check triage phase
	if req.TriagePhase != nil {
		if *req.TriagePhase != triage.TriagePhase.Next() {
			return serror.NewMsg(http.StatusBadRequest, "unacceptable triage phase")
		}
		// Here req.TriagePhase must be TRIAGING or TRIAGED
		triaging = true
		update = update.SetTriagePhase(*req.TriagePhase)
	} else {
		triaging = triage.TriagePhase == model.TriagePhaseTriaging
	}

	if !triaging {
		return serror.NewMsg(http.StatusBadRequest, "only in triaging status can update triage record.")
	} else if req.TriagePhase != nil && *req.TriagePhase == model.TriagePhaseTriaged {
		// check if all testbed have been released
		_, total, err := s.testbedSvc.List(ctx, common.TestbedQueryOpts{
			Status:     []model.TestBedInfoStatus{model.TestBedInfoStatusReady},
			PlanExecID: &id,
		})
		if err != nil {
			return err
		} else if total != 0 {
			return serror.NewMsg(http.StatusForbidden, "please release all the testbeds before finishing a triage")
		}
	}

	if len(req.TriagingUsers) > 0 {
		update = update.SetTriagingUsers(utils.Union(triage.TriagingUsers, req.TriagingUsers))
	}

	if req.Solution != nil {
		update = update.SetSolution(*req.Solution)
	}

	if req.Note != nil {
		update = update.SetNote(*req.Note)
	}

	_, err = update.Save(ctx.Request().Context())
	if err != nil {
		log.Errorf(err, "UpdateTriage: Triage.Update() failed")
		return err
	}
	return nil
}

func (s *triageService) GetPlanTriage(ctx utils.Context, id model.ID) (*model.Triage, error) {
	log := ctx.Log()

	res, err := s.ent.Triage.Query().
		Where(triage.PlanExecutionIDEQ(id)).
		Only(ctx.Request().Context())
	if err != nil {
		if ent.IsNotFound(err) {
			log.Warnf("triage of plan execution %d not found", id)
			return nil, nil
		}
		log.Errorf(err, "GetTriage: Triage.Get() failed")
		return nil, err
	} else if res == nil {
		return nil, nil
	}

	return toModelPlanTriage([]*ent.Triage{res})[0], nil
}

func (s *triageService) ListCaseTriages(ctx utils.Context, opts common.CaseTriageQueryOpts) ([]*model.CaseTriage, error) {
	query := s.ent.CaseTriage.Query().Offset(int(opts.Offset)).Limit(int(opts.Count))

	if opts.Reasons != nil {
		query = query.Where(casetriage.ReasonIn(opts.Reasons...))
	}
	if opts.Solutions != nil {
		query = query.Where(casetriage.SolutionIn(opts.Solutions...))
	}
	if opts.CaseIDs != nil {
		query = query.Where(casetriage.CaseIDIn(opts.CaseIDs...))
	}
	if opts.CaseExecIDs != nil {
		query = query.Where(casetriage.CaseExecutionIDIn(opts.CaseExecIDs...))
	}
	if opts.PlanIDs != nil {
		query = query.Where(casetriage.PlanIDIn(opts.PlanIDs...))
	}
	if opts.RootPlanExecIDs != nil {
		query = query.Where(casetriage.PlanExecutionIDIn(opts.RootPlanExecIDs...))
	}
	res, err := query.All(ctx.Request().Context())
	if err != nil {
		return nil, err
	}
	return toModelCaseTriage(res), nil
}

func (s *triageService) CreateCaseTriage(ctx utils.Context, caseTriage *model.CaseTriage) (*model.CaseTriage, error) {
	var res *ent.CaseTriage
	if err := s.ent.Transaction(ctx.Request().Context(), func(tx *ent.Tx) (err error) {
		res, err = s.createCaseTriage(ctx, caseTriage, tx)
		return
	}); err != nil {
		return nil, err
	} else if res == nil {
		return nil, nil
	}

	return toModelCaseTriage([]*ent.CaseTriage{res})[0], nil
}

func (s *triageService) createCaseTriage(ctx utils.Context, caseTriage *model.CaseTriage, tx *ent.Tx) (*ent.CaseTriage, error) {
	txClient := tx.Client()
	log := ctx.Log()

	caseExec, err := s.caseExecSvc.Get(ctx, caseTriage.CaseExecutionID)
	if err != nil {
		return nil, err
	} else if caseExec == nil {
		return nil, serror.NewMsg(http.StatusBadRequest, "Case Execution %d not found", caseTriage.CaseExecutionID)
	}

	triage, err := txClient.Triage.Query().Where(triage.PlanExecutionID(caseExec.RootPlanExecID)).Only(ctx.Request().Context())
	if err != nil {
		if ent.IsNotFound(err) {
			return nil, serror.New(http.StatusNotFound, err)
		}
		log.Errorf(err, "CreateCaseTriage: Triage.Get() failed")
		return nil, err
	}

	if triage.TriagePhase != model.TriagePhaseTriaging {
		return nil, serror.NewMsg(http.StatusBadRequest, "please start Triage in the plan Execution before you do case triage")
	}

	res, err := txClient.CaseTriage.Create().
		SetPlanExecutionID(caseExec.RootPlanExecID).
		SetPlanID(triage.PlanID).
		SetCaseExecutionID(caseTriage.CaseExecutionID).
		SetCaseID(caseExec.CaseIDAndName.ID).
		SetReason(caseTriage.Reason).
		SetSolution(caseTriage.Solution).
		SetIssueLink(caseTriage.IssueLink).
		SetNote(caseTriage.Note).
		SetForkedFrom(caseTriage.ForkedFrom).
		Save(ctx.Request().Context())
	if err != nil {
		if ent.IsConstraintError(err) {
			return nil, serror.New(http.StatusBadRequest, err)
		}
		log.Errorf(err, "CreateCaseTriage: CaseTriage.Create() failed")
		return nil, err
	}

	triagedCaseExecIDs := utils.Union(triage.TriagedCaseExecIDs, []model.ID{caseTriage.CaseExecutionID})
	toTriageCaseExecIDs := utils.Except(triage.ToTriageCaseExecIDs, triagedCaseExecIDs)

	planTriageReasons := triage.Reasons
	var flagInReasons bool
	for _, planTriageReason := range planTriageReasons {
		if caseTriage.Reason != planTriageReason {
			continue
		}
		flagInReasons = true
		break
	}
	if !flagInReasons {
		planTriageReasons = append(planTriageReasons, caseTriage.Reason)
	}

	if err = triage.Update().
		SetReasons(planTriageReasons).
		SetTriagedCaseExecIDs(triagedCaseExecIDs).
		SetToTriageCaseExecIDs(toTriageCaseExecIDs).
		Exec(ctx.Request().Context()); err != nil {
		return nil, err
	}

	return res, nil
}

func (s *triageService) UpdateCaseTriage(ctx utils.Context, id model.ID, req *reqM.CaseTriageUpdateRequest) error {
	caseTriage, err := s.ent.CaseTriage.Query().Where(casetriage.CaseExecutionID(id)).Only(ctx.Request().Context())
	if err != nil {
		return err
	}

	update := caseTriage.Update()
	if req.Reason != nil && *req.Reason != caseTriage.Reason {
		update = update.SetReason(*req.Reason)
	}
	if req.Solution != nil && *req.Solution != caseTriage.Solution {
		update = update.SetSolution(*req.Solution)
	}
	if req.IssueLink != nil && *req.IssueLink != caseTriage.IssueLink {
		update = update.SetIssueLink(*req.IssueLink)
	}
	if req.Note != nil && *req.Note != caseTriage.Note {
		update = update.SetNote(*req.Note)
	}
	update = update.SetForkedFrom(0)

	err = update.Exec(ctx.Request().Context())
	if err != nil {
		return err
	}

	err = s.updatePlanTriageReasons(ctx, caseTriage.PlanExecutionID)
	if err != nil {
		return err
	}

	return nil
}

func (s *triageService) updatePlanTriageReasons(ctx utils.Context, id model.ID) error {
	reasonsMap := make(map[model.CaseExecFailReason]bool)

	caseTriages, err := s.ent.CaseTriage.Query().Where(casetriage.PlanExecutionID(id)).All(ctx.Request().Context())
	if err != nil {
		return err
	}

	for _, caseTriage := range caseTriages {
		reasonsMap[caseTriage.Reason] = true
	}

	var reasons []model.CaseExecFailReason
	for k := range reasonsMap {
		reasons = append(reasons, k)
	}

	return s.ent.Triage.Update().
		Where(triage.PlanExecutionID(id)).
		SetReasons(reasons).
		Exec(ctx.Request().Context())
}

func (s *triageService) GetCaseTriage(ctx utils.Context, id model.ID) (*model.CaseTriage, error) {
	log := ctx.Log()

	res, err := s.ent.CaseTriage.Query().
		Where(casetriage.CaseExecutionID(id)).
		Only(ctx.Request().Context())
	if err != nil {
		if ent.IsNotFound(err) {
			return nil, nil
		}
		log.Errorf(err, "GetCaseTriage: CaseTriage.Get() failed")
		return nil, err
	} else if res == nil {
		return nil, nil
	}

	return toModelCaseTriage([]*ent.CaseTriage{res})[0], nil
}

func (s *triageService) ForkCaseTriages(ctx utils.Context, id, forkFromID model.ID) (*model.CaseTriage, error) {
	log := ctx.Log()

	if forkFromID == id {
		return nil, nil
	}

	from, err := s.ent.CaseTriage.Query().
		Where(casetriage.CaseExecutionID(forkFromID)).
		Only(ctx.Request().Context())
	if err != nil {
		if ent.IsNotFound(err) {
			return nil, nil
		}
		log.Errorf(err, "ForkCaseTriages: CaseTriage.Get() failed")
		return nil, err
	}

	to, err := s.ent.CaseTriage.Query().
		Where(casetriage.CaseExecutionID(id)).
		Only(ctx.Request().Context())
	if err != nil && !ent.IsNotFound(err) {
		log.Errorf(err, "ForkCaseTriages: CaseTriage.Get() failed")
		return nil, err
	}

	var res *ent.CaseTriage
	if err := s.ent.Transaction(ctx.Request().Context(), func(tx *ent.Tx) (err error) {
		if to != nil {
			err = tx.CaseTriage.DeleteOne(to).Exec(ctx.Request().Context())
			if err != nil {
				return
			}
		}
		res, err = s.createCaseTriage(ctx, &model.CaseTriage{
			CaseExecutionID: id,
			Reason:          from.Reason,
			Solution:        from.Solution,
			IssueLink:       from.IssueLink,
			Note:            from.Note,
			ForkedFrom:      from.CaseExecutionID,
		}, tx)
		return
	}); err != nil {
		return nil, err
	} else if res == nil {
		return nil, nil
	}

	return toModelCaseTriage([]*ent.CaseTriage{res})[0], nil
}

func (s *triageService) SuggestCaseTriages(ctx utils.Context, id model.ID) ([]*model.CaseTriage, error) {
	caseExec, err := s.caseExecSvc.Get(ctx, id)
	if err != nil {
		return nil, err
	}
	res, err := s.ent.CaseTriage.Query().
		Where(
			casetriage.CaseID(caseExec.CaseIDAndName.ID),
			casetriage.ForkedFrom(0),
			casetriage.CaseExecutionIDNEQ(id)).
		Order(ent.Desc(casetriage.FieldUpdatedAt)).Limit(10).All(ctx.Request().Context())
	if err != nil {
		if ent.IsNotFound(err) {
			return nil, nil
		} else {
			return nil, err
		}
	}
	return toModelCaseTriage(res), nil
}

func toModelPlanTriage(planTriages []*ent.Triage) []*model.Triage {
	res := make([]*model.Triage, 0, len(planTriages))
	for _, item := range planTriages {
		res = append(res, &model.Triage{
			ID:                  item.ID,
			PlanExecutionID:     item.PlanExecutionID,
			PlanID:              item.PlanID,
			TriagingUsers:       item.TriagingUsers,
			ToTriageCaseExecIDs: item.ToTriageCaseExecIDs,
			TriagedCaseExecIDs:  item.TriagedCaseExecIDs,
			TriagePhase:         item.TriagePhase,
			CreatedAt:           item.CreatedAt,
			UpdatedAt:           item.UpdatedAt,
			StartedAt:           item.StartedAt,
			TriagedAt:           item.TriagedAt,
			Reasons:             item.Reasons,
			Solution:            item.Solution,
			Note:                item.Note,
		})
	}
	return res
}

func toModelCaseTriage(caseTriages []*ent.CaseTriage) []*model.CaseTriage {
	res := make([]*model.CaseTriage, 0, len(caseTriages))
	for _, item := range caseTriages {
		res = append(res, &model.CaseTriage{
			ID:              item.ID,
			ForkedFrom:      item.ForkedFrom,
			PlanExecutionID: item.PlanExecutionID,
			PlanID:          item.PlanID,
			CaseExecutionID: item.CaseExecutionID,
			CaseID:          item.CaseID,
			CreatedAt:       item.CreatedAt,
			UpdatedAt:       item.UpdatedAt,
			Reason:          item.Reason,
			Solution:        item.Solution,
			IssueLink:       item.IssueLink,
			Note:            item.Note,
		})
	}
	return res
}
