package model

import (
	"bytes"
	"database/sql/driver"
	"encoding/json"
	"fmt"
	"io"
	"regexp"
	"strings"
	"time"

	validation "github.com/Thearas/ozzo-validation/v4"
	"github.com/hashicorp/go-multierror"
	"github.com/pingcap/test-infra/common/model/tcms/validator"
	"github.com/valyala/fasttemplate"
)

const (
	MaxPlanStepsLength = 200

	planNameFmt       string = "[a-z0-9](([@_\\-.a-z0-9]*[a-z0-9])?)*"
	planNameErrMsg    string = "plan name must consist of lower case alphanumeric characters, '@', '_', '-' or '.', and must start and end with an alphanumeric character"
	planNameMaxLength int    = 200

	// The pipeline-step name format is the same as argo's: https://github.com/argoproj/argo-workflows/blob/076ff18a804bbd3b4aba67024ac73dae82c2f049/pkg/apis/workflow/v1alpha1/validation_utils.go#L13
	pipelineStepNameFmt    string = "[a-zA-Z0-9][-a-zA-Z0-9]*"
	pipelineStepNameErrMsg string = "name must consist of alpha-numeric characters or '-', and must start with an alpha-numeric character (e.g. My-name1-2, 123-NAME)"
	pipelineStepMaxLength  int    = 128
)

var (
	planNameRegex          = regexp.MustCompile("^" + planNameFmt + "$")
	pipelineStepNameRegex  = regexp.MustCompile("^" + pipelineStepNameFmt + "$")
	PlanNameValidationRule = validation.By(func(v interface{}) error {
		if err := validation.Length(1, planNameMaxLength).Validate(v); err != nil {
			return err
		}
		return validation.Match(planNameRegex).Error(planNameErrMsg).Validate(v)
	})
	PipelineStepNameValidationRule = validation.By(func(v interface{}) error {
		if err := validation.Length(1, pipelineStepMaxLength).Validate(v); err != nil {
			return err
		}
		return validation.Match(pipelineStepNameRegex).Error(pipelineStepNameErrMsg).Validate(v)
	})
	testbedNameRefRegex = regexp.MustCompile(`^{{testbed\.([^.}]+)}}$`)
)

// see: https://pingcap.feishu.cn/docs/doccnS49yoTDIwPRqQp9p13Y0Xg
type TestPlanType string

const (
	TestPlanKindGeneral TestPlanType = "GENERAL"
	TestPlanKindGithub  TestPlanType = "GITHUB"
	TestPlanKindOneShot TestPlanType = "ONE_SHOT"
)

func (k TestPlanType) IsValid() bool {
	return k.Validate() == nil
}

func (v TestPlanType) Validate() error {
	values := []TestPlanType{
		TestPlanKindGeneral,
		TestPlanKindGithub,
		TestPlanKindOneShot,
	}

	for _, value := range values {
		if value == v {
			return nil
		}
	}

	return fmt.Errorf("plan-type should be one of %v", values)
}

// GitHubPlanInfo records where a github plan from and how to schedule it
// For example: pingcap/test-plan/master@7dae1d3/tidb/sql-infra/ddl/shard-bits.daily.yaml
type GitHubPlanInfo struct {
	// Repo is the pingcap/test-plan part for example, it's reserved and may use in the future
	Repo string `json:"repo,omitempty" db:"gh_repo"`
	// Branch is the master part on above example
	Branch string `json:"branch,omitempty" db:"gh_branch"`
	// CommitHash is the 7dae1d3 part for example
	//
	// We uses the commit hash for data integrity and idempotent, since we don't need to
	// decide how to remove a add/deleted/update a pipeline.
	CommitHash string `json:"commit,omitempty" db:"gh_commit"`
	// CommitTs is the commit unix nano timestamp
	CommitTs int64 `json:"commitTs,omitempty" db:"gh_commit_ts"`
	// Path is the tidb/sql-infra/ddl/shard-bits.daily.yaml part for example
	Path string `json:"path,omitempty" db:"gh_path"`

	// Tags is used to selected TestPlan, for example, release manager can tag one plan as ["release"] so that this one should be selected when release test
	Tags StringCSV `json:"tags,omitempty" db:"tags"`
	// Cron Schedule
	Schedule string `json:"schedule,omitempty" db:"gh_schedule"`
}

