package main

import (
	"errors"
	"fmt"
	"github.com/tidbcloud/operation-api/util"
	"go.uber.org/zap"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/mitchellh/mapstructure"
	"github.com/pingcap/log"
	"github.com/tidbcloud/operation-api/api"
	"github.com/tidbcloud/operation-api/config"
	sync2infra "github.com/tidbcloud/operation-api/crd"
	"github.com/tidbcloud/operation-api/crd/helper"
	cloudv1 "github.com/tidbcloud/operationlet/api/v1"

	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

var AuditLogger *zap.Logger

func init() {
	var err error
	conf := &log.Config{Level: "info", File: log.FileLogConfig{Filename: config.AuditLogFile}}
	AuditLogger, _, err = log.InitLogger(conf)
	if err != nil {
		panic(err)
	}

	gl, props, err := log.InitLogger(config.NewDefaultConfig())
	if err != nil {
		panic(err)
	}
	log.ReplaceGlobals(gl, props)
}

type OperationSvc struct {
	InfraClient *api.InfraClient
	Client      client.Client
	Engine      *gin.Engine
}

func NewOperationSvc(infraClient *api.InfraClient, client client.Client, e *gin.Engine) *OperationSvc {
	return &OperationSvc{
		InfraClient: infraClient,
		Client:      client,
		Engine:      e,
	}
}

func (svc *OperationSvc) RegisterAPI() {
	svc.OperationsAPI()
	svc.ApproveAPI()
	svc.DetailedAPI()
	svc.ListAPI()
}

func (svc *OperationSvc) MiddleWare() {
	svc.Engine.Use(auditMiddleWare())
}

func (svc *OperationSvc) OperationsAPI() {
	svc.Engine.POST("/v1/operations/job", func(ctx *gin.Context) {
		jsonData := make(map[string]interface{})
		ctx.BindJSON(&jsonData)
		opName := fmt.Sprint(jsonData["op_name"])
		email, err := util.ParseCookie(ctx)
		if err != nil {
			log.Error("Create infra CRD failed!", zap.Error(err))
			ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		// Get the job args.
		args := jsonData["args"].(map[string]interface{})
		cfg := &mapstructure.DecoderConfig{
			Metadata: nil,
			TagName:  "json",
		}
		switch jsonData["type"].(string) {
		case RestartInstance:
			// TODO: use interface to clean the code.
			log.Info("Create restart instance job")
			job := &sync2infra.RestartInstance{}
			cfg.Result = job
			decoder, _ := mapstructure.NewDecoder(cfg)
			decoder.Decode(args)
			job.OpName = opName
			crd := sync2infra.RestartInstance2Crd(job, email)
			err := svc.InfraClient.CreateOperationsCommandJob(ctx, crd)
			if err != nil {
				log.Error("Create infra CRD failed!", zap.Error(err))
				ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				return
			}
		}
		ctx.JSON(http.StatusOK, gin.H{"op_name": opName, "status": cloudv1.JobPhaseWaiting, "type": jsonData["type"]})
	})
}

func (svc *OperationSvc) ListAPI() {
	svc.Engine.GET("/v1/operations/list", func(ctx *gin.Context) {
		log.Info("List command job!")
		list := &cloudv1.OperationCommandJobList{}
		clusterId := ctx.Query("cluster_id")
		if clusterId == "" {
			log.Error("Get command job list failed!", zap.Error(errors.New("no cluster id")))
			ctx.JSON(http.StatusBadRequest, gin.H{"error": "Need cluster id"})
		}
		if err := svc.Client.List(ctx, list, client.InNamespace(sync2infra.TiDBPrefix+clusterId)); err != nil {
			log.Error("Get command job list failed!", zap.Error(err))
			ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		}
		jobList := helper.CrdToJobList(list)
		body := map[string]interface{}{
			"op_list": jobList,
		}
		ctx.JSON(http.StatusOK, body)
	})
}

func (svc *OperationSvc) DetailedAPI() {
	svc.Engine.GET("/v1/operations/detail", func(ctx *gin.Context) {
		name := ctx.Query("name")
		clusterId := ctx.Query("cluster_id")
		if name == "" || clusterId == "" {
			log.Error("Get command job detailed failed!")
			ctx.JSON(http.StatusBadRequest, gin.H{"error": "Get command job detailed failed!"})
		} else {
			crd := &cloudv1.OperationCommandJob{}
			ns := types.NamespacedName{
				Namespace: sync2infra.TiDBPrefix + clusterId,
				Name:      name,
			}
			if err := svc.Client.Get(ctx, ns, crd); err != nil {
				log.Error("Get command job detailed failed!", zap.Error(err))
				ctx.JSON(http.StatusBadRequest, gin.H{"error": "Get command job detailed failed!"})
				return
			}
			ctx.JSON(http.StatusOK, helper.CrdToJob(crd))
		}
	})
}

func (svc *OperationSvc) ApproveAPI() {
	svc.Engine.POST("/v1/approve", func(ctx *gin.Context) {
		jsonData := make(map[string]interface{})
		ctx.BindJSON(&jsonData)
		email, err := util.ParseCookie(ctx)
		if err != nil {
			log.Error("parse cookies error.", zap.Error(err))
			ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		log.Info("Approver approves the job", zap.String("Approver", email))
		crd := &cloudv1.OperationAuthorizeService{}
		ns := types.NamespacedName{
			Namespace: sync2infra.TiDBPrefix + "_" + sync2infra.Admin,
			Name:      "op_approvers",
		}
		err = svc.Client.Get(ctx, ns, crd)
		if err != nil {
			log.Error("Get approvers list failed!", zap.Error(err))
			ctx.JSON(http.StatusBadRequest, gin.H{"error": "Get approvers list failed!"})
			return
		}
		if email == "" || !isApprover(email, crd.Spec.ApproverList) {
			ctx.JSON(http.StatusForbidden, gin.H{"Forbidden": "You have not auth to approve job"})
			return
		}
		name := fmt.Sprint(jsonData["op_name"])
		clusterId := fmt.Sprint(jsonData["cluster_id"])
		log.Info("", zap.String("name", name), zap.String("cluster_id", clusterId))
		job := &cloudv1.OperationCommandJob{}
		ns = types.NamespacedName{
			Namespace: sync2infra.TiDBPrefix + clusterId,
			Name:      name,
		}
		if err := svc.Client.Get(ctx, ns, job); err != nil {
			log.Error("Get job CRD failed!", zap.Error(err))
			ctx.JSON(http.StatusBadRequest, gin.H{"error": "Get job CRD failed!"})
			return
		}
		if job.Spec.Email == email {
			// Approvers can not approve the job themselves.
			ctx.JSON(http.StatusForbidden, gin.H{"Forbidden": "You have not auth to approve job"})
			return
		}
		if job.Status.Phase == cloudv1.JobPhaseWaiting {
			if err := svc.InfraClient.UpdatePhase(ctx, job, cloudv1.JobPhasePending); err != nil {
				log.Error("Approve job CRD failed!", zap.Error(err))
				ctx.JSON(http.StatusBadRequest, gin.H{"error": "Approve job CRD failed!"})
				return
			}
		}
		log.Info("Approve job CRD successful!", zap.String("cluster_id", clusterId),
			zap.String("approver", email), zap.String("op_name", name))
		ctx.JSON(http.StatusOK, job)
		return
	})
}

func isApprover(s string, list []string) bool {
	for _, str := range list {
		if s == str {
			return true
		}
	}
	return false
}

func auditMiddleWare() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		req := ctx.Request
		url := req.URL.String()
		uri := req.RequestURI
		AuditLogger.Info("new request in", zap.String("Url", url), zap.String("Uri", uri))
		start := time.Now()
		ctx.Next()
		AuditLogger.Info("request done", zap.String("Url", url), zap.String("Uri", uri), zap.Duration("usedTime", time.Since(start)))
	}
}
