package repositories

import (
	"errors"
	"fmt"
	"time"

	"github.com/rs/zerolog/log"
	"gorm.io/gorm"

	"github.com/treytartt/honeydue-api/internal/dto/responses"
	"github.com/treytartt/honeydue-api/internal/models"
	"github.com/treytartt/honeydue-api/internal/task"
	"github.com/treytartt/honeydue-api/internal/task/categorization"
)

// ErrVersionConflict indicates a concurrent modification was detected
var ErrVersionConflict = errors.New("version conflict: task was modified by another request")

// TaskRepository handles database operations for tasks
type TaskRepository struct {
	db *gorm.DB
}

// NewTaskRepository creates a new task repository
func NewTaskRepository(db *gorm.DB) *TaskRepository {
	return &TaskRepository{db: db}
}

// DB returns the underlying database connection.
// Used by services that need to run transactions spanning multiple operations.
func (r *TaskRepository) DB() *gorm.DB {
	return r.db
}

// CreateCompletionTx creates a new task completion within an existing transaction.
func (r *TaskRepository) CreateCompletionTx(tx *gorm.DB, completion *models.TaskCompletion) error {
	return tx.Create(completion).Error
}

// taskUpdateFields returns the canonical field map used by both Update and UpdateTx.
// Centralised here so the two methods never drift out of sync.
func taskUpdateFields(t *models.Task) map[string]interface{} {
	return map[string]interface{}{
		"title":                t.Title,
		"description":          t.Description,
		"category_id":          t.CategoryID,
		"priority_id":          t.PriorityID,
		"frequency_id":         t.FrequencyID,
		"custom_interval_days": t.CustomIntervalDays,
		"in_progress":          t.InProgress,
		"assigned_to_id":       t.AssignedToID,
		"due_date":             t.DueDate,
		"next_due_date":        t.NextDueDate,
		"estimated_cost":       t.EstimatedCost,
		"actual_cost":          t.ActualCost,
		"contractor_id":        t.ContractorID,
		"is_cancelled":         t.IsCancelled,
		"is_archived":          t.IsArchived,
		"version":              gorm.Expr("version + 1"),
	}
}

// taskUpdateOmitAssociations lists the association fields to omit during task updates.
var taskUpdateOmitAssociations = []string{
	"Residence", "CreatedBy", "AssignedTo", "Category", "Priority", "Frequency", "ParentTask", "Completions",
}

// UpdateTx updates a task with optimistic locking within an existing transaction.
func (r *TaskRepository) UpdateTx(tx *gorm.DB, task *models.Task) error {
	result := tx.Model(task).
		Where("id = ? AND version = ?", task.ID, task.Version).
		Omit(taskUpdateOmitAssociations...).
		Updates(taskUpdateFields(task))
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return ErrVersionConflict
	}
	task.Version++ // Update local copy
	return nil
}

// === Task Filter Options ===

// TaskFilterOptions provides flexible filtering for task queries.
// Use exactly one of ResidenceID, ResidenceIDs, or UserIDs to specify the filter scope.
type TaskFilterOptions struct {
	// Filter by single residence (kanban single-residence view)
	ResidenceID uint

	// Filter by multiple residences (kanban all-residences view)
	ResidenceIDs []uint

	// Filter by users - matches tasks where assigned_to IN userIDs
	// OR residence owner IN userIDs (for notifications)
	UserIDs []uint

	// Include archived tasks (default: false, excludes archived)
	IncludeArchived bool

	// IncludeInProgress controls whether in-progress tasks are included in
	// overdue/due-soon/upcoming queries. Default is false (excludes in-progress)
	// for kanban column consistency. Set to true for notifications where
	// users should still be notified about in-progress tasks that are overdue.
	IncludeInProgress bool

	// Preload options
	PreloadCreatedBy   bool
	PreloadAssignedTo  bool
	PreloadResidence   bool
	PreloadCompletions bool // Minimal: just id, task_id, completed_at
	PreloadFrequency   bool // For smart notifications
}

// applyFilterOptions applies the filter options to a query.
// Returns a new query with filters and preloads applied.
func (r *TaskRepository) applyFilterOptions(query *gorm.DB, opts TaskFilterOptions) *gorm.DB {
	// Apply residence/user filters
	if opts.ResidenceID != 0 {
		query = query.Where("task_task.residence_id = ?", opts.ResidenceID)
	} else if len(opts.ResidenceIDs) > 0 {
		query = query.Where("task_task.residence_id IN ?", opts.ResidenceIDs)
	} else if len(opts.UserIDs) > 0 {
		// For notifications: tasks assigned to users OR owned by users (only from active residences)
		query = query.Where(
			"(task_task.assigned_to_id IN ? OR task_task.residence_id IN (SELECT id FROM residence_residence WHERE owner_id IN ? AND is_active = true))",
			opts.UserIDs, opts.UserIDs,
		)
	}

	// Apply archived filter (default excludes archived)
	if !opts.IncludeArchived {
		query = query.Where("task_task.is_archived = ?", false)
	}

	// Apply preloads
	if opts.PreloadCreatedBy {
		query = query.Preload("CreatedBy")
	}
	if opts.PreloadAssignedTo {
		query = query.Preload("AssignedTo")
	}
	if opts.PreloadResidence {
		query = query.Preload("Residence")
	}
	if opts.PreloadCompletions {
		query = query.Preload("Completions", func(db *gorm.DB) *gorm.DB {
			return db.Select("id", "task_id", "completed_at")
		})
	}
	if opts.PreloadFrequency {
		query = query.Preload("Frequency")
	}

	return query
}

