package oldreport

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/pingcap/log"
	"go.uber.org/zap"
	"gorm.io/gorm"

	"github.com/pingcap-inc/clinic/pkg/api/common"
	error2 "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/datasource"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/db/model"
	"github.com/pingcap-inc/clinic/pkg/o11y"
	"github.com/pingcap-inc/clinic/pkg/trafficjobclient"
	"github.com/pingcap-inc/clinic/pkg/types"
)

// @Summary		Touch New Traffic Comparison Report Job
// @Tags			Report
// @Description	touch report in on cluster
// @Accept			json
// @Param			x-csrf-token	header		string							true	"get value from login.ValidationResp response"
// @Param			oid				path		string							true	"organization id"
// @Param			cid				path		string							true	"cluster id"
// @Param			report-param	body		TouchTrafficComparisonReportReq	true	"traffic comparison report param"
// @Success		200				{object}	ReportResp
// @Router			/compute-engine/{oid}/{cid}/trafficcomparison [post]
func (rs *ReportService) TouchTrafficComparisonReport(c *gin.Context) {
	req := &TouchTrafficComparisonReportReq{}
	err := c.BindJSON(req)
	if err != nil {
		log.Error("bind json failed", zap.Error(err))
		_ = c.Error(error2.ErrBadRequest.New("bad parameter"))
		return
	}
	clusterID := c.Param("cid")
	if req.ClusterID1 != clusterID {
		_ = c.Error(error2.ErrBadRequest.New("bad parameter"))
		return
	}
	user, org, _, ok, err := common.CanReadCluster(c, rs.rbac, req.ClusterID1)
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("%s", err.Error()))
		return
	}
	if !ok {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}
	if !user.Internal {
		_ = c.Error(error2.ErrPreConditionFailed.NewWithNoMessage())
		return
	}

	cluster1, err := model.GetAnyClusterWithOrg(req.ClusterID1)
	if err != nil {
		log.Error("failed to get cluster1 from db", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
		return
	}
	cluster2, err := model.GetAnyClusterWithOrg(req.ClusterID2)
	if err != nil {
		log.Error("failed to get cluster2 from db", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
		return
	}
	cluster1URLs, err := rs.getClusterURLs(c.Request.Context(), cluster1)
	if err != nil {
		log.Error("failed to get cluster1 urls", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
		return
	}
	cluster2URLs, err := rs.getClusterURLs(c.Request.Context(), cluster2)
	if err != nil {
		log.Error("failed to get cluster2 urls", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
		return
	}

	var physicalClusterID1, dataPlaneID1 string
	if cluster1.DeployType == o11y.PremiumType {
		clusterID1, err := strconv.ParseUint(req.ClusterID1, 10, 64)
		if err != nil {
			log.Error("strconv clusterID1 failed", zap.Error(err))
			_ = c.Error(error2.ErrBadRequest.New("bad cluster id"))
			return
		}
		mgmtCluster1, err := datasource.RealtimeGetMgmtCluster(user, clusterID1)
		if err != nil {
			log.Error("get mgmt cluster1 failed", zap.Error(err))
			_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
			return
		}
		if mgmtCluster1.GetNextGenCluster() == nil {
			log.Error("get mgmt cluster1 failed: empty nextgen cluster")
			_ = c.Error(error2.ErrInternal.NewWithNoMessage())
			return
		}
		physicalClusterID1 = mgmtCluster1.GetNextGenCluster().SharedPoolId
		dataPlaneID1 = mgmtCluster1.GetNextGenCluster().DataplaneId
	}
	var physicalClusterID2, dataPlaneID2 string
	if cluster2.DeployType == o11y.PremiumType {
		clusterID2, err := strconv.ParseUint(req.ClusterID2, 10, 64)
		if err != nil {
			log.Error("strconv clusterID2 failed", zap.Error(err))
			_ = c.Error(error2.ErrBadRequest.New("bad cluster id"))
			return
		}
		mgmtCluster2, err := datasource.RealtimeGetMgmtCluster(user, clusterID2)
		if err != nil {
			log.Error("get mgmt cluster2 failed", zap.Error(err))
			_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
			return
		}
		if mgmtCluster2.GetNextGenCluster() == nil {
			log.Error("get mgmt cluster2 failed: empty nextgen cluster")
			_ = c.Error(error2.ErrInternal.NewWithNoMessage())
			return
		}
		physicalClusterID2 = mgmtCluster2.GetNextGenCluster().SharedPoolId
		dataPlaneID2 = mgmtCluster2.GetNextGenCluster().DataplaneId
	}

	rid := uuid.NewString()
	startTime1 := time.Unix(int64(req.StartTime1), 0).UTC().Format(time.RFC3339)
	startTime2 := time.Unix(int64(req.StartTime2), 0).UTC().Format(time.RFC3339)
	endTime1 := time.Unix(int64(req.EndTime1), 0).UTC().Format(time.RFC3339)
	endTime2 := time.Unix(int64(req.EndTime2), 0).UTC().Format(time.RFC3339)
	tjReq := &trafficjobclient.Request{
		ReportName: req.Name,
		ClusterA: trafficjobclient.Cluster{
			Region:            cluster1.Region,
			Provider:          cluster1.Provider,
			DeployType:        cluster1.DeployType,
			TenantID:          fmt.Sprintf("%d", cluster1.Organization.DbaasOrgID),
			ProjectID:         cluster1.ProjectID,
			ClusterID:         req.ClusterID1,
			PhysicalClusterID: physicalClusterID1,
			DataPlaneID:       dataPlaneID1,
			Alias:             req.ClusterID1,
			URLs:              *cluster1URLs,
			TimeRange: trafficjobclient.TimeRange{
				Start: startTime1,
				End:   endTime1,
			},
		},
		ClusterB: trafficjobclient.Cluster{
			Region:            cluster2.Region,
			Provider:          cluster2.Provider,
			DeployType:        cluster2.DeployType,
			TenantID:          fmt.Sprintf("%d", cluster2.Organization.DbaasOrgID),
			ProjectID:         cluster2.ProjectID,
			ClusterID:         req.ClusterID2,
			PhysicalClusterID: physicalClusterID2,
			DataPlaneID:       dataPlaneID2,
			Alias:             req.ClusterID2,
			URLs:              *cluster2URLs,
			TimeRange: trafficjobclient.TimeRange{
				Start: startTime2,
				End:   endTime2,
			},
		},
		CallbackURL: "",
		Options: trafficjobclient.Options{
			ReportID:       rid,
			TopN:           100,
			DigestDiffKeys: []string{"slowQueryDigestDiff", "topSqlDigestDiff", "slowQueryCountDiff"},
		},
	}
	arg, err := json.Marshal(tjReq)
	if err != nil {
		log.Error("marshal report arg failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
		return
	}
	err = model.InsertReport(&db.Report{
		UUID:       rid,
		OrgID:      org.ID,
		ClusterID:  clusterID,
		CreatorID:  user.UserID,
		ReportType: "traffic_comparison",
		Status:     string(db.StatusPending),
		Arg:        string(arg),
	})
	if err != nil {
		log.Error("insert reports failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
		return
	}
	c.JSON(http.StatusOK, ReportResp{
		TaskID: rid,
	})
}

// @Summary		list traffic comparison report
// @Tags			Report
// @Description	list traffic comparison report
// @Accept			json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			oid				path		string	true	"organization id"
// @Param			cid				path		string	true	"cluster id"
// @Param			limit			query		int		false	"the count per page"
// @Param			page			query		int		false	"the page index"
// @Param			brief			query		bool	false	"if need data"
// @Success		200				{object}	ListTrafficComparisonReportResp
// @Router			/compute-engine/{oid}/{cid}/trafficcomparison [get]
func (rs *ReportService) ListTrafficComparisonReport(c *gin.Context) {
	_, org, _, ok, err := common.CanReadCluster(c, rs.rbac, c.Param("cid"))
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("%s", err.Error()))
		return
	}
	if !ok {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}

	var (
		page  int
		limit int
	)
	pageParam := c.Query("page")
	if pageParam == "" {
		pageParam = "0"
	}
	page, err = strconv.Atoi(pageParam)
	if err != nil {
		log.Error("parse param failed", zap.Error(err))
		_ = c.Error(error2.ErrBadRequest.New("parse aren't int"))
	}

	limitParam := c.Query("limit")
	if limitParam == "" {
		limitParam = "0"
	}
	limit, err = strconv.Atoi(limitParam)
	if err != nil {
		log.Error("parse param failed", zap.Error(err))
		_ = c.Error(error2.ErrBadRequest.New("parse aren't int"))
	}

	reports, total, err := model.ListReportsOverview(&db.Report{
		OrgID:      org.ID,
		ClusterID:  c.Param("cid"),
		ReportType: "traffic_comparison",
	}, page, limit)
	if err != nil {
		log.Error("list all reports failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
		return
	}
	resp := ListTrafficComparisonReportResp{
		Total: uint(total),
	}
	for _, report := range reports {
		var args trafficjobclient.Request
		err = json.Unmarshal([]byte(report.Arg), &args)
		if err != nil {
			_ = c.Error(error2.ErrInternal.New("fail to unmarshal args"))
			return
		}

		var userName string
		u, err := model.GetUserByID(report.CreatorID)
		if err != nil {
			log.Error("get user failed", zap.Uint("user", report.CreatorID))
		}

		if u != nil {
			userName = u.UserName
		}
		startTime1, _ := time.Parse(time.RFC3339, args.ClusterA.TimeRange.Start)
		endTime1, _ := time.Parse(time.RFC3339, args.ClusterA.TimeRange.End)
		startTime2, _ := time.Parse(time.RFC3339, args.ClusterB.TimeRange.Start)
		endTime2, _ := time.Parse(time.RFC3339, args.ClusterB.TimeRange.End)
		resp.Reports = append(resp.Reports, TrafficComparisonReport{
			ID:           report.UUID,
			Name:         args.ReportName,
			ClusterID1:   args.ClusterA.ClusterID,
			StartTime1:   uint(startTime1.Unix()),
			EndTime1:     uint(endTime1.Unix()),
			ClusterID2:   args.ClusterB.ClusterID,
			StartTime2:   uint(startTime2.Unix()),
			EndTime2:     uint(endTime2.Unix()),
			OverAllDiff:  0,
			DatetimeDiff: 0,
			Status:       report.Status,
			Creator:      userName,
			CreatedAt:    uint(report.CreatedAt.Unix()),
			Data:         nil,
		})
	}
	c.JSON(http.StatusOK, resp)
}

// @Summary		get traffic comparison report
// @Tags			Report
// @Description	list traffic comparison report
// @Accept			json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			oid				path		string	true	"organization id"
// @Param			cid				path		string	true	"cluster id"
// @Param			rid				path		string	true	"report id"
// @Param			brief			query		bool	false	"if need data"
// @Success		200				{object}	TrafficComparisonReport
// @Router			/compute-engine/{oid}/{cid}/trafficcomparison/{rid} [get]
func (rs *ReportService) GetTrafficComparisonReport(c *gin.Context) {
	_, _, _, ok, err := common.CanReadCluster(c, rs.rbac, c.Param("cid"))
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("%s", err.Error()))
		return
	}
	if !ok {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}

	rid := c.Param("rid")
	report, err := model.GetReportByUUID(rid)
	if err == gorm.ErrRecordNotFound {
		_ = c.Error(error2.ErrNotFound.New("report not exist"))
		return
	}
	if err != nil {
		log.Error("GetReportByID failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("db connection failed"))
		return
	}

	var args trafficjobclient.Request
	err = json.Unmarshal([]byte(report.Arg), &args)
	if err != nil {
		log.Error("Unmarshal arg failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("get arg failed"))
		return
	}

	var userName string
	u, err := model.GetUserByID(report.CreatorID)
	if err != nil {
		log.Error("get user failed", zap.Uint("user", report.CreatorID))
	}
	if u != nil {
		userName = u.UserName
	}

	ctx := c.Request.Context()
	metricsDiff, err := rs.tjStore.GetMetricsDiff(ctx, &args)
	if err != nil {
		log.Error("get metrics diff failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("get metrics diff failed"))
		return
	}
	sqlDiff, err := rs.tjStore.GetSQLDiff(ctx, &args)
	if err != nil {
		log.Error("get sql diff failed", zap.Error(err))
	}

	startTime1, _ := time.Parse(time.RFC3339, args.ClusterA.TimeRange.Start)
	endTime1, _ := time.Parse(time.RFC3339, args.ClusterA.TimeRange.End)
	startTime2, _ := time.Parse(time.RFC3339, args.ClusterB.TimeRange.Start)
	endTime2, _ := time.Parse(time.RFC3339, args.ClusterB.TimeRange.End)
	c.JSON(http.StatusOK, TrafficComparisonReport{
		ID:           report.UUID,
		Name:         args.ReportName,
		ClusterID1:   args.ClusterA.ClusterID,
		StartTime1:   uint(startTime1.Unix()),
		EndTime1:     uint(endTime1.Unix()),
		ClusterID2:   args.ClusterB.ClusterID,
		StartTime2:   uint(startTime2.Unix()),
		EndTime2:     uint(endTime2.Unix()),
		OverAllDiff:  calcOverAllDiff(startTime1, endTime1, startTime2, endTime2),
		DatetimeDiff: calcDatetimeDiff(startTime1, endTime1, startTime2, endTime2),
		Status:       report.Status,
		Creator:      userName,
		CreatedAt:    uint(report.CreatedAt.Unix()),
		Data: &TrafficComparisonReportData{
			MetricsDiff: metricsDiff,
			SQLDiff:     sqlDiff,
		},
	})
}

// @Summary		delete traffic comparison report
// @Tags			Report
// @Description	delete traffic comparison report
// @Accept			json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			oid				path		string	true	"organization id"
// @Param			cid				path		string	true	"cluster id"
// @Param			rid				path		string	true	"report id"
// @Success		200				{string}	success
// @Router			/compute-engine/{oid}/{cid}/trafficcomparison/{rid} [delete]
func (rs *ReportService) DeleteTrafficComparisonReport(c *gin.Context) {
	_, _, _, ok, err := common.CanReadCluster(c, rs.rbac, c.Param("cid"))
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("%s", err.Error()))
		return
	}
	if !ok {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}

	rid := c.Param("rid")
	if rid == "" {
		_ = c.Error(error2.ErrBadRequest.New("empty rid"))
		return
	}
	err = model.DeleteReportByUUID(rid)
	if err == gorm.ErrRecordNotFound {
		_ = c.Error(error2.ErrNotFound.New("report not exist"))
		return
	}
	if err != nil {
		log.Error("delete report failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("db connection failed"))
		return
	}

	c.JSON(http.StatusOK, common.SuccessResponse())
}

func (rs *ReportService) getClusterURLs(ctx context.Context, cluster *db.Cluster) (*types.ClusterURLs, error) {
	return rs.urlsResolver.Resolve(ctx, cluster)
}

func calcOverAllDiff(start1, end1, start2, end2 time.Time) int {
	return int(end1.Sub(start1).Seconds())
}

func calcDatetimeDiff(start1, end1, start2, end2 time.Time) int {
	return int(start2.Sub(start1).Seconds())
}
