// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only

part of '../../backlog.dart';

/// Read-only backlog projection over the unified task list.
///
/// [BacklogView] is a query/helper object. It does not mutate tasks or own data;
/// it receives the current task list and exposes common backlog slices for UI.
/// That keeps backlog display logic out of widgets and avoids duplicating the
/// same filtering rules in multiple screens.
class BacklogView {
  /// Creates a `BacklogView` instance with the values required by its invariants.
  /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code.
  BacklogView({
    required List<Task> tasks,
    required this.now,
    this.staleAfter = const Duration(days: 31),
    this.stalenessSettings = const BacklogStalenessSettings(),
  }) : tasks = List<Task>.unmodifiable(tasks);

  /// Master task list supplied by the caller. Only `status == backlog` items are
  /// shown by this view.
  final List<Task> tasks;

  /// Clock value supplied by the caller so age/staleness behavior is testable.
  final DateTime now;

  /// Age since the task's Backlog freshness anchor that qualifies for the `stale` filter.
  ///
  /// Tasks persisted before the anchor existed fall back to [Task.createdAt].
  /// This keeps older data readable while letting Push to Backlog make a task
  /// fresh without rewriting original creation history.
  final Duration staleAfter;

  /// Color-bucket threshold configuration for backlog aging indicators.
  final BacklogStalenessSettings stalenessSettings;

  /// All tasks currently in backlog status.
  ///
  /// The returned list is a snapshot. It is not intended to be modified and then
  /// written back; state changes should go through scheduling/action services.
  List<Task> get backlogTasks {
    return tasks.where((task) => task.isBacklog).toList(growable: false);
  }

  /// Return backlog tasks matching a single user-facing filter.
  ///
  /// Filtering always starts from [backlogTasks], so a completed or planned task
  /// will never appear here even if it has matching statistics.
  List<Task> filter(BacklogFilter filter) {
    return backlogTasks.where((task) => _matchesFilter(task, filter)).toList(
          growable: false,
        );
  }

  /// Return all backlog tasks sorted by a user-facing ordering.
  ///
  /// A new list is created before sorting so the original [tasks] list is never
  /// reordered by a read operation. The final list is unmodifiable to make that
  /// intent explicit to callers.
  List<Task> sorted(BacklogSortKey sortKey) {
    final sortedTasks = backlogTasks
        .asMap()
        .entries
        .map(
          (entry) => _IndexedTask(
            task: entry.value,
            originalIndex: entry.key,
          ),
        )
        .toList(growable: false);
    sortedTasks.sort((a, b) {
      final comparison = _compareTasks(a.task, b.task, sortKey);
      if (comparison != 0) {
        return comparison;
      }

      return a.originalIndex.compareTo(b.originalIndex);
    });

    return List<Task>.unmodifiable(
      sortedTasks.map((indexedTask) => indexedTask.task),
    );
  }

  /// Return the green/blue/purple marker for one task.
  BacklogStalenessMarker stalenessMarkerFor(Task task) {
    return stalenessSettings.markerFor(task: task, now: now);
  }

  /// Private predicate implementing every [BacklogFilter] option.
  ///
  /// Keeping this as a switch expression makes new filters obvious: add the enum
  /// value and the compiler forces this method to handle it.
  bool _matchesFilter(Task task, BacklogFilter filter) {
    return switch (filter) {
      BacklogFilter.inbox => task.projectId == 'inbox',
      BacklogFilter.pushed =>
        task.stats.manuallyPushedCount > 0 || task.stats.autoPushedCount > 0,
      BacklogFilter.criticalMissed =>
        task.type == TaskType.critical && task.stats.missedCount > 0,
      BacklogFilter.wishlist => task.backlogTags.contains(BacklogTag.wishlist),
      BacklogFilter.stale =>
        now.difference(task.backlogFreshnessAnchorAt) >= staleAfter,
      BacklogFilter.noRewardSet => task.reward == RewardLevel.notSet,
    };
  }

  /// Comparison callback used by [sorted].
  ///
  /// Sort directions are encoded here. Higher priority/reward/push counts should
  /// appear earlier, while older age uses the earliest [Task.createdAt] first.
  int _compareTasks(Task a, Task b, BacklogSortKey sortKey) {
    return switch (sortKey) {
      BacklogSortKey.priority =>
        _priorityRank(b.priority).compareTo(_priorityRank(a.priority)),
      BacklogSortKey.rewardVsEffort =>
        _rewardVsEffortScore(b).compareTo(_rewardVsEffortScore(a)),
      BacklogSortKey.age =>
        a.backlogFreshnessAnchorAt.compareTo(b.backlogFreshnessAnchorAt),
      BacklogSortKey.project => a.projectId.compareTo(b.projectId),
      BacklogSortKey.timesPushed => _timesPushed(b).compareTo(_timesPushed(a)),
    };
  }
}

/// Convert nullable priority into a stable numeric rank for sorting.
///
/// Null priority is treated like medium so partially imported data behaves like
/// normal starter tasks instead of sinking to the bottom.
int _priorityRank(PriorityLevel? priority) {
  return switch (priority) {
    PriorityLevel.veryLow => 0,
    PriorityLevel.low => 1,
    PriorityLevel.medium || null => 2,
    PriorityLevel.high => 3,
    PriorityLevel.veryHigh => 4,
  };
}

/// Convert reward enum values to numeric ranks for derived scoring.
int _rewardRank(RewardLevel reward) {
  return switch (reward) {
    RewardLevel.notSet => 0,
    RewardLevel.veryLow => 1,
    RewardLevel.low => 2,
    RewardLevel.medium => 3,
    RewardLevel.high => 4,
    RewardLevel.veryHigh => 5,
  };
}

/// Convert difficulty enum values to numeric ranks for derived scoring.
int _difficultyRank(DifficultyLevel difficulty) {
  return switch (difficulty) {
    DifficultyLevel.notSet => 0,
    DifficultyLevel.veryEasy => 1,
    DifficultyLevel.easy => 2,
    DifficultyLevel.medium => 3,
    DifficultyLevel.hard => 4,
    DifficultyLevel.veryHard => 5,
  };
}

/// Simple motivation score: reward minus difficulty.
///
/// Positive scores suggest high payoff for lower activation cost. Negative scores
/// suggest high effort for lower payoff. This is deliberately simple for V1 and
/// can be replaced by richer heuristics later without changing the public sort
/// key.
int _rewardVsEffortScore(Task task) {
  return _rewardRank(task.reward) - _difficultyRank(task.difficulty);
}

/// Total manual and automatic pushes recorded on the task.
int _timesPushed(Task task) {
  return task.stats.manuallyPushedCount + task.stats.autoPushedCount;
}
