package api

import (
	"github.com/mohae/deepcopy"
	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/db/model"
	"github.com/pingcap/log"
	"github.com/tidbcloud/chaos-svc/idl/pbgen/proto/chaos"
	"go.uber.org/zap"
	"gorm.io/gorm"

	chaosmodel "github.com/pingcap-inc/chaosAPI/pkg/infra/db/model"
)

type Experiment struct {
	ExperimentId uint64           `json:"experiment_id"`
	ClusterId    uint64           `json:"cluster_id"`
	Draft        bool             `json:"draft"`
	Action       Action           `json:"action"`
	Summary      string           `json:"summary"`
	ReportLink   string           `json:"report_link"`
	Status       ExperimentStatus `json:"status" enums:"Waiting Approve,Draft,Running,Done,Failed,Deleted"`
	Message      string           `json:"message"`
	CreatedAt    *int64           `json:"created_at"`
	CreatedBy    session.CtxUser  `json:"created_by"`
	ApprovedBy   session.CtxUser  `json:"approved_by"`
	StartedAt    *int64           `json:"started_at"`
	CompletedAt  *int64           `json:"completed_at"`
	DeletedAt    *int64           `json:"deleted_at"`
}

func BuildExperiment(exp *chaos.Experiment) Experiment {
	if exp == nil {
		return Experiment{}
	}

	return *fromChaosExperiment(exp).
		buildCreatedBy(uint(exp.GetCreatedById())).
		buildStatus(exp.GetStatus(), exp.GetExperimentId()).
		buildApprovedBy(exp.GetExperimentId())
}

func ToOptional[T comparable](i T) *T {
	var tmp T
	if i == tmp {
		return nil
	}
	return &i
}

func fromChaosExperiment(exp *chaos.Experiment) *Experiment {
	return &Experiment{
		ExperimentId: exp.GetExperimentId(),
		ClusterId:    exp.GetClusterId(),
		Draft:        exp.GetDraft(),
		Action:       FromExperimentAction(exp.GetAction()),
		Summary:      exp.GetSummary(),
		ReportLink:   exp.GetReportLink(),
		Message:      exp.GetMessage(),
		CreatedAt:    ToOptional[int64](exp.GetCreatedAt()),
		StartedAt:    ToOptional[int64](exp.GetStartedAt()),
		CompletedAt:  ToOptional[int64](exp.GetCompletedAt()),
		DeletedAt:    ToOptional[int64](exp.GetDeletedAt()),
	}
}

func (exp *Experiment) buildCreatedBy(id uint) *Experiment {
	creatorModel, err := model.GetUserByID(id)
	if err != nil {
		log.Error("get user by id failed", zap.Error(err))
	}
	exp.CreatedBy = session.CtxUser{
		Username: creatorModel.UserName,
		UserID:   creatorModel.ID,
		Email:    creatorModel.Email,
	}
	return exp
}

func (exp *Experiment) buildApprovedBy(eid uint64) *Experiment {
	if exp.Status == ExperimentWaitingApprove {
		return exp
	}
	record, err := chaosmodel.QueryExperimentApprovedRecord(eid)
	if err != nil {
		if err != gorm.ErrRecordNotFound {
			log.Error("query experiment approved record failed", zap.Error(err))
		}
		return exp
	}
	ApprovedByModel, err := model.GetUserByID(uint(record.ApprovedByID))
	if err != nil {
		log.Error("get user by id failed", zap.Error(err))
		return exp
	}
	exp.ApprovedBy = session.CtxUser{
		Username: ApprovedByModel.UserName,
		UserID:   ApprovedByModel.ID,
		Email:    ApprovedByModel.Email,
	}
	return exp
}

func (exp *Experiment) buildStatus(status chaos.ExperimentStatus, eid uint64) *Experiment {
	exp.Status = BuildExperimentStatus(status, eid)
	return exp
}

type ExperimentStatus = string

const (
	ExperimentWaitingApprove ExperimentStatus = "Waiting Approve"
	ExperimentDraft          ExperimentStatus = "Draft"
	ExperimentRunning        ExperimentStatus = "Running"
	ExperimentDone           ExperimentStatus = "Done"
	ExperimentFailed         ExperimentStatus = "Failed"
	ExperimentDeleted        ExperimentStatus = "Deleted"
)

var ExperimentStatus2StatusName = map[chaos.ExperimentStatus]ExperimentStatus{
	chaos.ExperimentStatus_EXPERIMENT_STATUS_DRAFT:   ExperimentDraft,
	chaos.ExperimentStatus_EXPERIMENT_STATUS_RUNNING: ExperimentRunning,
	chaos.ExperimentStatus_EXPERIMENT_STATUS_SUCCESS: ExperimentDone,
	chaos.ExperimentStatus_EXPERIMENT_STATUS_FAILED:  ExperimentFailed,
	chaos.ExperimentStatus_EXPERIMENT_STATUS_DELETED: ExperimentDeleted,
}

