package api

import (
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/joomcode/errorx"
	"github.com/pingcap/log"
	"github.com/tidbcloud/chaos-svc/idl/pbgen/proto/chaos"
	"go.buf.build/protocolbuffers/go/linuxgit/tidbcloud/base"
	"go.uber.org/zap"

	"github.com/pingcap-inc/chaosAPI/pkg/infra/client"
	"github.com/pingcap-inc/chaosAPI/pkg/infra/db"
	"github.com/pingcap-inc/chaosAPI/pkg/infra/db/model"
	"github.com/pingcap-inc/chaosAPI/pkg/wrapper/ginWrapper"
)

type ExperimentResponse struct {
	// Total number of experiments
	Total       int64        `json:"total"`
	Experiments []Experiment `json:"experiments"`
}

func BuildListExperimentsResp(rsp *chaos.ListExperimentsResp) ExperimentResponse {
	var s ExperimentResponse
	s.Total = rsp.GetTotal()
	for _, e := range rsp.GetExperiments() {
		s.Experiments = append(s.Experiments, BuildExperiment(e))
	}
	return s
}

// ListChaosExperiments
// @Summary List chaos experiments
// @Tags Chaos
// @Description List chaos experiments
// @Produce json
// @Param oid path string true "organization id"
// @Param cid path string true "cluster id"
// @Param page query int false "the page index" minimum(1) default(1)
// @Param size query int false "the page size" minimum(1) default(10)
// @Success 200 {object} ExperimentResponse
// @Failure 400 {object} error
// @Failure 500 {object} error
// @Router /chaos/api/orgs/{oid}/clusters/{cid} [get]
func (s *Service) ListChaosExperiments(c *gin.Context) {
	args, err := GetArgs(c)
	if err != nil {
		ginWrapper.BadRequest(c, errorx.ExternalError.New("%s", err.Error()))
		return
	}

	var page int64
	if !ginWrapper.HasQuery(c, "page") {
		page = 1
	} else {
		page, err = ginWrapper.IntegerQuery[int64](c, "page").Get()
	}
	if err != nil {
		ginWrapper.BadRequest(c, errorx.ExternalError.New("%s", err.Error()))
		return
	}

	var size int64
	if !ginWrapper.HasQuery(c, "size") {
		size = 10
	} else {
		size, err = ginWrapper.IntegerQuery[int64](c, "size").Get()
	}
	if err != nil {
		ginWrapper.BadRequest(c, errorx.ExternalError.New("%s", err.Error()))
		return
	}

	deleted := false
	grpcRsp, _ := s.ChaosSvc.ListExperiments(c, &chaos.ListExperimentsRequest{
		Filter: &chaos.ExperimentFilter{
			ClusterIds: []uint64{args.ClusterID},
			// If you read the code of chaos-svc,
			// you will find if you don't set the ExperimentIds,
			// it will return all experiments.
			ExperimentIds: []uint64{},
			Deleted:       &deleted,
		},
		Pager: &base.Pager{
			Page:     page,
			PageSize: size,
		},
		Sorters: nil,
	})
	if client.ToGRPCError(grpcRsp.GetBaseResp()) != nil {
		ginWrapper.InternalServerError(c, client.ToGRPCError(grpcRsp.GetBaseResp()))
		return
	}

	expRsp := BuildListExperimentsResp(grpcRsp)
	c.JSON(http.StatusOK, expRsp)
}

// GetChaosExperiment
// @Summary Get chaos experiment
// @Tags Chaos
// @Description Get chaos experiment
// @Produce json
// @Param oid path string true "organization id"
// @Param cid path string true "cluster id"
// @Param eid path string true "experiment id"
// @Success 200 {object} Experiment
// @Failure 400 {object} error
// @Failure 500 {object} error
// @Router /chaos/api/orgs/{oid}/clusters/{cid}/experiments/{eid} [get]
func (s *Service) GetChaosExperiment(c *gin.Context) {
	args, err := GetArgs(c)
	if err != nil {
		ginWrapper.BadRequest(c, errorx.ExternalError.New("%s", err.Error()))
		return
	}

	eid := args.ExperimentID.MustGet()

	rsp, _ := s.ChaosSvc.GetExperiment(c, &chaos.GetExperimentRequest{
		ClusterId:    args.ClusterID,
		ProjectId:    args.ProjectID,
		ExperimentId: eid,
	})

	if client.ToGRPCError(rsp.GetBaseResp()) != nil {
		ginWrapper.InternalServerError(c, client.ToGRPCError(rsp.GetBaseResp()))
		return
	}

	c.JSON(http.StatusOK, BuildExperiment(rsp.GetExperiment()))
}

