package server

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/go-chi/chi/v5"
	chimiddleware "github.com/go-chi/chi/v5/middleware"
	"gorm.io/gorm"

	agsauth "github.com/pingcap/agent-git-service/auth"
	"github.com/pingcap/agent-git-service/config"
	"github.com/pingcap/agent-git-service/internal/githttp"
	"github.com/pingcap/agent-git-service/internal/gitstore"
	"github.com/pingcap/agent-git-service/internal/graphql"
	"github.com/pingcap/agent-git-service/internal/metrics"
	srvmiddleware "github.com/pingcap/agent-git-service/internal/middleware"
	"github.com/pingcap/agent-git-service/internal/oauth"
	"github.com/pingcap/agent-git-service/internal/rest"
	"github.com/pingcap/agent-git-service/internal/router"
	"github.com/pingcap/agent-git-service/internal/service"
	"github.com/pingcap/agent-git-service/internal/tenant"
	"github.com/pingcap/agent-git-service/internal/wikicatalog"
)

// gitSHA is set at build time via -ldflags.
var gitSHA = "unknown"

// Server exposes a programmatic server instance for embedders.
type Server struct {
	deps    *bootstrapDeps
	handler http.Handler
}

// Authenticator authenticates a request using host-provided identity. ok=false
// means no embedded identity was present and AGS should continue with its
// built-in token flow when applicable.
type Authenticator interface {
	Authenticate(*http.Request) (agsauth.Identity, bool, error)
}

type options struct {
	singleTenantAuthenticator Authenticator
	singleAuthenticatorSet    bool
	tenantAuthenticators      map[tenant.ID]Authenticator
	errors                    []error
}

// Option configures the embeddable server surface.
type Option func(*options)

// WithAuthenticator installs a host-provided request authenticator.
func WithAuthenticator(authenticator Authenticator) Option {
	return func(opts *options) {
		if opts.singleAuthenticatorSet {
			opts.errors = append(opts.errors, fmt.Errorf("WithAuthenticator may only be configured once"))
			return
		}
		opts.singleAuthenticatorSet = true
		if authenticator == nil {
			opts.errors = append(opts.errors, fmt.Errorf("embedded authenticator is nil"))
			return
		}
		opts.singleTenantAuthenticator = authenticator
	}
}

// WithTenantAuthenticator installs an embedded authenticator for exactly one
// configured tenant. Requests for all other tenants continue through their
// own token flow.
func WithTenantAuthenticator(tenantID string, authenticator Authenticator) Option {
	return func(opts *options) {
		id, err := tenant.ParseID(tenantID)
		if err != nil {
			opts.errors = append(opts.errors, err)
			return
		}
		if authenticator == nil {
			opts.errors = append(opts.errors, fmt.Errorf("tenant %q embedded authenticator is nil", id))
			return
		}
		if opts.tenantAuthenticators == nil {
			opts.tenantAuthenticators = make(map[tenant.ID]Authenticator)
		}
		if _, exists := opts.tenantAuthenticators[id]; exists {
			opts.errors = append(opts.errors, fmt.Errorf("tenant %q embedded authenticator is configured more than once", id))
			return
		}
		opts.tenantAuthenticators[id] = authenticator
	}
}

// bootstrapDeps holds all initialized dependencies for the application.
type bootstrapDeps struct {
	Cfg          config.Config
	Options      options
	Store        *gitstore.Store
	SrvCtx       context.Context
	SrvCancel    context.CancelFunc
	SvcDeps      *service.Service
	GqlSrv       *graphql.Server
	GitHandler   *githttp.Handler
	OauthHandler *oauth.Handler
	Handlers     *rest.Deps
	Registry     *tenantRegistry
	Resolver     tenantResolver
	Mux          http.Handler
	Servers      []*http.Server
	Labels       []string
}

// bootstrapResult is returned by bootstrap and contains all initialized components.
type bootstrapResult struct {
	Deps    *bootstrapDeps
	Err     error
	Partial *bootstrapDeps // Contains successfully initialized deps if bootstrap failed midway
}

