package api

import (
	"net/http"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/samber/lo"

	"github.com/pingcap-inc/clinic/pkg/ngm/error"
	"github.com/pingcap-inc/clinic/pkg/ngm/performance_insight/service"
)

type TaskAPI struct {
	taskSvc service.TaskService
}

func NewTaskAPI(taskSvc service.TaskService) *TaskAPI {
	return &TaskAPI{
		taskSvc: taskSvc,
	}
}

// @Summary		Create Task
// @Tags			performance_insight_task
// @Description	Create a performance insight task for a specific dbaas cluster.
// @Produce		json
// @Param			X-CLOUD-USER	header		string	true	"dbaas user header, added automatically by clinic api-gateway, {id:1} encodes to eyJpZCI6MX0="
// @Success		200				{object}	CreateTaskResponse
// @Failure		400				{object}	error.ErrorResponse
// @Failure		401				{object}	error.ErrorResponse
// @Failure		403				{object}	error.ErrorResponse
// @Failure		404				{object}	error.ErrorResponse
// @Failure		412				{object}	error.ErrorResponse
// @Failure		429				{object}	error.ErrorResponse
// @Failure		500				{object}	error.ErrorResponse
// @Router			/performance_insight/tasks [post]
func (t *TaskAPI) HandleCreate(g *gin.Context) {
	// Check user header auth info.
	uid, err := getUserId(g)
	if err != nil {
		_ = g.Error(error.ErrUnauthorized.New("%s", err.Error()))
	}

	// Get org and cluster id from path.
	oid, cid, err := getOrgAndClusterIDFromPath(g)
	if err != nil {
		_ = g.Error(error.ErrBadRequest.New("%s", err.Error()))
		return
	}

	// Forward request to service.
	taskID, err := t.taskSvc.CreateTask(g, oid, uid, cid)
	if err != nil {
		_ = g.Error(err)
		return
	}

	g.JSON(http.StatusOK, CreateTaskResponse{TaskID: taskID})
}

