package service

import (
	"context"
	"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/pgtype"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/teambition/rrule-go"
)

type EventService struct {
	pool      *pgxpool.Pool
	queries   *repository.Queries
	calendar  *CalendarService
	audit     *AuditService
	scheduler ReminderScheduler
}

type ReminderScheduler interface {
	ScheduleReminder(ctx context.Context, eventID, reminderID, userID uuid.UUID, triggerAt time.Time) error
}

func NewEventService(pool *pgxpool.Pool, queries *repository.Queries, calendar *CalendarService, audit *AuditService, scheduler ReminderScheduler) *EventService {
	return &EventService{pool: pool, queries: queries, calendar: calendar, audit: audit, scheduler: scheduler}
}

func (s *EventService) Create(ctx context.Context, userID uuid.UUID, req CreateEventRequest) (*models.Event, error) {
	if err := utils.ValidateEventTitle(req.Title); err != nil {
		return nil, err
	}
	if req.Timezone == "" {
		return nil, models.NewValidationError("timezone is required")
	}
	if err := utils.ValidateTimezone(req.Timezone); err != nil {
		return nil, err
	}
	if err := utils.ValidateTimeRange(req.StartTime, req.EndTime); err != nil {
		return nil, err
	}

	role, err := s.calendar.GetRole(ctx, req.CalendarID, userID)
	if err != nil {
		return nil, err
	}
	if role != "owner" && role != "editor" {
		return nil, models.ErrForbidden
	}

	startUTC := req.StartTime.UTC()
	endUTC := req.EndTime.UTC()

	if req.RecurrenceRule != nil && *req.RecurrenceRule != "" {
		if _, err := rrule.StrToRRule(*req.RecurrenceRule); err != nil {
			return nil, models.NewValidationError("invalid recurrence_rule: " + err.Error())
		}
	}

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

	eventID := uuid.New()
	tags := req.Tags
	if tags == nil {
		tags = []string{}
	}

	dbEvent, err := qtx.CreateEvent(ctx, repository.CreateEventParams{
		ID:             utils.ToPgUUID(eventID),
		CalendarID:     utils.ToPgUUID(req.CalendarID),
		Title:          req.Title,
		Description:    utils.ToPgTextPtr(req.Description),
		Location:       utils.ToPgTextPtr(req.Location),
		StartTime:      utils.ToPgTimestamptz(startUTC),
		EndTime:        utils.ToPgTimestamptz(endUTC),
		Timezone:       req.Timezone,
		AllDay:         req.AllDay,
		RecurrenceRule: utils.ToPgTextPtr(req.RecurrenceRule),
		Tags:           tags,
		CreatedBy:      utils.ToPgUUID(userID),
		UpdatedBy:      utils.ToPgUUID(userID),
	})
	if err != nil {
		return nil, models.ErrInternal
	}

	var reminders []models.Reminder
	for _, mins := range req.Reminders {
		if err := utils.ValidateReminderMinutes(mins); err != nil {
			return nil, err
		}
		rID := uuid.New()
		r, err := qtx.CreateReminder(ctx, repository.CreateReminderParams{
			ID:            utils.ToPgUUID(rID),
			EventID:       utils.ToPgUUID(eventID),
			MinutesBefore: mins,
		})
		if err != nil {
			return nil, models.ErrInternal
		}
		reminders = append(reminders, models.Reminder{
			ID:            utils.FromPgUUID(r.ID),
			MinutesBefore: r.MinutesBefore,
		})
	}

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

	for _, rem := range reminders {
		triggerAt := startUTC.Add(-time.Duration(rem.MinutesBefore) * time.Minute)
		if triggerAt.After(time.Now()) && s.scheduler != nil {
			_ = s.scheduler.ScheduleReminder(ctx, eventID, rem.ID, userID, triggerAt)
		}
	}

	s.audit.Log(ctx, "event", eventID, "CREATE_EVENT", userID)

	if reminders == nil {
		reminders = []models.Reminder{}
	}
	return eventFromDB(dbEvent, reminders, []models.Attendee{}, []models.Attachment{}), nil
}

