package catalog

import (
	"context"
	"errors"
	"fmt"
	"time"

	"bookra/apps/backend/internal/db"
	"bookra/apps/backend/internal/domain"
)

var (
	ErrLocationNotFound   = errors.New("location not found")
	ErrBlockedDayNotFound = errors.New("blocked day not found")
	ErrCustomerNotFound   = errors.New("customer not found")
	ErrBookingNotFound    = errors.New("booking not found")
	ErrInvalidBooking     = errors.New("invalid booking request")
	ErrTenantNotFound     = errors.New("tenant not found")
	ErrTenantMembership   = errors.New("tenant membership not found")
	ErrPlanLimitReached   = errors.New("plan limit reached")
)

type Service struct {
	repo           db.Repository
	billingService interface {
		GetEntitlements(ctx context.Context, tenantID string) (domain.PlanEntitlements, error)
	}
	notificationService interface {
		SendUsageWarning(ctx context.Context, tenantID string, locationCount, locationLimit, usagePercent int) error
	}
}

func NewService(repo db.Repository, billingService interface {
	GetEntitlements(ctx context.Context, tenantID string) (domain.PlanEntitlements, error)
}, notificationService interface {
	SendUsageWarning(ctx context.Context, tenantID string, locationCount, locationLimit, usagePercent int) error
}) *Service {
	return &Service{repo: repo, billingService: billingService, notificationService: notificationService}
}

// ============================================
// LOCATION / ZONE MANAGEMENT
// ============================================

func (s *Service) ListLocations(ctx context.Context, principal domain.Principal) ([]domain.Location, error) {
	membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
	if err != nil {
		return nil, ErrTenantMembership
	}

	records, err := s.repo.ListLocationsByTenant(ctx, membership.Tenant.ID)
	if err != nil {
		return nil, err
	}

	locations := make([]domain.Location, len(records))
	for i, rec := range records {
		locations[i] = domain.Location{
			ID:        rec.ID,
			TenantID:  rec.TenantID,
			Name:      rec.Name,
			Type:      "room", // Default type
			Capacity:  10,     // Default capacity
			Timezone:  rec.Timezone,
			CreatedAt: rec.CreatedAt,
		}
	}
	return locations, nil
}

func (s *Service) CreateLocation(ctx context.Context, principal domain.Principal, req domain.CreateLocationRequest) (domain.Location, error) {
	membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
	if err != nil {
		return domain.Location{}, ErrTenantMembership
	}

	// Check plan entitlements for location limit
	if s.billingService != nil {
		entitlements, err := s.billingService.GetEntitlements(ctx, membership.Tenant.ID)
		if err == nil && entitlements.MaxLocations > 0 {
			// Count existing locations
			locations, err := s.repo.ListLocationsByTenant(ctx, membership.Tenant.ID)
			if err == nil && len(locations) >= entitlements.MaxLocations {
				return domain.Location{}, fmt.Errorf("%w: location limit reached (%d/%d). Upgrade your plan to add more locations.", ErrPlanLimitReached, len(locations), entitlements.MaxLocations)
			}
		}
	}

	params := db.CreateLocationParams{
		TenantID: membership.Tenant.ID,
		Name:     req.Name,
		Timezone: membership.Tenant.Timezone,
	}

	rec, err := s.repo.CreateLocation(ctx, params)
	if err != nil {
		return domain.Location{}, err
	}

	// Send usage warning if at 80%+ of limit
	if s.notificationService != nil && s.billingService != nil {
		entitlements, err := s.billingService.GetEntitlements(ctx, membership.Tenant.ID)
		if err == nil && entitlements.MaxLocations > 0 {
			locations, _ := s.repo.ListLocationsByTenant(ctx, membership.Tenant.ID)
			usagePercent := (len(locations) * 100) / entitlements.MaxLocations
			if usagePercent >= 80 {
				_ = s.notificationService.SendUsageWarning(ctx, membership.Tenant.ID, len(locations), entitlements.MaxLocations, usagePercent)
			}
		}
	}

	return domain.Location{
		ID:        rec.ID,
		TenantID:  rec.TenantID,
		Name:      rec.Name,
		Type:      req.Type,
		Capacity:  10,
		Timezone:  rec.Timezone,
		CreatedAt: rec.CreatedAt,
	}, nil
}