// @Summary		List Task Overview
// @Tags			performance_insight_task
// @Description	List performance insight tasks.
// @Produce		json
// @Param			X-CLOUD-USER			header		string		true	"dbaas user header, added automatically by clinic api-gateway, {id:1} encodes to eyJpZCI6MX0="
// @Param			status					query		[]string	false	"task status list; if not specified, any status is allowed"	example(created,running,succeeded,failed)
// @Param			types					query		[]string	false	"task type list; if not specified, any type is allowed"		example(unstable_plan_insight,index_insight,hint_insight)
// @Param			order_by_update_time	query		string		false	"sort by update time before pagination"						Enums(asc,desc)	default(desc)
// @Param			limit					query		uint		false	"the number of items per page"								default(30)
// @Param			page					query		uint		false	"the page index"											default(1)
// @Success		200						{object}	ListTaskResponse
// @Failure		400						{object}	error.ErrorResponse
// @Failure		401						{object}	error.ErrorResponse
// @Failure		403						{object}	error.ErrorResponse
// @Failure		404						{object}	error.ErrorResponse
// @Failure		412						{object}	error.ErrorResponse
// @Failure		429						{object}	error.ErrorResponse
// @Failure		500						{object}	error.ErrorResponse
// @Router			/performance_insight/tasks [get]
func (t *TaskAPI) HandleQuery(g *gin.Context) {
	// Check user header auth info.
	_, err := getUserId(g)
	if err != nil {
		_ = g.Error(error.ErrUnauthorized.New("%s", err.Error()))
	}

	// Get org and cluster id from path.
	oid, cid, err := getOrgAndClusterIDFromPath(g)
	if err != nil {
		_ = g.Error(error.ErrBadRequest.New("%s", err.Error()))
		return
	}

	// Get and validate query parameters.
	var statusQ, typesQ []string
	if qs := g.Query("status"); qs != "" {
		// careful: splitting empty string returns [""], refer to https://github.com/golang/go/issues/35130
		statusQ = lo.FilterMap(strings.Split(qs, ","), func(s string, _ int) (string, bool) {
			trimmed := strings.TrimSpace(s)
			return trimmed, lo.Contains(service.TaskStatusList, trimmed)
		})
	}
	if qs := g.Query("types"); qs != "" {
		typesQ = lo.FilterMap(strings.Split(qs, ","), func(s string, _ int) (string, bool) {
			trimmed := strings.TrimSpace(s)
			return trimmed, lo.Contains(service.TaskTypeList, trimmed)
		})
	}
	updateTimeOrder := g.Query("order_by_update_time")
	if updateTimeOrder == "" {
		updateTimeOrder = "desc"
	}
	limit := g.Param("limit")
	if limit == "" {
		limit = "30"
	}
	limitInt, err := strconv.ParseUint(limit, 10, 64)
	if err != nil {
		_ = g.Error(error.ErrBadRequest.New("invalid limit"))
		return
	}
	page := g.Param("page")
	if page == "" {
		page = "1"
	}
	pageInt, err := strconv.ParseUint(page, 10, 64)
	if err != nil || pageInt == 0 {
		_ = g.Error(error.ErrBadRequest.New("invalid page"))
		return
	}

	// Forward request to service.
	tasks, total, err := t.taskSvc.ListTasks(g, oid, cid, statusQ, typesQ, updateTimeOrder, uint(limitInt), uint(pageInt))
	if err != nil {
		_ = g.Error(err)
		return
	}

	// Transform to response.
	tasksResp := make([]Task, 0, len(tasks))
	for _, task := range tasks {
		dbaasClusterID, err := strconv.ParseUint(task.PerformanceInsightCluster.Cluster.ClusterID, 10, 64)
		if err != nil {
			_ = g.Error(error.ErrInternal.Wrap(err, "invalid dbaas cluster id"))
			return
		}
		tasksResp = append(tasksResp, Task{
			TaskID:                  task.ID,
			CreatedAt:               uint64(task.CreatedAt.Unix()),
			UpdateAt:                uint64(task.UpdatedAt.Unix()),
			ClusterID:               task.PerformanceInsightCluster.Cluster.ID,
			PerformInsightClusterID: task.PerformanceInsightCluster.ID,
			DbaasClusterID:          dbaasClusterID,
			DbaasOrgID:              uint64(task.PerformanceInsightCluster.Cluster.Organization.DbaasOrgID),
			Status:                  task.Status,
			Type:                    task.Type,
			LastFailedMessage:       maskFailedMessage(task.LastFailedMessage),
			MaterialCount:           task.MaterialCount,
		})
	}

	g.JSON(http.StatusOK, ListTaskResponse{Total: uint64(total), Tasks: tasksResp})
}

