package auth

import (
	"bytes"
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"reflect"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/patrickmn/go-cache"
	"github.com/spf13/viper"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/transport/http/tigin"
	"go.uber.org/zap"
)

func AuthnOpsAK() gin.HandlerFunc {
	return func(c *gin.Context) {
		ctx := c.Request.Context()

		akMap := viper.GetStringMapString("ops-admin.ak-sk")
		if len(akMap) == 0 {
			tilog.Info(ctx, "not config ops ak")
			tigin.AbortWithError(c, errs.NewUnauthorized("unauthorized", nil))
			return
		}

		validator := GetAuthenticator(ctx)
		validated := validator.ValidateRequest(ctx, c.Request, akMap)
		if !validated {
			tigin.AbortWithError(c, errs.NewUnauthorized("unauthorized", nil))
			return
		}

		c.Next()
	}
}

// Authenticator holds the AK, SK, and a cache for used nonces
type Authenticator struct {
	NonceStore *cache.Cache // Using a TTL cache for nonce storage

}

// NewAuthenticator creates a new Authenticator
func NewServerAuthenticator() *Authenticator {
	// Initialize the cache with a default expiration time of 30 minutes, and cleanup interval of 6 Hour
	c := cache.New(30*time.Minute, 6*time.Hour)
	return &Authenticator{NonceStore: c}
}

// SignRequest signs the request with AK/SK, timestamp, and nonce
func (a *Authenticator) SignRequest(req *http.Request, ak, sk string) {
	timestamp := time.Now().UTC().Format(time.RFC3339)
	nonce := generateNonce()
	signature := a.generateSignature(req, timestamp, nonce, sk)

	req.Header.Set("X-AK", ak)
	req.Header.Set("X-Timestamp", timestamp)
	req.Header.Set("X-Nonce", nonce)
	req.Header.Set("X-Signature", signature)
}

// ValidateRequest validates the incoming request using AK/SK, timestamp, and nonce
func (a *Authenticator) ValidateRequest(ctx context.Context, req *http.Request, akMap map[string]string) bool {
	ak := req.Header.Get("X-AK")
	timestamp := req.Header.Get("X-Timestamp")
	nonce := req.Header.Get("X-Nonce")
	signature := req.Header.Get("X-Signature")

	val, ok := akMap[ak]
	if !ok {
		tilog.Info(ctx, "ak is not exist", zap.String("ak", ak))
		return false
	}
	// Check if the request is within 5 minutes of the current time
	requestTime, err := time.Parse(time.RFC3339, timestamp)
	if err != nil || time.Since(requestTime) > 2*time.Minute {
		tilog.Info(ctx, "timestamp is not valid, > 2 minutes", zap.String("timestamp", timestamp))
		return false
	}

	// Check if the nonce has been used
	if _, found := a.NonceStore.Get(nonce); found {
		return false
	}

	// Store the nonce
	a.NonceStore.Set(nonce, requestTime, cache.DefaultExpiration)

	// Generate the expected signature
	expectedSignature := a.generateSignature(req, timestamp, nonce, val)
	flag := hmac.Equal([]byte(signature), []byte(expectedSignature))
	tilog.Info(ctx, "hmac auth result="+fmt.Sprintf("%v, ak=%s", flag, ak))
	return flag
}

// generateSignature generates a signature using HMAC-SHA256
func (a *Authenticator) generateSignature(req *http.Request, timestamp, nonce, sk string) string {
	method := req.Method
	path := req.URL.Path

	var params string
	if method == http.MethodGet {
		params = getSortedParams(req.URL.Query())
	} else {
		bodyParams := getBodyParams(req)
		params = getSortedParams(bodyParams)
	}

	message := method + path + params + timestamp + nonce
	mac := hmac.New(sha256.New, []byte(sk))
	mac.Write([]byte(message))
	return hex.EncodeToString(mac.Sum(nil))
}

// getSortedParams returns a sorted query string or body parameters
func getSortedParams(values url.Values) string {
	var keys []string
	for key := range values {
		keys = append(keys, key)
	}
	sort.Strings(keys)

	var params []string
	for _, key := range keys {
		params = append(params, key+"="+values.Get(key))
	}
	return strings.Join(params, "&")
}

// getBodyParams extracts parameters from the request body and returns them as url.Values
func getBodyParams(req *http.Request) url.Values {
	var bodyParams map[string]interface{}
	body, err := io.ReadAll(req.Body)
	if err != nil {
		return nil
	}

	// Restore the request body to its original state
	req.Body = io.NopCloser(bytes.NewBuffer(body))

	if err := json.Unmarshal(body, &bodyParams); err != nil {
		return nil
	}

	values := url.Values{}
	for key, value := range bodyParams {
		values.Set(key, valueToString(value))
	}
	return values
}

// valueToString converts interface{} to string
func valueToString(v interface{}) string {
	val := reflect.ValueOf(v)
	switch val.Kind() {
	case reflect.String:
		return val.String()
	case reflect.Bool:
		return fmt.Sprintf("%t", val.Bool())
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return fmt.Sprintf("%d", val.Int())
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		return fmt.Sprintf("%d", val.Uint())
	case reflect.Float32, reflect.Float64:
		return fmt.Sprintf("%f", val.Float())
	case reflect.Slice, reflect.Array:
		var result []string
		for i := 0; i < val.Len(); i++ {
			result = append(result, valueToString(val.Index(i).Interface()))
		}
		return strings.Join(result, ",")
	case reflect.Map:
		var result []string
		for _, key := range val.MapKeys() {
			result = append(result, fmt.Sprintf("%s:%s", valueToString(key.Interface()), valueToString(val.MapIndex(key).Interface())))
		}
		return "{" + strings.Join(result, ",") + "}"
	default:
		jsonValue, err := json.Marshal(v)
		if err != nil {
			return ""
		}
		return string(jsonValue)
	}
}

// generateNonce generates a random nonce
func generateNonce() string {
	return time.Now().UTC().Format("20060102150405") // Simplified nonce generation
}

var authenticator *Authenticator
var authenticatorInitOnce sync.Once

// GetAuthenticator returns an instance of Authenticator,
func GetAuthenticator(ctx context.Context) *Authenticator {
	authenticatorInitOnce.Do(func() {

		authenticator = NewServerAuthenticator()

		tilog.Info(ctx, "authenticator init success")
	})
	return authenticator
}
