package report

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"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/auth/session"
	"github.com/pingcap-inc/clinic/pkg/config"
	_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/rbac"
	"github.com/pingcap-inc/clinic/pkg/resource_limit"
	"github.com/pingcap-inc/clinic/pkg/task_manager"
)

type ReportService struct {
	cfg            *config.Config
	rbac           rbac.Auth
	sessionManager *session.SessionManager
	taskManager    task_manager.TaskManagerClient
	quotalimiter   resource_limit.IQuotaLimiter
}

func NewReportService(cfg *config.Config, rbac rbac.Auth, sessionManager *session.SessionManager, taskManagerCli *task_manager.TaskManagerClient, quotalimiter resource_limit.IQuotaLimiter) *ReportService {
	return &ReportService{
		cfg:            cfg,
		rbac:           rbac,
		sessionManager: sessionManager,
		taskManager:    *taskManagerCli,
		quotalimiter:   quotalimiter,
	}
}

// @Summary		Touch New Report Job
// @Tags			Reports
// @Description	touch report 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_type		query		string				true	"report type"
// @Param			reportParams	body		TouchReportRequest	true	"report param"
// @Success		200				{object}	TouchReportResponse
// @Router			/reports/{oid}/{cid} [post]
func (rs *ReportService) TouchReport(c *gin.Context) {
	user, org, _, ok, err := common.CanReadCluster(c, rs.rbac, c.Param("cid"))
	if err != nil {
		_ = c.Error(error2.ErrNotFound.WrapWithNoMessage(err))
		return
	}
	if !ok {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}

	rtype := c.Query("report_type")
	if rtype == "" {
		_ = c.Error(error2.ErrBadRequest.New("empty report type"))
		return
	}
	arg, err := io.ReadAll(c.Request.Body)
	if err != nil {
		_ = c.Error(error2.ErrBadRequest.New("%s", err.Error()))
		return
	}

	var req TouchReportRequest
	if err := json.Unmarshal(arg, &req); err != nil {
		_ = c.Error(error2.ErrBadRequest.New("bad param, error %s", err.Error()))
		return
	}
	if err := req.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")
	if !user.Internal && org.OrgType != "cloud" && !rs.quotalimiter.AskForResource(resource_limit.ReportCntPerCluster, 1, org.ID, clusterID) {
		_ = c.Error(error2.ErrPreConditionFailed.New("%s", resource_limit.HealthReportCntPerCluster.Reason()))
		return
	}

	r := db.Report{
		UUID:       uuid.NewString(),
		OrgID:      org.ID,
		ClusterID:  clusterID,
		CreatorID:  user.UserID,
		ReportType: rtype,
		Status:     string(db.StatusPending),
		Arg:        string(arg),
	}
	err = model.InsertReport(&r)
	if err != nil {
		log.Error("insert reports failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
		return
	}

	// ask task-manager to rebuild data for op cluster
	if org.OrgType != "cloud" {
		err = sendRebuildRequest(rs.taskManager, org.ID, clusterID, int64(req.BeginTime), int64(req.EndTime))
		if err != nil {
			log.Error("send rebuild request failed", zap.Error(err))
			_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
			return
		}
	}

	// scaner on task-manager will auto read from DB and start to generate report
	c.JSON(http.StatusOK, TouchReportResponse{ReportID: r.UUID})
}

// @Summary		list health report
// @Tags			Reports
// @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			report_type		query		string	true	"report type"
// @Param			limit			query		int		false	"the count per page"
// @Param			page			query		int		false	"the page index"
// @Success		200				{object}	ListReportResponse
// @Router			/reports/{oid}/{cid} [get]
func (rs *ReportService) ListReport(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
	}

	rtype := c.Query("report_type")

	var page, limit int
	pageParam := c.Query("page")
	if pageParam == "" {
		page = 0
	} else {
		page, err = strconv.Atoi(pageParam)
		if err != nil {
			log.Error("parse param page failed", zap.Error(err))
			_ = c.Error(error2.ErrBadRequest.New("parse aren't int"))
			return
		}
	}
	limitParam := c.Query("limit")
	if limitParam == "" {
		limit = 0
	} else {
		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"))
			return
		}
	}

	reports, total, err := model.ListReportsOverview(&db.Report{OrgID: org.ID, ClusterID: c.Param("cid"), ReportType: rtype}, page, limit)
	if err != nil {
		log.Error("list all reports failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
		return
	}
	resp := ListReportResponse{
		Total: total,
	}
	for _, report := range reports {
		var args any
		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
		}
		resp.Reports = append(resp.Reports, reportOverview{
			ReportID:   report.UUID,
			ReportType: report.ReportType,
			Status:     report.Status,
			Progress:   report.Progress,
			Args:       args,
			Creator:    userName,
			CreatedAt:  uint(report.CreatedAt.Unix()),
		})
	}

	c.JSON(http.StatusOK, resp)
}