type CreateExperimentRequest struct {
	Action  Action  `json:"action"`
	Summary *string `json:"summary"`
}

// CreateDraftChaosExperiment
// @Summary Create draft chaos experiment
// @Tags Chaos
// @Description Create draft chaos experiment
// @Produce json
// @Param oid path string true "organization id"
// @Param cid path string true "cluster id"
// @Param experiment body CreateExperimentRequest true "create draft experiment"
// @Success 200 {object} Experiment
// @Failure 400 {object} error
// @Failure 500 {object} error
// @Router /chaos/api/draft/orgs/{oid}/clusters/{cid} [post]
func (s *Service) CreateDraftChaosExperiment(c *gin.Context) {
	args, err := GetArgs(c)
	if err != nil {
		ginWrapper.BadRequest(c, errorx.ExternalError.New("%s", err.Error()))
		return
	}

	var req CreateExperimentRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		ginWrapper.BadRequest(c, errorx.ExternalError.New("%s", err.Error()))
		return
	}

	rsp, _ := s.ChaosSvc.CreateExperiment(c, &chaos.CreateExperimentRequest{
		ProjectId: args.ProjectID,
		ClusterId: args.ClusterID,
		Draft:     true,
		Action:    req.Action.ToExperimentAction(),
		Summary:   req.Summary,
		UserId:    args.UserID,
	})

	if client.ToGRPCError(rsp.GetBaseResp()) != nil {
		ginWrapper.InternalServerError(c, client.ToGRPCError(rsp.GetBaseResp()))
		return
	}

	c.JSON(http.StatusOK, BuildExperiment(rsp.Experiment))
}

// ApproveDraftChaosExperiment
// @Summary Approve a draft chaos experiment
// @Tags Chaos
// @Description Approve a draft chaos experiment
// @Produce json
// @Param oid path string true "organization id"
// @Param cid path string true "cluster id"
// @Param eid path string true "experiment id"
// @Success 200 {object} nil
// @Failure 400 {object} error
// @Failure 500 {object} error
// @Router /chaos/api/approve/orgs/{oid}/clusters/{cid}/experiments/{eid} [post]
func (s *Service) ApproveDraftChaosExperiment(c *gin.Context) {
	args, err := GetArgs(c)
	if err != nil {
		ginWrapper.BadRequest(c, errorx.ExternalError.New("%s", err.Error()))
		return
	}

	eid := args.ExperimentID.MustGet()

	approved := IsApproved(eid)
	if approved {
		ginWrapper.BadRequest(c, errorx.ExternalError.New("experiment have has been approved"))
		return
	}

	rsp, _ := s.ChaosSvc.GetExperiment(c, &chaos.GetExperimentRequest{
		ClusterId:    args.ClusterID,
		ProjectId:    args.ProjectID,
		ExperimentId: eid,
	})

	if client.ToGRPCError(rsp.GetBaseResp()) != nil {
		ginWrapper.InternalServerError(c, client.ToGRPCError(rsp.GetBaseResp()))
		return
	}

	exp := rsp.GetExperiment()
	if exp == nil {
		ginWrapper.InternalServerError(c, errorx.ExternalError.New("fail to get experiment from chaos svc"))
		return
	}

	status := BuildExperimentStatus(exp.GetStatus(), eid)
	if status != ExperimentWaitingApprove {
		log.Info("experiment is not waiting approve", zap.String("status", status))
		ginWrapper.BadRequest(c,
			errorx.ExternalError.New("%s", fmt.Sprintf("experiment is not waiting approve, status %s", status)))
		return
	}

	if exp.GetCreatedById() == args.UserID {
		ginWrapper.BadRequest(c, errorx.ExternalError.New("experiment can't be approved by same user"))
		return
	}

	if err := model.InsertExperimentApprovedRecord(&db.ExperimentApproveRecord{
		ProjectID:    args.ProjectID,
		ClusterID:    args.ClusterID,
		ExperimentID: eid,
		CreatedByID:  exp.CreatedById,
		ApprovedByID: args.UserID,
	}); err != nil {
		ginWrapper.InternalServerError(c, errorx.ExternalError.New("%s", err.Error()))
		return
	}

	c.JSON(http.StatusOK, nil)
}

