package main

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

	"github.com/prometheus/client_golang/prometheus"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.34.0"
)

var appVersion = "dev"

type config struct {
	port       string
	database   string
	chaosRate  float64
	logLevel   slog.Level
	traceReady bool
}

func main() {
	if err := run(); err != nil {
		slog.Error("startup failed", "error", err)
		os.Exit(1)
	}
}

func run() error {
	cfg, err := loadConfig()
	if err != nil {
		return err
	}
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: cfg.logLevel}))
	slog.SetDefault(logger)

	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
	defer stop()
	shutdownTrace, err := configureTracing(ctx, cfg.traceReady)
	if err != nil {
		return err
	}
	defer func() {
		if err := shutdownTrace(context.Background()); err != nil {
			logger.Error("trace shutdown failed", "error", err)
		}
	}()

	registry := prometheus.NewRegistry()
	m := newMetrics(registry)
	db, err := newPostgresStore(ctx, cfg.database, m)
	if err != nil {
		return err
	}
	defer db.Close()

	var migrated atomic.Bool
	go migrateUntilReady(ctx, db, &migrated, logger)
	application := &api{store: db, metrics: m, logger: logger, version: appVersion, chaosRate: cfg.chaosRate, migrated: &migrated, prometheus: registry}
	server := &http.Server{
		Addr:              ":" + cfg.port,
		Handler:           application.routes(),
		ReadHeaderTimeout: 5 * time.Second,
		WriteTimeout:      30 * time.Second,
		IdleTimeout:       60 * time.Second,
		MaxHeaderBytes:    1 << 20,
	}
	errCh := make(chan error, 1)
	go func() { errCh <- server.ListenAndServe() }()
	logger.Info("server started", "port", cfg.port, "version", appVersion)
	select {
	case <-ctx.Done():
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		if err := server.Shutdown(shutdownCtx); err != nil {
			return fmt.Errorf("shutdown server: %w", err)
		}
		return nil
	case err := <-errCh:
		if errors.Is(err, http.ErrServerClosed) {
			return nil
		}
		return fmt.Errorf("serve HTTP: %w", err)
	}
}

func loadConfig() (config, error) {
	cfg := config{port: envOr("PORT", "8080"), database: os.Getenv("DATABASE_URL"), traceReady: os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") != ""}
	if cfg.database == "" {
		return config{}, errors.New("DATABASE_URL is required")
	}
	chaos, err := strconv.ParseFloat(envOr("CHAOS_ERROR_RATE", "0.0"), 64)
	if err != nil || chaos < 0 || chaos > 1 {
		return config{}, errors.New("CHAOS_ERROR_RATE must be between 0.0 and 1.0")
	}
	cfg.chaosRate = chaos
	switch envOr("LOG_LEVEL", "info") {
	case "debug":
		cfg.logLevel = slog.LevelDebug
	case "info":
		cfg.logLevel = slog.LevelInfo
	case "warn":
		cfg.logLevel = slog.LevelWarn
	case "error":
		cfg.logLevel = slog.LevelError
	default:
		return config{}, errors.New("LOG_LEVEL must be debug, info, warn, or error")
	}
	return cfg, nil
}

func envOr(name, fallback string) string {
	if value := os.Getenv(name); value != "" {
		return value
	}
	return fallback
}

func configureTracing(ctx context.Context, enabled bool) (func(context.Context) error, error) {
	if !enabled {
		return func(context.Context) error { return nil }, nil
	}
	exporter, err := otlptracegrpc.New(ctx)
	if err != nil {
		return nil, fmt.Errorf("create trace exporter: %w", err)
	}
	res, err := resource.New(ctx, resource.WithAttributes(semconv.ServiceName("nereus-api")))
	if err != nil {
		return nil, fmt.Errorf("create trace resource: %w", err)
	}
	provider := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter), sdktrace.WithResource(res))
	otel.SetTracerProvider(provider)
	return provider.Shutdown, nil
}

func migrateUntilReady(ctx context.Context, db store, ready *atomic.Bool, logger *slog.Logger) {
	for {
		attempt, cancel := context.WithTimeout(ctx, 5*time.Second)
		err := db.Migrate(attempt)
		cancel()
		if err == nil {
			ready.Store(true)
			logger.Info("database migrations applied")
			return
		}
		logger.Warn("database unavailable; migration will retry", "error", err)
		select {
		case <-ctx.Done():
			return
		case <-time.After(5 * time.Second):
		}
	}
}
