package services

// AgendaService builds a merged, date-sorted feed of deadlines + appointments
// across every Project the caller can see. It underpins the `/agenda` page —
// a unified timeline that is neither deadline-centric (like /deadlines) nor
// appointment-centric (like /appointments/calendar).
//
// Visibility: reuses the same team-membership predicate applied everywhere
// else (paliad.project_teams + path walk). Personal Appointments (project_id
// IS NULL) remain creator-only.

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/jmoiron/sqlx"
)

// AgendaService returns agenda feed rows for the Dashboard's /agenda page.
type AgendaService struct {
	db         *sqlx.DB
	users      *UserService
	eventTypes *EventTypeService
}

// NewAgendaService wires the service. eventTypes powers the optional
// Event-Type filter on /agenda (t-paliad-088); pass nil in tests that
// don't exercise that surface.
func NewAgendaService(db *sqlx.DB, users *UserService, eventTypes *EventTypeService) *AgendaService {
	return &AgendaService{db: db, users: users, eventTypes: eventTypes}
}

// AgendaItem is one row in the merged feed. `Type` is "deadline" or
// "appointment"; date fields are populated differently per type (deadlines
// have a date-only DueDate, appointments have StartAt/EndAt). The client
// groups by the local calendar day derived from `Date`.
type AgendaItem struct {
	ID           uuid.UUID  `json:"id"`
	Type         string     `json:"type"` // "deadline" | "appointment"
	Title        string     `json:"title"`
	Date         time.Time  `json:"date"`                // canonical sort key (day start for deadlines, start_at for appointments)
	EndAt        *time.Time `json:"end_at,omitempty"`    // appointments only
	DueDate      *string    `json:"due_date,omitempty"`  // deadlines only (YYYY-MM-DD)
	Status       *string    `json:"status,omitempty"`    // deadlines: pending/completed/...
	Location     *string    `json:"location,omitempty"`  // appointments only
	AppointmentType *string `json:"appointment_type,omitempty"`
	Urgency      string     `json:"urgency"` // overdue | today | tomorrow | this_week | later
	ProjectID    *uuid.UUID `json:"project_id,omitempty"`
	ProjectTitle *string    `json:"project_title,omitempty"`
	ProjectType  *string    `json:"project_type,omitempty"`
	ProjectRef   *string    `json:"project_reference,omitempty"`
	// ApprovalStatus (t-paliad-138) — "pending" → render warning pill on
	// the agenda timeline. "approved"/"legacy" → no pill.
	ApprovalStatus *string `json:"approval_status,omitempty"`
}

// AgendaFilter narrows the merged feed.
//
// EventTypeIDs / IncludeUntyped restrict the deadline side of the feed
// (appointments are unaffected — they have no event_types). When both
// are zero/false the filter is inactive.
type AgendaFilter struct {
	From                time.Time // inclusive, UTC
	To                  time.Time // exclusive, UTC
	IncludeDeadlines    bool
	IncludeAppointments bool
	EventTypeIDs        []uuid.UUID
	IncludeUntyped      bool
}

// List returns all AgendaItems for the user's visible projects within
// [From, To), sorted by Date ascending. Completed deadlines are excluded —
// the agenda is about what's coming up, not audit history.
func (s *AgendaService) List(ctx context.Context, userID uuid.UUID, f AgendaFilter) ([]AgendaItem, error) {
	if !f.IncludeDeadlines && !f.IncludeAppointments {
		return []AgendaItem{}, nil
	}
	if f.To.Before(f.From) || f.To.Equal(f.From) {
		return []AgendaItem{}, nil
	}

	// Existence gate — visibility predicate inside loadDeadlines/Appointments
	// reads global_role directly from paliad.users via the SQL helper, so we
	// no longer need the row itself, just confirmation that the caller exists.
	user, err := s.users.GetByID(ctx, userID)
	if err != nil {
		return nil, err
	}
	if user == nil {
		return []AgendaItem{}, nil
	}

	items := make([]AgendaItem, 0, 64)

	if f.IncludeDeadlines {
		rows, err := s.loadDeadlines(ctx, userID, f.From, f.To, f.EventTypeIDs, f.IncludeUntyped)
		if err != nil {
			return nil, err
		}
		items = append(items, rows...)
	}
	if f.IncludeAppointments {
		rows, err := s.loadAppointments(ctx, userID, f.From, f.To)
		if err != nil {
			return nil, err
		}
		items = append(items, rows...)
	}

	sort.SliceStable(items, func(i, j int) bool {
		if items[i].Date.Equal(items[j].Date) {
			// Stable tiebreaker: deadlines before appointments on the same
			// instant, then alphabetic by title so the feed is deterministic.
			if items[i].Type != items[j].Type {
				return items[i].Type == "deadline"
			}
			return items[i].Title < items[j].Title
		}
		return items[i].Date.Before(items[j].Date)
	})

	annotateAgendaUrgency(items, time.Now().UTC())
	return items, nil
}