func (s *Service) UpdateLocation(ctx context.Context, principal domain.Principal, locationID string, req domain.UpdateLocationRequest) (domain.Location, error) {
	membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
	if err != nil {
		return domain.Location{}, ErrTenantMembership
	}

	// Verify location belongs to tenant
	loc, err := s.repo.GetLocationByID(ctx, locationID)
	if err != nil {
		return domain.Location{}, ErrLocationNotFound
	}
	if loc.TenantID != membership.Tenant.ID {
		return domain.Location{}, ErrLocationNotFound
	}

	params := db.UpdateLocationParams{}
	if req.Name != "" {
		params.Name = &req.Name
	}

	rec, err := s.repo.UpdateLocation(ctx, locationID, params)
	if err != nil {
		return domain.Location{}, err
	}

	return domain.Location{
		ID:        rec.ID,
		TenantID:  rec.TenantID,
		Name:      rec.Name,
		Type:      req.Type,
		Capacity:  10,
		Timezone:  rec.Timezone,
		CreatedAt: rec.CreatedAt,
	}, nil
}

func (s *Service) DeleteLocation(ctx context.Context, principal domain.Principal, locationID string) error {
	membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
	if err != nil {
		return ErrTenantMembership
	}

	loc, err := s.repo.GetLocationByID(ctx, locationID)
	if err != nil {
		return ErrLocationNotFound
	}
	if loc.TenantID != membership.Tenant.ID {
		return ErrLocationNotFound
	}

	return s.repo.DeleteLocation(ctx, locationID)
}

// ============================================
// BLOCKED DAYS MANAGEMENT
// ============================================

func (s *Service) ListBlockedDays(ctx context.Context, principal domain.Principal, from time.Time, to time.Time) ([]domain.BlockedDay, error) {
	membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
	if err != nil {
		return nil, ErrTenantMembership
	}

	records, err := s.repo.ListBlockedDaysByTenant(ctx, membership.Tenant.ID, from, to)
	if err != nil {
		return nil, err
	}

	blockedDays := make([]domain.BlockedDay, len(records))
	for i, rec := range records {
		blockedDays[i] = domain.BlockedDay{
			ID:        rec.ID,
			TenantID:  rec.TenantID,
			Date:      rec.StartsAt,
			Reason:    rec.Reason,
			Type:      rec.Kind,
			StaffID:   rec.StaffID,
			CreatedAt: rec.CreatedAt,
		}
	}
	return blockedDays, nil
}

func (s *Service) CreateBlockedDay(ctx context.Context, principal domain.Principal, req domain.CreateBlockedDayRequest) (domain.BlockedDay, error) {
	membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
	if err != nil {
		return domain.BlockedDay{}, ErrTenantMembership
	}

	date, err := time.Parse(time.RFC3339, req.Date)
	if err != nil {
		return domain.BlockedDay{}, ErrInvalidBooking
	}

	params := db.CreateBlockedDayParams{
		TenantID: membership.Tenant.ID,
		StaffID:  req.StaffID,
		StartsAt: date,
		EndsAt:   date.Add(24 * time.Hour),
		Kind:     req.Type,
		Reason:   req.Reason,
	}

	rec, err := s.repo.CreateBlockedDay(ctx, params)
	if err != nil {
		return domain.BlockedDay{}, err
	}

	return domain.BlockedDay{
		ID:        rec.ID,
		TenantID:  rec.TenantID,
		Date:      rec.StartsAt,
		Reason:    rec.Reason,
		Type:      rec.Kind,
		StaffID:   rec.StaffID,
		CreatedAt: rec.CreatedAt,
	}, nil
}

