package service

import (
	"context"
	"sort"
	"strings"
	"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/pgtype"
)

type AvailabilityService struct {
	queries  *repository.Queries
	calendar *CalendarService
	event    *EventService
}

func NewAvailabilityService(queries *repository.Queries, calendar *CalendarService, event *EventService) *AvailabilityService {
	return &AvailabilityService{queries: queries, calendar: calendar, event: event}
}

func (s *AvailabilityService) GetBusyBlocks(ctx context.Context, userID uuid.UUID, calendarID uuid.UUID, rangeStart, rangeEnd time.Time) (*models.AvailabilityResponse, error) {
	if _, err := s.calendar.GetRole(ctx, calendarID, userID); err != nil {
		return nil, err
	}

	pgCalID := utils.ToPgUUID(calendarID)
	events, err := s.queries.ListEventsByCalendarInRange(ctx, repository.ListEventsByCalendarInRangeParams{
		CalendarID: pgCalID,
		EndTime:    utils.ToPgTimestamptz(rangeStart),
		StartTime:  utils.ToPgTimestamptz(rangeEnd),
	})
	if err != nil {
		return nil, models.ErrInternal
	}

	recurring, err := s.queries.ListRecurringEventsByCalendar(ctx, repository.ListRecurringEventsByCalendarParams{
		CalendarID: pgCalID,
		StartTime:  utils.ToPgTimestamptz(rangeEnd),
	})
	if err != nil {
		return nil, models.ErrInternal
	}

	var busy []models.BusyBlock
	for _, ev := range events {
		if ev.RecurrenceRule.Valid {
			continue
		}
		busy = append(busy, models.BusyBlock{
			Start:   utils.FromPgTimestamptz(ev.StartTime),
			End:     utils.FromPgTimestamptz(ev.EndTime),
			EventID: utils.FromPgUUID(ev.ID),
		})
	}

	for _, ev := range recurring {
		occs := s.event.expandRecurrence(ev, rangeStart, rangeEnd)
		for _, occ := range occs {
			if occ.OccurrenceStartTime != nil {
				busy = append(busy, models.BusyBlock{
					Start:   *occ.OccurrenceStartTime,
					End:     *occ.OccurrenceEndTime,
					EventID: occ.ID,
				})
			}
		}
	}

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

	if busy == nil {
		busy = []models.BusyBlock{}
	}

	return &models.AvailabilityResponse{
		CalendarID: calendarID,
		RangeStart: rangeStart,
		RangeEnd:   rangeEnd,
		Busy:       busy,
	}, nil
}

// getBusyBlocksForCalendar
func (s *AvailabilityService) getBusyBlocksForCalendar(ctx context.Context, calID uuid.UUID, rangeStart, rangeEnd time.Time) ([]models.BusyBlock, error) {
	pgCalID := utils.ToPgUUID(calID)
	events, err := s.queries.ListEventsByCalendarInRange(ctx, repository.ListEventsByCalendarInRangeParams{
		CalendarID: pgCalID,
		EndTime:    utils.ToPgTimestamptz(rangeStart),
		StartTime:  utils.ToPgTimestamptz(rangeEnd),
	})
	if err != nil {
		return nil, err
	}

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

	var busy []models.BusyBlock
	for _, ev := range events {
		if ev.RecurrenceRule.Valid {
			continue
		}
		busy = append(busy, models.BusyBlock{
			Start:   utils.FromPgTimestamptz(ev.StartTime),
			End:     utils.FromPgTimestamptz(ev.EndTime),
			EventID: utils.FromPgUUID(ev.ID),
		})
	}

	for _, ev := range recurring {
		occs := s.event.expandRecurrence(ev, rangeStart, rangeEnd)
		for _, occ := range occs {
			if occ.OccurrenceStartTime != nil {
				busy = append(busy, models.BusyBlock{
					Start:   *occ.OccurrenceStartTime,
					End:     *occ.OccurrenceEndTime,
					EventID: occ.ID,
				})
			}
		}
	}

	return busy, nil
}

// GetBusyBlocksByTokenPublic returns busy blocks for a calendar by its public token. No auth required.
func (s *AvailabilityService) GetBusyBlocksByTokenPublic(ctx context.Context, token string, rangeStart, rangeEnd time.Time) (*models.AvailabilityResponse, error) {
	if token == "" {
		return nil, models.ErrNotFound
	}

	cal, err := s.queries.GetCalendarByToken(ctx, pgtype.Text{String: token, Valid: true})
	if err != nil {
		if err == pgx.ErrNoRows {
			return nil, models.ErrNotFound
		}
		return nil, models.ErrInternal
	}

	calID := utils.FromPgUUID(cal.ID)
	busy, err := s.getBusyBlocksForCalendar(ctx, calID, rangeStart, rangeEnd)
	if err != nil {
		return nil, models.ErrInternal
	}

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

	if busy == nil {
		busy = []models.BusyBlock{}
	}

	return &models.AvailabilityResponse{
		CalendarID: calID,
		RangeStart: rangeStart,
		RangeEnd:   rangeEnd,
		Busy:       busy,
	}, nil
}

// GetBusyBlocksAggregate returns merged busy blocks across multiple calendars by their tokens. No auth required.
func (s *AvailabilityService) GetBusyBlocksAggregate(ctx context.Context, tokens []string, rangeStart, rangeEnd time.Time) (*models.AvailabilityResponse, error) {
	if len(tokens) == 0 {
		return &models.AvailabilityResponse{
			RangeStart: rangeStart,
			RangeEnd:   rangeEnd,
			Busy:       []models.BusyBlock{},
		}, nil
	}

	seen := make(map[uuid.UUID]bool)
	var allBusy []models.BusyBlock

	for _, token := range tokens {
		token = strings.TrimSpace(token)
		if token == "" {
			continue
		}

		cal, err := s.queries.GetCalendarByToken(ctx, pgtype.Text{String: token, Valid: true})
		if err != nil {
			if err == pgx.ErrNoRows {
				continue
			}
			return nil, models.ErrInternal
		}

		calID := utils.FromPgUUID(cal.ID)
		if seen[calID] {
			continue
		}
		seen[calID] = true

		busy, err := s.getBusyBlocksForCalendar(ctx, calID, rangeStart, rangeEnd)
		if err != nil {
			return nil, models.ErrInternal
		}
		allBusy = append(allBusy, busy...)
	}

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

	if allBusy == nil {
		allBusy = []models.BusyBlock{}
	}

	return &models.AvailabilityResponse{
		RangeStart: rangeStart,
		RangeEnd:   rangeEnd,
		Busy:       allBusy,
	}, nil
}
