package handler

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

	validation "github.com/Thearas/ozzo-validation/v4"
	"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"
)

// ListPlanExecutions ...
// @Summary List recent plan-executions (order by created time desc)
// @ID list-plan-executions
// @Tags plan-execution
// @Security Bearer
// @Param query query string false "Search by query" minlength(1) maxlength(255)
// @Param email query string false "Filter by creator or maintainer"
// @Param ids query []uint64 false "Filter by plan execution ids" collectionFormat(csv)
// @Param plan_ids query []uint64 false "Filter by plan ids" collectionFormat(csv)
// @Param trigger_ids query []uint64 false "Filter by trigger ids, can only choose one between plan_ids and trigger_ids" collectionFormat(csv)
// @Param status query string false "Filter by status" Enums(PENDING,RUNNING,SUCCESS,FAILURE,SKIPPED,TIMEOUT,CANCELLED,ERROR)
// @Param triage_phase query string false "Filter by triage_phase" Enums(TO_TRIAGE,TRIAGING,TRIAGED)
// @Param resource_pool_id query uint64 false "Filter by resourcePool"
// @Param has_living_testbed query bool false "Filter plan-execution has living testbeds"
// @Param cron query bool false "Filter cron plan-execution"
// @Param offset query integer false "Offset/skip number of plan-executions" default(0) minimum(0)
// @Param count query integer false "Number of plan-executions returned" default(20) minimum(1) maximum(100)
// @Param date query string false "Filter by StartedAt, choose by Asia/Shanghai date. format(2006-01-02)"
// @Success 200 {object} respM.PlanExecutionSearchResponse
// @Failure default {object} HTTPError
// @Router /plan-executions [get]
func (h *HTTPHandler) ListPlanExecutions(c echo.Context) error {
	ctx := c.(utils.Context)

	query := ctx.QueryParam("query")
	if len(query) > 255 {
		query = string(([]rune(query))[:255])
	}
	email := ctx.QueryParam("email")
	if len(email) > 255 {
		email = string(([]rune(email))[:255])
	}
	if email != "" && !strings.HasSuffix(email, "@pingcap.com") {
		email += "@pingcap.com"
	}

	var status *model.ExecutionStatus
	status_ := model.ExecutionStatus(ctx.QueryParam("status"))
	if status_ != "" {
		if err := status_.Validate(); err != nil {
			return serror.New(http.StatusBadRequest, err)
		}
		status = &status_
	}

	var triagePhase *model.TriagePhase
	phase := model.TriagePhase(ctx.QueryParam("triage_phase"))
	if phase != "" {
		if err := phase.Validate(); err != nil {
			return serror.New(http.StatusBadRequest, err)
		}
		triagePhase = &phase
	}

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

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

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

	// use search instead of db
	if query != "" || email != "" {
		var triggerID model.ID
		if len(triggerIDs) > 0 {
			triggerID = triggerIDs[0]
		}
		ctx.Log().With(
			"query", query,
			"email", email,
			"status", status,
			"triggerID", triggerID,
			"offset", offset,
			"count", count,
		).Infof("search plan-executions")
		data, total, err := h.service.Search.SearchPlanExecutions(ctx, query, email, status, triggerID, uint(offset), uint(count))
		if err != nil {
			ctx.Log().Errorf(err, "search plan-executions failed")
			return err
		}
		return ctx.JSON(http.StatusOK, struct {
			Total int           `json:"total"`
			Data  []interface{} `json:"data"`
		}{
			Total: total,
			Data:  data,
		})
	}

	// use db
	planIDs, err := utils.ParseCSVID(ctx.QueryParam("plan_ids"))
	if err != nil {
		return serror.New(http.StatusBadRequest, err)
	}

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

	if len(planIDs) > 0 && len(triggerIDs) > 0 {
		return serror.NewMsg(http.StatusBadRequest, "can only choose one between plan_ids and trigger_ids")
	}

	// check has_living_testbed
	hasLivingTestbed, _ := strconv.ParseBool(ctx.QueryParam("has_living_testbed"))

	onlyCron, _ := strconv.ParseBool(ctx.QueryParam("cron"))

	opts := common.PlanExecQueryOpts{
		IDs:              ids,
		PlanIDs:          planIDs,
		TriggerIDs:       triggerIDs,
		OnlyCron:         onlyCron,
		HasLivingTestbed: hasLivingTestbed,
		SortedBy:         common.PlanExecSortedByCreatedAt,
		SortedDescending: true,
		Offset:           uint(offset),
		Count:            uint(count),
	}
	if status != nil {
		opts.Status = []model.ExecutionStatus{*status}
	}

	if triagePhase != nil {
		opts.TriagePhase = triagePhase
	}

	if rpID, err := utils.ParseID(ctx.QueryParam("resource_pool_id")); err == nil {
		opts.ResourcePoolID = &rpID
	}

	if date := ctx.QueryParam("date"); date != "" {
		if theDate, err := time.ParseInLocation("2006-01-02", date, time.Local); err != nil {
			return serror.NewMsg(http.StatusBadRequest, "parse date failed, err= %v", err)
		} else {
			start := theDate.Unix()
			to := theDate.Add(24 * time.Hour).Unix()
			opts.FromStartedAt = &start
			opts.ToStartedAt = &to
		}
	}

	log := ctx.Log()
	log.With("planIDs", planIDs, "triggerIDs", triggerIDs, "offset", offset, "count", count).Infof("ListPlanExecutions")

	execs, total, err := h.service.PlanExec.List(ctx, opts)
	if err != nil {
		log.Errorf(err, "ListPlanExecutions failed")
		return err
	}

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

// GetPlanExecution ...
// @Summary Get plan-execution by ID
// @ID get-plan-execution
// @Tags plan-execution
// @Security Bearer
// @Param id path integer true "PlanExecution ID"
// @Success 200 {object} model.TestPlanExecution
// @Failure default {object} HTTPError
// @Router /plan-executions/{id} [get]
func (h *HTTPHandler) GetPlanExecution(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("planExecutionID", id)
	log.Infof("GetPlanExecution")

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

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

// UpdatePlanExecution ...
// @Summary Update an existing plan-execution, can not update if the execution's status is already not PENDING or RUNNING
// @ID update-plan-execution
// @Tags plan-execution
// @Security Bearer
// @Param id path integer true "PlanExecution ID"
// @Param plan-execution body reqM.PlanExecutionUpdateRequest true "PlanExecution to update"
// @Success 200 {object} model.TestPlanExecution
// @Failure default {object} HTTPError
// @Router /plan-executions/{id} [put]
func (h *HTTPHandler) UpdatePlanExecution(c echo.Context) error {
	ctx := c.(utils.Context)

	id, err := utils.ParseID(ctx.Param("id"))
	if err != nil {
		return serror.New(http.StatusBadRequest, err)
	}
	e, err := bindPlanExecutionUpdateRequest(ctx)
	if err != nil {
		ctx.Log().Warnf("failed to parse request, err= %v", err)
		return err
	}

	log := ctx.Log()
	log.With("planExecutionID", id, "status", e.Status).Infof("UpdatePlanExecution")

	// can not update if the execution's status is already not PENDING, RUNNING or CANCELLING
	oldStatus, err := h.service.PlanExec.GetStatus(ctx, id)
	if err != nil {
		return err
	} else if oldStatus == nil {
		return serror.NewMsg(http.StatusNotFound, "plan-exec #%d not found", id)
	} else if !oldStatus.CanTransferTo(e.Status) {
		log.With("oldStatus", oldStatus, "newStatus", e.Status).Warnf("can not update status")
		return serror.NewMsg(http.StatusBadRequest, "can not update execution from %s to %s", *oldStatus, e.Status)
	}

	if e.Status.NeedTriage() {
		log.Infof("plan execution %d need triage", id)
		// TODO:@gaozifeng notify maintainers to triage here
		// TODO:@codingpoet initiate triagingUsers with plan maintainers
		caseExecs, _, err := h.service.CaseExec.List(ctx, common.CaseExecQueryOpts{
			Status: []model.ExecutionStatus{
				model.ExecutionStatusFailure,
				model.ExecutionStatusTimeout,
			},
			RootPlanExecID: &id,
		})

		if err != nil {
			return err
		}

		var toTriageCaseExecIDs []model.ID
		for _, caseExec := range caseExecs {
			toTriageCaseExecIDs = append(toTriageCaseExecIDs, caseExec.ID)
		}

		_, err = h.service.Triage.CreatePlanTriage(ctx, &model.Triage{
			PlanExecutionID:     id,
			TriagingUsers:       []model.ID{},
			ToTriageCaseExecIDs: toTriageCaseExecIDs,
			TriagedCaseExecIDs:  []model.ID{},
		})
		if err != nil {
			return err
		}
	}

	exec, err := h.service.PlanExec.Update(ctx, model.ID(id), e)
	if err != nil {
		log.Errorf(err, "UpdatePlanExecution failed")
		return err
	}

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

// GetPlanByPlanExecution ...
// @Summary Get plan rendered by plan-execution
// @ID get-plan-by-plan-execution
// @Tags plan-execution
// @Security Bearer
// @Param id path integer true "PlanExecution ID"
// @Success 200 {object} model.TestPlan
// @Failure default {object} HTTPError
// @Router /plan-executions/{id}/plan [get]
func (h *HTTPHandler) GetPlanByPlanExecution(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("planExecutionID", id)
	log.Infof("GetPlanExecution")

	exec, err := h.service.PlanExec.Get(ctx, model.ID(id))
	if err != nil {
		log.Errorf(err, "getPlanExecution when GetPlanByPlanExecution failed")
		return err
	} else if exec == nil {
		return serror.New(http.StatusNotFound)
	}

	plan, err := h.service.Plan.Get(ctx, exec.PlanIDAndName.ID)
	if err != nil {
		log.Errorf(err, "getPlan when GetPlanByPlanExecution failed")
		return err
	} else if exec == nil {
		return serror.New(http.StatusNotFound)
	}

	pb, err := plan.PatchParameters(&exec.MetaParams, false)
	if err != nil {
		return err
	}

	plan.TestPlanBase = *pb

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

// UpdatePlanExecutionNote ...
// @Summary Update an plan-execution note.
// @ID update-plan-execution-note
// @Tags plan-execution
// @Security Bearer
// @Param id path integer true "PlanExecution ID"
// @Param note body reqM.ExecutionNoteUpdateRequest true "the note"
// @Success 200 {object} map[string]interface{}
// @Failure default {object} HTTPError
// @Router /plan-executions/{id}/note [put]
func (h *HTTPHandler) UpdatePlanExecutionNote(c echo.Context) error {
	ctx := c.(utils.Context)

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

	r := reqM.ExecutionNoteUpdateRequest{}
	if err := ctx.Bind(&r); err != nil {
		return serror.New(http.StatusBadRequest, err)
	}
	if err := r.Validate(); err != nil {
		return serror.New(http.StatusBadRequest, err)
	}

	log := ctx.Log()
	log.With("planExecutionID", id).Infof("UpdatePlanExecutionNote")

	if err := h.service.PlanExec.UpdateNote(ctx, id, r.Note); err != nil {
		log.Errorf(err, "UpdatePlanExecutionNote failed")
		return err
	}

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

// UpdatePlanExecutionReason ...
// @Summary Update an plan-execution reason.
// @ID update-plan-execution-reason
// @Tags plan-execution
// @Security Bearer
// @Param id path integer true "PlanExecution ID"
// @Param reason body reqM.ExecutionReasonUpdateRequest true "reason"
// @Success 200 {object} map[string]interface{}
// @Failure default {object} HTTPError
// @Router /plan-executions/{id}/reason [put]
func (h *HTTPHandler) UpdatePlanExecutionReason(c echo.Context) error {
	ctx := c.(utils.Context)

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

	r := reqM.ExecutionReasonUpdateRequest{}
	if err := ctx.Bind(&r); err != nil {
		return serror.New(http.StatusBadRequest, err)
	}
	if err := r.Validate(); err != nil {
		return serror.New(http.StatusBadRequest, err)
	}

	log := ctx.Log()
	log.With("planExecutionID", id).Infof("UpdatePlanExecutionReason")

	if err := h.service.PlanExec.UpdateReason(ctx, id, r.Reason); err != nil {
		log.Errorf(err, "UpdatePlanExecutionReason failed")
		return err
	}

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

// RerunPlanExecution ...
// @Summary Rerun the plan-execution.
// @ID rerun-plan-execution
// @Tags plan-execution
// @Security Bearer
// @Param id path integer true "Execution ID"
// @Param only_failed query bool false "Only rerun failed steps (default to rerun all steps, include success)" default(false)
// // @Param steps query []string false "Select plan steps, incompatible with `only_failed`" collectionFormat(csv)
// @Success 200 {object} map[string]interface{}
// @Failure default {object} HTTPError
// @Router /plan-executions/{id}/rerun [post]
func (h *HTTPHandler) RerunPlanExecution(c echo.Context) error {
	ctx := c.(utils.Context)

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

	steps, err := utils.ParseCSV(ctx.QueryParam("steps"), model.MaxPlanStepsLength)
	if err != nil {
		return serror.NewMsg(http.StatusBadRequest, "steps is invalid, err: %v", err)
	} else if err := validation.Each(model.PipelineStepNameValidationRule).Validate(steps); err != nil {
		return serror.NewMsg(http.StatusBadRequest, "steps is invalid, err: %v", err)
	}

	// get step-names from case-executions
	onlyFailed, _ := strconv.ParseBool(ctx.QueryParam("only_failed"))
	if onlyFailed {
		if len(steps) > 0 {
			return serror.NewMsg(http.StatusBadRequest, "steps and only_failed are incompatible")
		}
		steps, err = h.service.PlanExec.ListFailedSteps(ctx, id)
		if err != nil {
			return err
		} else if len(steps) == 0 {
			return serror.NewMsg(http.StatusBadRequest, "no failed steps in plan-execution #%d", id)
		}

		// NOTE: The old case-executions will not have step_name,
		// we return error in that situation.
		for _, step := range steps {
			if step == "" {
				return serror.NewMsg(http.StatusBadRequest, "the execution is too old, partial rerunning it is not allowed")
			}
		}
	}

	log := ctx.Log()
	log.With(
		"id", id,
		"onlyFailed", onlyFailed,
		"steps", steps,
	).Infof("RerunPlanExecution")

	execID, err := h.service.PlanExec.Rerun(ctx, id, steps)
	if err != nil {
		log.Errorf(err, "RerunPlanExecution failed")
		return err
	}
	return ctx.JSON(http.StatusOK, &respM.PlanExecutionRerunResponse{
		ID:  execID,
		URL: h.getTCMSURLForPlanExec(execID),
	})
}

// CancelPlanExecution ...
// @Summary Cancel pending plan-executions.
// @ID cancel-plan-execution
// @Tags plan-execution
// @Security Bearer
// @Param ids query []uint64 false "Execution IDs" collectionFormat(csv)
// @Success 200 {object} map[string]interface{}
// @Failure default {object} HTTPError
// @Router /plan-executions/cancel [put]
func (h *HTTPHandler) CancelPlanExecution(c echo.Context) error {
	ctx := c.(utils.Context)

	execIDs, err := utils.ParseCSVID(ctx.QueryParam("ids"))
	if err != nil {
		return serror.New(http.StatusBadRequest, err)
	} else if len(execIDs) == 0 {
		return serror.NewMsg(http.StatusBadRequest, "execution ids is empty")
	}

	log := ctx.Log()
	log.With("execIDs", execIDs).Infof("CancelPlanExecution")

	err = h.service.PlanExec.Cancel(ctx, nil, execIDs)
	if err != nil {
		log.Errorf(err, "CancelPlanExecution failed")
		return err
	}

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

// ListPlanExecutionTestbeds ...
// @Summary List testbeds of a plan-execution.
// @ID list-plan-execution-testbed
// @Tags plan-execution
// @Security Bearer
// @Param id path uint64 true "Plan execution id"
// @Success 200 {object} respM.TestBedInfoListResponse
// @Failure default {object} HTTPError
// @Router /plan-executions/{id}/testbeds [get]
func (h *HTTPHandler) ListPlanExecutionTestbeds(c echo.Context) error {
	c.QueryParams().Set("plan_exec_id", c.Param("id"))
	c.QueryParams().Set("show_released", "1")
	return h.ListTestBedInfos(c)
}

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