package region

import (
	"bytes"
	"cmp"
	"fmt"
	"math"
	"net/http"
	"slices"
	"strconv"
	"strings"

	"braces.dev/errtrace"
	"github.com/gin-gonic/gin"
	"github.com/ipipdotnet/ipdb-go"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/encoding/protojson"

	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	openapiserverlessv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/serverless/v1beta1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/domain/adapter/ipip"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/pkg/apimiddleware/response"
)

var (
	// only used for ui api
	getServerlessRegionsRequest = "get:/api/v2023/serverless/v1beta1/regions"

	regionGeoLocation = map[string]*ipdb.CityInfo{
		"regions/aws-us-west-2":           {Longitude: "-122.7011", Latitude: "45.5122"},
		"regions/aws-us-east-1":           {Longitude: "-77.4728", Latitude: "37.7664"},
		"regions/aws-eu-central-1":        {Longitude: "8.6821", Latitude: "50.1109"},
		"regions/aws-ap-southeast-1":      {Longitude: "103.852", Latitude: "1.2975"},
		"regions/aws-ap-northeast-1":      {Longitude: "139.6917", Latitude: "35.6895"},
		"regions/aws-ap-east-1":           {Longitude: "114.2723", Latitude: "22.2908"},
		"regions/aws-ap-south-1":          {Longitude: "72.8826", Latitude: "19.0728"},
		"regions/alicloud-ap-southeast-1": {Longitude: "103.852", Latitude: "1.2975"},
		"regions/alicloud-ap-northeast-1": {Longitude: "139.6917", Latitude: "35.6895"},
		"regions/alicloud-us-east-1":      {Longitude: "-77.4875", Latitude: "39.0437"},
		"regions/alicloud-ap-southeast-5": {Longitude: "106.8456", Latitude: "-6.2088"},
		"regions/alicloud-na-south-1":     {Longitude: "-99.1332", Latitude: "19.4326"},
		"regions/alicloud-cn-hongkong":    {Longitude: "114.2723", Latitude: "22.2908"},
	}

	// marshalOptions is a configurable JSON format marshaller.
	marshalOptions = protojson.MarshalOptions{
		// UseProtoNames:   true,
		EmitDefaultValues: true,
	}
	// unmarshalOptions is a configurable JSON format parser.
	unmarshalOptions = protojson.UnmarshalOptions{
		DiscardUnknown: true,
	}
)

// GeoLocationHandler is a gin middleware that sorts the response body of region by location
func GeoLocationHandler(ipipImpl ipip.Client) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		uri := strings.ToLower(fmt.Sprintf("%s:%s", ctx.Request.Method, ctx.Request.URL))

		ctx.Next()

		// get response body
		// only modify response body when status code is 2xx
		if ctx.Writer.Status() >= http.StatusMultipleChoices {
			return
		}
		if strings.HasPrefix(uri, getServerlessRegionsRequest) {
			err := sortRegionByLocation(ctx, ipipImpl)
			if err != nil {
				tilog.Error(
					ctx.Request.Context(),
					"sort region by location failed",
					zap.String("client_ip", ctx.ClientIP()),
					zap.Error(err),
				)
				return
			}
			tilog.Info(ctx.Request.Context(),
				"sort region by location success",
				zap.String("client_ip", ctx.ClientIP()))
		}
	}
}

// sortRegionByLocation
func sortRegionByLocation(ctx *gin.Context, ipipImpl ipip.Client) error {
	city, err := ipipImpl.IP2CityInfo(ctx.ClientIP())
	if err != nil || city.Longitude == "" || city.Latitude == "" {
		tilog.Info(ctx.Request.Context(), "sort region by location, ip2cityinfo failed, skip sort",
			zap.String("client_ip", ctx.ClientIP()),
			zap.Any("city", city), zap.Error(err))
		return nil
	}
	if w, ok := ctx.Writer.(*response.WriterWrapper); ok {
		var regions openapiserverlessv1beta1.ListRegionsResponse

		// keep resp is available
		resp := w.Body.Bytes()
		defer func() {
			w.Body = bytes.NewBuffer(resp)
		}()

		err := unmarshalOptions.Unmarshal(w.Body.Bytes(), &regions)
		if err != nil {
			return errtrace.Wrap(err)
		}

		tilog.Info(ctx.Request.Context(), "sort region by location, before sort",
			zap.String("client_ip", ctx.ClientIP()),
			zap.Any("regions", regions.Regions))
		// sort by location
		slices.SortFunc(regions.Regions, func(a, b *openapicommonv1beta1.Region) int {
			return cmp.Compare(
				calculateDistance(regionGeoLocation[a.Name], city),
				calculateDistance(regionGeoLocation[b.Name], city),
			)
		})
		tilog.Info(ctx.Request.Context(), "sort region by location, after sort",
			zap.String("client_ip", ctx.ClientIP()),
			zap.Any("regions", regions.Regions))

		r, err := marshalOptions.Marshal(&regions)
		if err != nil {
			return errtrace.Wrap(err)
		}
		resp = r
	}
	return nil
}

// unit: km
func calculateDistance(start, end *ipdb.CityInfo) int64 {
	if start == nil {
		// us-west-2 as default location
		start = &ipdb.CityInfo{Longitude: "-122.7011", Latitude: "45.5122"}
	}

	startLng, _ := strconv.ParseFloat(start.Longitude, 64)
	startLat, _ := strconv.ParseFloat(start.Latitude, 64)
	endLng, _ := strconv.ParseFloat(end.Longitude, 64)
	endLat, _ := strconv.ParseFloat(end.Latitude, 64)

	rad := math.Pi / 180.0
	startLat *= rad
	startLng *= rad
	endLat *= rad
	endLng *= rad
	theta := endLng - startLng
	dist := math.Acos(math.Sin(startLat)*math.Sin(endLat) + math.Cos(startLat)*math.Cos(endLat)*math.Cos(theta))
	return int64(dist * 6371000.0 /* radius */ / 1000)
}
