// Package server wires the backend's HTTP listener: the gin engine, its route
// groups, the per-request telemetry middleware and the start/stop lifecycle.
//
// The /api/v1 route groups (public, user, internal, admin) attach their endpoints
// to a stable structure; the /user group requires the X-User-ID identity header.
// The probes /healthz (liveness) and /readyz (database + session-cache readiness)
// are unauthenticated.
package server

import (
	"context"
	"database/sql"
	"errors"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"go.uber.org/zap"

	"scrabble/backend/internal/account"
	"scrabble/backend/internal/adminconsole"
	"scrabble/backend/internal/ads"
	"scrabble/backend/internal/banview"
	"scrabble/backend/internal/connector"
	"scrabble/backend/internal/engine"
	"scrabble/backend/internal/feedback"
	"scrabble/backend/internal/game"
	"scrabble/backend/internal/gamelimits"
	"scrabble/backend/internal/link"
	"scrabble/backend/internal/lobby"
	"scrabble/backend/internal/mynalogsync"
	"scrabble/backend/internal/notify"
	"scrabble/backend/internal/payments"
	"scrabble/backend/internal/ratewatch"
	"scrabble/backend/internal/render"
	"scrabble/backend/internal/robokassa"
	"scrabble/backend/internal/session"
	"scrabble/backend/internal/social"
	"scrabble/backend/internal/telemetry"
	"scrabble/backend/internal/yookassa"
)

// shutdownTimeout bounds how long Run waits for in-flight requests to finish
// during a graceful shutdown.
const shutdownTimeout = 10 * time.Second

// defaultPingTimeout bounds the /readyz database ping when Deps.PingTimeout is
// not set.
const defaultPingTimeout = 5 * time.Second

// Deps carries the runtime dependencies the HTTP layer needs.
type Deps struct {
	// Logger receives lifecycle, request and readiness diagnostics.
	Logger *zap.Logger
	// DB backs the /readyz database ping. A nil DB skips the database check.
	DB *sql.DB
	// PingTimeout bounds the /readyz database ping.
	PingTimeout time.Duration
	// SessionsReady reports whether the session cache has been warmed. A nil
	// func skips the session-readiness check.
	SessionsReady func() bool
	// Sessions, Accounts and Games are the identity, account and game-domain
	// services the REST handlers route to.
	Sessions *session.Service
	Accounts *account.Store
	Games    *game.Service
	// Feedback is the user-feedback domain service (the /api/v1/user/feedback
	// endpoints and the console section). A nil Feedback disables them.
	Feedback *feedback.Service
	// Social, Matchmaker, Invitations and Emails are the domain services
	// the REST handlers route to.
	Social      *social.Service
	Matchmaker  *lobby.Matchmaker
	Invitations *lobby.InvitationService
	Emails      *account.EmailService
	// Links drives account linking & merge: the /api/v1/user/link
	// endpoints. A nil Links disables them.
	Links *link.Service
	// Registry holds the resident dictionaries; the admin console reads its
	// versions and installs new ones uploaded through it. DictDir is the dictionary
	// directory the console stages uploads in and writes version subdirectories to.
	// A nil Registry disables the console.
	Registry *engine.Registry
	DictDir  string
	// Connector is the backend's Telegram connector client for operator broadcasts;
	// nil when BACKEND_CONNECTOR_ADDR is unset (broadcasts show a "not configured"
	// notice).
	Connector *connector.Client
	// RateWatch ingests the gateway's rate-limiter rejection reports: the
	// admin console's throttled view + the high-rate auto-flag. A nil RateWatch
	// disables the internal report endpoint and the console view.
	RateWatch *ratewatch.Watch
	// BanView mirrors the gateway's active IP bans for the admin console and
	// collects operator unban requests. A nil BanView disables the internal
	// ban-sync endpoint and the console's active-bans panel.
	BanView *banview.View
	// Ads is the advertising-banner domain service: campaign rotation feeding the
	// profile.get banner block, plus the banner admin console section. A nil Ads
	// omits the banner block and disables the banner console.
	Ads *ads.Service
	// Payments is the in-game currency domain service (wallet, benefits, catalog).
	// The data-foundation layer exposes only a reachability check; its user and
	// console routes are registered when the wallet surface lands. A nil Payments
	// omits them.
	Payments *payments.Service
	// GameLimits is the per-tier, per-kind active-game limit config, cached in memory. The
	// game domain reads it through game.Service (SetGameLimits) for the new-game gate; the admin
	// console reads and edits it here. A nil GameLimits omits the limits console section.
	GameLimits *gamelimits.Service
	// Notifier publishes live-event intents — here the banner-eligibility re-poll
	// signal the banner/hint/role console actions emit. A nil Notifier discards
	// them (notify.Nop).
	Notifier notify.Publisher
	// ExportSignKey signs the finished-game export download URLs (export.go). An
	// empty key leaves the export-URL endpoints answering 503/404.
	ExportSignKey string
	// Renderer is the image-render sidecar client for the PNG export artifact. A
	// nil Renderer makes the PNG download answer 404 (the GCG artifact still works).
	Renderer *render.Client
	// YooKassa configures the direct-rail (RUB) provider — one merchant shop per channel; an empty
	// set leaves the order and notification endpoints unregistered and falls the direct rail back to
	// Robokassa.
	YooKassa yookassa.Shops
	// YooKassaVatCode is the VAT rate code stamped on every fiscal receipt line (54-ФЗ tag 1199).
	// Unset sends no receipt at all — the state a merchant outside 54-ФЗ runs in.
	YooKassaVatCode int
	// PublicBaseURL is the canonical https origin the payment return URL is built from. It is
	// required whenever YooKassa is configured.
	PublicBaseURL string
	// Robokassa configures the retired direct-rail provider — one merchant shop per channel. No
	// deployment sets it, so the set is empty and the direct rail resolves to YooKassa; it stays
	// wired so restoring the rail is a credentials change (backend/internal/robokassa/README.md).
	Robokassa robokassa.Shops
	// MyNalog drives the professional-income tax export from the admin console. A nil MyNalog
	// leaves the console section unregistered, which is how a deployment without an encryption key
	// runs — the rail is dormant rather than half-present.
	MyNalog *mynalogsync.Exporter
}

