package serve

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"github.com/elnormous/contenttype"
	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/render"
	"github.com/go-playground/locales/en"
	ut "github.com/go-playground/universal-translator"
	"github.com/go-playground/validator/v10"
	en_translations "github.com/go-playground/validator/v10/translations/en"
	"github.com/goccy/go-yaml"
	"github.com/gookit/validate"
	manInfo "github.com/openziti/edge-api/rest_management_api_client/informational"
	"github.com/openziti/edge-api/rest_model"
	"github.com/openziti/edge-api/rest_util"
	"github.com/openziti/ziti/ziti/cmd/api"
	"github.com/openziti/ziti/ziti/cmd/ascode/exporter"
	"github.com/openziti/ziti/ziti/cmd/ascode/importer"
	"github.com/openziti/ziti/ziti/cmd/common"
	"github.com/openziti/ziti/ziti/cmd/edge"
	"github.com/patrickmn/go-cache"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/sagikazarmark/slog-shim"
	sloggin "github.com/samber/slog-gin"
	"github.com/spf13/viper"
	"net/http"
)

const (
	MediaTypeJson = "application/json"
	MediaTypeYaml = "application/x-yaml"
	DefaultHost   = "0.0.0.0"
	DefaultPort   = "8080"
)

type Serve struct {
	client    *ZitiClient
	validator *validator.Validate
	debug     bool
}

func (s Serve) Run() {

	s.client = &ZitiClient{
		cache: cache.New(5*time.Minute, 10*time.Minute),
	}

	// Set environment variable prefix (optional)
	viper.SetEnvPrefix("ZT")

	// Automatically read environment variables
	viper.AutomaticEnv()

	// Read environment variable (e.g., MYAPP_PORT)
	port := viper.GetString("PORT")
	if port == "" {
		port = DefaultPort
	}
	host := viper.GetString("HOST")
	if host == "" {
		host = DefaultHost
	}
	debug := viper.GetBool("DEBUG")

	// parse environment variable to get log level
	level := parseLoggingLevel(viper.GetString("LOG_LEVEL"))

	var handler slog.Handler
	// default logging for the application
	if parseLoggingHandler(viper.GetString("LOG_HANDLER")) == "json" {
		handler = slog.NewJSONHandler(gin.DefaultWriter, &slog.HandlerOptions{Level: level})
	} else {
		handler = slog.NewTextHandler(gin.DefaultWriter, &slog.HandlerOptions{Level: level})
	}
	slog.SetDefault(slog.New(handler))

	config := sloggin.Config{
		WithSpanID:  true,
		WithTraceID: true,
		Filters: []sloggin.Filter{
			sloggin.IgnorePath("/health"),
		},
	}

	if !debug {
		gin.SetMode(gin.ReleaseMode)
	}

	s.validator = validator.New(validator.WithRequiredStructEnabled())
	en := en.New()
	uni := ut.New(en, en)
	trans, _ := uni.GetTranslator("en")
	err := en_translations.RegisterDefaultTranslations(s.validator, trans)
	if err != nil {
		slog.With(err).Error("Failed to initialize validator")
		return
	}

	engine := gin.New()

	engine.Use(
		gin.Recovery(),
		sloggin.NewWithConfig(
			// logger for gin (request/response)
			slog.New(handler),
			config,
		),
	)

	engine.GET("/health", func(c *gin.Context) {
		c.String(http.StatusOK, "UP")
	})

	engine.POST("/export", s.exportEntities)
	engine.POST("/import", s.importEntities)

	srv := &http.Server{
		Addr:    host + ":" + port,
		Handler: engine.Handler(),
	}

	go func() {
		slog.Debug("Starting server...")
		if err := srv.ListenAndServe(); err != nil && !errors.Is(http.ErrServerClosed, err) {
			slog.With(err).Error("server error")
		}
	}()

	// Wait for interrupt signal to gracefully shutdown the server with a timeout of 3 seconds.
	quit := make(chan os.Signal, 1)
	// kill (no param) default send syscall.SIGTERM
	// kill -2 is syscall.SIGINT
	// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	slog.Info("Shutting down server...")

	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	if err := srv.Shutdown(ctx); err != nil {
		slog.With(err).Error("Server Shutdown")
		return
	}
	// catching ctx.Done(). timeout of 5 seconds.
	select {
	case <-ctx.Done():
		slog.Info("timeout of 3 seconds.")
	}
	slog.Info("Server exited")
}

type ExportRequest struct {
	Entities      []string `form:"entities" json:"entities" yaml:"entities"`
	ControllerUrl string   `form:"controllerUrl" json:"controllerUrl" yaml:"controllerUrl" validate:"required|full_url|starts_with:https://" message:"full_url:must be a valid url|starts_with:only supports https"`
	UserName      string   `form:"username" json:"username" yaml:"username" validate:"required_without:token" message:"required_without:must be provided unless token is present"`
	Password      string   `form:"password" json:"password" yaml:"password" validate:"required_with:username" message:"required_with:must be provided when username is present"`
	Token         string   `form:"token" json:"token" yaml:"token" validate:"required_without_all:username,password" message:"required_without_all:must be provided if username and password is not provided"`
}

