package server

import (
	"context"
	"encoding/json"
	"errors"
	"net"
	"net/http"
	"net/netip"
	"strings"

	"vocat/internal/store"
)

const accessSettingKey = "security.access"

// accessConfig is the persisted network access policy.
type accessConfig struct {
	Mode              string   `json:"mode"`               // "internal" (default) or "public"
	AllowedCIDRs      []string `json:"allowed_cidrs"`      // extra CIDRs always allowed
	TrustProxyHeaders bool     `json:"trust_proxy_headers"` // honor X-Forwarded-For
}

// parsedAccessConfig is the validated runtime form of accessConfig.
type parsedAccessConfig struct {
	mode       string
	cidrs      []netip.Prefix
	trustProxy bool
}

// internalNetworks are always allowed when mode is "internal": loopback,
// RFC1918 private ranges, link-local, and IPv6 ULA.
var internalNetworks = []netip.Prefix{
	netip.MustParsePrefix("127.0.0.0/8"),
	netip.MustParsePrefix("10.0.0.0/8"),
	netip.MustParsePrefix("172.16.0.0/12"),
	netip.MustParsePrefix("192.168.0.0/16"),
	netip.MustParsePrefix("169.254.0.0/16"),
	netip.MustParsePrefix("::1/128"),
	netip.MustParsePrefix("fe80::/10"),
	netip.MustParsePrefix("fc00::/7"),
}

func defaultAccessConfig() parsedAccessConfig {
	return parsedAccessConfig{mode: "internal"}
}

// parseAccessConfig validates and parses a persisted access policy.
func parseAccessConfig(config accessConfig) (parsedAccessConfig, error) {
	mode := strings.ToLower(strings.TrimSpace(config.Mode))
	if mode == "" {
		mode = "internal"
	}
	if mode != "internal" && mode != "public" {
		return parsedAccessConfig{}, errors.New("mode must be \"internal\" or \"public\"")
	}
	parsed := parsedAccessConfig{
		mode:       mode,
		trustProxy: config.TrustProxyHeaders,
	}
	for _, raw := range config.AllowedCIDRs {
		raw = strings.TrimSpace(raw)
		if raw == "" {
			continue
		}
		if prefix, err := netip.ParsePrefix(raw); err == nil {
			parsed.cidrs = append(parsed.cidrs, prefix.Masked())
			continue
		}
		if address, err := netip.ParseAddr(raw); err == nil {
			bits := 32
			if address.Is6() {
				bits = 128
			}
			parsed.cidrs = append(parsed.cidrs, netip.PrefixFrom(address, bits))
			continue
		}
		return parsedAccessConfig{}, errors.New("invalid CIDR or IP: " + raw)
	}
	return parsed, nil
}

// allowed reports whether a client address may reach the service.
func (config parsedAccessConfig) allowed(address netip.Addr) bool {
	if !address.IsValid() {
		return false
	}
	// Normalize IPv4-mapped IPv6 addresses (e.g. ::ffff:192.168.1.5 seen on
	// dual-stack listeners) to their IPv4 form so they match the internal
	// ranges below; without this they would be denied even though they are
	// ordinary internal IPv4 clients.
	address = address.Unmap()
	if config.mode == "public" {
		return true
	}
	if address.IsLoopback() {
		return true
	}
	for _, prefix := range internalNetworks {
		if prefix.Contains(address) {
			return true
		}
	}
	for _, prefix := range config.cidrs {
		if prefix.Contains(address) {
			return true
		}
	}
	return false
}

// clientIP determines the request's source address, honoring X-Forwarded-For
// only when the deployment is configured to trust proxy headers.
func (config parsedAccessConfig) clientIP(r *http.Request) netip.Addr {
	if config.trustProxy {
		if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
			first := strings.TrimSpace(strings.Split(forwarded, ",")[0])
			if address, err := netip.ParseAddr(first); err == nil {
				return address.Unmap()
			}
		}
		if real := strings.TrimSpace(r.Header.Get("X-Real-IP")); real != "" {
			if address, err := netip.ParseAddr(real); err == nil {
				return address.Unmap()
			}
		}
	}
	host, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
	if err != nil {
		host = strings.TrimSpace(r.RemoteAddr)
	}
	address, err := netip.ParseAddr(host)
	if err != nil {
		return netip.Addr{}
	}
	// Report the canonical (unmapped) form so logs, the login rate-limit key,
	// and the access decision all agree on one representation of an IPv4 client.
	return address.Unmap()
}

