package model

import (
	"database/sql/driver"
	"encoding/json"
	"fmt"
	"time"
)

type ExecPriority int

const (
	ExecPriorityLow     ExecPriority = -100
	ExecPriorityDefault ExecPriority = 0
	// The execs that user created for debug
	ExecPriorityUserDebug   ExecPriority = 50
	ExecPriorityHigh        ExecPriority = 100
	ExecPriorityHighPlusOne ExecPriority = ExecPriorityHigh + 1
)

func (t ExecPriority) Validate() error {
	var allExecPriorities = []ExecPriority{
		ExecPriorityLow,
		ExecPriorityDefault,
		ExecPriorityUserDebug,
		ExecPriorityHigh,
		ExecPriorityHighPlusOne,
	}

	for _, p := range allExecPriorities {
		if p == t {
			return nil
		}
	}

	return fmt.Errorf("invalid exec priority: %d, expect one of %v", t, allExecPriorities)
}

type ExecutionStatus string

const (
	ExecutionStatusPending        ExecutionStatus = "PENDING"
	ExecutionStatusRunning        ExecutionStatus = "RUNNING"
	ExecutionStatusSuccess        ExecutionStatus = "SUCCESS"
	ExecutionStatusFailure        ExecutionStatus = "FAILURE"
	ExecutionStatusSkipped        ExecutionStatus = "SKIPPED"
	ExecutionStatusOmitted        ExecutionStatus = "OMITTED"
	ExecutionStatusTimeout        ExecutionStatus = "TIMEOUT"
	ExecutionStatusCancelling     ExecutionStatus = "CANCELLING"
	ExecutionStatusCancelled      ExecutionStatus = "CANCELLED"
	ExecutionStatusError          ExecutionStatus = "ERROR"
	ExecutionStatusPreparing      ExecutionStatus = "PREPARING"
	ExecutionStatusPrepareBackoff ExecutionStatus = "PREPAREBACKOFF"
)

var IncompletedExecutionStates = []ExecutionStatus{ExecutionStatusPending, ExecutionStatusRunning, ExecutionStatusPreparing, ExecutionStatusPrepareBackoff}

var completedStatus = map[ExecutionStatus]struct{}{
	ExecutionStatusSuccess:   {},
	ExecutionStatusFailure:   {},
	ExecutionStatusSkipped:   {},
	ExecutionStatusOmitted:   {},
	ExecutionStatusTimeout:   {},
	ExecutionStatusCancelled: {},
	ExecutionStatusError:     {},
}

func (es ExecutionStatus) IsCompleted() bool {
	if _, ok := completedStatus[es]; ok {
		return true
	}
	return false
}

func (es ExecutionStatus) CanTransferTo(newStatus ExecutionStatus) bool {
	if es.IsCompleted() && es != newStatus {
		return false
	}
	if es == ExecutionStatusCancelling {
		return newStatus == ExecutionStatusCancelling || newStatus == ExecutionStatusCancelled
	}
	return true
}

var ExecutionErrorStatus = []ExecutionStatus{
	ExecutionStatusFailure,
	ExecutionStatusTimeout,
	ExecutionStatusError,
}

func (es ExecutionStatus) NeedTriage() bool {
	for _, value := range ExecutionErrorStatus {
		if value == es {
			return true
		}
	}
	return false
}

func (es ExecutionStatus) IsValid() bool {
	return es.Validate() == nil
}

func (es ExecutionStatus) Validate() error {
	values := []ExecutionStatus{
		ExecutionStatusPending,
		ExecutionStatusRunning,
		ExecutionStatusSuccess,
		ExecutionStatusFailure,
		ExecutionStatusSkipped,
		ExecutionStatusTimeout,
		ExecutionStatusCancelled,
		ExecutionStatusCancelling,
		ExecutionStatusError,
		ExecutionStatusPreparing,
		ExecutionStatusPrepareBackoff,
	}

	for _, value := range values {
		if value == es {
			return nil
		}
	}
	return fmt.Errorf("execution-status should be one of %v", values)
}

type ExecutionMeta map[string]string

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

func (s *ExecutionMeta) 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 = ExecutionMeta{}
		return nil
	}
	return json.Unmarshal(data, s)
}