func (s Serve) exportEntities(c *gin.Context) {

	acceptError := s.accepted(c)
	if acceptError != nil {
		c.AbortWithStatus(http.StatusUnsupportedMediaType)
		return
	}

	var request ExportRequest
	if err := c.ShouldBind(&request); err != nil {
		s.negotiate(c, errorFormat(http.StatusBadRequest, err.Error()))
		return
	}
	v := validate.New(request)
	if !v.Validate() {
		s.negotiate(c, errorFormatValidationErrors(http.StatusBadRequest, v.Errors))
		return
	}

	if request.Entities == nil {
		request.Entities = []string{"all"}
	}

	if request.Token == "" {
		token, err := s.client.getToken(request.ControllerUrl, request.UserName, request.Password)
		if err != nil {
			s.negotiate(c, errorFormat(http.StatusBadRequest, err.Error()))
			return
		}
		request.Token = token
	} else {
		slog.Debug("using token from request")
	}
	outBuf := bufio.NewWriter(&bytes.Buffer{})
	errBuf := &bytes.Buffer{}

	e := exporter.Exporter{
		Out: *outBuf,
		Err: errBuf,
	}

	loginOpts := edge.LoginOptions{
		Options: api.Options{
			CommonOptions: common.CommonOptions{
				Out:     os.Stdout,
				Err:     os.Stderr,
				Verbose: s.debug,
			},
		},
		ControllerUrl: request.ControllerUrl,
		Username:      request.UserName,
		Password:      request.Password,
		Token:         request.Token,
		ReadOnly:      true,
		Yes:           true,
		IgnoreConfig:  true,
	}
	client, err := loginOpts.NewMgmtClient()
	if err != nil {
		slog.With(err).Error("unable to create ziti management client")
		s.negotiate(c, errorFormat(http.StatusBadRequest, err.Error()))
		return
	}
	_, err = client.CurrentAPISession.GetCurrentAPISession(nil, nil)
	if err != nil {
		if payloadErr, ok := err.(rest_util.ApiErrorPayload); ok {
			s.negotiate(c, errorFormat(http.StatusInternalServerError, payloadErr.GetPayload().Error.Message))
			return
		}
		s.negotiate(c, errorFormat(http.StatusInternalServerError, err.Error()))
		return
	}
	e.Client = client

	result, err := e.Execute(request.Entities)
	if err != nil {
		slog.With(err).Error("unable to execute export command")
	}

	var versionInfo = &rest_model.Version{}
	versionParams := manInfo.NewListVersionParams()
	listVersion, acceptError := client.Informational.ListVersion(versionParams)
	if acceptError != nil {
		slog.With(acceptError.Error()).Error("unable to get controller version")
		versionInfo = &rest_model.Version{}
	} else {
		versionInfo = listVersion.Payload.Data
	}
	if versionInfo.Version[0] == 'v' {
		result["version"] = versionInfo.Version[1:]
	} else {
		result["version"] = versionInfo.Version
	}
	s.negotiate(c, &NegotiateFormat{
		Status: http.StatusOK,
		JSONData: func() []byte {
			out, err := json.Marshal(result)
			if err != nil {
				slog.With(err).Error("unable to marshal response")
			}
			return out
		},
		YAMLData: func() []byte {
			out, err := yaml.Marshal(result)
			if err != nil {
				slog.With(err).Error("unable to marshal response")
			}
			return out
		},
	})
}

type ImportRequest struct {
	Entities      []string                 `form:"entities" json:"entities"`
	Data          map[string][]interface{} `form:"data" json:"data" binding:"required"`
	ControllerUrl string                   `form:"controllerUrl" json:"controllerUrl" binding:"required"`
	UserName      string                   `form:"username" json:"username"`
	Password      string                   `form:"password" json:"password"`
	Token         string                   `form:"token" json:"token" yaml:"token"`
}

