package web

import (
	"crypto/subtle"
	"fmt"
	"html/template"
	"net/http"
	"strings"
)

const csrfFormField = "_csrf"
const csrfHeaderName = "X-CSRF-Token"

// CsrfProtect validates CSRF tokens on unsafe HTTP methods (POST, PUT, DELETE, PATCH).
// Safe methods (GET, HEAD, OPTIONS) pass through unchanged.
//
// Exempt cases:
//   - Auth is disabled (no password configured)
//   - Request has a valid Authorization: Bearer header (API key / hub auth)
func (s *Server) CsrfProtect(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Safe methods: no CSRF check needed
		switch r.Method {
		case http.MethodGet, http.MethodHead, http.MethodOptions:
			next.ServeHTTP(w, r)
			return
		}

		// Skip CSRF if auth is disabled (no password set = open access)
		if !s.authEnabled() {
			next.ServeHTTP(w, r)
			return
		}

		// Skip CSRF for Bearer-token authenticated requests.
		// Validate the token against the configured API key before skipping.
		if auth := r.Header.Get("Authorization"); strings.HasPrefix(auth, "Bearer ") {
			token := strings.TrimPrefix(auth, "Bearer ")
			apiKey := s.cfg.Hub.APIKey
			if apiKey != "" && subtle.ConstantTimeCompare([]byte(token), []byte(apiKey)) == 1 {
				next.ServeHTTP(w, r)
				return
			}
			// Invalid Bearer token — fall through to CSRF validation
		}

		// Get the session's CSRF token
		cookie, err := r.Cookie(sessionCookieName)
		if err != nil {
			s.csrfReject(w, r, "no session cookie")
			return
		}
		expected := s.csrfTokenForSession(cookie.Value)
		if expected == "" {
			s.csrfReject(w, r, "invalid or expired session")
			return
		}

		// Check form field first, then header (for fetch/AJAX calls)
		submitted := r.FormValue(csrfFormField)
		if submitted == "" {
			submitted = r.Header.Get(csrfHeaderName)
		}

		if submitted == "" || subtle.ConstantTimeCompare([]byte(submitted), []byte(expected)) != 1 {
			s.csrfReject(w, r, "token mismatch")
			return
		}

		next.ServeHTTP(w, r)
	})
}

// csrfReject sends a 403 response. Returns JSON for /api/ paths, plain text otherwise.
func (s *Server) csrfReject(w http.ResponseWriter, r *http.Request, reason string) {
	s.logger.Printf("[WARN] CSRF rejected: %s %s from %s (%s)", r.Method, r.URL.Path, r.RemoteAddr, reason)
	if strings.HasPrefix(r.URL.Path, "/api/") {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusForbidden)
		fmt.Fprint(w, `{"ok":false,"error":"CSRF token missing or invalid"}`)
		return
	}
	http.Error(w, "CSRF token missing or invalid. Please reload the page and try again.", http.StatusForbidden)
}

// csrfToken returns the CSRF token for the current request's session.
func (s *Server) csrfToken(r *http.Request) string {
	cookie, err := r.Cookie(sessionCookieName)
	if err != nil {
		return ""
	}
	return s.csrfTokenForSession(cookie.Value)
}

// csrfField returns an HTML hidden input for embedding in forms.
func (s *Server) csrfField(r *http.Request) template.HTML {
	token := s.csrfToken(r)
	return template.HTML(`<input type="hidden" name="` + csrfFormField + `" value="` + template.HTMLEscapeString(token) + `">`)
}
