package middleware

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

type EdgeSecurityOptions struct {
	TLSRequired          bool
	HSTSEnabled          bool
	AllowedHosts         []string
	TrustedProxyRequired bool
}

type CSRFOptions struct {
	Enabled        bool
	AuthCookieName string
	CSRFCookieName string
	HeaderName     string
}

func EdgeSecurity(opts EdgeSecurityOptions) Middleware {
	allowedHosts := map[string]bool{}
	for _, host := range opts.AllowedHosts {
		host = normalizeHost(host)
		if host != "" {
			allowedHosts[host] = true
		}
	}

	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			host := normalizeHost(r.Host)
			if len(allowedHosts) > 0 && !allowedHosts[host] {
				http.Error(w, http.StatusText(http.StatusMisdirectedRequest), http.StatusMisdirectedRequest)
				return
			}
			if opts.TLSRequired && !requestIsHTTPS(r, opts.TrustedProxyRequired) {
				http.Error(w, "https is required", http.StatusUpgradeRequired)
				return
			}
			if opts.HSTSEnabled {
				w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
			}
			next.ServeHTTP(w, r)
		})
	}
}

func InternalCIDRAllowlist(cidrs []string) Middleware {
	allowedNetworks := make([]*net.IPNet, 0, len(cidrs))
	for _, cidr := range cidrs {
		_, network, err := net.ParseCIDR(strings.TrimSpace(cidr))
		if err == nil {
			allowedNetworks = append(allowedNetworks, network)
		}
	}

	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ip := remoteIP(r.RemoteAddr)
			for _, network := range allowedNetworks {
				if network.Contains(ip) {
					next.ServeHTTP(w, r)
					return
				}
			}
			http.Error(w, "metrics endpoint is internal only", http.StatusForbidden)
		})
	}
}

func CSRFProtection(opts CSRFOptions) Middleware {
	if opts.AuthCookieName == "" {
		opts.AuthCookieName = "banking_refresh_token"
	}
	if opts.CSRFCookieName == "" {
		opts.CSRFCookieName = "banking_csrf_token"
	}
	if opts.HeaderName == "" {
		opts.HeaderName = "X-CSRF-Token"
	}

	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if !opts.Enabled || isSafeMethod(r.Method) {
				next.ServeHTTP(w, r)
				return
			}
			if _, err := r.Cookie(opts.AuthCookieName); err != nil {
				next.ServeHTTP(w, r)
				return
			}
			csrfCookie, err := r.Cookie(opts.CSRFCookieName)
			if err != nil {
				http.Error(w, "csrf token is required", http.StatusForbidden)
				return
			}
			header := strings.TrimSpace(r.Header.Get(opts.HeaderName))
			if !validCSRFToken(header, csrfCookie.Value) {
				http.Error(w, "csrf token is invalid", http.StatusForbidden)
				return
			}
			next.ServeHTTP(w, r)
		})
	}
}

func remoteIP(remoteAddr string) net.IP {
	host, _, err := net.SplitHostPort(remoteAddr)
	if err != nil {
		host = remoteAddr
	}
	return net.ParseIP(strings.TrimSpace(host))
}

func requestIsHTTPS(r *http.Request, trustedProxyRequired bool) bool {
	if r.TLS != nil {
		return true
	}
	if trustedProxyRequired {
		return strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https")
	}
	return strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") ||
		strings.EqualFold(r.Header.Get("X-Forwarded-Ssl"), "on")
}

func normalizeHost(raw string) string {
	host := strings.ToLower(strings.TrimSpace(raw))
	if strings.HasPrefix(host, "[") {
		if withoutPort, _, err := net.SplitHostPort(host); err == nil {
			return strings.Trim(withoutPort, "[]")
		}
		return strings.Trim(host, "[]")
	}
	if strings.Count(host, ":") == 1 {
		if withoutPort, _, err := net.SplitHostPort(host); err == nil {
			return withoutPort
		}
	}
	return host
}

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

func validCSRFToken(header, cookie string) bool {
	header = strings.TrimSpace(header)
	cookie = strings.TrimSpace(cookie)
	if len(header) < 32 || len(cookie) < 32 || len(header) != len(cookie) {
		return false
	}
	return subtle.ConstantTimeCompare([]byte(header), []byte(cookie)) == 1
}

func ValidateCSRFForTests(header, cookie string) error {
	if !validCSRFToken(header, cookie) {
		return fmt.Errorf("invalid csrf token")
	}
	return nil
}
