package api

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"net"
	"net/http"
	"strings"
	"time"

	"github.com/owncord/server/auth"
	"github.com/owncord/server/db"
	"github.com/owncord/server/permissions"
)

// contextKey is an unexported type for context keys in this package.
type contextKey int

const (
	// UserKey is the context key for the authenticated *db.User.
	UserKey contextKey = iota
	// SessionKey is the context key for the authenticated *db.Session.
	SessionKey
	// RoleKey is the context key for the *db.Role of the authenticated user.
	RoleKey
)

// AuthMiddleware reads the "Authorization: Bearer <token>" header, validates
// the session, and injects the user and session into the request context.
// Returns 401 if the token is missing, invalid, or the session is expired.
func AuthMiddleware(database *db.DB) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			token, ok := auth.ExtractBearerToken(r)
			if !ok {
				writeJSON(w, http.StatusUnauthorized, errorResponse{
					Error:   "UNAUTHORIZED",
					Message: "missing or invalid authorization header",
				})
				return
			}

			hash := auth.HashToken(token)
			// Resolve the bearer token to a principal. A login session is matched
			// first (existing behavior unchanged); an API token is the fallback.
			user, role, sess, err := auth.ResolveTokenHash(r.Context(), database, hash)
			switch {
			case errors.Is(err, auth.ErrTokenExpired):
				// Clean up the expired login session in the background. The request
				// ctx is cancelled once the 401 is written, so detach cancellation.
				cleanupCtx := context.WithoutCancel(r.Context())
				go func(h string) {
					if err := database.DeleteSession(cleanupCtx, h); err != nil {
						slog.WarnContext(cleanupCtx, "expired session cleanup failed", "error", err)
					}
				}(hash)
				writeJSON(w, http.StatusUnauthorized, errorResponse{
					Error:   "UNAUTHORIZED",
					Message: "session has expired",
				})
				return
			case errors.Is(err, auth.ErrUserNotFound):
				writeJSON(w, http.StatusUnauthorized, errorResponse{
					Error:   "UNAUTHORIZED",
					Message: "user not found",
				})
				return
			case errors.Is(err, auth.ErrRoleNotFound):
				writeJSON(w, http.StatusUnauthorized, errorResponse{
					Error:   "UNAUTHORIZED",
					Message: "role not found",
				})
				return
			case err != nil:
				// ErrTokenNotFound or a wrapped DB error. A DB outage is not a bad
				// token — log it so it's distinguishable from ordinary 401s.
				if !errors.Is(err, auth.ErrTokenNotFound) {
					slog.ErrorContext(r.Context(), "auth: token resolution failed", "error", err)
				}
				writeJSON(w, http.StatusUnauthorized, errorResponse{
					Error:   "UNAUTHORIZED",
					Message: "invalid or expired session",
				})
				return
			}

			// Reject effectively-banned users before any further processing.
			if auth.IsEffectivelyBanned(user) {
				writeJSON(w, http.StatusForbidden, errorResponse{
					Error:   "FORBIDDEN",
					Message: "your account has been suspended",
				})
				return
			}

			// Touch last-used — non-fatal. A login session is touched inline as
			// before; an API-token principal (sess == nil) is touched off the hot
			// path so it never adds latency to bot/CI traffic.
			if sess != nil {
				if err := database.TouchSession(r.Context(), hash); err != nil {
					slog.Warn("failed to touch session", "error", err, "user_id", user.ID)
				}
			} else {
				touchCtx := context.WithoutCancel(r.Context())
				go func(h string) {
					if err := database.TouchAPIToken(touchCtx, h); err != nil {
						slog.WarnContext(touchCtx, "failed to touch api token", "error", err)
					}
				}(hash)
			}

			ctx := context.WithValue(r.Context(), UserKey, user)
			ctx = context.WithValue(ctx, SessionKey, sess) // nil for API-token principals; consumers guard nil
			ctx = context.WithValue(ctx, RoleKey, role)
			next.ServeHTTP(w, r.WithContext(ctx))
		})
	}
}

// RequirePermission returns middleware gating a route on SERVER-WIDE role
// permissions. Returns 403 if the user lacks them.
//
// Scope contract — this is the whole reason the middleware and the service
// layer look like two permission systems:
//   - It consults the role bitfield only. Channel overrides are NOT applied,
//     because a route reaching this middleware has no channel id to resolve
//     them against, and a per-channel allow must never open a server-wide gate.
//   - Anything channel-scoped belongs in the service layer behind
//     permissions.Checker (via svc.Permissions), which resolves overrides.
//   - ADMINISTRATOR bypasses; multi-bit masks require ALL bits.
//
// The rule itself lives in permissions.HasServerPerm so no call site can
// re-derive it.
func RequirePermission(perm int64) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			role, ok := r.Context().Value(RoleKey).(*db.Role)
			if !ok || role == nil {
				writeJSON(w, http.StatusForbidden, errorResponse{
					Error:   "FORBIDDEN",
					Message: "insufficient permissions",
				})
				return
			}

			if !permissions.HasServerPerm(role.Permissions, perm) {
				writeJSON(w, http.StatusForbidden, errorResponse{
					Error:   "FORBIDDEN",
					Message: "insufficient permissions",
				})
				return
			}

			next.ServeHTTP(w, r)
		})
	}
}

