package main

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

	"server/internal/auth"
	"server/internal/config"
	"server/internal/db"
	"server/internal/http/controllers"
	"server/internal/http/routes"
	"server/internal/mail"
	"server/internal/roles"
	"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()

	configPath := envOrDefault("CONFIG_PATH", "configs/config.json")
	cfg, err := config.LoadConfig(configPath)
	if err != nil {
		log.Fatalf("load config: %v", err)
	}
	if secret := os.Getenv("AUTH_SECRET"); secret != "" {
		cfg.Auth.Secret = secret
	}

	dbCfg := db.Config{
		Driver: envOrDefault("DB_driver", "sqlite"),
		DSN:    envOrDefault("DB_dsn", "file:./data/data.db?_foreign_keys=on"),
	}
	dbConn, err := db.Init(dbCfg)
	if err != nil {
		log.Fatalf("init db: %v", err)
	}

	authService, err := auth.New(auth.Config{
		Secret:             cfg.Auth.Secret,
		Issuer:             cfg.Auth.Issuer,
		AccessTokenExpiry:  time.Duration(cfg.Auth.AccessTokenExpiryMinutes) * time.Minute,
		RefreshTokenExpiry: time.Duration(cfg.Auth.RefreshTokenExpiryMinutes) * time.Minute,
	})
	if err != nil {
		log.Fatalf("setup auth: %v", err)
	}

	mailService, err := mail.New(mail.Config{
		AppName:           cfg.AppName,
		Mode:              cfg.Mail.Mode,
		From:              cfg.Mail.From,
		DebugDir:          cfg.Mail.DebugDir,
		TemplatesDir:      cfg.Mail.TemplatesDir,
		FrontendBaseURL:   cfg.Mail.FrontendBaseURL,
		ResetPasswordPath: cfg.Mail.ResetPasswordPath,
		SMTP: mail.SMTPConfig{
			Host:     cfg.Mail.SMTP.Host,
			Port:     cfg.Mail.SMTP.Port,
			Username: cfg.Mail.SMTP.Username,
			Password: cfg.Mail.SMTP.Password,
		},
	})
	if err != nil {
		log.Fatalf("setup mail: %v", err)
	}

	roleConfigPath := cfg.RolesConfigPath
	if envRoleConfig := os.Getenv("ROLES_CONFIG_PATH"); envRoleConfig != "" {
		roleConfigPath = envRoleConfig
	}
	if roleConfigPath == "" {
		roleConfigPath = envOrDefault("ROLES_CONFIG_PATH", "configs/roles.json")
	}
	roleResolver, err := controllers.LoadRoleConfig(roleConfigPath)
	if err != nil {
		log.Fatalf("load role config: %v", err)
	}
	roles.CheckUserRoleConsistency(dbConn, roleResolver)

	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"
			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"
			}
			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(controllers.RequireEndpointPermission(roleResolver, authService))
	routes.Register(app, authService, mailService)

	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)
		}
	}
}
