package handler

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

	"github.com/creasty/defaults"
	"github.com/labstack/echo/v4"
	"github.com/labstack/gommon/random"
	"golang.org/x/sync/errgroup"

	"github.com/pingcap/test-infra/common/model"
	reqM "github.com/pingcap/test-infra/common/model/tcms/request"
	respM "github.com/pingcap/test-infra/common/model/tcms/response"
	"github.com/pingcap/test-infra/services/tcms/service/common"
	serror "github.com/pingcap/test-infra/services/tcms/service/common/errors"
	"github.com/pingcap/test-infra/services/tcms/utils"
)

// ListPlans ...
// @Summary List recent plans (order by updated time desc)
// @ID list-plans
// @Tags plan
// @Security Bearer
// @Param query query string false "Search by query, all but query and tags are ignored" minlength(1) maxlength(255)
// @Param ids query []uint64 false "Filter by plan ids" collectionFormat(csv) maxItems(100) uniqueItems(true)
// @Param type query string false "Filter by plan type" Enums(GENERAL, GITHUB, ONE_SHOT)
// @Param branch query string false "Filter by GitHub branch"
// @Param offset query integer false "Offset/skip number of plans" default(0) minimum(0)
// @Param count query integer false "Number of plans returned" default(20) minimum(1) maximum(100)
// @Success 200 {object} respM.PlanListResponse
// @Failure default {object} HTTPError
// @Router /plans [get]
func (h *HTTPHandler) ListPlans(c echo.Context) error {
	ctx := c.(utils.Context)

	var err error
	query := ctx.QueryParam("query")
	if len(query) > 255 {
		query = string(([]rune(query))[:255])
	}

	ids, err := utils.ParseCSVID(ctx.QueryParam("ids"))
	if err != nil {
		return serror.New(http.StatusBadRequest, err)
	}

	ty := model.TestPlanType(c.QueryParam("type"))
	if err := ty.Validate(); ty != "" && err != nil {
		return serror.New(http.StatusBadRequest, err)
	}

	ghBranch := c.QueryParam("branch")

	offset, err := strconv.Atoi(c.QueryParam("offset"))
	if offset < 0 || err != nil {
		offset = 0
	}

	count, err := strconv.Atoi(c.QueryParam("count"))
	if count <= 0 || err != nil {
		count = 20
	} else if count > 500 {
		count = 500
	}

	log := ctx.Log()
	log.With("offset", offset, "count", count, "ghBranch", ghBranch).Infof("ListPlans")

	// use search instead of db
	if query != "" {
		data, total, err := h.service.Search.SearchPlans(ctx, query, ty, ghBranch, uint(offset), uint(count))
		if err != nil {
			log.Errorf(err, "SearchPlans failed")
			return err
		}
		return ctx.JSON(http.StatusOK, struct {
			Total int           `json:"total"`
			Data  []interface{} `json:"data"`
		}{
			Total: total,
			Data:  data,
		})
	}

	plans, total, err := h.service.Plan.List(ctx, common.PlanQueryOpts{
		IDs:              ids,
		Type:             ty,
		Branch:           ghBranch,
		SortedBy:         common.PlanSortedByUpdatedAt,
		SortedDescending: true,
		Offset:           uint(offset),
		Count:            uint(count),
	})
	if err != nil {
		log.Errorf(err, "ListPlans failed")
		return err
	}

	return ctx.JSON(http.StatusOK, respM.PlanListResponse{
		Total: total,
		Data:  plans,
	})
}

