package handler

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

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

// ListCaseExecutions ...
// @Summary List recent case-executions (order by updated time desc)
// @ID list-case-executions
// @Tags case-execution
// @Security Bearer
// @Param root_plan_id query uint64 false "Filter by root_plan_id"
// @Param root_plan_exec_id query uint64 false "Filter by root_plan_exec_id"
// @Param ids query []uint64 false "Filter by case execution ids" collectionFormat(csv)
// @Param case_ids query []uint64 false "Filter by case ids" collectionFormat(csv)
// @Param status query string false "Filter by status" Enums(PENDING,RUNNING,SUCCESS,FAILURE,SKIPPED,TIMEOUT,CANCELLED,ERROR)
// @Param offset query integer false "Offset/skip number of case-executions" default(0) minimum(0)
// @Param count query integer false "Number of case-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.CaseExecutionListResponse
// @Failure default {object} HTTPError
// @Router /case-executions [get]
func (h *HTTPHandler) ListCaseExecutions(c echo.Context) error {
	ctx := c.(utils.Context)

	var rootPlanID *model.ID
	if id := ctx.QueryParam("root_plan_id"); id != "" {
		id, err := utils.ParseID(id)
		if err != nil {
			return serror.New(http.StatusBadRequest, err)
		}
		rootPlanID = &id
	}

	var rootPlanExecID *model.ID
	if id := ctx.QueryParam("root_plan_exec_id"); id != "" {
		id, err := utils.ParseID(id)
		if err != nil {
			return serror.New(http.StatusBadRequest, err)
		}
		rootPlanExecID = &id
	}

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

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

	var status *model.ExecutionStatus
	status_ := model.ExecutionStatus(ctx.QueryParam("status"))
	if status_ != "" {
		if !status_.IsValid() {
			err := fmt.Errorf("unknown status %s", status_)
			return serror.New(http.StatusBadRequest, err)
		}
		status = &status_
	}

	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
	}

	opts := common.CaseExecQueryOpts{
		IDs:            ids,
		CaseIDs:        caseIDs,
		RootPlanID:     rootPlanID,
		RootPlanExecID: rootPlanExecID,
		Offset:         uint(offset),
		Count:          uint(count),
	}
	if status != nil {
		opts.Status = []model.ExecutionStatus{*status}
	}

	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("rootPlanExecID", rootPlanExecID, "caseIDs", caseIDs, "offset", offset, "count", count).Infof("ListCaseExecutions")

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

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

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

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

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

// CreateCaseExecution ...
// @Summary Create new case-execution
// @ID create-case-execution
// @Tags case-execution
// @Security Bearer
// @Param case-execution body reqM.CaseExecutionCreateRequest true "CaseExecution to create"
// @Success 200 {object} model.TestCaseExecution
// @Failure default {object} HTTPError
// @Router /case-executions [post]
func (h *HTTPHandler) CreateCaseExecution(c echo.Context) error {
	ctx := c.(utils.Context)

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

	log := ctx.Log()
	log.With(
		"rootPlanExecID", e.RootPlanExecID,
		"caseID", e.CaseID,
		"status", e.Status,
	).Infof("CreateCaseExecution")

	exec, err := h.service.CaseExec.Create(ctx, e)
	if err != nil {
		log.Errorf(err, "CreateCaseExecution failed")
		return err
	}

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

// UpdateCaseExecution ...
// @Summary Update an existing case-execution, can not update if the execution's status is already not PENDING or RUNNING
// @ID update-case-execution
// @Tags case-execution
// @Security Bearer
// @Param id path integer true "CaseExecution ID"
// @Param case-execution body reqM.CaseExecutionUpdateRequest true "CaseExecution to update"
// @Success 200 {object} model.TestCaseExecution
// @Failure default {object} HTTPError
// @Router /case-executions/{id} [put]
func (h *HTTPHandler) UpdateCaseExecution(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 := bindCaseExecutionUpdateRequest(ctx)
	if err != nil {
		ctx.Log().Warnf("failed to parse request, err= %v", err)
		return err
	}

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

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

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

// UpdateCaseExecutionNote ...
// @Summary Update an case-execution note.
// @ID update-case-execution-note
// @Tags case-execution
// @Security Bearer
// @Param id path integer true "CaseExecution ID"
// @Param note body reqM.ExecutionNoteUpdateRequest true "the note"
// @Success 200 {object} map[string]interface{}
// @Failure default {object} HTTPError
// @Router /case-executions/{id}/note [put]
func (h *HTTPHandler) UpdateCaseExecutionNote(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("caseExecutionID", id).Infof("UpdateCaseExecutionNote")

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

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

// UpdateCaseExecutionReason ...
// @Summary Update an case-execution reason.
// @ID update-case-execution-reason
// @Tags case-execution
// @Security Bearer
// @Param id path integer true "CaseExecution ID"
// @Param reason body reqM.ExecutionReasonUpdateRequest true "the reason"
// @Success 200 {object} map[string]interface{}
// @Failure default {object} HTTPError
// @Router /case-executions/{id}/reason [put]
func (h *HTTPHandler) UpdateCaseExecutionReason(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("caseExecutionID", id).Infof("UpdateCaseExecutionReason")

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

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

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

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