// === Single-Purpose Task Query Functions ===
// These functions use the scopes from internal/task/scopes for consistent filtering.
// They are the single source of truth for task categorization queries, used by both
// kanban and notification handlers.

// GetOverdueTasks returns active, non-completed tasks past their effective due date.
// Uses task.ScopeOverdue for consistent filtering logic.
// The `now` parameter should be in the user's timezone for accurate overdue detection.
//
// By default, excludes in-progress tasks for kanban column consistency.
// Set opts.IncludeInProgress=true for notifications where in-progress tasks should still appear.
func (r *TaskRepository) GetOverdueTasks(now time.Time, opts TaskFilterOptions) ([]models.Task, error) {
	var tasks []models.Task
	query := r.db.Model(&models.Task{})

	if opts.IncludeArchived {
		// When including archived, build the query manually to skip the archived check
		// but still apply cancelled check, not-completed check, and date check
		// IMPORTANT: Use date comparison (not timestamps) for timezone correctness
		todayStr := now.Format("2006-01-02")
		query = query.Where("is_cancelled = ?", false).
			Scopes(task.ScopeNotCompleted).
			Where("DATE(COALESCE(next_due_date, due_date)) < DATE(?)", todayStr)
	} else {
		// Use the combined scope which includes is_archived = false
		query = query.Scopes(task.ScopeOverdue(now))
	}

	query = query.Scopes(task.ScopeKanbanOrder)
	if !opts.IncludeInProgress {
		query = query.Scopes(task.ScopeNotInProgress)
	}
	query = r.applyFilterOptions(query, opts)
	err := query.Find(&tasks).Error
	return tasks, err
}

// GetDueSoonTasks returns active, non-completed tasks due within the threshold.
// Uses task.ScopeDueSoon for consistent filtering logic.
// The `now` parameter should be in the user's timezone for accurate detection.
//
// By default, excludes in-progress tasks for kanban column consistency.
// Set opts.IncludeInProgress=true for notifications where in-progress tasks should still appear.
func (r *TaskRepository) GetDueSoonTasks(now time.Time, daysThreshold int, opts TaskFilterOptions) ([]models.Task, error) {
	var tasks []models.Task
	query := r.db.Model(&models.Task{}).
		Scopes(task.ScopeDueSoon(now, daysThreshold), task.ScopeKanbanOrder)
	if !opts.IncludeInProgress {
		query = query.Scopes(task.ScopeNotInProgress)
	}
	query = r.applyFilterOptions(query, opts)
	err := query.Find(&tasks).Error
	return tasks, err
}

// GetInProgressTasks returns active, non-completed tasks marked as in-progress.
// Uses task.ScopeInProgress for consistent filtering logic.
//
// Note: Excludes completed tasks to match kanban column behavior (completed has higher priority).
func (r *TaskRepository) GetInProgressTasks(opts TaskFilterOptions) ([]models.Task, error) {
	var tasks []models.Task
	query := r.db.Model(&models.Task{}).
		Scopes(task.ScopeActive, task.ScopeNotCompleted, task.ScopeInProgress, task.ScopeKanbanOrder)
	query = r.applyFilterOptions(query, opts)
	err := query.Find(&tasks).Error
	return tasks, err
}

// GetUpcomingTasks returns active, non-completed tasks due after the threshold or with no due date.
// Uses task.ScopeUpcoming for consistent filtering logic.
//
// By default, excludes in-progress tasks for kanban column consistency.
// Set opts.IncludeInProgress=true for notifications where in-progress tasks should still appear.
func (r *TaskRepository) GetUpcomingTasks(now time.Time, daysThreshold int, opts TaskFilterOptions) ([]models.Task, error) {
	var tasks []models.Task
	query := r.db.Model(&models.Task{}).
		Scopes(task.ScopeUpcoming(now, daysThreshold), task.ScopeKanbanOrder)
	if !opts.IncludeInProgress {
		query = query.Scopes(task.ScopeNotInProgress)
	}
	query = r.applyFilterOptions(query, opts)
	err := query.Find(&tasks).Error
	return tasks, err
}

