package edgeguard

import (
	"bufio"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/netip"
	"os"
	"sort"
	"strings"
	"time"
)

type RateLimit struct {
	RatePerSecond float64 `json:"rate_per_second"`
	Burst         int     `json:"burst"`
}

type RateRule struct {
	Name          string  `json:"name"`
	Host          string  `json:"host,omitempty"`
	PathPrefix    string  `json:"path_prefix"`
	RatePerSecond float64 `json:"rate_per_second"`
	Burst         int     `json:"burst"`
}

type AutoBanConfig struct {
	Enabled          bool `json:"enabled"`
	Threshold        int  `json:"threshold"`
	WindowSeconds    int  `json:"window_seconds"`
	BanSeconds       int  `json:"ban_seconds"`
	ScannerWeight    int  `json:"scanner_weight"`
	MethodWeight     int  `json:"method_weight"`
	RateLimitWeight  int  `json:"rate_limit_weight"`
	InvalidURIWeight int  `json:"invalid_uri_weight"`
}

type Config struct {
	Listen              string        `json:"listen"`
	AllowedHosts        []string      `json:"allowed_hosts"`
	BlacklistFile       string        `json:"blacklist_file"`
	RateExemptFile      string        `json:"rate_exempt_file"`
	StateFile           string        `json:"state_file"`
	ReloadSeconds       int           `json:"reload_seconds"`
	MaxURILength        int           `json:"max_uri_length"`
	MaxTrackedIPs       int           `json:"max_tracked_ips"`
	GlobalLimit         RateLimit     `json:"global_limit"`
	PerIPLimit          RateLimit     `json:"per_ip_limit"`
	Rules               []RateRule    `json:"rules"`
	BlockedMethods      []string      `json:"blocked_methods"`
	ScannerPathPrefixes []string      `json:"scanner_path_prefixes"`
	AutoBan             AutoBanConfig `json:"auto_ban"`
}

type RuntimeConfig struct {
	Config
	allowedHosts   map[string]struct{}
	blockedMethods map[string]struct{}
	blacklist      []netip.Prefix
	rateExempt     []netip.Prefix
	fingerprint    string
}

func DefaultConfig() Config {
	return Config{
		Listen:        "127.0.0.1:9081",
		ReloadSeconds: 15,
		MaxURILength:  8192,
		MaxTrackedIPs: 100000,
		GlobalLimit: RateLimit{
			RatePerSecond: 2500,
			Burst:         5000,
		},
		PerIPLimit: RateLimit{
			RatePerSecond: 200,
			Burst:         500,
		},
		Rules: []RateRule{
			{Name: "access-login", PathPrefix: "/_sessionguard/auth/login", RatePerSecond: 5, Burst: 100},
			{Name: "access-callback", PathPrefix: "/_sessionguard/auth/oidc/callback", RatePerSecond: 10, Burst: 100},
			{Name: "admin-oidc", PathPrefix: "/oidc/", RatePerSecond: 5, Burst: 50},
		},
		BlockedMethods: []string{"CONNECT", "TRACE", "TRACK"},
		ScannerPathPrefixes: []string{
			"/.env", "/.git", "/.svn", "/.hg", "/wp-admin", "/wp-login.php",
			"/phpmyadmin", "/pma", "/cgi-bin", "/server-status", "/actuator",
			"/vendor/phpunit", "/boaform", "/HNAP1", "/solr/", "/jenkins/",
		},
		AutoBan: AutoBanConfig{
			Enabled:          true,
			Threshold:        10,
			WindowSeconds:    120,
			BanSeconds:       900,
			ScannerWeight:    5,
			MethodWeight:     3,
			RateLimitWeight:  0,
			InvalidURIWeight: 3,
		},
	}
}

func LoadRuntimeConfig(path string) (*RuntimeConfig, error) {
	cfg := DefaultConfig()
	b, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("read config: %w", err)
	}
	if err := json.Unmarshal(b, &cfg); err != nil {
		return nil, fmt.Errorf("decode config: %w", err)
	}
	if err := validateConfig(&cfg); err != nil {
		return nil, err
	}

	rc := &RuntimeConfig{Config: cfg}
	rc.allowedHosts = make(map[string]struct{}, len(cfg.AllowedHosts))
	for _, h := range cfg.AllowedHosts {
		h = normalizeHost(h)
		if h != "" {
			rc.allowedHosts[h] = struct{}{}
		}
	}
	rc.blockedMethods = make(map[string]struct{}, len(cfg.BlockedMethods))
	for _, m := range cfg.BlockedMethods {
		m = strings.ToUpper(strings.TrimSpace(m))
		if m != "" {
			rc.blockedMethods[m] = struct{}{}
		}
	}

	rc.blacklist, err = loadPrefixFile(cfg.BlacklistFile)
	if err != nil {
		return nil, fmt.Errorf("load blacklist: %w", err)
	}
	rc.rateExempt, err = loadPrefixFile(cfg.RateExemptFile)
	if err != nil {
		return nil, fmt.Errorf("load rate exempt list: %w", err)
	}

	h := sha256.New()
	h.Write(b)
	for _, p := range rc.blacklist {
		h.Write([]byte("blacklist:" + p.String() + "\n"))
	}
	for _, p := range rc.rateExempt {
		h.Write([]byte("rate-exempt:" + p.String() + "\n"))
	}
	rc.fingerprint = hex.EncodeToString(h.Sum(nil))
	return rc, nil
}

