package main

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

	"wucher/internal/app/api"
	"wucher/internal/config"
	"wucher/internal/service"

	_ "wucher/docs"
)

var (
	apiRunFn   = run
	apiFatalFn = log.Fatal
)

func configFilePath() string {
	if v := os.Getenv("CONFIG_FILE"); v != "" {
		return v
	}
	basePath := os.Getenv("CONFIG_BASE_PATH")
	if basePath == "" {
		basePath = "/tmp"
	}
	filename := os.Getenv("CONFIG_FILENAME")
	if filename == "" {
		filename = "config.generated.json"
	}
	return basePath + "/" + filename
}

type apiServer interface {
	Start(addr string) error
	Shutdown(ctx context.Context) error
}

// @title Wucher API
// @version 1.0
// @description Wucher backend API (JSON:API).
// @description
// @description Auth Flow (Email/Password):
// @description 1) POST /api/v1/auth/register
// @description 2) Check email, open verify link (GET /api/v1/auth/verify-email)
// @description 3) POST /api/v1/auth/login
// @description - If TOTP enabled: response 202 with challenge_token
// @description 4) POST /api/v1/auth/totp/verify (challenge_token + code) -> cookies
// @description
// @description Auth Flow (TOTP setup):
// @description 1) POST /api/v1/auth/totp/setup -> secret + otpauth_url (enabled=false)
// @description 2) POST /api/v1/auth/totp/confirm -> enabled=true
// @description
// @description Auth Flow (Microsoft Entra SSO):
// @description 1) GET /api/v1/auth/microsoft/login -> redirect to Microsoft
// @description 2) Microsoft redirects to /api/v1/auth/microsoft/callback
// @description 3) Backend checks existing SSO mapping (no auto-register)
// @description 4) If linked user exists -> set cookies + optional redirect to AUTH_SSO_SUCCESS_REDIRECT
// @description 5) If mapping not linked -> login rejected (401)
// @description 6) Existing authenticated user can link/unlink via /api/v1/auth/sso/link and /api/v1/auth/sso/unlink
// @tag.name Complaints - Sign Off
// @tag.description Corrective-action sign-off for a helicopter. After a complaint's action_taken is recorded, it is signed off here (POST /action-signoffs/sign with complaint_id); a fleet-wide sign-off (no complaint_id) is also supported. A signed sign-off is the prerequisite for creating an EASA maintenance release.
// @BasePath /
func main() {
	if err := apiRunFn(); err != nil {
		apiFatalFn(err)
	}
}

func run() error {
	appCfg, err := config.LoadFromFile(configFilePath())
	if err != nil {
		return fmt.Errorf("load config: %w", err)
	}
	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()
	return runWithContext(ctx, appCfg)
}

func runWithContext(ctx context.Context, appCfg config.AppConfig) error {
	if ctx == nil {
		ctx = context.Background()
	}

	db, err := initializeDatabase(appCfg.MySQL)
	if err != nil {
		return err
	}
	if sqlDB, sqlErr := db.DB(); sqlErr == nil {
		defer sqlDB.Close()
	}

	if err := bootstrapData(db); err != nil {
		return err
	}
	repos, err := initializeRepositories(db)
	if err != nil {
		return err
	}
	infra, err := initializeInfrastructure(ctx, appCfg, db)
	if err != nil {
		return err
	}
	services, err := initializeServices(appCfg, repos, infra)
	if err != nil {
		return err
	}
	if err := services.MasterSettings.SeedMicrosoftEntraConfigIfMissing(ctx, appCfg.MicrosoftSSO); err != nil {
		return fmt.Errorf("seed microsoft entra master settings: %w", err)
	}
	handlersSet, err := initializeHandlers(services, infra)
	if err != nil {
		return err
	}
	if err := initializeRealtimeComponents(ctx, repos, services, infra); err != nil {
		return err
	}

	app, addr, err := buildHTTPServer(appCfg, services, handlersSet, infra)
	if err != nil {
		return err
	}
	srv := api.NewServer(app, api.WithShutdownTimeout(appCfg.HTTP.ShutdownTimeout))

	infra.Logger.Info("api listening",
		"addr", addr,
		"queue_backend", "sqs",
		"queue_outbox_enabled", appCfg.Queue.OutboxEnabled,
	)
	if err := serveAPI(ctx, srv, addr); err != nil {
		return err
	}
	infra.Logger.Info("api stopped")
	return nil
}

func serveAPI(ctx context.Context, srv apiServer, addr string) error {
	if srv == nil {
		return errors.New("api server is required")
	}
	if ctx == nil {
		ctx = context.Background()
	}

	serverErrCh := make(chan error, 1)
	go func() {
		serverErrCh <- srv.Start(addr)
	}()

	select {
	case err := <-serverErrCh:
		return err
	case <-ctx.Done():
	}

	shutdownErr := srv.Shutdown(context.Background())
	serverErr := <-serverErrCh
	if shutdownErr != nil {
		return shutdownErr
	}
	return serverErr
}

