package handler

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

	"github.com/creasty/defaults"
	"github.com/labstack/echo/v4"

	"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"
)

// ListTriggers ...
// @Summary List recent triggers (order by updated time desc)
// @ID list-triggers
// @Tags trigger
// @Param plan_id query uint64 false "Filter by plan ID"
// @Param type query string false "Filter by trigger type" Enums(ONCE, CRON, RELEASE)
// @Param creator query uint64 false "Filter by creator's ID"
// @Param release_branch query string false "Filter by release branch"
// @Param offset query integer false "Offset/skip number of triggers" default(0) minimum(0)
// @Param count query integer false "Number of triggers returned" default(20) minimum(1) maximum(100)
// @Success 200 {object} respM.TriggerListResponse
// @Failure default {object} HTTPError
// @Router /triggers [get]
func (h *HTTPHandler) ListTriggers(c echo.Context) error {
	ctx := c.(utils.Context)

	var planID *model.ID
	if id := c.QueryParam("plan_id"); id != "" {
		id, err := utils.ParseID(id)
		if err != nil {
			err := fmt.Errorf("parse plan_id failed, err= %v", err)
			return serror.New(http.StatusBadRequest, err)
		}
		_planID := model.ID(id)
		planID = &_planID
	}

	var triggerType *model.TriggerType
	ty := model.TriggerType(c.QueryParam("type"))
	if ty != "" {
		if !ty.IsValid() {
			err := fmt.Errorf("unknown type %s", ty)
			return serror.New(http.StatusBadRequest, err)
		}
		triggerType = &ty
	}

	var creator *model.ID
	if id := c.QueryParam("creator"); id != "" {
		id, err := utils.ParseID(id)
		if err != nil {
			err := fmt.Errorf("parse creator failed, err= %v", err)
			return serror.New(http.StatusBadRequest, err)
		}
		creator = &id
	}

	releaseBranch := c.QueryParam("release_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("type", triggerType, "creator", creator, "offset", offset, "count", count).Infof("ListTriggers")

	triggers, total, err := h.service.Trigger.List(ctx, common.TriggerQueryOpts{
		PlanID:        planID,
		Type:          triggerType,
		Creator:       creator,
		ReleaseBranch: releaseBranch,
		Offset:        uint(offset),
		Count:         uint(count),
	})
	if err != nil {
		log.Errorf(err, "ListTriggers failed")
		return err
	}

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

// AutoCompleteTrigger ...
// @Summary Auto complete trigger by name. This API only works on CRON trigger.
// @ID autocomplete-trigger
// @Tags trigger
// @Param query query string true "Filter by query" minlength(1) maxlength(255)
// @Param type query string false "Filter by type" Enums(CRON) default(CRON)
// @Param count query uint32 false "Number of items returned" default(10) minimum(1) maximum(100)
// @Success 200 {object} respM.TriggerListResponse
// @Failure default {object} HTTPError
// @Router /triggers/autocomplete [get]
func (h *HTTPHandler) AutoCompleteTrigger(c echo.Context) error {
	ctx := c.(utils.Context)

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

	// only support cron type trigger
	ty := model.TriggerType(c.QueryParam("type"))
	if ty != "" && ty != model.TriggerTypeCron {
		return serror.NewMsg(http.StatusBadRequest, "only support cron type trigger")
	}
	ty = model.TriggerTypeCron

	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)
	triggers, err := h.service.Trigger.AutoComplete(ctx, query, ty, count)
	if err != nil {
		log.Errorf(err, "auto complete failed")
		return err
	}

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

// GetTrigger ...
// @Summary Get trigger by ID
// @ID get-trigger
// @Tags trigger
// @Param id path integer true "Trigger ID"
// @Success 200 {object} model.TestTrigger
// @Failure default {object} HTTPError
// @Router /triggers/{id} [get]
func (h *HTTPHandler) GetTrigger(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("triggerID", id)
	log.Infof("GetTrigger")

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

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

// CreateTrigger ...
// @Summary Create new trigger
// @ID create-trigger
// @Tags trigger
// @Security Bearer
// @Param trigger body reqM.TriggerCreateRequest true "Trigger to create"
// @Success 200 {object} model.TestTrigger
// @Failure default {object} HTTPError
// @Router /triggers [post]
func (h *HTTPHandler) CreateTrigger(c echo.Context) error {
	ctx := c.(utils.Context)

	t, err := h.bindTriggerCreateRequest(ctx)
	if err != nil {
		ctx.Log().Warnf("failed to bind request, err= %v", err)
		return err
	}

	log := ctx.Log().With("refPlanID", t.PlanID)
	log.Infof("CreateTrigger")

	// create trigger
	tt, err := h.service.Trigger.Create(ctx, t)
	if err != nil {
		log.Errorf(err, "CreateTrigger failed")
		return err
	}

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

// UpdateTriggerPlanRef ...
// @Summary Override an existing trigger's PlanRef
// @ID update-trigger-plan-ref
// @Tags trigger
// @Security Bearer
// @Param id path integer true "Trigger ID"
// @Param planIDs body reqM.UpdateTriggerPlanRefRequest true "PlanIDs to override into PlanRef"
// @Success 200 {object} map[string]interface{}
// @Failure default {object} HTTPError
// @Router /triggers/{id}/plan-ref [put]
func (h *HTTPHandler) UpdateTriggerPlanRef(c echo.Context) error {
	ctx := c.(utils.Context)

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

	updateTriggerPlanRef := &reqM.UpdateTriggerPlanRefRequest{}
	if err := ctx.Bind(updateTriggerPlanRef); err != nil {
		return serror.New(http.StatusBadRequest, err)
	} else if len(updateTriggerPlanRef.NewPlanIDs) == 0 {
		return serror.New(http.StatusBadRequest, errors.New("newPlanIDs should not be empty"))
	}

	log := ctx.Log().With("triggerID", id, "refPlanID", updateTriggerPlanRef.NewPlanIDs)
	log.Infof("UpdateTriggerPlanIDs")

	// Update planIDs into trigger.PlanIDs
	// Create corresponding plan-executions
	err = h.service.Trigger.AddPlanIDs(ctx, id, updateTriggerPlanRef.NewPlanIDs)
	if err != nil {
		log.Errorf(err, "add trigger planID failed")
		return err
	}

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

// DeleteTrigger ...
// @Summary Delete an existing trigger
// @ID delete-trigger
// @Tags trigger
// @Security Bearer
// @Param id path integer true "Trigger ID"
// @Success 200 {object} map[string]interface{}
// @Failure default {object} HTTPError
// @Router /triggers/{id} [delete]
func (h *HTTPHandler) DeleteTrigger(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("triggerID", id)
	log.Infof("DeleteTrigger")

	if err := h.service.Trigger.Delete(ctx, id); err != nil {
		log.Errorf(err, "DeleteTrigger failed")
		return err
	}

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

func (h *HTTPHandler) bindTriggerCreateRequest(ctx utils.Context) (*reqM.TriggerCreateRequest, error) {
	req := &reqM.TriggerCreateRequest{}
	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)
	}

	log := ctx.Log().With("name", req.Name, "refPlanID", req.PlanID)

	if len(req.InlinePlans) > 0 {
		if err := checkInlineEntities(ctx, req.InlinePlans); err != nil {
			return nil, serror.NewMsg(http.StatusBadRequest, "bind inline plans failed, err= %v", err)
		}
	}

	if req.Type == model.TriggerTypeCron {
		return nil, serror.NewMsg(http.StatusBadRequest, "create or update a cron trigger is unsupported")
	} else if req.Type == model.TriggerTypeRelease {
		releasePlanIDs := []model.ID{}
		if req.AutoIncludeReleasePlans {
			var err error
			releasePlanIDs, err = h.service.Plan.GetReleasePlans(ctx, req.ReleaseBranch, 999)
			if err != nil {
				return nil, err
			}
		}
		req.PlanIDs = releasePlanIDs
		req.PlanID = 0
	} else if req.PlanID != 0 || len(req.PlanIDs) > 0 {
		planIDs := make(map[model.ID]struct{}, len(req.PlanIDs))
		if req.PlanID != 0 {
			planIDs[req.PlanID] = struct{}{}
		}
		for _, planID := range req.PlanIDs {
			if _, duplicated := planIDs[planID]; !duplicated {
				return nil, serror.NewMsg(http.StatusBadRequest, "the plan #%d referred by trigger is duplicated", planID)
			}
			planIDs[planID] = struct{}{}
		}
		allExist, err := h.service.Plan.AllExist(ctx, planIDs)
		if err != nil {
			log.Errorf(err, "check referenced plans exist failed")
			return nil, err
		} else if !allExist {
			log.Warnf("referenced plans not found, planIDs= %v", planIDs)
			return nil, serror.NewMsg(http.StatusBadRequest, "some plans are not found")
		}
	}

	req.Creator = ctx.User().ID
	return req, nil
}

func checkInlineEntities(ctx utils.Context, plans []*reqM.TriggerCreateRequestInlinePlan) error {
	const maxEntities = 1000
	nbEntities := 0
	for _, p := range plans {
		nbEntities += 1 + len(p.InlineCases)
	}
	if nbEntities > maxEntities {
		ctx.Log().Infof("too many inline entities, nbEntities= %d", nbEntities)
		return fmt.Errorf("too many inline entities, max %d, got %d", maxEntities, nbEntities)
	}
	return nil
}
