package app
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"time"
"go.bigb.es/auxilia/culpa"
"go.bigb.es/auxilia/scribe"
"sourcecraft.dev/bigbes/huntsman/internal/config"
"sourcecraft.dev/bigbes/huntsman/internal/domain/search"
"sourcecraft.dev/bigbes/huntsman/internal/platform/health"
"sourcecraft.dev/bigbes/huntsman/internal/platform/observability"
"sourcecraft.dev/bigbes/huntsman/internal/server"
)
// loggerWire builds the application's structured logger from config and
// installs it as the slog default so package-level callers pick it up.
type loggerWire struct {
Config config.Config `config:""`
Log *slog.Logger
}
func (w *loggerWire) Init(_ context.Context) error {
w.Log = observability.NewLogger(w.Config.Log.Level, w.Config.Log.Format)
slog.SetDefault(w.Log)
return nil
}
// searchWire constructs the search service and HTTP handler. Both are
// kept on the wire so HTTP can inject the handler and other services
// can call into the service if needed later.
type searchWire struct {
Config config.Config `config:""`
Service *search.Service
Handler *search.Handler
}
func (w *searchWire) Init(_ context.Context) error {
svc, err := search.NewService(w.Config.Search.DefaultProvider, w.Config.Server.PublicURL)
if err != nil {
return culpa.Wrap(err, "build search service")
}
w.Service = svc
w.Handler = search.NewHandler(svc)
return nil
}
// healthWire wraps the health registry so steward owns its lifetime.
// Add upstream probes by calling Health.Register from another service's
// Init via inject:"".
type healthWire struct {
Health *health.Health
}
func (w *healthWire) Init(_ context.Context) error {
w.Health = health.New()
return nil
}
// shutdownTimeout is how long http.Server.Shutdown is allowed to drain
// in-flight requests before returning.
const shutdownTimeout = 30 * time.Second
// httpServer is the root steward service. It depends on logger, search,
// and health wires (resolved by type). Init builds the chi router; Start
// runs the listener in a goroutine; Stop drains via Shutdown.
type httpServer struct {
Config config.Config `config:""`
Logger *loggerWire `inject:""`
Search *searchWire `inject:""`
Health *healthWire `inject:""`
Info BuildInfo
srv *http.Server
}
func (s *httpServer) Init(_ context.Context) error {
router := server.Routes(s.Logger.Log, s.Health.Health, s.Search.Handler)
addr := fmt.Sprintf("%s:%d", s.Config.Server.Host, s.Config.Server.Port)
s.srv = &http.Server{
Addr: addr,
Handler: router,
ReadTimeout: s.Config.Server.ReadTimeout,
WriteTimeout: s.Config.Server.WriteTimeout,
IdleTimeout: 60 * time.Second,
}
return nil
}
func (s *httpServer) Start(_ context.Context) error {
go func() {
s.Logger.Log.Info("server starting",
"addr", s.srv.Addr,
"version", s.Info.Version,
)
if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
s.Logger.Log.Error("server stopped unexpectedly",
scribe.Err(culpa.Wrap(err, "ListenAndServe")),
)
}
}()
return nil
}
func (s *httpServer) Stop(_ context.Context) error {
s.Logger.Log.Info("shutting down gracefully")
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if err := s.srv.Shutdown(shutdownCtx); err != nil {
return culpa.Wrap(err, "http shutdown")
}
return nil
}