// loadDeadlines pulls pending deadlines whose due_date falls in [from, to).
// Completed deadlines are hidden — agenda is forward-looking.
// eventTypeIDs / includeUntyped restrict which deadlines are returned;
// see AgendaFilter for the OR-composition semantics.
func (s *AgendaService) loadDeadlines(ctx context.Context, userID uuid.UUID, from, to time.Time, eventTypeIDs []uuid.UUID, includeUntyped bool) ([]AgendaItem, error) {
	// due_date is a DATE; compare against the date portion of the window.
	fromDate := from.Format("2006-01-02")
	toDate := to.Format("2006-01-02")

	args := []any{userID, fromDate, toDate}
	etClause := ""
	if len(eventTypeIDs) > 0 || includeUntyped {
		parts := []string{}
		if len(eventTypeIDs) > 0 {
			placeholders := make([]string, 0, len(eventTypeIDs))
			for _, id := range eventTypeIDs {
				args = append(args, id)
				placeholders = append(placeholders, fmt.Sprintf("$%d", len(args)))
			}
			parts = append(parts, fmt.Sprintf(`EXISTS (
				SELECT 1 FROM paliad.deadline_event_types det
				 WHERE det.deadline_id = f.id
				   AND det.event_type_id IN (%s)
			)`, strings.Join(placeholders, ", ")))
		}
		if includeUntyped {
			parts = append(parts, `NOT EXISTS (
				SELECT 1 FROM paliad.deadline_event_types det
				 WHERE det.deadline_id = f.id
			)`)
		}
		if len(parts) == 1 {
			etClause = "\n   AND " + parts[0]
		} else {
			etClause = "\n   AND (" + strings.Join(parts, " OR ") + ")"
		}
	}

	query := `
SELECT f.id,
       f.title,
       f.due_date,
       f.status,
       f.approval_status,
       p.id         AS project_id,
       p.title      AS project_title,
       p.type       AS project_type,
       p.reference  AS project_reference
  FROM paliad.deadlines f
  JOIN paliad.projects  p ON p.id = f.project_id
 WHERE f.status = 'pending'
   AND f.due_date >= $2::date
   AND f.due_date <  $3::date
   AND ` + visibilityPredicatePositional("p", 1) + etClause + `
 ORDER BY f.due_date ASC, f.created_at ASC`

	type row struct {
		ID               uuid.UUID `db:"id"`
		Title            string    `db:"title"`
		DueDate          time.Time `db:"due_date"`
		Status           string    `db:"status"`
		ApprovalStatus   string    `db:"approval_status"`
		ProjectID        uuid.UUID `db:"project_id"`
		ProjectTitle     string    `db:"project_title"`
		ProjectType      string    `db:"project_type"`
		ProjectReference *string   `db:"project_reference"`
	}
	var rows []row
	if err := s.db.SelectContext(ctx, &rows, query, userID, fromDate, toDate); err != nil {
		return nil, fmt.Errorf("agenda deadlines: %w", err)
	}

	out := make([]AgendaItem, 0, len(rows))
	for _, r := range rows {
		due := r.DueDate.Format("2006-01-02")
		status := r.Status
		approvalStatus := r.ApprovalStatus
		projectID := r.ProjectID
		projectTitle := r.ProjectTitle
		projectType := r.ProjectType
		out = append(out, AgendaItem{
			ID:             r.ID,
			Type:           "deadline",
			Title:          r.Title,
			Date:           time.Date(r.DueDate.Year(), r.DueDate.Month(), r.DueDate.Day(), 0, 0, 0, 0, time.UTC),
			DueDate:        &due,
			Status:         &status,
			ApprovalStatus: &approvalStatus,
			ProjectID:      &projectID,
			ProjectTitle:   &projectTitle,
			ProjectType:    &projectType,
			ProjectRef:     r.ProjectReference,
		})
	}
	return out, nil
}

