package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/url"
	"os"
	"strings"

	_ "git.dustin.coffee/hobokenchicken/dumpsterChat/docs"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/audit"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/auth"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/bot"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/channel"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/db"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/dm"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/email"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/gateway"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/giphy"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/invite"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/message"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/moderation"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/notification"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/permissions"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/push"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/reaction"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/readstate"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/server"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/servergroup"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/upload"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/voice"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/webhook"
	"github.com/go-chi/chi/v5"
	chimw "github.com/go-chi/chi/v5/middleware"
	httpSwagger "github.com/swaggo/http-swagger"
)

// @title Dumpster API
// @version 1.0
// @description A chaotic, self-hosted Discord-like platform API
// @host localhost:8080
// @BasePath /api/v1
// @schemes http https
// @securityDefinitions.apikey SessionAuth
// @in cookie
// @name dumpster_session
func main() {
	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

	cfg := config.Load()

	database, err := db.New(cfg)
	if err != nil {
		logger.Error("failed to connect to database", "error", err)
		os.Exit(1)
	}
	defer database.Close()

	if err := database.RunMigrations(); err != nil {
		logger.Error("failed to run migrations", "error", err)
		os.Exit(1)
	}

	// Session store for middleware
	sessionStore := auth.NewSessionStore(database.DB, cfg)

	// WebSocket origin allowlist
	wsOrigins := []string{"https://" + cfg.Host}
	if cfg.Host == "localhost" {
		wsOrigins = append(wsOrigins, "http://localhost:"+cfg.Port)
	}
	gateway.SetAllowedOrigins(wsOrigins)

	// WebSocket hub
	hub := gateway.NewHub(database.DB, logger)
	go hub.Run()

	// Giphy client (nil if no API key)
	giphyClient := giphy.NewClient(cfg.Giphy.APIKey)

	// Upload handler (nil if no MinIO config)
	uploadHandler, err := upload.NewHandler(cfg)
	if err != nil {
		logger.Warn("upload handler not available", "error", err)
	}

	// Voice client (LiveKit)
	voiceClient := voice.NewClient(cfg.LiveKit.APIKey, cfg.LiveKit.Secret, cfg.LiveKit.URL)
	if voiceClient == nil {
		logger.Warn("voice client not configured (missing LIVEKIT_API_KEY/SECRET)")
	}

	// Push notification handler (nil if no VAPID keys)
	pushHandler := push.NewHandler(database.DB, cfg.WebPush.PublicKey, cfg.WebPush.PrivateKey, cfg.WebPush.Subject, logger)
	if cfg.WebPush.PublicKey == "" {
		logger.Warn("push notifications not configured (missing VAPID_PUBLIC_KEY)")
	}

	// Email mailer
	mailer := email.NewMailer(cfg.SMTP)

	// Auth handler
	authHandler := auth.NewHandler(database.DB, cfg, hub, mailer)

	// Permissions checker
	permissionsChecker := permissions.NewChecker(database.DB)
	memberHandler := server.NewMemberHandler(database.DB)

	r := chi.NewRouter()
	r.Use(chimw.Logger)
	r.Use(chimw.Recoverer)
	r.Use(chimw.RequestID)
	r.Use(middleware.SecurityHeaders)

	// Health check
	r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("ok"))
	})

	// WebSocket endpoint
	r.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
		gateway.ServeWS(database.DB, hub, logger, w, r, cfg.Session.CookieName)
	})

	// API routes
	r.Route("/api/v1", func(r chi.Router) {
		// Auth (public: register, login, logout) with strict rate limiting
		r.Route("/auth", func(r chi.Router) {
			r.Use(middleware.RateLimit(5, 10)) // 5 req/s, burst 10
			authHandler.RegisterPublicRoutes(r)
		})

		// Protected routes
		r.Group(func(r chi.Router) {
			r.Use(middleware.Session(sessionStore, cfg))
			r.Use(middleware.RequireAuth)
			r.Use(middleware.CSRFProtect(cfg.Host, cfg.Port, strings.Split(os.Getenv("DUMPSTER_CSRF_ORIGINS"), ",")))

			// Auth (protected: me, update profile)
			authHandler.RegisterProtectedRoutes(r)

			// Servers
			r.Route("/servers", func(r chi.Router) {
				serverHandler := server.NewHandler(database.DB)
				r.Post("/", serverHandler.Create)
				r.Get("/", serverHandler.List)

				modHandler := moderation.NewHandler(database.DB, hub)
				auditLogger := audit.NewLogger(database.DB)
				auditHandler := audit.NewHandler(database.DB)

				r.Route("/{serverID}", func(r chi.Router) {
					r.Get("/", serverHandler.Get)
					r.Patch("/", serverHandler.Update)
					r.Delete("/", serverHandler.Delete)

					// Audit log
					r.With(middleware.RequirePermission(permissionsChecker, permissions.MANAGE_SERVER)).Get("/audit-log", auditHandler.List)

					// Server members
					memberHandler.RegisterRoutes(r)

					// Moderation
					r.With(middleware.RequirePermission(permissionsChecker, permissions.KICK_MEMBERS)).Delete("/members/{userID}", func(w http.ResponseWriter, r *http.Request) {
						modHandler.Kick(w, r)
						serverID := chi.URLParam(r, "serverID")
						targetID := chi.URLParam(r, "userID")
						userID, _ := middleware.UserIDFromContext(r.Context())
						_ = auditLogger.Log(r.Context(), serverID, userID, audit.ActionKick, "user", targetID, "", nil)
					})
					r.With(middleware.RequirePermission(permissionsChecker, permissions.BAN_MEMBERS)).Post("/bans", func(w http.ResponseWriter, r *http.Request) {
						modHandler.Ban(w, r)
					})
					r.With(middleware.RequirePermission(permissionsChecker, permissions.BAN_MEMBERS)).Get("/bans", modHandler.ListBans)
					r.With(middleware.RequirePermission(permissionsChecker, permissions.BAN_MEMBERS)).Delete("/bans/{userID}", func(w http.ResponseWriter, r *http.Request) {
						modHandler.Unban(w, r)
						serverID := chi.URLParam(r, "serverID")
						targetID := chi.URLParam(r, "userID")
						userID, _ := middleware.UserIDFromContext(r.Context())
						_ = auditLogger.Log(r.Context(), serverID, userID, audit.ActionUnban, "user", targetID, "", nil)
					})
					r.With(middleware.RequirePermission(permissionsChecker, permissions.MUTE_MEMBERS)).Post("/mutes", func(w http.ResponseWriter, r *http.Request) {
						modHandler.Mute(w, r)
					})
					r.With(middleware.RequirePermission(permissionsChecker, permissions.MUTE_MEMBERS)).Delete("/mutes/{userID}", func(w http.ResponseWriter, r *http.Request) {
						modHandler.Unmute(w, r)
						serverID := chi.URLParam(r, "serverID")
						targetID := chi.URLParam(r, "userID")
						userID, _ := middleware.UserIDFromContext(r.Context())
						_ = auditLogger.Log(r.Context(), serverID, userID, audit.ActionUnmute, "user", targetID, "", nil)
					})

					// Channels (nested under servers)
					r.Route("/channels", func(r chi.Router) {
						channel.NewHandler(database.DB, permissionsChecker).RegisterRoutes(r)
					})

					// Server groups (sub-server sections)
					r.Route("/groups", func(r chi.Router) {
						servergroup.NewHandler(database.DB).RegisterRoutes(r)
					})

					// Availability
					r.Get("/availability", authHandler.GetServerAvailability)
				})
			})

			// Direct messages
			dmHandler := dm.NewHandler(database.DB, hub, pushHandler, logger)
			r.Route("/conversations", func(r chi.Router) {
				dmHandler.RegisterRoutes(r)
			})

			// Roles and member-role assignment
			roleHandler := server.NewRoleHandler(database.DB)
			roleHandler.RegisterRoleRoutes(r)

			// Messages (under channels)
			r.Route("/channels/{channelID}/messages", func(r chi.Router) {
								message.NewHandler(database.DB, hub, pushHandler, logger, permissionsChecker).RegisterRoutes(r)
			})

			// Polls
			pollHandler := message.NewPollHandler(database.DB, hub, permissionsChecker)
			r.Route("/polls", func(r chi.Router) {
				pollHandler.RegisterRoutes(r)
			})

			// Feature requests
			frHandler := message.NewFeatureRequestHandler(database.DB)
			r.Route("/servers/{serverID}/feature-requests", func(r chi.Router) {
				frHandler.RegisterRoutes(r)
			})

			// Per-channel notification settings
			r.Route("/channels/{channelID}/notifications", func(r chi.Router) {
				notification.NewHandler(database.DB, logger).RegisterRoutes(r)
			})

			// Push notifications
			pushHandler.RegisterRoutes(r)

			// Read receipts
			rsHandler := readstate.NewHandler(database.DB, logger)
			r.Put("/channels/{channelID}/read", rsHandler.MarkRead)
			r.Get("/users/me/read-states", rsHandler.GetAll)

			// Giphy search
			if giphyClient != nil {
				r.Get("/gifs/search", func(w http.ResponseWriter, r *http.Request) {
					query := r.URL.Query().Get("q")
					if query == "" {
						http.Error(w, `{"error":"missing query"}`, http.StatusBadRequest)
						return
					}
					gifs, err := giphyClient.Search(query, 20)
					if err != nil {
						http.Error(w, `{"error":"search failed"}`, http.StatusInternalServerError)
						return
					}
					w.Header().Set("Content-Type", "application/json")
					json.NewEncoder(w).Encode(gifs)
				})
				r.Get("/gifs/trending", func(w http.ResponseWriter, r *http.Request) {
					gifs, err := giphyClient.GetTrending(20)
					if err != nil {
						http.Error(w, `{"error":"trending failed"}`, http.StatusInternalServerError)
						return
					}
					w.Header().Set("Content-Type", "application/json")
					json.NewEncoder(w).Encode(gifs)
				})
				r.Get("/gifs/proxy", func(w http.ResponseWriter, r *http.Request) {
					targetURL := r.URL.Query().Get("url")
					if targetURL == "" {
						http.Error(w, `{"error":"missing url"}`, http.StatusBadRequest)
						return
					}

					// Verify it's a Giphy domain to prevent general proxy abuse
					parsed, err := url.Parse(targetURL)
					if err != nil || parsed.Scheme != "https" || !strings.HasSuffix(parsed.Host, ".giphy.com") {
						http.Error(w, `{"error":"invalid proxy target"}`, http.StatusBadRequest)
						return
					}

					resp, err := http.Get(targetURL)
					if err != nil {
						http.Error(w, `{"error":"failed to fetch image"}`, http.StatusInternalServerError)
						return
					}
					defer resp.Body.Close()

					// Stream response
					w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
					w.Header().Set("Content-Length", resp.Header.Get("Content-Length"))
					w.Header().Set("Cache-Control", "public, max-age=604800") // Cache for 7 days
					w.WriteHeader(resp.StatusCode)
					io.Copy(w, resp.Body)
				})
			}

			// File upload
			if uploadHandler != nil {
				r.Post("/upload", uploadHandler.Upload)
			}

			// Voice
			if voiceClient != nil {
				r.Route("/voice", func(r chi.Router) {
					voice.NewHandler(database.DB, voiceClient, hub).RegisterRoutes(r)
				})
			}

			// Reactions
			r.Route("/messages/{messageID}/reactions", func(r chi.Router) {
				reaction.NewHandler(database.DB, hub).RegisterRoutes(r)
			})

			// Bots
			r.Route("/bots", func(r chi.Router) {
				bot.NewHandler(database.DB).RegisterRoutes(r)
			})

			// Bot slash commands
			r.Route("/bots/{botID}/commands", func(r chi.Router) {
				bot.NewCommandHandler(database.DB).RegisterCommandRoutes(r)
			})

			// Webhooks (protected: create/list/delete)
			webhookHandler := webhook.NewHandler(database.DB, hub)
			r.Route("/channels/{channelID}/webhooks", func(r chi.Router) {
				webhookHandler.RegisterRoutes(r)
			})
			r.Delete("/webhooks/{webhookID}", webhookHandler.Delete)

			// Invites (rate limited to prevent brute-force join)
			inviteHandler := invite.NewHandler(database.DB)
			r.Post("/servers/{serverID}/invites", inviteHandler.Create)
			r.Get("/invites/{code}", inviteHandler.Get)
			r.Route("/invites/{code}/join", func(r chi.Router) {
				r.Use(middleware.RateLimit(2, 5))
				r.Post("/", inviteHandler.Join)
			})
		})
	})

	// File serving (public, for viewing uploaded files)
	if uploadHandler != nil {
		r.Get("/files/*", uploadHandler.Serve)

		// Legacy redirect: old uploads returned /{bucket}/objectName
		r.Get("/dumpster-files/*", func(w http.ResponseWriter, r *http.Request) {
			objectName := strings.TrimPrefix(r.URL.Path, "/dumpster-files/")
			http.Redirect(w, r, "/files/"+objectName, http.StatusMovedPermanently)
		})
	}

	// Public webhook execution (no auth required)
	r.Post("/webhooks/{webhookID}/{token}", func(w http.ResponseWriter, r *http.Request) {
		webhook.NewHandler(database.DB, hub).Execute(w, r)
	})

	// Swagger UI (only accessible from localhost to avoid exposing API docs)
	r.Group(func(r chi.Router) {
		r.Use(func(next http.Handler) http.Handler {
			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				host := r.Host
				if host == "" {
					host = r.Header.Get("Host")
				}
				if host != "localhost:"+cfg.Port && host != "127.0.0.1:"+cfg.Port && host != "[::1]:"+cfg.Port {
					http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
					return
				}
				next.ServeHTTP(w, r)
			})
		})
		r.Get("/docs/*", httpSwagger.Handler(
			httpSwagger.URL("/docs/swagger.json"),
		))
	})

	// Static file serving for production (SPA)
	staticDir := "web/dist"
	if _, err := os.Stat(staticDir); err == nil {
		fileServer := http.FileServer(http.Dir(staticDir))
		r.NotFound(func(w http.ResponseWriter, r *http.Request) {
			// If the file exists, serve it; otherwise serve index.html (SPA fallback)
			path := staticDir + r.URL.Path
			if _, err := os.Stat(path); os.IsNotExist(err) {
				http.ServeFile(w, r, staticDir+"/index.html")
				return
			}
			fileServer.ServeHTTP(w, r)
		})
	}

	addr := fmt.Sprintf(":%s", cfg.Port)
	logger.Info("starting server", "addr", addr)
	if err := http.ListenAndServe(addr, r); err != nil {
		logger.Error("server error", "error", err)
		os.Exit(1)
	}
}