// GetCompletedTasks returns completed tasks (NextDueDate nil with at least one completion).
// Uses task.ScopeCompleted for consistent filtering logic.
func (r *TaskRepository) GetCompletedTasks(opts TaskFilterOptions) ([]models.Task, error) {
	var tasks []models.Task
	// Completed tasks: not cancelled, has completion, no next due date
	// Note: We don't apply ScopeActive because completed tasks may not be "active" in that sense
	query := r.db.Model(&models.Task{}).
		Where("is_cancelled = ?", false).
		Scopes(task.ScopeCompleted, task.ScopeKanbanOrder)
	query = r.applyFilterOptions(query, opts)
	err := query.Find(&tasks).Error
	return tasks, err
}

// GetCancelledTasks returns cancelled OR archived tasks.
// Archived tasks are grouped with cancelled for kanban purposes - they both represent
// tasks that are no longer active/actionable.
func (r *TaskRepository) GetCancelledTasks(opts TaskFilterOptions) ([]models.Task, error) {
	var tasks []models.Task
	// Include both cancelled and archived tasks in this column
	// Archived tasks should ONLY appear here, not in any other column
	query := r.db.Model(&models.Task{}).
		Where("is_cancelled = ? OR is_archived = ?", true, true).
		Scopes(task.ScopeKanbanOrder)

	// Override IncludeArchived to true since this function specifically handles archived tasks
	opts.IncludeArchived = true
	query = r.applyFilterOptions(query, opts)
	err := query.Find(&tasks).Error
	return tasks, err
}

// GetActiveTasksForUsers returns all active, non-completed tasks for the specified users.
// This is used by the smart notification system to evaluate all tasks for potential reminders.
// It includes tasks that are overdue, due soon, or upcoming - the caller determines
// which reminders to send based on the task's frequency and due date.
func (r *TaskRepository) GetActiveTasksForUsers(now time.Time, opts TaskFilterOptions) ([]models.Task, error) {
	var tasks []models.Task

	// Get all active, non-completed tasks
	query := r.db.Model(&models.Task{}).
		Scopes(task.ScopeActive, task.ScopeNotCompleted)

	// Include in-progress tasks if specified
	if !opts.IncludeInProgress {
		query = query.Scopes(task.ScopeNotInProgress)
	}

	// Apply filters and preloads
	query = r.applyFilterOptions(query, opts)

	// Order by due date for consistent processing
	query = query.Order("COALESCE(next_due_date, due_date) ASC NULLS LAST")

	err := query.Find(&tasks).Error
	return tasks, err
}

// === Task CRUD ===

// FindByID finds a task by ID with preloaded relations
// Note: Category, Priority, Frequency are NOT preloaded - client resolves from cache using IDs
func (r *TaskRepository) FindByID(id uint) (*models.Task, error) {
	var task models.Task
	err := r.db.Preload("Residence").
		Preload("CreatedBy").
		Preload("AssignedTo").
		Preload("Completions").
		Preload("Completions.Images").
		Preload("Completions.CompletedBy").
		First(&task, id).Error
	if err != nil {
		return nil, err
	}
	return &task, nil
}

// FindByResidence finds all tasks for a residence
// Note: Category, Priority, Frequency are NOT preloaded - client resolves from cache using IDs
func (r *TaskRepository) FindByResidence(residenceID uint) ([]models.Task, error) {
	var tasks []models.Task
	err := r.db.Preload("CreatedBy").
		Preload("AssignedTo").
		Preload("Completions").
		Preload("Completions.Images").
		Preload("Completions.CompletedBy").
		Where("residence_id = ?", residenceID).
		Order("due_date ASC NULLS LAST, created_at DESC").
		Find(&tasks).Error
	return tasks, err
}

// FindByUser finds all tasks accessible to a user (across all their residences)
// Note: Category, Priority, Frequency are NOT preloaded - client resolves from cache using IDs
func (r *TaskRepository) FindByUser(userID uint, residenceIDs []uint) ([]models.Task, error) {
	var tasks []models.Task
	err := r.db.Preload("Residence").
		Preload("CreatedBy").
		Preload("AssignedTo").
		Preload("Completions").
		Preload("Completions.Images").
		Preload("Completions.CompletedBy").
		Where("residence_id IN ?", residenceIDs).
		Order("due_date ASC NULLS LAST, created_at DESC").
		Find(&tasks).Error
	return tasks, err
}

// Create creates a new task
func (r *TaskRepository) Create(task *models.Task) error {
	return r.db.Create(task).Error
}

// CreateTx creates a new task within an existing transaction. Used by
// bulk-create flows where multiple inserts must succeed or fail together.
func (r *TaskRepository) CreateTx(tx *gorm.DB, task *models.Task) error {
	return tx.Create(task).Error
}

// FindByIDTx loads a task within an existing transaction. Preloads only the
// fields the bulk-create response needs (CreatedBy, AssignedTo). Category /
// Priority / Frequency are resolved client-side from the lookup cache, so
// we skip them here to match the FindByResidence preload set.
func (r *TaskRepository) FindByIDTx(tx *gorm.DB, id uint) (*models.Task, error) {
	var task models.Task
	err := tx.Preload("CreatedBy").
		Preload("AssignedTo").
		First(&task, id).Error
	if err != nil {
		return nil, err
	}
	return &task, nil
}