// @Summary		get report
// @Tags			Reports
// @Description	list 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				{object}	GetReportResponse
// @Router			/reports/{oid}/{cid}/{rid} [get]
func (rs *ReportService) GetReport(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.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 arg, result any
	err = json.Unmarshal([]byte(report.Arg), &arg)
	if err != nil {
		log.Error("Unmarshal arg failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("get arg failed"))
		return
	}
	// return err when result is "", so ignore error
	_ = json.Unmarshal([]byte(report.Result), &result)

	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
	}

	c.JSON(http.StatusOK, GetReportResponse{
		Status:    report.Status,
		Progress:  report.Progress,
		Args:      arg,
		Result:    result,
		Creator:   userName,
		CreatedAt: uint(report.CreatedAt.Unix()),
	})
}

// @Summary		rebuild report
// @Tags			Reports
// @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				{object}	common.SuccessResp
// @Router			/reports/{oid}/{cid}/{rid} [post]
func (rs *ReportService) RebuildReport(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
	}

	rid := c.Param("rid")
	if rid == "" {
		_ = c.Error(error2.ErrBadRequest.New("empty rid"))
		return
	}

	// get report params from DB
	r, err := model.GetReportByUUID(rid)
	if err != nil {
		log.Error("get report info failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("db connection failed"))
		return
	}
	err = model.UpdateReport(&db.Report{
		UUID:   rid,
		Status: string(db.StatusPending),
	})
	if err != nil {
		log.Error("rebuild report failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("db connection failed"))
		return
	}
	var args TouchReportRequest
	err = json.Unmarshal([]byte(r.Arg), &args)
	if err != nil {
		log.Error("parse args from db failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("rebuild report failed"))
		return
	}

	// ask task-manager to rebuild data for op cluster
	if org.OrgType != "cloud" {
		err = sendRebuildRequest(rs.taskManager, org.ID, r.ClusterID, int64(args.BeginTime), int64(args.EndTime))
		if err != nil {
			log.Error("send rebuild request failed", zap.Error(err))
			_ = c.Error(error2.ErrInternal.New("%s", err.Error()))
			return
		}
	}

	// scaner on task-manager will auto read from DB and start to generate report

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

// @Summary		delete report
// @Tags			Reports
// @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				{object}	common.SuccessResp
// @Router			/reports/{oid}/{cid}/{rid} [delete]
func (rs *ReportService) DeleteReport(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())
}

// Deprecated: move to internal api
//
//	@Summary		put report
//	@Tags			Reports
//	@Description	put 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			reportParams	body		PutReportRequest	true	"report information"
//	@Success		200				{object}	common.SuccessResp
//	@Router			/reports/{oid}/{cid}/{rid} [put]
func (rs *ReportService) PutReport(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
	}

	req := PutReportRequest{}
	err = c.BindJSON(&req)
	if err != nil {
		_ = c.Error(error2.ErrBadRequest.New("invalid request"))
	}

	resultJSON, err := json.Marshal(req.Result)
	if err != nil {
		_ = c.Error(error2.ErrBadRequest.New("invalid result"))
	}

	err = model.UpdateReport(&db.Report{
		UUID:     rid,
		Status:   req.Status,
		Progress: req.Progress,
		Result:   string(resultJSON),
	})
	if err == gorm.ErrRecordNotFound {
		_ = c.Error(error2.ErrNotFound.New("report not exist"))
		return
	}
	if err != nil {
		log.Error("put report failed", zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("db connection failed"))
		return
	}

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

func sendRebuildRequest(tmcli task_manager.TaskManagerClient, orgID uint, clusterID string, beginTime, endTime int64) error {
	packages, _, err := model.ListUploadFiles(orgID, clusterID, 0, 0, beginTime, endTime)
	if err != nil {
		return err
	}
	for _, p := range packages {
		err = tmcli.Rebuild(common.Rebuild{
			DataType:  int(_const.AllTask),
			ItemID:    p.FileUUID,
			OrgID:     orgID,
			ClusterID: clusterID,
		})
		if err != nil {
			return err
		}
	}
	return nil
}

// @Summary		get report statistics
// @Tags			Reports
// @Description	get report statistics
// @Accept			json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			event			query		string	true	"event name"
// @Param			hash			query		string	true	"hash value"
// @Success		200				{object}	ReportStatistics
// @Router			/reports/statistics [get]
func (rs *ReportService) GetReportStatistics(c *gin.Context) {
	u := session.GetUser(c)
	if u == nil {
		_ = c.Error(error2.ErrNotFound.New("invalid user"))
		return
	}

	//if !u.Internal {
	//	_ = c.Error(error2.ErrForbidden.New("permission deny"))
	//	return
	//}

	event := c.Query("event")
	hash := c.Query("hash")

	if event == "" {
		_ = c.Error(error2.ErrBadRequest.New("bad parameter"))
		return
	}

	event, _ = url.QueryUnescape(event)
	hash, _ = url.QueryUnescape(hash)
	count, err := rs.reportCountByType(event, hash)
	if err != nil {
		_ = c.Error(error2.ErrInternal.New("Statistics report failed"))
		return
	}

	c.JSON(http.StatusOK, ReportStatistics{
		Count: count,
	})
}

func (rs *ReportService) reportCountByType(eventName, hash string) (int, error) {
	u := "https://mixpanel.com/api/2.0/events"
	if hash != "" {
		u = "https://mixpanel.com/api/2.0/events/properties"
	} else {
		eventName = fmt.Sprintf(`["%s"]`, eventName)
	}

	urlA, err := url.Parse(u)
	if err != nil {
		log.Error("parse url failed", zap.Error(err))
		return 0, err
	}

	values := urlA.Query()
	values.Add("project_id", fmt.Sprintf("%d", rs.cfg.MixPanelConfig.ProjectID))
	values.Add("event", eventName)
	values.Add("name", "hash")
	values.Add("values", fmt.Sprintf(`["%s"]`, hash))
	values.Add("type", "general")
	values.Add("unit", "month")
	values.Add("from_date", time.Now().Add(-time.Hour*24*365).Format("2006-01-02"))
	values.Add("to_date", time.Now().Format("2006-01-02"))

	urlA.RawQuery = values.Encode()
	req, _ := http.NewRequest("GET", urlA.String(), nil)

	req.Header.Add("accept", "application/json")
	req.SetBasicAuth(rs.cfg.MixPanelConfig.UserName, rs.cfg.MixPanelConfig.Secret)

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Error("request mixpanel failed", zap.Error(err))
		return 0, err
	}

	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		log.Error("mixpanel response failed", zap.String("status", res.Status), zap.Int("code", res.StatusCode), zap.Error(err))
		return 0, err
	}

	var result MixPanelResult
	if err = json.Unmarshal(body, &result); err != nil {
		log.Error("unmarshal body failed", zap.String("body", string(body)), zap.Error(err))
		return 0, err
	}

	count := 0
	for _, v := range result.Data.Values {
		for _, c := range v {
			count += c
		}
	}

	return count, nil
}

type MixPanelResult struct {
	Data MixPanelData `json:"data"`
}

type MixPanelData struct {
	Values map[string]map[string]int `json:"values"`
}
