// Command web is the Phase 1 walking-skeleton HTTP server. It loads env,
// builds a slog handler, opens a pgxpool, runs goose migrations, mounts the
// chi router, and serves /, /healthz, /readyz, /demo/time, and /static/* with
// graceful shutdown on SIGINT/SIGTERM (CONTEXT D-19, D-10, DEPLOY-03/04).
//
// No .env parser lives here — `.env` is exported into the process
// environment by `just dev`; production injects real env vars (D-15).
package main

import (
	"context"
	"errors"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"syscall"
	"time"

	assets "backend"
	"backend/internal/auth"
	"backend/internal/db"
	"backend/internal/db/sqlc"
	"backend/internal/files"
	"backend/internal/web"
)

func main() {
	env := os.Getenv("ENV")
	if env == "" {
		env = "development"
	}
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
	dsn := os.Getenv("DATABASE_URL")

	// Logger first so even fatal-on-missing-DSN paths produce structured
	// output. Per Pattern 3: JSON in production, text everywhere else.
	slog.SetDefault(slog.New(web.NewSlogHandler(env, os.Stdout)))

	if dsn == "" {
		slog.Error("DATABASE_URL is required but unset")
		os.Exit(1)
	}

	// Load the CSRF authentication key from SESSION_SECRET env var (D-15).
	// Fails fast with a clear message if missing or wrong length — the server
	// cannot operate without a valid CSRF key (AUTH-06).
	csrfKey, err := auth.LoadKeyFromEnv()
	if err != nil {
		slog.Error("invalid SESSION_SECRET", "err", err,
			"hint", "generate with: openssl rand -hex 32")
		os.Exit(1)
	}

	// signal.NotifyContext (Go 1.21+) is the canonical idiom — equivalent
	// to signal.Notify + a channel but the resulting ctx propagates the
	// cancellation through to handlers, pgxpool dialing, etc.
	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()

	pool, err := db.NewPool(ctx, dsn)
	if err != nil {
		// T-01-12: never log the DSN — only the error type/message.
		slog.Error("db connect failed", "err", err)
		os.Exit(1)
	}

	// D-10: run goose migrations from the embedded FS before constructing the
	// router. goose.Up is idempotent — already-applied migrations are skipped.
	if err := db.RunMigrations(ctx, pool, assets.Migrations); err != nil {
		slog.Error("migrations failed", "err", err)
		os.Exit(1)
	}

	q := sqlc.New(pool)
	store := auth.NewStore(q)
	secure := env != "development" && env != "dev"

	// Rate limiter for POST /login (D-16, AUTH-07).
	rl := auth.NewLimiterStore()
	stopJanitor := make(chan struct{})
	rl.StartJanitor(time.Minute, stopJanitor)

	oauthCfg := auth.OAuthConfig{
		Google: auth.GoogleProviderConfig{
			ClientID:     os.Getenv("GOOGLE_CLIENT_ID"),
			ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
			RedirectURL:  os.Getenv("GOOGLE_REDIRECT_URL"),
		},
	}
	var googleExchanger auth.CodeExchanger
	var googleVerifier auth.IDTokenVerifier
	if oauthCfg.Google.Configured() {
		googleExchanger = auth.OAuth2CodeExchanger{Config: oauthCfg.Google.OAuth2Config()}
		googleVerifier = auth.OIDCVerifier{
			Provider: "google",
			Issuer:   "https://accounts.google.com",
			ClientID: oauthCfg.Google.ClientID,
		}
	}

	deps := web.AuthDeps{
		Queries:              q,
		Store:                store,
		Secure:               secure,
		Limiter:              rl,
		DB:                   pool,
		OAuth:                oauthCfg,
		GoogleTokenExchanger: googleExchanger,
		GoogleVerifier:       googleVerifier,
	}
	tabloDeps := web.TablosDeps{Queries: q}
	taskDeps := web.TasksDeps{Queries: q}

	// S3 / files store (D-02: read env vars and pass to files.NewStore).
	s3Endpoint := os.Getenv("S3_ENDPOINT")
	s3Bucket := os.Getenv("S3_BUCKET")
	s3AccessKey := os.Getenv("S3_ACCESS_KEY")
	s3SecretKey := os.Getenv("S3_SECRET_KEY")
	s3Region := os.Getenv("S3_REGION")
	if s3Region == "" {
		s3Region = "us-east-1"
	}
	s3UsePathStyle := os.Getenv("S3_USE_PATH_STYLE") == "true"

	maxUploadMB := 25
	if v := os.Getenv("MAX_UPLOAD_SIZE_MB"); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n > 0 {
			maxUploadMB = n
		}
	}

	var filesStore web.FileStorer
	if s3Endpoint != "" && s3Bucket != "" {
		var fsErr error
		filesStore, fsErr = files.NewStore(ctx, s3Endpoint, s3Bucket, s3Region, s3AccessKey, s3SecretKey, s3UsePathStyle)
		if fsErr != nil {
			slog.Error("s3 client init failed", "err", fsErr)
			os.Exit(1)
		}
	} else {
		slog.Warn("S3_ENDPOINT or S3_BUCKET unset — file upload routes will return 503")
	}

	fileDeps := web.FilesDeps{Queries: q, Files: filesStore, MaxUploadMB: maxUploadMB}
	etapeDeps := web.EtapesDeps{Queries: q}
	eventDeps := web.EventsDeps{Queries: q}
	discussionDeps := web.DiscussionDeps{Queries: q, Realtime: web.NewDiscussionBroker()}
	planningDeps := web.PlanningDeps{Queries: q}

	// D-09: pass the embedded static FS — binary has zero runtime file dependencies.
	router, err := web.NewRouter(pool, assets.Static, deps, tabloDeps, taskDeps, etapeDeps, eventDeps, discussionDeps, planningDeps, fileDeps, csrfKey, env)
	if err != nil {
		slog.Error("router init failed", "err", err)
		os.Exit(1)
	}

	srv := &http.Server{
		Addr:    ":" + port,
		Handler: router,
		// T-01-10 slow-client mitigation per RESEARCH Security Domain.
		// ReadTimeout covers request header + body read; 15 s is sufficient for API
		// calls but upload routes read up to MAX_UPLOAD_SIZE_MB (default 25 MB). The
		// MaxBytesReader in FileUploadHandler bounds the body size, not time; a slow
		// upload at ~256 KB/s takes ~100 s. WriteTimeout covers the full request
		// lifecycle from accept to response flush, so it must be generous enough for
		// large uploads. 120 s accommodates 25 MB at ~250 KB/s with headroom.
		ReadTimeout:  120 * time.Second,
		WriteTimeout: 120 * time.Second,
		IdleTimeout:  60 * time.Second,
	}

	go func() {
		slog.Info("listening", "addr", srv.Addr, "env", env)
		if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			slog.Error("server error", "err", err)
			os.Exit(1)
		}
	}()

	<-ctx.Done()
	slog.Info("shutting down")

	shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	if err := srv.Shutdown(shutdownCtx); err != nil {
		slog.Error("shutdown error", "err", err)
	}

	// Stop the rate-limiter janitor goroutine after HTTP server is fully shut down.
	close(stopJanitor)

	// Pitfall 4: close the pool AFTER Shutdown returns, NOT via defer in
	// main — defer ordering is unreliable on fatal-exit paths.
	pool.Close()
	slog.Info("shutdown complete")
}