func startFileManagerAutoPurgeLoop(
	ctx context.Context,
	fileManagerSvc *service.FileManagerService,
	trashCfg config.FileManagerTrashPolicyConfig,
	logger *slog.Logger,
) {
	if fileManagerSvc == nil || !trashCfg.AutoPurgeEnabled {
		return
	}
	interval := trashCfg.AutoPurgeInterval
	if interval <= 0 {
		interval = time.Hour
	}
	batchSize := trashCfg.AutoPurgeBatchSize
	if batchSize <= 0 {
		batchSize = 200
	}

	runOnce := func() {
		purgedFiles, purgedFolders, err := fileManagerSvc.PurgeExpiredTrash(ctx, time.Now().UTC(), batchSize)
		if err != nil {
			if logger != nil {
				logger.Error("file manager auto-purge run failed", "error", err)
			}
			return
		}
		if logger != nil && (purgedFiles > 0 || purgedFolders > 0) {
			logger.Info(
				"file manager auto-purge completed",
				"purged_files", purgedFiles,
				"purged_folders", purgedFolders,
				"batch_size", batchSize,
			)
		}
	}

	go func() {
		ticker := time.NewTicker(interval)
		defer ticker.Stop()
		runOnce()
		for {
			select {
			case <-ctx.Done():
				return
			case <-ticker.C:
				runOnce()
			}
		}
	}()
}

func startFleetStatusAOGRecomputeLoop(
	ctx context.Context,
	fleetStatusSvc *service.FleetStatusService,
	logger *slog.Logger,
) {
	if fleetStatusSvc == nil {
		return
	}
	// TESTING ONLY: sweep tiap menit supaya kolom AOG + audit nyusul cepat dengan
	// MEL grace yang sementara dihitung dalam menit.
	// TODO: kembalikan ke time.Hour sebelum production.
	interval := time.Minute
	runOnce := func() {
		n, err := fleetStatusSvc.RecalculateAllAOG(ctx)
		if err != nil {
			if logger != nil {
				logger.Error("fleet status AOG recompute failed", "error", err)
			}
			return
		}
		if logger != nil {
			logger.Info("fleet status AOG recompute completed", "helicopters", n)
		}
	}
	go func() {
		ticker := time.NewTicker(interval)
		defer ticker.Stop()
		runOnce()
		for {
			select {
			case <-ctx.Done():
				return
			case <-ticker.C:
				runOnce()
			}
		}
	}()
}

func startFileManagerLifecycleCleanupLoop(
	ctx context.Context,
	fileManagerSvc *service.FileManagerService,
	lifecycleCfg config.FileManagerLifecycleConfig,
	logger *slog.Logger,
) {
	if fileManagerSvc == nil || !lifecycleCfg.EnableCleanupJob {
		return
	}
	interval := time.Hour
	batchSize := lifecycleCfg.CleanupBatchSize
	if batchSize <= 0 {
		batchSize = 100
	}
	grace := time.Duration(lifecycleCfg.OrphanGraceDays) * 24 * time.Hour
	tempGrace := time.Duration(lifecycleCfg.TempUploadGraceHours) * time.Hour
	dryRun := lifecycleCfg.CleanupDryRun

	runOnce := func() {
		stats, err := fileManagerSvc.CleanupOrphanPendingFiles(ctx, time.Now().UTC(), grace, batchSize, dryRun)
		if err != nil {
			if logger != nil {
				logger.Error("file manager lifecycle cleanup failed", "error", err)
			}
			return
		}
		if logger != nil {
			logger.Info("file manager lifecycle cleanup run completed",
				"scanned", stats.Scanned,
				"deleted", stats.Deleted,
				"skipped", stats.Skipped,
				"failed", stats.Failed,
				"dry_run", stats.DryRun,
				"batch_size", batchSize,
			)
		}

		tempStats, err := fileManagerSvc.CleanupStaleTempUploads(ctx, time.Now().UTC(), tempGrace, batchSize, dryRun)
		if err != nil {
			if logger != nil {
				logger.Error("file manager temp upload cleanup failed", "error", err)
			}
			return
		}
		if logger != nil {
			logger.Info("file manager temp upload cleanup run completed",
				"scanned", tempStats.Scanned,
				"deleted", tempStats.Deleted,
				"skipped", tempStats.Skipped,
				"failed", tempStats.Failed,
				"dry_run", tempStats.DryRun,
				"batch_size", batchSize,
			)
		}

		draftStats, err := fileManagerSvc.CleanupStaleDraftFiles(ctx, time.Now().UTC(), tempGrace, batchSize, dryRun)
		if err != nil {
			if logger != nil {
				logger.Error("file manager draft cleanup failed", "error", err)
			}
			return
		}
		if logger != nil {
			logger.Info("file manager draft cleanup run completed",
				"scanned", draftStats.Scanned,
				"deleted", draftStats.Deleted,
				"skipped", draftStats.Skipped,
				"failed", draftStats.Failed,
				"dry_run", draftStats.DryRun,
				"batch_size", batchSize,
			)
		}
	}

	go func() {
		ticker := time.NewTicker(interval)
		defer ticker.Stop()
		runOnce()
		for {
			select {
			case <-ctx.Done():
				return
			case <-ticker.C:
				runOnce()
			}
		}
	}()
}

func startHelicopterFileAttachmentReconcileLoop(
	ctx context.Context,
	helicopterFileSvc *service.HelicopterFileService,
	logger *slog.Logger,
) {
	if helicopterFileSvc == nil {
		return
	}
	interval := 2 * time.Second
	batchSize := 50

	runOnce := func() int {
		finalized, err := helicopterFileSvc.FinalizePendingAttachments(ctx, batchSize)
		if err != nil {
			if logger != nil {
				logger.Error("helicopter file attachment reconcile failed", "error", err)
			}
			return 0
		}
		if logger != nil && finalized > 0 {
			logger.Info("helicopter file attachment reconcile completed", "finalized", finalized, "batch_size", batchSize)
		}
		return finalized
	}

	go func() {
		ticker := time.NewTicker(interval)
		defer ticker.Stop()
		for {
			if ctx.Err() != nil {
				return
			}
			if finalized := runOnce(); finalized > 0 {
				continue
			}
			select {
			case <-ctx.Done():
				return
			case <-ticker.C:
			}
		}
	}()
}
