// Package netsettings holds the DB-backed network settings the request path
// needs. Today that's the trusted reverse-proxy depth used to pull a real
// client address out of X-Forwarded-For (#2453).
//
// Values are cached under an RWMutex and refreshed on write. That isn't an
// optimisation: auth.ClientIP runs in the RequireUser middleware for every
// authenticated request, so a per-request query here would put the database
// on the critical path of the entire API.
package netsettings

import (
	"context"
	"errors"
	"log/slog"
	"sync"

	"github.com/jackc/pgx/v5/pgxpool"

	"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)

const (
	// DefaultTrustedProxyHops mirrors migration 0053's column default. One
	// proxy, because anything publicly reachable needs a TLS terminator in
	// front of it.
	DefaultTrustedProxyHops = 1
	// MaxTrustedProxyHops mirrors the CHECK in migration 0053.
	MaxTrustedProxyHops = 10
)

// ErrHopsOutOfRange is returned by SetHops for values the CHECK would reject,
// so the API layer can answer 400 instead of surfacing a constraint violation.
var ErrHopsOutOfRange = errors.New("trusted proxy hops must be between 0 and 10")

// Service caches the network settings and owns their persistence.
type Service struct {
	pool   *pgxpool.Pool
	logger *slog.Logger

	mu   sync.RWMutex
	hops int
}

// New loads the settings once and caches them.
//
// It ALWAYS returns a usable Service, even alongside a non-nil error. The
// value it holds sits on the authenticated request path, so a boot-time
// database hiccup must degrade to the default rather than take every request
// down with it (rule #131). The error is returned so the caller can log that
// the cache holds a default rather than stored state.
func New(ctx context.Context, pool *pgxpool.Pool, logger *slog.Logger) (*Service, error) {
	s := &Service{pool: pool, logger: logger, hops: DefaultTrustedProxyHops}
	if pool == nil {
		return s, nil
	}
	row, err := dbq.New(pool).GetNetworkSettings(ctx)
	if err != nil {
		return s, err
	}
	s.hops = int(row.TrustedProxyHops)
	return s, nil
}

// Hops returns the cached trusted-proxy depth.
//
// Nil-safe: test contexts construct routers without this service, and a
// missing setting should mean "trust nothing" rather than a panic in
// middleware.
func (s *Service) Hops() int {
	if s == nil {
		return 0
	}
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.hops
}

// SetHops persists a new depth and refreshes the cache, so an admin change
// takes effect on the next request with no restart (rule #25).
func (s *Service) SetHops(ctx context.Context, hops int) error {
	// Range first, availability second. The argument is wrong regardless of
	// whether the database is reachable, and the distinction is user-visible:
	// this ordering answers 400 for a bad value, where the reverse would
	// report 500 and blame the server for the caller's input.
	if hops < 0 || hops > MaxTrustedProxyHops {
		return ErrHopsOutOfRange
	}
	if s == nil || s.pool == nil {
		// Mirrors Hops()'s nil-tolerance: handlers can be constructed without
		// this service in tests, and a write attempt there should be an error
		// rather than a panic in an HTTP handler.
		return errors.New("network settings unavailable")
	}
	row, err := dbq.New(s.pool).UpdateTrustedProxyHops(ctx, int32(hops))
	if err != nil {
		return err
	}
	s.mu.Lock()
	s.hops = int(row.TrustedProxyHops)
	s.mu.Unlock()
	// Worth a line in the log: this changes how much of a client-supplied
	// header the server believes, so an operator debugging odd addresses in
	// the sessions list wants to see when it last moved.
	if s.logger != nil {
		s.logger.Info("netsettings: trusted proxy hops updated", "hops", hops)
	}
	return nil
}
