package handler

import (
	"context"
	"net/http"

	"github.com/brpaz/echozap"
	"github.com/hashicorp/go-multierror"
	"github.com/labstack/echo/v4"
	echoSwagger "github.com/swaggo/echo-swagger"

	_ "github.com/pingcap/test-infra/services/tcms/docs"
	mw "github.com/pingcap/test-infra/services/tcms/handler/middleware"
	"github.com/pingcap/test-infra/services/tcms/service"
	"github.com/pingcap/test-infra/services/tcms/service/cache"
	serror "github.com/pingcap/test-infra/services/tcms/service/common/errors"
	"github.com/pingcap/test-infra/services/tcms/service/google"
	"github.com/pingcap/test-infra/services/tcms/service/internalrpc"
	"github.com/pingcap/test-infra/services/tcms/service/search"
	"github.com/pingcap/test-infra/services/tcms/service/store"
	"github.com/pingcap/test-infra/services/tcms/utils"
	"github.com/pingcap/test-infra/services/tcms/utils/log"
)

type HTTPHandler struct {
	Echo *echo.Echo

	config  utils.Config
	service *HandlerService
}

func (h *HTTPHandler) Shutdown() (errs error) {
	if err := h.Echo.Shutdown(context.Background()); err != nil {
		multierror.Append(errs, err)
	}
	if err := h.service.Shutdown(); err != nil {
		multierror.Append(errs, err)
	}
	return
}

// HandlerService holds the services used by handler.
type HandlerService struct {
	Shutdown func() error

	Search           service.SearchService
	Google           service.GoogleService
	RMSRPC           service.RMSRPCService
	User             service.UserService
	Token            service.TokenService
	Case             service.CaseService
	Plan             service.PlanService
	GitHubPlan       service.GitHubPlanService
	Trigger          service.TriggerService
	CaseExec         service.CaseExecutionService
	PlanExec         service.PlanExecutionService
	Testbed          service.TestbedService
	Summary          service.SummaryService
	RMS              service.RMSService
	ResourcePool     service.ResourcePoolService
	ResourceCohort   service.ResourceCohortService
	Triage           service.TriageService
	ResourceDuration service.ResourceDurationService
	TestbedSetup     service.TestbedSetupService
	SvcTask          service.SvcTaskService
	SvcTaskRun       service.SvcTaskRunService
	Store            *store.Store
}

func NewHandlerService(config *utils.Config) *HandlerService {
	internalrpc := internalrpc.New()
	store := store.New(config, internalrpc.RMS)
	s := &HandlerService{
		Search:           search.New(config.MeiliSearch.Endpoint),
		Google:           google.New(config.GoogleOAuthAud),
		RMSRPC:           internalrpc.RMS,
		User:             store.User,
		Token:            store.Token,
		Case:             store.Case,
		Plan:             store.Plan,
		GitHubPlan:       store.GitHubPlan,
		Trigger:          store.Trigger,
		CaseExec:         store.CaseExec,
		PlanExec:         store.PlanExec,
		Testbed:          store.Testbed,
		Summary:          store.Summary,
		RMS:              store.RMS,
		ResourcePool:     store.ResourcePool,
		ResourceCohort:   store.ResourceCohort,
		Triage:           store.Triage,
		ResourceDuration: store.ResourceDuration,
		TestbedSetup:     store.TestbedSetup,
		SvcTask:          store.SvcTask,
		SvcTaskRun:       store.SvcTaskRun,
		Store:            store,
	}

	// Use cache if possible.
	cache, err := cache.New(config.RedisAddr, store.User, store.Token)
	if err == nil {
		s.User = cache.User
		s.Token = cache.Token
	}

	s.Shutdown = func() (errs error) {
		if cache != nil {
			if err := cache.Shutdown(); err != nil {
				multierror.Append(errs, err)
			}
		}
		if err := store.Shutdown(); err != nil {
			multierror.Append(errs, err)
		}
		return errs
	}
	return s
}

