package router

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/go-playground/validator/v10"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"github.com/rs/zerolog/log"
	"gorm.io/gorm"

	"github.com/treytartt/honeydue-api/internal/admin"
	"github.com/treytartt/honeydue-api/internal/apperrors"
	"github.com/treytartt/honeydue-api/internal/config"
	"github.com/treytartt/honeydue-api/internal/dto/responses"
	"github.com/treytartt/honeydue-api/internal/handlers"
	"github.com/treytartt/honeydue-api/internal/i18n"
	custommiddleware "github.com/treytartt/honeydue-api/internal/middleware"
	"github.com/treytartt/honeydue-api/internal/monitoring"
	"github.com/treytartt/honeydue-api/internal/push"
	"github.com/treytartt/honeydue-api/internal/repositories"
	"github.com/treytartt/honeydue-api/internal/services"
	customvalidator "github.com/treytartt/honeydue-api/internal/validator"
	"github.com/treytartt/honeydue-api/pkg/utils"
)

const Version = "2.0.0"

// Dependencies holds all dependencies needed by the router
type Dependencies struct {
	DB                *gorm.DB
	Cache             *services.CacheService
	Config            *config.Config
	EmailService      *services.EmailService
	PDFService        *services.PDFService
	PushClient        *push.Client // Direct APNs/FCM client
	StorageService    *services.StorageService
	MonitoringService *monitoring.Service
}