type coreDeps struct {
	cfg       config.Config
	cfgLoaded bool
	store     *gitstore.Store
	registry  *tenantRegistry
}

type serviceDeps struct {
	svc          *service.Service
	gqlSrv       *graphql.Server
	gitHandler   *githttp.Handler
	oauthHandler *oauth.Handler
}

type muxDeps struct {
	handlers *rest.Deps
	router   *chi.Mux
	mux      http.Handler
}

type serverDeps struct {
	servers []*http.Server
	labels  []string
}

type embeddedAuthenticatorAdapter struct {
	byTenant map[tenant.ID]Authenticator
}

func (a embeddedAuthenticatorAdapter) Authenticate(r *http.Request) (srvmiddleware.EmbeddedIdentity, bool, error) {
	info, err := tenant.Require(r.Context())
	if err != nil {
		return srvmiddleware.EmbeddedIdentity{}, false, err
	}
	authenticator := a.byTenant[info.ID]
	if authenticator == nil {
		return srvmiddleware.EmbeddedIdentity{}, false, nil
	}
	identity, ok, err := authenticator.Authenticate(r)
	if err != nil || !ok {
		return srvmiddleware.EmbeddedIdentity{}, ok, err
	}
	return srvmiddleware.EmbeddedIdentity{
		Provider:  identity.Provider,
		Subject:   identity.Subject,
		Login:     identity.Login,
		Name:      identity.Name,
		Email:     identity.Email,
		Groups:    append([]string(nil), identity.Groups...),
		SiteAdmin: identity.SiteAdmin,
	}, true, nil
}

func embeddedAuthConfig(opts options, registry *tenantRegistry) (srvmiddleware.EmbeddedAuthConfig, error) {
	byTenant := make(map[tenant.ID]Authenticator, len(opts.tenantAuthenticators)+1)
	for id, authenticator := range opts.tenantAuthenticators {
		if authenticator == nil {
			return srvmiddleware.EmbeddedAuthConfig{}, fmt.Errorf("tenant %q embedded authenticator is nil", id)
		}
		if _, err := registry.ForTenant(id); err != nil {
			return srvmiddleware.EmbeddedAuthConfig{}, fmt.Errorf("embedded authenticator: %w", err)
		}
		byTenant[id] = authenticator
	}
	if opts.singleTenantAuthenticator != nil {
		if len(registry.ids) != 1 {
			return srvmiddleware.EmbeddedAuthConfig{}, fmt.Errorf("WithAuthenticator requires exactly one configured tenant; use WithTenantAuthenticator for multitenant servers")
		}
		id := registry.ids[0]
		if _, exists := byTenant[id]; exists {
			return srvmiddleware.EmbeddedAuthConfig{}, fmt.Errorf("tenant %q has both global and tenant-specific authenticators", id)
		}
		byTenant[id] = opts.singleTenantAuthenticator
	}
	if len(byTenant) == 0 {
		return srvmiddleware.EmbeddedAuthConfig{}, nil
	}
	return srvmiddleware.EmbeddedAuthConfig{
		Authenticator: embeddedAuthenticatorAdapter{byTenant: byTenant},
	}, nil
}

func initCoreDepsFromConfig(cfg config.Config) (coreDeps, error) {
	deps := coreDeps{cfg: cfg, cfgLoaded: true}
	registry, err := initTenantRegistry(cfg)
	if err != nil {
		return deps, err
	}
	deps.registry = registry

	store, err := gitstore.NewTenantRouter(func(ctx context.Context) (*gitstore.Store, error) {
		resources, err := registry.tenantForContext(ctx)
		if err != nil {
			return nil, err
		}
		return resources.Git, nil
	})
	if err != nil {
		registry.close()
		return deps, err
	}
	deps.store = store
	return deps, nil
}

