// Package server hosts the backend HTTP listener and the route
// configuration that wires the documented `backend/openapi.yaml`
// contract against the per-domain handler sets.
//
// router.go is the single place where route groups, group-scoped
// middleware, and per-domain handlers are mounted. Domain handlers
// hold their own Service references; the routing layout is stable.
package server

import (
	"net/http"
	"sync"

	"galaxy/backend/internal/server/httperr"
	"galaxy/backend/internal/server/middleware/basicauth"
	"galaxy/backend/internal/server/middleware/geocounter"
	"galaxy/backend/internal/server/middleware/logging"
	"galaxy/backend/internal/server/middleware/metrics"
	"galaxy/backend/internal/server/middleware/panicrecovery"
	"galaxy/backend/internal/server/middleware/requestid"
	"galaxy/backend/internal/server/middleware/userid"
	"galaxy/backend/internal/telemetry"

	"github.com/gin-gonic/gin"
	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
	"go.uber.org/zap"
)

const (
	// otelServerName is the operation-name attribute attached to spans
	// produced by otelgin.
	otelServerName = "galaxy-backend"

	// adminBasicAuthRealm is the realm advertised on `WWW-Authenticate`
	// responses from the admin surface.
	adminBasicAuthRealm = "galaxy-admin"
)

var configureGinModeOnce sync.Once

// RouterDependencies aggregates every collaborator required to build the
// backend HTTP handler chain.
//
// Logger, Telemetry, and Ready come from the process bootstrap.
// AdminVerifier gates the admin surface; production wires
// `*admin.Service`. The handler-set fields are allowed to be nil —
// NewRouter substitutes a freshly-constructed placeholder set so
// callers can supply only the slices they want to override.
type RouterDependencies struct {
	Logger *zap.Logger
	Telemetry *telemetry.Runtime
	Ready func() bool
	AdminVerifier basicauth.Verifier

	// GeoCounter, when non-nil, is mounted as middleware on the
	// `/api/v1/user/*` route group so that every authenticated request
	// dispatches a fire-and-forget counter increment. A nil value
	// leaves the route group untouched, which keeps existing tests
	// that build the router without geo wiring working as before.
	GeoCounter geocounter.Service

	PublicAuth *PublicAuthHandlers
	UserAccount *UserAccountHandlers
	UserLobbyGames *UserLobbyGamesHandlers
	UserLobbyApplications *UserLobbyApplicationsHandlers
	UserLobbyInvites *UserLobbyInvitesHandlers
	UserLobbyMemberships *UserLobbyMembershipsHandlers
	UserLobbyMy *UserLobbyMyHandlers
	UserLobbyRaceNames *UserLobbyRaceNamesHandlers
	UserGames *UserGamesHandlers
	UserMail *UserMailHandlers
	UserSessions *UserSessionsHandlers
	AdminAdminAccounts *AdminAdminAccountsHandlers
	AdminUsers *AdminUsersHandlers
	AdminGames *AdminGamesHandlers
	AdminRuntimes *AdminRuntimesHandlers
	AdminEngineVersions *AdminEngineVersionsHandlers
	AdminMail *AdminMailHandlers
	AdminDiplomail *AdminDiplomailHandlers
	AdminNotifications *AdminNotificationsHandlers
	AdminGeo *AdminGeoHandlers
	InternalSessions *InternalSessionsHandlers
	InternalUsers *InternalUsersHandlers
}

// NewRouter constructs the backend gin engine wired with the documented
// middleware chain and every placeholder route from `backend/openapi.yaml`.
// The returned handler is safe to pass into Server.NewServer.
func NewRouter(deps RouterDependencies) (http.Handler, error) {
	configureGinModeOnce.Do(func() {
		gin.SetMode(gin.ReleaseMode)
	})

	if deps.Logger == nil {
		deps.Logger = zap.NewNop()
	}

	deps = withDefaultHandlers(deps)

	logger := deps.Logger.Named("http")

	var instruments *metrics.Instruments
	if deps.Telemetry != nil {
		var err error
		instruments, err = metrics.NewInstruments(deps.Telemetry.MeterProvider().Meter(otelServerName))
		if err != nil {
			return nil, err
		}
	}

	router := gin.New()
	router.HandleMethodNotAllowed = true

	router.Use(requestid.Middleware())
	router.Use(panicrecovery.Middleware(logger))
	router.Use(otelgin.Middleware(otelServerName))
	router.Use(logging.Middleware(logger))

	router.GET("/healthz", metrics.Middleware(instruments, metrics.GroupProbes), handleHealthz)
	router.GET("/readyz", metrics.Middleware(instruments, metrics.GroupProbes), handleReadyz(deps.Ready))

	registerPublicRoutes(router, instruments, deps)
	registerUserRoutes(router, instruments, deps)
	registerAdminRoutes(router, instruments, deps)
	registerInternalRoutes(router, instruments, deps)

	router.NoMethod(func(c *gin.Context) {
		if allow := allowedMethodsForPath(c.Request.URL.Path); allow != "" {
			c.Header("Allow", allow)
		}
		httperr.Abort(c, http.StatusMethodNotAllowed, httperr.CodeMethodNotAllowed, "request method is not allowed for this route")
	})
	router.NoRoute(func(c *gin.Context) {
		httperr.Abort(c, http.StatusNotFound, httperr.CodeNotFound, "resource was not found")
	})

	return router, nil
}

