package server

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	guuid "github.com/google/uuid"
	"github.com/prometheus/client_golang/prometheus/promhttp"

	"edge-infra.dev/pkg/f8n/ipranger"
	"edge-infra.dev/pkg/lib/logging"
)

type Server struct {
	Router *gin.Engine
	cfg    Config
	r      *ipranger.IPRanger
}

type Option func(*Config) error

type Config struct {
	port     int
	listener net.Listener
}

func toConfig(options []Option) (Config, error) {
	cfg := Config{
		port: 8080,
	}
	var errs []string
	for _, option := range options {
		err := option(&cfg)
		if err != nil {
			errs = append(errs, err.Error())
		}
	}
	if len(errs) > 0 {
		return Config{}, fmt.Errorf("error(s) building server config: %v", strings.Join(errs, "; "))
	}
	return cfg, nil
}

type NetcfgQuery struct {
	Region  string `form:"region" binding:"required"`
	Project string `form:"project" binding:"required"`
	Cluster string `form:"cluster" binding:"required"`
}

type NetcfgReq struct{}

type NetcfgResp struct {
	Network    string `json:"network"`
	Subnetwork string `json:"subnetwork"`
	Netmask    string `json:"netmask"`
}

func toNetcfgResp(subnet ipranger.Subnet) NetcfgResp {
	return NetcfgResp{
		Network:    subnet.Network,
		Subnetwork: subnet.Ref,
		Netmask:    fmt.Sprintf("/%d", ipranger.Netmask),
	}
}

type ErrResp struct {
	Message string `json:"message"`
}

// HealthzEndpoints contains the Server type's http health endpoints for k8s that returns 200 OK.
var HealthzEndpoints = []string{"/healthz", "/ipranger/healthz"}

// MetricsEndpoints contains the Server type's http endpoints for time series metrics (prometheus).
var MetricsEndpoints = []string{"/metrics", "/ipranger/metrics"}

const NetcfgEndpoint = "/api/v1/netcfg"

func NewServer(options ...Option) (*Server, error) {
	ctx := context.Background()
	cfg, err := toConfig(options)
	if err != nil {
		return nil, err
	}

	ranger, err := ipranger.New(ctx)
	if err != nil {
		return nil, err
	}

	s := &Server{
		Router: gin.New(),
		cfg:    cfg,
		r:      ranger,
	}

	// http req/res logging
	s.Router.Use(LoggerWithOperationID(logging.NewLogger()))

	// healthz and metrics builtins
	for _, hzep := range HealthzEndpoints {
		s.Router.GET(hzep, s.HealthHandlerFunction)
	}
	metricsHandlerFunc := gin.WrapH(promhttp.Handler())
	for _, mep := range MetricsEndpoints {
		s.Router.GET(mep, metricsHandlerFunc)
	}

	s.Router.GET(NetcfgEndpoint, s.HandleGetV1Netcfg)

	return s, nil
}

// Run runs the Gin server on the configured port or listener.
func (s *Server) Run() error {
	if s.cfg.listener != nil {
		return s.Router.RunListener(s.cfg.listener)
	}
	addr := fmt.Sprintf(":%d", s.cfg.port)
	return s.Router.Run(addr)
}

// HealthHandlerFunction returns status `200 OK`.
func (s *Server) HealthHandlerFunction(c *gin.Context) {
	c.JSON(http.StatusOK, "UP")
}

func (s *Server) HandleGetV1Netcfg(c *gin.Context) {
	log := getLogger(c)
	var q NetcfgQuery
	qerr := c.ShouldBindQuery(&q)
	if qerr != nil {
		c.JSON(http.StatusBadRequest, ErrResp{
			Message: "invalid query string",
		})
		return
	}
	region := regionalLocation(q.Region)
	subnet, err := s.r.FindOrCreateSubnet(q.Project, region)
	if err != nil {
		log.Error(err, "error finding or creating new subnet", "region", q.Region)
		c.JSON(http.StatusInternalServerError, ErrResp{
			Message: "failed to get or create GCP subnet",
		})
		return
	}

	c.JSON(http.StatusOK, toNetcfgResp(subnet))
}

func LoggerWithOperationID(log *logging.EdgeLogger) func(*gin.Context) {
	return func(c *gin.Context) {
		// initialize contextual logger
		opID := guuid.New().String()
		log := log.WithValues(logging.OperationKeysAndValues(opID)...).
			WithValues("method", c.Request.Method).
			WithValues("url", c.Request.URL)
		setLogger(c, log)

		// http incoming request
		log.Info("http request", "url", c.Request.URL)

		// process handlers
		t := time.Now()
		c.Next()
		delta := time.Since(t)

		// http outgoing response
		log.Info(
			"http response",
			"status", c.Writer.Status(),
			"elapsed_ms", delta.Milliseconds(),
			"bytes", c.Writer.Size(),
		)
	}
}

const loggerCtxKey = "logger"

func getLogger(c *gin.Context) *logging.EdgeLogger {
	log, ok := c.Get(loggerCtxKey)
	if ok {
		return log.(*logging.EdgeLogger)
	}
	return logging.NewLogger()
}

func setLogger(c *gin.Context, l *logging.EdgeLogger) {
	c.Set(loggerCtxKey, l)
}

// ensures the location is regional, compared to zonal.
// e.g., us-east1 is regional, us-east1-a is zonal. subnetworks
// are always regional, but a GKE cluster location may be given
// as zonal.
func regionalLocation(loc string) string {
	locsplit := strings.Split(loc, "-")
	return strings.Join(locsplit[:2], "-")
}