package middleware

import (
	"crypto/rand"
	"encoding/hex"
	"net/http"
	"net/url"
	"strings"

	"github.com/gin-gonic/gin"
	origincfg "github.com/nexuscrm/backend/internal/interfaces/origin"
	"github.com/rs/zerolog/log"
)

const (
	csrfCookieName = "_csrf"
	csrfHeaderName = "X-CSRF-Token"
	csrfTokenBytes = 32
)

// csrfExemptPaths lists paths that are exempt from CSRF validation. These
// endpoints use their own authentication mechanism (e.g. one-time tokens)
// and are called by non-browser clients that cannot provide Origin headers.
var csrfExemptPaths = map[string]struct{}{
	"/services/oauth2/token":        {},
	"/services/oauth2/google/token": {},
}

// CSRF returns a middleware that rejects state-changing requests (POST, PUT,
// PATCH, DELETE) whose Origin (or Referer) header does not match any of the
// configured allowed origins. This prevents cross-site request forgery where a
// malicious page triggers an authenticated request against our API.
//
// When neither Origin nor Referer is present the middleware falls back to the
// double-submit cookie pattern: the request must carry an X-CSRF-Token header
// whose value matches the _csrf cookie (set via SetCSRFCookie on login).
// Safe methods (GET, HEAD, OPTIONS) are always exempt.
func CSRF() gin.HandlerFunc {
	cfg := origincfg.LoadConfigFromEnv()

	return func(c *gin.Context) {
		method := c.Request.Method
		if method == http.MethodGet || method == http.MethodHead || method == http.MethodOptions {
			c.Next()
			return
		}

		if _, exempt := csrfExemptPaths[c.Request.URL.Path]; exempt {
			c.Next()
			return
		}

		// Bearer token authentication is inherently CSRF-safe: an attacker
		// cannot force a victim's browser to include an Authorization header
		// in a cross-origin request. Only cookie-based sessions need CSRF
		// protection. (OWASP CSRF Prevention Cheat Sheet §Token-Based Mitigation)
		if auth := c.GetHeader("Authorization"); strings.HasPrefix(auth, "Bearer ") {
			c.Next()
			return
		}

		if secFetchSite := c.GetHeader("Sec-Fetch-Site"); secFetchSite == "cross-site" {
			rejectCSRF(c, "Sec-Fetch-Site=cross-site", method)
			return
		}

		origin := c.GetHeader("Origin")
		if origin != "" {
			if cfg.IsAllowedOrigin(origin) {
				c.Next()
				return
			}
			rejectCSRF(c, origin, method)
			return
		}

		referer := c.GetHeader("Referer")
		if referer != "" {
			if u, err := url.Parse(referer); err == nil && u.Host != "" {
				refOrigin := u.Scheme + "://" + u.Host
				if cfg.IsAllowedOrigin(refOrigin) {
					c.Next()
					return
				}
				rejectCSRF(c, refOrigin, method)
				return
			}
		}

		// Neither Origin nor Referer present — require double-submit token.
		cookie, cookieErr := c.Cookie(csrfCookieName)
		header := c.GetHeader(csrfHeaderName)
		if cookieErr != nil || cookie == "" || header == "" || cookie != header {
			rejectCSRF(c, "missing-csrf-token", method)
			return
		}

		c.Next()
	}
}

// SetCSRFCookie sets (or refreshes) the double-submit CSRF cookie. Call this
// after a successful login so that subsequent browser requests can include the
// token value as the X-CSRF-Token header.
func SetCSRFCookie(c *gin.Context) {
	token := generateCSRFToken()
	secure := !isLocalhost(c.Request.Host)
	c.SetSameSite(http.SameSiteLaxMode)
	c.SetCookie(csrfCookieName, token, 86400*30, "/", "", secure, false)
}

func generateCSRFToken() string {
	b := make([]byte, csrfTokenBytes)
	if _, err := rand.Read(b); err != nil {
		panic("CSRF: crypto/rand.Read failed: " + err.Error())
	}
	return hex.EncodeToString(b)
}

func isLocalhost(host string) bool {
	h := strings.Split(host, ":")[0]
	return h == "localhost" || h == "127.0.0.1" || h == "::1"
}

func rejectCSRF(c *gin.Context, origin, method string) {
	log.Warn().
		Str("origin", origin).
		Str("method", method).
		Str("path", c.Request.URL.Path).
		Msg("CSRF: rejected cross-origin state-changing request")
	c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "cross-origin request blocked"})
}