// loadAppointments pulls appointments whose start_at falls in [from, to).
// Includes personal appointments (project_id IS NULL, creator-only) and
// project-attached appointments subject to the team predicate.
func (s *AgendaService) loadAppointments(ctx context.Context, userID uuid.UUID, from, to time.Time) ([]AgendaItem, error) {
	query := `
SELECT t.id,
       t.title,
       t.start_at,
       t.end_at,
       t.location,
       t.appointment_type,
       t.approval_status,
       t.project_id,
       p.title      AS project_title,
       p.type       AS project_type,
       p.reference  AS project_reference
  FROM paliad.appointments t
  LEFT JOIN paliad.projects p ON p.id = t.project_id
 WHERE t.start_at >= $2
   AND t.start_at <  $3
   AND (
        (t.project_id IS NULL AND t.created_by = $1)
        OR (t.project_id IS NOT NULL AND ` + visibilityPredicatePositional("p", 1) + `)
       )
 ORDER BY t.start_at ASC, t.created_at ASC`

	type row struct {
		ID               uuid.UUID  `db:"id"`
		Title            string     `db:"title"`
		StartAt          time.Time  `db:"start_at"`
		EndAt            *time.Time `db:"end_at"`
		Location         *string    `db:"location"`
		AppointmentType  *string    `db:"appointment_type"`
		ApprovalStatus   string     `db:"approval_status"`
		ProjectID        *uuid.UUID `db:"project_id"`
		ProjectTitle     *string    `db:"project_title"`
		ProjectType      *string    `db:"project_type"`
		ProjectReference *string    `db:"project_reference"`
	}
	var rows []row
	if err := s.db.SelectContext(ctx, &rows, query, userID, from, to); err != nil {
		return nil, fmt.Errorf("agenda appointments: %w", err)
	}

	out := make([]AgendaItem, 0, len(rows))
	for _, r := range rows {
		approvalStatus := r.ApprovalStatus
		out = append(out, AgendaItem{
			ID:              r.ID,
			Type:            "appointment",
			Title:           r.Title,
			Date:            r.StartAt,
			EndAt:           r.EndAt,
			Location:        r.Location,
			AppointmentType: r.AppointmentType,
			ApprovalStatus:  &approvalStatus,
			ProjectID:       r.ProjectID,
			ProjectTitle:    r.ProjectTitle,
			ProjectType:     r.ProjectType,
			ProjectRef:      r.ProjectReference,
		})
	}
	return out, nil
}

// annotateAgendaUrgency classifies each item so the client can apply the
// traffic-light styling without re-deriving the buckets.
//
//   overdue   — in the past (deadlines only; appointments only go "later")
//   today     — same calendar day (UTC — kept in sync with server window)
//   tomorrow  — next calendar day
//   this_week — within the next 7 days (exclusive of today/tomorrow)
//   later     — beyond 7 days
func annotateAgendaUrgency(items []AgendaItem, now time.Time) {
	today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
	tomorrow := today.AddDate(0, 0, 1)
	dayAfterTomorrow := today.AddDate(0, 0, 2)
	endOfWeek := today.AddDate(0, 0, 7)

	for i := range items {
		d := items[i].Date
		switch {
		case d.Before(today):
			items[i].Urgency = "overdue"
		case !d.Before(today) && d.Before(tomorrow):
			items[i].Urgency = "today"
		case !d.Before(tomorrow) && d.Before(dayAfterTomorrow):
			items[i].Urgency = "tomorrow"
		case !d.Before(dayAfterTomorrow) && d.Before(endOfWeek):
			items[i].Urgency = "this_week"
		default:
			items[i].Urgency = "later"
		}
	}
}
