package middleware

import "net/http"

// SecurityHeadersConfig configures the SecurityHeaders middleware. All headers
// are opt-in via cfg so they can be tuned (or disabled) without code changes;
// the defaults are safe for an SPA + WebSocket app. HSTS is gated on TLS — it is
// only emitted when TLSEnabled is true, since advertising HSTS over plain HTTP
// either does nothing or (once cached) breaks local http access.
type SecurityHeadersConfig struct {
	Enabled    bool
	TLSEnabled bool
	// HSTSMaxAgeSeconds is the Strict-Transport-Security max-age. 0 → default
	// (1 year) when TLSEnabled.
	HSTSMaxAgeSeconds int
	// FrameOptions sets X-Frame-Options; empty → "DENY".
	FrameOptions string
	// ReferrerPolicy sets Referrer-Policy; empty → "no-referrer".
	ReferrerPolicy string
	// ContentSecurityPolicy sets CSP; empty → header omitted (CSP can break the
	// SPA + WS handshake if mis-set, so it is off by default and opt-in).
	ContentSecurityPolicy string
}

// SecurityHeaders returns middleware that stamps standard security response
// headers. When cfg.Enabled is false it is a transparent pass-through, so it can
// be wired unconditionally and toggled by config.
func SecurityHeaders(cfg SecurityHeadersConfig) func(http.Handler) http.Handler {
	frame := cfg.FrameOptions
	if frame == "" {
		frame = "DENY"
	}
	referrer := cfg.ReferrerPolicy
	if referrer == "" {
		referrer = "no-referrer"
	}
	hstsAge := cfg.HSTSMaxAgeSeconds
	if hstsAge <= 0 {
		hstsAge = 31536000 // 1 year
	}
	hstsValue := "max-age=" + itoa(hstsAge) + "; includeSubDomains"

	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if !cfg.Enabled {
				next.ServeHTTP(w, r)
				return
			}
			h := w.Header()
			h.Set("X-Content-Type-Options", "nosniff")
			h.Set("X-Frame-Options", frame)
			h.Set("Referrer-Policy", referrer)
			if cfg.ContentSecurityPolicy != "" {
				h.Set("Content-Security-Policy", cfg.ContentSecurityPolicy)
			}
			if cfg.TLSEnabled {
				h.Set("Strict-Transport-Security", hstsValue)
			}
			next.ServeHTTP(w, r)
		})
	}
}

// itoa is a tiny non-allocating-ish int->string for positive values, avoiding a
// strconv import for a single use.
func itoa(n int) string {
	if n == 0 {
		return "0"
	}
	var buf [20]byte
	i := len(buf)
	for n > 0 {
		i--
		buf[i] = byte('0' + n%10)
		n /= 10
	}
	return string(buf[i:])
}