func initTenantServiceDeps(registry *tenantRegistry, store *gitstore.Store, srvCtx context.Context) (serviceDeps, error) {
	var deps serviceDeps
	if registry == nil || len(registry.ids) == 0 {
		return deps, fmt.Errorf("tenant registry is required")
	}

	svcDeps, err := service.NewService(srvCtx, registry)
	if err != nil {
		return deps, err
	}
	for _, id := range registry.ids {
		resources, err := registry.ForTenant(id)
		if err != nil {
			return deps, err
		}
		resources.WikiCatalog.DBFor = func(ctx context.Context) *gorm.DB {
			return svcDeps.DBForTenantResources(ctx, resources)
		}
		resources.WikiCatalog.OnChangeSetCommitted = svcDeps.WikiCatalogPostCommit
	}

	deps.svc = svcDeps
	deps.gqlSrv = graphql.NewServer(svcDeps)
	deps.gitHandler = githttp.New(store, svcDeps)
	deps.oauthHandler = oauth.New(svcDeps)
	return deps, nil
}

// httpMuxConfig holds all dependencies required to build the HTTP multiplexer.
// This struct reduces parameter count in buildHTTPMux and related functions.
type httpMuxConfig struct {
	Cfg          config.Config
	Registry     *tenantRegistry
	Resolver     tenantResolver
	ServiceDeps  *service.Service
	GQLServer    *graphql.Server
	GitHandler   *githttp.Handler
	OAuthHandler *oauth.Handler
	Version      string
	EmbeddedAuth srvmiddleware.EmbeddedAuthConfig
}

func buildHTTPMux(cfg httpMuxConfig) (muxDeps, error) {
	handlers := &rest.Deps{
		Svc: cfg.ServiceDeps,
	}

	root := chi.NewRouter()
	root.Use(chimiddleware.RealIP)
	root.Use(chimiddleware.RequestID)
	root.Use(srvmiddleware.RequestIDResponseHeader())
	root.Use(srvmiddleware.Recoverer())
	metricsHandler := metrics.Init()
	root.Get("/metrics", metricsHandler.ServeHTTP)
	root.Get("/readyz", readyzHandler(readyzConfig{
		Databases: cfg.Registry.readinessDatabases(),
		Storage:   cfg.Registry.readinessStorage(),
		Version:   cfg.Version,
	}))

	app := chi.NewRouter()
	app.Use(tenantResolutionMiddleware(cfg.Resolver))
	app.Use(srvmiddleware.RequestLogging())
	app.Use(srvmiddleware.MetricsInstrumentation())
	hostMux := router.RegisterRoutes(app, handlers, cfg.GitHandler, cfg.GQLServer, cfg.OAuthHandler, cfg.EmbeddedAuth)
	root.Mount("/", hostMux)

	return muxDeps{handlers: handlers, router: root, mux: root}, nil
}

func buildServers(cfg config.Config, mux http.Handler) (serverDeps, error) {
	addr := ":" + cfg.Port

	if cfg.ListenMode == "production" {
		return serverDeps{
			servers: []*http.Server{
				{Addr: addr, Handler: mux},
			},
			labels: []string{
				fmt.Sprintf("http://0.0.0.0:%s", cfg.Port),
			},
		}, nil
	}

	tlsCert, err := tls.LoadX509KeyPair("cert.pem", "key.pem")
	if err != nil {
		return serverDeps{}, fmt.Errorf("TLS: %w", err)
	}
	tlsCfg := &tls.Config{Certificates: []tls.Certificate{tlsCert}}

	return serverDeps{
		servers: []*http.Server{
			{Addr: ":443", Handler: mux, TLSConfig: tlsCfg},
			{Addr: addr, Handler: mux, TLSConfig: tlsCfg},
			{Addr: ":8081", Handler: mux, TLSConfig: tlsCfg},
			{Addr: ":80", Handler: mux},
			{Addr: ":4003", Handler: mux},
		},
		labels: []string{
			"https://github.localhost:443",
			fmt.Sprintf("https://localhost:%s", cfg.Port),
			"https://github.localhost:8081",
			"http://github.localhost:80",
			"http://github.localhost:4003",
		},
	}, nil
}