// accessControl rejects requests whose source IP is outside the configured
// access policy. It wraps the whole mux so every route (API, SPA, websheets) is
// protected uniformly.
func (s *Server) accessControl(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		s.accessMu.RLock()
		config := s.access
		s.accessMu.RUnlock()
		address := config.clientIP(r)
		if config.allowed(address) {
			next.ServeHTTP(w, r)
			return
		}
		s.logger.Warn(
			"request denied by network access policy",
			"remote_addr", r.RemoteAddr,
			"client_ip", address.String(),
			"path", r.URL.Path,
		)
		writeError(
			w,
			http.StatusForbidden,
			"network_access_denied",
			"access is restricted to internal network addresses",
		)
	})
}

func (s *Server) currentAccessConfig() parsedAccessConfig {
	s.accessMu.RLock()
	defer s.accessMu.RUnlock()
	return s.access
}

// loadAccessConfig reads the persisted policy (defaulting to internal) into the
// runtime cache. Called at startup.
func (s *Server) loadAccessConfig(ctx context.Context) {
	config := defaultAccessConfig()
	setting, err := s.store.AppSetting(ctx, accessSettingKey)
	if err == nil {
		var stored accessConfig
		if json.Unmarshal(setting.Value, &stored) == nil {
			if parsed, parseErr := parseAccessConfig(stored); parseErr == nil {
				config = parsed
			}
		}
	} else if !errors.Is(err, store.ErrNotFound) {
		s.logger.Warn("load access policy failed", "error", err)
	}
	s.accessMu.Lock()
	s.access = config
	s.accessMu.Unlock()
}

// handleSecuritySettings reads and writes the network access policy.
//
//	GET /api/settings/security
//	PUT /api/settings/security
func (s *Server) handleSecuritySettings(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		config := s.currentAccessConfig()
		address := config.clientIP(r)
		cidrs := make([]string, 0, len(config.cidrs))
		for _, prefix := range config.cidrs {
			cidrs = append(cidrs, prefix.String())
		}
		writeJSON(w, http.StatusOK, map[string]any{
			"data": map[string]any{
				"mode":                config.mode,
				"allowed_cidrs":       cidrs,
				"trust_proxy_headers": config.trustProxy,
				"client_ip":           address.String(),
				"client_allowed":      config.allowed(address),
			},
		})
	case http.MethodPut:
		var request accessConfig
		if err := s.decodeJSON(w, r, &request); err != nil {
			writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
			return
		}
		parsed, err := parseAccessConfig(request)
		if err != nil {
			writeError(w, http.StatusBadRequest, "invalid_access_policy", err.Error())
			return
		}
		payload, err := json.Marshal(request)
		if err != nil {
			writeError(w, http.StatusInternalServerError, "internal_error", "an internal error occurred")
			return
		}
		if err := s.store.UpsertAppSetting(r.Context(), store.AppSetting{
			Key:   accessSettingKey,
			Value: payload,
		}); err != nil {
			s.writeStoreError(w, err)
			return
		}
		s.accessMu.Lock()
		s.access = parsed
		s.accessMu.Unlock()
		s.audit(r, "settings.security.update", "settings", "security", "success")
		address := parsed.clientIP(r)
		cidrs := make([]string, 0, len(parsed.cidrs))
		for _, prefix := range parsed.cidrs {
			cidrs = append(cidrs, prefix.String())
		}
		writeJSON(w, http.StatusOK, map[string]any{
			"data": map[string]any{
				"mode":                parsed.mode,
				"allowed_cidrs":       cidrs,
				"trust_proxy_headers": parsed.trustProxy,
				"client_ip":           address.String(),
				"client_allowed":      parsed.allowed(address),
			},
		})
	default:
		w.Header().Set("Allow", "GET, PUT")
		writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
	}
}
