package middleware

import (
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
)

const (
	writeAdmissionMaxInFlightEnv  = "NEXUSCRM_WRITE_ADMISSION_MAX_IN_FLIGHT"
	writeAdmissionWaitMillisEnv   = "NEXUSCRM_WRITE_ADMISSION_WAIT_MS"
	writeAdmissionRetrySecondsEnv = "NEXUSCRM_WRITE_ADMISSION_RETRY_AFTER_SECONDS"

	defaultWriteAdmissionRetrySeconds = 2
)

var (
	writeAdmissionInFlight = promauto.NewGauge(
		prometheus.GaugeOpts{
			Name: "nexuscrm_write_admission_in_flight",
			Help: "Number of data API write requests currently admitted through the foreground write gate.",
		},
	)
	writeAdmissionRejected = promauto.NewCounter(
		prometheus.CounterOpts{
			Name: "nexuscrm_write_admission_rejected_total",
			Help: "Total number of data API write requests rejected because the foreground write gate was full.",
		},
	)
)

type WriteAdmissionConfig struct {
	MaxInFlight       int
	WaitTimeout       time.Duration
	RetryAfterSeconds int
}

func WriteAdmissionConfigFromEnv(getenv func(string) string) WriteAdmissionConfig {
	if getenv == nil {
		getenv = os.Getenv
	}
	return WriteAdmissionConfig{
		MaxInFlight:       writeAdmissionPositiveIntEnv(getenv, writeAdmissionMaxInFlightEnv, 0),
		WaitTimeout:       time.Duration(writeAdmissionNonNegativeIntEnv(getenv, writeAdmissionWaitMillisEnv, 0)) * time.Millisecond,
		RetryAfterSeconds: writeAdmissionPositiveIntEnv(getenv, writeAdmissionRetrySecondsEnv, defaultWriteAdmissionRetrySeconds),
	}
}

func WriteAdmissionFromEnv() gin.HandlerFunc {
	return NewWriteAdmission(WriteAdmissionConfigFromEnv(os.Getenv))
}

func NewWriteAdmission(cfg WriteAdmissionConfig) gin.HandlerFunc {
	if cfg.MaxInFlight <= 0 {
		return nil
	}
	if cfg.RetryAfterSeconds <= 0 {
		cfg.RetryAfterSeconds = defaultWriteAdmissionRetrySeconds
	}
	sem := make(chan struct{}, cfg.MaxInFlight)
	return func(c *gin.Context) {
		if !writeAdmissionApplies(c) {
			c.Next()
			return
		}
		if !acquireWriteAdmission(c, sem, cfg.WaitTimeout) {
			writeAdmissionRejected.Inc()
			c.Header("Retry-After", strconv.Itoa(cfg.RetryAfterSeconds))
			c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{
				"error":   "write_admission_full",
				"message": "The write path is busy; retry shortly.",
			})
			return
		}
		writeAdmissionInFlight.Inc()
		defer func() {
			<-sem
			writeAdmissionInFlight.Dec()
		}()
		c.Next()
	}
}

func acquireWriteAdmission(c *gin.Context, sem chan struct{}, wait time.Duration) bool {
	select {
	case sem <- struct{}{}:
		return true
	default:
	}
	if wait <= 0 {
		return false
	}
	timer := time.NewTimer(wait)
	defer timer.Stop()
	select {
	case sem <- struct{}{}:
		return true
	case <-timer.C:
		return false
	case <-c.Request.Context().Done():
		return false
	}
}

func writeAdmissionApplies(c *gin.Context) bool {
	method := ""
	if c != nil && c.Request != nil {
		method = c.Request.Method
	}
	switch strings.ToUpper(strings.TrimSpace(method)) {
	case http.MethodPut, http.MethodPatch, http.MethodDelete:
		return true
	case http.MethodPost:
		return !writeAdmissionReadPostRoute(c)
	default:
		return false
	}
}

func writeAdmissionReadPostRoute(c *gin.Context) bool {
	path := ""
	if c != nil {
		path = c.FullPath()
		if path == "" && c.Request != nil && c.Request.URL != nil {
			path = c.Request.URL.Path
		}
	}
	path = strings.ToLower(strings.TrimSpace(path))
	return strings.HasSuffix(path, "/query") || strings.HasSuffix(path, "/queryall")
}

func writeAdmissionPositiveIntEnv(getenv func(string) string, key string, fallback int) int {
	n := writeAdmissionNonNegativeIntEnv(getenv, key, fallback)
	if n <= 0 {
		return fallback
	}
	return n
}

func writeAdmissionNonNegativeIntEnv(getenv func(string) string, key string, fallback int) int {
	raw := strings.TrimSpace(getenv(key))
	if raw == "" {
		return fallback
	}
	n, err := strconv.Atoi(raw)
	if err != nil || n < 0 {
		return fallback
	}
	return n
}