func withDefaultHandlers(deps RouterDependencies) RouterDependencies {
	if deps.PublicAuth == nil {
		deps.PublicAuth = NewPublicAuthHandlers(nil, deps.Logger)
	}
	if deps.UserAccount == nil {
		deps.UserAccount = NewUserAccountHandlers(nil, deps.Logger)
	}
	if deps.UserLobbyGames == nil {
		deps.UserLobbyGames = NewUserLobbyGamesHandlers(nil, deps.Logger)
	}
	if deps.UserLobbyApplications == nil {
		deps.UserLobbyApplications = NewUserLobbyApplicationsHandlers(nil, deps.Logger)
	}
	if deps.UserLobbyInvites == nil {
		deps.UserLobbyInvites = NewUserLobbyInvitesHandlers(nil, deps.Logger)
	}
	if deps.UserLobbyMemberships == nil {
		deps.UserLobbyMemberships = NewUserLobbyMembershipsHandlers(nil, deps.Logger)
	}
	if deps.UserLobbyMy == nil {
		deps.UserLobbyMy = NewUserLobbyMyHandlers(nil, deps.Logger)
	}
	if deps.UserLobbyRaceNames == nil {
		deps.UserLobbyRaceNames = NewUserLobbyRaceNamesHandlers(nil, deps.Logger)
	}
	if deps.UserGames == nil {
		deps.UserGames = NewUserGamesHandlers(nil, nil, deps.Logger)
	}
	if deps.UserMail == nil {
		deps.UserMail = NewUserMailHandlers(nil, nil, nil, deps.Logger)
	}
	if deps.UserSessions == nil {
		deps.UserSessions = NewUserSessionsHandlers(nil, deps.Logger)
	}
	if deps.AdminAdminAccounts == nil {
		deps.AdminAdminAccounts = NewAdminAdminAccountsHandlers(nil, deps.Logger)
	}
	if deps.AdminUsers == nil {
		deps.AdminUsers = NewAdminUsersHandlers(nil, deps.Logger)
	}
	if deps.AdminGames == nil {
		deps.AdminGames = NewAdminGamesHandlers(nil, deps.Logger)
	}
	if deps.AdminRuntimes == nil {
		deps.AdminRuntimes = NewAdminRuntimesHandlers(nil, deps.Logger)
	}
	if deps.AdminEngineVersions == nil {
		deps.AdminEngineVersions = NewAdminEngineVersionsHandlers(nil, deps.Logger)
	}
	if deps.AdminMail == nil {
		deps.AdminMail = NewAdminMailHandlers(nil, deps.Logger)
	}
	if deps.AdminDiplomail == nil {
		deps.AdminDiplomail = NewAdminDiplomailHandlers(nil, deps.Logger)
	}
	if deps.AdminNotifications == nil {
		deps.AdminNotifications = NewAdminNotificationsHandlers(nil, deps.Logger)
	}
	if deps.AdminGeo == nil {
		deps.AdminGeo = NewAdminGeoHandlers(nil, deps.Logger)
	}
	if deps.InternalSessions == nil {
		deps.InternalSessions = NewInternalSessionsHandlers(nil, deps.Logger)
	}
	if deps.InternalUsers == nil {
		deps.InternalUsers = NewInternalUsersHandlers(nil, deps.Logger)
	}
	return deps
}

func registerPublicRoutes(router *gin.Engine, instruments *metrics.Instruments, deps RouterDependencies) {
	group := router.Group("/api/v1/public")
	group.Use(metrics.Middleware(instruments, metrics.GroupPublic))

	auth := group.Group("/auth")
	auth.POST("/send-email-code", deps.PublicAuth.SendEmailCode())
	auth.POST("/confirm-email-code", deps.PublicAuth.ConfirmEmailCode())
}