// Update updates a task with optimistic locking.
// The update only succeeds if the task's version in the database matches the expected version.
// On success, the local task.Version is incremented to reflect the new version.
func (r *TaskRepository) Update(task *models.Task) error {
	result := r.db.Model(task).
		Where("id = ? AND version = ?", task.ID, task.Version).
		Omit(taskUpdateOmitAssociations...).
		Updates(taskUpdateFields(task))
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return ErrVersionConflict
	}
	task.Version++ // Update local copy
	return nil
}

// Delete hard-deletes a task
func (r *TaskRepository) Delete(id uint) error {
	return r.db.Delete(&models.Task{}, id).Error
}

// === Task State Operations ===

// MarkInProgress marks a task as in progress with optimistic locking.
func (r *TaskRepository) MarkInProgress(id uint, version int) error {
	result := r.db.Model(&models.Task{}).
		Where("id = ? AND version = ?", id, version).
		Updates(map[string]interface{}{
			"in_progress": true,
			"version":     gorm.Expr("version + 1"),
		})
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return ErrVersionConflict
	}
	return nil
}

// Cancel cancels a task with optimistic locking.
func (r *TaskRepository) Cancel(id uint, version int) error {
	result := r.db.Model(&models.Task{}).
		Where("id = ? AND version = ?", id, version).
		Updates(map[string]interface{}{
			"is_cancelled": true,
			"version":      gorm.Expr("version + 1"),
		})
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return ErrVersionConflict
	}
	return nil
}

// Uncancel uncancels a task with optimistic locking.
func (r *TaskRepository) Uncancel(id uint, version int) error {
	result := r.db.Model(&models.Task{}).
		Where("id = ? AND version = ?", id, version).
		Updates(map[string]interface{}{
			"is_cancelled": false,
			"version":      gorm.Expr("version + 1"),
		})
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return ErrVersionConflict
	}
	return nil
}

// Archive archives a task with optimistic locking.
func (r *TaskRepository) Archive(id uint, version int) error {
	result := r.db.Model(&models.Task{}).
		Where("id = ? AND version = ?", id, version).
		Updates(map[string]interface{}{
			"is_archived": true,
			"version":     gorm.Expr("version + 1"),
		})
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return ErrVersionConflict
	}
	return nil
}

// Unarchive unarchives a task with optimistic locking.
func (r *TaskRepository) Unarchive(id uint, version int) error {
	result := r.db.Model(&models.Task{}).
		Where("id = ? AND version = ?", id, version).
		Updates(map[string]interface{}{
			"is_archived": false,
			"version":     gorm.Expr("version + 1"),
		})
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return ErrVersionConflict
	}
	return nil
}

// === Kanban Board ===

// buildKanbanColumns builds the kanban column array from categorized task slices.
// This is a helper function to reduce duplication between GetKanbanData and GetKanbanDataForMultipleResidences.
// Note: cancelled/archived tasks are intentionally hidden from the kanban board.
// They still retain "cancelled_tasks" as task-level categorization for detail views/actions.
func buildKanbanColumns(
	overdue, inProgress, dueSoon, upcoming, completed []models.Task,
) []models.KanbanColumn {
	return []models.KanbanColumn{
		{
			Name:        string(categorization.ColumnOverdue),
			DisplayName: "Overdue",
			ButtonTypes: []string{"edit", "complete", "cancel", "mark_in_progress"},
			Icons:       map[string]string{"ios": "exclamationmark.triangle", "android": "Warning"},
			Color:       "#FF3B30",
			Tasks:       overdue,
			Count:       len(overdue),
		},
		{
			Name:        string(categorization.ColumnInProgress),
			DisplayName: "In Progress",
			ButtonTypes: []string{"edit", "complete", "cancel"},
			Icons:       map[string]string{"ios": "hammer", "android": "Build"},
			Color:       "#5856D6",
			Tasks:       inProgress,
			Count:       len(inProgress),
		},
		{
			Name:        string(categorization.ColumnDueSoon),
			DisplayName: "Due Soon",
			ButtonTypes: []string{"edit", "complete", "cancel", "mark_in_progress"},
			Icons:       map[string]string{"ios": "clock", "android": "Schedule"},
			Color:       "#FF9500",
			Tasks:       dueSoon,
			Count:       len(dueSoon),
		},
		{
			Name:        string(categorization.ColumnUpcoming),
			DisplayName: "Upcoming",
			ButtonTypes: []string{"edit", "complete", "cancel", "mark_in_progress"},
			Icons:       map[string]string{"ios": "calendar", "android": "Event"},
			Color:       "#007AFF",
			Tasks:       upcoming,
			Count:       len(upcoming),
		},
		{
			Name:        string(categorization.ColumnCompleted),
			DisplayName: "Completed",
			ButtonTypes: []string{},
			Icons:       map[string]string{"ios": "checkmark.circle", "android": "CheckCircle"},
			Color:       "#34C759",
			Tasks:       completed,
			Count:       len(completed),
		},
		// Intentionally hidden from board:
		// cancelled/archived tasks are not returned as a kanban column.
		// {
		// 	Name:        string(categorization.ColumnCancelled),
		// 	DisplayName: "Cancelled",
		// 	ButtonTypes: []string{"uncancel", "delete"},
		// 	Icons:       map[string]string{"ios": "xmark.circle", "android": "Cancel"},
		// 	Color:       "#8E8E93",
		// 	Tasks:       cancelled,
		// 	Count:       len(cancelled),
		// },
	}
}