func (s *EventService) Get(ctx context.Context, userID uuid.UUID, eventID uuid.UUID) (*models.Event, error) {
	ev, err := s.queries.GetEventByID(ctx, utils.ToPgUUID(eventID))
	if err != nil {
		if err == pgx.ErrNoRows {
			return nil, models.ErrNotFound
		}
		return nil, models.ErrInternal
	}

	calID := utils.FromPgUUID(ev.CalendarID)
	if _, err := s.calendar.GetRole(ctx, calID, userID); err != nil {
		return nil, err
	}

	reminders, err := s.loadReminders(ctx, eventID)
	if err != nil {
		return nil, err
	}
	attendees, err := s.loadAttendees(ctx, eventID)
	if err != nil {
		return nil, err
	}
	attachments, err := s.loadAttachments(ctx, eventID)
	if err != nil {
		return nil, err
	}

	return eventFromDB(ev, reminders, attendees, attachments), nil
}

func (s *EventService) List(ctx context.Context, userID uuid.UUID, params ListEventParams) ([]models.Event, *string, error) {
	if err := utils.ValidateRecurrenceRangeLimit(params.RangeStart, params.RangeEnd); err != nil {
		return nil, nil, err
	}

	limit := utils.ClampLimit(params.Limit)

	var cursorTime pgtype.Timestamptz
	var cursorID pgtype.UUID
	if params.Cursor != "" {
		ct, cid, err := utils.ParseCursor(params.Cursor)
		if err != nil {
			return nil, nil, models.NewValidationError("invalid cursor")
		}
		cursorTime = utils.ToPgTimestamptzPtr(ct)
		cursorID = utils.ToPgUUID(*cid)
	}

	nonRecurring, err := s.queries.ListEventsInRange(ctx, repository.ListEventsInRangeParams{
		UserID:     utils.ToPgUUID(userID),
		RangeEnd:   utils.ToPgTimestamptz(params.RangeEnd),
		RangeStart: utils.ToPgTimestamptz(params.RangeStart),
		CalendarID: optionalPgUUID(params.CalendarID),
		Search:     utils.ToPgTextPtr(params.Search),
		Tag:        utils.ToPgTextPtr(params.Tag),
		CursorTime: cursorTime,
		CursorID:   cursorID,
		Lim:        limit + 1,
	})
	if err != nil {
		return nil, nil, models.ErrInternal
	}

	recurring, err := s.queries.ListRecurringEventsInRange(ctx, repository.ListRecurringEventsInRangeParams{
		UserID:     utils.ToPgUUID(userID),
		RangeEnd:   utils.ToPgTimestamptz(params.RangeEnd),
		CalendarID: optionalPgUUID(params.CalendarID),
	})
	if err != nil {
		return nil, nil, models.ErrInternal
	}

	var allEvents []models.Event
	recurringIDs := make(map[uuid.UUID]bool)

	for _, ev := range recurring {
		recurringIDs[utils.FromPgUUID(ev.ID)] = true
		occurrences := s.expandRecurrence(ev, params.RangeStart, params.RangeEnd)
		allEvents = append(allEvents, occurrences...)
	}

	for _, ev := range nonRecurring {
		eid := utils.FromPgUUID(ev.ID)
		if recurringIDs[eid] {
			continue
		}
		allEvents = append(allEvents, *eventFromDB(ev, nil, nil, nil))
	}

	sort.Slice(allEvents, func(i, j int) bool {
		si := effectiveStart(allEvents[i])
		sj := effectiveStart(allEvents[j])
		if si.Equal(sj) {
			return allEvents[i].ID.String() < allEvents[j].ID.String()
		}
		return si.Before(sj)
	})

	hasMore := int32(len(allEvents)) > limit
	if hasMore {
		allEvents = allEvents[:limit]
	}

	relatedLoaded := make(map[uuid.UUID]bool)
	remindersMap := make(map[uuid.UUID][]models.Reminder)
	attendeesMap := make(map[uuid.UUID][]models.Attendee)
	attachmentsMap := make(map[uuid.UUID][]models.Attachment)
	for _, ev := range allEvents {
		if relatedLoaded[ev.ID] {
			continue
		}
		relatedLoaded[ev.ID] = true
		remindersMap[ev.ID], _ = s.loadReminders(ctx, ev.ID)
		attendeesMap[ev.ID], _ = s.loadAttendees(ctx, ev.ID)
		attachmentsMap[ev.ID], _ = s.loadAttachments(ctx, ev.ID)
	}
	for i := range allEvents {
		allEvents[i].Reminders = remindersMap[allEvents[i].ID]
		allEvents[i].Attendees = attendeesMap[allEvents[i].ID]
		allEvents[i].Attachments = attachmentsMap[allEvents[i].ID]
	}

	if hasMore {
		last := allEvents[len(allEvents)-1]
		cursor := utils.EncodeCursor(effectiveStart(last), last.ID)
		return allEvents, &cursor, nil
	}

	return allEvents, nil, nil
}