// StartChaosExperiment
// @Summary Start chaos experiment
// @Tags Chaos
// @Description Start chaos experiment
// @Produce json
// @Param oid path string true "organization id"
// @Param cid path string true "cluster id"
// @Param eid path string true "experiment id"
// @Success 200 {object} Experiment
// @Failure 400 {object} error
// @Failure 500 {object} error
// @Router /chaos/api/start/orgs/{oid}/clusters/{cid}/experiments/{eid} [post]
func (s *Service) StartChaosExperiment(c *gin.Context) {
	args, err := GetArgs(c)
	if err != nil {
		ginWrapper.BadRequest(c, errorx.ExternalError.New("%s", err.Error()))
		return
	}

	eid := args.ExperimentID.MustGet()

	rsp, _ := s.ChaosSvc.GetExperiment(c, &chaos.GetExperimentRequest{
		ClusterId:    args.ClusterID,
		ProjectId:    args.ProjectID,
		ExperimentId: eid,
	})

	if client.ToGRPCError(rsp.GetBaseResp()) != nil {
		ginWrapper.InternalServerError(c, client.ToGRPCError(rsp.GetBaseResp()))
		return
	}

	exp := rsp.GetExperiment()
	if exp == nil {
		ginWrapper.InternalServerError(c, errorx.ExternalError.New("fail to get experiment from chaos svc"))
		return
	}

	status := BuildExperimentStatus(exp.GetStatus(), exp.ExperimentId)
	if status != ExperimentDraft {
		log.Info("experiment is not draft", zap.String("status", status))
		ginWrapper.BadRequest(c, errorx.ExternalError.New("experiment is not draft, status %s", status))
		return
	}

	draft := false
	newRsp, _ := s.ChaosSvc.UpdateExperiment(c, &chaos.UpdateExperimentRequest{
		ProjectId:    args.ProjectID,
		ClusterId:    args.ClusterID,
		ExperimentId: eid,
		Draft:        &draft,
	})

	if client.ToGRPCError(rsp.GetBaseResp()) != nil {
		ginWrapper.InternalServerError(c, client.ToGRPCError(newRsp.GetBaseResp()))
		return
	}

	c.JSON(http.StatusOK, BuildExperiment(newRsp.Experiment))
}

type ExperimentInfo struct {
	Summary    *string `json:"summary"`
	ReportLink *string `json:"report_link"`
}

// UpdateExperimentInfo
// @Summary Update experiment info
// @Tags Chaos
// @Description Update experiment info
// @Produce json
// @Param oid path string true "organization id"
// @Param cid path string true "cluster id"
// @Param eid path string true "experiment id"
// @Param Info body ExperimentInfo true "experiment info"
// @Success 200 {object} nil
// @Failure 400 {object} error
// @Failure 500 {object} error
// @Router /chaos/api/summary/orgs/{oid}/clusters/{cid}/experiments/{eid} [post]
func (s *Service) UpdateExperimentInfo(c *gin.Context) {
	args, err := GetArgs(c)
	if err != nil {
		ginWrapper.BadRequest(c, errorx.ExternalError.New("%s", err.Error()))
		return
	}

	eid := args.ExperimentID.MustGet()

	var info ExperimentInfo
	if err := c.ShouldBindJSON(&info); err != nil {
		ginWrapper.BadRequest(c, errorx.ExternalError.New("%s", err.Error()))
		return
	}

	rsp, _ := s.ChaosSvc.UpdateExperiment(c, &chaos.UpdateExperimentRequest{
		ProjectId:    args.ProjectID,
		ClusterId:    args.ClusterID,
		ExperimentId: eid,
		Draft:        nil,
		Summary:      info.Summary,
		ReportLink:   info.ReportLink,
		BaseReq:      nil,
	})

	if client.ToGRPCError(rsp.GetBaseResp()) != nil {
		ginWrapper.InternalServerError(c, client.ToGRPCError(rsp.GetBaseResp()))
		return
	}

	c.JSON(http.StatusOK, nil)
}

