package controller

import (
	"dataservices/commons/config"
	"dataservices/commons/ginparser"
	rpc "dataservices/commons/grpc"
	httpRegion "dataservices/commons/http"
	dto "dataservices/internal/dto/dataapi"
	cdto "dataservices/internal/dto/dataapi_control"
	"dataservices/internal/entity"
	"dataservices/internal/repository"
	service "dataservices/internal/service/central"
	"fmt"
	"strconv"
	"time"

	jsoniter "github.com/json-iterator/go"
	errs "github.com/tidbcloud/tiinf/errs"

	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

func queryGetOrEmpty(q map[string][]string, field string) string {
	list, ok := q[field]
	if ok && len(list) > 0 {
		return list[0]
	}
	return ""
}

func fetchDataAppLogQueryParam(params *ginparser.CommonParams, needPage bool, query map[string][]string) (*cdto.QueryUserExecutionLogReq, uint64, error) {
	req := cdto.QueryUserExecutionLogReq{}
	req.Org = fmt.Sprintf("%d", params.OrgID)
	req.DataApp = fmt.Sprintf("%d", params.AppID)

	var err error
	req.Start, err = strconv.ParseUint(queryGetOrEmpty(query, "start"), 10, 64)
	if err != nil {
		return nil, 0, errs.NewBadRequest("invalid param start", nil)
	}
	req.Start *= uint64(time.Second)
	req.End, err = strconv.ParseUint(queryGetOrEmpty(query, "end"), 10, 64)
	if err != nil {
		return nil, 0, errs.NewBadRequest("invalid param end", nil)
	}
	req.End *= uint64(time.Second)
	if req.End <= req.Start {
		return nil, 0, errs.NewBadRequest("invalid param query time range", nil)
	}
	req.End += uint64(time.Second) - 1
	if needPage {
		req.Page, err = strconv.ParseUint(queryGetOrEmpty(query, "page"), 10, 64)
		if err != nil || req.Page == 0 {
			return nil, 0, errs.NewBadRequest("invalid param page", nil)
		}
	}
	req.PageSize = 25
	status := queryGetOrEmpty(query, "status")
	switch status {
	case "", "All":
	case "OK":
		req.LogLevel = "INFO"
	case "Error":
		req.LogLevel = "ERROR"
	default:
		return nil, 0, errs.NewBadRequest("invalid param status", nil)
	}
	req.Type = queryGetOrEmpty(query, "type")
	req.ReqID = queryGetOrEmpty(query, "request_id")
	req.TraceID = queryGetOrEmpty(query, "trace_id")
	req.Passback = queryGetOrEmpty(query, "passback")
	req.ClusterID = queryGetOrEmpty(query, "cluster_id")
	if len(req.ClusterID) == 0 {
		return nil, 0, errs.NewBadRequest("empty param clusterId", nil)
	}
	clusterId, err := strconv.ParseUint(req.ClusterID, 10, 64)
	if err != nil {
		return nil, 0, errs.NewBadRequest("invalid param clusterId", nil)
	}
	req.EndpointID = queryGetOrEmpty(query, "endpoint_id")
	if len(req.EndpointID) > 0 {
		_, err := strconv.ParseUint(req.EndpointID, 10, 64)
		if err != nil {
			return nil, 0, errs.NewBadRequest("invalid param endpoint id", nil)
		}
	}
	return &req, clusterId, nil
}

// Query dataapp log
// @Summary query dataapp log
// @Description query dataapp log
// @Tags dataapi
// @Param org_id   path string true "org ID"
// @Param project_id path string true "project ID"
// @Param app_id path string true "app ID"
// @Param start query int true "query start timestamp"
// @Param end query int true "query end timestamp"
// @Param cluster_id query string true "cluster filter"
// @Param endpoint_id query string true "endpoint filter"
// @Param page query int true "page number, starts from 1"
// @Param status query string false "enum OK/Error/All"
// @Param type query string false "type filter,enum all/endpoints"
// @Param request_id query string false "request id filter"
// @Param trace_id query string false "trace id filter"
// @Param passback query string false "passback, fetch from the latest query result, omit when create new query: first query/user change query conditions/click Refresh button"
// @Produce json
// @Success 200 {object} dto.GetDataAppLogRes
// @Router /orgs/{org_id}/projects/{project_id}/apps/{app_id}/log [get]
func QueryDataAppLog(c *gin.Context) {
	ctx := c.Request.Context()
	validateKeys := &[]string{ginparser.PARAM_ORG_ID, ginparser.PARAM_PROJECT_ID, ginparser.PARAM_APP_ID}
	params := ginparser.NewCommonParamsByGin(c)
	validateErr := params.ValidateUrl(validateKeys)
	createdBy, _ := params.GetUserEmail()
	if validateErr != nil {
		tilog.Error(ctx, "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/log[get]-QueryDataAppLog param validate failed!", zap.Error(validateErr), zap.String("user", createdBy))
		c.Error(validateErr)
		return
	}
	query := c.Request.URL.Query()
	req, clusterId, err := fetchDataAppLogQueryParam(params, true, query)
	if err != nil {
		tilog.Warn(ctx, "param invalid", zap.Error(err))
		c.Error(err)
		return
	}
	clusterInfo, err := rpc.GetCluster(ctx, params.OrgID, params.ProjectID, clusterId)
	if err != nil {
		tilog.Warn(ctx, "GetCluster failed", zap.Uint64("cluster_id", clusterId), zap.Error(err))
		c.Error(errs.NewBadRequest("invalid cluster", err))
		return
	}
	tilog.Info(ctx, "cluster info", zap.String("regin", clusterInfo.RegionName))
	if clusterInfo.RegionName == "" {
		tilog.Error(ctx, "user cluster region info is empty", zap.Uint64("cluster_id", clusterId))
		c.Error(errs.NewBadRequest("invalid cluster region", nil))
		return
	}
	// Global fast-fail keeps unsupported AliCloud log requests out of regional control.
	if err := service.GuardRegionalCapabilityForCluster(clusterInfo, service.RegionalCapabilityDataAppLog); err != nil {
		c.Error(err)
		return
	}
	region := clusterInfo.RegionName
	if !config.IsProd() {
		region = "us-west-2"
	}
	tilog.Info(ctx, "QueryDataAppLog query region begin", zap.String("region", region), zap.String("req", fmt.Sprintf("%+v", req)))
	res, err := httpRegion.QueryDataAppLog(ctx, region, params.AppID, req)
	if err != nil {
		tilog.Error(ctx, "QueryDataAppLog failed", zap.Error(err))
		c.Error(errs.NewInternalServerError("query failed", err))
	}
	tilog.Info(ctx, "QueryDataAppLog query region end", zap.String("resp", fmt.Sprintf("%+v", res)))
	resp := dto.GetDataAppLogRes{
		Passback: res.Passback,
		HasMore:  res.HasMore,
	}
	for _, item := range res.Data {
		output := dto.DataAppLogEntry{
			RequestID:       item.ReqID,
			TraceID:         item.TraceID,
			DateTime:        int64(item.LogTime / uint64(time.Second)),
			Type:            "Endpoint",
			Status:          "Success",
			EndpointName:    item.EndpoitName,
			ResponseTime:    item.Latency,
			ResponseSize:    item.RspSize,
			ResponseBody:    item.Message,
			QueryParam:      item.QueryParam,
			ReqHeader:       item.ReqHeader,
			ReqBody:         item.ReqBody,
			ReqUrl:          item.ReqUrl,
			ClientInfo:      item.ClientInfo,
			HttpCode:        item.HttpCode,
			HttpCodeMessage: item.HttpCodeMessage,
			RspRowCnt:       item.RspRowCnt,
			RspCached:       item.RspCached,
			Method:          item.Method,
			EndpointId:      item.EndpointId,
		}
		if item.Level == "ERROR" {
			output.Status = "Error"
		}
		resp.Logs = append(resp.Logs, &output)
	}
	c.JSON(200, &resp)
}

// submit download log task
// @Summary submit download log task
// @Description submit download log task
// @Tags dataapi
// @Param org_id   path string true "org ID"
// @Param project_id path string true "project ID"
// @Param app_id path string true "app ID"
// @Param start query int true "query start timestamp"
// @Param end query int true "query end timestamp"
// @Param cluster_id query string true "cluster filter"
// @Param endpoint_id query string true "endpoint filter"
// @Param status query string false "enum OK/Error/All"
// @Param type query string false "type filter,enum all/endpoints"
// @Param request_id query string false "request id filter"
// @Param trace_id query string false "trace id filter"
// @Param passback query string false "passback, fetch from the latest query result, omit when create new query: first query/user change query conditions/click Refresh button"
// @Produce json
// @Success 200 {object} dto.SubmitDownloadDataAPPLogTaskRes
// @Router /orgs/{org_id}/projects/{project_id}/apps/{app_id}/download_log_task [post]
func SubmitDownloadDataAPPLogTask(c *gin.Context) {
	ctx := c.Request.Context()
	validateKeys := &[]string{ginparser.PARAM_ORG_ID, ginparser.PARAM_PROJECT_ID, ginparser.PARAM_APP_ID}
	params := ginparser.NewCommonParamsByGin(c)
	validateErr := params.ValidateUrl(validateKeys)
	createdBy, _ := params.GetUserEmail()
	if validateErr != nil {
		tilog.Error(ctx, "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/download_log_task[post]-SubmitDownloadDataAPPLogTask param validate failed!", zap.Error(validateErr), zap.String("user", createdBy))
		c.Error(validateErr)
		return
	}
	query := c.Request.URL.Query()
	req, clusterId, err := fetchDataAppLogQueryParam(params, false, query)
	if err != nil {
		tilog.Warn(ctx, "param invalid", zap.Error(err))
		c.Error(err)
		return
	}
	clusterInfo, err := rpc.GetCluster(ctx, params.OrgID, params.ProjectID, clusterId)
	if err != nil {
		tilog.Warn(ctx, "GetCluster failed", zap.Uint64("cluster_id", clusterId), zap.Error(err))
		c.Error(errs.NewBadRequest("invalid cluster", err))
		return
	}
	tilog.Info(ctx, "cluster info", zap.String("regin", clusterInfo.RegionName))
	if clusterInfo.RegionName == "" {
		tilog.Error(ctx, "user cluster region info is empty", zap.Uint64("cluster_id", clusterId))
		c.Error(errs.NewBadRequest("invalid cluster region", nil))
		return
	}
	// Global fast-fail keeps unsupported AliCloud log requests out of regional control.
	if err := service.GuardRegionalCapabilityForCluster(clusterInfo, service.RegionalCapabilityDataAppLog); err != nil {
		c.Error(err)
		return
	}
	region := clusterInfo.RegionName
	if !config.IsProd() {
		region = "us-west-2"
	}
	tilog.Info(ctx, "QueryDataAppLog query region begin", zap.String("region", region), zap.String("req", fmt.Sprintf("%+v", req)))

	downloadParam, _ := jsoniter.MarshalToString(req)

	task := &entity.DataAppLogDownloadTask{
		Region:    region,
		Org:       params.OrgID,
		Project:   params.ProjectID,
		Param:     downloadParam,
		Status:    entity.ConstDataAppLogDownloadTaskStatusDownloading,
		CreatedBy: createdBy,
	}
	task, err = repository.SubmitDataAppLogDownloadTask(ctx, task)
	if err != nil {
		tilog.Error(ctx, "SubmitDataAppLogDownloadTask failed", zap.String("task", fmt.Sprintf("%+v", task)), zap.Error(err))
		c.Error(errs.NewInternalDB("db failed", err))
		return
	}
	tilog.Info(ctx, "SubmitDataAppLogDownloadTask suc", zap.String("task", fmt.Sprintf("%+v", task)))
	dataapp, err := repository.GetDataApp(params.AppID)
	if err != nil {
		tilog.Warn(ctx, "SubmitDataAppLogDownloadTask failed, invalid datapp", zap.String("task", fmt.Sprintf("%+v", task)), zap.Error(err))
		c.Error(errs.NewBadRequest("invalid dataapp id", nil))
		return
	}
	err = httpRegion.SubmitDataAppLogDownloadTask(ctx, region, params.AppID, &cdto.DownloadLogReq{
		QueryUserExecutionLogReq: *req,
		TaskId:                   fmt.Sprintf("%d", task.ID),
		DataAppName:              dataapp.AppName,
	})
	if err != nil {
		tilog.Error(ctx, "SubmitDataAppLogDownloadTask failed", zap.String("region", region), zap.Error(err))
		c.Error(errs.NewInternalServerError("submit task failed", err))
		return
	}
	resp := dto.SubmitDownloadDataAPPLogTaskRes{
		TaskID: fmt.Sprintf("%d", task.ID),
	}
	c.JSON(200, &resp)
}

// get download log task status
// @Summary get download log task status
// @Description get download log task status
// @Tags dataapi
// @Param task_id path string true "download task id"
// @Produce json
// @Success 200 {object} dto.DownloadDataAPPLogTaskStatusRes
// @Router /orgs/{org_id}/projects/{project_id}/apps/{app_id}/download_log_task/{task_id} [get]
func GetDownloadDataAPPLogTaskStatus(c *gin.Context) {
	ctx := c.Request.Context()
	validateKeys := &[]string{ginparser.PARAM_ORG_ID, ginparser.PARAM_PROJECT_ID, ginparser.PARAM_APP_ID}
	params := ginparser.NewCommonParamsByGin(c)
	validateErr := params.ValidateUrl(validateKeys)
	createdBy, _ := params.GetUserEmail()
	if validateErr != nil {
		tilog.Error(ctx, "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/download_log_task[post]-SubmitDownloadDataAPPLogTask param validate failed!", zap.Error(validateErr), zap.String("user", createdBy))
		c.Error(validateErr)
		return
	}
	taskIdStr := c.Param("task_id")
	id, err := strconv.ParseUint(taskIdStr, 10, 64)
	if err != nil {
		c.Error(errs.NewBadRequest("invalid taskId", nil))
		return
	}
	task, err := repository.GetDataAppLogDownloadTask(ctx, id)
	if err != nil {
		tilog.Error(ctx, "GetDataAppLogDownloadTask failed", zap.String("task", taskIdStr), zap.Error(err))
		c.Error(errs.NewInternalDB("get status failed", err))
		return
	}
	if task == nil {
		tilog.Warn(ctx, "no suck task", zap.String("task", taskIdStr), zap.Error(err))
		c.Error(errs.NewBadRequest("no such task", nil))
		return
	}
	if task.Project != params.ProjectID || task.Org != params.OrgID {
		tilog.Warn(ctx, "forbidden", zap.String("task", taskIdStr), zap.String("org", fmt.Sprintf("expect %d, param %d", task.Org, params.OrgID)), zap.String("proj", fmt.Sprintf("expect %d, param %d", task.Project, params.ProjectID)))
		c.Error(errs.NewBadRequest("no such task", nil))
		return
	}
	resp := dto.DownloadDataAPPLogTaskStatusRes{
		Status:      task.Status,
		DownloadURL: task.Url,
	}
	c.JSON(200, &resp)
}

func ReportDownloadDataAPPLogTaskResult(c *gin.Context) {
	ctx := c.Request.Context()
	params := ginparser.NewCommonParamsByGin(c)
	req := cdto.DownloadLogResult{}
	err := params.BindParam(&req)
	if err != nil {
		tilog.Error(ctx, "report parse failed", zap.Error(err))
		c.Error(errs.NewBadRequest("json parse failed", nil))
		return
	}
	taskId, err := strconv.ParseUint(req.TaskId, 10, 64)
	if err != nil {
		tilog.Error(ctx, "invalid task id", zap.Error(err), zap.String("taskid", req.TaskId))
		c.Error(errs.NewBadRequest("invalid task id", nil))
		return
	}
	if err := repository.UpdateDataAppLogDownloadTask(ctx, taskId, req.Result, req.URL, ""); err != nil {
		tilog.Error(ctx, "UpdateDataAppLogDownloadTask failed", zap.Error(err))
		c.Error(errs.NewInternalDB("UpdateDataAppLogDownloadTask failed", nil))
		return
	}
	c.JSON(200, &dto.BaseResp{})
}