// SetupRouter creates and configures the Echo router
func SetupRouter(deps *Dependencies) *echo.Echo {
	cfg := deps.Config

	e := echo.New()
	e.HideBanner = true
	e.Validator = customvalidator.NewCustomValidator()
	e.HTTPErrorHandler = customHTTPErrorHandler

	// NOTE: Removed AddTrailingSlash() middleware - it conflicted with admin routes
	// which don't use trailing slashes. Mobile API routes explicitly include trailing slashes.

	// Global middleware
	e.Use(custommiddleware.RequestIDMiddleware())
	e.Use(utils.EchoRecovery())
	e.Use(custommiddleware.StructuredLogger())

	// Security headers (X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, etc.)
	//
	// CSP is permissive enough to serve the marketing landing page at / (which
	// loads same-origin CSS/JS/images and Google Fonts over https). JSON API
	// responses are unaffected — they don't load any assets, so any CSP is fine.
	// frame-ancestors stays 'none' to block clickjacking.
	e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
		XSSProtection:      "1; mode=block",
		ContentTypeNosniff: "nosniff",
		XFrameOptions:      "SAMEORIGIN",
		HSTSMaxAge:         31536000,
		ReferrerPolicy:     "strict-origin-when-cross-origin",
		ContentSecurityPolicy: "default-src 'self'; " +
			"style-src 'self' https://fonts.googleapis.com; " +
			"font-src 'self' https://fonts.gstatic.com data:; " +
			"img-src 'self' data:; " +
			"script-src 'self'; " +
			"connect-src 'self'; " +
			"frame-ancestors 'none'",
	}))
	e.Use(middleware.BodyLimitWithConfig(middleware.BodyLimitConfig{
		Limit: "1M", // 1MB default for JSON payloads
		Skipper: func(c echo.Context) bool {
			// Allow larger payloads for webhook endpoints (Apple/Google/Stripe notifications)
			return strings.HasPrefix(c.Request().URL.Path, "/api/subscription/webhook")
		},
	}))
	e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
		Timeout: 30 * time.Second,
		Skipper: func(c echo.Context) bool {
			path := c.Request().URL.Path
			// Skip timeout for reverse proxy and WebSocket routes — the
			// timeout middleware wraps the response writer in *http.timeoutWriter
			// which does not implement http.Flusher, causing a panic when
			// httputil.ReverseProxy or WebSocket upgraders try to flush.
			// Also skip for admin subdomain (all requests proxied to Next.js).
			adminHost := os.Getenv("ADMIN_HOST")
			return (adminHost != "" && c.Request().Host == adminHost) ||
				strings.HasPrefix(path, "/_next") ||
				strings.HasSuffix(path, "/ws")
		},
	}))
	e.Use(corsMiddleware(cfg))
	e.Use(i18n.Middleware())

	// Gzip compression (skip media endpoints since they serve binary files)
	e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
		Level: 5,
		Skipper: func(c echo.Context) bool {
			return strings.HasPrefix(c.Request().URL.Path, "/api/media/")
		},
	}))

	// Monitoring metrics middleware (if monitoring is enabled)
	if deps.MonitoringService != nil {
		if metricsMiddleware := deps.MonitoringService.MetricsMiddleware(); metricsMiddleware != nil {
			e.Use(metricsMiddleware)
		}
	}

	// Serve landing page static files (if static directory is configured)
	staticDir := cfg.Server.StaticDir
	if staticDir != "" {
		e.Static("/css", staticDir+"/css")
		e.Static("/js", staticDir+"/js")
		e.Static("/images", staticDir+"/images")
		e.File("/favicon.ico", staticDir+"/images/favicon.svg")

		// Serve index.html at root
		e.GET("/", func(c echo.Context) error {
			return c.File(staticDir + "/index.html")
		})
	}

	// Health check endpoints (no auth required)
	e.GET("/api/health/", readinessCheck(deps))
	e.GET("/api/health/live", liveCheck)

	// Initialize onboarding email service for tracking handler
	onboardingService := services.NewOnboardingEmailService(deps.DB, deps.EmailService, cfg.Server.BaseURL)

	// Email tracking endpoint (no auth required - used by email tracking pixels)
	trackingHandler := handlers.NewTrackingHandler(onboardingService)
	e.GET("/api/track/open/:trackingID", trackingHandler.TrackEmailOpen)

	// NOTE: Public static file serving removed for security.
	// All uploaded media is now served through authenticated proxy endpoints:
	// - GET /api/media/document/:id
	// - GET /api/media/document-image/:id
	// - GET /api/media/completion-image/:id
	// These endpoints verify the user has access to the residence before serving files.

	// Initialize repositories
	userRepo := repositories.NewUserRepository(deps.DB)
	residenceRepo := repositories.NewResidenceRepository(deps.DB)
	taskRepo := repositories.NewTaskRepository(deps.DB)
	contractorRepo := repositories.NewContractorRepository(deps.DB)
	documentRepo := repositories.NewDocumentRepository(deps.DB)
	notificationRepo := repositories.NewNotificationRepository(deps.DB)
	subscriptionRepo := repositories.NewSubscriptionRepository(deps.DB)
	taskTemplateRepo := repositories.NewTaskTemplateRepository(deps.DB)

	// Initialize services
	authService := services.NewAuthService(userRepo, cfg)
	authService.SetNotificationRepository(notificationRepo) // For creating notification preferences on registration
	userService := services.NewUserService(userRepo)
	residenceService := services.NewResidenceService(residenceRepo, userRepo, cfg)
	residenceService.SetTaskRepository(taskRepo) // Wire up task repo for statistics
	taskService := services.NewTaskService(taskRepo, residenceRepo)
	contractorService := services.NewContractorService(contractorRepo, residenceRepo)
	documentService := services.NewDocumentService(documentRepo, residenceRepo)
	notificationService := services.NewNotificationService(notificationRepo, deps.PushClient)

	// Wire up notification, email, residence, and storage services to task service
	taskService.SetNotificationService(notificationService)
	taskService.SetEmailService(deps.EmailService)
	taskService.SetResidenceService(residenceService) // For including TotalSummary in CRUD responses
	taskService.SetStorageService(deps.StorageService) // For reading completion images for email
	subscriptionService := services.NewSubscriptionService(subscriptionRepo, residenceRepo, taskRepo, contractorRepo, documentRepo)
	residenceService.SetSubscriptionService(subscriptionService) // Wire up subscription service for tier limit enforcement
	taskTemplateService := services.NewTaskTemplateService(taskTemplateRepo)
	suggestionService := services.NewSuggestionService(deps.DB, residenceRepo)

	// Initialize Stripe service
	stripeService := services.NewStripeService(subscriptionRepo, userRepo)

	// Initialize webhook event repo for deduplication
	webhookEventRepo := repositories.NewWebhookEventRepository(deps.DB)

	// Initialize webhook handler for Apple/Google/Stripe subscription notifications
	subscriptionWebhookHandler := handlers.NewSubscriptionWebhookHandler(subscriptionRepo, userRepo, webhookEventRepo, cfg.Features.WebhooksEnabled)
	subscriptionWebhookHandler.SetStripeService(stripeService)

	// Initialize middleware
	authMiddleware := custommiddleware.NewAuthMiddlewareWithConfig(deps.DB, deps.Cache, cfg)

	// Initialize Apple auth service
	appleAuthService := services.NewAppleAuthService(deps.Cache, cfg)
	googleAuthService := services.NewGoogleAuthService(deps.Cache, cfg)

	// Initialize audit service for security event logging
	auditService := services.NewAuditService(deps.DB)

	// Initialize handlers
	authHandler := handlers.NewAuthHandler(authService, deps.EmailService, deps.Cache)
	authHandler.SetAppleAuthService(appleAuthService)
	authHandler.SetGoogleAuthService(googleAuthService)
	authHandler.SetStorageService(deps.StorageService)
	authHandler.SetAuditService(auditService)
	userHandler := handlers.NewUserHandler(userService)
	residenceHandler := handlers.NewResidenceHandler(residenceService, deps.PDFService, deps.EmailService, cfg.Features.PDFReportsEnabled)
	taskHandler := handlers.NewTaskHandler(taskService, deps.StorageService)
	contractorHandler := handlers.NewContractorHandler(contractorService)
	documentHandler := handlers.NewDocumentHandler(documentService, deps.StorageService)
	notificationHandler := handlers.NewNotificationHandler(notificationService)
	subscriptionHandler := handlers.NewSubscriptionHandler(subscriptionService, stripeService)
	staticDataHandler := handlers.NewStaticDataHandler(residenceService, taskService, contractorService, taskTemplateService, deps.Cache)
	taskTemplateHandler := handlers.NewTaskTemplateHandler(taskTemplateService)
	suggestionHandler := handlers.NewSuggestionHandler(suggestionService)

	// Initialize upload handler (if storage service is available)
	var uploadHandler *handlers.UploadHandler
	var mediaHandler *handlers.MediaHandler
	if deps.StorageService != nil {
		uploadHandler = handlers.NewUploadHandler(deps.StorageService, services.NewFileOwnershipService(deps.DB))
		mediaHandler = handlers.NewMediaHandler(documentRepo, taskRepo, residenceRepo, deps.StorageService)
	}

	// Prometheus metrics endpoint (no auth required, for scraping)
	if deps.MonitoringService != nil {
		e.GET("/metrics", prometheusMetrics(deps.MonitoringService))
	}

	// Set up admin routes with monitoring handler (if available)
	var monitoringHandler *monitoring.Handler
	if deps.MonitoringService != nil {
		monitoringHandler = deps.MonitoringService.Handler()
	}

	adminDeps := &admin.Dependencies{
		EmailService:      deps.EmailService,
		PushClient:        deps.PushClient,
		OnboardingService: onboardingService,
		MonitoringHandler: monitoringHandler,
	}
	admin.SetupRoutes(e, deps.DB, cfg, adminDeps)

	// API group
	api := e.Group("/api")
	{
		// Public auth routes (no auth required)
		setupPublicAuthRoutes(api, authHandler, cfg.Server.Debug)

		// Public data routes (no auth required)
		setupPublicDataRoutes(api, residenceHandler, taskHandler, contractorHandler, staticDataHandler, subscriptionHandler, taskTemplateHandler)

		// Subscription webhook routes (no auth - called by Apple/Google servers)
		setupWebhookRoutes(api, subscriptionWebhookHandler)

		// Protected routes (auth required)
		protected := api.Group("")
		protected.Use(authMiddleware.TokenAuth())
		protected.Use(custommiddleware.TimezoneMiddleware())
		{
			setupProtectedAuthRoutes(protected, authHandler)
			setupResidenceRoutes(protected, residenceHandler)
			setupTaskRoutes(protected, taskHandler)
			setupSuggestionRoutes(protected, suggestionHandler)
			setupContractorRoutes(protected, contractorHandler)
			setupDocumentRoutes(protected, documentHandler)
			setupNotificationRoutes(protected, notificationHandler)
			setupSubscriptionRoutes(protected, subscriptionHandler)
			setupUserRoutes(protected, userHandler)

			// Upload routes (only if storage service is configured)
			if uploadHandler != nil {
				setupUploadRoutes(protected, uploadHandler)
			}

			// Media routes (authenticated media serving)
			if mediaHandler != nil {
				setupMediaRoutes(protected, mediaHandler)
			}
		}
	}

	return e
}