func (v GitHubPlanInfo) Validate() error {
	return validation.ValidateStruct(&v,
		validation.Field(&v.Repo, validation.Required, validation.Length(1, 100)),
		validation.Field(&v.Branch, validation.Required, validation.Length(1, 100)),
		validation.Field(&v.CommitHash, validation.Required, validation.Length(1, 40)),
		validation.Field(&v.CommitTs, validation.Required),
		validation.Field(&v.Path, validation.Required, validation.Length(1, 1024)),
		validation.Field(&v.Tags, validation.Each(validation.Required, validation.Length(1, 255))),
		validation.Field(&v.Schedule,
			validator.Cron,
			validation.
				When(v.Schedule == "@daily" || v.Schedule == "@weekly", validation.Skip).
				Else(validator.StartsWith([]string{"0 0 * * "}, "unsupported cron schedule: "+v.Schedule)),
		),
	)
}

type InterruptAction string

const (
	InterruptActionNone  InterruptAction = ""
	InterruptActionAbort InterruptAction = "ABORT"
)

func (v InterruptAction) Validate() error {
	values := []InterruptAction{
		InterruptActionNone,
		InterruptActionAbort,
	}

	for _, value := range values {
		if value == v {
			return nil
		}
	}

	return fmt.Errorf("interruptAction should be one of %v", values)
}

// TestPlanBase is the minimal requirements for running a plan.
type TestPlanBase struct {
	// For the GitHub Plan, name is in the format of: $plan_name@$branch
	// For the OnShot Plan, name is no need
	Name     string   `json:"name" db:"name"`
	TestBed  TestBed  `json:"testbed" db:"test_bed"`
	Pipeline Pipeline `json:"pipeline,omitempty" db:"pipeline"`
	// Steps is a pipeline that executes serially. Incompatible with `Pipeline`.
	Steps   Pipeline `json:"steps,omitempty" db:"-"`
	IsSteps bool     `json:"-" db:"is_steps" swaggerignore:"true"`
	// If specified, will fail this plan-execution if any of its child case-executions has failed.
	FailFast        bool             `json:"failFast" db:"fail_fast"`
	Variables       PlanVarDeclSlice `json:"variables,omitempty" db:"variables"`
	MetaParams      PlanVarDeclSlice `json:"metaParams,omitempty" db:"meta_params"`
	InterruptAction InterruptAction  `json:"interruptAction,omitempty" db:"interrupt_action"`
}

func (r TestPlanBase) Validate() (err error) {
	if err := validation.ValidateStruct(&r,
		validation.Field(&r.Name, PlanNameValidationRule),
		validation.Field(&r.TestBed),
		validation.Field(&r.Pipeline, validation.Length(1, MaxPlanStepsLength)),
		validation.Field(&r.Steps,
			validation.Required.When(len(r.Pipeline) == 0).Error("require one of pipeline and steps"),
			validation.Empty.When(len(r.Pipeline) > 0).Error("only one of pipeline and steps can be selected"),
			validation.Length(1, MaxPlanStepsLength),
			// after must be empty in step
			validation.Each(validation.By(func(v interface{}) error {
				step := v.(PipelineStep)
				if len(step.After) > 0 {
					return fmt.Errorf("after must be empty in step")
				}
				return nil
			})),
		),
		validation.Field(&r.Variables),
		validation.Field(&r.InterruptAction),
	); err != nil {
		return err
	}

	// TestBed & Pipeline
	var testbedItems = make(map[string]interface{}, len(r.TestBed.Items))
	for _, item := range r.TestBed.Items {
		testbedItems[item.Name] = true
	}
	pipeline := r.Pipeline
	if len(pipeline) == 0 {
		pipeline = r.Steps
	}
	// record all non-empty step name
	nonEmptyStepNames := make(map[string]struct{}, len(pipeline))
	for seq, step := range pipeline {
		if step.Name != "" {
			// pipeline-step name should be unique
			if _, ok := nonEmptyStepNames[step.Name]; !ok {
				nonEmptyStepNames[step.Name] = struct{}{}
			} else {
				err = multierror.Append(err, fmt.Errorf("pipeline-step name should be uniqued, but step[#%d][%s] is defined multiple times", seq, step.Name))
			}
		}

		for _, argument := range step.CaseRef.Arguments {
			if testbedNameRefRegex.MatchString(argument.Value) {
				name := testbedNameRefRegex.FindStringSubmatch(argument.Value)[1]
				if _, exist := testbedItems[name]; !exist {
					err = multierror.Append(err, fmt.Errorf("testbed name `%s` that refers on the step[#%d][%s] isn't defined on the testbed", name, seq, step.Name))
				}
			}
		}
	}
	if err != nil {
		return err
	}
	if len(nonEmptyStepNames) != 0 && len(nonEmptyStepNames) != len(pipeline) {
		return multierror.Append(err, fmt.Errorf("all pipeline-step name should be empty or all of them should be non-empty"))
	}
	// validate `step.after`
	for _, step := range pipeline {
		for _, after := range step.After {
			for _, dep := range getDependencies(after) {
				if _, ok := nonEmptyStepNames[dep]; !ok {
					err = multierror.Append(err, fmt.Errorf("the step[%s] used in step[%s].after is not found", dep, step.Name))
				}
			}
		}
	}
	return err
}