// DeleteChaosExperiment
// @Summary Delete chaos experiment
// @Tags Chaos
// @Description Delete chaos experiment
// @Produce json
// @Param oid path string true "organization id"
// @Param cid path string true "cluster id"
// @Param eid path string true "experiment id"
// @Success 200 {object} nil
// @Failure 400 {object} error
// @Failure 500 {object} error
// @Router /chaos/api/orgs/{oid}/clusters/{cid}/experiments/{eid} [delete]
func (s *Service) DeleteChaosExperiment(c *gin.Context) {
	args, err := GetArgs(c)
	if err != nil {
		ginWrapper.BadRequest(c, errorx.ExternalError.New("%s", err.Error()))
		return
	}

	eid := args.ExperimentID.MustGet()

	rsp, _ := s.ChaosSvc.DeleteExperiment(c, &chaos.DeleteExperimentRequest{
		ProjectId:    args.ProjectID,
		ClusterId:    args.ClusterID,
		ExperimentId: eid,
	})

	if client.ToGRPCError(rsp.GetBaseResp()) != nil {
		ginWrapper.InternalServerError(c, client.ToGRPCError(rsp.GetBaseResp()))
		return
	}
	c.JSON(http.StatusOK, nil)
}

type ActionSchema struct {
	Kind      ActionKind        `json:"kind"`
	Component []TargetComponent `json:"component"`
}

// ListExperimentActionSchema
// @Summary List experiment actions
// @Tags Chaos
// @Description List experiment actions
// @Produce json
// @Success 200 {object} []ActionSchema
// @Router /chaos/api/actions/schema [get]
func (s *Service) ListExperimentActionSchema(c *gin.Context) {
	kinds := []ActionKind{
		KindPodShutdown,
	}
	components := []TargetComponent{
		TargetComponentTiDB,
		TargetComponentTiKV,
		TargetComponentPd,
	}
	var actionSchemas []ActionSchema
	for _, kind := range kinds {
		actionSchemas = append(actionSchemas, ActionSchema{
			Kind:      kind,
			Component: components,
		})
	}
	c.JSON(http.StatusOK, actionSchemas)
}

type Component struct {
	Tidb    []PodInfo `json:"tidb"`
	TiKV    []PodInfo `json:"tikv"`
	Pd      []PodInfo `json:"pd"`
	Tiflash []PodInfo `json:"tiflash"`
}

func ToComponent(pods *chaos.TiDBComponentTopology) Component {
	if pods == nil {
		return Component{}
	}

	return Component{
		Tidb:    ToPodInfoList(pods.Tidb),
		TiKV:    ToPodInfoList(pods.Tikv),
		Pd:      ToPodInfoList(pods.Pd),
		Tiflash: ToPodInfoList(pods.Tiflash),
	}
}

type PodInfo struct {
	Name string `json:"name"`
}

func ToPodInfo(component *chaos.ComponentPodInfo) PodInfo {
	if component == nil {
		return PodInfo{}
	}
	return PodInfo{
		Name: component.Name,
	}
}

func ToPodInfoList(components []*chaos.ComponentPodInfo) []PodInfo {
	var podInfos []PodInfo
	for _, component := range components {
		if component == nil {
			continue
		}
		podInfos = append(podInfos, ToPodInfo(component))
	}
	return podInfos
}

// ListComponents
// @Summary List components
// @Tags Chaos
// @Description List components
// @Produce json
// @Param oid path string true "organization id"
// @Param cid path string true "cluster id"
// @Success 200 {object} Component
// @Failure 400 {object} error
// @Failure 500 {object} error
// @Router /chaos/api/components/orgs/{oid}/clusters/{cid} [get]
func (s *Service) ListComponents(c *gin.Context) {
	args, err := GetArgs(c)
	if err != nil {
		ginWrapper.BadRequest(c, errorx.ExternalError.New("%s", err.Error()))
		return
	}

	rsp, _ := s.ChaosSvc.GetTiDBComponentTopology(c, &chaos.GetTiDBComponentTopologyRequest{
		ProjectId: args.ProjectID,
		ClusterId: args.ClusterID,
	})

	if client.ToGRPCError(rsp.GetBaseResp()) != nil {
		ginWrapper.InternalServerError(c, client.ToGRPCError(rsp.GetBaseResp()))
		return
	}

	c.JSON(http.StatusOK, ToComponent(rsp.Topology))
}