// corsMiddleware configures CORS with restricted origins in production.
// In debug mode, explicit localhost origins are allowed for development.
// In production, origins are read from the CORS_ALLOWED_ORIGINS environment variable
// (comma-separated), falling back to a restrictive default set.
func corsMiddleware(cfg *config.Config) echo.MiddlewareFunc {
	var origins []string
	if cfg.Server.Debug {
		origins = []string{
			"http://localhost:3000",
			"http://localhost:3001",
			"http://localhost:8080",
			"http://localhost:8000",
			"http://127.0.0.1:3000",
			"http://127.0.0.1:3001",
			"http://127.0.0.1:8080",
			"http://127.0.0.1:8000",
		}
	} else {
		origins = cfg.Server.CorsAllowedOrigins
		if len(origins) == 0 {
			// Restrictive default: only the known production domains
			origins = []string{
				"https://api.myhoneydue.com",
				"https://myhoneydue.com",
				"https://admin.myhoneydue.com",
			}
		}
	}

	return middleware.CORSWithConfig(middleware.CORSConfig{
		AllowOrigins:     origins,
		AllowMethods:     []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodOptions},
		AllowHeaders:     []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization, "X-Requested-With", "X-Timezone"},
		ExposeHeaders:    []string{echo.HeaderContentLength, "X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset", "Retry-After"},
		AllowCredentials: false,
		MaxAge:           int((12 * time.Hour).Seconds()),
	})
}

// liveCheck returns a simple 200 for Kubernetes liveness probes
func liveCheck(c echo.Context) error {
	return c.JSON(http.StatusOK, map[string]interface{}{
		"status":    "alive",
		"version":   Version,
		"timestamp": time.Now().UTC().Format(time.RFC3339),
	})
}