func getDependencies(after string) []string {
	deps := strings.FieldsFunc(after, func(r rune) bool {
		return r == '|' || r == '&' || r == '!' || r == ')' || r == '(' || r == ' '
	})
	rt := make([]string, len(deps))
	for i, dep := range deps {
		t := strings.SplitN(dep, ".", 2)
		rt[i] = t[0]
	}
	return rt
}

func (r TestPlanBase) PatchParameters(binds *PlanVarBindSlice, dryRun bool) (*TestPlanBase, error) {
	params := make(map[string]string)
	for _, item := range r.MetaParams {
		params[fmt.Sprintf("meta.parameters.%s", item.Name)] = item.Default
	}
	if binds != nil {
		for _, item := range *binds {
			params[fmt.Sprintf("meta.parameters.%s", item.Name)] = item.Value
		}
	}

	patch := func(object string) (string, error) {
		buf := &bytes.Buffer{}
		tmpl := fasttemplate.New(object, "{{", "}}")
		_, err := tmpl.ExecuteFunc(buf, func(w io.Writer, tag string) (int, error) {
			// only substitute meta.parameters prefix variables
			if strings.HasPrefix(tag, "meta.parameters.") {
				val, e := params[tag]
				if !e {
					return 0, fmt.Errorf("unknown variable %q", tag)
				}
				return w.Write([]byte(val))
			} else {
				return w.Write([]byte(fmt.Sprintf("{{%s}}", tag)))
			}
		})
		if err != nil {
			return "", err
		}
		return buf.String(), nil
	}

	// patch pipelines
	for i, pipelineItem := range r.Pipeline {
		for j, caseRefItem := range pipelineItem.Arguments {
			val, err := patch(caseRefItem.Value)
			if err != nil {
				return nil, err
			}
			if !dryRun {
				r.Pipeline[i].Arguments[j].Value = val
			}
		}
	}

	// patch steps
	for i, stepItem := range r.Steps {
		for j, caseRefItem := range stepItem.Arguments {
			val, err := patch(caseRefItem.Value)
			if err != nil {
				return nil, err
			}
			if !dryRun {
				r.Steps[i].Arguments[j].Value = val
			}
		}
	}

	// patch testbed
	bTestbed, err := json.Marshal(r.TestBed)
	if err != nil {
		return nil, err
	}
	bPatchedTestbed, err := patch(string(bTestbed))
	if err != nil {
		return nil, err
	}
	if !dryRun {
		if err = json.Unmarshal([]byte(bPatchedTestbed), &r.TestBed); err != nil {
			return nil, err
		}
	}

	return &r, nil
}

// TestPlan contains three kinds of Plan: General TestPlan, Pipeline TestPlan and OneShot TestPlan.
//
// 1. The General TestPlan is the Plan of the first version, a trigger is needed for scheduling
//
// 2. The Github TestPlan is a read-only ref of a Plan defined on the Github repo, it's read-only on test-infra and every updates is perform on the github repo.
// It's trigger is generated by test-infra and can't create a trigger manually for it
//
// 3. The OneShot TestPlan is for testing usage, it's triggered immediately by test-infra.
type TestPlan struct {
	TestPlanBase

	ID        ID           `json:"id" db:"id"`
	Type      TestPlanType `json:"type" db:"type" default:"ONE_SHOT"`
	CreatedBy `json:"createdBy"`
	// Github kind plan only
	GitHubPlanInfo
	// If specified, will fail this plan-execution if any of its child case-executions has failed.
	CreatedAt   time.Time `json:"createdAt" db:"created_at"`
	UpdatedAt   time.Time `json:"updatedAt" db:"updated_at"`
	DeletedAt   int64     `json:"-" db:"deleted_at"`
	WaitTimeout int64     `json:"waitTimeout" db:"wait_timeout"` // todo maybe should be in exec, but use can set here for one-shot plan
}

