package api

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

	"github.com/go-chi/chi/v5"

	"github.com/alexei/docker-watcher/internal/auth"
	"github.com/alexei/docker-watcher/internal/backup"
	"github.com/alexei/docker-watcher/internal/crypto"
	"github.com/alexei/docker-watcher/internal/dns"
	"github.com/alexei/docker-watcher/internal/docker"
	"github.com/alexei/docker-watcher/internal/events"
	"github.com/alexei/docker-watcher/internal/npm"
	"github.com/alexei/docker-watcher/internal/proxy"
	"github.com/alexei/docker-watcher/internal/stale"
	"github.com/alexei/docker-watcher/internal/store"
	"github.com/alexei/docker-watcher/internal/webhook"
)

// DNSProviderChangedFunc is called when DNS settings change so the caller can
// update the provider on the deployer.
type DNSProviderChangedFunc func(provider dns.Provider)

// Server holds all dependencies for the API layer.
type Server struct {
	store         *store.Store
	docker        *docker.Client
	npm           *npm.Client         // optional: only for NPM-specific endpoints (certificates)
	proxyProvider proxy.Provider
	deployer      DeployTriggerer
	webhook      *webhook.Handler
	eventBus     *events.Bus
	encKey       [32]byte
	localAuth    *auth.LocalAuth
	oidcProvider *auth.OIDCProvider
	staleScanner *stale.Scanner

	dnsProviderMu        sync.RWMutex
	dnsProvider          dns.Provider
	onDNSProviderChanged DNSProviderChangedFunc

	backupEngine         *backup.Engine
	dbPath               string
	shutdownFunc         func()                    // called after restore to trigger graceful shutdown
	onBackupSettingsChanged func(enabled bool, intervalHours int) // called when backup settings change
}

// NewServer creates a new API Server with all required dependencies.
func NewServer(
	st *store.Store,
	dockerClient *docker.Client,
	npmClient *npm.Client,
	proxyProvider proxy.Provider,
	deployer DeployTriggerer,
	webhookHandler *webhook.Handler,
	eventBus *events.Bus,
	encKey [32]byte,
) *Server {
	localAuth := auth.NewLocalAuth(encKey)

	s := &Server{
		store:         st,
		docker:        dockerClient,
		npm:           npmClient,
		proxyProvider: proxyProvider,
		deployer:      deployer,
		webhook:       webhookHandler,
		eventBus:      eventBus,
		encKey:        encKey,
		localAuth:     localAuth,
	}

	// Try to initialize OIDC provider from stored settings.
	authSettings, err := st.GetAuthSettings()
	if err == nil && authSettings.AuthMode == "oidc" && authSettings.OIDCIssuerURL != "" {
		s.initOIDCProvider(context.Background(), authSettings)
	}

	return s
}

// SetStaleScanner sets the stale scanner on the server.
// Called after both the API server and scanner are initialized.
func (s *Server) SetStaleScanner(scanner *stale.Scanner) {
	s.staleScanner = scanner
}

// SetBackupEngine sets the backup engine on the server.
func (s *Server) SetBackupEngine(engine *backup.Engine) {
	s.backupEngine = engine
}

// SetDBPath sets the database file path (needed for restore).
func (s *Server) SetDBPath(path string) {
	s.dbPath = path
}

// SetShutdownFunc sets the function called after a restore to trigger graceful shutdown.
func (s *Server) SetShutdownFunc(fn func()) {
	s.shutdownFunc = fn
}

// SetBackupSettingsChangedCallback sets the callback for when backup settings change.
func (s *Server) SetBackupSettingsChangedCallback(fn func(enabled bool, intervalHours int)) {
	s.onBackupSettingsChanged = fn
}

// SetDNSProvider sets the current DNS provider on the server.
func (s *Server) SetDNSProvider(provider dns.Provider) {
	s.dnsProviderMu.Lock()
	defer s.dnsProviderMu.Unlock()
	s.dnsProvider = provider
}

