package middleware

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

	"github.com/gin-gonic/gin"
	origincfg "github.com/nexuscrm/backend/internal/host/adapters/apihost/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.
// Each exempt route must enforce an equivalent route-local boundary before it
// can use an ambient cookie/session credential.
var csrfExemptPaths = map[string]struct{}{
	"/oauth2/token":                 {},
	"/oauth2/google/token":          {},
	"/services/oauth2/token":        {},
	"/services/oauth2/google/token": {},
	// /mcp performs its own Origin validation. More importantly, its first
	// route-local middleware always returns an OAuth discovery 401 unless an
	// explicit Bearer token is present, so a browser session cookie can never
	// authenticate an MCP RPC request.
	"/mcp": {},
}

// csrfExemptPrefixes lists path prefixes exempt from CSRF validation, for
// route families whose concrete paths are dynamic and cannot be enumerated in
// csrfExemptPaths. Each entry must be a route family that has NO ambient
// cookie/session authentication (so there is no cross-site-forgeable
// credential) and uses its own request-level authentication.
//
//   - "/webhooks/" — the generic inbound webhook ingress (NCRM-INT-04 / #338),
//     POST /webhooks/:namespace/*path. Callers are non-browser provider servers
//     that supply neither Origin/Referer nor the double-submit token; the route
//     carries no auth middleware and treats the per-pack HMAC webhook secret as
//     its trust boundary, so CSRF does not apply. The trailing slash keeps this
//     from matching unrelated prefixes such as "/connector-webhooks" (the
//     permission-gated admin API, which stays CSRF-protected).
var csrfExemptPrefixes = []string{
	"/webhooks/",
}

// 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
		}

		for _, prefix := range csrfExemptPrefixes {
			if strings.HasPrefix(c.Request.URL.Path, prefix) {
				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"})
}