// Pipeline is a []PipelineStep but stores as a JSON array in the database.
type Pipeline []PipelineStep

func (s Pipeline) Value() (driver.Value, error) {
	if len(s) == 0 {
		return []byte("[]"), nil
	}
	return json.Marshal(s)
}

func (s *Pipeline) Scan(src interface{}) error {
	var data []byte
	switch v := src.(type) {
	case string:
		data = []byte(v)
	case []byte:
		data = v
	default:
	}
	if len(data) == 0 {
		*s = []PipelineStep{}
		return nil
	}
	return json.Unmarshal(data, s)
}

// PlanVarDeclSlice is a []PlanVarDecl but stores as a JSON array in the database.
type PlanVarDeclSlice []PlanVarDecl

func (s PlanVarDeclSlice) Value() (driver.Value, error) {
	if len(s) == 0 {
		return []byte("[]"), nil
	}
	return json.Marshal(s)
}

func (s *PlanVarDeclSlice) Scan(src interface{}) error {
	var data []byte
	switch v := src.(type) {
	case string:
		data = []byte(v)
	case []byte:
		data = v
	default:
	}
	if len(data) == 0 {
		*s = []PlanVarDecl{}
		return nil
	}
	return json.Unmarshal(data, s)
}

type PlanVarDecl struct {
	Name    string `json:"name"`
	Default string `json:"default"`
}

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

type PlanVarBind struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

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

// PlanVarBindSlice is a []PlanVarBind but stores as a JSON array in the database.
type PlanVarBindSlice []PlanVarBind

func (s PlanVarBindSlice) Value() (driver.Value, error) {
	if len(s) == 0 {
		return []byte("[]"), nil
	}
	return json.Marshal(s)
}

func (s *PlanVarBindSlice) Scan(src interface{}) error {
	var data []byte
	switch v := src.(type) {
	case string:
		data = []byte(v)
	case []byte:
		data = v
	default:
	}
	if len(data) == 0 {
		*s = []PlanVarBind{}
		return nil
	}
	return json.Unmarshal(data, s)
}

type PipelineStep struct {
	Name string `json:"name" db:"name"`
	// Seq is the user-specified sequence of pipeline-step (e.g. in yaml), isn't the actual execution sequence.
	Seq uint `json:"seq" db:"seq" swaggerignore:"true"`
	// After is the list of pipeline-steps that this pipeline-step depends on.
	// NOTE: `after` is forbidden for `steps`, only `pipeline` can use it.
	After   StringCSV `json:"after,omitempty" db:"after"`
	Timeout int       `json:"timeout" db:"timeout"`
	CaseRef

	PlanID    ID    `json:"-" db:"plan_id"`
	DeletedAt int64 `json:"-" db:"deleted_at"`
}

func (v PipelineStep) Validate() error {
	if err := v.CaseRef.Validate(); err != nil {
		return err
	}
	return validation.ValidateStruct(&v,
		validation.Field(&v.Name, PipelineStepNameValidationRule),
		validation.Field(&v.Timeout, validation.Min(0)),
	)
}

type CaseRef struct {
	CaseID    ID                 `json:"caseID,omitempty" db:"ref_case_id"`
	CaseName  string             `json:"caseName,omitempty" db:"ref_case_name"`
	CaseImage string             `json:"caseImage,omitempty" db:"ref_case_image"`
	Arguments CaseParamBindSlice `json:"arguments,omitempty" db:"arguments"`
}

func (v CaseRef) Validate() error {
	return validation.ValidateStruct(&v,
		validation.Field(&v.CaseID,
			validation.Required.When(v.CaseName == "").Error("require one of caseID and caseName"),
		),
		validation.Field(&v.Arguments),
	)
}

type PlanRef struct {
	PlanID        ID               `json:"planID" db:"plan_id"`
	PlanIDs       IDSlice          `json:"planIDs" db:"plan_ids"`
	VariableBinds PlanVarBindSlice `json:"variables" db:"variables"`
}

func (v PlanRef) Validate() error {
	return validation.ValidateStruct(&v,
		validation.Field(&v.VariableBinds),
		validation.Field(&v.PlanIDs, validation.Empty.When(v.PlanID != 0), validation.Length(1, 100)),
	)
}

type PlanIDAndName struct {
	ID   ID     `json:"id" db:"plan_id"`
	Name string `json:"name" db:"plan_name"`
}