func (s Serve) importEntities(c *gin.Context) {

	acceptError := s.accepted(c)
	if acceptError != nil {
		c.AbortWithStatus(http.StatusUnsupportedMediaType)
		return
	}

	var request ImportRequest
	if err := c.ShouldBind(&request); err != nil {
		if request.UserName == "" && request.Token == "" {
			s.negotiate(c, errorFormat(http.StatusBadRequest, "either username + password or token is required"))
			return
		}
		if request.UserName != "" && request.Password == "" {
			s.negotiate(c, errorFormat(http.StatusBadRequest, "password is required when username is provided"))
			return
		}
		s.negotiate(c, errorFormat(http.StatusBadRequest, err.Error()))
		return
	}

	if request.Entities == nil {
		request.Entities = []string{"all"}
	}
	if request.ControllerUrl[0:7] == "http://" {
		s.negotiate(c, errorFormat(http.StatusBadRequest, "only https is supported for controller url"))
	}
	if request.ControllerUrl[0:8] == "https://" {
		request.ControllerUrl = request.ControllerUrl[8:]
	}
	if request.Token == "" {
		token, err := s.client.getToken(request.ControllerUrl, request.UserName, request.Password)
		if err != nil {
			s.negotiate(c, errorFormat(http.StatusBadRequest, err.Error()))
			return
		}
		request.Token = token
	}
	outBuf := &bytes.Buffer{}
	errBuf := &bytes.Buffer{}

	i := importer.Importer{
		Out:  outBuf,
		Err:  errBuf,
		Data: request.Data,
	}

	loginOpts := edge.LoginOptions{
		Options: api.Options{
			CommonOptions: common.CommonOptions{
				Out:     os.Stdout,
				Err:     os.Stderr,
				Verbose: s.debug,
			},
		},
		ControllerUrl: request.ControllerUrl,
		Username:      request.UserName,
		Password:      request.Password,
		Token:         request.Token,
		Yes:           true,
		IgnoreConfig:  true,
	}
	client, err := loginOpts.NewMgmtClient()
	if err != nil {
		slog.With(err).Error("unable to create ziti management client")
		s.negotiate(c, errorFormat(http.StatusBadRequest, err.Error()))
		return
	}
	_, err = client.CurrentAPISession.GetCurrentAPISession(nil, nil)
	if err != nil {
		if payloadErr, ok := err.(rest_util.ApiErrorPayload); ok {
			s.negotiate(c, errorFormat(http.StatusInternalServerError, payloadErr.GetPayload().Error.Message))
			return
		}
		s.negotiate(c, errorFormat(http.StatusInternalServerError, err.Error()))
		return
	}
	i.Client = client

	err = i.Execute(request.Entities)
	if err != nil {
		s.negotiate(c, errorFormat(http.StatusInternalServerError, err.Error()))
		return
	}

	s.negotiate(c, &NegotiateFormat{
		Status: http.StatusOK,
		JSONData: func() []byte {
			out := outBuf.Bytes()
			return out
		},
		YAMLData: func() []byte {
			out := outBuf.Bytes()
			return out
		},
	})
}

func (s Serve) accepted(c *gin.Context) error {
	accepted, _, acceptError := contenttype.GetAcceptableMediaType(c.Request, []contenttype.MediaType{
		contenttype.NewMediaType(MediaTypeJson),
		contenttype.NewMediaType(MediaTypeYaml),
	})
	if acceptError != nil {
		return acceptError
	}
	c.Set("accepted", accepted.MIME())
	return nil
}

func (s Serve) negotiate(c *gin.Context, config *NegotiateFormat) {
	contentType := c.GetString("accepted")
	if config.ForceContentType != "" {
		contentType = config.ForceContentType
	}

	var data []byte
	switch contentType {
	case MediaTypeJson:
		data = config.JSONData()
		break
	case MediaTypeYaml:
		data = config.YAMLData()
		break
	default:
		c.String(http.StatusNotAcceptable, "Unable to negotiate content type")
	}

	c.Render(config.Status, render.Data{
		ContentType: contentType,
		Data:        data,
	})
}

type NegotiateFormat struct {
	Status           int
	ForceContentType string
	JSONData         func() []byte
	YAMLData         func() []byte
}

func errorFormatValidationErrors(status int, errors validate.Errors) *NegotiateFormat {
	messages := map[string][]string{}
	for key, _ := range errors {
		fieldErrors := messages[key]
		if fieldErrors == nil {
			fieldErrors = []string{}
		}
		one := errors.FieldOne(key)
		messages[key] = append(fieldErrors, one)
	}
	return &NegotiateFormat{
		Status: status,
		JSONData: func() []byte {
			out, _ := json.Marshal(map[string]any{"err": messages})
			return out
		},
		YAMLData: func() []byte {
			out, _ := yaml.Marshal(map[string]any{"err": messages})
			return out
		},
	}
}

func errorFormat(status int, msg string) *NegotiateFormat {
	return &NegotiateFormat{
		Status: status,
		JSONData: func() []byte {
			out, _ := json.Marshal(map[string]string{"err": msg})
			return out
		},
		YAMLData: func() []byte {
			out, _ := yaml.Marshal(map[string]string{"err": msg})
			return out
		},
	}
}

func parseLoggingLevel(logValue string) slog.Level {
	var level slog.Level
	if logValue == "" {
		level = slog.LevelWarn
		slog.Debug("no log level specified, defaulting to WARN")
	} else {
		var err = level.UnmarshalText([]byte(logValue))
		if err != nil {
			level = slog.LevelWarn
			slog.With(err).Warn("unable to parse log level, defaulting to WARN")
		}
	}
	return level
}

func parseLoggingHandler(handlerValue string) string {
	if handlerValue == "" || handlerValue == "json" {
		slog.Debug("no log handler specified, defaulting to JSON")
		return "json"
	} else if handlerValue == "text" || handlerValue == "console" {
		slog.Debug("no log handler specified, defaulting to JSON")
		return "text"
	}
	panic("unknown log handler: " + handlerValue)
}

func setupDefaultLogging(r *gin.Engine, level slog.Level) {
	// log as json
	logger := slog.NewJSONHandler(gin.DefaultWriter, &slog.HandlerOptions{Level: level})

	// default logging for the application
	slog.SetDefault(slog.New(logger))

	// logger for gin (request/response)
	r.Use(sloggin.New(slog.New(logger)))
}