// readinessCheck returns 200 if PostgreSQL and Redis are reachable, 503 otherwise.
// This is used by Kubernetes readiness probes and load balancers.
func readinessCheck(deps *Dependencies) echo.HandlerFunc {
	return func(c echo.Context) error {
		ctx, cancel := context.WithTimeout(c.Request().Context(), 5*time.Second)
		defer cancel()

		status := "healthy"
		httpStatus := http.StatusOK
		checks := make(map[string]string)

		// Check PostgreSQL
		sqlDB, err := deps.DB.DB()
		if err != nil {
			checks["postgres"] = fmt.Sprintf("failed to get sql.DB: %v", err)
			status = "unhealthy"
			httpStatus = http.StatusServiceUnavailable
		} else if err := sqlDB.PingContext(ctx); err != nil {
			checks["postgres"] = fmt.Sprintf("ping failed: %v", err)
			status = "unhealthy"
			httpStatus = http.StatusServiceUnavailable
		} else {
			checks["postgres"] = "ok"
		}

		// Check Redis (if cache service is available)
		if deps.Cache != nil {
			if err := deps.Cache.Client().Ping(ctx).Err(); err != nil {
				checks["redis"] = fmt.Sprintf("ping failed: %v", err)
				status = "unhealthy"
				httpStatus = http.StatusServiceUnavailable
			} else {
				checks["redis"] = "ok"
			}
		} else {
			checks["redis"] = "not configured"
		}

		return c.JSON(httpStatus, map[string]interface{}{
			"status":    status,
			"version":   Version,
			"checks":    checks,
			"timestamp": time.Now().UTC().Format(time.RFC3339),
		})
	}
}

// prometheusMetrics returns an Echo handler that outputs metrics in Prometheus text format.
// It uses the existing monitoring service's HTTP stats collector to avoid adding external dependencies.
func prometheusMetrics(monSvc *monitoring.Service) echo.HandlerFunc {
	return func(c echo.Context) error {
		httpCollector := monSvc.HTTPCollector()
		if httpCollector == nil {
			return c.String(http.StatusOK, "# No HTTP metrics available (collector not initialized)\n")
		}

		stats := httpCollector.GetStats()
		var b strings.Builder

		// Request count by method+path+status
		b.WriteString("# HELP http_requests_total Total number of HTTP requests.\n")
		b.WriteString("# TYPE http_requests_total counter\n")
		for statusCode, count := range stats.ByStatusCode {
			fmt.Fprintf(&b, "http_requests_total{status_code=\"%d\"} %d\n", statusCode, count)
		}

		// Per-endpoint request count
		b.WriteString("# HELP http_endpoint_requests_total Total requests per endpoint.\n")
		b.WriteString("# TYPE http_endpoint_requests_total counter\n")
		for endpoint, epStats := range stats.ByEndpoint {
			// endpoint is "METHOD /path"
			parts := strings.SplitN(endpoint, " ", 2)
			method := endpoint
			path := ""
			if len(parts) == 2 {
				method = parts[0]
				path = parts[1]
			}
			fmt.Fprintf(&b, "http_endpoint_requests_total{method=\"%s\",path=\"%s\"} %d\n", method, path, epStats.Count)
		}

		// Request duration (avg latency as a gauge, since we don't have raw histogram buckets)
		b.WriteString("# HELP http_request_duration_ms Average request duration in milliseconds per endpoint.\n")
		b.WriteString("# TYPE http_request_duration_ms gauge\n")
		for endpoint, epStats := range stats.ByEndpoint {
			parts := strings.SplitN(endpoint, " ", 2)
			method := endpoint
			path := ""
			if len(parts) == 2 {
				method = parts[0]
				path = parts[1]
			}
			fmt.Fprintf(&b, "http_request_duration_ms{method=\"%s\",path=\"%s\",quantile=\"avg\"} %.2f\n", method, path, epStats.AvgLatencyMs)
			fmt.Fprintf(&b, "http_request_duration_ms{method=\"%s\",path=\"%s\",quantile=\"p95\"} %.2f\n", method, path, epStats.P95LatencyMs)
		}

		// Error rate
		b.WriteString("# HELP http_error_rate Overall error rate (4xx+5xx / total).\n")
		b.WriteString("# TYPE http_error_rate gauge\n")
		fmt.Fprintf(&b, "http_error_rate %.4f\n", stats.ErrorRate)

		// Requests per minute
		b.WriteString("# HELP http_requests_per_minute Current request rate.\n")
		b.WriteString("# TYPE http_requests_per_minute gauge\n")
		fmt.Fprintf(&b, "http_requests_per_minute %.2f\n", stats.RequestsPerMinute)

		c.Response().Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
		return c.String(http.StatusOK, b.String())
	}
}