// Server owns the gin engine, the underlying HTTP server and the readiness
// dependencies.
type Server struct {
	log           *zap.Logger
	http          *http.Server
	db            *sql.DB
	pingTimeout   time.Duration
	sessionsReady func() bool

	sessions    *session.Service
	accounts    *account.Store
	games       *game.Service
	feedback    *feedback.Service
	social      *social.Service
	matchmaker  *lobby.Matchmaker
	invitations *lobby.InvitationService
	emails      *account.EmailService
	links       *link.Service
	registry    *engine.Registry
	dictDir     string
	connector   *connector.Client
	ratewatch   *ratewatch.Watch
	banview     *banview.View
	ads         *ads.Service
	payments    *payments.Service
	gamelimits  *gamelimits.Service
	yookassa    yookassa.Shops
	vatCode     int
	publicURL   string
	robokassa   robokassa.Shops
	mynalog     *mynalogsync.Exporter
	notifier    notify.Publisher
	console     *adminconsole.Renderer
	exportKey   []byte
	renderer    *render.Client

	public   *gin.RouterGroup
	user     *gin.RouterGroup
	internal *gin.RouterGroup
}

// New returns a Server that will listen on addr. It installs the recovery and
// telemetry middleware, the infrastructure probes, and the /api/v1 route groups.
func New(addr string, deps Deps) *Server {
	log := deps.Logger
	if log == nil {
		log = zap.NewNop()
	}
	pingTimeout := deps.PingTimeout
	if pingTimeout <= 0 {
		pingTimeout = defaultPingTimeout
	}

	gin.SetMode(gin.ReleaseMode)
	engine := gin.New()
	engine.Use(gin.Recovery())
	engine.Use(telemetry.Middleware(log))

	notifier := deps.Notifier
	if notifier == nil {
		notifier = notify.Nop{}
	}

	s := &Server{
		log:           log,
		db:            deps.DB,
		pingTimeout:   pingTimeout,
		sessionsReady: deps.SessionsReady,
		sessions:      deps.Sessions,
		accounts:      deps.Accounts,
		games:         deps.Games,
		feedback:      deps.Feedback,
		social:        deps.Social,
		matchmaker:    deps.Matchmaker,
		invitations:   deps.Invitations,
		emails:        deps.Emails,
		links:         deps.Links,
		registry:      deps.Registry,
		dictDir:       deps.DictDir,
		connector:     deps.Connector,
		ratewatch:     deps.RateWatch,
		banview:       deps.BanView,
		ads:           deps.Ads,
		payments:      deps.Payments,
		gamelimits:    deps.GameLimits,
		yookassa:      deps.YooKassa,
		vatCode:       deps.YooKassaVatCode,
		publicURL:     deps.PublicBaseURL,
		robokassa:     deps.Robokassa,
		mynalog:       deps.MyNalog,
		notifier:      notifier,
		renderer:      deps.Renderer,
		http:          &http.Server{Addr: addr, Handler: engine},
	}
	if deps.ExportSignKey != "" {
		s.exportKey = []byte(deps.ExportSignKey)
	}
	s.registerProbes(engine)
	s.registerAPIGroups(engine)
	s.registerRoutes()
	s.registerConsole(engine)
	return s
}

