package request

import (
	"errors"
	"fmt"

	validation "github.com/Thearas/ozzo-validation/v4"

	. "github.com/pingcap/test-infra/common/model"
	"github.com/pingcap/test-infra/common/model/tcms/validator"
)

// =================================================================
// TestCase
// =================================================================

type CaseCreateRequest struct {
	// TcctlHeader is optional, and only be used by tcctl.
	*TcctlHeader `yaml:",omitempty,inline"`

	TestCaseBase `yaml:",inline"`
	TestCaseMeta `yaml:",inline"`
	Designer     string `json:"designer" yaml:"designer"`

	// helper fields
	DesignerID ID    `json:"-" yaml:"-"`
	DeletedAt  int64 `json:"-" yaml:"-"`
}

func (r *CaseCreateRequest) Validate() error {
	if err := validation.ValidateStruct(r,
		validation.Field(&r.Name, validation.Required, CaseNameValidationRule),
		validation.Field(&r.TestCaseMeta),
		validation.Field(&r.Designer, validator.PingCAPEmail),
		validation.Field(&r.DeletedAt, validation.Empty),
	); err != nil {
		return err
	}

	if r.Automation == CaseAutomationStatusAutomated {
		return validation.ValidateStruct(r, validation.Field(&r.TestCaseBase))
	}
	return nil
}

func (r *CaseCreateRequest) IntoTestCase(id ID, t CreatedAtAndUpdatedAt) *TestCase {
	return &TestCase{
		TestCaseBase: r.TestCaseBase,
		TestCaseMeta: r.TestCaseMeta,
		DesignedBy: DesignedBy{
			ID:    r.DesignerID,
			Email: r.Designer,
		},
		ID:                    id,
		CreatedAtAndUpdatedAt: t,
	}
}

func (r *CaseCreateRequest) FromTestCase(c *TestCase) *CaseCreateRequest {
	h := NewTcctlHeaderV1(TcctlResourceKindCase)
	*r = CaseCreateRequest{
		TcctlHeader:  &h,
		TestCaseBase: c.TestCaseBase,
		TestCaseMeta: c.TestCaseMeta,
		Designer:     c.DesignedBy.Email,
	}
	return r
}

type CaseUpdateRequest CaseCreateRequest

func (r *CaseUpdateRequest) Validate() error {
	return ((*CaseCreateRequest)(r)).Validate()
}

// =================================================================
// TestPlan
// =================================================================

type PlanCreateRequestOneshotSpec struct {
	InlineCases []*TestCaseBase `json:"cases,omitempty"`
}

func (r PlanCreateRequestOneshotSpec) Validate() error {
	// validate inlineCases name uniqued
	uniqInlineCaseNames := make(map[string]struct{}, len(r.InlineCases))
	return validation.ValidateStruct(&r,
		validation.Field(&r.InlineCases,
			validation.Length(1, 10),
			validation.Each(validation.By(func(v interface{}) error {
				ic := v.(TestCaseBase)
				if _, duplicated := uniqInlineCaseNames[ic.Name]; duplicated {
					return fmt.Errorf("inline case name %s duplicated", ic.Name)
				}
				uniqInlineCaseNames[ic.Name] = struct{}{}
				return nil
			})),
		),
	)
}

type PlanCreateRequest struct {
	TestPlanBase

	Type TestPlanType `json:"type,omitempty" default:"ONE_SHOT"`

	// Only for ONESHOT type
	PlanCreateRequestOneshotSpec
	// Only for GITHUB type
	Priority        ExecPriority `json:"priority,omitempty" default:"0" enums:"-100,0,50,100,101"`
	*GitHubPlanInfo `json:",omitempty"`

	WaitTimeout int `json:"waitTimeout" default:"604800"`

	// helper fields
	// Extract from session.
	Creator ID `json:"-"`
}