// bootstrap initializes all application dependencies in order.
// It returns a bootstrapResult with either fully initialized deps or partial deps on failure.
func bootstrap() bootstrapResult {
	cfg, err := config.New()
	if err != nil {
		return bootstrapResult{Deps: &bootstrapDeps{}, Err: fmt.Errorf("config: %w", err)}
	}
	return bootstrapWithConfig(cfg, options{})
}

func bootstrapWithConfig(cfg config.Config, opts options) bootstrapResult {
	result := bootstrapResult{
		Deps: &bootstrapDeps{},
	}
	deps := result.Deps
	deps.Options = opts
	if len(opts.errors) > 0 {
		result.Err = errors.Join(opts.errors...)
		return result
	}

	// 1. Core dependencies (config, database, embedding, gitstore).
	core, err := initCoreDepsFromConfig(cfg)
	if err != nil {
		result.Err = err
		if core.cfgLoaded {
			partial := &bootstrapDeps{Cfg: core.cfg}
			if core.store != nil {
				partial.Store = core.store
			}
			result.Partial = partial
		}
		return result
	}
	deps.Cfg = core.cfg
	deps.Store = core.store
	deps.Registry = core.registry

	resolver, err := newTenantResolver(core.registry.bindings, cfg.TrustForwardedHost)
	if err != nil {
		core.registry.close()
		result.Err = fmt.Errorf("tenant resolver: %w", err)
		return result
	}
	deps.Resolver = resolver

	embeddedAuth, err := embeddedAuthConfig(opts, core.registry)
	if err != nil {
		core.registry.close()
		result.Err = err
		return result
	}

	// 2. Server-level context.
	srvCtx, srvCancel := context.WithCancel(context.Background())
	deps.SrvCtx = srvCtx
	deps.SrvCancel = srvCancel

	// 3. One shared handler/service graph backed by tenant-routed resources.
	svc, err := initTenantServiceDeps(core.registry, core.store, srvCtx)
	if err != nil {
		srvCancel()
		core.registry.close()
		result.Err = err
		return result
	}
	deps.SvcDeps = svc.svc
	deps.GqlSrv = svc.gqlSrv
	deps.GitHandler = svc.gitHandler
	deps.OauthHandler = svc.oauthHandler

	// 4. Build router and host-aware mux.
	mux, err := buildHTTPMux(httpMuxConfig{
		Cfg:          core.cfg,
		Registry:     core.registry,
		Resolver:     resolver,
		ServiceDeps:  svc.svc,
		GQLServer:    svc.gqlSrv,
		GitHandler:   svc.gitHandler,
		OAuthHandler: svc.oauthHandler,
		Version:      gitSHA,
		EmbeddedAuth: embeddedAuth,
	})
	if err != nil {
		srvCancel()
		core.registry.close()
		result.Err = err
		return result
	}
	deps.Handlers = mux.handlers
	deps.Mux = mux.mux

	// 6. Set up HTTP servers.
	srvs, err := buildServers(core.cfg, mux.mux)
	if err != nil {
		srvCancel()
		core.registry.close()
		result.Err = err
		return result
	}
	deps.Servers = srvs.servers
	deps.Labels = srvs.labels

	return result
}

// shutdownConfig holds configuration for shutdown behavior.
type shutdownConfig struct {
	GracePeriod time.Duration
}

// shutdownResult captures the results of shutdown operations.
type shutdownResult struct {
	HTTPShutdownErrors []error
	BackgroundDrainErr error
	BgDrained          bool
	BgDrainTimedOut    bool
	ContextCanceled    bool
	ResourcesClosed    bool
}

func (r shutdownResult) Err() error {
	errs := append([]error(nil), r.HTTPShutdownErrors...)
	if r.BackgroundDrainErr != nil {
		errs = append(errs, r.BackgroundDrainErr)
	}
	if !r.ResourcesClosed && len(errs) == 0 {
		errs = append(errs, errors.New("shutdown completed without closing tenant resources"))
	}
	return errors.Join(errs...)
}