func registerUserRoutes(router *gin.Engine, instruments *metrics.Instruments, deps RouterDependencies) {
	group := router.Group("/api/v1/user")
	group.Use(metrics.Middleware(instruments, metrics.GroupUser))
	group.Use(userid.Middleware())
	if deps.GeoCounter != nil {
		group.Use(geocounter.Middleware(deps.GeoCounter))
	}

	account := group.Group("/account")
	account.GET("", deps.UserAccount.Get())
	account.PATCH("/profile", deps.UserAccount.UpdateProfile())
	account.PATCH("/settings", deps.UserAccount.UpdateSettings())
	account.POST("/delete", deps.UserAccount.Delete())

	lobbyGroup := group.Group("/lobby")
	games := lobbyGroup.Group("/games")
	games.GET("", deps.UserLobbyGames.List())
	games.POST("", deps.UserLobbyGames.Create())
	games.GET("/:game_id", deps.UserLobbyGames.Get())
	games.PATCH("/:game_id", deps.UserLobbyGames.Update())
	games.POST("/:game_id/open-enrollment", deps.UserLobbyGames.OpenEnrollment())
	games.POST("/:game_id/ready-to-start", deps.UserLobbyGames.ReadyToStart())
	games.POST("/:game_id/start", deps.UserLobbyGames.Start())
	games.POST("/:game_id/pause", deps.UserLobbyGames.Pause())
	games.POST("/:game_id/resume", deps.UserLobbyGames.Resume())
	games.POST("/:game_id/cancel", deps.UserLobbyGames.Cancel())
	games.POST("/:game_id/retry-start", deps.UserLobbyGames.RetryStart())

	games.POST("/:game_id/applications", deps.UserLobbyApplications.Submit())
	games.POST("/:game_id/applications/:application_id/approve", deps.UserLobbyApplications.Approve())
	games.POST("/:game_id/applications/:application_id/reject", deps.UserLobbyApplications.Reject())

	games.POST("/:game_id/invites", deps.UserLobbyInvites.Issue())
	games.POST("/:game_id/invites/:invite_id/redeem", deps.UserLobbyInvites.Redeem())
	games.POST("/:game_id/invites/:invite_id/decline", deps.UserLobbyInvites.Decline())
	games.POST("/:game_id/invites/:invite_id/revoke", deps.UserLobbyInvites.Revoke())

	games.GET("/:game_id/memberships", deps.UserLobbyMemberships.List())
	games.POST("/:game_id/memberships/:membership_id/remove", deps.UserLobbyMemberships.Remove())
	games.POST("/:game_id/memberships/:membership_id/block", deps.UserLobbyMemberships.Block())

	my := lobbyGroup.Group("/my")
	my.GET("/games", deps.UserLobbyMy.Games())
	my.GET("/applications", deps.UserLobbyMy.Applications())
	my.GET("/invites", deps.UserLobbyMy.Invites())
	my.GET("/race-names", deps.UserLobbyMy.RaceNames())

	lobbyMail := lobbyGroup.Group("/mail")
	lobbyMail.GET("/unread-counts", deps.UserMail.UnreadCounts())

	raceNames := lobbyGroup.Group("/race-names")
	raceNames.POST("/register", deps.UserLobbyRaceNames.Register())

	userGames := group.Group("/games")
	userGames.POST("/:game_id/commands", deps.UserGames.Commands())
	userGames.POST("/:game_id/orders", deps.UserGames.Orders())
	userGames.GET("/:game_id/orders", deps.UserGames.GetOrders())
	userGames.GET("/:game_id/reports/:turn", deps.UserGames.Report())
	userGames.GET("/:game_id/battles/:turn/:battle_id", deps.UserGames.Battle())

	userMail := userGames.Group("/:game_id/mail")
	userMail.POST("/messages", deps.UserMail.SendPersonal())
	userMail.POST("/broadcast", deps.UserMail.SendBroadcast())
	userMail.POST("/admin", deps.UserMail.SendAdmin())
	userMail.GET("/messages/:message_id", deps.UserMail.Get())
	userMail.POST("/messages/:message_id/read", deps.UserMail.MarkRead())
	userMail.DELETE("/messages/:message_id", deps.UserMail.Delete())
	userMail.GET("/inbox", deps.UserMail.Inbox())
	userMail.GET("/sent", deps.UserMail.Sent())

	userSessions := group.Group("/sessions")
	userSessions.GET("", deps.UserSessions.List())
	userSessions.POST("/revoke-all", deps.UserSessions.RevokeAll())
	userSessions.POST("/:device_session_id/revoke", deps.UserSessions.Revoke())
}

