package service

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"sort"
	"time"

	"github.com/calendarapi/internal/models"
	"github.com/calendarapi/internal/repository"
	"github.com/calendarapi/internal/utils"
	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"
)

type BookingService struct {
	pool     *pgxpool.Pool
	queries  *repository.Queries
	calendar *CalendarService
	event    *EventService
}

func NewBookingService(pool *pgxpool.Pool, queries *repository.Queries, calendar *CalendarService, event *EventService) *BookingService {
	return &BookingService{pool: pool, queries: queries, calendar: calendar, event: event}
}

func (s *BookingService) CreateLink(ctx context.Context, userID uuid.UUID, calID uuid.UUID, config models.BookingConfig) (*models.BookingLink, error) {
	role, err := s.calendar.GetRole(ctx, calID, userID)
	if err != nil {
		return nil, err
	}
	if role != "owner" {
		return nil, models.ErrForbidden
	}

	tokenBytes := make([]byte, 16)
	if _, err := rand.Read(tokenBytes); err != nil {
		return nil, models.ErrInternal
	}
	token := hex.EncodeToString(tokenBytes)

	whJSON, err := json.Marshal(config.WorkingHours)
	if err != nil {
		return nil, models.ErrInternal
	}

	_, err = s.queries.CreateBookingLink(ctx, repository.CreateBookingLinkParams{
		ID:              utils.ToPgUUID(uuid.New()),
		CalendarID:      utils.ToPgUUID(calID),
		Token:           token,
		DurationMinutes: int32(config.DurationMinutes),
		BufferMinutes:   int32(config.BufferMinutes),
		Timezone:        config.Timezone,
		WorkingHours:    whJSON,
		Active:          config.Active,
	})
	if err != nil {
		return nil, models.ErrInternal
	}

	return &models.BookingLink{
		Token:     token,
		PublicURL: fmt.Sprintf("/booking/%s", token),
		Settings:  config,
	}, nil
}

func (s *BookingService) GetAvailability(ctx context.Context, token string, rangeStart, rangeEnd time.Time) (*models.BookingAvailability, error) {
	bl, err := s.queries.GetBookingLinkByToken(ctx, token)
	if err != nil {
		if err == pgx.ErrNoRows {
			return nil, models.ErrNotFound
		}
		return nil, models.ErrInternal
	}
	if !bl.Active {
		return nil, models.NewNotFoundError("booking link is not active")
	}

	var workingHours map[string][]models.Slot
	if err := json.Unmarshal(bl.WorkingHours, &workingHours); err != nil {
		return nil, models.ErrInternal
	}

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

	// Get owner to fetch all calendars that count for availability
	cal, err := s.queries.GetCalendarByID(ctx, bl.CalendarID)
	if err != nil {
		return nil, models.ErrInternal
	}
	ownerID := cal.OwnerID

	// Include busy blocks from ALL calendars owned by the user that have count_for_availability=true
	availCals, err := s.queries.ListCalendarsByOwnerForAvailability(ctx, ownerID)
	if err != nil {
		return nil, models.ErrInternal
	}

	var busyBlocks []models.TimeSlot
	buf := time.Duration(bl.BufferMinutes) * time.Minute

	for _, ac := range availCals {
		events, err := s.queries.ListEventsByCalendarInRange(ctx, repository.ListEventsByCalendarInRangeParams{
			CalendarID: ac.ID,
			EndTime:    utils.ToPgTimestamptz(rangeStart),
			StartTime:  utils.ToPgTimestamptz(rangeEnd),
		})
		if err != nil {
			continue
		}

		recurring, err := s.queries.ListRecurringEventsByCalendar(ctx, repository.ListRecurringEventsByCalendarParams{
			CalendarID: ac.ID,
			StartTime:  utils.ToPgTimestamptz(rangeEnd),
		})
		if err != nil {
			continue
		}

		for _, ev := range events {
			if ev.RecurrenceRule.Valid {
				continue
			}
			start := utils.FromPgTimestamptz(ev.StartTime)
			end := utils.FromPgTimestamptz(ev.EndTime)
			busyBlocks = append(busyBlocks, models.TimeSlot{
				Start: start.Add(-buf),
				End:   end.Add(buf),
			})
		}

		for _, ev := range recurring {
			occs := s.event.expandRecurrence(ev, rangeStart, rangeEnd)
			for _, occ := range occs {
				if occ.OccurrenceStartTime != nil {
					busyBlocks = append(busyBlocks, models.TimeSlot{
						Start: occ.OccurrenceStartTime.Add(-buf),
						End:   occ.OccurrenceEndTime.Add(buf),
					})
				}
			}
		}
	}
	duration := time.Duration(bl.DurationMinutes) * time.Minute

	dayNames := []string{"sun", "mon", "tue", "wed", "thu", "fri", "sat"}
	var slots []models.TimeSlot

	for d := rangeStart; d.Before(rangeEnd); d = d.Add(24 * time.Hour) {
		localDay := d.In(loc)
		dayName := dayNames[localDay.Weekday()]
		windows, ok := workingHours[dayName]
		if !ok || len(windows) == 0 {
			continue
		}

		for _, w := range windows {
			wStart, err1 := time.Parse("15:04", w.Start)
			wEnd, err2 := time.Parse("15:04", w.End)
			if err1 != nil || err2 != nil {
				continue
			}

			windowStart := time.Date(localDay.Year(), localDay.Month(), localDay.Day(),
				wStart.Hour(), wStart.Minute(), 0, 0, loc).UTC()
			windowEnd := time.Date(localDay.Year(), localDay.Month(), localDay.Day(),
				wEnd.Hour(), wEnd.Minute(), 0, 0, loc).UTC()

			for slotStart := windowStart; slotStart.Add(duration).Before(windowEnd) || slotStart.Add(duration).Equal(windowEnd); slotStart = slotStart.Add(duration) {
				slotEnd := slotStart.Add(duration)
				if !isConflict(slotStart, slotEnd, busyBlocks) {
					slots = append(slots, models.TimeSlot{Start: slotStart, End: slotEnd})
				}
			}
		}
	}

	sort.Slice(slots, func(i, j int) bool {
		return slots[i].Start.Before(slots[j].Start)
	})

	if slots == nil {
		slots = []models.TimeSlot{}
	}

	return &models.BookingAvailability{
		Token:           token,
		Timezone:        bl.Timezone,
		DurationMinutes: int(bl.DurationMinutes),
		Slots:           slots,
	}, nil
}

