package scheduler

import (
	"context"
	"math/rand"
	"time"

	"github.com/google/uuid"
	"github.com/warmbly/warmbly/internal/repository"
)

// SchedulerService provides task scheduling functionality
type SchedulerService interface {
	// Warmup scheduling
	CalculateNextWarmupTime(ctx context.Context, accountID uuid.UUID) (time.Time, error)

	// Campaign scheduling
	CalculateNextCampaignTime(ctx context.Context, campaignID uuid.UUID) (time.Time, *repository.ContactSequencePair, uuid.UUID, error)

	// Email scheduling (smart send)
	CalculateNextEmailTime(ctx context.Context, accountID uuid.UUID) (time.Time, error)
}

type schedulerService struct {
	taskRepo             repository.TaskRepository
	warmupRepo           repository.WarmupRepository
	campaignProgressRepo repository.CampaignProgressRepository
	emailRepo            repository.EmailRepository
	campaignRepo         repository.CampaignRepository
	// contactRepo is used only to read/cache the recipient ESP/provider for
	// ESP matching (no MX dial on the hot path). nil-safe.
	contactRepo repository.ContactRepository
	// campaignLogRepo records send-path decision logs (e.g. ESP defer,
	// new-lead cap). Optional/nil-safe so the scheduler keeps working without it.
	campaignLogRepo repository.CampaignLogRepository
}

// NewSchedulerService creates a new scheduler service
func NewSchedulerService(
	taskRepo repository.TaskRepository,
	warmupRepo repository.WarmupRepository,
	campaignProgressRepo repository.CampaignProgressRepository,
	emailRepo repository.EmailRepository,
	campaignRepo repository.CampaignRepository,
	contactRepo repository.ContactRepository,
	campaignLogRepo repository.CampaignLogRepository,
) SchedulerService {
	return &schedulerService{
		taskRepo:             taskRepo,
		warmupRepo:           warmupRepo,
		campaignProgressRepo: campaignProgressRepo,
		emailRepo:            emailRepo,
		campaignRepo:         campaignRepo,
		contactRepo:          contactRepo,
		campaignLogRepo:      campaignLogRepo,
	}
}

// Timezone utilities

// loadLocation safely loads a timezone location with fallback to UTC
func loadLocation(tz string) *time.Location {
	if tz == "" {
		return time.UTC
	}

	loc, err := time.LoadLocation(tz)
	if err != nil {
		return time.UTC
	}
	return loc
}

// convertToAccountTimezone converts a time from campaign timezone to account timezone
func convertToAccountTimezone(t time.Time, campaignTZ, accountTZ string) time.Time {
	campaignLoc := loadLocation(campaignTZ)
	accountLoc := loadLocation(accountTZ)

	// Get the wall clock time in campaign timezone
	year, month, day := t.In(campaignLoc).Date()
	hour, min, sec := t.In(campaignLoc).Clock()

	// Reconstruct in account timezone
	return time.Date(year, month, day, hour, min, sec, 0, accountLoc)
}

// Time calculation helpers

// parseTimeOfDay parses a time string like "08:00" and returns minutes since midnight
func parseTimeOfDay(timeStr string) int {
	if timeStr == "" {
		return 0
	}

	t, err := time.Parse("15:04", timeStr)
	if err != nil {
		return 0
	}

	return t.Hour()*60 + t.Minute()
}

// calculateBusinessHoursRemaining calculates hours remaining in business day
func calculateBusinessHoursRemaining(timezone string) float64 {
	loc := loadLocation(timezone)
	now := time.Now().In(loc)

	// Business hours: 8am to 8pm (12 hours)
	endOfDay := time.Date(now.Year(), now.Month(), now.Day(), 20, 0, 0, 0, loc)

	if now.After(endOfDay) {
		return 0
	}

	remaining := endOfDay.Sub(now).Hours()
	return max(0, remaining)
}

// calculateFirstSlotTomorrow calculates first business hour slot tomorrow
func calculateFirstSlotTomorrow(timezone string) time.Time {
	loc := loadLocation(timezone)
	now := time.Now().In(loc)

	// Tomorrow at 8am + random jitter (0-60 minutes)
	tomorrow := now.Add(24 * time.Hour)
	firstSlot := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 8, 0, 0, 0, loc)

	// Add random jitter
	jitter := randomJitter(0, 60)
	return firstSlot.Add(time.Minute * time.Duration(jitter))
}

// randomJitter generates random jitter between min and max minutes
func randomJitter(min, max int) int {
	if min >= max {
		return min
	}
	return min + rand.Intn(max-min)
}

// roundToNearestMinute rounds time to nearest N minutes
func roundToNearestMinute(t time.Time, minutes int) time.Time {
	if minutes <= 0 {
		return t
	}

	rounded := t.Truncate(time.Minute * time.Duration(minutes))
	if t.Sub(rounded) >= time.Minute*time.Duration(minutes)/2 {
		rounded = rounded.Add(time.Minute * time.Duration(minutes))
	}

	return rounded
}