func (s *Service) UpdateBlockedDay(ctx context.Context, principal domain.Principal, blockedDayID string, req domain.UpdateBlockedDayRequest) (domain.BlockedDay, error) {
	membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
	if err != nil {
		return domain.BlockedDay{}, ErrTenantMembership
	}

	// Verify blocked day belongs to tenant
	bd, err := s.repo.ListBlockedDaysByTenant(ctx, membership.Tenant.ID, time.Time{}, time.Now().Add(365*24*time.Hour))
	if err != nil {
		return domain.BlockedDay{}, err
	}

	found := false
	for _, b := range bd {
		if b.ID == blockedDayID {
			found = true
			break
		}
	}
	if !found {
		return domain.BlockedDay{}, ErrBlockedDayNotFound
	}

	params := db.UpdateBlockedDayParams{}
	if req.Reason != "" {
		params.Reason = &req.Reason
	}
	if req.Type != "" {
		params.Kind = &req.Type
	}

	rec, err := s.repo.UpdateBlockedDay(ctx, blockedDayID, params)
	if err != nil {
		return domain.BlockedDay{}, err
	}

	return domain.BlockedDay{
		ID:        rec.ID,
		TenantID:  rec.TenantID,
		Date:      rec.StartsAt,
		Reason:    rec.Reason,
		Type:      rec.Kind,
		StaffID:   rec.StaffID,
		CreatedAt: rec.CreatedAt,
	}, nil
}

func (s *Service) DeleteBlockedDay(ctx context.Context, principal domain.Principal, blockedDayID string) error {
	membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
	if err != nil {
		return ErrTenantMembership
	}

	// Verify blocked day belongs to tenant
	bd, err := s.repo.ListBlockedDaysByTenant(ctx, membership.Tenant.ID, time.Time{}, time.Now().Add(365*24*time.Hour))
	if err != nil {
		return err
	}

	found := false
	for _, b := range bd {
		if b.ID == blockedDayID {
			found = true
			break
		}
	}
	if !found {
		return ErrBlockedDayNotFound
	}

	return s.repo.DeleteBlockedDay(ctx, blockedDayID)
}

// ============================================
// CUSTOMER MANAGEMENT
// ============================================

func (s *Service) ListCustomers(ctx context.Context, principal domain.Principal, limit int, offset int) ([]domain.Customer, error) {
	membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
	if err != nil {
		return nil, ErrTenantMembership
	}

	records, err := s.repo.ListCustomersByTenant(ctx, membership.Tenant.ID, limit, offset)
	if err != nil {
		return nil, err
	}

	customers := make([]domain.Customer, len(records))
	for i, rec := range records {
		bookingsCount, _ := s.repo.GetCustomerBookingsCount(ctx, rec.ID)
		lastBooking, _ := s.repo.GetCustomerLastBooking(ctx, rec.ID)

		customers[i] = domain.Customer{
			ID:            rec.ID,
			TenantID:      rec.TenantID,
			Name:          rec.Name,
			Email:         rec.Email,
			Phone:         rec.Phone,
			Status:        rec.Status,
			BookingsCount: bookingsCount,
			LastBookingAt: lastBooking,
			CreatedAt:     rec.CreatedAt,
			Notes:         "",
		}
		if rec.Notes != nil {
			customers[i].Notes = *rec.Notes
		}
	}
	return customers, nil
}

func (s *Service) CreateCustomer(ctx context.Context, principal domain.Principal, req domain.CreateCustomerRequest) (domain.Customer, error) {
	membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
	if err != nil {
		return domain.Customer{}, ErrTenantMembership
	}

	status := req.Status
	if status == "" {
		status = "active"
	}

	params := db.CreateCustomerParams{
		TenantID: membership.Tenant.ID,
		Name:     req.Name,
		Email:    req.Email,
		Phone:    req.Phone,
		Status:   status,
		Notes:    nil,
	}

	rec, err := s.repo.CreateCustomer(ctx, params)
	if err != nil {
		return domain.Customer{}, err
	}

	return domain.Customer{
		ID:        rec.ID,
		TenantID:  rec.TenantID,
		Name:      rec.Name,
		Email:     rec.Email,
		Phone:     rec.Phone,
		Status:    rec.Status,
		CreatedAt: rec.CreatedAt,
		Notes:     "",
	}, nil
}