// GetKanbanData retrieves tasks organized for kanban display.
// Fetches all non-cancelled, non-archived tasks for the residence in a single query,
// then categorizes them in-memory using the task categorization chain for consistency
// with the predicate-based logic used throughout the application.
// The `now` parameter should be the start of day in the user's timezone for accurate overdue detection.
//
// Optimization: Single query with preloads, then in-memory categorization.
// Images and CompletedBy are NOT preloaded - fetch separately when viewing completion details.
func (r *TaskRepository) GetKanbanData(residenceID uint, daysThreshold int, now time.Time) (*models.KanbanBoard, error) {
	// Fetch all tasks for this residence in a single query (excluding cancelled/archived)
	var allTasks []models.Task
	query := r.db.Model(&models.Task{}).
		Where("task_task.residence_id = ?", residenceID).
		Preload("CreatedBy").
		Preload("AssignedTo").
		Preload("Completions", func(db *gorm.DB) *gorm.DB {
			return db.Select("id", "task_id", "completed_at")
		}).
		Scopes(task.ScopeKanbanOrder)

	if err := query.Find(&allTasks).Error; err != nil {
		return nil, fmt.Errorf("get tasks for kanban: %w", err)
	}

	// Categorize all tasks in-memory using the categorization chain
	columnMap := categorization.CategorizeTasksIntoColumnsWithTime(allTasks, daysThreshold, now)

	columns := buildKanbanColumns(
		columnMap[categorization.ColumnOverdue],
		columnMap[categorization.ColumnInProgress],
		columnMap[categorization.ColumnDueSoon],
		columnMap[categorization.ColumnUpcoming],
		columnMap[categorization.ColumnCompleted],
	)

	return &models.KanbanBoard{
		Columns:       columns,
		DaysThreshold: daysThreshold,
		ResidenceID:   fmt.Sprintf("%d", residenceID),
	}, nil
}

// GetKanbanDataForMultipleResidences retrieves tasks from multiple residences organized for kanban display.
// Fetches all tasks in a single query, then categorizes them in-memory using the
// task categorization chain for consistency with predicate-based logic.
// The `now` parameter should be the start of day in the user's timezone for accurate overdue detection.
//
// Optimization: Single query with preloads, then in-memory categorization.
// Images and CompletedBy are NOT preloaded - fetch separately when viewing completion details.
func (r *TaskRepository) GetKanbanDataForMultipleResidences(residenceIDs []uint, daysThreshold int, now time.Time) (*models.KanbanBoard, error) {
	// Fetch all tasks for these residences in a single query (excluding cancelled/archived)
	var allTasks []models.Task
	query := r.db.Model(&models.Task{}).
		Where("task_task.residence_id IN ?", residenceIDs).
		Preload("CreatedBy").
		Preload("AssignedTo").
		Preload("Residence").
		Preload("Completions", func(db *gorm.DB) *gorm.DB {
			return db.Select("id", "task_id", "completed_at")
		}).
		Scopes(task.ScopeKanbanOrder)

	if err := query.Find(&allTasks).Error; err != nil {
		return nil, fmt.Errorf("get tasks for kanban: %w", err)
	}

	// Categorize all tasks in-memory using the categorization chain
	columnMap := categorization.CategorizeTasksIntoColumnsWithTime(allTasks, daysThreshold, now)

	columns := buildKanbanColumns(
		columnMap[categorization.ColumnOverdue],
		columnMap[categorization.ColumnInProgress],
		columnMap[categorization.ColumnDueSoon],
		columnMap[categorization.ColumnUpcoming],
		columnMap[categorization.ColumnCompleted],
	)

	return &models.KanbanBoard{
		Columns:       columns,
		DaysThreshold: daysThreshold,
		ResidenceID:   "all",
	}, nil
}

// === Lookup Operations ===

// GetAllCategories returns all task categories
func (r *TaskRepository) GetAllCategories() ([]models.TaskCategory, error) {
	var categories []models.TaskCategory
	err := r.db.Order("display_order, name").Find(&categories).Error
	return categories, err
}

