package server

import (
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"syscall"

	"github.com/a-h/templ/examples/integration-gin/gintemplrenderer"
	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"github.com/haydenhargreaves/Potion/internal/app/service"
	domain "github.com/haydenhargreaves/Potion/internal/domain/server"
	"github.com/haydenhargreaves/Potion/internal/infrastructure/auth"
	"github.com/haydenhargreaves/Potion/internal/infrastructure/database/repository"
	"github.com/haydenhargreaves/Potion/internal/infrastructure/logging"
	"github.com/haydenhargreaves/Potion/internal/infrastructure/logging/loggers"
	"github.com/jmoiron/sqlx"

	_ "github.com/lib/pq"
)

type Server struct {
	port         int
	Router       *gin.Engine
	config       cors.Config
	DB           *sqlx.DB
	deps         domain.InjectedDependencies
	logs         []logging.Logger
	cleanupFuncs []func() error
}

// Init initializes the server with the provided port. CORS settings are defined here.
// A pointer to a server object is returned which allows for method chaining.
func Init(port int) *Server {
	server := &Server{
		Router:       gin.New(), // Not default anymore, to allow for custom logger
		port:         port,
		config:       cors.DefaultConfig(),
		logs:         []logging.Logger{},
		cleanupFuncs: []func() error{},
	}

	// Default logger which logs everything
	server.logs = append(server.logs, loggers.NewConsoleLogger(logging.LogLevelTrace))

	// Some stuff for templ rendering
	// TODO: Remove this
	htmlRenderer := server.Router.HTMLRender
	server.Router.HTMLRender = &gintemplrenderer.HTMLTemplRenderer{FallbackHtmlRenderer: htmlRenderer}

	// Disable proxy warnings
	server.Router.SetTrustedProxies(nil)

	// Setup the CORS settings and active them
	// server.config.AllowAllOrigins = true
	server.config.AllowOrigins = []string{"http://localhost:5173", "https://potion.gophernest.net"}
	server.config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE"}
	server.config.AllowHeaders = []string{"Origin", "Content-Type", "Authorization"}
	server.config.AllowCredentials = true
	server.Router.Use(cors.New(server.config))

	// We can use release mode since we don't need Gin's logging
	gin.SetMode(gin.ReleaseMode)

	return server
}

// Start starts the server on the port provided when the server was initialized
func (s *Server) Start() {
	logging.LogAll(s.logs, logging.LogLevelDebug, "Server started on :%d\n", s.port)

	// Create channel that only listens for SIGINT and SIGTERM
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

	go func() {
		if err := s.Router.Run(fmt.Sprintf(":%d", s.port)); err != nil {
			logging.LogAll(s.logs, logging.LogLevelFatal, "Server failed: %s\n", err)
		}
	}()

	// Block until we get a message on the quit channel
	<-quit
	logging.LogAll(s.logs, logging.LogLevelInfo, "Shutting down server...\n")

	s.cleanup()
	logging.LogAll(s.logs, logging.LogLevelInfo, "Server exited\n")
}

func (s *Server) cleanup() {
	for _, cleanup := range s.cleanupFuncs {
		// NOTE: Ignoring error
		cleanup()
	}
	if s.DB != nil {
		s.DB.Close()
	}
}

