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

part of '../../models.dart';

/// Starter task model for the scheduling core.
///
/// [Task] is the main domain object passed through the scheduler. It is written
/// as an immutable value object: operations do not mutate an existing task, they
/// return a copied task with changed fields. That approach makes scheduling
/// actions easier to test because every function receives an input list and
/// returns a new output list.
///
/// Important modeling choices:
/// - [type] controls scheduling behavior: flexible, critical, locked, etc.
/// - [status] controls lifecycle state: planned, completed, backlog, etc.
/// - [scheduledStart] and [scheduledEnd] are optional because backlog items and
///   unscheduled captures do not have timeline placement yet.
/// - [stats] records quiet metadata for future reports. It should not clutter
///   the everyday UI unless a report or filter specifically needs it.
/// - [parentTaskId] links child tasks to a larger parent task without requiring
///   a nested object graph. That keeps persistence simple and avoids recursive
///   scheduling structures.
///
/// This is still a starter V1 model, so behavior changes should be explicit and
/// backed by tests as the product rules settle.
class Task {
  /// Creates a `Task` 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.
  Task({
    required String id,
    required String title,
    required String projectId,
    required this.type,
    required this.status,
    required this.createdAt,
    required this.updatedAt,
    this.priority,
    this.reward = RewardLevel.notSet,
    this.difficulty = DifficultyLevel.notSet,
    int? durationMinutes,
    this.scheduledStart,
    this.scheduledEnd,
    this.actualStart,
    this.actualEnd,
    this.completedAt,
    this.backlogEnteredAt,
    this.backlogEnteredAtProvenance,
    String? parentTaskId,
    Set<BacklogTag> backlogTags = const <BacklogTag>{},
    this.reminderOverride,
    this.stats = const TaskStatistics(),
  })  : id = _requiredTrimmed(
          id,
          'id',
          DomainValidationCode.blankStableId,
          'Stable id is required.',
        ),
        title = _requiredTrimmed(
          title,
          'title',
          DomainValidationCode.blankTitle,
          'Title is required.',
        ),
        projectId = _requiredTrimmed(
          projectId,
          'projectId',
          DomainValidationCode.blankProjectId,
          'Project id is required.',
        ),
        durationMinutes = durationMinutes,
        parentTaskId = _nullableTrimmed(
          parentTaskId,
          'parentTaskId',
          DomainValidationCode.blankParentTaskId,
          'Parent task id cannot be blank.',
        ),
        backlogTags = Set<BacklogTag>.unmodifiable(backlogTags) {
    _validatePositiveDuration(durationMinutes, 'durationMinutes');
    _validateOptionalInterval(
      start: scheduledStart,
      end: scheduledEnd,
      partialCode: DomainValidationCode.partialScheduledInterval,
      invalidCode: DomainValidationCode.invalidScheduledInterval,
      name: 'scheduledStart/scheduledEnd',
      partialMessage:
          'Scheduled start and scheduled end must both be present or absent.',
      invalidMessage: 'Scheduled end must be after scheduled start.',
    );
    _validateOptionalInterval(
      start: actualStart,
      end: actualEnd,
      partialCode: DomainValidationCode.partialActualInterval,
      invalidCode: DomainValidationCode.invalidActualInterval,
      name: 'actualStart/actualEnd',
      partialMessage:
          'Actual start and actual end must both be present or absent.',
      invalidMessage: 'Actual end must be after actual start.',
    );
    if (this.parentTaskId == this.id) {
      throw DomainValidationException(
        code: DomainValidationCode.selfParent,
        invalidValue: parentTaskId,
        name: 'parentTaskId',
        message: 'A task cannot be its own parent.',
      );
    }
  }