// NewHTTPHandler @Title TCMS API
// @Version 1.0
// @BasePath /api/v1
// @produce	application/json
// @consumes application/json
// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
func NewHTTPHandler(config utils.Config, svc *HandlerService) *HTTPHandler {
	e := echo.New()
	// NOTE: It's always better to return `serror.Error` in handler.
	e.HTTPErrorHandler = HTTPErrorHandler
	mw.Setup(e)

	h := &HTTPHandler{
		Echo:    e,
		config:  config,
		service: svc,
	}

	e.GET("/ping", func(c echo.Context) error { return c.String(http.StatusOK, "pong") })
	e.GET("/swagger/*", echoSwagger.WrapHandler)

	mwAuth := mw.Auth(config.JWTKey, svc.Token.Validate)

	v1 := e.Group("/api/v1", echozap.ZapLogger(log.Log.Desugar()))
	{
		users := v1.Group("/users")
		users.GET("", h.ListUsers, mwAuth)
		users.GET("/current", h.CurrentUser, mwAuth)
		users.GET("/autocomplete", h.AutoCompleteUser, mwAuth)

		login := users.Group("/login")
		login.GET("/google-auth", h.LoginGoogleAuth)

		token := v1.Group("/tokens")
		token.GET("/validate", h.ValidateToken)
		token.GET("", h.ListTokens, mwAuth)
		token.POST("", h.CreateToken, mwAuth)
		token.DELETE("", h.RevokeToken, mwAuth)

		cases := v1.Group("/cases", mwAuth)
		cases.GET("", h.ListCases)
		cases.GET("/autocomplete", h.AutoCompleteCase)
		cases.GET("/:id", h.GetCase)
		cases.GET("/:id/histories", h.ListCaseHistories)
		cases.POST("", h.CreateCase, mw.SupportDryRun)
		cases.PUT("/:id", h.UpdateCase, mw.SupportDryRun)
		cases.DELETE("/:id", h.DeleteCase)

		plans := v1.Group("/plans", mwAuth)
		plans.GET("", h.ListPlans)
		plans.GET("/release", h.ListReleasePlans)
		plans.GET("/branchs", h.ListGitHubPlanBranchs)
		plans.GET("/autocomplete", h.AutoCompletePlan)
		plans.GET("/:id", h.GetPlan)
		plans.POST("", h.CreatePlan, mw.SupportDryRun)
		plans.DELETE("/:id", h.DeletePlan)

		ghplans := v1.Group("/gh-plans", mwAuth)
		{
			ghplans.POST("", h.CreateGitHubPlan, mw.SupportDryRun)
			ghplans.DELETE("/old", h.DeleteOldGitHubPlans)
		}

		triggers := v1.Group("/triggers", mwAuth)
		triggers.GET("", h.ListTriggers)
		triggers.GET("/autocomplete", h.AutoCompleteTrigger)
		triggers.GET("/:id", h.GetTrigger)
		triggers.POST("", h.CreateTrigger)
		triggers.PUT("/:id/plan-ref", h.UpdateTriggerPlanRef)
		triggers.DELETE("/:id", h.DeleteTrigger)
		triggers.GET("/:id/affected-version/cases", h.GetAffectedVersionCases)

		planExec := v1.Group("/plan-executions", mwAuth)
		planExec.GET("", h.ListPlanExecutions)
		planExec.PUT("/cancel", h.CancelPlanExecution)
		planExec.GET("/:id", h.GetPlanExecution)
		planExec.PUT("/:id", h.UpdatePlanExecution, mw.InternalAPI)
		planExec.GET("/:id/plan", h.GetPlanByPlanExecution)
		planExec.POST("/:id/rerun", h.RerunPlanExecution)
		planExec.PUT("/:id/note", h.UpdatePlanExecutionNote)
		planExec.PUT("/:id/reason", h.UpdatePlanExecutionReason)
		planExec.GET("/:id/testbeds", h.ListPlanExecutionTestbeds) // forward to /testbeds/info?plan_exec_id=:id
		planExec.GET("/:id/triage", h.GetTriage)
		planExec.PUT("/:id/triage", h.UpdateTriage)
		planExec.GET("/:id/resource-duration", h.GetResourceDuration)

		planExec.PUT("/:id/testbeds/:name/info/status", h.UpdateTestBedInfoStatus, mw.InternalAPI)
		planExec.GET("/:id/testbeds/:name", h.GetTestBed)
		planExec.PUT("/:id/testbeds/:name/renew", h.RenewTestBed)
		planExec.PUT("/:id/testbeds/:name/release", h.ReleaseTestBed)
		planExec.PUT("/:id/testbeds/:name/restore-monitor", h.RestoreMonitor)

		caseExec := v1.Group("/case-executions", mwAuth)
		caseExec.GET("", h.ListCaseExecutions)
		caseExec.GET("/:id", h.GetCaseExecution)
		caseExec.POST("", h.CreateCaseExecution, mw.InternalAPI)
		caseExec.PUT("/:id", h.UpdateCaseExecution, mw.InternalAPI)
		caseExec.PUT("/:id/note", h.UpdateCaseExecutionNote)
		caseExec.PUT("/:id/reason", h.UpdateCaseExecutionReason)
		caseExec.POST("/triages", h.CreateCaseTriage)
		caseExec.PUT("/:id/triage", h.UpdateCaseTriage)
		caseExec.GET("/:id/triage", h.GetCaseTriage)
		caseExec.GET("/:id/triage/suggest", h.SuggestCaseTriage)
		caseExec.PUT("/:id/triage/fork", h.ForkCaseTriage)

		testbeds := v1.Group("/testbeds", mwAuth)
		testbeds.GET("/info", h.ListTestBedInfos)
		testbeds.POST("/info", h.CreateTestBedInfo, mw.InternalAPI)

		config := v1.Group("/config", mwAuth)
		config.GET("", h.GetRemoteConfigs)

		dailyCI := v1.Group("/dailyci", mwAuth)
		dailyCI.GET("", h.GetDailyCIReport)
		dailyCI.POST("/trigger", h.TriggerDailyTestsByDailyCI)
		dailyCI.GET("/trigger/:id", h.GetTriggerStatusForDailyCI)

		summary := v1.Group("/summary", mwAuth)
		summary.GET("", h.GetSummaryByDate)
		summary.GET("/release/:id", h.GetReleaseSummary)

		resourcePool := v1.Group("/resourcepool", mwAuth)
		resourcePool.GET("", h.ListResourcePools)
		resourcePool.POST("", h.CreateResourcePool)
		resourcePool.PUT("", h.UpdateResourcePool)
		resourcePool.PUT("/status", h.UpdateResourcePoolsStatus)
		resourcePool.DELETE("", h.DeleteResourcePools)

		resourceCohort := v1.Group("/resource-cohorts", mwAuth)
		resourceCohort.GET("", h.ListResourceCohorts)
		resourceCohort.GET("/:name", h.GetResourceCohort)
		resourceCohort.POST("", h.CreateResourceCohort)
		resourceCohort.DELETE("/:name", h.DeleteResourceCohort)
		resourceCohort.POST("/:name/resourcepools", h.AddPool2ResourceCohort)
		resourceCohort.DELETE("/:name/resourcepools/:respoolName", h.RemovePoolFromResourceCohort)

		rms := v1.Group("/rms", mwAuth)
		rms.GET("", h.ListRMSs)
		rms.POST("", h.CreateRMS, mw.InternalAPI)
		rms.PUT("", h.UpdateRMS, mw.InternalAPI)
		rms.GET("/:name", h.GetRMS, mw.InternalAPI)

		setupRun := v1.Group("/testbedsetups", mw.SupportDryRun)
		setupRun.GET("", h.ListTestbedSetup)
		setupRun.GET("/schedule-graph", h.GenerateScheduleGraph)
		setupRun.POST("", h.CreateTestbedSetup, mwAuth)
		setupRun.GET("/:id", h.GetTestbedSetup)
		setupRun.PUT("/:id/status/schedule", h.UpdateTestbedSetupSchedule, mwAuth)
		setupRun.PUT("/:id/status/run", h.UpdateTestbedSetupReport, mwAuth)
		setupRun.PUT("/:id/status/report", h.UpdateTestbedSetupRun, mwAuth)
		// setupRun.PATCH("", h.BatchCreateTestbedSetup)

		svcTaskHandler := SvcTaskHandler{svc: svc.Store.SvcTask, decoder: decoder}
		svcTask := v1.Group("/svctasks")
		svcTask.GET("", svcTaskHandler.List)
		svcTask.GET("/name/:name", svcTaskHandler.GetByName)
		svcTask.GET("/:id", svcTaskHandler.Get)
		svcTask.PUT("/:id", svcTaskHandler.Update)
		svcTask.POST("", svcTaskHandler.Create)

		svcTaskRunHandler := SvcTaskRunHandler{svc: svc.Store.SvcTaskRun, decoder: decoder}
		svcTaskRun := v1.Group("/svctaskruns")
		svcTaskRun.GET("", svcTaskRunHandler.List)
		svcTaskRun.GET("/:id", svcTaskRunHandler.Get)
		svcTaskRun.POST("", svcTaskRunHandler.Create)
		svcTaskRun.PUT("/:id/status", svcTaskRunHandler.UpdateStatus)

		reportHandler := ReportHandler{svc.Store.TestReport}
		planReport := v1.Group("/reports/plan")
		planReport.GET("/:id", reportHandler.GetPlanReport)
	}

	return h
}