// @Summary		Get Details of A Task
// @Tags			performance_insight_task
// @Description	Get a performance insight task.
// @Produce		json
// @Param			X-CLOUD-USER	header		string	true	"dbaas user header, added automatically by clinic api-gateway, {id:1} encodes to eyJpZCI6MX0="
// @Param			task_id			path		uint	true	"cluster id from dbaas"
// @Success		200				{object}	Task
// @Failure		400				{object}	error.ErrorResponse
// @Failure		401				{object}	error.ErrorResponse
// @Failure		403				{object}	error.ErrorResponse
// @Failure		404				{object}	error.ErrorResponse
// @Failure		412				{object}	error.ErrorResponse
// @Failure		429				{object}	error.ErrorResponse
// @Failure		500				{object}	error.ErrorResponse
// @Router			/performance_insight/tasks/{task_id} [get]
func (t *TaskAPI) HandleGet(g *gin.Context) {
	// Check user header auth info.
	_, err := getUserId(g)
	if err != nil {
		_ = g.Error(error.ErrUnauthorized.New("%s", err.Error()))
	}

	// Get org and cluster id from path.
	oid, cid, err := getOrgAndClusterIDFromPath(g)
	if err != nil {
		_ = g.Error(error.ErrBadRequest.New("%s", err.Error()))
		return
	}

	tid := g.Param("task_id")
	tidInt, err := strconv.ParseUint(tid, 10, 64)
	if err != nil {
		_ = g.Error(error.ErrBadRequest.New("invalid task id (tid)"))
		return
	}

	// Forward request to service.
	task, err := t.taskSvc.GetTask(g, oid, cid, uint(tidInt))
	if err != nil {
		_ = g.Error(err)
		return
	}

	// Transform to response.
	dbaasClusterID, err := strconv.ParseUint(task.PerformanceInsightCluster.Cluster.ClusterID, 10, 64)
	if err != nil {
		_ = g.Error(error.ErrInternal.Wrap(err, "invalid dbaas cluster id"))
		return
	}
	taskResp := Task{
		TaskID:                  task.ID,
		CreatedAt:               uint64(task.CreatedAt.Unix()),
		UpdateAt:                uint64(task.UpdatedAt.Unix()),
		ClusterID:               task.PerformanceInsightCluster.Cluster.ID,
		PerformInsightClusterID: task.PerformanceInsightCluster.ID,
		DbaasClusterID:          dbaasClusterID,
		DbaasOrgID:              uint64(task.PerformanceInsightCluster.Cluster.Organization.DbaasOrgID),
		Status:                  task.Status,
		Type:                    task.Type,
		LastFailedMessage:       maskFailedMessage(task.LastFailedMessage),
		MaterialCount:           task.MaterialCount,
	}

	g.JSON(http.StatusOK, taskResp)
}

