package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"

	"github.com/docker/docker/client"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

type Orchestrator struct {
	config *Config
	db     *gorm.DB
	docker client.APIClient
	api    *APIClient

	// gsltPool is rebuilt from the DB on startup, see rebuildGSLTAllocations.
	gsltPool []GSLTAllocation
	gsltMu   sync.Mutex
}

func main() {
	log.SetFlags(log.LstdFlags | log.Lshortfile)
	log.Println("tapper game server orchestrator starting...")

	cfg, err := LoadConfig()
	if err != nil {
		log.Fatalf("failed to load config: %v", err)
	}
	log.Printf("config loaded: public_host=%s ports=%d-%d gslt_tokens=%d image=%s",
		cfg.PublicHost, cfg.PortRangeStart, cfg.PortRangeEnd, len(cfg.GSLTTokens), cfg.GameserverImage)

	db, err := connectDB(cfg.DatabaseURL)
	if err != nil {
		log.Fatalf("failed to connect to database: %v", err)
	}
	log.Println("database connected")

	if err := db.AutoMigrate(&GameServer{}, &PortPool{}); err != nil {
		log.Fatalf("failed to run migrations: %v", err)
	}
	log.Println("database migrations complete")

	dockerClient, err := connectDocker(cfg.DockerHost)
	if err != nil {
		log.Fatalf("failed to connect to Docker: %v", err)
	}
	defer dockerClient.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	info, err := dockerClient.Info(ctx)
	cancel()
	if err != nil {
		log.Fatalf("failed to ping Docker daemon: %v", err)
	}
	log.Printf("Docker connected: %s (containers: %d)", info.Name, info.Containers)

	apiClient := NewAPIClient(cfg.APIURL, cfg.APISecret)

	orch := &Orchestrator{
		config: cfg,
		db:     db,
		docker: dockerClient,
		api:    apiClient,
	}

	orch.gsltPool = make([]GSLTAllocation, len(cfg.GSLTTokens))
	for i, token := range cfg.GSLTTokens {
		orch.gsltPool[i] = GSLTAllocation{Token: token}
	}
	log.Printf("GSLT pool initialized with %d tokens", len(orch.gsltPool))

	if err := orch.SeedPortPool(); err != nil {
		log.Fatalf("failed to seed port pool: %v", err)
	}

	reconCtx, reconCancel := context.WithTimeout(context.Background(), 60*time.Second)
	if err := orch.ReconcileOnStartup(reconCtx); err != nil {
		// Continue anyway, the loops will handle inconsistencies.
		log.Printf("WARNING: startup reconciliation failed: %v", err)
	}
	reconCancel()

	ctx, cancel = context.WithCancel(context.Background())
	defer cancel()

	var wg sync.WaitGroup

	wg.Add(1)
	go func() {
		defer wg.Done()
		orch.ProvisioningLoop(ctx)
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		orch.HealthCheckLoop(ctx)
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		orch.CleanupLoop(ctx)
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		orch.LiveServerMonitorLoop(ctx)
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		orch.MonitorContainerEvents(ctx)
	}()

	go func() {
		mux := http.NewServeMux()
		mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
			w.WriteHeader(http.StatusOK)
			w.Write([]byte(`{"status":"ok"}`))
		})
		if err := http.ListenAndServe(":8081", mux); err != nil && err != http.ErrServerClosed {
			log.Printf("health server error: %v", err)
		}
	}()

	log.Println("all orchestrator loops started")

	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
	sig := <-sigCh
	log.Printf("received signal %v, initiating graceful shutdown...", sig)

	cancel()

	done := make(chan struct{})
	go func() {
		wg.Wait()
		close(done)
	}()

	select {
	case <-done:
		log.Println("all loops stopped cleanly")
	case <-time.After(30 * time.Second):
		log.Println("WARNING: shutdown timed out after 30s, forcing exit")
	}

	log.Println("orchestrator stopped")
}

func connectDB(databaseURL string) (*gorm.DB, error) {
	gormLogger := logger.New(
		log.New(os.Stdout, "[gorm] ", log.LstdFlags),
		logger.Config{
			SlowThreshold:             200 * time.Millisecond,
			LogLevel:                  logger.Warn,
			IgnoreRecordNotFoundError: true,
			Colorful:                  false,
		},
	)

	db, err := gorm.Open(postgres.Open(databaseURL), &gorm.Config{
		Logger: gormLogger,
	})
	if err != nil {
		return nil, err
	}

	sqlDB, err := db.DB()
	if err != nil {
		return nil, err
	}
	sqlDB.SetMaxIdleConns(5)
	sqlDB.SetMaxOpenConns(20)
	sqlDB.SetConnMaxLifetime(30 * time.Minute)

	return db, nil
}

func connectDocker(dockerHost string) (client.APIClient, error) {
	opts := []client.Opt{
		client.FromEnv,
		client.WithAPIVersionNegotiation(),
	}

	if dockerHost != "" {
		opts = append(opts, client.WithHost(dockerHost))
	}

	return client.NewClientWithOpts(opts...)
}
