package main

import (
	"bufio"
	"flag"
	"log"
	"os"
	"os/signal"
	"strconv"
	"strings"
	"syscall"
	"time"

	"server/internal/config"
	"server/internal/db"
	"server/internal/migrations"
	"server/internal/roles"
	"server/internal/routes"
	"server/internal/tokens"

	"server/internal/seed"

	"github.com/gofiber/fiber/v3"
	"github.com/gofiber/fiber/v3/middleware/cors"
	"github.com/gofiber/fiber/v3/middleware/logger"
	"github.com/gofiber/fiber/v3/middleware/recover"
	"github.com/gofiber/fiber/v3/middleware/requestid"
)

// Typescript: TSDeclaration= Nullable<T> = T | null;
// Typescript: TSDeclaration= Record<K extends string | number | symbol, T> = { [P in K]: T; }

func main() {
	loadDotEnv(".env")

	seedCount := flag.Int("seed", 0, "seed N fake users at startup (0 to skip)")
	flag.Parse()

	cfg, err := config.GetConfig()
	if err != nil {
		log.Fatalf("config: %v", err)
	}

	dbConn, err := db.GetDB()
	if err != nil {
		log.Fatalf("init db: %v", err)
	}

	if err := migrations.AutoMigrate(dbConn); err != nil {
		log.Fatalf("migrate user: %v", err)
	}

	tokenService, err := tokens.GetTockenService()
	if err != nil {
		log.Fatalf("init tokens: %v", err)
	}

	app := fiber.New(fiber.Config{
		AppName:      cfg.AppName,
		ReadTimeout:  time.Duration(cfg.ReadTimeoutSeconds) * time.Second,
		WriteTimeout: time.Duration(cfg.WriteTimeoutSeconds) * time.Second,
		IdleTimeout:  time.Duration(cfg.IdleTimeoutSeconds) * time.Second,
		ErrorHandler: func(c fiber.Ctx, err error) error {
			code := fiber.StatusInternalServerError
			msg := "internal server error: " + err.Error()
			if e, ok := err.(*fiber.Error); ok {
				code = e.Code
				msg = e.Message
			}
			reqID := requestid.FromContext(c)
			log.Printf("error request_id=%s status=%d method=%s path=%s ip=%s ua=%q err=%v", reqID, code, c.Method(), c.Path(), c.IP(), c.Get("User-Agent"), err)
			if code >= 500 {
				msg = "internal server error: " + err.Error()
			}
			return c.Status(code).JSON(fiber.Map{"data": nil, "error": msg})
		},
	})

	app.Use(requestid.New())
	app.Use(recover.New())
	app.Use(logger.New())
	app.Use(cors.New(cors.Config{
		AllowOrigins:     []string{"http://localhost:9000"},
		AllowMethods:     []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
		AllowHeaders:     []string{"Origin", "Auth-Token", "cache-control", "Content-Type", "Accept", "Authorization"},
		AllowCredentials: true,
		ExposeHeaders:    []string{"Auth-Token"},
	}))

	app.Options("/*", func(c fiber.Ctx) error {
		return c.SendStatus(fiber.StatusNoContent)
	})

	// Make the DB handle available in request context for future handlers.
	app.Use(func(c fiber.Ctx) error {
		c.Locals("db", dbConn)
		return c.Next()
	})

	app.Use(roles.RequireEndpointPermission(dbConn, tokenService))

	routes.Register(app)

	port := envOrDefault("PORT", "3000")

	seedToRun := *seedCount
	if seedToRun <= 0 {
		seedToRun = envIntOrDefault("SEED", 0)
	}
	if seedToRun > 0 {
		_, creds, err := seed.SeedUsers(dbConn, seedToRun)
		if err != nil {
			log.Fatalf("seed users: %v", err)
		}
		for _, cred := range creds {
			log.Printf("seeded user email=%s password=%s", cred.Email, cred.Password)
		}
	}

	// Graceful shutdown so ctrl+c or SIGTERM stops the server cleanly.
	go func() {
		if err := app.Listen(":" + port); err != nil {
			log.Fatalf("server failed: %v", err)
		}
	}()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit

	log.Println("Shutting down server...")
	if err := app.Shutdown(); err != nil {
		log.Printf("shutdown error: %v", err)
	}
}

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

func envIntOrDefault(key string, fallback int) int {
	value := os.Getenv(key)
	if value == "" {
		return fallback
	}
	parsed, err := strconv.Atoi(value)
	if err != nil {
		log.Printf("warning: invalid %s=%q, using default %d", key, value, fallback)
		return fallback
	}
	return parsed
}

func loadDotEnv(path string) {
	file, err := os.Open(path)
	if err != nil {
		return
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		parts := strings.SplitN(line, "=", 2)
		if len(parts) != 2 {
			continue
		}
		key := strings.TrimSpace(parts[0])
		value := strings.TrimSpace(parts[1])
		if key == "" {
			continue
		}
		if _, exists := os.LookupEnv(key); !exists {
			_ = os.Setenv(key, value)
		}
	}
}