// @Summary		Get Details of the Latest Task
// @Tags			performance_insight_task
// @Description	Get the latest performance insight task.
// @Produce		json
// @Param			X-CLOUD-USER	header		string	true	"dbaas user header, added automatically by clinic api-gateway, {id:1} encodes to eyJpZCI6MX0="
// @Success		200				{object}	LatestTaskResponse
// @Failure		400				{object}	error.ErrorResponse
// @Failure		401				{object}	error.ErrorResponse
// @Failure		403				{object}	error.ErrorResponse
// @Failure		404				{object}	error.ErrorResponse
// @Failure		412				{object}	error.ErrorResponse
// @Failure		429				{object}	error.ErrorResponse
// @Failure		500				{object}	error.ErrorResponse
// @Router			/performance_insight/tasks/latest [get]
func (t *TaskAPI) HandleGetLatest(g *gin.Context) {
	// Check user header auth info.
	_, err := getUserId(g)
	if err != nil {
		_ = g.Error(error.ErrUnauthorized.New("%s", err.Error()))
	}

	// Get org and cluster id from path.
	oid, cid, err := getOrgAndClusterIDFromPath(g)
	if err != nil {
		_ = g.Error(error.ErrBadRequest.New("%s", err.Error()))
		return
	}

	// Forward request to service.
	latestTask, err := t.taskSvc.GetLatestTask(g, oid, cid)
	if err != nil && err != service.ErrTaskNotFound {
		_ = g.Error(err)
		return
	}
	var latest *Task
	if latestTask != nil {
		var latestTaskAdviseCount int64
		if latestTask.Status == service.TaskStatusSucceeded && latestTask.Type == service.TaskTypeIndexInsight {
			latestTaskAdviseCount, err = t.taskSvc.GetTaskResultCount(g, latestTask.ID)
			if err != nil {
				_ = g.Error(err)
				return
			}
		}
		dbaasClusterID, err := strconv.ParseUint(latestTask.PerformanceInsightCluster.Cluster.ClusterID, 10, 64)
		if err != nil {
			_ = g.Error(error.ErrInternal.Wrap(err, "invalid dbaas cluster id"))
			return
		}
		latest = &Task{
			TaskID:                  latestTask.ID,
			CreatedAt:               uint64(latestTask.CreatedAt.Unix()),
			UpdateAt:                uint64(latestTask.UpdatedAt.Unix()),
			ClusterID:               latestTask.PerformanceInsightCluster.Cluster.ID,
			PerformInsightClusterID: latestTask.PerformanceInsightCluster.ID,
			DbaasClusterID:          dbaasClusterID,
			DbaasOrgID:              uint64(latestTask.PerformanceInsightCluster.Cluster.Organization.DbaasOrgID),
			Status:                  latestTask.Status,
			Type:                    latestTask.Type,
			LastFailedMessage:       maskFailedMessage(latestTask.LastFailedMessage),
			MaterialCount:           latestTask.MaterialCount,
			AdviseCount:             latestTaskAdviseCount,
		}
	}

	latestFinishedTask, err := t.taskSvc.GetLatestFinishTask(g, oid, cid)
	if err != nil && err != service.ErrTaskNotFound {
		_ = g.Error(err)
		return
	}
	var latestFinished *Task
	if latestFinishedTask != nil {
		var latestFinishedTaskAdviseCount int64
		if latestFinishedTask.Status == service.TaskStatusSucceeded && latestFinishedTask.Type == service.TaskTypeIndexInsight {
			latestFinishedTaskAdviseCount, err = t.taskSvc.GetTaskResultCount(g, latestFinishedTask.ID)
			if err != nil {
				_ = g.Error(err)
				return
			}
		}
		dbaasClusterID, err := strconv.ParseUint(latestFinishedTask.PerformanceInsightCluster.Cluster.ClusterID, 10, 64)
		if err != nil {
			_ = g.Error(error.ErrInternal.Wrap(err, "invalid dbaas cluster id"))
			return
		}
		latestFinished = &Task{
			TaskID:                  latestFinishedTask.ID,
			CreatedAt:               uint64(latestFinishedTask.CreatedAt.Unix()),
			UpdateAt:                uint64(latestFinishedTask.UpdatedAt.Unix()),
			ClusterID:               latestFinishedTask.PerformanceInsightCluster.Cluster.ID,
			PerformInsightClusterID: latestFinishedTask.PerformanceInsightCluster.ID,
			DbaasClusterID:          dbaasClusterID,
			DbaasOrgID:              uint64(latestFinishedTask.PerformanceInsightCluster.Cluster.Organization.DbaasOrgID),
			Status:                  latestFinishedTask.Status,
			Type:                    latestFinishedTask.Type,
			LastFailedMessage:       maskFailedMessage(latestFinishedTask.LastFailedMessage),
			MaterialCount:           latestFinishedTask.MaterialCount,
			AdviseCount:             latestFinishedTaskAdviseCount,
		}
	}

	// Transform to response.
	g.JSON(http.StatusOK, LatestTaskResponse{
		Latest:         latest,
		LatestFinished: latestFinished,
	})
}

// @Summary		Cancel A Task
// @Tags			performance_insight_task
// @Description	cancel a performance insight task.
// @Produce		json
// @Param			X-CLOUD-USER	header		string	true	"dbaas user header, added automatically by clinic api-gateway, {id:1} encodes to eyJpZCI6MX0="
// @Param			task_id			path		uint	true	"cluster id from dbaas"
// @Success		200				{string}	string	"succeeded"
// @Failure		400				{object}	error.ErrorResponse
// @Failure		401				{object}	error.ErrorResponse
// @Failure		403				{object}	error.ErrorResponse
// @Failure		404				{object}	error.ErrorResponse
// @Failure		412				{object}	error.ErrorResponse
// @Failure		429				{object}	error.ErrorResponse
// @Failure		500				{object}	error.ErrorResponse
// @Router			/performance_insight/tasks/{task_id} [delete]
func (t *TaskAPI) HandleCancel(g *gin.Context) {
	// Check user header auth info.
	_, err := getUserId(g)
	if err != nil {
		_ = g.Error(error.ErrUnauthorized.New("%s", err.Error()))
	}

	// Get org and cluster id from path.
	oid, cid, err := getOrgAndClusterIDFromPath(g)
	if err != nil {
		_ = g.Error(error.ErrBadRequest.New("%s", err.Error()))
		return
	}

	tid := g.Param("task_id")
	tidInt, err := strconv.ParseUint(tid, 10, 64)
	if err != nil {
		_ = g.Error(error.ErrBadRequest.New("invalid task id (tid)"))
		return
	}

	// Forward request to service.
	if err := t.taskSvc.CancelTask(g, oid, cid, uint(tidInt)); err != nil {
		_ = g.Error(err)
		return
	}

	g.String(http.StatusOK, "succeeded")
}