/*
	Format:
	```
	{
		"errors": {
			"code": 200001,
			"message": "service error"
		}
	}
	```
*/
func HTTPErrorHandler(err error, c echo.Context) {
	if c.Response().Committed {
		return
	}

	// make it work before custom context middleware
	var ctx utils.Context
	if c_, ok := c.(utils.Context); ok {
		ctx = c_
	} else {
		ctx = utils.NewContext(c).(utils.Context)
	}

	// status code and body
	var status int
	body := HTTPError{
		Errors: &serror.Error{},
	}
	switch e := err.(type) {
	case *serror.Error:
		status = e.HTTPStatus
		body.Errors = e
	case *echo.HTTPError:
		status = e.Code
		body.Errors.Code = e.Code
		msg := "unknown"
		if m, ok := e.Message.(string); ok {
			msg = m
		}
		body.Errors.Message = msg
	default:
		status = http.StatusInternalServerError
		body.Errors.Code = http.StatusInternalServerError
		body.Errors.Message = err.Error()
	}

	// Send response
	if c.Request().Method == http.MethodHead {
		err = ctx.NoContent(status)
	} else {
		err = ctx.JSON(status, body)
	}
	if err != nil {
		log.Log.Errorf(err, "handle error failed")
	}
}

type HTTPError struct {
	Errors *serror.Error `json:"errors"`
}