func (s *EventService) Update(ctx context.Context, userID uuid.UUID, eventID uuid.UUID, req UpdateEventRequest) (*models.Event, error) {
	ev, err := s.queries.GetEventByID(ctx, utils.ToPgUUID(eventID))
	if err != nil {
		if err == pgx.ErrNoRows {
			return nil, models.ErrNotFound
		}
		return nil, models.ErrInternal
	}

	calID := utils.FromPgUUID(ev.CalendarID)
	role, err := s.calendar.GetRole(ctx, calID, userID)
	if err != nil {
		return nil, err
	}
	if role != "owner" && role != "editor" {
		return nil, models.ErrForbidden
	}

	if req.Title != nil {
		if err := utils.ValidateEventTitle(*req.Title); err != nil {
			return nil, err
		}
	}
	if req.Timezone != nil {
		if err := utils.ValidateTimezone(*req.Timezone); err != nil {
			return nil, err
		}
	}

	startTime := utils.FromPgTimestamptz(ev.StartTime)
	endTime := utils.FromPgTimestamptz(ev.EndTime)
	if req.StartTime != nil {
		startTime = req.StartTime.UTC()
	}
	if req.EndTime != nil {
		endTime = req.EndTime.UTC()
	}
	if err := utils.ValidateTimeRange(startTime, endTime); err != nil {
		return nil, err
	}

	if req.RecurrenceRule != nil && *req.RecurrenceRule != "" {
		if _, err := rrule.StrToRRule(*req.RecurrenceRule); err != nil {
			return nil, models.NewValidationError("invalid recurrence_rule: " + err.Error())
		}
	}

	var pgStart, pgEnd pgtype.Timestamptz
	if req.StartTime != nil {
		pgStart = utils.ToPgTimestamptz(startTime)
	}
	if req.EndTime != nil {
		pgEnd = utils.ToPgTimestamptz(endTime)
	}

	updated, err := s.queries.UpdateEvent(ctx, repository.UpdateEventParams{
		ID:             utils.ToPgUUID(eventID),
		Title:          utils.ToPgTextPtr(req.Title),
		Description:    utils.ToPgTextPtr(req.Description),
		Location:       utils.ToPgTextPtr(req.Location),
		StartTime:      pgStart,
		EndTime:        pgEnd,
		Timezone:       utils.ToPgTextPtr(req.Timezone),
		AllDay:         optionalPgBool(req.AllDay),
		RecurrenceRule: utils.ToPgTextPtr(req.RecurrenceRule),
		Tags:           req.Tags,
		UpdatedBy:      utils.ToPgUUID(userID),
	})
	if err != nil {
		if err == pgx.ErrNoRows {
			return nil, models.ErrNotFound
		}
		return nil, models.ErrInternal
	}

	s.audit.Log(ctx, "event", eventID, "UPDATE_EVENT", userID)

	reminders, _ := s.loadReminders(ctx, eventID)
	attendees, _ := s.loadAttendees(ctx, eventID)
	attachments, _ := s.loadAttachments(ctx, eventID)
	return eventFromDB(updated, reminders, attendees, attachments), nil
}