// @Summary		Activate and Submit MySQL Account/Password
// @Tags			performance_insight_connection
// @Description	Submit mysql account and password. Performance insight tasks use them to execute sql directly against TiDB cluster.
// @Produce		json
// @Param			X-CLOUD-USER	header		string						true	"dbaas user header, added automatically by clinic api-gateway, {id:1} encodes to eyJpZCI6MX0="
// @Param			account			body		SubmitMysqlAccountRequest	true	"mysql account"
// @Success		200				{string}	string						"succeeded"
// @Failure		400				{object}	error.ErrorResponse
// @Failure		401				{object}	error.ErrorResponse
// @Failure		403				{object}	error.ErrorResponse
// @Failure		404				{object}	error.ErrorResponse
// @Failure		412				{object}	error.ErrorResponse
// @Failure		429				{object}	error.ErrorResponse
// @Failure		500				{object}	error.ErrorResponse
// @Router			/performance_insight/tidb_connection [post]
func (t *TaskAPI) Activate(g *gin.Context) {
	// Check user header auth info.
	uid, err := getUserId(g)
	if err != nil {
		_ = g.Error(error.ErrUnauthorized.New("%s", err.Error()))
	}

	// Get org and cluster id from path.
	oid, cid, err := getOrgAndClusterIDFromPath(g)
	if err != nil {
		_ = g.Error(error.ErrBadRequest.New("%s", err.Error()))
		return
	}

	// Get project id from path
	projectIDStr := g.Param("project")
	projectID, err := strconv.ParseUint(projectIDStr, 10, 64)
	if projectIDStr == "" || err != nil {
		_ = g.Error(error.ErrBadRequest.New("invalid project id"))
		return
	}

	// Validate request body.
	var req SubmitMysqlAccountRequest
	if err := g.BindJSON(&req); err != nil {
		_ = g.Error(error.ErrBadRequest.Wrap(err, "invalid request body"))
		return
	}
	if req.User == "" { // Empty password is allowed.
		_ = g.Error(error.ErrBadRequest.New("invalid request body"))
		return
	}

	// Forward request to service.
	if err := t.taskSvc.Activate(g, oid, uid, projectID, cid, req.User, req.Password); err != nil {
		_ = g.Error(err)
		return
	}

	// Transform to response.
	g.String(http.StatusOK, "succeeded")
}

// @Summary		Deactivate and Delete All records
// @Tags			performance_insight_connection
// @Description	Delete cluster info, task info and all results.
// @Produce		json
// @Param			X-CLOUD-USER	header		string	true	"dbaas user header, added automatically by clinic api-gateway, {id:1} encodes to eyJpZCI6MX0="
// @Success		200				{string}	string	"succeeded"
// @Failure		400				{object}	error.ErrorResponse
// @Failure		401				{object}	error.ErrorResponse
// @Failure		403				{object}	error.ErrorResponse
// @Failure		404				{object}	error.ErrorResponse
// @Failure		412				{object}	error.ErrorResponse
// @Failure		429				{object}	error.ErrorResponse
// @Failure		500				{object}	error.ErrorResponse
// @Router			/performance_insight/tidb_connection [delete]
func (t *TaskAPI) Deactivate(g *gin.Context) {
	// Check user header auth info.
	uid, err := getUserId(g)
	if err != nil {
		_ = g.Error(error.ErrUnauthorized.New("%s", err.Error()))
	}

	// Get org and cluster id from path.
	oid, cid, err := getOrgAndClusterIDFromPath(g)
	if err != nil {
		_ = g.Error(error.ErrBadRequest.New("%s", err.Error()))
		return
	}

	// Forward request to service.
	if err := t.taskSvc.Deactivate(g, oid, uid, cid); err != nil {
		_ = g.Error(err)
		return
	}

	// Transform to response.
	g.String(http.StatusOK, "succeeded")
}