// RateLimitMiddleware returns middleware that limits requests per IP using the
// provided RateLimiter. The client IP is resolved via clientIPWithProxies using
// the supplied trustedProxies CIDRs — pass nil to always use RemoteAddr.
// Returns 429 with Retry-After when the limit is exceeded.
func RateLimitMiddleware(limiter *auth.RateLimiter, limit int, window time.Duration, trustedProxies ...[]string) func(http.Handler) http.Handler {
	return rateLimitMiddlewareWithPrefix(limiter, "", limit, window, trustedProxies...)
}

func rateLimitMiddlewareWithPrefix(limiter *auth.RateLimiter, prefix string, limit int, window time.Duration, trustedProxies ...[]string) func(http.Handler) http.Handler {
	var proxies []string
	if len(trustedProxies) > 0 {
		proxies = trustedProxies[0]
	}
	proxyNets := parseCIDRList(proxies) // W3-3a: parse once at construction
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ip := clientIPWithProxies(r, proxyNets)
			key := prefix + ip

			if !limiter.Allow(key, limit, window) {
				w.Header().Set("Retry-After", fmt.Sprintf("%d", int(window.Seconds())))
				writeJSON(w, http.StatusTooManyRequests, errorResponse{
					Error:   "RATE_LIMITED",
					Message: "too many requests, please slow down",
				})
				return
			}

			next.ServeHTTP(w, r)
		})
	}
}

// ─── Helpers ──────────────────────────────────────────────────────────────────

// clientIP returns the connecting IP from RemoteAddr, ignoring any proxy
// headers. It is safe to use for audit logging and lockout keys where proxy
// header trust has not been established. For rate-limiting with proxy support
// use clientIPWithProxies.
func clientIP(r *http.Request) string {
	return clientIPWithProxies(r, nil)
}

// clientIPWithProxies returns the real client IP for rate-limiting purposes.
//
// Security model:
//   - Always parse the actual connecting address from r.RemoteAddr.
//   - Only honour X-Real-IP or X-Forwarded-For if the connecting address matches
//     one of the trustedNets. This prevents clients from forging their IP to
//     bypass rate limits.
//   - If trustedNets is empty (the default), RemoteAddr is always used.
//
// trustedNets is the pre-parsed trusted-proxy list — parse the configured CIDR
// strings ONCE at middleware/handler construction with parseCIDRList (W3-3a);
// never parse on the request path.
func clientIPWithProxies(r *http.Request, trustedNets []*net.IPNet) string {
	remoteHost, _, err := net.SplitHostPort(r.RemoteAddr)
	if err != nil {
		// RemoteAddr without port (e.g. Unix socket or test stub) — use as-is.
		remoteHost = r.RemoteAddr
	}

	if len(trustedNets) == 0 {
		return remoteHost
	}

	if !ipInNets(remoteHost, trustedNets) {
		return remoteHost
	}

	// Prefer X-Real-IP when coming from a trusted proxy.
	// BUG-112: Validate extracted IP to prevent spoofed rate-limit keys.
	if xri := strings.TrimSpace(r.Header.Get("X-Real-IP")); xri != "" {
		if net.ParseIP(xri) != nil {
			return xri
		}
	}

	// Fall back to X-Forwarded-For, walking from the RIGHT and skipping entries
	// that are themselves trusted proxies. The first non-trusted, valid address
	// is the real client. Taking the leftmost entry (BUG-112) would trust a
	// client-supplied value: a client can prepend a spoofed IP
	// (`X-Forwarded-For: <spoofed>, <real>`) that the proxy then appends to,
	// letting it forge per-IP rate-limit and lockout keys.
	if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
		parts := strings.Split(xff, ",")
		leftmostValid := ""
		for i := len(parts) - 1; i >= 0; i-- {
			candidate := strings.TrimSpace(parts[i])
			if candidate == "" || net.ParseIP(candidate) == nil {
				continue
			}
			leftmostValid = candidate
			if ipInNets(candidate, trustedNets) {
				continue // our own proxy hop, keep walking left
			}
			return candidate
		}
		// Every entry fell inside trustedCIDRs — a config that covers client
		// networks too (e.g. trusted_proxies: 10.0.0.0/8 with LAN clients).
		// Falling back to RemoteAddr here would collapse ALL clients behind
		// the proxy into one rate-limit/lockout bucket, so one user's failed
		// logins would lock out everyone. The leftmost valid entry is the
		// furthest-upstream hop — the best distinct per-client key available
		// under such a config. trusted_proxies must list only proxy hops;
		// startup validation warns about entries that cannot be proxies.
		if leftmostValid != "" {
			return leftmostValid
		}
	}

	return remoteHost
}