// waitForWaitGroup waits for a wait group to complete or context to timeout.
func waitForWaitGroup(ctx context.Context, wg *sync.WaitGroup, name string) error {
	done := make(chan struct{})
	go func() {
		wg.Wait()
		close(done)
	}()
	select {
	case <-done:
		slog.Info("wait group drained", "name", name)
		return nil
	case <-ctx.Done():
		slog.Warn("wait group drain timed out", "name", name, "error", ctx.Err())
		return fmt.Errorf("drain %s: %w", name, ctx.Err())
	}
}

// shutdownWithContext gracefully stops all servers and waits for background
// workers. Tenant resources remain open unless every owner has drained.
func shutdownWithContext(ctx context.Context, deps *bootstrapDeps) shutdownResult {
	result := shutdownResult{}

	for _, srv := range deps.Servers {
		if err := srv.Shutdown(ctx); err != nil {
			result.HTTPShutdownErrors = append(result.HTTPShutdownErrors, err)
		}
	}
	slog.Info("all listeners stopped; waiting for background goroutines")

	if deps.SrvCancel != nil {
		deps.SrvCancel()
		result.ContextCanceled = true
	}

	if deps.SvcDeps == nil {
		result.BgDrained = true
	} else if err := waitForWaitGroup(ctx, &deps.SvcDeps.Wg, "background goroutines (svcDeps)"); err != nil {
		result.BackgroundDrainErr = err
		result.BgDrainTimedOut = true
	} else {
		result.BgDrained = true
	}

	if len(result.HTTPShutdownErrors) == 0 && result.BgDrained {
		if deps.Registry != nil {
			deps.Registry.close()
		}
		result.ResourcesClosed = true
	}

	return result
}

// shutdown applies the process grace period to listener and worker drain.
func shutdown(deps *bootstrapDeps, cfg shutdownConfig) shutdownResult {
	ctx, cancel := context.WithTimeout(context.Background(), cfg.GracePeriod)
	defer cancel()
	return shutdownWithContext(ctx, deps)
}

func run(sigCh <-chan struct{}, shutdownCfg shutdownConfig) error {
	return runWithBootstrapResult(sigCh, shutdownCfg, bootstrap())
}

func runWithConfig(sigCh <-chan struct{}, shutdownCfg shutdownConfig, cfg config.Config) error {
	normalized, err := config.Normalize(cfg)
	if err != nil {
		return fmt.Errorf("config: %w", err)
	}
	return runWithBootstrapResult(sigCh, shutdownCfg, bootstrapWithConfig(normalized, options{}))
}

func runWithBootstrapResult(sigCh <-chan struct{}, shutdownCfg shutdownConfig, result bootstrapResult) error {
	if result.Err != nil {
		return result.Err
	}
	deps := result.Deps
	startRuntimeWorkers(deps)

	// Start HTTP servers.
	for i, srv := range deps.Servers {
		lbl := deps.Labels[i]
		s := srv
		go func() {
			fmt.Printf("gh-server listening on %s\n", lbl)
			var err error
			if s.TLSConfig != nil {
				err = s.ListenAndServeTLS("", "")
			} else {
				err = s.ListenAndServe()
			}
			if err != nil && err != http.ErrServerClosed {
				slog.Error("listener exited unexpectedly", "listener", lbl, "error", err)
			}
		}()
	}

	// Handle shutdown signals.
	<-sigCh
	slog.Info("shutdown initiated", "grace_period", shutdownCfg.GracePeriod.String())

	return shutdown(deps, shutdownCfg).Err()
}

type wikiReindexSelection struct {
	tenantID tenant.ID
	all      bool
	repo     string
}

