package api

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"strconv"
	"time"

	"bookra/apps/backend/internal/admin"
	"bookra/apps/backend/internal/auth"
	"bookra/apps/backend/internal/billing"
	"bookra/apps/backend/internal/bookings"
	"bookra/apps/backend/internal/catalog"
	"bookra/apps/backend/internal/config"
	"bookra/apps/backend/internal/db"
	"bookra/apps/backend/internal/domain"
	"bookra/apps/backend/internal/httpx"
	"bookra/apps/backend/internal/notifications"
	"bookra/apps/backend/internal/sms"
	"bookra/apps/backend/internal/tenancy"

	"github.com/getsentry/sentry-go"
	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"golang.org/x/time/rate"
)

type Server struct {
	router              *gin.Engine
	cfg                 config.Config
	pools               *db.Pools
	verifier            *auth.Verifier
	authService         *auth.Service
	adminService        *admin.Service
	billingService      *billing.Service
	notificationService *notifications.Service
	smsService          *sms.Service
}

func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
	verifier, err := auth.NewVerifier(cfg.NeonAuthURL, cfg.AuthJWTSecret)
	if err != nil {
		return nil, err
	}

	repository := db.NewRepository(pools, cfg.DemoMode)
	notificationService := notifications.NewService(cfg, repository)
	bookingService := bookings.NewService(repository, notificationService)
	customerBookingService := bookings.NewCustomerService(repository, notificationService)
	tenantService := tenancy.NewService(repository)
	billingService := billing.NewService(cfg, repository)
	catalogService := catalog.NewService(repository, billingService, notificationService)
	authService := auth.NewService(repository, cfg.AuthJWTSecret)
	adminService := admin.NewService(repository, cfg.AdminEmail, cfg.AdminKey)
	smsService := sms.NewService(cfg, repository)
	publicRateLimiter := httpx.NewRateLimiter(rate.Every(time.Second), 5)

	server := &Server{
		router:              gin.New(),
		cfg:                 cfg,
		pools:               pools,
		verifier:            verifier,
		authService:         authService,
		adminService:        adminService,
		billingService:      billingService,
		notificationService: notificationService,
		smsService:          smsService,
	}

	server.router.Use(gin.Logger(), gin.Recovery(), cors.New(cors.Config{
		AllowOrigins:     allowedOrigins(cfg),
		AllowHeaders:     []string{"Authorization", "Content-Type"},
		AllowMethods:     []string{"GET", "POST", "OPTIONS"},
		AllowCredentials: true,
	}), httpx.SecurityHeaders())

	server.router.GET("/healthz", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"status":             "ok",
			"environment":        cfg.Environment,
			"databaseConfigured": pools.DatabaseConfigured(),
		})
	})

	// Test endpoint for Sentry
	server.router.GET("/debug/sentry", func(c *gin.Context) {
		sentry.CaptureMessage("Test message from Bookra API")
		c.JSON(http.StatusOK, gin.H{"status": "sent", "message": "Test error sent to Sentry"})
	})

	// Test endpoint for billing
	server.router.GET("/debug/billing-test", func(c *gin.Context) {
		if billingService != nil {
			c.JSON(http.StatusOK, gin.H{
				"status":         "ok",
				"billingService": "initialized",
				"message":        "Billing service is working",
			})
		} else {
			c.JSON(http.StatusOK, gin.H{"status": "ok", "billingService": "not initialized"})
		}
	})

	server.router.GET("/v1/meta/config", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"environment":       cfg.Environment,
			"neonAuthEnabled":   verifier.Enabled(),
			"apiUrl":            cfg.APIURL,
			"demoMode":          cfg.DemoMode,
			"adminLoginEnabled": adminService.IsConfigured(),
		})
	})

	// ============================================
	// AUTH API
	// ============================================
	authGroup := server.router.Group("/v1/auth")
	{
		authGroup.POST("/register", func(c *gin.Context) {
			var request struct {
				Email    string `json:"email" binding:"required,email"`
				Password string `json:"password" binding:"required,min=8"`
				Name     string `json:"name"`
			}
			if err := c.ShouldBindJSON(&request); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
				return
			}
			user, tokens, err := authService.RegisterWithPassword(c.Request.Context(), request.Email, request.Password, request.Name)
			if err != nil {
				if errors.Is(err, auth.ErrEmailAlreadyExists) {
					c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
					return
				}
				if errors.Is(err, auth.ErrPasswordTooShort) {
					c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
					return
				}
				c.JSON(http.StatusInternalServerError, gin.H{"error": "registration_failed"})
				return
			}
			c.JSON(http.StatusCreated, gin.H{
				"user": gin.H{
					"id":    user.ID,
					"email": user.Email,
					"name":  user.Name,
				},
				"accessToken":  tokens.AccessToken,
				"refreshToken": tokens.RefreshToken,
				"tokenType":    tokens.TokenType,
				"expiresIn":    tokens.ExpiresIn,
			})
		})

		authGroup.POST("/login", func(c *gin.Context) {
			var request struct {
				Email    string `json:"email" binding:"required,email"`
				Password string `json:"password" binding:"required"`
			}
			if err := c.ShouldBindJSON(&request); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
				return
			}
			user, tokens, err := authService.LoginWithPassword(c.Request.Context(), request.Email, request.Password)
			if err != nil {
				if errors.Is(err, auth.ErrInvalidCredentials) {
					c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
					return
				}
				c.JSON(http.StatusInternalServerError, gin.H{"error": "login_failed"})
				return
			}
			c.JSON(http.StatusOK, gin.H{
				"user": gin.H{
					"id":    user.ID,
					"email": user.Email,
					"name":  user.Name,
				},
				"accessToken":  tokens.AccessToken,
				"refreshToken": tokens.RefreshToken,
				"tokenType":    tokens.TokenType,
				"expiresIn":    tokens.ExpiresIn,
			})
		})

		authGroup.POST("/magic-link", func(c *gin.Context) {
			var request struct {
				Email string `json:"email" binding:"required,email"`
			}
			if err := c.ShouldBindJSON(&request); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
				return
			}
			_, err := authService.CreateMagicLink(c.Request.Context(), request.Email)
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": "magic_link_failed"})
				return
			}
			c.JSON(http.StatusOK, gin.H{"message": "magic_link_sent"})
		})

		authGroup.POST("/verify", func(c *gin.Context) {
			var request struct {
				Token string `json:"token" binding:"required"`
			}
			if err := c.ShouldBindJSON(&request); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
				return
			}
			user, tokens, err := authService.VerifyMagicLink(c.Request.Context(), request.Token)
			if err != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, gin.H{
				"user": gin.H{
					"id":    user.ID,
					"email": user.Email,
					"name":  user.Name,
				},
				"accessToken":  tokens.AccessToken,
				"refreshToken": tokens.RefreshToken,
				"tokenType":    tokens.TokenType,
				"expiresIn":    tokens.ExpiresIn,
			})
		})

		authGroup.POST("/refresh", func(c *gin.Context) {
			var request struct {
				RefreshToken string `json:"refreshToken" binding:"required"`
			}
			if err := c.ShouldBindJSON(&request); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
				return
			}
			tokens, err := authService.RefreshToken(c.Request.Context(), request.RefreshToken)
			if err != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, gin.H{
				"accessToken":  tokens.AccessToken,
				"refreshToken": tokens.RefreshToken,
				"tokenType":    tokens.TokenType,
				"expiresIn":    tokens.ExpiresIn,
			})
		})
	}

	// ============================================
	// ADMIN API
	// ============================================
	adminGroup := server.router.Group("/v1/admin")
	adminGroup.Use(admin.RequireAdmin(adminService, authService))
	{
		adminGroup.GET("/stats", func(c *gin.Context) {
			stats, err := adminService.GetDashboardStats(c.Request.Context())
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, stats)
		})

		adminGroup.GET("/tenants", func(c *gin.Context) {
			page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
			pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
			result, err := adminService.ListTenants(c.Request.Context(), page, pageSize)
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, result)
		})

		adminGroup.GET("/users", func(c *gin.Context) {
			page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
			pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
			result, err := adminService.ListUsers(c.Request.Context(), page, pageSize)
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, result)
		})

		adminGroup.PUT("/users/:userID/role", func(c *gin.Context) {
			var request domain.UpdateUserRoleRequest
			if err := c.ShouldBindJSON(&request); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
				return
			}
			adminUserID, _ := c.Get("userID")
			err := adminService.UpdateUserRole(
				c.Request.Context(),
				adminUserID.(string),
				c.Param("userID"),
				request.Role,
				c.ClientIP(),
				c.GetHeader("User-Agent"),
			)
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, gin.H{"status": "updated"})
		})

		// Trigger trial ending email check
		adminGroup.POST("/trigger-trial-emails", func(c *gin.Context) {
			err := billingService.CheckAndSendTrialEndingEmails(c.Request.Context(), notificationService)
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, gin.H{"status": "completed", "message": "Trial ending emails sent"})
		})
	}

	server.router.GET("/v1/public/tenants/:tenantSlug/availability", func(c *gin.Context) {
		response, err := bookingService.Availability(c.Request.Context(), c.Param("tenantSlug"))
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, bookings.ErrTenantNotFound) {
				status = http.StatusNotFound
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, response)
	})

	server.router.POST("/v1/public/bookings", publicRateLimiter.Middleware(), func(c *gin.Context) {
		var request struct {
			TenantSlug     string  `json:"tenantSlug" binding:"required"`
			BookingMode    string  `json:"bookingMode" binding:"required"`
			ServiceID      *string `json:"serviceId"`
			ClassSessionID *string `json:"classSessionId"`
			StaffID        *string `json:"staffId"`
			LocationID     *string `json:"locationId"`
			CustomerName   string  `json:"customerName" binding:"required"`
			CustomerEmail  string  `json:"customerEmail" binding:"required,email"`
			CustomerPhone  string  `json:"customerPhone"`
			Notes          string  `json:"notes"`
			StartsAt       string  `json:"startsAt" binding:"required"`
			EndsAt         string  `json:"endsAt" binding:"required"`
		}
		if err := c.ShouldBindJSON(&request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
			return
		}

		response, err := bookingService.Create(c.Request.Context(), domain.CreateBookingRequest(request))
		if err != nil {
			status := http.StatusInternalServerError
			switch {
			case errors.Is(err, bookings.ErrInvalidBooking):
				status = http.StatusBadRequest
			case errors.Is(err, bookings.ErrTenantNotFound):
				status = http.StatusNotFound
			case errors.Is(err, bookings.ErrBookingConflict):
				status = http.StatusConflict
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}

		c.JSON(http.StatusCreated, response)
	})

	server.router.POST("/v1/public/contact", publicRateLimiter.Middleware(), func(c *gin.Context) {
		var request struct {
			Name    string `json:"name" binding:"required"`
			Email   string `json:"email" binding:"required,email"`
			Message string `json:"message" binding:"required,min=10"`
		}
		if err := c.ShouldBindJSON(&request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
			return
		}
		if err := notificationService.SendContactEmail(c.Request.Context(), request.Name, request.Email, request.Message); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to send message"})
			return
		}
		c.JSON(http.StatusOK, gin.H{"status": "sent"})
	})

	protected := server.router.Group("/v1")
	protected.Use(auth.RequireAuth(verifier, repository, cfg.DemoMode))

	protected.GET("/dashboard/summary", func(c *gin.Context) {
		response, err := bookingService.DashboardSummary(c.Request.Context(), auth.PrincipalFromContext(c))
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, bookings.ErrTenantMembership) {
				status = http.StatusNotFound
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, response)
	})
	protected.GET("/tenants/bootstrap", func(c *gin.Context) {
		response, err := tenantService.Bootstrap(c.Request.Context(), auth.PrincipalFromContext(c))
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, response)
	})
	protected.POST("/tenants/onboard", func(c *gin.Context) {
		var request domain.OnboardTenantRequest
		if err := c.ShouldBindJSON(&request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
			return
		}
		response, err := tenantService.Onboard(c.Request.Context(), auth.PrincipalFromContext(c), request)
		if err != nil {
			status := http.StatusInternalServerError
			switch {
			case errors.Is(err, tenancy.ErrInvalidOnboarding):
				status = http.StatusBadRequest
			case errors.Is(err, tenancy.ErrTenantAlreadyProvisioned), errors.Is(err, tenancy.ErrTenantSlugTaken):
				status = http.StatusConflict
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusCreated, response)
	})

	// ============================================
	// CATALOG API - Locations / Zones
	// ============================================
	protected.GET("/catalog/locations", func(c *gin.Context) {
		response, err := catalogService.ListLocations(c.Request.Context(), auth.PrincipalFromContext(c))
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, catalog.ErrTenantMembership) {
				status = http.StatusNotFound
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, response)
	})
	protected.POST("/catalog/locations", func(c *gin.Context) {
		var request domain.CreateLocationRequest
		if err := c.ShouldBindJSON(&request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
			return
		}
		response, err := catalogService.CreateLocation(c.Request.Context(), auth.PrincipalFromContext(c), request)
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, catalog.ErrTenantMembership) {
				status = http.StatusNotFound
			} else if errors.Is(err, catalog.ErrPlanLimitReached) {
				status = http.StatusForbidden
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusCreated, response)
	})
	protected.PUT("/catalog/locations/:locationID", func(c *gin.Context) {
		var request domain.UpdateLocationRequest
		if err := c.ShouldBindJSON(&request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
			return
		}
		response, err := catalogService.UpdateLocation(c.Request.Context(), auth.PrincipalFromContext(c), c.Param("locationID"), request)
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, catalog.ErrTenantMembership) {
				status = http.StatusNotFound
			} else if errors.Is(err, catalog.ErrLocationNotFound) {
				status = http.StatusNotFound
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, response)
	})
	protected.DELETE("/catalog/locations/:locationID", func(c *gin.Context) {
		err := catalogService.DeleteLocation(c.Request.Context(), auth.PrincipalFromContext(c), c.Param("locationID"))
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, catalog.ErrTenantMembership) {
				status = http.StatusNotFound
			} else if errors.Is(err, catalog.ErrLocationNotFound) {
				status = http.StatusNotFound
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, gin.H{"status": "deleted"})
	})

	// ============================================
	// CATALOG API - Blocked Days
	// ============================================
	protected.GET("/catalog/blocked-days", func(c *gin.Context) {
		from := time.Now()
		to := from.AddDate(0, 3, 0) // 3 months ahead
		response, err := catalogService.ListBlockedDays(c.Request.Context(), auth.PrincipalFromContext(c), from, to)
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, catalog.ErrTenantMembership) {
				status = http.StatusNotFound
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, response)
	})
	protected.POST("/catalog/blocked-days", func(c *gin.Context) {
		var request domain.CreateBlockedDayRequest
		if err := c.ShouldBindJSON(&request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
			return
		}
		response, err := catalogService.CreateBlockedDay(c.Request.Context(), auth.PrincipalFromContext(c), request)
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, catalog.ErrTenantMembership) {
				status = http.StatusNotFound
			} else if errors.Is(err, catalog.ErrInvalidBooking) {
				status = http.StatusBadRequest
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusCreated, response)
	})
	protected.PUT("/catalog/blocked-days/:blockedDayID", func(c *gin.Context) {
		var request domain.UpdateBlockedDayRequest
		if err := c.ShouldBindJSON(&request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
			return
		}
		response, err := catalogService.UpdateBlockedDay(c.Request.Context(), auth.PrincipalFromContext(c), c.Param("blockedDayID"), request)
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, catalog.ErrTenantMembership) {
				status = http.StatusNotFound
			} else if errors.Is(err, catalog.ErrBlockedDayNotFound) {
				status = http.StatusNotFound
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, response)
	})
	protected.DELETE("/catalog/blocked-days/:blockedDayID", func(c *gin.Context) {
		err := catalogService.DeleteBlockedDay(c.Request.Context(), auth.PrincipalFromContext(c), c.Param("blockedDayID"))
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, catalog.ErrTenantMembership) {
				status = http.StatusNotFound
			} else if errors.Is(err, catalog.ErrBlockedDayNotFound) {
				status = http.StatusNotFound
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, gin.H{"status": "deleted"})
	})

	// ============================================
	// CATALOG API - Customers
	// ============================================
	protected.GET("/catalog/customers", func(c *gin.Context) {
		limit := 50
		offset := 0
		if l, err := strconv.Atoi(c.Query("limit")); err == nil && l > 0 && l <= 100 {
			limit = l
		}
		if o, err := strconv.Atoi(c.Query("offset")); err == nil && o >= 0 {
			offset = o
		}
		response, err := catalogService.ListCustomers(c.Request.Context(), auth.PrincipalFromContext(c), limit, offset)
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, catalog.ErrTenantMembership) {
				status = http.StatusNotFound
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, response)
	})
	protected.POST("/catalog/customers", func(c *gin.Context) {
		var request domain.CreateCustomerRequest
		if err := c.ShouldBindJSON(&request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
			return
		}
		response, err := catalogService.CreateCustomer(c.Request.Context(), auth.PrincipalFromContext(c), request)
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, catalog.ErrTenantMembership) {
				status = http.StatusNotFound
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusCreated, response)
	})
	protected.PUT("/catalog/customers/:customerID", func(c *gin.Context) {
		var request domain.UpdateCustomerRequest
		if err := c.ShouldBindJSON(&request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
			return
		}
		response, err := catalogService.UpdateCustomer(c.Request.Context(), auth.PrincipalFromContext(c), c.Param("customerID"), request)
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, catalog.ErrTenantMembership) {
				status = http.StatusNotFound
			} else if errors.Is(err, catalog.ErrCustomerNotFound) {
				status = http.StatusNotFound
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, response)
	})
	protected.DELETE("/catalog/customers/:customerID", func(c *gin.Context) {
		err := catalogService.DeleteCustomer(c.Request.Context(), auth.PrincipalFromContext(c), c.Param("customerID"))
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, catalog.ErrTenantMembership) {
				status = http.StatusNotFound
			} else if errors.Is(err, catalog.ErrCustomerNotFound) {
				status = http.StatusNotFound
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, gin.H{"status": "deleted"})
	})

	// ============================================
	// CATALOG API - Working Hours
	// ============================================
	protected.GET("/catalog/working-hours", func(c *gin.Context) {
		response, err := catalogService.ListWorkingHours(c.Request.Context(), auth.PrincipalFromContext(c))
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, catalog.ErrTenantMembership) {
				status = http.StatusNotFound
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, response)
	})
	protected.PUT("/catalog/working-hours/:dayOfWeek", func(c *gin.Context) {
		dayOfWeek, err := strconv.Atoi(c.Param("dayOfWeek"))
		if err != nil || dayOfWeek < 0 || dayOfWeek > 6 {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_day_of_week"})
			return
		}
		var request domain.UpdateWorkingHoursRequest
		if err := c.ShouldBindJSON(&request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
			return
		}
		err = catalogService.UpdateWorkingHours(c.Request.Context(), auth.PrincipalFromContext(c), dayOfWeek, request)
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, catalog.ErrTenantMembership) {
				status = http.StatusNotFound
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, gin.H{"status": "updated"})
	})

	// ============================================
	// CUSTOMER BOOKING MANAGEMENT API (Public)
	// ============================================
	server.router.GET("/v1/public/bookings/:reference", func(c *gin.Context) {
		token := c.Query("token")
		response, err := customerBookingService.GetBookingByReference(c.Request.Context(), c.Param("reference"), token)
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, bookings.ErrBookingNotFound) {
				status = http.StatusNotFound
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, response)
	})
	server.router.POST("/v1/public/bookings/:reference/reschedule", publicRateLimiter.Middleware(), func(c *gin.Context) {
		token := c.Query("token")
		var request domain.RescheduleBookingRequest
		if err := c.ShouldBindJSON(&request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
			return
		}
		err := customerBookingService.RescheduleBooking(c.Request.Context(), c.Param("reference"), request, token)
		if err != nil {
			status := http.StatusInternalServerError
			switch {
			case errors.Is(err, bookings.ErrBookingNotFound):
				status = http.StatusNotFound
			case errors.Is(err, bookings.ErrBookingCancelled):
				status = http.StatusConflict
			case errors.Is(err, bookings.ErrInvalidReschedule):
				status = http.StatusBadRequest
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, gin.H{"status": "rescheduled"})
	})
	server.router.POST("/v1/public/bookings/:reference/cancel", publicRateLimiter.Middleware(), func(c *gin.Context) {
		token := c.Query("token")
		err := customerBookingService.CancelBooking(c.Request.Context(), c.Param("reference"), token)
		if err != nil {
			status := http.StatusInternalServerError
			switch {
			case errors.Is(err, bookings.ErrBookingNotFound):
				status = http.StatusNotFound
			case errors.Is(err, bookings.ErrBookingCancelled):
				status = http.StatusConflict
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, gin.H{"status": "cancelled"})
	})

	protected.GET("/billing/subscription", func(c *gin.Context) {
		response, err := billingService.GetSubscription(c.Request.Context(), auth.PrincipalFromContext(c))
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, billing.ErrBillingMembership) {
				status = http.StatusNotFound
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, response)
	})
	protected.POST("/billing/checkout", func(c *gin.Context) {
		var request domain.CheckoutSessionRequest
		if err := c.ShouldBindJSON(&request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
			return
		}
		response, err := billingService.CreateCheckoutSession(c.Request.Context(), auth.PrincipalFromContext(c), request.PlanCode, request.Currency, request.BillingInterval)
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, billing.ErrBillingMembership) {
				status = http.StatusNotFound
			}
			if errors.Is(err, billing.ErrBillingPlanUnsupported) {
				status = http.StatusBadRequest
			}
			if errors.Is(err, billing.ErrPaddleNotConfigured) {
				status = http.StatusServiceUnavailable
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, response)
	})
	protected.POST("/billing/refresh", func(c *gin.Context) {
		response, err := billingService.Refresh(c.Request.Context(), auth.PrincipalFromContext(c))
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, billing.ErrBillingMembership) {
				status = http.StatusNotFound
			}
			if errors.Is(err, billing.ErrPaddleNotConfigured) {
				status = http.StatusServiceUnavailable
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, response)
	})
	protected.POST("/billing/portal", func(c *gin.Context) {
		response, err := billingService.CreatePortalSession(c.Request.Context(), auth.PrincipalFromContext(c))
		if err != nil {
			status := http.StatusInternalServerError
			switch {
			case errors.Is(err, billing.ErrBillingMembership):
				status = http.StatusNotFound
			case errors.Is(err, billing.ErrBillingCustomerMissing):
				status = http.StatusBadRequest
			case errors.Is(err, billing.ErrPaddleNotConfigured):
				status = http.StatusServiceUnavailable
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, response)
	})

	// ============================================
	// SMS API
	// ============================================
	protected.GET("/sms/settings", func(c *gin.Context) {
		membership, err := tenantService.GetTenantMembership(c.Request.Context(), auth.PrincipalFromContext(c))
		if err != nil {
			c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
			return
		}
		settings, err := smsService.GetSettings(c.Request.Context(), membership.Tenant.ID)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, settings)
	})
	protected.POST("/sms/settings", func(c *gin.Context) {
		membership, err := tenantService.GetTenantMembership(c.Request.Context(), auth.PrincipalFromContext(c))
		if err != nil {
			c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
			return
		}
		var request domain.UpdateSMSSettingsRequest
		if err := c.ShouldBindJSON(&request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
			return
		}
		settings, err := smsService.UpdateSettings(c.Request.Context(), membership.Tenant.ID, request)
		if err != nil {
			status := http.StatusInternalServerError
			if errors.Is(err, sms.ErrSMSPlanNotAllowed) {
				status = http.StatusForbidden
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, settings)
	})
	protected.POST("/sms/send", func(c *gin.Context) {
		membership, err := tenantService.GetTenantMembership(c.Request.Context(), auth.PrincipalFromContext(c))
		if err != nil {
			c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
			return
		}
		var request domain.SendSMSRequest
		if err := c.ShouldBindJSON(&request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
			return
		}
		response, err := smsService.SendMessage(c.Request.Context(), membership.Tenant.ID, request)
		if err != nil {
			status := http.StatusInternalServerError
			switch {
			case errors.Is(err, sms.ErrSMSNotConfigured), errors.Is(err, sms.ErrSMSNotEnabled):
				status = http.StatusServiceUnavailable
			case errors.Is(err, sms.ErrSMSPlanNotAllowed):
				status = http.StatusForbidden
			case errors.Is(err, sms.ErrSMSLimitReached):
				status = http.StatusTooManyRequests
			case errors.Is(err, sms.ErrSMSInvalidPhone):
				status = http.StatusBadRequest
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, response)
	})
	protected.GET("/sms/usage", func(c *gin.Context) {
		membership, err := tenantService.GetTenantMembership(c.Request.Context(), auth.PrincipalFromContext(c))
		if err != nil {
			c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
			return
		}
		yearMonth := c.Query("month")
		if yearMonth == "" {
			now := time.Now().UTC()
			yearMonth = fmt.Sprintf("%04d-%02d", now.Year(), now.Month())
		}
		report, err := smsService.GetUsage(c.Request.Context(), membership.Tenant.ID, yearMonth)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, report)
	})
	protected.GET("/sms/history", func(c *gin.Context) {
		membership, err := tenantService.GetTenantMembership(c.Request.Context(), auth.PrincipalFromContext(c))
		if err != nil {
			c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
			return
		}
		limit := 50
		if l, err := strconv.Atoi(c.Query("limit")); err == nil && l > 0 && l <= 100 {
			limit = l
		}
		logs, err := smsService.GetUsageHistory(c.Request.Context(), membership.Tenant.ID, limit)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, gin.H{"logs": logs})
	})
	protected.GET("/sms/invoices", func(c *gin.Context) {
		membership, err := tenantService.GetTenantMembership(c.Request.Context(), auth.PrincipalFromContext(c))
		if err != nil {
			c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
			return
		}
		limit := 12
		if l, err := strconv.Atoi(c.Query("limit")); err == nil && l > 0 && l <= 24 {
			limit = l
		}
		reports, err := smsService.GetMonthlyReports(c.Request.Context(), membership.Tenant.ID, limit)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, gin.H{"reports": reports})
	})

	// Internal job endpoint for SMS monthly invoicing
	server.router.POST("/v1/internal/jobs/sms/invoices", func(c *gin.Context) {
		if !authorizeJobRunner(c, cfg) {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
			return
		}
		yearMonth := c.Query("month")
		response, err := smsService.GenerateMonthlyInvoices(c.Request.Context(), yearMonth, notificationService)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, response)
	})

	server.router.POST("/v1/webhooks/paddle", func(c *gin.Context) {
		if err := billingService.HandleWebhook(c.Request.Context(), c.Request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, gin.H{"status": "accepted"})
	})
	server.router.POST("/v1/webhooks/stripe", func(c *gin.Context) {
		if err := billingService.HandleStripeWebhook(c.Request.Context(), c.Request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, gin.H{"status": "accepted"})
	})
	server.router.POST("/api/paddle_webhook", func(c *gin.Context) {
		if err := billingService.HandleWebhook(c.Request.Context(), c.Request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, gin.H{"status": "accepted"})
	})

	server.router.POST("/v1/internal/jobs/reminders/dispatch", func(c *gin.Context) {
		if !authorizeJobRunner(c, cfg) {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
			return
		}

		var request domain.DispatchReminderJobsRequest
		if err := c.ShouldBindJSON(&request); err != nil && !errors.Is(err, io.EOF) {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
			return
		}

		response, err := notificationService.DispatchDue(c.Request.Context(), request.Limit)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, response)
	})

	// Widget embeddable script endpoint - serves JavaScript for external sites
	server.router.GET("/v1/public/widget.js", func(c *gin.Context) {
		c.Header("Content-Type", "application/javascript; charset=utf-8")
		c.Header("Cache-Control", "public, max-age=3600") // Cache for 1 hour
		c.String(http.StatusOK, widgetJavaScript(cfg.APIURL))
	})

	return server, nil
}

