package oldreport

import (
	"fmt"
	"net/http"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"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/compute_engine"
	_const "github.com/pingcap-inc/clinic/pkg/const"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/db/model"
	"github.com/pingcap-inc/clinic/pkg/resource_limit"
)

// @Summary		Touch New Health Report Job
// @Tags			HealthReport
// @Description	touch Health 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			reportParams	body		HealthReportReq	true	"health report param"
// @Success		200				{object}	ReportResp
// @Router			/compute-engine/{oid}/{cid}/health [post]
func (rs *ReportService) TouchHealthReport(c *gin.Context) {
	user, 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 hrReq HealthReportReq
	if err := c.BindJSON(&hrReq); err != nil {
		_ = c.Error(error2.ErrBadRequest.New("bad param"))
		return
	}
	if err := hrReq.Validate(org.OrgType); err != nil {
		_ = c.Error(error2.ErrBadRequest.New("%s", err.Error()))
		return
	}
	// check report cnt exceed the quota limit
	clusterID := c.Param("cid")
	// FIXME(shenjun): skip resource limit on org type for cloud
	if !user.Internal && org.OrgType != "cloud" && !rs.quotalimiter.AskForResource(resource_limit.HealthReportCntPerCluster, 1, org.ID, clusterID, hrReq.ItemID) {
		_ = c.Error(error2.ErrPreConditionFailed.New("%s", resource_limit.HealthReportCntPerCluster.Reason()))
		return
	}

	if org.OrgType == "cloud" && hrReq.EndTime-hrReq.StartTime > 24*60*60 || hrReq.EndTime < hrReq.StartTime {
		_ = c.Error(error2.ErrBadRequest.New("time range should not be more than 24 hours"))
	}
	// if the report is for cluster under org with cloud type,
	// no need to rebuild any resource.
	// if the report is for cluster under org with op type,
	// need to rebuild missing resource.
	dataType := int(_const.AllTask)
	itemID := hrReq.ItemID
	if org.OrgType == "cloud" {
		dataType = int(_const.ReportTask)
		if len(itemID) == 0 {
			itemID = fmt.Sprintf("%s_%d_%d", clusterID, hrReq.StartTime, hrReq.EndTime)
		}
	}

	err = rs.taskManager.Rebuild(
		common.Rebuild{
			DataType:              dataType,
			ItemID:                itemID,
			HealthReport:          true,
			HealthReportStartUnix: hrReq.StartTime,
			HealthReportEndUnix:   hrReq.EndTime,
			OrgType:               org.OrgType,
			OrgID:                 org.ID,
			ClusterID:             clusterID,
		})
	if err != nil {
		log.Error("call taskmanager cli failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("remote call failed"))
	}
	c.JSON(http.StatusOK, ReportResp{
		TaskID: hrReq.ItemID,
	})
}

// @Summary		list health report
// @Tags			HealthReport
// @Description	list health 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			startTime		query		uint	false	"start time"
// @Param			endTime			query		uint	false	"end time"
// @Param			brief			query		bool	false	"if need data"
// @Success		200				{object}	HealthReportResp
// @Router			/compute-engine/{oid}/{cid}/health [get]
func (rs *ReportService) ListHealthReport(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
	}

	cid := c.Param("cid")
	var startTime uint64
	var endTime uint64

	startTimeStr := c.Query("startTime")
	endTimeStr := c.Query("endTime")
	// we only validate the time range for non cloud case.
	if org.OrgType != "cloud" {
		if startTimeStr == "" {
			_ = c.Error(error2.ErrBadRequest.New("startTime is empty"))
			return
		}
		if endTimeStr == "" {
			_ = c.Error(error2.ErrBadRequest.New("endTime is empty"))
			return
		}
		startTime, err = strconv.ParseUint(startTimeStr, 10, 64)
		if err != nil {
			log.Error("parse time failed", zap.Error(err))
			_ = c.Error(error2.ErrBadRequest.New("startTime need be uint64"))
			return
		}
		endTime, err = strconv.ParseUint(endTimeStr, 10, 64)
		if err != nil {
			log.Error("parse time failed", zap.Error(err))
			_ = c.Error(error2.ErrBadRequest.New("endTime need be uint64"))
			return
		}
		if endTime <= startTime {
			_ = c.Error(error2.ErrBadRequest.New("endTime must larger than startTime"))
			return
		}
	}

	brief := false
	briefstr := c.Query("brief")
	if briefstr != "" {
		brief, err = strconv.ParseBool(briefstr)
		if err != nil {
			log.Error("parse brief failed", zap.Error(err))
			_ = c.Error(error2.ErrBadRequest.New("brief need be bool"))
			return
		}
	}

	var resp *HealthReportResp
	if org.OrgType == "cloud" {
		resp, err = rs.listAllHealthReportForCloudCluster(cid, brief)
		if err != nil {
			log.Error("list health report for cloud cluster failed", zap.Error(err))
			_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
			return
		}
	} else {
		resp, err = rs.listHealthReport(cid, uint(startTime), uint(endTime), brief)
		if err != nil {
			log.Error("list health report failed", zap.Error(err))
			_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
			return
		}
	}
	c.JSON(http.StatusOK, resp)
}