func registerAdminRoutes(router *gin.Engine, instruments *metrics.Instruments, deps RouterDependencies) {
	group := router.Group("/api/v1/admin")
	group.Use(metrics.Middleware(instruments, metrics.GroupAdmin))
	group.Use(basicauth.Middleware(deps.AdminVerifier, adminBasicAuthRealm))

	adminAccounts := group.Group("/admin-accounts")
	adminAccounts.GET("", deps.AdminAdminAccounts.List())
	adminAccounts.POST("", deps.AdminAdminAccounts.Create())
	adminAccounts.GET("/:username", deps.AdminAdminAccounts.Get())
	adminAccounts.POST("/:username/disable", deps.AdminAdminAccounts.Disable())
	adminAccounts.POST("/:username/enable", deps.AdminAdminAccounts.Enable())
	adminAccounts.POST("/:username/reset-password", deps.AdminAdminAccounts.ResetPassword())

	users := group.Group("/users")
	users.GET("", deps.AdminUsers.List())
	users.GET("/:user_id", deps.AdminUsers.Get())
	users.POST("/:user_id/sanctions", deps.AdminUsers.AddSanction())
	users.POST("/:user_id/limits", deps.AdminUsers.AddLimit())
	users.POST("/:user_id/entitlements", deps.AdminUsers.AddEntitlement())
	users.POST("/:user_id/soft-delete", deps.AdminUsers.SoftDelete())

	games := group.Group("/games")
	games.GET("", deps.AdminGames.List())
	games.POST("", deps.AdminGames.Create())
	games.GET("/:game_id", deps.AdminGames.Get())
	games.POST("/:game_id/force-start", deps.AdminGames.ForceStart())
	games.POST("/:game_id/force-stop", deps.AdminGames.ForceStop())
	games.POST("/:game_id/ban-member", deps.AdminGames.BanMember())
	games.POST("/:game_id/mail", deps.AdminDiplomail.Send())

	runtimes := group.Group("/runtimes")
	runtimes.GET("/:game_id", deps.AdminRuntimes.Get())
	runtimes.POST("/:game_id/restart", deps.AdminRuntimes.Restart())
	runtimes.POST("/:game_id/patch", deps.AdminRuntimes.Patch())
	runtimes.POST("/:game_id/force-next-turn", deps.AdminRuntimes.ForceNextTurn())

	engineVersions := group.Group("/engine-versions")
	engineVersions.GET("", deps.AdminEngineVersions.List())
	engineVersions.POST("", deps.AdminEngineVersions.Create())
	engineVersions.PATCH("/:id", deps.AdminEngineVersions.Update())
	engineVersions.POST("/:id/disable", deps.AdminEngineVersions.Disable())

	mail := group.Group("/mail")
	mail.GET("/deliveries", deps.AdminMail.ListDeliveries())
	mail.GET("/deliveries/:delivery_id", deps.AdminMail.GetDelivery())
	mail.GET("/deliveries/:delivery_id/attempts", deps.AdminMail.ListDeliveryAttempts())
	mail.POST("/deliveries/:delivery_id/resend", deps.AdminMail.ResendDelivery())
	mail.GET("/dead-letters", deps.AdminMail.ListDeadLetters())
	mail.GET("/messages", deps.AdminDiplomail.List())
	mail.POST("/broadcast", deps.AdminDiplomail.Broadcast())
	mail.POST("/cleanup", deps.AdminDiplomail.Cleanup())

	notifications := group.Group("/notifications")
	notifications.GET("", deps.AdminNotifications.List())
	notifications.GET("/dead-letters", deps.AdminNotifications.ListDeadLetters())
	notifications.GET("/malformed", deps.AdminNotifications.ListMalformed())
	notifications.GET("/:notification_id", deps.AdminNotifications.Get())

	geo := group.Group("/geo")
	geo.GET("/users/:user_id/countries", deps.AdminGeo.ListUserCountries())
}

func registerInternalRoutes(router *gin.Engine, instruments *metrics.Instruments, deps RouterDependencies) {
	group := router.Group("/api/v1/internal")
	group.Use(metrics.Middleware(instruments, metrics.GroupInternal))

	sessions := group.Group("/sessions")
	sessions.GET("/:device_session_id", deps.InternalSessions.Get())

	users := group.Group("/users")
	users.GET("/:user_id/account-internal", deps.InternalUsers.GetAccountInternal())
}

// allowedMethodsForPath returns the comma-separated list of methods
// the gin router accepts on requestPath. Only the probe paths declare
// a non-empty list so NoMethod can advertise a useful `Allow` header
// on `/healthz` and `/readyz`. Other endpoints fall through to NoRoute.
func allowedMethodsForPath(requestPath string) string {
	switch requestPath {
	case "/healthz", "/readyz":
		return http.MethodGet
	default:
		return ""
	}
}