  /// Create a minimal captured task without requiring planning details.
  ///
  /// Quick capture is intentionally forgiving: the user can enter only a title
  /// and the system can still create a valid backlog item. Defaults route the
  /// item into the inbox project as a medium-priority flexible backlog task.
  ///
  /// The only hard validation here is that [title] must contain non-whitespace
  /// text. Scheduling validation, such as requiring a positive duration, happens
  /// in `quick_capture.dart` because that depends on the requested capture flow.
  factory Task.quickCapture({
    required String id,
    required String title,
    required DateTime createdAt,
    String projectId = 'inbox',
    TaskType type = TaskType.flexible,
    TaskStatus status = TaskStatus.backlog,
    PriorityLevel priority = PriorityLevel.medium,
    RewardLevel reward = RewardLevel.notSet,
    DifficultyLevel difficulty = DifficultyLevel.notSet,
    Set<BacklogTag> backlogTags = const <BacklogTag>{},
    DateTime? updatedAt,
  }) {
    final entersBacklog = status == TaskStatus.backlog;
    return Task(
      id: id,
      title: title.trim(),
      projectId: projectId,
      type: type,
      status: status,
      priority: priority,
      reward: reward,
      difficulty: difficulty,
      createdAt: createdAt,
      updatedAt: updatedAt ?? createdAt,
      backlogEnteredAt: entersBacklog ? createdAt : null,
      backlogEnteredAtProvenance:
          entersBacklog ? backlogEnteredAtProvenanceQuickCapture : null,
      backlogTags: backlogTags,
    );
  }

  /// Provenance value for tasks first created directly in Backlog.
  static const backlogEnteredAtProvenanceQuickCapture = 'quickCapture';

  /// Provenance value for scheduled tasks moved or pushed back to Backlog.
  static const backlogEnteredAtProvenanceMovedToBacklog = 'movedToBacklog';

  /// Stable identifier used by persistence, UI selection, and scheduler changes.
  final String id;

  /// User-facing task title. The model expects this to already be trimmed.
  final String title;

  /// Owning project/profile id. `inbox` is used for uncategorized captures.
  final String projectId;

  /// Scheduling behavior category. See [TaskType] for rule-level meaning.
  final TaskType type;

  /// Current lifecycle state. See [TaskStatus] for state-level meaning.
  final TaskStatus status;

  /// Optional importance. Most creation paths default this to medium, but it is
  /// nullable to leave room for imports or legacy data that have not set it yet.
  final PriorityLevel? priority;

  /// Motivational payoff used by backlog sorting and future planning hints.
  final RewardLevel reward;

  /// Activation/effort estimate used by backlog sorting and future planning hints.
  final DifficultyLevel difficulty;

  /// Estimated task length. Required for most scheduling operations, optional for
  /// backlog capture because not every captured thought has an estimate yet.
  final int? durationMinutes;

  /// Inclusive scheduled start time. Null means the task is not currently placed.
  final DateTime? scheduledStart;

  /// Exclusive scheduled end time. Null means the task is not currently placed.
  final DateTime? scheduledEnd;

  /// Actual work start time, when known after the fact.
  final DateTime? actualStart;

  /// Actual work end time, when known after the fact.
  final DateTime? actualEnd;

  /// Explicit completion timestamp, distinct from the generic update timestamp.
  final DateTime? completedAt;

  /// Freshness anchor for Backlog aging. Null falls back to [createdAt].
  final DateTime? backlogEnteredAt;

  /// Optional explanation for how [backlogEnteredAt] was set.
  final String? backlogEnteredAtProvenance;

  /// Parent task id when this task is a child/subtask. Null means top-level task.
  final String? parentTaskId;

  /// Backlog-specific flags, such as wishlist/someday behavior.
  final Set<BacklogTag> backlogTags;

  /// Optional task-level reminder override.
  final ReminderProfile? reminderOverride;

  /// Creation timestamp used for age/staleness sorting.
  final DateTime createdAt;

  /// Last domain-level update timestamp. Scheduling actions set this when moving
  /// or changing tasks so persistence and reports can detect recent activity.
  final DateTime updatedAt;

  /// Quiet counters for reporting and later heuristics.
  final TaskStatistics stats;