// ListReleasePlans ...
// @Summary List release plans (order by updated time desc)
// @ID list-release-plans
// @Tags plan
// @Security Bearer
// @Param triggerID query uint64 false "release trigger id"
// @Param branch query string false "Filter by GitHub branch"
// @Success 200 {object} respM.ListReleasePlansResponse
// @Failure default {object} HTTPError
// @Router /plans/release [get]
func (h *HTTPHandler) ListReleasePlans(c echo.Context) error {
	ctx := c.(utils.Context)

	triggerID, err := utils.ParseID(ctx.QueryParam("triggerID"))
	if err != nil {
		return serror.New(http.StatusBadRequest, err)
	}

	ghBranch := c.QueryParam("branch")
	if ghBranch == "" {
		return serror.New(http.StatusBadRequest, errors.New("param branch needed"))
	}

	log := ctx.Log()
	log.With("branch", ghBranch, "triggerID", triggerID).Infof("ListReleasePlans")

	var plans []*model.TestPlan
	var total int
	g := errgroup.Group{}

	g.Go(func() (err error) {
		plans, total, err = h.service.Plan.List(ctx, common.PlanQueryOpts{
			Branch: ghBranch,
			// FIXME@feihu support pages
			Count:            999,
			SortedBy:         common.PlanSortedByUpdatedAt,
			SortedDescending: true,
		})
		if err != nil {
			return err
		}
		return nil
	})

	var tt *model.TestTrigger
	g.Go(func() (err error) {
		tt, err = h.service.Trigger.Get(ctx, model.ID(triggerID))
		if err != nil {
			return err
		} else if tt == nil {
			return serror.New(http.StatusNotFound)
		}
		return nil
	})

	if err := g.Wait(); err != nil {
		return err
	}

	var notTriggerred, triggerred []*respM.TestPlanWithTriggerred
	var flags = make(map[model.ID]struct{})
	for _, v := range tt.PlanIDs {
		flags[v] = struct{}{}
	}

	for _, v := range plans {
		if _, ok := flags[v.ID]; !ok {
			// not triggered should be ahead of triggered
			notTriggerred = append(notTriggerred, &respM.TestPlanWithTriggerred{
				TestPlan:   v,
				Triggerred: false,
			})
		} else {
			// set Triggerred
			triggerred = append(triggerred, &respM.TestPlanWithTriggerred{
				TestPlan:   v,
				Triggerred: true,
			})
		}
	}

	res := append(notTriggerred, triggerred...)

	return ctx.JSON(http.StatusOK, respM.ListReleasePlansResponse{
		Total: total,
		Data:  res,
	})
}

// AutoCompletePlan ...
// @Summary Auto complete plan by name.
// @ID autocomplete-plan
// @Tags plan
// @Security Bearer
// @Param type query string false "Filter by plan type" Enums(GENERAL, GITHUB, ONE_SHOT) default(GENERAL)
// @Param query query string true "Filter by query" minlength(1) maxlength(255)
// @Param count query uint32 false "Number of items returned" default(10) minimum(1) maximum(100)
// @Success 200 {object} model.TestPlan
// @Failure default {object} HTTPError
// @Router /plans/autocomplete [get]
func (h *HTTPHandler) AutoCompletePlan(c echo.Context) error {
	ctx := c.(utils.Context)

	ty := model.TestPlanType(c.QueryParam("type"))
	if ty == "" || ty.Validate() != nil {
		ty = model.TestPlanKindGeneral
	}

	query := c.QueryParam("query")
	if query == "" || len(query) > 255 {
		return ctx.JSON(http.StatusOK, respM.PlanListResponse{
			Total: 0,
			Data:  []*model.TestPlan{},
		})
	}

	count, err := strconv.Atoi(c.QueryParam("count"))
	if count <= 0 || err != nil {
		count = 10
	} else if count > 500 {
		count = 500
	}

	log := ctx.Log().With("query", query, "count", count)
	plans, err := h.service.Plan.AutoComplete(ctx, ty, query, count)
	if err != nil {
		log.Errorf(err, "auto complete failed")
		return err
	}

	return ctx.JSON(http.StatusOK, respM.PlanListResponse{
		Total: len(plans),
		Data:  plans,
	})
}