// GetAllPriorities returns all task priorities
func (r *TaskRepository) GetAllPriorities() ([]models.TaskPriority, error) {
	var priorities []models.TaskPriority
	err := r.db.Order("level").Find(&priorities).Error
	return priorities, err
}

// GetAllFrequencies returns all task frequencies
func (r *TaskRepository) GetAllFrequencies() ([]models.TaskFrequency, error) {
	var frequencies []models.TaskFrequency
	err := r.db.Order("display_order").Find(&frequencies).Error
	return frequencies, err
}

// GetFrequencyByID retrieves a single frequency by ID
func (r *TaskRepository) GetFrequencyByID(id uint) (*models.TaskFrequency, error) {
	var frequency models.TaskFrequency
	err := r.db.First(&frequency, id).Error
	if err != nil {
		return nil, err
	}
	return &frequency, nil
}

// CountByResidence counts tasks in a residence
func (r *TaskRepository) CountByResidence(residenceID uint) (int64, error) {
	var count int64
	err := r.db.Model(&models.Task{}).
		Where("residence_id = ? AND is_cancelled = ? AND is_archived = ?", residenceID, false, false).
		Count(&count).Error
	return count, err
}

// CountByResidenceIDs counts all active tasks across multiple residences in a single query.
// Returns the total count of non-cancelled, non-archived tasks for the given residence IDs.
func (r *TaskRepository) CountByResidenceIDs(residenceIDs []uint) (int64, error) {
	if len(residenceIDs) == 0 {
		return 0, nil
	}
	var count int64
	err := r.db.Model(&models.Task{}).
		Where("residence_id IN ? AND is_cancelled = ? AND is_archived = ?", residenceIDs, false, false).
		Count(&count).Error
	return count, err
}

// === Task Completion Operations ===

// CreateCompletion creates a new task completion
func (r *TaskRepository) CreateCompletion(completion *models.TaskCompletion) error {
	return r.db.Create(completion).Error
}

// FindCompletionByID finds a completion by ID
func (r *TaskRepository) FindCompletionByID(id uint) (*models.TaskCompletion, error) {
	var completion models.TaskCompletion
	err := r.db.Preload("Task").
		Preload("CompletedBy").
		Preload("Images").
		First(&completion, id).Error
	if err != nil {
		return nil, err
	}
	return &completion, nil
}

// FindCompletionsByTask finds all completions for a task
func (r *TaskRepository) FindCompletionsByTask(taskID uint) ([]models.TaskCompletion, error) {
	var completions []models.TaskCompletion
	err := r.db.Preload("CompletedBy").
		Preload("Images").
		Where("task_id = ?", taskID).
		Order("completed_at DESC").
		Find(&completions).Error
	return completions, err
}

// FindCompletionsByUser finds all completions by a user
func (r *TaskRepository) FindCompletionsByUser(userID uint, residenceIDs []uint) ([]models.TaskCompletion, error) {
	var completions []models.TaskCompletion
	err := r.db.Preload("Task").
		Preload("CompletedBy").
		Preload("Images").
		Joins("JOIN task_task ON task_task.id = task_taskcompletion.task_id").
		Where("task_task.residence_id IN ?", residenceIDs).
		Order("completed_at DESC").
		Find(&completions).Error
	return completions, err
}

// UpdateCompletion updates an existing task completion
func (r *TaskRepository) UpdateCompletion(completion *models.TaskCompletion) error {
	return r.db.Omit("Task", "CompletedBy", "Images").Save(completion).Error
}

// DeleteCompletion deletes a task completion and its associated images atomically.
// Wrapped in a transaction so that if the completion delete fails, image
// deletions are rolled back as well.
func (r *TaskRepository) DeleteCompletion(id uint) error {
	return r.db.Transaction(func(tx *gorm.DB) error {
		// Delete images first
		if err := tx.Where("completion_id = ?", id).Delete(&models.TaskCompletionImage{}).Error; err != nil {
			log.Error().Err(err).Uint("completion_id", id).Msg("Failed to delete completion images")
			return err
		}
		return tx.Delete(&models.TaskCompletion{}, id).Error
	})
}

// CreateCompletionImage creates a new completion image
func (r *TaskRepository) CreateCompletionImage(image *models.TaskCompletionImage) error {
	return r.db.Create(image).Error
}

// DeleteCompletionImage deletes a completion image
func (r *TaskRepository) DeleteCompletionImage(id uint) error {
	return r.db.Delete(&models.TaskCompletionImage{}, id).Error
}

// FindCompletionImageByID finds a completion image by ID
func (r *TaskRepository) FindCompletionImageByID(id uint) (*models.TaskCompletionImage, error) {
	var image models.TaskCompletionImage
	err := r.db.First(&image, id).Error
	if err != nil {
		return nil, err
	}
	return &image, nil
}