// getDNSProviderLocked returns the current DNS provider under read lock.
func (s *Server) getDNSProviderLocked() dns.Provider {
	s.dnsProviderMu.RLock()
	defer s.dnsProviderMu.RUnlock()
	return s.dnsProvider
}

// SetDNSProviderChangedCallback sets the callback for when DNS settings change.
func (s *Server) SetDNSProviderChangedCallback(fn DNSProviderChangedFunc) {
	s.onDNSProviderChanged = fn
}

// initOIDCProvider creates an OIDC provider from settings. Errors are logged, not fatal.
func (s *Server) initOIDCProvider(ctx context.Context, as store.AuthSettings) {
	// Decrypt the OIDC client secret if it's encrypted.
	clientSecret := as.OIDCClientSecret
	if clientSecret != "" {
		if decrypted, err := crypto.Decrypt(s.encKey, clientSecret); err == nil {
			clientSecret = decrypted
		}
		// If decrypt fails, assume it's already plaintext (migration scenario).
	}
	provider, err := auth.NewOIDCProvider(ctx, auth.OIDCConfig{
		IssuerURL:    as.OIDCIssuerURL,
		ClientID:     as.OIDCClientID,
		ClientSecret: clientSecret,
		RedirectURL:  as.OIDCRedirectURL,
	})
	if err != nil {
		slog.Warn("failed to initialize OIDC provider", "error", err)
		return
	}
	s.oidcProvider = provider
	slog.Info("OIDC provider initialized", "issuer", as.OIDCIssuerURL)
}