type TestPlanExecution struct {
	ID   ID            `json:"id" db:"id"`
	Meta ExecutionMeta `json:"meta,omitempty" db:"meta"`
	// Deprecated: Use PlanIDAndName.ID instead.
	PlanID         ID `json:"planID" db:"-"`
	PlanIDAndName  `json:"plan"`
	TriggerID      ID               `json:"triggerID" db:"trigger_id"`
	VariableBinds  PlanVarBindSlice `json:"variables" db:"variables"`
	MetaParams     PlanVarBindSlice `json:"metaParams" db:"meta_params"`
	ResourcepoolID ID               `json:"resourcepoolID,omitempty" db:"resourcepool_id"`

	CreatedBy `json:"createdBy"`
	// TODO: Change type to `time.Time`.
	CreatedAt   Timestamp       `json:"createdAt" db:"created_at"`
	UpdatedAt   time.Time       `json:"updatedAt" db:"updated_at"`
	StartedAt   *Timestamp      `json:"startedAt" db:"started_at"`
	CompletedAt *Timestamp      `json:"completedAt" db:"completed_at"`
	Priority    ExecPriority    `json:"priority" db:"priority"`
	Status      ExecutionStatus `json:"status" db:"status"`
	Output      string          `json:"output" db:"output"`
	Note        string          `json:"note" db:"note"`
	Reason      ExecFailReason  `json:"reason" db:"reason"`
	RetryCnt    int             `json:"retryCnt" db:"retry_cnt"`
	// The execution ID that it reruns. Empty if it's not a rerun execution.
	RerunID ID `json:"rerunID,omitempty" db:"rerun_id"`
	// The plan-steps to run.
	Steps StringSlice `json:"steps" db:"steps"`
	// TODO(lijie,gaozifeng): Write argo url to plan-execution table.
	ArgoURL string `json:"argoURL,omitempty" db:"argo_url"`
}

type ResourceDuration struct {
	ID        ID        `json:"id"`
	CreatedAt time.Time `json:"createdAt,omitempty"`
	UpdatedAt time.Time `json:"updatedAt,omitempty"`

	PlanExecutionID ID                   `json:"planExecutionID"`
	Type            ResourceDurationType `json:"type"`

	// unit: hour * 1 core
	CpuDuration uint64 `json:"cpuDuration"`
	// unit: hour * 1 GB
	MemoryDuration uint64 `json:"memoryDuration"`
}

type ResourceDurationType string

const (
	PrepareErrorDuration ResourceDurationType = "PREPARE_ERROR"
	PrepareDuration      ResourceDurationType = "PREPARE"
	RunningDuration      ResourceDurationType = "RUNNING"
	WaitTriageDuration   ResourceDurationType = "WAIT_TRIAGE"
	TriageDuration       ResourceDurationType = "TRIAGE"
)

func (t ResourceDurationType) Values() []string {
	return []string{
		string(PrepareErrorDuration),
		string(PrepareDuration),
		string(RunningDuration),
		string(WaitTriageDuration),
		string(TriageDuration),
	}
}

func (p *TestPlanExecution) MarshalJSON() ([]byte, error) {
	type temp TestPlanExecution
	t := (*temp)(p)
	t.PlanID = t.PlanIDAndName.ID
	return json.Marshal(t)
}

type TestCaseExecution struct {
	ID             ID            `json:"id" db:"id"`
	Meta           ExecutionMeta `json:"meta,omitempty" db:"meta"`
	CaseIDAndName  `json:"case"`
	RootPlanID     ID     `json:"rootPlanID" db:"root_plan_id"`
	RootPlanExecID ID     `json:"rootPlanExecID" db:"root_plan_exec_id"`
	StepName       string `json:"stepName" db:"step_name"`
	StepSeq        uint   `json:"stepSeq" db:"step_seq"`
	// TODO: Change type to `time.Time`.
	CreatedAt   Timestamp       `json:"createdAt" db:"created_at"`
	UpdatedAt   time.Time       `json:"updatedAt" db:"updated_at"`
	StartedAt   *Timestamp      `json:"startedAt" db:"started_at"`
	CompletedAt *Timestamp      `json:"completedAt" db:"completed_at"`
	Status      ExecutionStatus `json:"status" db:"status"`
	Output      string          `json:"output" db:"output"`
	Note        string          `json:"note" db:"note"`
	Reason      ExecFailReason  `json:"reason" db:"reason"`
	// TODO(lijie,gaozifeng): Write argo url to plan-execution table.
	ArgoURL string `json:"argoURL,omitempty" db:"argo_url"`
	CaseID  ID     `json:"caseID" db:"-"`
}

func (c *TestCaseExecution) MarshalJSON() ([]byte, error) {
	type temp TestCaseExecution
	t := (*temp)(c)
	if t.CaseID == 0 {
		t.CaseID = t.CaseIDAndName.ID
	}
	return json.Marshal(t)
}