func (r *PlanCreateRequest) Validate() error {
	if r.Type == TestPlanKindGithub {
		if r.GitHubPlanInfo == nil {
			return errors.New("GitHubPlanInfo is nil")
		}
		if err := r.GitHubPlanInfo.Validate(); err != nil {
			return err
		}
	}

	if _, err := r.PatchParameters(nil, true); err != nil {
		return err
	}

	return validation.ValidateStruct(r,
		validation.Field(&r.Name, validation.When(r.Type != TestPlanKindOneShot, validation.Required)),
		validation.Field(&r.TestPlanBase),
		validation.Field(&r.Type),
		validation.Field(&r.Priority, validation.Empty.When(r.Type != TestPlanKindGithub)),
		validation.Field(&r.InlineCases, validation.Empty.When(r.Type != TestPlanKindOneShot)),
		validation.Field(&r.Pipeline),
	)
}

// =================================================================
// TestTrigger
// =================================================================

type TriggerCreateRequestInlinePlan struct {
	TestPlanBase
	PlanCreateRequestOneshotSpec
}

func (r TriggerCreateRequestInlinePlan) Validate() error {
	return validation.ValidateStruct(&r,
		validation.Field(&r.Name, validation.Required.Error("inline plan name is required")),
		validation.Field(&r.TestPlanBase),
		validation.Field(&r.Pipeline),
		validation.Field(&r.PlanCreateRequestOneshotSpec),
	)
}

type TriggerCreateRequest struct {
	Name          string      `json:"name,omitempty"`
	Summary       string      `json:"summary"`
	Type          TriggerType `json:"type" default:"ONCE"`
	Active        bool        `json:"-" default:"true"`
	ReleaseBranch string      `json:"releaseBranch"`
	// Auto include plans with `release` tag.
	AutoIncludeReleasePlans bool `json:"autoIncludeReleasePlans"`

	// RFC3339 are accepted.
	// NOTE: If `Expr` is empty, the plan will be triggered immediately.
	Expr string `json:"expr"`
	PlanRef

	// InlinePlans is only for ONCE trigger.
	InlinePlans []*TriggerCreateRequestInlinePlan `json:"plans,omitempty"`

	// helper fields
	// Extract from session.
	Creator      ID           `json:"-"`
	ExecPriority ExecPriority `json:"-"`
}

func (r *TriggerCreateRequest) Validate() error {
	uniqInlinePlanNames := make(map[string]struct{}, len(r.InlinePlans))
	return validation.ValidateStruct(r,
		validation.Field(&r.Name,
			validation.Required,
			TriggerNameValidationRule,
			validation.When(r.Type == TriggerTypeRelease,
				validator.StartsWith([]string{TriggerNamePrefixRelease}, fmt.Sprintf(`release trigger name should start with "%s"`, TriggerNamePrefixRelease)),
			),
		),
		validation.Field(&r.Type),
		validation.Field(&r.Expr, validation.When(r.Type == TriggerTypeCron, validation.Required.Error("cron schedule is required"), validator.Cron)),
		validation.Field(&r.ReleaseBranch, validation.Required.When(r.Type == TriggerTypeRelease)),
		validation.Field(&r.AutoIncludeReleasePlans, validation.Empty.When(r.Type != TriggerTypeRelease)),
		validation.Field(&r.PlanRef, validation.By(func(_ interface{}) error {
			if r.Type == TriggerTypeCron && r.PlanID == 0 {
				return errors.New("cron trigger requires a plan id")
			}
			return nil
		})),
		validation.Field(&r.InlinePlans,
			validation.Empty.When(
				r.Type != TriggerTypeOnce ||
					// TODO(gaozifeng): Remove this check when we migrate the old `trigger.PlanID` to `trigger.PlanIDs`.
					r.PlanRef.PlanID != 0,
			),
			validation.Each(validation.By(func(v interface{}) error {
				plan := v.(TriggerCreateRequestInlinePlan)
				if _, duplicated := uniqInlinePlanNames[plan.Name]; duplicated {
					return fmt.Errorf("inline plan name %s duplicated", plan.Name)
				}
				uniqInlinePlanNames[plan.Name] = struct{}{}
				return nil
			})),
		),
	)
}

// =================================================================
// TestPlanExecution & TestCaseExecution
// =================================================================

