package httpserver

import (
	"context"
	"crypto/rand"
	"crypto/subtle"
	"encoding/base64"
	"fmt"
	"net/http"
	"strings"
	"time"
)

const (
	adminCookiePath = "/admin"
	csrfFormField   = "csrf_token"
	csrfHeaderName  = "X-CSRF-Token"
)

type csrfTokenContextKey struct{}

func (h *handler) adminCSRF(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		token, cookie, err := h.ensureCSRFCookie(r)
		if err != nil {
			http.Error(w, "csrf setup failed", http.StatusInternalServerError)
			return
		}

		if cookie != nil {
			http.SetCookie(w, cookie)
		}

		r = r.WithContext(context.WithValue(r.Context(), csrfTokenContextKey{}, token))

		if requiresCSRFProtection(r.Method) {
			submittedToken, err := h.submittedCSRFToken(w, r)
			if err != nil || subtle.ConstantTimeCompare([]byte(token), []byte(submittedToken)) != 1 {
				http.Error(w, "csrf validation failed", http.StatusForbidden)
				return
			}
		}

		next.ServeHTTP(w, r)
	})
}

func (h *handler) csrfToken(r *http.Request) string {
	if token, ok := r.Context().Value(csrfTokenContextKey{}).(string); ok {
		return token
	}

	cookie, err := r.Cookie(h.config.CSRFCookieName)
	if err != nil {
		return ""
	}

	if !validCSRFCookieToken(cookie.Value) {
		return ""
	}

	return cookie.Value
}

func (h *handler) issueCSRFCookie(w http.ResponseWriter) (string, error) {
	token, err := generateCSRFToken()
	if err != nil {
		return "", err
	}

	http.SetCookie(w, h.csrfCookie(token))
	return token, nil
}

func (h *handler) clearCSRFCookie() *http.Cookie {
	return &http.Cookie{
		Name:     h.config.CSRFCookieName,
		Value:    "",
		Path:     adminCookiePath,
		HttpOnly: true,
		SameSite: http.SameSiteStrictMode,
		Secure:   h.config.SecureCookies,
		Expires:  time.Unix(0, 0).UTC(),
		MaxAge:   -1,
	}
}

func (h *handler) ensureCSRFCookie(r *http.Request) (string, *http.Cookie, error) {
	cookie, err := r.Cookie(h.config.CSRFCookieName)
	if err == nil && validCSRFCookieToken(cookie.Value) {
		return cookie.Value, nil, nil
	}

	token, err := generateCSRFToken()
	if err != nil {
		return "", nil, fmt.Errorf("generate csrf token: %w", err)
	}

	return token, h.csrfCookie(token), nil
}

func (h *handler) csrfCookie(token string) *http.Cookie {
	return &http.Cookie{
		Name:     h.config.CSRFCookieName,
		Value:    token,
		Path:     adminCookiePath,
		HttpOnly: true,
		SameSite: http.SameSiteStrictMode,
		Secure:   h.config.SecureCookies,
	}
}

func (h *handler) submittedCSRFToken(w http.ResponseWriter, r *http.Request) (string, error) {
	if token := strings.TrimSpace(r.Header.Get(csrfHeaderName)); token != "" {
		return token, nil
	}

	contentType := strings.ToLower(strings.TrimSpace(r.Header.Get("Content-Type")))
	if strings.HasPrefix(contentType, "multipart/form-data") {
		r.Body = http.MaxBytesReader(w, r.Body, maxUploadRequestLimit(h.config.MaxUploadBytes))
		if err := r.ParseMultipartForm(16 << 20); err != nil {
			return "", err
		}

		return strings.TrimSpace(r.FormValue(csrfFormField)), nil
	}

	if err := r.ParseForm(); err != nil {
		return "", err
	}

	return strings.TrimSpace(r.FormValue(csrfFormField)), nil
}

func generateCSRFToken() (string, error) {
	buf := make([]byte, 32)
	if _, err := rand.Read(buf); err != nil {
		return "", err
	}

	return base64.RawURLEncoding.EncodeToString(buf), nil
}

func requiresCSRFProtection(method string) bool {
	switch method {
	case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace:
		return false
	default:
		return true
	}
}

func validCSRFCookieToken(token string) bool {
	token = strings.TrimSpace(token)
	return len(token) >= 32
}