func (s *Server) Handler() http.Handler {
	return s.router
}

func (s *Server) Close() {
	if s.verifier != nil {
		s.verifier.Close()
	}
}

// StartBackgroundJobs runs periodic background tasks
func (s *Server) StartBackgroundJobs() {
	// Run trial ending check every 6 hours
	ticker := time.NewTicker(6 * time.Hour)
	defer ticker.Stop()

	// Run immediately on startup after a brief delay
	time.Sleep(30 * time.Second)
	s.runTrialEndingCheck()

	for {
		select {
		case <-ticker.C:
			s.runTrialEndingCheck()
		}
	}
}

func (s *Server) runTrialEndingCheck() {
	if s.billingService == nil || s.notificationService == nil {
		return
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	err := s.billingService.CheckAndSendTrialEndingEmails(ctx, s.notificationService)
	if err != nil {
		log.Printf("Background job: trial ending check failed: %v", err)
	} else {
		log.Printf("Background job: trial ending check completed")
	}
}

func authorizeJobRunner(c *gin.Context, cfg config.Config) bool {
	if cfg.JobRunnerKey == "" {
		return false
	}
	return c.GetHeader("X-Bookra-Job-Key") == cfg.JobRunnerKey
}

func allowedOrigins(cfg config.Config) []string {
	origins := []string{cfg.FrontendURL}
	if cfg.Environment == "development" {
		origins = append(origins,
			"http://localhost:3000",
			"http://127.0.0.1:3000",
			"http://localhost:4173",
			"http://127.0.0.1:4173",
		)
	}

	seen := make(map[string]struct{}, len(origins))
	unique := origins[:0]
	for _, origin := range origins {
		if origin == "" {
			continue
		}
		if _, ok := seen[origin]; ok {
			continue
		}
		seen[origin] = struct{}{}
		unique = append(unique, origin)
	}
	return unique
}

// widgetJavaScript returns the embeddable widget script that can be included on external sites
func widgetJavaScript(apiURL string) string {
	return `(function() {
  'use strict';
  
  // Bookra Widget v1.0 - Embeddable Booking Widget
  // Usage: <script src="` + apiURL + `/v1/public/widget.js" data-tenant="your-slug" async defer></script>
  
  const WIDGET_VERSION = '1.0.0';
  const WIDGET_ORIGIN = '` + apiURL + `';
  
  // Configuration from script tag attributes
  const scripts = document.querySelectorAll('script[src*="widget.js"]');
  
  scripts.forEach(function(script) {
    const config = {
      tenant: script.getAttribute('data-tenant') || script.getAttribute('data-tenant-slug'),
      theme: script.getAttribute('data-theme') || 'auto',
      size: script.getAttribute('data-size') || 'default',
      color: script.getAttribute('data-color') || '#a65c3e',
      position: script.getAttribute('data-position') || 'bottom-right',
      widgetId: script.getAttribute('data-widget-id') || 'bookra-widget-' + Math.random().toString(36).substr(2, 9)
    };
    
    if (!config.tenant) {
      console.error('[Bookra Widget] Missing data-tenant attribute');
      return;
    }
    
    // Find or create widget container
    let container = document.getElementById(config.widgetId);
    if (!container) {
      container = document.createElement('div');
      container.id = config.widgetId;
      container.className = 'bookra-widget-container';
      
      // For floating widgets, append to body
      if (config.size === 'floating' || script.getAttribute('data-floating') === 'true') {
        container.style.cssText = 'position:fixed;' + getFloatingPosition(config.position) + ';z-index:9999;';
        document.body.appendChild(container);
      }
    }
    
    // Apply theme
    const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
    const isDark = config.theme === 'dark' || (config.theme === 'auto' && prefersDark);
    container.setAttribute('data-theme', isDark ? 'dark' : 'light');
    
    // Build booking URL
    const bookingUrl = WIDGET_ORIGIN.replace('/api', '') + '/book/' + encodeURIComponent(config.tenant);
    
    // Create widget content based on type
    const widgetType = script.getAttribute('data-type') || 'iframe';
    
    switch (widgetType) {
      case 'button':
        container.innerHTML = createButtonWidget(config, bookingUrl);
        break;
      case 'modal':
        container.innerHTML = createModalWidget(config, bookingUrl);
        break;
      case 'floating':
        container.innerHTML = createFloatingWidget(config, bookingUrl);
        break;
      case 'inline-calendar':
        container.innerHTML = createCalendarWidget(config, bookingUrl, WIDGET_ORIGIN);
        break;
      default:
        container.innerHTML = createIframeWidget(config, bookingUrl);
    }
    
    // Add styles
    addWidgetStyles(config.color, isDark);
  });
  
  function getFloatingPosition(position) {
    switch (position) {
      case 'top-left': return 'top:20px;left:20px';
      case 'top-right': return 'top:20px;right:20px';
      case 'bottom-left': return 'bottom:20px;left:20px';
      default: return 'bottom:20px;right:20px';
    }
  }
  
  function createIframeWidget(config, url) {
    const height = config.size === 'compact' ? '400px' : config.size === 'full' ? '100vh' : '760px';
    return '<iframe src="' + url + '" style="width:100%;height:' + height + ';border:none;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,0.1);" loading="lazy" title="Book appointment"></iframe>';
  }
  
  function createButtonWidget(config, url) {
    return '<button class="bookra-widget-btn" onclick="window.open(\'' + url + '\', \'_blank\')" style="background:' + config.color + ';color:white;padding:14px 28px;border:none;border-radius:8px;font-size:16px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;gap:8px;transition:opacity 0.2s;">' +
           '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>' +
           'Book appointment</button>';
  }
  
  function createModalWidget(config, url) {
    const modalId = config.widgetId + '-modal';
    return '<button class="bookra-widget-btn" onclick="document.getElementById(\'' + modalId + '\').style.display=\'flex\'" style="background:' + config.color + ';color:white;padding:14px 28px;border:none;border-radius:8px;font-size:16px;font-weight:600;cursor:pointer;">Book now</button>' +
           '<div id="' + modalId + '" onclick="if(event.target===this)this.style.display=\'none\'" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:10000;align-items:center;justify-content:center;padding:20px;">' +
           '<div style="background:white;border-radius:16px;width:100%;max-width:900px;height:90vh;position:relative;overflow:hidden;">' +
           '<button onclick="document.getElementById(\'' + modalId + '\').style.display=\'none\'" style="position:absolute;top:16px;right:16px;background:#f5f5f5;border:none;width:36px;height:36px;border-radius:50%;cursor:pointer;font-size:18px;z-index:10;">✕</button>' +
           '<iframe src="' + url + '" style="width:100%;height:100%;border:none;"></iframe>' +
           '</div></div>';
  }
  
  function createFloatingWidget(config, url) {
    const modalId = config.widgetId + '-modal';
    return '<button class="bookra-floating-btn" onclick="document.getElementById(\'' + modalId + '\').style.display=\'flex\'" style="background:' + config.color + ';color:white;width:60px;height:60px;border:none;border-radius:50%;cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,0.15);display:flex;align-items:center;justify-content:center;transition:transform 0.2s;" onmouseover="this.style.transform=\'scale(1.1)\'" onmouseout="this.style.transform=\'scale(1)\'">' +
           '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>' +
           '</button>' +
           '<div id="' + modalId + '" onclick="if(event.target===this)this.style.display=\'none\'" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:10000;align-items:center;justify-content:center;padding:20px;">' +
           '<div style="background:white;border-radius:16px;width:100%;max-width:500px;max-height:90vh;position:relative;overflow:hidden;">' +
           '<button onclick="document.getElementById(\'' + modalId + '\').style.display=\'none\'" style="position:absolute;top:16px;right:16px;background:#f5f5f5;border:none;width:36px;height:36px;border-radius:50%;cursor:pointer;font-size:18px;z-index:10;">✕</button>' +
           '<iframe src="' + url + '" style="width:100%;height:80vh;border:none;"></iframe>' +
           '</div></div>';
  }
  
  function createCalendarWidget(config, url, apiUrl) {
    // Placeholder for inline calendar - would fetch availability and render mini-calendar
    return '<div class="bookra-calendar-widget" style="background:white;border-radius:12px;padding:20px;box-shadow:0 2px 10px rgba(0,0,0,0.1);">' +
           '<h3 style="margin:0 0 16px 0;font-size:18px;">Select a time</h3>' +
           '<p style="color:#666;margin:0 0 16px 0;">Loading availability...</p>' +
           '<a href="' + url + '" target="_blank" style="display:inline-block;background:' + config.color + ';color:white;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:500;">View all times</a>' +
           '</div>';
  }
  
  function addWidgetStyles(primaryColor, isDark) {
    if (document.getElementById('bookra-widget-styles')) return;
    
    const styles = document.createElement('style');
    styles.id = 'bookra-widget-styles';
    styles.textContent = '.bookra-widget-container { font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif; }' +
      '.bookra-widget-container[data-theme="dark"] iframe { filter: invert(0.95) hue-rotate(180deg); }' +
      '.bookra-floating-btn:hover { transform: scale(1.1); }' +
      '@keyframes bookra-pulse { 0%, 100% { box-shadow: 0 4px 12px rgba(0,0,0,0.15); } 50% { box-shadow: 0 4px 20px rgba(0,0,0,0.25); } }' +
      '.bookra-floating-btn { animation: bookra-pulse 2s infinite; }';
    document.head.appendChild(styles);
  }
  
  // Log initialization
  console.log('[Bookra Widget] v' + WIDGET_VERSION + ' initialized for ' + scripts.length + ' widget(s)');
})();`
}