func validateConfig(cfg *Config) error {
	if strings.TrimSpace(cfg.Listen) == "" {
		return fmt.Errorf("listen must not be empty")
	}
	if cfg.ReloadSeconds <= 0 {
		cfg.ReloadSeconds = 15
	}
	if cfg.MaxURILength <= 0 {
		cfg.MaxURILength = 8192
	}
	if cfg.MaxTrackedIPs <= 0 {
		cfg.MaxTrackedIPs = 100000
	}
	if err := validateRate("global_limit", cfg.GlobalLimit); err != nil {
		return err
	}
	if err := validateRate("per_ip_limit", cfg.PerIPLimit); err != nil {
		return err
	}
	for i := range cfg.Rules {
		r := &cfg.Rules[i]
		r.Host = normalizeHost(r.Host)
		r.PathPrefix = strings.ToLower(strings.TrimSpace(r.PathPrefix))
		if r.PathPrefix == "" {
			return fmt.Errorf("rules[%d].path_prefix must not be empty", i)
		}
		if r.RatePerSecond <= 0 || r.Burst <= 0 {
			return fmt.Errorf("rules[%d] must have positive rate_per_second and burst", i)
		}
	}
	if cfg.AutoBan.Enabled {
		if cfg.AutoBan.Threshold <= 0 || cfg.AutoBan.WindowSeconds <= 0 || cfg.AutoBan.BanSeconds <= 0 {
			return fmt.Errorf("auto_ban threshold/window_seconds/ban_seconds must be positive")
		}
	}
	return nil
}

func validateRate(name string, r RateLimit) error {
	if r.RatePerSecond <= 0 || r.Burst <= 0 {
		return fmt.Errorf("%s must have positive rate_per_second and burst", name)
	}
	return nil
}

func loadPrefixFile(path string) ([]netip.Prefix, error) {
	path = strings.TrimSpace(path)
	if path == "" {
		return nil, nil
	}
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	var out []netip.Prefix
	s := bufio.NewScanner(f)
	lineNo := 0
	for s.Scan() {
		lineNo++
		line := strings.TrimSpace(s.Text())
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		if i := strings.IndexByte(line, '#'); i >= 0 {
			line = strings.TrimSpace(line[:i])
		}
		p, err := parsePrefix(line)
		if err != nil {
			return nil, fmt.Errorf("%s:%d: %w", path, lineNo, err)
		}
		out = append(out, p)
	}
	if err := s.Err(); err != nil {
		return nil, err
	}
	sort.Slice(out, func(i, j int) bool { return out[i].String() < out[j].String() })
	return out, nil
}

func parsePrefix(s string) (netip.Prefix, error) {
	if p, err := netip.ParsePrefix(s); err == nil {
		return p.Masked(), nil
	}
	a, err := netip.ParseAddr(s)
	if err != nil {
		return netip.Prefix{}, fmt.Errorf("invalid IP/CIDR %q", s)
	}
	bits := 128
	if a.Is4() {
		bits = 32
	}
	return netip.PrefixFrom(a, bits), nil
}

func normalizeHost(s string) string {
	s = strings.TrimSpace(strings.ToLower(s))
	if i := strings.IndexByte(s, ':'); i > 0 && strings.Count(s, ":") == 1 {
		s = s[:i]
	}
	return strings.TrimSuffix(s, ".")
}

func (r *RuntimeConfig) IsBlacklisted(ip netip.Addr) bool {
	return containsPrefix(r.blacklist, ip)
}

func (r *RuntimeConfig) IsRateExempt(ip netip.Addr) bool {
	return containsPrefix(r.rateExempt, ip)
}

func containsPrefix(prefixes []netip.Prefix, ip netip.Addr) bool {
	for _, p := range prefixes {
		if p.Contains(ip) {
			return true
		}
	}
	return false
}

func (r *RuntimeConfig) HostAllowed(host string) bool {
	if len(r.allowedHosts) == 0 {
		return true
	}
	_, ok := r.allowedHosts[normalizeHost(host)]
	return ok
}

func (r *RuntimeConfig) MethodBlocked(method string) bool {
	_, ok := r.blockedMethods[strings.ToUpper(strings.TrimSpace(method))]
	return ok
}

func (r *RuntimeConfig) MatchingRule(host, path string) *RateRule {
	host = normalizeHost(host)
	for i := range r.Rules {
		rule := &r.Rules[i]
		if rule.Host != "" && rule.Host != host {
			continue
		}
		if strings.HasPrefix(path, rule.PathPrefix) {
			return rule
		}
	}
	return nil
}

func (r *RuntimeConfig) ReloadInterval() time.Duration {
	return time.Duration(r.ReloadSeconds) * time.Second
}

func (r *RuntimeConfig) Fingerprint() string { return r.fingerprint }
