package handler

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

	validation "github.com/Thearas/ozzo-validation/v4"
	"github.com/creasty/defaults"
	"github.com/labstack/echo/v4"
	"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/common/model/tcms/validator"
	"github.com/pingcap/test-infra/pkg/docker"
	"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"
	"github.com/pingcap/test-infra/services/tcms/utils/timing"
)

// ListCases ...
// @Summary List recent cases order by updated time desc (default not to return all fields).
// @ID list-cases
// @Tags case
// @Security Bearer
// @Param query query string false "Search by query, all but query and tags are ignored" minlength(1) maxlength(255)
// @Param tags query []string false "Filter by tags label" collectionFormat(csv) maxItems(10) uniqueItems(true)
// @Param ids query []uint64 false "Filter by case ids" collectionFormat(csv) maxItems(100) uniqueItems(true)
// @Param names query []string false "Filter by case names" collectionFormat(csv) maxItems(100) uniqueItems(true)
// @Param designer query string false "Search by designer email" minlength(1) maxlength(255)
// @Param tcctl query bool false "Export as tcctl format"
// @Param allFields query bool false "Export all case fields"
// @Param offset query integer false "Offset/skip number of cases" default(0) minimum(0)
// @Param count query integer false "Number of cases returned" default(20) minimum(1) maximum(100)
// @Success 200 {object} respM.CaseSearchResponse
// @Failure default {object} HTTPError
// @Router /cases [get]
func (h *HTTPHandler) ListCases(c echo.Context) error {
	ctx := c.(utils.Context)

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

	tags, err := utils.ParseCSV(ctx.QueryParam("tags"))
	if err != nil {
		return serror.NewMsg(http.StatusBadRequest, "parse tags failed, err= %v", err)
	} else if err := validation.Each(validator.Tag).Validate(tags); err != nil {
		return serror.NewMsg(http.StatusBadRequest, "invalid tags, err= %v", err)
	}

	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("query", query, "tags", tags, "offset", offset, "count", count).Infof("ListCases")

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

	// use db
	ids, err := utils.ParseCSVID(ctx.QueryParam("ids"))
	if err != nil {
		return serror.New(http.StatusBadRequest, err)
	}
	caseNames, err := utils.ParseCSV(ctx.QueryParam("names"))
	if err != nil {
		return serror.New(http.StatusBadRequest, err)
	}
	designerEmail := ctx.QueryParam("designer")
	if err := validator.PingCAPEmail.Validate(designerEmail); err != nil {
		return serror.New(http.StatusBadRequest, err)
	}
	tcctl, _ := strconv.ParseBool(ctx.QueryParam("tcctl"))
	allFields, _ := strconv.ParseBool(ctx.QueryParam("allFields"))
	cases, total, err := h.service.Case.List(ctx, common.CaseQueryOpts{
		Tags:          tags,
		IDs:           ids,
		Names:         caseNames,
		DesignerEmail: designerEmail,
		// Return all fields if export as tcctl format.
		AllFields:        allFields || tcctl,
		SortedDescending: true,
		Offset:           uint(offset),
		Count:            uint(count),
	})
	if err != nil {
		log.Errorf(err, "ListCases failed")
		return err
	}

	if tcctl {
		return ctx.YAML(http.StatusOK, toTcctlCases(cases)...)
	}
	return ctx.JSON(http.StatusOK, respM.CaseListResponse{
		Total: total,
		Data:  cases,
	})
}