type PlanExecutionUpdateRequest struct {
	Meta ExecutionMeta `json:"meta"`

	StartedAt      *Timestamp      `json:"startedAt,omitempty"`
	CompletedAt    *Timestamp      `json:"completedAt,omitempty"`
	Priority       ExecPriority    `json:"priority"`
	Status         ExecutionStatus `json:"status"`
	RetryCnt       int             `json:"retryCnt"`
	Output         string          `json:"output"`
	ArgoURL        string          `json:"argoURL,omitempty"`
	ResourcepoolID ID              `json:"resourcepoolID,omitempty"`
}

func (r *PlanExecutionUpdateRequest) Validate() error {
	// used in internal RPC, trust our friends
	return nil
}

type CaseExecutionCreateRequest struct {
	Meta ExecutionMeta `json:"meta"`

	CaseID         ID     `json:"caseID" validate:"required"`
	RootPlanExecID ID     `json:"rootPlanExecID" validate:"required"`
	StepName       string `json:"stepName"`
	StepSeq        uint   `json:"stepSeq"`

	StartedAt   *Timestamp      `json:"startedAt,omitempty"`
	CompletedAt *Timestamp      `json:"completedAt,omitempty"`
	Status      ExecutionStatus `json:"status"`
	Output      string          `json:"output"`
	ArgoURL     string          `json:"argoURL,omitempty"`
}

func (r *CaseExecutionCreateRequest) Validate() error {
	// used in internal RPC, trust our friends
	return nil
}

type CaseExecutionUpdateRequest struct {
	Meta ExecutionMeta `json:"meta"`

	StartedAt   *Timestamp      `json:"startedAt,omitempty"`
	CompletedAt *Timestamp      `json:"completedAt,omitempty"`
	Status      ExecutionStatus `json:"status" validate:"execution-status"`
	Output      string          `json:"output"`
	ArgoURL     string          `json:"argoURL,omitempty"`
}

func (r *CaseExecutionUpdateRequest) Validate() error {
	// used in internal RPC, trust our friends
	return nil
}

type ExecutionNoteUpdateRequest struct {
	Note string `json:"note"`
}

func (r *ExecutionNoteUpdateRequest) Validate() error {
	return validation.ValidateStruct(r, validation.Field(&r.Note, validation.Length(1, 1024)))
}

type ExecutionReasonUpdateRequest struct {
	Reason ExecFailReason `json:"reason"`
}

func (r *ExecutionReasonUpdateRequest) Validate() error {
	failReason := ExecFailReason(r.Reason)
	_, err := failReason.Value()
	return err
}

type ResourcePoolUpdateRequest struct {
	ResourcePools []*ResourcePool `json:"resourcePools"`
}

type ResourceCohortCreateRequest struct {
	Name          string                          `json:"name"`
	Summary       string                          `json:"summary"`
	Resourcepools []*ResourceCohortAddPoolRequest `json:"resourcepools,omitempty"`
}

func (v ResourceCohortCreateRequest) Validate() error {
	return validation.ValidateStruct(&v,
		validation.Field(&v.Name, validation.Required),
		validation.Field(&v.Summary, validation.Required, validation.Length(0, 1024)),
		validation.Field(&v.Resourcepools, validation.Length(0, 100)),
	)
}

type ResourceCohortAddPoolRequest struct {
	// Name is the name of the resource pool.
	Name string `json:"name"`
}

func (v ResourceCohortAddPoolRequest) Validate() error {
	return validation.ValidateStruct(&v,
		validation.Field(&v.Name, validation.Required),
	)
}

type UpdateTriggerPlanRefRequest struct {
	NewPlanIDs IDSlice `json:"newPlanIDs"`
}

type PlanTriageUpdateRequest struct {
	TriagePhase   *TriagePhase      `json:"triagePhase"`
	TriagingUsers []ID              `json:"triagingUsers"`
	Solution      *ExecFailSolution `json:"solution,omitempty"`
	Note          *string           `json:"note,omitempty"`
}

type CaseTriageUpdateRequest struct {
	Reason    *CaseExecFailReason `json:"reason,omitempty"`
	Solution  *ExecFailSolution   `json:"solution,omitempty"`
	IssueLink *string             `json:"issueLink,omitempty"`
	Note      *string             `json:"note,omitempty"`
}