// setupPublicAuthRoutes configures public authentication routes with
// per-endpoint rate limiters to mitigate brute-force and credential-stuffing.
// Rate limiters are disabled in debug mode to allow UI test suites to run
// without hitting 429 errors.
func setupPublicAuthRoutes(api *echo.Group, authHandler *handlers.AuthHandler, debug bool) {
	auth := api.Group("/auth")

	if debug {
		// No rate limiters in debug/local mode
		auth.POST("/login/", authHandler.Login)
		auth.POST("/register/", authHandler.Register)
		auth.POST("/forgot-password/", authHandler.ForgotPassword)
		auth.POST("/verify-reset-code/", authHandler.VerifyResetCode)
		auth.POST("/reset-password/", authHandler.ResetPassword)
		auth.POST("/apple-sign-in/", authHandler.AppleSignIn)
		auth.POST("/google-sign-in/", authHandler.GoogleSignIn)
	} else {
		// Rate limiters — created once, shared across requests.
		loginRL := custommiddleware.LoginRateLimiter()            // 10 req/min
		registerRL := custommiddleware.RegistrationRateLimiter()  // 5 req/min
		passwordRL := custommiddleware.PasswordResetRateLimiter() // 3 req/min

		auth.POST("/login/", authHandler.Login, loginRL)
		auth.POST("/register/", authHandler.Register, registerRL)
		auth.POST("/forgot-password/", authHandler.ForgotPassword, passwordRL)
		auth.POST("/verify-reset-code/", authHandler.VerifyResetCode, passwordRL)
		auth.POST("/reset-password/", authHandler.ResetPassword, passwordRL)
		auth.POST("/apple-sign-in/", authHandler.AppleSignIn, loginRL)
		auth.POST("/google-sign-in/", authHandler.GoogleSignIn, loginRL)
	}
}

// setupProtectedAuthRoutes configures protected authentication routes
func setupProtectedAuthRoutes(api *echo.Group, authHandler *handlers.AuthHandler) {
	auth := api.Group("/auth")
	{
		auth.POST("/logout/", authHandler.Logout)
		auth.POST("/refresh/", authHandler.RefreshToken)
		auth.GET("/me/", authHandler.CurrentUser)
		auth.PUT("/profile/", authHandler.UpdateProfile)
		auth.PATCH("/profile/", authHandler.UpdateProfile)
		auth.POST("/verify/", authHandler.VerifyEmail)       // Alias for mobile app compatibility
		auth.POST("/verify-email/", authHandler.VerifyEmail) // Original route
		auth.POST("/resend-verification/", authHandler.ResendVerification)
		auth.DELETE("/account/", authHandler.DeleteAccount)
	}
}

// setupPublicDataRoutes configures public data routes (lookups, static data)
func setupPublicDataRoutes(api *echo.Group, residenceHandler *handlers.ResidenceHandler, taskHandler *handlers.TaskHandler, contractorHandler *handlers.ContractorHandler, staticDataHandler *handlers.StaticDataHandler, subscriptionHandler *handlers.SubscriptionHandler, taskTemplateHandler *handlers.TaskTemplateHandler) {
	// Static data routes (public, cached)
	staticData := api.Group("/static_data")
	{
		staticData.GET("/", staticDataHandler.GetStaticData)
		staticData.POST("/refresh/", staticDataHandler.RefreshStaticData)
	}

	// Public subscription routes (upgrade triggers needed at app start before login)
	subscriptionPublic := api.Group("/subscription")
	{
		subscriptionPublic.GET("/upgrade-triggers/", subscriptionHandler.GetAllUpgradeTriggers)
	}

	// Lookup routes (public)
	api.GET("/residences/types/", residenceHandler.GetResidenceTypes)
	api.GET("/tasks/categories/", taskHandler.GetCategories)
	api.GET("/tasks/priorities/", taskHandler.GetPriorities)
	api.GET("/tasks/frequencies/", taskHandler.GetFrequencies)
	api.GET("/contractors/specialties/", contractorHandler.GetSpecialties)

	// Task template routes (public, for app autocomplete)
	templates := api.Group("/tasks/templates")
	{
		templates.GET("/", taskTemplateHandler.GetTemplates)
		templates.GET("/grouped/", taskTemplateHandler.GetTemplatesGrouped)
		templates.GET("/search/", taskTemplateHandler.SearchTemplates)
		templates.GET("/by-category/:category_id/", taskTemplateHandler.GetTemplatesByCategory)
		// /by-region/ removed — climate zone now participates in the main
		// GET /api/tasks/suggestions/ scoring via the template JSON conditions.
		templates.GET("/:id/", taskTemplateHandler.GetTemplate)
	}
}