// Router returns a chi router with all API routes mounted.
func (s *Server) Router() chi.Router {
	r := chi.NewRouter()

	// Global middleware.
	r.Use(recovery)
	r.Use(securityHeaders)
	r.Use(logging)
	r.Use(cors)

	loginLimiter := newRateLimiter()

	r.Route("/api", func(r chi.Router) {
		// JSON content type and body size limit for API routes.
		r.Use(jsonContentType)
		r.Use(limitBody)

		// Public auth endpoints (no auth required).
		r.Get("/auth/mode", s.authMode)
		r.Post("/auth/login", s.rateLimitedLogin(loginLimiter))
		r.Get("/auth/oidc/login", s.oidcLogin)
		r.Get("/auth/oidc/callback", s.oidcCallback)
		r.Post("/auth/oidc/token", s.oidcExchangeToken)

		// Webhook handler (uses its own secret-based auth).
		r.Mount("/webhook", s.webhook.Route())

		// Protected routes: require valid JWT.
		r.Group(func(r chi.Router) {
			r.Use(auth.Middleware(s.localAuth))

			// Read-only endpoints (any authenticated user).
			r.Get("/health", s.getHealth)
			r.Get("/auth/me", s.currentUser)
			r.Post("/auth/logout", s.logout)
			r.Get("/proxies", s.listProxyRoutes)
			r.Get("/projects", s.listProjects)
			r.Route("/projects/{id}", func(r chi.Router) {
				r.Get("/", s.getProject)
				r.Get("/stages/{stage}/env", s.listStageEnv)
				r.Get("/stages/{stage}/instances", s.listInstances)
				r.Get("/stages/{stage}/instances/{iid}/stats", s.getInstanceStats)
				r.Get("/volumes", s.listVolumes)
				r.Get("/volumes/{volId}/browse", s.browseVolume)
				r.Get("/volumes/{volId}/download", s.downloadVolume)

				// Admin-only project mutations.
				r.Group(func(r chi.Router) {
					r.Use(auth.AdminOnly)
					r.Put("/", s.updateProject)
					r.Delete("/", s.deleteProject)

					// Stage endpoints.
					r.Post("/stages", s.createStage)
					r.Put("/stages/{stage}", s.updateStage)
					r.Delete("/stages/{stage}", s.deleteStage)

					// Stage env override endpoints.
					r.Post("/stages/{stage}/env", s.createStageEnv)
					r.Put("/stages/{stage}/env/{envId}", s.updateStageEnv)
					r.Delete("/stages/{stage}/env/{envId}", s.deleteStageEnv)

					// Instance endpoints.
					r.Post("/stages/{stage}/instances", s.deployInstance)
					r.Delete("/stages/{stage}/instances/{iid}", s.removeInstance)

					// Instance control endpoints.
					r.Post("/stages/{stage}/instances/{iid}/stop", s.stopInstance)
					r.Post("/stages/{stage}/instances/{iid}/start", s.startInstance)
					r.Post("/stages/{stage}/instances/{iid}/restart", s.restartInstance)

					// Volume endpoints.
					r.Post("/volumes", s.createVolume)
					r.Put("/volumes/{volId}", s.updateVolume)
					r.Delete("/volumes/{volId}", s.deleteVolume)
					r.Post("/volumes/{volId}/upload", s.uploadToVolume)
				})
			})
			r.Get("/deploys", s.listDeploys)
			r.Get("/deploys/{id}/logs", s.streamDeployLogs)
			r.Get("/events", s.streamEvents)
			r.Get("/events/log", s.listEventLog)
			r.Get("/events/log/stats", s.getEventLogStats)
			r.Get("/registries", s.listRegistries)
			r.Route("/registries/{id}", func(r chi.Router) {
				r.Get("/tags/*", s.listRegistryTags)
				r.Get("/images", s.listRegistryImages)

				// Admin-only registry mutations.
				r.Group(func(r chi.Router) {
					r.Use(auth.AdminOnly)
					r.Put("/", s.updateRegistry)
					r.Delete("/", s.deleteRegistry)
					r.Post("/test", s.testRegistry)
				})
			})
			r.Get("/settings", s.getSettings)
			r.Get("/settings/npm-certificates", s.listNpmCertificates)

			// Volume scope metadata (read-only).
			r.Get("/volumes/scopes", s.listVolumeScopes)

			// Stale container endpoints (read).
			r.Get("/containers/stale", s.listStaleContainers)

			// Admin-only routes: require admin role.
			r.Group(func(r chi.Router) {
				r.Use(auth.AdminOnly)

				// Config export (reveals project/infra details).
				r.Get("/config/export", s.exportConfig)

				// Auth management.
				r.Get("/auth/settings", s.getAuthSettings)
				r.Put("/auth/settings", s.updateAuthSettings)
				r.Get("/auth/users", s.listUsers)
				r.Post("/auth/users", s.createUser)
				r.Put("/auth/users/{uid}", s.updateUser)
				r.Put("/auth/users/{uid}/password", s.changePassword)
				r.Delete("/auth/users/{uid}", s.deleteUser)

				// Project creation.
				r.Post("/projects", s.createProject)

				// Quick deploy endpoints.
				r.Post("/deploy/inspect", s.inspectImage)
				r.Post("/deploy/quick", s.quickDeploy)

				// Registry creation.
				r.Post("/registries", s.createRegistry)

				// Stale container cleanup endpoints.
				// Bulk route must be registered before parameterized route.
				r.Post("/containers/stale/cleanup", s.bulkCleanupStaleContainers)
				r.Post("/containers/stale/{id}/cleanup", s.cleanupStaleContainer)

				// Settings endpoints.
				r.Put("/settings", s.updateSettings)
				r.Get("/settings/webhook-url", s.getWebhookURL)
				r.Post("/settings/webhook-url/regenerate", s.regenerateWebhookSecret)

				// NPM connection test.
				r.Post("/settings/npm/test", s.testNpmConnection)

				// DNS management endpoints.
				r.Post("/settings/dns/test", s.testDNSConnection)
				r.Post("/settings/dns/zones", s.listDNSZones)
				r.Get("/dns/records", s.listDNSRecords)
				r.Post("/dns/sync", s.syncDNSRecords)
				r.Delete("/dns/records/{fqdn}", s.deleteDNSRecord)

				// Backup endpoints.
				r.Get("/backups", s.listBackups)
				r.Post("/backups", s.triggerBackup)
				r.Get("/backups/{id}/download", s.downloadBackup)
				r.Delete("/backups/{id}", s.deleteBackup)
				r.Post("/backups/{id}/restore", s.restoreBackup)
			})
		})
	})

	return r
}