func (s *EventService) Delete(ctx context.Context, userID uuid.UUID, eventID uuid.UUID) error {
	ev, err := s.queries.GetEventByID(ctx, utils.ToPgUUID(eventID))
	if err != nil {
		if err == pgx.ErrNoRows {
			return models.ErrNotFound
		}
		return models.ErrInternal
	}

	calID := utils.FromPgUUID(ev.CalendarID)
	role, err := s.calendar.GetRole(ctx, calID, userID)
	if err != nil {
		return err
	}
	if role != "owner" && role != "editor" {
		return models.ErrForbidden
	}

	if err := s.queries.SoftDeleteEvent(ctx, utils.ToPgUUID(eventID)); err != nil {
		return models.ErrInternal
	}

	s.audit.Log(ctx, "event", eventID, "DELETE_EVENT", userID)
	return nil
}

func (s *EventService) expandRecurrence(ev repository.Event, rangeStart, rangeEnd time.Time) []models.Event {
	if !ev.RecurrenceRule.Valid {
		return nil
	}

	ruleStr := ev.RecurrenceRule.String
	dtStart := utils.FromPgTimestamptz(ev.StartTime)
	duration := utils.FromPgTimestamptz(ev.EndTime).Sub(dtStart)

	fullRule := fmt.Sprintf("DTSTART:%s\nRRULE:%s", dtStart.UTC().Format("20060102T150405Z"), ruleStr)
	rule, err := rrule.StrToRRuleSet(fullRule)
	if err != nil {
		return nil
	}

	exceptions, _ := s.queries.ListExceptionsByEvent(context.Background(), ev.ID)
	exceptionDates := make(map[string]bool)
	for _, ex := range exceptions {
		if ex.ExceptionDate.Valid {
			exceptionDates[ex.ExceptionDate.Time.Format("2006-01-02")] = true
		}
	}

	occurrences := rule.Between(rangeStart.UTC(), rangeEnd.UTC(), true)
	var results []models.Event

	for _, occ := range occurrences {
		dateKey := occ.Format("2006-01-02")
		if exceptionDates[dateKey] {
			continue
		}

		occEnd := occ.Add(duration)
		occStart := occ
		results = append(results, models.Event{
			ID:                  utils.FromPgUUID(ev.ID),
			CalendarID:          utils.FromPgUUID(ev.CalendarID),
			Title:               ev.Title,
			Description:         utils.FromPgText(ev.Description),
			Location:            utils.FromPgText(ev.Location),
			StartTime:           occStart,
			EndTime:             occEnd,
			Timezone:            ev.Timezone,
			AllDay:              ev.AllDay,
			RecurrenceRule:      utils.FromPgText(ev.RecurrenceRule),
			IsOccurrence:        true,
			OccurrenceStartTime: &occStart,
			OccurrenceEndTime:   &occEnd,
			CreatedBy:           utils.FromPgUUID(ev.CreatedBy),
			UpdatedBy:           utils.FromPgUUID(ev.UpdatedBy),
			CreatedAt:           utils.FromPgTimestamptz(ev.CreatedAt),
			UpdatedAt:           utils.FromPgTimestamptz(ev.UpdatedAt),
			Tags:                ev.Tags,
			Reminders:           []models.Reminder{},
			Attendees:           []models.Attendee{},
			Attachments:         []models.Attachment{},
		})
	}

	return results
}

func (s *EventService) loadReminders(ctx context.Context, eventID uuid.UUID) ([]models.Reminder, error) {
	rows, err := s.queries.ListRemindersByEvent(ctx, utils.ToPgUUID(eventID))
	if err != nil {
		return nil, models.ErrInternal
	}
	result := make([]models.Reminder, len(rows))
	for i, r := range rows {
		result[i] = models.Reminder{
			ID:            utils.FromPgUUID(r.ID),
			MinutesBefore: r.MinutesBefore,
		}
	}
	return result, nil
}