// setupResidenceRoutes configures residence routes
func setupResidenceRoutes(api *echo.Group, residenceHandler *handlers.ResidenceHandler) {
	residences := api.Group("/residences")
	{
		residences.GET("/", residenceHandler.ListResidences)
		residences.POST("/", residenceHandler.CreateResidence)
		residences.GET("/my-residences/", residenceHandler.GetMyResidences)
		residences.GET("/summary/", residenceHandler.GetSummary)
		residences.POST("/join-with-code/", residenceHandler.JoinWithCode)

		residences.GET("/:id/", residenceHandler.GetResidence)
		residences.PUT("/:id/", residenceHandler.UpdateResidence)
		residences.PATCH("/:id/", residenceHandler.UpdateResidence)
		residences.DELETE("/:id/", residenceHandler.DeleteResidence)

		residences.GET("/:id/share-code/", residenceHandler.GetShareCode)
		residences.POST("/:id/generate-share-code/", residenceHandler.GenerateShareCode)
		residences.POST("/:id/generate-share-package/", residenceHandler.GenerateSharePackage)
		residences.POST("/:id/generate-tasks-report/", residenceHandler.GenerateTasksReport)
		residences.GET("/:id/users/", residenceHandler.GetResidenceUsers)
		residences.DELETE("/:id/users/:user_id/", residenceHandler.RemoveResidenceUser)
	}
}

// setupTaskRoutes configures task routes
func setupTaskRoutes(api *echo.Group, taskHandler *handlers.TaskHandler) {
	tasks := api.Group("/tasks")
	{
		tasks.GET("/", taskHandler.ListTasks)
		tasks.POST("/", taskHandler.CreateTask)
		tasks.POST("/bulk/", taskHandler.BulkCreateTasks)
		tasks.GET("/by-residence/:residence_id/", taskHandler.GetTasksByResidence)

		tasks.GET("/:id/", taskHandler.GetTask)
		tasks.PUT("/:id/", taskHandler.UpdateTask)
		tasks.PATCH("/:id/", taskHandler.UpdateTask)
		tasks.DELETE("/:id/", taskHandler.DeleteTask)

		tasks.POST("/:id/mark-in-progress/", taskHandler.MarkInProgress)
		tasks.POST("/:id/cancel/", taskHandler.CancelTask)
		tasks.POST("/:id/uncancel/", taskHandler.UncancelTask)
		tasks.POST("/:id/archive/", taskHandler.ArchiveTask)
		tasks.POST("/:id/unarchive/", taskHandler.UnarchiveTask)
		tasks.POST("/:id/quick-complete/", taskHandler.QuickComplete)
		tasks.GET("/:id/completions/", taskHandler.GetTaskCompletions)
	}

	// Task Completions
	completions := api.Group("/task-completions")
	{
		completions.GET("/", taskHandler.ListCompletions)
		completions.POST("/", taskHandler.CreateCompletion)
		completions.GET("/:id/", taskHandler.GetCompletion)
		completions.PUT("/:id/", taskHandler.UpdateCompletion)
		completions.DELETE("/:id/", taskHandler.DeleteCompletion)
	}
}

// setupSuggestionRoutes configures task suggestion routes
func setupSuggestionRoutes(api *echo.Group, suggestionHandler *handlers.SuggestionHandler) {
	tasks := api.Group("/tasks")
	{
		tasks.GET("/suggestions/", suggestionHandler.GetSuggestions)
	}
}

// setupContractorRoutes configures contractor routes
func setupContractorRoutes(api *echo.Group, contractorHandler *handlers.ContractorHandler) {
	contractors := api.Group("/contractors")
	{
		contractors.GET("/", contractorHandler.ListContractors)
		contractors.POST("/", contractorHandler.CreateContractor)
		contractors.GET("/by-residence/:residence_id/", contractorHandler.ListContractorsByResidence)
		contractors.GET("/:id/", contractorHandler.GetContractor)
		contractors.PUT("/:id/", contractorHandler.UpdateContractor)
		contractors.PATCH("/:id/", contractorHandler.UpdateContractor)
		contractors.DELETE("/:id/", contractorHandler.DeleteContractor)
		contractors.POST("/:id/toggle-favorite/", contractorHandler.ToggleFavorite)
		contractors.GET("/:id/tasks/", contractorHandler.GetContractorTasks)
	}
}

// setupDocumentRoutes configures document routes
func setupDocumentRoutes(api *echo.Group, documentHandler *handlers.DocumentHandler) {
	documents := api.Group("/documents")
	{
		documents.GET("/", documentHandler.ListDocuments)
		documents.POST("/", documentHandler.CreateDocument)
		documents.GET("/warranties/", documentHandler.ListWarranties)
		documents.GET("/:id/", documentHandler.GetDocument)
		documents.PUT("/:id/", documentHandler.UpdateDocument)
		documents.PATCH("/:id/", documentHandler.UpdateDocument)
		documents.DELETE("/:id/", documentHandler.DeleteDocument)
		documents.POST("/:id/activate/", documentHandler.ActivateDocument)
		documents.POST("/:id/deactivate/", documentHandler.DeactivateDocument)
		documents.POST("/:id/images/", documentHandler.UploadDocumentImage)
		documents.DELETE("/:id/images/:imageId/", documentHandler.DeleteDocumentImage)
	}
}

