package main

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

	"commilitia.net/cdrop/internal/calls"
	"commilitia.net/cdrop/internal/clipboard"
	"commilitia.net/cdrop/internal/config"
	"commilitia.net/cdrop/internal/db"
	"commilitia.net/cdrop/internal/httpapi"
	"commilitia.net/cdrop/internal/hub"
	"commilitia.net/cdrop/internal/jwtauth"
	"commilitia.net/cdrop/internal/push"
	"commilitia.net/cdrop/internal/relay"
	"commilitia.net/cdrop/internal/transfer"
)

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
	slog.SetDefault(logger)

	cfg, err := config.Load()
	if err != nil {
		slog.Error("config load failed", "err", err)
		os.Exit(1)
	}
	slog.Info("cdropd booting",
		"auth_mode", cfg.AuthMode,
		"listen", cfg.Listen,
		"db_path", cfg.DBPath,
	)

	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer cancel()

	dbConn, err := db.Open(cfg.DBPath)
	if err != nil {
		slog.Error("db open failed", "err", err)
		os.Exit(1)
	}
	defer dbConn.Close()

	if err := db.Bootstrap(ctx, dbConn); err != nil {
		slog.Error("db bootstrap failed", "err", err)
		os.Exit(1)
	}

	queries := db.New(dbConn)
	auth := jwtauth.New(cfg, queries)
	h := hub.New(queries)
	defer h.Close()
	relayMgr := relay.NewManager(relay.DefaultMaxSessions, relay.DefaultSessionCap)
	pushSender := push.NewSender(queries, cfg.VAPIDPublicKey, cfg.VAPIDPrivateKey, cfg.VAPIDSubjectOrDefault())
	if pushSender.Enabled() {
		slog.Info("web push enabled (VAPID configured)")
	} else {
		slog.Info("web push disabled (VAPID keys unset)")
	}
	transfers := transfer.NewService(queries, h, relayMgr, pushSender)

	var cfTurn *calls.Provider
	if cfg.CFTurnKeyID != "" && cfg.CFTurnAPIToken != "" {
		cfTurn = calls.NewProvider(cfg.CFTurnKeyID, cfg.CFTurnAPIToken, calls.DefaultTTL)
		slog.Info("cloudflare realtime TURN enabled")
	} else {
		slog.Info("cloudflare TURN not configured; falling back to STUN-only")
	}

	clip := clipboard.New(
		queries,
		h,
		cfg.ClipboardMaxBytes,
		time.Duration(cfg.ClipboardDebounceSec)*time.Second,
		time.Duration(cfg.ClipboardTTLSec)*time.Second,
	)

	go transfer.RunSweeper(ctx, transfers)
	go relayMgr.RunReaper(ctx)
	go jwtauth.RunDeviceSweeper(ctx, queries, time.Duration(cfg.DeviceTTLHours)*time.Hour)
	go httpapi.RunWebSessionReaper(ctx, queries)
	if cfg.ClipboardTTLSec > 0 {
		go clipboard.RunSweeper(ctx, clip)
	}

	srv := &http.Server{
		Addr:              cfg.Listen,
		Handler:           httpapi.New(cfg, dbConn, queries, auth, h, transfers, relayMgr, cfTurn, clip, pushSender).Handler(),
		ReadHeaderTimeout: 5 * time.Second,
	}

	go func() {
		slog.Info("http server listening", "addr", cfg.Listen)
		if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			slog.Error("http server error", "err", err)
			cancel()
		}
	}()

	<-ctx.Done()
	slog.Info("shutdown initiated")

	shutCtx, shutCancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer shutCancel()
	if err := srv.Shutdown(shutCtx); err != nil {
		slog.Error("http shutdown error", "err", err)
	}
	slog.Info("cdropd stopped")
}