// GetPlan ...
// @Summary Get plan by ID
// @ID get-plan
// @Tags plan
// @Security Bearer
// @Param id path integer true "Plan ID"
// @Success 200 {object} model.TestPlan
// @Failure default {object} HTTPError
// @Router /plans/{id} [get]
func (h *HTTPHandler) GetPlan(c echo.Context) error {
	ctx := c.(utils.Context)

	id, err := utils.ParseID(ctx.Param("id"))
	if err != nil {
		return serror.New(http.StatusBadRequest, err)
	}

	log := ctx.Log().With("planID", id)
	log.Infof("GetPlan")

	tp, err := h.service.Plan.Get(ctx, model.ID(id))
	if err != nil {
		log.Errorf(err, "GetPlan failed")
		return err
	} else if tp == nil {
		return serror.New(http.StatusNotFound)
	}

	var tpb *model.TestPlanBase
	if tpb, err = tp.PatchParameters(nil, false); err != nil {
		return err
	}
	tp.TestPlanBase = *tpb

	return ctx.JSON(http.StatusOK, tp)
}

// CreatePlan ...
// @Summary Create new plan (For Github plan, see `CreateGitHubPlan` in `plan_github.go`)
// @ID create-plan
// @Tags plan
// @Security Bearer
// @Param plan body reqM.PlanCreateRequest true "Plan to create"
// @Success 200 {object} respM.PlanCreateResponse
// @Failure default {object} HTTPError
// @Router /plans [post]
func (h *HTTPHandler) CreatePlan(c echo.Context) error {
	ctx := c.(utils.Context)

	plan, err := bindPlanCreateRequest(ctx)
	if err != nil {
		ctx.Log().Warnf("failed to parse request, err= %v", err)
		return err
	}

	log := ctx.Log().With("planName", plan.Name)
	log.Infof("CreatePlan")

	if plan.Type == model.TestPlanKindOneShot {
		createdPlan, execID, err := h.service.PlanExec.CreateOneShotPlan(ctx, plan)
		if err != nil {
			return err
		}
		return ctx.JSON(http.StatusOK, &respM.PlanCreateResponse{
			TestPlan:         createdPlan,
			PlanExecutionURL: h.getTCMSURLForPlanExec(execID),
		})
	}

	tp, err := h.service.Plan.Create(ctx, plan)
	if err != nil {
		log.Errorf(err, "CreatePlan failed")
		return err
	}

	return ctx.JSON(http.StatusOK, &respM.PlanCreateResponse{
		TestPlan: tp,
	})
}

func (h *HTTPHandler) getTCMSURLForPlanExec(execID model.ID) string {
	return fmt.Sprintf("%s/dashboard/executions/plan/%d", h.config.BaseURL, execID)
}

// DeletePlan ...
// @Summary Delete an existing plan (For Github plan, see `DeleteOldGitHubPlans` in `plan_github.go`)
// @ID delete-plan
// @Tags plan
// @Security Bearer
// @Param id path integer true "Plan ID"
// @Success 200 {object} map[string]interface{}
// @Failure default {object} HTTPError
// @Router /plans/{id} [delete]
func (h *HTTPHandler) DeletePlan(c echo.Context) error {
	ctx := c.(utils.Context)

	id, err := utils.ParseID(ctx.Param("id"))
	if err != nil {
		return serror.New(http.StatusBadRequest, err)
	}

	log := ctx.Log().With("planID", id)
	log.Infof("DeletePlan")

	if err := h.service.Plan.Delete(ctx, model.ID(id)); err != nil {
		log.Errorf(err, "DeletePlan failed")
		return err
	}

	return ctx.JSON(http.StatusOK, echo.Map{"result": "ok"})
}

func bindPlanCreateRequest(ctx utils.Context) (*reqM.PlanCreateRequest, error) {
	req := &reqM.PlanCreateRequest{}
	if err := ctx.Bind(req); err != nil {
		return nil, serror.New(http.StatusBadRequest, err)
	}
	defaults.MustSet(req)
	if err := req.Validate(); err != nil {
		return nil, serror.New(http.StatusBadRequest, err)
	}

	switch req.Type {
	case model.TestPlanKindOneShot:
		// give a random name if name is empty
		if req.Name == "" {
			req.Name += random.New().String(8, random.Lowercase)
		}
		req.Name += "@one_shot"
	case model.TestPlanKindGithub:
		return nil, serror.NewMsg(http.StatusBadRequest, "this API can not create GITHUB plan")
	}
	req.Creator = ctx.User().ID
	return req, nil
}