// TODO: (9/4/2025)  Abstract these functions and cleanup.	This is fucking messy...
// TODO: (1/26/2026) Abstract these functions and cleanup.	This is fucking messy... still
func (s *Server) Setup() *Server {
	// SETUP THE ENVIRONMENT CONFIGURATION
	cfg, err := domain.LoadEnvironment()
	if err != nil {
		logging.LogAll(s.logs, logging.LogLevelFatal, err.Error())
		panic(err.Error())
	}
	if cfg == nil {
		logging.LogAll(s.logs, logging.LogLevelFatal, "Environment configuration is nil, crashing.")
		panic("Environment configuration is nil, crashing.")
	}

	logging.LogAll(s.logs, logging.LogLevelDebug, "env: %+v\n", cfg)

	// SETUP GOOGLE AUTH
	var (
		// NOTE: USING V2 NOW
		redirectUrl  string   = fmt.Sprintf("%s%s", cfg.Domain, domain.API_AUTH_CALLBACK_V2)
		clientId     string   = cfg.GoogleClientId
		clientSecret string   = cfg.GoogleClientSecret
		scope        []string = []string{
			"https://www.googleapis.com/auth/userinfo.email",
			"https://www.googleapis.com/auth/userinfo.profile",
		}
	)

	// Setup Google OAuth
	auth.NewGoogleConfig(redirectUrl, clientId, clientSecret, scope)

	// SETUP DATABASE
	db, err := sqlx.Open("postgres", cfg.DatabaseUrl)
	if err != nil {
		panic("Could not connect to database: " + err.Error())
	}

	if err = db.Ping(); err != nil {
		panic("Error pinging database: " + err.Error())
	}

	s.DB = db

	// TODO: Implement environment here for logging file

	if cfg.LogFilePath != "" {
		fileLogger, cleanup, err := loggers.NewFileLogger(cfg.LogFilePath, logging.LogLevelDebug)
		if err != nil {
			logging.LogAll(s.logs, logging.LogLevelWarning, "Failed to create file logger. %s\n", err.Error())
		} else {
			logging.LogAll(s.logs, logging.LogLevelDebug, "Initialized file logger on file '%s'\n", cfg.LogFilePath)
			s.logs = append(s.logs, fileLogger)
			s.cleanupFuncs = append(s.cleanupFuncs, cleanup)
		}
	}

	databaseLogger, err := loggers.NewDatabaseLogger(s.DB, "logs", logging.LogLevelInfo)
	if err != nil {
		logging.LogAll(s.logs, logging.LogLevelWarning, "Failed to create database logger. %s\n", err.Error())
	} else {
		s.logs = append(s.logs, databaseLogger)
	}

	// SETUP JWT
	jwtSecret := []byte(cfg.JwtSecret)

	// Initialize and inject dependencies
	userRepo := repository.NewUserRepository(s.DB)
	recipeRepo := repository.NewRecipeRepository(s.DB)
	engagementRepo := repository.NewEngagementRepository(s.DB)
	userService := service.NewUserService(userRepo)
	authService := service.NewAuthService(userRepo, jwtSecret, s.logs)
	recipeService := service.NewRecipeService(recipeRepo, engagementRepo)
	engagementService := service.NewEngagementService(engagementRepo, recipeRepo)

	s.deps = domain.InjectedDependencies{
		UserService:       userService,
		AuthService:       authService,
		RecipeService:     recipeService,
		EngagementService: engagementService,
		EnvironmentConfig: *cfg,
	}

	// Apply middleware
	s.Router.Use(gin.Recovery(), RecoveryMiddleware(s.logs), LoggingMiddleware(s.logs))

	// Redirect index to home page: Update this as needed
	s.Router.GET("/", func(ctx *gin.Context) { ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) })

	// Wrap all routes with a version
	router_v1 := s.Router.Group(domain.VERSION_1)
	router_v2 := s.Router.Group(domain.VERSION_2)

	// Domain specific routers
	router_web := router_v1.Group(domain.WEB)
	router_api := router_v1.Group(domain.API)
	router_state := router_web.Group("state")

	// Static routes
	router_web.Static("/static", "./web/static")

	// API router endpoints
	router_api.GET("/", func(ctx *gin.Context) { ctx.JSON(200, gin.H{"message": "Server is active."}) })

	// WEB router endpoints
	router_web.GET("/", func(ctx *gin.Context) { ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) })
	router_web.GET("/login", s.LoginPageHandler)
	router_web.GET("/home", s.HomePageHandler)
	router_web.GET("/favorites", s.FavoritesPageHandler)
	router_web.GET("/create", s.CreatePageHandler)
	router_web.GET("/profile", s.ProfilePageHandler)
	router_web.GET("/list", s.ListPageHandler)
	router_web.GET("/recipe/:id", s.RecipePageHandler)
	router_web.GET("/search", s.SearchPageHandler)
	router_web.GET("/404", s.NotFoundPageHandler)

	// WEB state endpoints
	router_state.POST("/tags", s.NewTagHandler)
	router_state.POST("/tags/delete", s.DeleteTagHandler)

	// Authentication
	router_api.GET("/auth/login", s.GoogleLoginHandler)
	router_api.GET("/auth/callback", s.GoogleCallbackHandler)
	router_api.GET("/auth/logout", s.LogoutHandler)

	// Recipe endpoints
	router_api.POST("/recipe", s.CreateRecipeHandler)
	router_api.POST("/recipe/search", s.SearchRecipesHandler)
	router_api.POST("/recipe/search/favorites", s.SearchRecipesFavoritesHandler)

	router_api.GET("/user/recipes", s.GetUserFavoriteRecipesHandler)
	router_api.GET("/user/favorites", s.GetUserFavoriteRecipesHandler)

	// Engagement endpoints
	router_api.POST("/engagement/view/:id", s.EngagementViewRecipeHandler)
	router_api.POST("/engagement/share/:id", s.EngagementShareRecipeHandler)
	router_api.POST("/engagement/favorite/:id", s.EngagementFavoriteRecipeHandler)
	router_api.POST("/engagement/make/:id", s.EngagementMakeRecipeHandler)

	// Catch un-routed URLS
	s.Router.NoRoute(func(ctx *gin.Context) {
		path := ctx.Request.URL.Path

		// TODO: Use constants for errors?
		if strings.HasPrefix(path, domain.VERSION_1+domain.API) {
			ctx.JSON(http.StatusNotFound, gin.H{
				"status":  http.StatusNotFound,
				"error":   "API_NOT_FOUND",
				"message": "The request endpoint does not exist.",
				"path":    path,
			})
			return
		}

		ctx.Redirect(http.StatusSeeOther, domain.WEB_NOT_FOUND)
	})

	// ---- VERSION 2 ROUTES ---- //
	router_api_v2 := router_v2.Group(domain.API)
	router_api_v2.POST("/recipe", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.CreateRecipeHandlerV2)
	router_api_v2.GET("/recipe/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetRecipeHandlerV2)
	router_api_v2.PUT("/recipe/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EditRecipeHandlerV2)
	router_api_v2.GET("/recipe/of-the-week", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetRecipeOfTheWeekHandlerV2)
	router_api_v2.POST("/recipe/search", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.SearchRecipeHandlerV2)
	router_api_v2.DELETE("/recipe/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.DeleteRecipeHandlerV2)
	router_api_v2.GET("/recipe/:id/is-owner", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.IsRecipeOwnerV2)

	router_api_v2.GET("/auth/login", s.GetGoogleAuthUrlHandlerV2)
	router_api_v2.GET("/auth/callback", s.GoogleCallbackHandlerV2)
	router_api_v2.GET("/auth/logout", s.LogoutHandlerV2)

	router_api_v2.GET("/user", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenticatedUserHandlerV2)
	router_api_v2.GET("/user/:id", s.GetUserV2)
	router_api_v2.GET("/user/recipes", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserRecipesV2)
	router_api_v2.GET("/user/favorites", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserFavoritesV2)
	router_api_v2.GET("/user/engagement", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserEngagementV2)

	router_api_v2.GET("/user/recipes/made", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserMadeRecipesV2)
	router_api_v2.GET("/user/recipes/viewed", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserViewedRecipesV2)

	router_api_v2.POST("/engagement/view/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementViewRecipeHandlerV2)
	router_api_v2.POST("/engagement/share/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementShareRecipeHandlerV2)
	router_api_v2.POST("/engagement/favorite/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementFavoriteRecipeHandlerV2)
	router_api_v2.POST("/engagement/make/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementMakeRecipeHandlerV2)

	if cfg.Environment == "dev" {
		s.debugDisplayRoutes()
	}

	return s
}

func (s *Server) debugDisplayRoutes() {
	for _, route := range s.Router.Routes() {
		format := "%-8s %s"
		logging.LogAll(s.logs, logging.LogLevelDebug, format, route.Method, route.Path)
	}
}