// @Summary		get health report
// @Tags			HealthReport
// @Description	list health 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}	compute_engine.Report
// @Router			/compute-engine/{oid}/{cid}/health/{rid} [get]
func (rs *ReportService) GetHealthReport(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
	}
	report, err := model.GetHealthReportByID(rid)
	if err == gorm.ErrRecordNotFound {
		_ = c.Error(error2.ErrNotFound.New("need to compute health report"))
		return
	}
	if err != nil {
		log.Error("GetHealthReportByID failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("db connection failed"))
		return
	}

	brief := false
	briefstr := c.Query("brief")
	if briefstr != "" {
		brief, err = strconv.ParseBool(briefstr)
		if err != nil {
			log.Error("parse brief failed", zap.Error(err))
			_ = c.Error(error2.ErrBadRequest.New("brief need be bool"))
			return
		}
	}

	var rep *compute_engine.Report
	if !brief {
		rep, err = compute_engine.GlobalCMClient.FetchReport(report.ReportUUID)
		if err != nil {
			log.Error("compute engine client failed", zap.Error(err))
			_ = c.Error(error2.ErrInternal.New("failed request to remote service"))
			return
		}
	} else {
		rep = &compute_engine.Report{
			Status:    report.Status,
			CreatedAt: uint(report.CreatedAt.Unix()),
			UpdatedAt: uint(report.CreatedAt.Unix()),
		}
	}
	c.JSON(http.StatusOK, rep)
}

// @Summary		delete health report
// @Tags			HealthReport
// @Description	delete health 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}/health/{rid} [delete]
func (rs *ReportService) DeleteHealthReport(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.HideHealthReportByID(rid)
	if err == gorm.ErrRecordNotFound {
		_ = c.Error(error2.ErrNotFound.New("report not exist"))
		return
	}
	if err != nil {
		log.Error("HideHealthReportByID failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("db connection failed"))
		return
	}

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

type reportRes struct {
	FileUUID   string
	ReportUUID string
	Status     string
	BeginTime  time.Time
	EndTime    time.Time
	Created_at time.Time
	Updated_at time.Time
}

func (rs *ReportService) listHealthReport(clusterID string, startTime, endTime uint, brief bool) (*HealthReportResp, error) {
	res := []reportRes{}
	err := model.ListUploadFilesAndReportInCluster(clusterID, time.Unix(int64(startTime), 0), time.Unix(int64(endTime), 0), &res)
	if err != nil {
		log.Error("fetch upload report failed", zap.Error(err))
		return nil, err
	}
	return rs.ToHealthReportResponse(res, brief)
}

func (rs *ReportService) listAllHealthReportForCloudCluster(clusterID string, brief bool) (*HealthReportResp, error) {
	res := make([]reportRes, 0)
	reports, err := model.ListAllReportInCloudTypeCluster(clusterID)
	if err != nil {
		log.Error("list all report for cloud cluster failed", zap.Error(err))
		return nil, err
	}
	for _, report := range reports {
		res = append(res, reportRes{
			FileUUID:   report.FileUUID,
			ReportUUID: report.ReportUUID,
			Status:     report.Status,
			BeginTime:  time.Unix(int64(report.StartUnix), 0),
			EndTime:    time.Unix(int64(report.EndUnix), 0),
			Created_at: report.CreatedAt,
			Updated_at: report.UpdatedAt,
		})
	}
	return rs.ToHealthReportResponse(res, brief)
}

func (rs *ReportService) ToHealthReportResponse(res []reportRes, brief bool) (*HealthReportResp, error) {
	HReportResp := &HealthReportResp{
		Reports: make([]HealthReport, 0),
	}
	trans := func(res reportRes) (*HealthReport, error) {
		healthReport := &HealthReport{
			ItemID:    res.FileUUID,
			StartTime: uint(res.BeginTime.Unix()),
			EndTime:   uint(res.EndTime.Unix()),
			Status:    res.Status,
		}
		switch res.Status {
		case string(db.StatusSuccess):
			healthReport.Progress = 100
			healthReport.ReportExisted = true
			if !brief {
				report, err := compute_engine.GlobalCMClient.FetchReport(res.ReportUUID)
				if err != nil {
					log.Error("fetch report failed", zap.Error(err))
					return nil, err
				}
				healthReport.HealthData = HealthData{
					Data:      report,
					CreatedAt: uint(res.Created_at.Unix()),
					UpdatedAt: uint(res.Updated_at.Unix()),
				}
			}
		case string(db.StatusPending):
			healthReport.InRebuild = true
			progress, err := rs.generateStatus(res.FileUUID)
			if err != nil {
				log.Error("generate running statusa failed", zap.Error(err))
				return nil, err
			}
			healthReport.Progress = uint(progress)
		case string(db.StatusRunning):
			progress, err := rs.generateStatus(res.FileUUID)
			if err != nil {
				log.Error("generate running statusa failed", zap.Error(err))
				return nil, err
			}
			healthReport.Progress = uint(progress)
		}
		return healthReport, nil
	}
	for _, r := range res {
		rep, err := trans(r)
		if err != nil {
			log.Error("get report failed, ", zap.Error(err))
			return nil, err
		}
		HReportResp.Reports = append(HReportResp.Reports, *rep)
	}
	return HReportResp, nil
}

func (rs *ReportService) generateStatus(uuid string) (int, error) {
	resp, err := rs.taskManager.ReportStatus(uuid)
	if err != nil {
		log.Error("get status failed", zap.Error(err))
		return 0, err
	}

	processed := 0
	for _, item := range resp.Items {
		processed += item.Status
	}
	return processed / len(resp.Items), nil
}