// AutoCompleteCase ...
// @Summary Auto complete case by name.
// @ID autocomplete-case
// @Tags case
// @Security Bearer
// @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.CaseListResponse
// @Failure default {object} HTTPError
// @Router /cases/autocomplete [get]
func (h *HTTPHandler) AutoCompleteCase(c echo.Context) error {
	ctx := c.(utils.Context)

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

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

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

// GetCase ...
// @Summary Get case by ID
// @ID get-case
// @Tags case
// @Security Bearer
// @Param id path integer true "Case ID"
// @Param revision query integer false "Get case of this revision. You can view all revisions in /cases/{id}/histories."
// @Param ts query string false "Get case of the most recently revision which was created before ts, RFC3339Nano format, e.g. 2022-02-01T16:09:53+08:00"
// @Param tcctl query bool false "Export as tcctl format"
// @Success 200 {object} model.TestCase
// @Failure default {object} HTTPError
// @Router /cases/{id} [get]
func (h *HTTPHandler) GetCase(c echo.Context) error {
	ctx := c.(utils.Context)

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

	var opts *common.HistoryGetOpts
	revision := ctx.QueryParam("revision")
	ts := ctx.QueryParam("ts")
	if revision != "" {
		v, err := utils.ParseID(revision)
		if err != nil {
			return serror.New(http.StatusBadRequest, err)
		}
		opts = &common.HistoryGetOpts{Revision: &v}
	} else if ts != "" {
		t, err := time.Parse(time.RFC3339Nano, ts)
		if err != nil {
			return serror.NewMsg(http.StatusBadRequest, "ts should be a valid RFC3339 time: %v", err)
		}
		opts = &common.HistoryGetOpts{Ts: &t}
	}

	log := ctx.Log().With("caseID", id, "revision", revision, "ts", ts)
	log.Infof("GetCase")

	tc, err := h.service.Case.Get(ctx, id, opts)
	if err != nil {
		log.Errorf(err, "GetCase failed")
		return err
	} else if tc == nil {
		return serror.New(http.StatusNotFound)
	}

	if tcctl, _ := strconv.ParseBool(ctx.QueryParam("tcctl")); tcctl {
		return ctx.YAML(http.StatusOK, toTcctlCases([]*model.TestCase{tc})[0])
	}
	return ctx.JSON(http.StatusOK, tc)
}

// ListCaseHistories ...
// @Summary List case histories by ID.
// @ID list-case-histories
// @Tags case
// @Security Bearer
// @Param id path integer true "Case ID"
// @Param from_ts query string false "Get case history after the ts. RFC3339Nano format, e.g. 2022-02-01T13:09:53+08:00"
// @Param to_ts query string false "Get case history before the ts, RFC3339Nano format, e.g. 2022-02-01T16:09:53+08:00"
// @Param offset query integer false "Offset/skip number of items" default(0) minimum(0)
// @Param count query integer false "Number of items returned" default(20) minimum(1) maximum(100)
// @Success 200 {object} []model.HistoryRecord
// @Failure default {object} HTTPError
// @Router /cases/{id}/histories [get]
func (h *HTTPHandler) ListCaseHistories(c echo.Context) error {
	ctx := c.(utils.Context)

	id, err := utils.ParseID(ctx.Param("id"))
	if err != nil {
		return serror.New(http.StatusBadRequest, err)
	}
	var fromTs, toTs *time.Time
	ts := c.QueryParam("from_ts")
	if ts != "" {
		t, err := utils.ParseTime(ts)
		if err != nil {
			return serror.NewMsg(http.StatusBadRequest, "from_ts should be a valid RFC3339 time: %v", err)
		}
		fromTs = &t
	}
	ts = c.QueryParam("to_ts")
	if ts != "" {
		t, err := utils.ParseTime(ts)
		if err != nil {
			return serror.NewMsg(http.StatusBadRequest, "to_ts should be a valid RFC3339 time: %v", err)
		}
		toTs = &t
	}
	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().With("caseID", id)
	log.Infof("GetCaseHistories")

	histories, err := h.service.Case.ListHistories(ctx, id, common.HistoryListOpts{
		FromTs: fromTs,
		ToTs:   toTs,
		Offset: uint(offset),
		Count:  uint(count),
	})
	if err != nil {
		log.Errorf(err, "GetCaseHistories failed")
		return err
	}
	return ctx.JSON(http.StatusOK, histories)
}

// CreateCase ...
// @Summary Create new case
// @ID create-case
// @Tags case
// @Security Bearer
// @Param dryrun query bool true "Dry run" default(false)
// @Param case body reqM.CaseCreateRequest true "Case to create"
// @Success 200 {object} model.TestCase
// @Failure default {object} HTTPError
// @Router /cases [post]
func (h *HTTPHandler) CreateCase(c echo.Context) error {
	ctx := c.(utils.Context)

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

	log := ctx.Log().With("caseName", case_.Name)
	log.Infof("CreateCase")

	testCase, err := h.service.Case.Create(ctx, case_)
	if err != nil {
		log.Errorf(err, "CreateCase failed")
		return err
	}
	return ctx.JSON(http.StatusOK, testCase)
}

// UpdateCase ...
// @Summary Override an existing case
// @ID update-case
// @Tags case
// @Security Bearer
// @Param id path integer true "Case ID"
// @Param case body reqM.CaseUpdateRequest true "Case to override"
// @Success 200 {object} model.TestCase
// @Failure default {object} HTTPError
// @Router /cases/{id} [put]
func (h *HTTPHandler) UpdateCase(c echo.Context) error {
	ctx := c.(utils.Context)

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

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

	log := ctx.Log().With("caseID", id, "caseName", case_.Name)
	log.Infof("UpdateCase")

	tc, err := h.service.Case.Update(ctx, id, (*reqM.CaseUpdateRequest)(case_))
	if err != nil {
		log.Errorf(err, "UpdateCase failed")
		return err
	}

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

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

	if err := h.service.Case.Delete(ctx, id); err != nil {
		log.Warnf("DeleteCase failed, err= %v", err)
		return err
	}

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

func (h *HTTPHandler) bindCaseCreateRequest(ctx utils.Context) (*reqM.CaseCreateRequest, error) {
	req := &reqM.CaseCreateRequest{}
	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("caseName", req.Name, "designer", req.Designer)

	g := errgroup.Group{}
	g.Go(func() (err error) {
		var designer *model.User
		if req.Designer != "" {
			designer, err = h.service.User.GetByEmail(ctx, req.Designer)
		} else {
			designer, err = h.service.User.Get(ctx, ctx.User().ID)
		}
		if err != nil {
			log.Errorf(err, "get designer id failed")
			return err
		} else if designer == nil {
			return serror.New(http.StatusBadRequest, fmt.Errorf("designer not found: %s", req.Designer))
		}
		req.DesignerID = designer.ID
		req.Designer = designer.Email
		return nil
	})

	if req.Framework.Type() != model.FrameworkTypeUnknown {
		timing := timing.RPC(ctx.Request().Context(), "check-image")
		image := req.Framework.Generic.Image
		checker, err := docker.NewImageCheckerFromImage(image)
		if err != nil {
			return nil, err
		}
		if exist, err := checker.IsImageExist(); err != nil {
			return nil, serror.NewMsg(http.StatusBadRequest, "validate image existence failed: %v", err)
		} else if !exist {
			return nil, serror.New(http.StatusBadRequest, fmt.Errorf("image %s doesn't exist", image))
		}
		timing.End()
	}

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

// TODO: Use common model.
type CaseWithAdditionalInfo struct {
	reqM.CaseCreateRequest      `yaml:",inline"`
	model.CreatedAtAndUpdatedAt `yaml:",inline"`
	ID                          model.ID `yaml:"id,omitempty"`
	DeletedAt                   int64    `yaml:"deletedAt,omitempty"`
}

func toTcctlCases(cases []*model.TestCase) []interface{} {
	tcctlCases := make([]interface{}, len(cases))
	for i, c := range cases {
		tcctlCases[i] = &CaseWithAdditionalInfo{
			CaseCreateRequest:     *new(reqM.CaseCreateRequest).FromTestCase(c),
			ID:                    c.ID,
			CreatedAtAndUpdatedAt: c.CreatedAtAndUpdatedAt,
			DeletedAt:             c.DeletedAt,
		}
	}
	return tcctlCases
}