func parseWikiReindexArgs(args []string) (wikiReindexSelection, error) {
	switch {
	case len(args) == 1 && args[0] == "--all-tenants":
		return wikiReindexSelection{all: true}, nil
	case len(args) == 2 && args[0] == "--tenant":
		id, err := tenant.ParseID(args[1])
		if err != nil {
			return wikiReindexSelection{}, err
		}
		return wikiReindexSelection{tenantID: id}, nil
	case len(args) == 3 && args[0] == "--tenant" && strings.TrimSpace(args[2]) != "":
		id, err := tenant.ParseID(args[1])
		if err != nil {
			return wikiReindexSelection{}, err
		}
		return wikiReindexSelection{tenantID: id, repo: strings.TrimSpace(args[2])}, nil
	default:
		return wikiReindexSelection{}, fmt.Errorf("usage: wiki-reindex --tenant <tenant-id> [owner/repo] | --all-tenants")
	}
}

// RunWikiReindex reindexes wiki search data for one explicit tenant or all
// tenants. It never selects a default tenant.
func RunWikiReindex(args []string) error {
	selection, err := parseWikiReindexArgs(args)
	if err != nil {
		return err
	}
	return runWikiReindex(selection, bootstrap())
}

// RunWikiReindexWithConfig reindexes wiki data using caller-supplied
// application configuration.
func RunWikiReindexWithConfig(cfg config.Config, args []string) error {
	selection, err := parseWikiReindexArgs(args)
	if err != nil {
		return err
	}
	normalized, err := config.Normalize(cfg)
	if err != nil {
		return fmt.Errorf("config: %w", err)
	}
	return runWikiReindex(selection, bootstrapWithConfig(normalized, options{}))
}

func runWikiReindex(selection wikiReindexSelection, result bootstrapResult) error {
	if result.Err != nil {
		return result.Err
	}
	deps := result.Deps

	ctx, cancel := context.WithTimeout(deps.SrvCtx, 10*time.Minute)

	runTenant := func(id tenant.ID) error {
		resources, err := deps.Registry.ForTenant(id)
		if err != nil {
			return err
		}
		tenantCtx := tenant.WithInfo(ctx, resources.Info)
		if selection.repo != "" {
			count, err := deps.SvcDeps.ReindexWikiSearch(tenantCtx, selection.repo)
			if err != nil {
				return err
			}
			fmt.Printf("wiki-reindex tenant=%s repo=%s indexed=%d\n", id, selection.repo, count)
			return nil
		}
		count, err := deps.SvcDeps.ReindexAllWikiSearch(tenantCtx)
		if err != nil {
			return err
		}
		fmt.Printf("wiki-reindex tenant=%s indexed=%d\n", id, count)
		return nil
	}

	runErr := func() error {
		if selection.all {
			for _, id := range deps.Registry.ids {
				if err := runTenant(id); err != nil {
					return fmt.Errorf("tenant %q: %w", id, err)
				}
			}
			return nil
		}
		return runTenant(selection.tenantID)
	}()
	cancel()

	shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer shutdownCancel()
	return errors.Join(runErr, shutdownWithContext(shutdownCtx, deps).Err())
}

// Run starts the gh-server listeners and blocks until shutdown is requested.
func Run(sigCh <-chan struct{}) error {
	return run(sigCh, shutdownConfig{GracePeriod: 10 * time.Second})
}

// RunWithConfig starts the gh-server listeners using caller-supplied
// application configuration.
func RunWithConfig(sigCh <-chan struct{}, cfg config.Config) error {
	return runWithConfig(sigCh, shutdownConfig{GracePeriod: 10 * time.Second}, cfg)
}

// RunRepositorySyncWorkerWithConfig starts only the repository sync control
// plane dependencies and workers. It intentionally opens no HTTP listeners;
// deploy it independently from gh-server so long Git/LFS transfers cannot
// consume request-serving process capacity.
func RunRepositorySyncWorkerWithConfig(sigCh <-chan struct{}, cfg config.Config) error {
	normalized, err := config.Normalize(cfg)
	if err != nil {
		return fmt.Errorf("config: %w", err)
	}
	result := bootstrapWithConfig(normalized, options{})
	if result.Err != nil {
		return result.Err
	}
	deps := result.Deps
	configureRepositorySync(deps, true)
	<-sigCh
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	return shutdownWithContext(shutdownCtx, deps).Err()
}