func (s *EventService) loadAttendees(ctx context.Context, eventID uuid.UUID) ([]models.Attendee, error) {
	rows, err := s.queries.ListAttendeesByEvent(ctx, utils.ToPgUUID(eventID))
	if err != nil {
		return nil, models.ErrInternal
	}
	result := make([]models.Attendee, len(rows))
	for i, a := range rows {
		var uid *uuid.UUID
		if a.UserID.Valid {
			u := utils.FromPgUUID(a.UserID)
			uid = &u
		}
		result[i] = models.Attendee{
			ID:     utils.FromPgUUID(a.ID),
			UserID: uid,
			Email:  utils.FromPgText(a.Email),
			Status: a.Status,
		}
	}
	return result, nil
}

func (s *EventService) loadAttachments(ctx context.Context, eventID uuid.UUID) ([]models.Attachment, error) {
	rows, err := s.queries.ListAttachmentsByEvent(ctx, utils.ToPgUUID(eventID))
	if err != nil {
		return nil, models.ErrInternal
	}
	result := make([]models.Attachment, len(rows))
	for i, a := range rows {
		result[i] = models.Attachment{
			ID:      utils.FromPgUUID(a.ID),
			FileURL: a.FileUrl,
		}
	}
	return result, nil
}

func eventFromDB(ev repository.Event, reminders []models.Reminder, attendees []models.Attendee, attachments []models.Attachment) *models.Event {
	if reminders == nil {
		reminders = []models.Reminder{}
	}
	if attendees == nil {
		attendees = []models.Attendee{}
	}
	if attachments == nil {
		attachments = []models.Attachment{}
	}
	tags := ev.Tags
	if tags == nil {
		tags = []string{}
	}

	return &models.Event{
		ID:             utils.FromPgUUID(ev.ID),
		CalendarID:     utils.FromPgUUID(ev.CalendarID),
		Title:          ev.Title,
		Description:    utils.FromPgText(ev.Description),
		Location:       utils.FromPgText(ev.Location),
		StartTime:      utils.FromPgTimestamptz(ev.StartTime),
		EndTime:        utils.FromPgTimestamptz(ev.EndTime),
		Timezone:       ev.Timezone,
		AllDay:         ev.AllDay,
		RecurrenceRule: utils.FromPgText(ev.RecurrenceRule),
		CreatedBy:      utils.FromPgUUID(ev.CreatedBy),
		UpdatedBy:      utils.FromPgUUID(ev.UpdatedBy),
		CreatedAt:      utils.FromPgTimestamptz(ev.CreatedAt),
		UpdatedAt:      utils.FromPgTimestamptz(ev.UpdatedAt),
		Reminders:      reminders,
		Attendees:      attendees,
		Attachments:    attachments,
		Tags:           tags,
	}
}

type CreateEventRequest struct {
	CalendarID     uuid.UUID
	Title          string
	Description    *string
	Location       *string
	StartTime      time.Time
	EndTime        time.Time
	Timezone       string
	AllDay         bool
	RecurrenceRule *string
	Reminders      []int32
	Tags           []string
}

type UpdateEventRequest struct {
	Title          *string
	Description    *string
	Location       *string
	StartTime      *time.Time
	EndTime        *time.Time
	Timezone       *string
	AllDay         *bool
	RecurrenceRule *string
	Tags           []string
}

type ListEventParams struct {
	RangeStart time.Time
	RangeEnd   time.Time
	CalendarID *uuid.UUID
	Search     *string
	Tag        *string
	Limit      int
	Cursor     string
}

func effectiveStart(e models.Event) time.Time {
	if e.OccurrenceStartTime != nil {
		return *e.OccurrenceStartTime
	}
	return e.StartTime
}

func optionalPgUUID(id *uuid.UUID) pgtype.UUID {
	if id == nil {
		return pgtype.UUID{Valid: false}
	}
	return utils.ToPgUUID(*id)
}

func optionalPgBool(b *bool) pgtype.Bool {
	if b == nil {
		return pgtype.Bool{Valid: false}
	}
	return pgtype.Bool{Bool: *b, Valid: true}
}