// setupNotificationRoutes configures notification routes
func setupNotificationRoutes(api *echo.Group, notificationHandler *handlers.NotificationHandler) {
	notifications := api.Group("/notifications")
	{
		notifications.GET("/", notificationHandler.ListNotifications)
		notifications.GET("/unread-count/", notificationHandler.GetUnreadCount)
		notifications.POST("/mark-all-read/", notificationHandler.MarkAllAsRead)
		notifications.POST("/:id/read/", notificationHandler.MarkAsRead)

		notifications.POST("/devices/", notificationHandler.RegisterDevice)
		notifications.POST("/devices/register/", notificationHandler.RegisterDevice) // Alias for mobile clients
		notifications.POST("/devices/unregister/", notificationHandler.UnregisterDevice)
		notifications.GET("/devices/", notificationHandler.ListDevices)
		notifications.DELETE("/devices/:id/", notificationHandler.DeleteDevice)

		notifications.GET("/preferences/", notificationHandler.GetPreferences)
		notifications.PUT("/preferences/", notificationHandler.UpdatePreferences)
		notifications.PATCH("/preferences/", notificationHandler.UpdatePreferences)
	}
}

// setupSubscriptionRoutes configures subscription routes (authenticated)
// Note: /upgrade-triggers/ is in setupPublicDataRoutes (public, no auth required)
func setupSubscriptionRoutes(api *echo.Group, subscriptionHandler *handlers.SubscriptionHandler) {
	subscription := api.Group("/subscription")
	{
		subscription.GET("/", subscriptionHandler.GetSubscription)
		subscription.GET("/status/", subscriptionHandler.GetSubscriptionStatus)
		subscription.GET("/upgrade-trigger/:key/", subscriptionHandler.GetUpgradeTrigger)
		subscription.GET("/features/", subscriptionHandler.GetFeatureBenefits)
		subscription.GET("/promotions/", subscriptionHandler.GetPromotions)
		subscription.POST("/purchase/", subscriptionHandler.ProcessPurchase)
		subscription.POST("/cancel/", subscriptionHandler.CancelSubscription)
		subscription.POST("/restore/", subscriptionHandler.RestoreSubscription)
		subscription.POST("/checkout/", subscriptionHandler.CreateCheckoutSession)
		subscription.POST("/portal/", subscriptionHandler.CreatePortalSession)
	}
}

// setupUserRoutes configures user routes
func setupUserRoutes(api *echo.Group, userHandler *handlers.UserHandler) {
	users := api.Group("/users")
	{
		users.GET("/", userHandler.ListUsers)
		users.GET("/:id/", userHandler.GetUser)
		users.GET("/profiles/", userHandler.ListProfiles)
	}
}

// setupUploadRoutes configures file upload routes
func setupUploadRoutes(api *echo.Group, uploadHandler *handlers.UploadHandler) {
	uploads := api.Group("/uploads")
	{
		uploads.POST("/image/", uploadHandler.UploadImage)
		uploads.POST("/document/", uploadHandler.UploadDocument)
		uploads.POST("/completion/", uploadHandler.UploadCompletion)
		uploads.DELETE("/", uploadHandler.DeleteFile)
	}
}

// setupMediaRoutes configures authenticated media serving routes
func setupMediaRoutes(api *echo.Group, mediaHandler *handlers.MediaHandler) {
	media := api.Group("/media")
	{
		media.GET("/document/:id", mediaHandler.ServeDocument)
		media.GET("/document-image/:id", mediaHandler.ServeDocumentImage)
		media.GET("/completion-image/:id", mediaHandler.ServeCompletionImage)
	}
}

// setupWebhookRoutes configures subscription webhook routes for Apple/Google server-to-server notifications
// These routes are public (no auth) since they're called by Apple/Google servers
func setupWebhookRoutes(api *echo.Group, webhookHandler *handlers.SubscriptionWebhookHandler) {
	webhooks := api.Group("/subscription/webhook")
	{
		webhooks.POST("/apple/", webhookHandler.HandleAppleWebhook)
		webhooks.POST("/google/", webhookHandler.HandleGoogleWebhook)
		webhooks.POST("/stripe/", webhookHandler.HandleStripeWebhook)
	}
}