  /// Convenience predicate for the task type most scheduler movement operates on.
  bool get isFlexible => type == TaskType.flexible;

  /// Critical and inflexible tasks are both visible to the user and treated as
  /// blocked time by flexible scheduling.
  bool get isRequiredVisible =>
      type == TaskType.critical || type == TaskType.inflexible;

  /// Locked tasks behave as timeline constraints, not normal interactive cards.
  bool get isLocked => type == TaskType.locked;

  /// Backlog status means the task is stored for later and has no active slot.
  bool get isBacklog => status == TaskStatus.backlog;

  /// Timestamp used for Backlog freshness and age filters.
  DateTime get backlogFreshnessAnchorAt => backlogEnteredAt ?? createdAt;

  /// Return a copy with selected fields changed.
  ///
  /// The core uses this instead of mutation. That matters because scheduling
  /// operations often need to produce an auditable before/after result, including
  /// [SchedulingChange]-style records elsewhere.
  ///
  /// [clearSchedule] is a deliberate escape hatch for nullable schedule fields.
  /// Without it, passing null would be ambiguous: it could mean "do not change"
  /// or "clear this value." When [clearSchedule] is true, both schedule fields
  /// are removed even if [scheduledStart] or [scheduledEnd] are omitted.
  ///
  /// The other `clear*` flags provide the same explicit semantics for nullable
  /// fields that the product can remove.
  Task copyWith({
    String? id,
    String? title,
    String? projectId,
    TaskType? type,
    TaskStatus? status,
    PriorityLevel? priority,
    RewardLevel? reward,
    DifficultyLevel? difficulty,
    int? durationMinutes,
    DateTime? scheduledStart,
    DateTime? scheduledEnd,
    DateTime? actualStart,
    DateTime? actualEnd,
    DateTime? completedAt,
    DateTime? backlogEnteredAt,
    String? backlogEnteredAtProvenance,
    String? parentTaskId,
    Set<BacklogTag>? backlogTags,
    ReminderProfile? reminderOverride,
    DateTime? createdAt,
    DateTime? updatedAt,
    TaskStatistics? stats,
    bool clearPriority = false,
    bool clearDuration = false,
    bool clearSchedule = false,
    bool clearActualInterval = false,
    bool clearCompletion = false,
    bool clearBacklogEntry = false,
    bool clearParentTask = false,
    bool clearReminderOverride = false,
  }) {
    return Task(
      id: id ?? this.id,
      title: title ?? this.title,
      projectId: projectId ?? this.projectId,
      type: type ?? this.type,
      status: status ?? this.status,
      priority: clearPriority ? null : (priority ?? this.priority),
      reward: reward ?? this.reward,
      difficulty: difficulty ?? this.difficulty,
      durationMinutes:
          clearDuration ? null : (durationMinutes ?? this.durationMinutes),
      scheduledStart:
          clearSchedule ? null : (scheduledStart ?? this.scheduledStart),
      scheduledEnd: clearSchedule ? null : (scheduledEnd ?? this.scheduledEnd),
      actualStart:
          clearActualInterval ? null : (actualStart ?? this.actualStart),
      actualEnd: clearActualInterval ? null : (actualEnd ?? this.actualEnd),
      completedAt: clearCompletion ? null : (completedAt ?? this.completedAt),
      backlogEnteredAt: clearBacklogEntry
          ? null
          : (backlogEnteredAt ?? this.backlogEnteredAt),
      backlogEnteredAtProvenance: clearBacklogEntry
          ? null
          : (backlogEnteredAtProvenance ?? this.backlogEnteredAtProvenance),
      parentTaskId:
          clearParentTask ? null : (parentTaskId ?? this.parentTaskId),
      backlogTags: backlogTags ?? this.backlogTags,
      reminderOverride: clearReminderOverride
          ? null
          : (reminderOverride ?? this.reminderOverride),
      createdAt: createdAt ?? this.createdAt,
      updatedAt: updatedAt ?? this.updatedAt,
      stats: stats ?? this.stats,
    );
  }
}