// GetOverdueCountByResidence returns a map of residence ID to overdue task count.
// Uses the task.scopes package for consistent filtering logic.
// Excludes in-progress tasks to match what's displayed in the kanban overdue column.
// The `now` parameter should be the start of day in the user's timezone for accurate overdue detection.
func (r *TaskRepository) GetOverdueCountByResidence(residenceIDs []uint, now time.Time) (map[uint]int, error) {
	if len(residenceIDs) == 0 {
		return map[uint]int{}, nil
	}

	// Query to get overdue count grouped by residence
	type result struct {
		ResidenceID uint
		Count       int64
	}
	var results []result

	err := r.db.Model(&models.Task{}).
		Select("residence_id, COUNT(*) as count").
		Scopes(task.ScopeForResidences(residenceIDs), task.ScopeOverdue(now), task.ScopeNotInProgress).
		Group("residence_id").
		Scan(&results).Error
	if err != nil {
		return nil, err
	}

	// Convert to map
	countMap := make(map[uint]int)
	for _, r := range results {
		countMap[r.ResidenceID] = int(r.Count)
	}

	return countMap, nil
}

// kanbanColumnColors maps kanban column names to their hex colors.
var kanbanColumnColors = map[string]string{
	"overdue_tasks":     "#FF3B30",
	"in_progress_tasks": "#5856D6",
	"due_soon_tasks":    "#FF9500",
	"upcoming_tasks":    "#007AFF",
	"completed_tasks":   "#34C759",
	"cancelled_tasks":   "#8E8E93",
}

// KanbanColumnColor returns the hex color for a kanban column name.
func KanbanColumnColor(column string) string {
	if color, ok := kanbanColumnColors[column]; ok {
		return color
	}
	return "#34C759" // default to green
}

// completionAggRow is an internal type for scanning aggregated completion data.
type completionAggRow struct {
	ResidenceID         uint
	CompletedFromColumn string
	CompletedMonth      string
	Count               int64
}

// GetCompletionSummary returns completion summary data for a single residence.
// Returns total all-time count and monthly breakdowns (by column) for the last 12 months.
func (r *TaskRepository) GetCompletionSummary(residenceID uint, now time.Time, maxPerMonth int) (*responses.CompletionSummary, error) {
	// 1. Total all-time completions for this residence
	var totalAllTime int64
	err := r.db.Model(&models.TaskCompletion{}).
		Joins("JOIN task_task ON task_task.id = task_taskcompletion.task_id").
		Where("task_task.residence_id = ?", residenceID).
		Count(&totalAllTime).Error
	if err != nil {
		return nil, err
	}

	// 2. Monthly breakdown for last 12 months
	startDate := time.Date(now.Year()-1, now.Month(), 1, 0, 0, 0, 0, now.Location())

	// Use dialect-appropriate date formatting (PostgreSQL vs SQLite)
	dateExpr := "TO_CHAR(task_taskcompletion.completed_at, 'YYYY-MM')"
	if r.db.Dialector.Name() == "sqlite" {
		dateExpr = "strftime('%Y-%m', task_taskcompletion.completed_at)"
	}

	var rows []completionAggRow
	err = r.db.Model(&models.TaskCompletion{}).
		Select(fmt.Sprintf("task_task.residence_id, task_taskcompletion.completed_from_column, %s as completed_month, COUNT(*) as count", dateExpr)).
		Joins("JOIN task_task ON task_task.id = task_taskcompletion.task_id").
		Where("task_task.residence_id = ? AND task_taskcompletion.completed_at >= ?", residenceID, startDate).
		Group(fmt.Sprintf("task_task.residence_id, task_taskcompletion.completed_from_column, %s", dateExpr)).
		Order("completed_month ASC").
		Scan(&rows).Error
	if err != nil {
		return nil, err
	}

	// Build month map
	type monthData struct {
		columns map[string]int
		total   int
	}
	monthMap := make(map[string]*monthData)

	// Initialize all 12 months
	for i := 0; i < 12; i++ {
		m := startDate.AddDate(0, i, 0)
		key := m.Format("2006-01")
		monthMap[key] = &monthData{columns: make(map[string]int)}
	}

	// Populate from query results
	totalLast12 := 0
	for _, row := range rows {
		md, ok := monthMap[row.CompletedMonth]
		if !ok {
			continue
		}
		md.columns[row.CompletedFromColumn] = int(row.Count)
		md.total += int(row.Count)
		totalLast12 += int(row.Count)
	}

	// Convert to response DTOs
	months := make([]responses.MonthlyCompletionSummary, 0, 12)
	for i := 0; i < 12; i++ {
		m := startDate.AddDate(0, i, 0)
		key := m.Format("2006-01")
		md := monthMap[key]

		completions := make([]responses.ColumnCompletionCount, 0)
		for col, count := range md.columns {
			completions = append(completions, responses.ColumnCompletionCount{
				Column: col,
				Color:  KanbanColumnColor(col),
				Count:  count,
			})
		}

		overflow := 0
		if md.total > maxPerMonth {
			overflow = md.total - maxPerMonth
		}

		months = append(months, responses.MonthlyCompletionSummary{
			Month:       key,
			Completions: completions,
			Total:       md.total,
			Overflow:    overflow,
		})
	}

	return &responses.CompletionSummary{
		TotalAllTime:      int(totalAllTime),
		TotalLast12Months: totalLast12,
		Months:            months,
	}, nil
}