// parseCIDRList parses CIDR strings into networks, skipping invalid entries
// with a warning — a misconfigured entry must not take the server down. It is
// called once per middleware/handler at construction (startup), never on the
// request path (W3-3a).
func parseCIDRList(cidrs []string) []*net.IPNet {
	nets := make([]*net.IPNet, 0, len(cidrs))
	for _, c := range cidrs {
		_, n, err := net.ParseCIDR(c)
		if err != nil {
			slog.Warn("ignoring invalid CIDR entry (use address/prefix notation, e.g. 10.0.0.1/32)",
				"cidr", c, "error", err)
			continue
		}
		nets = append(nets, n)
	}
	return nets
}

// ipInNets reports whether ipStr (a plain IP, no port) falls inside any of
// the parsed networks.
func ipInNets(ipStr string, nets []*net.IPNet) bool {
	ip := net.ParseIP(ipStr)
	if ip == nil {
		return false
	}
	for _, n := range nets {
		if n.Contains(ip) {
			return true
		}
	}
	return false
}

// AdminIPRestrict returns middleware that blocks requests from IPs not in the
// allowed CIDR list. Returns 403 Forbidden for disallowed IPs. If the CIDR
// list is empty, all requests are allowed (no restriction).
//
// trustedProxyCIDRs specifies which connecting IPs are trusted reverse proxies.
// When the connecting IP matches a trusted proxy, the real client IP is read
// from X-Real-IP or X-Forwarded-For headers (BUG-116).
//
// Both lists are parsed once at construction (W3-3a); invalid entries are
// skipped with a warning. A non-empty allowedCIDRs list whose entries are all
// invalid yields zero networks — nothing matches, so access is denied (fail
// closed), same as before the hoist.
func AdminIPRestrict(allowedCIDRs, trustedProxyCIDRs []string) func(http.Handler) http.Handler {
	allowedNets := parseCIDRList(allowedCIDRs)
	proxyNets := parseCIDRList(trustedProxyCIDRs)
	restrict := len(allowedCIDRs) > 0
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if !restrict {
				next.ServeHTTP(w, r)
				return
			}

			ip := clientIPWithProxies(r, proxyNets)
			if !ipInNets(ip, allowedNets) {
				writeJSON(w, http.StatusForbidden, errorResponse{
					Error:   "FORBIDDEN",
					Message: "access denied",
				})
				return
			}
			next.ServeHTTP(w, r)
		})
	}
}

// SecurityHeadersWithTLS returns middleware that sets a standard suite of
// defensive HTTP response headers. When tlsMode is non-empty (TLS is enabled),
// the Strict-Transport-Security header is also set.
//
// Header choices:
//   - X-Content-Type-Options: nosniff          — prevent MIME-type sniffing
//   - X-Frame-Options: DENY                    — block clickjacking via iframes
//   - X-XSS-Protection: 0                      — disable legacy XSS filter; rely on CSP
//   - Referrer-Policy: strict-origin-when-cross-origin
//   - Content-Security-Policy: default-src 'self'
//   - Permissions-Policy: camera=(), microphone=(), geolocation=()
//   - Cache-Control: no-store                  — prevent sensitive data caching
//   - Strict-Transport-Security (when TLS enabled)
func SecurityHeadersWithTLS(tlsMode string) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			h := w.Header()
			h.Set("X-Content-Type-Options", "nosniff")
			h.Set("X-Frame-Options", "DENY")
			h.Set("X-XSS-Protection", "0")
			h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
			h.Set("Content-Security-Policy", "default-src 'self'")
			h.Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
			h.Set("Cache-Control", "no-store")
			if tlsMode != "" {
				h.Set("Strict-Transport-Security", fmt.Sprintf("max-age=%d; includeSubDomains", hstsMaxAgeSeconds))
			}
			next.ServeHTTP(w, r)
		})
	}
}

// MaxBodySize wraps r.Body with http.MaxBytesReader so that reads beyond
// maxBytes return an error. This prevents clients from exhausting server memory
// by sending arbitrarily large request bodies.
//
// Usage in the router:
//
//	r.Use(MaxBodySize(1 << 20)) // 1 MiB default for API endpoints
//
// Upload endpoints that need a higher limit should apply their own
// http.MaxBytesReader or a route-scoped middleware with a larger value.
func MaxBodySize(maxBytes int64) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
			next.ServeHTTP(w, r)
		})
	}
}

// MaxBodySizeUnless is like MaxBodySize but skips the limit for paths that
// match any of the given prefixes. Exempted paths apply their own limit via
// route-scoped middleware.
func MaxBodySizeUnless(maxBytes int64, exemptPrefixes ...string) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			exempt := false
			for _, prefix := range exemptPrefixes {
				if strings.HasPrefix(r.URL.Path, prefix) {
					exempt = true
					break
				}
			}
			if !exempt {
				r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
			}
			next.ServeHTTP(w, r)
		})
	}
}

// errorResponse is the standard error JSON shape.
type errorResponse struct {
	Error   string `json:"error"`
	Message string `json:"message"`
}