// New constructs a server from a caller-supplied config.
func New(cfg config.Config, opts ...Option) (*Server, error) {
	normalized, err := config.Normalize(cfg)
	if err != nil {
		return nil, err
	}
	parsed := options{}
	for _, opt := range opts {
		if opt != nil {
			opt(&parsed)
		}
	}
	result := bootstrapWithConfig(normalized, parsed)
	if result.Err != nil {
		return nil, result.Err
	}
	startRuntimeWorkers(result.Deps)
	return &Server{
		deps:    result.Deps,
		handler: result.Deps.Mux,
	}, nil
}

// Handler returns the fully wired application handler tree.
func (s *Server) Handler() http.Handler { return s.handler }

// Shutdown gracefully stops listeners and background work. Tenant resources
// close only after both have drained; callers may retry with a fresh context
// after a timeout without racing an in-flight owner against closed resources.
func (s *Server) Shutdown(ctx context.Context) error {
	return shutdownWithContext(ctx, s.deps).Err()
}

// readyzHandler returns an http.HandlerFunc that pings every tenant database.
// It returns 200 only when all configured partitions are reachable.
// readyzConfig holds dependencies for the readiness probe handler.
// This struct reduces parameter count and improves clarity.
type readyzConfig struct {
	Databases map[string]*gorm.DB
	Storage   map[string]tenantStorageReadiness
	Version   string
}

type tenantStorageReadiness struct {
	TenantID tenant.ID
	Git      *gitstore.Store
	WikiBlob *wikicatalog.BlobStore
}

func readyzHandler(cfg readyzConfig) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		w.Header().Set("Content-Type", "application/json")

		type checkResult struct {
			Status string `json:"status"`
			Error  string `json:"error,omitempty"`
		}
		checks := map[string]checkResult{}
		healthy := true

		if len(cfg.Databases) == 0 {
			checks["tenant_registry"] = checkResult{Status: "unavailable", Error: "no tenant databases configured"}
			healthy = false
		}
		for name, database := range cfg.Databases {
			if database == nil {
				checks[name] = checkResult{Status: "unavailable", Error: "database is nil"}
				healthy = false
				continue
			}
			if sqlDB, err := database.DB(); err != nil {
				checks[name] = checkResult{Status: "unavailable", Error: err.Error()}
				healthy = false
			} else if err := sqlDB.PingContext(ctx); err != nil {
				checks[name] = checkResult{Status: "unavailable", Error: err.Error()}
				healthy = false
			} else {
				checks[name] = checkResult{Status: "ok"}
			}
		}
		for id, storage := range cfg.Storage {
			name := "tenant_" + id + "_storage"
			switch {
			case storage.Git == nil:
				checks[name] = checkResult{Status: "unavailable", Error: "Git store is nil"}
				healthy = false
			case storage.WikiBlob == nil:
				checks[name] = checkResult{Status: "unavailable", Error: "wiki blob store is nil"}
				healthy = false
			default:
				if err := storage.Git.VerifyTenantRoot(storage.TenantID); err != nil {
					checks[name] = checkResult{Status: "unavailable", Error: err.Error()}
					healthy = false
				} else {
					gitRoot := storage.Git.CanonicalRoot()
					wikiRoot := filepath.Clean(storage.WikiBlob.Root())
					stat, err := os.Stat(wikiRoot)
					if err != nil {
						checks[name] = checkResult{Status: "unavailable", Error: err.Error()}
						healthy = false
					} else if !stat.IsDir() || wikiRoot != gitRoot {
						checks[name] = checkResult{Status: "unavailable", Error: "wiki blob root does not match tenant Git root"}
						healthy = false
					} else {
						checks[name] = checkResult{Status: "ok"}
					}
				}
			}
		}

		status := "ready"
		code := http.StatusOK
		if !healthy {
			status = "not_ready"
			code = http.StatusServiceUnavailable
		}
		metrics.ObserveReadyz(status)
		w.WriteHeader(code)
		_ = json.NewEncoder(w).Encode(map[string]any{
			"status":  status,
			"version": cfg.Version,
			"checks":  checks,
		})
	}
}