func (s *Service) UpdateCustomer(ctx context.Context, principal domain.Principal, customerID string, req domain.UpdateCustomerRequest) (domain.Customer, error) {
	membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
	if err != nil {
		return domain.Customer{}, ErrTenantMembership
	}

	// Verify customer belongs to tenant
	cust, err := s.repo.GetCustomerByID(ctx, customerID)
	if err != nil {
		return domain.Customer{}, ErrCustomerNotFound
	}
	if cust.TenantID != membership.Tenant.ID {
		return domain.Customer{}, ErrCustomerNotFound
	}

	params := db.UpdateCustomerParams{}
	if req.Name != "" {
		params.Name = &req.Name
	}
	if req.Email != "" {
		params.Email = &req.Email
	}
	if req.Phone != nil {
		params.Phone = req.Phone
	}
	if req.Status != "" {
		params.Status = &req.Status
	}
	if req.Notes != "" {
		params.Notes = &req.Notes
	}

	rec, err := s.repo.UpdateCustomer(ctx, customerID, params)
	if err != nil {
		return domain.Customer{}, err
	}

	bookingsCount, _ := s.repo.GetCustomerBookingsCount(ctx, rec.ID)
	lastBooking, _ := s.repo.GetCustomerLastBooking(ctx, rec.ID)

	return domain.Customer{
		ID:            rec.ID,
		TenantID:      rec.TenantID,
		Name:          rec.Name,
		Email:         rec.Email,
		Phone:         rec.Phone,
		Status:        rec.Status,
		BookingsCount: bookingsCount,
		LastBookingAt: lastBooking,
		CreatedAt:     rec.CreatedAt,
		Notes:         "",
	}, nil
}

func (s *Service) DeleteCustomer(ctx context.Context, principal domain.Principal, customerID string) error {
	membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
	if err != nil {
		return ErrTenantMembership
	}

	cust, err := s.repo.GetCustomerByID(ctx, customerID)
	if err != nil {
		return ErrCustomerNotFound
	}
	if cust.TenantID != membership.Tenant.ID {
		return ErrCustomerNotFound
	}

	return s.repo.DeleteCustomer(ctx, customerID)
}

// ============================================
// WORKING HOURS MANAGEMENT
// ============================================

func (s *Service) ListWorkingHours(ctx context.Context, principal domain.Principal) ([]domain.WorkingHours, error) {
	membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
	if err != nil {
		return nil, ErrTenantMembership
	}

	records, err := s.repo.ListWorkingHoursByTenant(ctx, membership.Tenant.ID)
	if err != nil {
		return nil, err
	}

	hours := make([]domain.WorkingHours, len(records))
	for i, rec := range records {
		hours[i] = domain.WorkingHours{
			DayOfWeek: rec.DayOfWeek,
			Open:      rec.StartsLocal,
			Close:     rec.EndsLocal,
			IsOpen:    rec.StartsLocal != "" && rec.EndsLocal != "",
		}
	}
	return hours, nil
}

func (s *Service) UpdateWorkingHours(ctx context.Context, principal domain.Principal, dayOfWeek int, req domain.UpdateWorkingHoursRequest) error {
	membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
	if err != nil {
		return ErrTenantMembership
	}

	params := db.UpdateWorkingHoursParams{}
	if req.Open != "" {
		params.StartsLocal = &req.Open
	}
	if req.Close != "" {
		params.EndsLocal = &req.Close
	}

	return s.repo.UpdateWorkingHours(ctx, membership.Tenant.ID, dayOfWeek, params)
}
