package admin

import (
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"net"
	"os"
	"strings"
	"time"
)

// Config holds all configuration for the admin backend listener.
//
// Security invariants enforced here (see doc/02 §1 and doc/06 §2 红线
// "管理后台不得暴露公网"):
//   - Listen must never bind 0.0.0.0 / :: / an empty host; only a concrete
//     loopback or internal address is accepted.
//   - AllowCIDRs defaults to loopback + RFC1918 / ULA internal ranges only.
type Config struct {
	// Listen is the admin HTTP listen address, e.g. "127.0.0.1:9443".
	Listen string

	// AllowCIDRs is the IP allowlist applied by mw_ipallow. A request whose
	// source address is not contained in any of these networks is rejected
	// with 403 before any handler runs.
	AllowCIDRs []*net.IPNet

	// SecretKey is the 32-byte key (AES-256) used to encrypt TOTP secrets at
	// rest and to sign session/CSRF tokens' opaque ids are random, not signed.
	SecretKey []byte

	// SessionTTL is the sliding idle timeout for an admin session.
	SessionTTL time.Duration

	// LoginFailMax is the number of consecutive failed logins (per username)
	// before the account is temporarily locked.
	LoginFailMax int

	// LoginLockDuration is how long a username stays locked after hitting
	// LoginFailMax.
	LoginLockDuration time.Duration

	// CookieSecure controls the Secure attribute on the session cookie.
	// Defaults to true; only disabled explicitly for local/dev over plain HTTP.
	CookieSecure bool
}

// defaultInternalCIDRs are the loopback and private/ULA ranges allowed by
// default — the admin port must only be reachable over SSH tunnel / intranet.
var defaultInternalCIDRs = []string{
	"127.0.0.0/8",
	"::1/128",
	"10.0.0.0/8",
	"172.16.0.0/12",
	"192.168.0.0/16",
	"fc00::/7",
}

// FromEnv builds a Config from environment variables, applying safe defaults.
//
//	ADMIN_LISTEN          listen address          (default 127.0.0.1:9443)
//	ADMIN_ALLOW_CIDRS     comma-separated allowlist(default internal ranges)
//	ADMIN_SECRET_KEY      hex/base64 32-byte key  (required)
//	ADMIN_SESSION_TTL     Go duration             (default 30m)
//	ADMIN_LOGIN_FAIL_MAX  int                     (default 5)
//	ADMIN_LOGIN_LOCK      Go duration             (default 15m)
//	ADMIN_COOKIE_INSECURE "1" disables Secure flag (dev only)
func FromEnv() (*Config, error) {
	c := &Config{
		Listen:            getEnvDefault("ADMIN_LISTEN", "127.0.0.1:9443"),
		SessionTTL:        30 * time.Minute,
		LoginFailMax:      5,
		LoginLockDuration: 15 * time.Minute,
		CookieSecure:      os.Getenv("ADMIN_COOKIE_INSECURE") != "1",
	}

	if err := validateListen(c.Listen); err != nil {
		return nil, err
	}

	cidrs := os.Getenv("ADMIN_ALLOW_CIDRS")
	var raw []string
	if strings.TrimSpace(cidrs) == "" {
		raw = defaultInternalCIDRs
	} else {
		raw = splitTrim(cidrs)
	}
	nets, err := ParseCIDRs(raw)
	if err != nil {
		return nil, err
	}
	c.AllowCIDRs = nets

	key, err := parseSecretKey(os.Getenv("ADMIN_SECRET_KEY"))
	if err != nil {
		return nil, err
	}
	c.SecretKey = key

	if v := os.Getenv("ADMIN_SESSION_TTL"); v != "" {
		d, err := time.ParseDuration(v)
		if err != nil {
			return nil, fmt.Errorf("config: ADMIN_SESSION_TTL: %w", err)
		}
		c.SessionTTL = d
	}
	if v := os.Getenv("ADMIN_LOGIN_LOCK"); v != "" {
		d, err := time.ParseDuration(v)
		if err != nil {
			return nil, fmt.Errorf("config: ADMIN_LOGIN_LOCK: %w", err)
		}
		c.LoginLockDuration = d
	}
	if v := os.Getenv("ADMIN_LOGIN_FAIL_MAX"); v != "" {
		var n int
		if _, err := fmt.Sscanf(v, "%d", &n); err != nil || n <= 0 {
			return nil, fmt.Errorf("config: ADMIN_LOGIN_FAIL_MAX must be a positive integer")
		}
		c.LoginFailMax = n
	}

	return c, nil
}

// validateListen rejects any address that would expose the admin port on a
// public/wildcard interface. This is the code-level guard behind the red line.
func validateListen(addr string) error {
	host, _, err := net.SplitHostPort(addr)
	if err != nil {
		return fmt.Errorf("config: ADMIN_LISTEN %q invalid: %w", addr, err)
	}
	host = strings.TrimSpace(host)
	if host == "" || host == "0.0.0.0" || host == "::" || host == "[::]" {
		return fmt.Errorf("config: ADMIN_LISTEN must bind a concrete internal address, not a wildcard (%q)", addr)
	}
	ip := net.ParseIP(host)
	if ip == nil {
		// A hostname (e.g. an internal DNS name) is permitted; we cannot resolve
		// here, but we have rejected the obvious wildcard forms above.
		return nil
	}
	if ip.IsUnspecified() {
		return fmt.Errorf("config: ADMIN_LISTEN must not be the unspecified address (%q)", addr)
	}
	return nil
}

// ParseCIDRs parses a list of CIDR strings into *net.IPNet.
func ParseCIDRs(raw []string) ([]*net.IPNet, error) {
	nets := make([]*net.IPNet, 0, len(raw))
	for _, r := range raw {
		r = strings.TrimSpace(r)
		if r == "" {
			continue
		}
		// Allow bare IPs by appending the host-route mask.
		if !strings.Contains(r, "/") {
			if strings.Contains(r, ":") {
				r += "/128"
			} else {
				r += "/32"
			}
		}
		_, n, err := net.ParseCIDR(r)
		if err != nil {
			return nil, fmt.Errorf("config: invalid CIDR %q: %w", r, err)
		}
		nets = append(nets, n)
	}
	if len(nets) == 0 {
		return nil, fmt.Errorf("config: empty IP allowlist")
	}
	return nets, nil
}

func parseSecretKey(s string) ([]byte, error) {
	s = strings.TrimSpace(s)
	if s == "" {
		return nil, fmt.Errorf("config: ADMIN_SECRET_KEY is required (32-byte hex or base64)")
	}
	if b, err := hex.DecodeString(s); err == nil && len(b) == 32 {
		return b, nil
	}
	if b, err := base64.StdEncoding.DecodeString(s); err == nil && len(b) == 32 {
		return b, nil
	}
	if b, err := base64.RawStdEncoding.DecodeString(s); err == nil && len(b) == 32 {
		return b, nil
	}
	return nil, fmt.Errorf("config: ADMIN_SECRET_KEY must decode to exactly 32 bytes (hex or base64)")
}

func getEnvDefault(key, def string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return def
}

func splitTrim(s string) []string {
	parts := strings.Split(s, ",")
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		if t := strings.TrimSpace(p); t != "" {
			out = append(out, t)
		}
	}
	return out
}