// customHTTPErrorHandler handles all errors returned from handlers in a consistent way.
// It converts AppErrors, validation errors, and Echo HTTPErrors to JSON responses.
// Also includes fallback handling for legacy service-level errors.
func customHTTPErrorHandler(err error, c echo.Context) {
	// Already committed? Skip
	if c.Response().Committed {
		return
	}

	// Handle AppError (our custom application errors)
	var appErr *apperrors.AppError
	if errors.As(err, &appErr) {
		message := i18n.LocalizedMessage(c, appErr.MessageKey)
		// If i18n key not found (returns the key itself), use fallback message
		if message == appErr.MessageKey && appErr.Message != "" {
			message = appErr.Message
		} else if message == appErr.MessageKey {
			message = appErr.MessageKey // Use the key as last resort
		}

		// Log internal errors
		if appErr.Err != nil {
			log.Error().Err(appErr.Err).Str("message_key", appErr.MessageKey).Msg("Application error")
		}

		c.JSON(appErr.Code, responses.ErrorResponse{Error: message})
		return
	}

	// Handle validation errors from go-playground/validator
	var validationErrs validator.ValidationErrors
	if errors.As(err, &validationErrs) {
		c.JSON(http.StatusBadRequest, customvalidator.FormatValidationErrors(err))
		return
	}

	// Handle Echo's built-in HTTPError
	var httpErr *echo.HTTPError
	if errors.As(err, &httpErr) {
		msg := fmt.Sprintf("%v", httpErr.Message)
		c.JSON(httpErr.Code, responses.ErrorResponse{Error: msg})
		return
	}

	// Handle service-layer errors and map them to appropriate HTTP status codes
	switch {
	// Task errors - 404 Not Found
	case errors.Is(err, services.ErrTaskNotFound):
		c.JSON(http.StatusNotFound, responses.ErrorResponse{
			Error: i18n.LocalizedMessage(c, "error.task_not_found"),
		})
		return
	case errors.Is(err, services.ErrCompletionNotFound):
		c.JSON(http.StatusNotFound, responses.ErrorResponse{
			Error: i18n.LocalizedMessage(c, "error.completion_not_found"),
		})
		return

	// Task errors - 403 Forbidden
	case errors.Is(err, services.ErrTaskAccessDenied):
		c.JSON(http.StatusForbidden, responses.ErrorResponse{
			Error: i18n.LocalizedMessage(c, "error.task_access_denied"),
		})
		return

	// Task errors - 400 Bad Request
	case errors.Is(err, services.ErrTaskAlreadyCancelled):
		c.JSON(http.StatusBadRequest, responses.ErrorResponse{
			Error: i18n.LocalizedMessage(c, "error.task_already_cancelled"),
		})
		return
	case errors.Is(err, services.ErrTaskAlreadyArchived):
		c.JSON(http.StatusBadRequest, responses.ErrorResponse{
			Error: i18n.LocalizedMessage(c, "error.task_already_archived"),
		})
		return

	// Residence errors - 404 Not Found
	case errors.Is(err, services.ErrResidenceNotFound):
		c.JSON(http.StatusNotFound, responses.ErrorResponse{
			Error: i18n.LocalizedMessage(c, "error.residence_not_found"),
		})
		return

	// Residence errors - 403 Forbidden
	case errors.Is(err, services.ErrResidenceAccessDenied):
		c.JSON(http.StatusForbidden, responses.ErrorResponse{
			Error: i18n.LocalizedMessage(c, "error.residence_access_denied"),
		})
		return
	case errors.Is(err, services.ErrNotResidenceOwner):
		c.JSON(http.StatusForbidden, responses.ErrorResponse{
			Error: i18n.LocalizedMessage(c, "error.not_residence_owner"),
		})
		return
	case errors.Is(err, services.ErrPropertiesLimitReached):
		c.JSON(http.StatusForbidden, responses.ErrorResponse{
			Error: i18n.LocalizedMessage(c, "error.properties_limit_reached"),
		})
		return

	// Residence errors - 400 Bad Request
	case errors.Is(err, services.ErrCannotRemoveOwner):
		c.JSON(http.StatusBadRequest, responses.ErrorResponse{
			Error: i18n.LocalizedMessage(c, "error.cannot_remove_owner"),
		})
		return
	case errors.Is(err, services.ErrShareCodeExpired):
		c.JSON(http.StatusBadRequest, responses.ErrorResponse{
			Error: i18n.LocalizedMessage(c, "error.share_code_expired"),
		})
		return

	// Residence errors - 404 Not Found (share code)
	case errors.Is(err, services.ErrShareCodeInvalid):
		c.JSON(http.StatusNotFound, responses.ErrorResponse{
			Error: i18n.LocalizedMessage(c, "error.share_code_invalid"),
		})
		return

	// Residence errors - 409 Conflict
	case errors.Is(err, services.ErrUserAlreadyMember):
		c.JSON(http.StatusConflict, responses.ErrorResponse{
			Error: i18n.LocalizedMessage(c, "error.user_already_member"),
		})
		return
	}

	// Default: Internal server error (don't expose error details to client)
	log.Error().Err(err).Msg("Unhandled error")
	c.JSON(http.StatusInternalServerError, responses.ErrorResponse{
		Error: i18n.LocalizedMessage(c, "error.internal"),
	})
}