// @Summary		Check if the cluster is activated
// @Tags			performance_insight_connection
// @Description	Check if the cluster is activated
// @Produce		json
// @Param			X-CLOUD-USER	header		string	true	"dbaas user header, added automatically by clinic api-gateway, {id:1} encodes to eyJpZCI6MX0="
// @Success		200				{bool}		bool	true/false
// @Failure		400				{object}	error.ErrorResponse
// @Failure		401				{object}	error.ErrorResponse
// @Failure		403				{object}	error.ErrorResponse
// @Failure		404				{object}	error.ErrorResponse
// @Failure		412				{object}	error.ErrorResponse
// @Failure		429				{object}	error.ErrorResponse
// @Failure		500				{object}	error.ErrorResponse
// @Router			/performance_insight/tidb_connection [get]
func (t *TaskAPI) ClusterActivated(g *gin.Context) {
	// Check user header auth info.
	uid, err := getUserId(g)
	if err != nil {
		_ = g.Error(error.ErrUnauthorized.New("%s", err.Error()))
	}

	// Get org and cluster id from path.
	oid, cid, err := getOrgAndClusterIDFromPath(g)
	if err != nil {
		_ = g.Error(error.ErrBadRequest.New("%s", err.Error()))
		return
	}

	// Forward request to service.
	activated, err := t.taskSvc.ClusterActivated(g, oid, uid, cid)
	if err != nil {
		_ = g.Error(err)
		return
	}

	// Transform to response.
	g.JSON(http.StatusOK, activated)
}

// @Summary		Get Clinic Public Addresses
// @Tags			performance_insight_connection
// @Description	Get clinic public addresses, so that customers can add them into the ip whitelist.
// @Produce		json
// @Param			X-CLOUD-USER	header		string	true	"dbaas user header, added automatically by clinic api-gateway, {id:1} encodes to eyJpZCI6MX0="
// @Success		200				{object}	GetClinicPublicAddressResponse
// @Failure		400				{object}	error.ErrorResponse
// @Failure		401				{object}	error.ErrorResponse
// @Failure		403				{object}	error.ErrorResponse
// @Failure		404				{object}	error.ErrorResponse
// @Failure		412				{object}	error.ErrorResponse
// @Failure		429				{object}	error.ErrorResponse
// @Failure		500				{object}	error.ErrorResponse
// @Router			/performance_insight/tidb_connection/clinic_outbound_public_addresses [get]
func (t *TaskAPI) HandleGetClinicPublicAddresses(g *gin.Context) {
	// Check user header auth info.
	_, err := getUserId(g)
	if err != nil {
		_ = g.Error(error.ErrUnauthorized.New("%s", err.Error()))
	}

	addresses, err := t.taskSvc.GetClinicPublicAddresses(g)
	if err != nil {
		_ = g.Error(err)
		return
	}

	g.JSON(http.StatusOK, GetClinicPublicAddressResponse{Addresses: addresses})
}

func maskFailedMessage(message string) string {
	array := strings.Split(message, ". Detail: ") // See: performance_insight/task/scanner_performance_insight.go
	if len(array) == 0 {
		return ""
	}
	return array[0]
}