// GetBatchCompletionSummaries returns completion summaries for multiple residences
// in two queries total (one for all-time counts, one for monthly breakdowns),
// instead of 2*N queries when calling GetCompletionSummary per residence.
func (r *TaskRepository) GetBatchCompletionSummaries(residenceIDs []uint, now time.Time, maxPerMonth int) (map[uint]*responses.CompletionSummary, error) {
	result := make(map[uint]*responses.CompletionSummary, len(residenceIDs))
	if len(residenceIDs) == 0 {
		return result, nil
	}

	// 1. Total all-time completions per residence (single query)
	type allTimeRow struct {
		ResidenceID uint
		Count       int64
	}
	var allTimeRows []allTimeRow
	err := r.db.Model(&models.TaskCompletion{}).
		Select("task_task.residence_id, COUNT(*) as count").
		Joins("JOIN task_task ON task_task.id = task_taskcompletion.task_id").
		Where("task_task.residence_id IN ?", residenceIDs).
		Group("task_task.residence_id").
		Scan(&allTimeRows).Error
	if err != nil {
		return nil, err
	}

	allTimeMap := make(map[uint]int64, len(allTimeRows))
	for _, row := range allTimeRows {
		allTimeMap[row.ResidenceID] = row.Count
	}

	// 2. Monthly breakdown for last 12 months across all residences (single query)
	startDate := time.Date(now.Year()-1, now.Month(), 1, 0, 0, 0, 0, now.Location())

	dateExpr := "TO_CHAR(task_taskcompletion.completed_at, 'YYYY-MM')"
	if r.db.Dialector.Name() == "sqlite" {
		dateExpr = "strftime('%Y-%m', task_taskcompletion.completed_at)"
	}

	var rows []completionAggRow
	err = r.db.Model(&models.TaskCompletion{}).
		Select(fmt.Sprintf("task_task.residence_id, task_taskcompletion.completed_from_column, %s as completed_month, COUNT(*) as count", dateExpr)).
		Joins("JOIN task_task ON task_task.id = task_taskcompletion.task_id").
		Where("task_task.residence_id IN ? AND task_taskcompletion.completed_at >= ?", residenceIDs, startDate).
		Group(fmt.Sprintf("task_task.residence_id, task_taskcompletion.completed_from_column, %s", dateExpr)).
		Order("completed_month ASC").
		Scan(&rows).Error
	if err != nil {
		return nil, err
	}

	// 3. Build per-residence summaries
	type monthData struct {
		columns map[string]int
		total   int
	}

	// Initialize all residences with empty month maps
	residenceMonths := make(map[uint]map[string]*monthData, len(residenceIDs))
	for _, rid := range residenceIDs {
		mm := make(map[string]*monthData, 12)
		for i := 0; i < 12; i++ {
			m := startDate.AddDate(0, i, 0)
			key := m.Format("2006-01")
			mm[key] = &monthData{columns: make(map[string]int)}
		}
		residenceMonths[rid] = mm
	}

	// Populate from query results
	residenceLast12 := make(map[uint]int, len(residenceIDs))
	for _, row := range rows {
		mm, ok := residenceMonths[row.ResidenceID]
		if !ok {
			continue
		}
		md, ok := mm[row.CompletedMonth]
		if !ok {
			continue
		}
		md.columns[row.CompletedFromColumn] = int(row.Count)
		md.total += int(row.Count)
		residenceLast12[row.ResidenceID] += int(row.Count)
	}

	// Convert to response DTOs per residence
	for _, rid := range residenceIDs {
		mm := residenceMonths[rid]
		months := make([]responses.MonthlyCompletionSummary, 0, 12)
		for i := 0; i < 12; i++ {
			m := startDate.AddDate(0, i, 0)
			key := m.Format("2006-01")
			md := mm[key]

			completions := make([]responses.ColumnCompletionCount, 0)
			for col, count := range md.columns {
				completions = append(completions, responses.ColumnCompletionCount{
					Column: col,
					Color:  KanbanColumnColor(col),
					Count:  count,
				})
			}

			overflow := 0
			if md.total > maxPerMonth {
				overflow = md.total - maxPerMonth
			}

			months = append(months, responses.MonthlyCompletionSummary{
				Month:       key,
				Completions: completions,
				Total:       md.total,
				Overflow:    overflow,
			})
		}

		result[rid] = &responses.CompletionSummary{
			TotalAllTime:      int(allTimeMap[rid]),
			TotalLast12Months: residenceLast12[rid],
			Months:            months,
		}
	}

	return result, nil
}