func IsApproved(eid uint64) bool {
	_, err := chaosmodel.QueryExperimentApprovedRecord(eid)
	if err != nil && err != gorm.ErrRecordNotFound {
		log.Error("query experiment approved record failed", zap.Error(err))
	}
	return err == nil
}

func BuildExperimentStatus(rawStatus chaos.ExperimentStatus, eid uint64) ExperimentStatus {
	status := ExperimentStatus2StatusName[rawStatus]
	if status == ExperimentDraft && !IsApproved(eid) {
		return ExperimentWaitingApprove
	}
	return status
}

type Action struct {
	Kind        ActionKind  `json:"kind" enums:"pod_shutdown"`
	PodShutdown PodShutdown `json:"pod_shutdown"`
}

type ActionKind = string

const KindPodShutdown ActionKind = "pod_shutdown"

var ActionKind2KindName = map[chaos.ActionKind]ActionKind{
	chaos.ActionKind_ACTION_KIND_POD_SHUTDOWN: KindPodShutdown,
}

func reverseActionKind2Name(m map[chaos.ActionKind]ActionKind) map[ActionKind]chaos.ActionKind {
	n := make(map[ActionKind]chaos.ActionKind, len(m))
	for k, v := range m {
		n[v] = k
	}
	return n
}

var KindName2ActionKind = reverseActionKind2Name(ActionKind2KindName)

func FromExperimentAction(expAction *chaos.ExperimentAction) Action {
	var action Action
	action.Kind = ActionKind2KindName[expAction.GetType()]
	if expAction.GetPodShutdown() != nil {
		action.PodShutdown = FromActionPodShutdown(expAction.GetPodShutdown())
	}
	return action
}

func (a Action) ToExperimentAction() *chaos.ExperimentAction {
	var action chaos.ExperimentAction
	action.Type = KindName2ActionKind[a.Kind]
	if a.Kind == "pod_shutdown" {
		action.PodShutdown = a.PodShutdown.ToActionPodShutdown()
	}
	return &action
}

type PodShutdown struct {
	Target Target `json:"target"`
}

func FromActionPodShutdown(action *chaos.ExperimentAction_ActionPodShutdown) PodShutdown {
	return PodShutdown{
		Target: FromExperimentTarget(action.GetTarget()),
	}
}

func (p PodShutdown) ToActionPodShutdown() *chaos.ExperimentAction_ActionPodShutdown {
	return &chaos.ExperimentAction_ActionPodShutdown{
		Target: p.Target.ToExperimentTarget(),
	}
}

type Target struct {
	Mode      string          `json:"mode" enums:"random,specified"`
	Component TargetComponent `json:"component" enums:"tidb,tikv,pd"`
	Name      string          `json:"name"`
}

var TargetMode2ModeName = map[chaos.TargetMode]string{
	chaos.TargetMode_TARGET_MODE_RANDOM:    "random",
	chaos.TargetMode_TARGET_MODE_SPECIFIED: "specified",
}

type TargetComponent = string

const (
	TargetComponentTiDB TargetComponent = "tidb"
	TargetComponentTiKV TargetComponent = "tikv"
	TargetComponentPd   TargetComponent = "pd"
)

var TargetComponent2ComponentName = map[chaos.TargetComponent]TargetComponent{
	chaos.TargetComponent_TARGET_COMPONENT_TIDB: TargetComponentTiDB,
	chaos.TargetComponent_TARGET_COMPONENT_TIKV: TargetComponentTiKV,
	chaos.TargetComponent_TARGET_COMPONENT_PD:   TargetComponentPd,
}

func reverseTargetMode2Name(m map[chaos.TargetMode]string) map[string]chaos.TargetMode {
	n := make(map[string]chaos.TargetMode, len(m))
	for k, v := range m {
		n[v] = k
	}
	return n
}

var ModeName2TargetMode = reverseTargetMode2Name(TargetMode2ModeName)

func reverseTargetComponent2Name(m map[chaos.TargetComponent]TargetComponent) map[TargetComponent]chaos.TargetComponent {
	n := make(map[TargetComponent]chaos.TargetComponent, len(m))
	for k, v := range m {
		n[v] = k
	}
	return n
}

var ComponentName2TargetComponent = reverseTargetComponent2Name(TargetComponent2ComponentName)

func FromExperimentTarget(action *chaos.ExperimentTarget) Target {
	if action == nil {
		return Target{}
	}
	return Target{
		Mode:      TargetMode2ModeName[action.GetMode()],
		Component: TargetComponent2ComponentName[action.GetComponent()],
		Name:      action.GetName(),
	}
}

func (t Target) ToExperimentTarget() *chaos.ExperimentTarget {
	name := deepcopy.Copy(t.Name).(string)
	return &chaos.ExperimentTarget{
		Mode:      ModeName2TargetMode[t.Mode],
		Component: ComponentName2TargetComponent[t.Component],
		Name:      &name,
	}
}