func (s *BookingService) Reserve(ctx context.Context, token string, name, email string, slotStart, slotEnd time.Time, notes *string) (*models.Event, error) {
	bl, err := s.queries.GetBookingLinkByToken(ctx, token)
	if err != nil {
		if err == pgx.ErrNoRows {
			return nil, models.ErrNotFound
		}
		return nil, models.ErrInternal
	}
	if !bl.Active {
		return nil, models.NewNotFoundError("booking link is not active")
	}

	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return nil, models.ErrInternal
	}
	defer tx.Rollback(ctx)
	qtx := s.queries.WithTx(tx)

	overlap, err := qtx.CheckEventOverlapForUpdate(ctx, repository.CheckEventOverlapForUpdateParams{
		CalendarID: bl.CalendarID,
		EndTime:    utils.ToPgTimestamptz(slotStart),
		StartTime:  utils.ToPgTimestamptz(slotEnd),
	})
	if err != nil {
		return nil, models.ErrInternal
	}
	if overlap {
		return nil, models.NewConflictError("slot no longer available")
	}

	cal, err := s.queries.GetCalendarByID(ctx, bl.CalendarID)
	if err != nil {
		return nil, models.ErrInternal
	}

	title := fmt.Sprintf("Booking: %s", name)
	desc := fmt.Sprintf("Booked by %s (%s)", name, email)
	if notes != nil && *notes != "" {
		desc += "\nNotes: " + *notes
	}

	eventID := uuid.New()
	ownerID := utils.FromPgUUID(cal.OwnerID)
	ev, err := qtx.CreateEvent(ctx, repository.CreateEventParams{
		ID:          utils.ToPgUUID(eventID),
		CalendarID:  bl.CalendarID,
		Title:       title,
		Description: utils.ToPgText(desc),
		StartTime:   utils.ToPgTimestamptz(slotStart.UTC()),
		EndTime:     utils.ToPgTimestamptz(slotEnd.UTC()),
		Timezone:    bl.Timezone,
		Tags:        []string{"booking"},
		CreatedBy:   utils.ToPgUUID(ownerID),
		UpdatedBy:   utils.ToPgUUID(ownerID),
	})
	if err != nil {
		return nil, models.ErrInternal
	}

	if err := tx.Commit(ctx); err != nil {
		return nil, models.ErrInternal
	}

	return eventFromDB(ev, []models.Reminder{}, []models.Attendee{}, []models.Attachment{}), nil
}

func isConflict(start, end time.Time, busy []models.TimeSlot) bool {
	for _, b := range busy {
		if start.Before(b.End) && end.After(b.Start) {
			return true
		}
	}
	return false
}