// registerProbes installs the unauthenticated infrastructure probes: /healthz
// reports process liveness and /readyz reports readiness to serve traffic
// (database reachable and session cache warmed).
func (s *Server) registerProbes(engine *gin.Engine) {
	engine.GET("/healthz", func(c *gin.Context) { c.String(http.StatusOK, "ok") })
	engine.GET("/readyz", s.readyz)
}

// readyz reports 200 only when the database answers a bounded ping and the
// session cache is warmed; otherwise 503.
func (s *Server) readyz(c *gin.Context) {
	if s.db != nil {
		ctx, cancel := context.WithTimeout(c.Request.Context(), s.pingTimeout)
		defer cancel()
		if err := s.db.PingContext(ctx); err != nil {
			s.log.Warn("readiness: database ping failed", zap.Error(err))
			c.String(http.StatusServiceUnavailable, "database unavailable")
			return
		}
	}
	if s.sessionsReady != nil && !s.sessionsReady() {
		c.String(http.StatusServiceUnavailable, "sessions not ready")
		return
	}
	c.String(http.StatusOK, "ok")
}

// registerAPIGroups wires the /api/v1 route groups. They are populated by the
// stages that add their first endpoint; the /user group requires X-User-ID,
// which the gateway injects after resolving a session.
func (s *Server) registerAPIGroups(engine *gin.Engine) {
	v1 := engine.Group("/api/v1")
	s.public = v1.Group("/public")
	s.user = v1.Group("/user")
	s.user.Use(RequireUserID())
	// Capture the gateway-injected trusted platform (X-Platform) into the request context,
	// so the payments gate can read it via platform(c). Optional: an untrusted session simply
	// carries no platform and is treated as view-only. Never rejects.
	s.user.Use(platformContext())
	// The suspension gate runs after identity is established: a blocked account is refused on
	// every user route (except the block-status probe) so the UI can show the blocked screen.
	s.user.Use(s.requireNotSuspended())
	s.internal = v1.Group("/internal")
}

// PublicGroup returns the unauthenticated public route group.
func (s *Server) PublicGroup() *gin.RouterGroup { return s.public }

// UserGroup returns the authenticated user route group (requires X-User-ID).
func (s *Server) UserGroup() *gin.RouterGroup { return s.user }

// InternalGroup returns the gateway-facing internal route group.
func (s *Server) InternalGroup() *gin.RouterGroup { return s.internal }

// Social returns the social domain service for the handlers.
func (s *Server) Social() *social.Service { return s.social }

// Matchmaker returns the in-memory matchmaking pool for the handlers.
func (s *Server) Matchmaker() *lobby.Matchmaker { return s.matchmaker }

// Invitations returns the friend-game invitation service for the handlers.
func (s *Server) Invitations() *lobby.InvitationService { return s.invitations }

// Emails returns the email confirm-code service for the handlers.
func (s *Server) Emails() *account.EmailService { return s.emails }

// Handler returns the underlying HTTP handler. It lets tests drive the server
// without binding a socket and lets callers compose the backend behind
// another listener.
func (s *Server) Handler() http.Handler { return s.http.Handler }

// Run starts the listener and blocks until ctx is cancelled, then shuts the
// server down gracefully within shutdownTimeout. It returns the first error
// that is not the expected http.ErrServerClosed.
func (s *Server) Run(ctx context.Context) error {
	errc := make(chan error, 1)
	go func() {
		s.log.Info("http listener starting", zap.String("addr", s.http.Addr))
		errc <- s.http.ListenAndServe()
	}()

	select {
	case err := <-errc:
		if errors.Is(err, http.ErrServerClosed) {
			return nil
		}
		return err
	case <-ctx.Done():
		s.log.Info("http listener stopping")
		shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
		defer cancel()
		return s.http.Shutdown(shutdownCtx)
	}
}
