package originauth

import (
	"crypto/subtle"
	"fmt"
	"net"
	"net/http"
	"net/netip"
	"os"
	"strings"

	"github.com/wangjia/pangolin/server/internal/apierr"
)

// DefaultHeader is the request header the CDN injects on origin requests.
const DefaultHeader = "X-Origin-Auth"

// errForbidden is the desensitised 403 body (no "VPN"/"proxy" wording).
var errForbidden = &apierr.Error{
	Code:      "FORBIDDEN",
	MessageZH: "访问被拒绝",
	MessageEn: "Access denied",
}

// Config configures the middleware.
type Config struct {
	// Header is the auth header name. Empty defaults to DefaultHeader.
	Header string
	// Current is the active auth value (required).
	Current string
	// Previous is the prior auth value accepted during a rotation window
	// (optional).
	Previous string
	// AllowedCIDRs are the CDN egress ranges permitted to reach the origin
	// (required, at least one).
	AllowedCIDRs []string
}

// Middleware enforces CDN-only origin access.
type Middleware struct {
	header   string
	current  string
	previous string
	nets     []netip.Prefix
}

// New validates cfg and builds a Middleware.
func New(cfg Config) (*Middleware, error) {
	header := cfg.Header
	if header == "" {
		header = DefaultHeader
	}
	if cfg.Current == "" {
		return nil, fmt.Errorf("originauth: Current auth value is required")
	}
	if len(cfg.AllowedCIDRs) == 0 {
		return nil, fmt.Errorf("originauth: at least one AllowedCIDR is required")
	}
	nets := make([]netip.Prefix, 0, len(cfg.AllowedCIDRs))
	for _, c := range cfg.AllowedCIDRs {
		c = strings.TrimSpace(c)
		if c == "" {
			continue
		}
		p, err := netip.ParsePrefix(c)
		if err != nil {
			return nil, fmt.Errorf("originauth: invalid CIDR %q: %w", c, err)
		}
		nets = append(nets, p.Masked())
	}
	if len(nets) == 0 {
		return nil, fmt.Errorf("originauth: at least one AllowedCIDR is required")
	}
	return &Middleware{
		header:   header,
		current:  cfg.Current,
		previous: cfg.Previous,
		nets:     nets,
	}, nil
}

// Handler is the net/http middleware. It calls next only when both the source
// IP and the auth header are accepted; otherwise it writes a 403.
func (m *Middleware) Handler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !m.ipAllowed(r.RemoteAddr) || !m.headerAllowed(r.Header.Get(m.header)) {
			apierr.WriteJSON(w, http.StatusForbidden, errForbidden)
			return
		}
		next.ServeHTTP(w, r)
	})
}

// ipAllowed reports whether the TCP peer address falls in an allowed CDN range.
// It uses the real connection peer (RemoteAddr), never a client-supplied header,
// so a forged X-Forwarded-For cannot bypass the check.
func (m *Middleware) ipAllowed(remoteAddr string) bool {
	host := remoteAddr
	if h, _, err := net.SplitHostPort(remoteAddr); err == nil {
		host = h
	}
	addr, err := netip.ParseAddr(strings.TrimSpace(host))
	if err != nil {
		return false
	}
	addr = addr.Unmap()
	for _, p := range m.nets {
		if p.Contains(addr) {
			return true
		}
	}
	return false
}

// headerAllowed reports whether v matches the current or previous auth value,
// using constant-time comparison.
func (m *Middleware) headerAllowed(v string) bool {
	if v == "" {
		return false
	}
	if subtle.ConstantTimeCompare([]byte(v), []byte(m.current)) == 1 {
		return true
	}
	if m.previous != "" && subtle.ConstantTimeCompare([]byte(v), []byte(m.previous)) == 1 {
		return true
	}
	return false
}

// FromEnv builds a Config from environment variables:
//
//	ORIGIN_AUTH_HEADER     (optional, default X-Origin-Auth)
//	ORIGIN_AUTH_CURRENT    (required)
//	ORIGIN_AUTH_PREVIOUS   (optional, rotation window)
//	ORIGIN_AUTH_CIDRS      (required, comma-separated CDN egress ranges)
//
// The returned Config is still passed to New for validation.
func FromEnv() Config {
	var cidrs []string
	if raw := os.Getenv("ORIGIN_AUTH_CIDRS"); raw != "" {
		cidrs = strings.Split(raw, ",")
	}
	return Config{
		Header:       os.Getenv("ORIGIN_AUTH_HEADER"),
		Current:      os.Getenv("ORIGIN_AUTH_CURRENT"),
		Previous:     os.Getenv("ORIGIN_AUTH_PREVIOUS"),
		AllowedCIDRs: cidrs,
	}
}
