// Core domain model for the ADHD scheduling starter project.
//
// This file intentionally contains small immutable value objects and enums. It
// should stay free of UI, persistence, notification, and platform code. Keeping
// this layer plain makes the scheduler easy to test and easy to move between a
// command-line prototype, Flutter UI, or future backend service.
//
// Reading order for humans:
//   1. `TaskType` and `TaskStatus` explain the two main axes of a task.
//   2. `Task` shows the actual data carried through the scheduling engine.
//   3. `ProjectProfile` explains how project defaults create tasks.
//   4. `TimeInterval` is the shared time-span helper used by scheduling logic.

import 'task_statistics.dart';

/// Stable validation failure categories for domain model boundaries.
///
/// Callers should branch on these codes instead of parsing exception text. The
/// explanatory messages may change, but these enum values are part of the V1
/// backend contract.
enum DomainValidationCode {
  blankStableId,
  blankTitle,
  blankProjectId,
  blankName,
  blankColorKey,
  blankParentTaskId,
  selfParent,
  nonPositiveDuration,
  partialScheduledInterval,
  invalidScheduledInterval,
  partialActualInterval,
  invalidActualInterval,
  invalidTimeInterval,
  invalidSchedulingWindow,
  invalidCivilDate,
  invalidClockTime,
  blankTimeZoneId,
  nonexistentLocalTime,
  ambiguousLocalTime,
  emptyRecurrence,
  invalidLockedBlock,
  invalidLockedOverride,
}

/// Typed validation error thrown by domain model constructors.
class DomainValidationException extends ArgumentError {
  DomainValidationException({
    required this.code,
    required Object? invalidValue,
    required String name,
    required String message,
  }) : super.value(invalidValue, name, message);

  /// Stable category for application and persistence layers.
  final DomainValidationCode code;
}

/// Scheduling behavior category.
///
/// This enum is one of the central concepts in the planner. The type answers
/// the question: "how should the scheduler treat this item?" It is separate
/// from [TaskStatus], which answers "where is this item in its lifecycle?"
///
/// For example, a flexible task can be planned, completed, missed, or moved to
/// backlog. A locked item, by contrast, acts more like a calendar constraint
/// than a normal task card. Keeping behavior and lifecycle separate makes later
/// UI and persistence logic easier to reason about.
enum TaskType {
  /// Movable planned work.
  flexible,

  /// Required visible block that should not be moved automatically.
  inflexible,

  /// Required visible task that remains actionable if missed.
  critical,

  /// Hidden scheduling constraint, not a task card.
  locked,

  /// Unplanned completed/logged task.
  surprise,

  /// Intentional rest time.
  freeSlot,
}

/// Current lifecycle state of a task.
///
/// Status is intentionally data-oriented: it describes what happened to a task,
/// not how important it is or how the scheduler should move it. Scheduler rules
/// combine [TaskStatus] with [TaskType]. For instance, a planned flexible task
/// can be pushed, while a planned critical task should remain visible and become
/// backlog if missed.
enum TaskStatus {
  /// Scheduled or queued work that has not started.
  planned,

  /// Work currently in progress.
  active,

  /// Work finished by the user.
  completed,

  /// Work that was not completed in its intended time.
  missed,

  /// Work intentionally removed from the plan.
  cancelled,

  /// Work intentionally dismissed because it no longer applies.
  noLongerRelevant,

  /// Unscheduled work kept for later planning.
  backlog,
}

/// User-facing importance level.
///
/// Priority is a relative ordering hint. It should help decide what rises to the
/// top of a queue, but it should not be treated as an absolute promise that the
/// task must happen at a specific time. The scheduling engine currently ranks
/// this with simple numeric helpers in `backlog.dart`; more advanced heuristics
/// can build on the same enum later.
enum PriorityLevel {
  /// Lowest priority.
  veryLow,

  /// Low priority.
  low,

  /// Default middle priority.
  medium,

  /// High priority.
  high,

  /// Highest priority.
  veryHigh,
}

/// Expected reward or payoff from completing a task.
///
/// Reward is meant to capture motivational payoff, not objective value. In this
/// app design it supports ADHD-friendly planning: a small, easy, high-reward task
/// may be a better momentum starter than a large low-reward task.
enum RewardLevel {
  /// No reward level has been captured; this is not equivalent to low reward.
  notSet,

  /// Very low expected reward.
  veryLow,

  /// Low expected reward.
  low,

  /// Medium expected reward.
  medium,

  /// High expected reward.
  high,

  /// Very high expected reward.
  veryHigh,
}

/// Expected effort or activation difficulty for a task.
///
/// Difficulty is not the same as duration. A five-minute phone call might be
/// very hard to start, while an hour of familiar maintenance may be easy. The
/// backlog view uses this with [RewardLevel] to expose a simple
/// reward-versus-effort sort.
enum DifficultyLevel {
  /// No difficulty has been captured yet.
  notSet,

  /// Very easy to start or complete.
  veryEasy,

  /// Easy to start or complete.
  easy,

  /// Medium effort.
  medium,

  /// Hard to start or complete.
  hard,

  /// Very hard to start or complete.
  veryHard,
}

/// Reminder intensity preference.
///
/// This is currently stored as project metadata rather than enforced by the core
/// scheduler. Future notification/UI layers can use it to decide how aggressive
/// reminders should be without adding reminder-specific logic to [Task].
enum ReminderProfile {
  /// No reminder nudges.
  silent,

  /// Low-friction reminder nudges.
  gentle,

  /// Repeated reminder nudges.
  persistent,

  /// Strong reminder behavior for required items.
  strict,
}

/// Lightweight backlog-only metadata.
///
/// Tags here are deliberately narrow. They are not meant to replace a general
/// tagging system. They identify special backlog behavior that the planner needs
/// to understand, such as "wishlist/someday" items.
enum BacklogTag {
  /// Task is intentionally saved as a someday/wishlist item.
  wishlist,
}

/// 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 {
  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,
    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,
  }) {
    return Task(
      id: id,
      title: title.trim(),
      projectId: projectId,
      type: type,
      status: status,
      priority: priority,
      reward: reward,
      difficulty: difficulty,
      createdAt: createdAt,
      updatedAt: updatedAt ?? createdAt,
      backlogTags: backlogTags,
    );
  }

  /// 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;

  /// 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;

  /// 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,
    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 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),
      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,
    );
  }
}

/// Starter project defaults used when creating or scheduling tasks.
///
/// A project profile represents reusable defaults for a group of tasks. UI code
/// can let the user pick a project, then call [createTask] so new tasks inherit
/// a color, default priority, reward, difficulty, reminder profile, and duration.
///
/// The scheduler itself mostly cares about the resulting [Task] fields. Keeping
/// project defaults separate prevents every scheduling function from needing to
/// know project configuration details.
class ProjectProfile {
  ProjectProfile({
    required String id,
    required String name,
    required String colorKey,
    this.defaultPriority = PriorityLevel.medium,
    this.defaultReward = RewardLevel.notSet,
    this.defaultDifficulty = DifficultyLevel.notSet,
    this.defaultReminderProfile = ReminderProfile.gentle,
    int? defaultDurationMinutes,
    this.archivedAt,
  })  : id = _requiredTrimmed(
          id,
          'id',
          DomainValidationCode.blankStableId,
          'Stable id is required.',
        ),
        name = _requiredTrimmed(
          name,
          'name',
          DomainValidationCode.blankName,
          'Project name is required.',
        ),
        colorKey = _requiredTrimmed(
          colorKey,
          'colorKey',
          DomainValidationCode.blankColorKey,
          'Project color key is required.',
        ),
        defaultDurationMinutes = defaultDurationMinutes {
    _validatePositiveDuration(
      defaultDurationMinutes,
      'defaultDurationMinutes',
    );
  }

  /// Stable project id stored on tasks.
  final String id;

  /// User-facing project name.
  final String name;

  /// Theme/color token for UI rendering. This is a key, not a raw color value.
  final String colorKey;

  /// Default importance assigned when a task does not override priority.
  final PriorityLevel defaultPriority;

  /// Default motivational payoff assigned when a task does not override reward.
  final RewardLevel defaultReward;

  /// Default activation difficulty assigned when a task does not override effort.
  final DifficultyLevel defaultDifficulty;

  /// Default reminder behavior for future notification/UI layers.
  final ReminderProfile defaultReminderProfile;

  /// Optional duration estimate used for newly created tasks.
  final int? defaultDurationMinutes;

  /// When present, this project is hidden from normal pickers but tasks keep
  /// their project ids for history and filtering.
  final DateTime? archivedAt;

  /// Whether the project has been archived.
  bool get isArchived => archivedAt != null;

  /// Create a task using project defaults while allowing explicit overrides.
  ///
  /// This keeps capture and project-default behavior in one place. Callers can
  /// pass only the fields the user explicitly set; everything else falls back to
  /// this profile. The created task receives this profile's [id] as [Task.projectId].
  Task createTask({
    required String id,
    required String title,
    required DateTime createdAt,
    TaskType type = TaskType.flexible,
    TaskStatus status = TaskStatus.backlog,
    PriorityLevel? priority,
    RewardLevel? reward,
    DifficultyLevel? difficulty,
    int? durationMinutes,
    DateTime? scheduledStart,
    DateTime? scheduledEnd,
    DateTime? actualStart,
    DateTime? actualEnd,
    DateTime? completedAt,
    String? parentTaskId,
    Set<BacklogTag> backlogTags = const <BacklogTag>{},
    ReminderProfile? reminderOverride,
    DateTime? updatedAt,
  }) {
    return Task(
      id: id,
      title: title,
      projectId: this.id,
      type: type,
      status: status,
      priority: priority ?? defaultPriority,
      reward: reward ?? defaultReward,
      difficulty: difficulty ?? defaultDifficulty,
      durationMinutes: durationMinutes ?? defaultDurationMinutes,
      scheduledStart: scheduledStart,
      scheduledEnd: scheduledEnd,
      actualStart: actualStart,
      actualEnd: actualEnd,
      completedAt: completedAt,
      parentTaskId: parentTaskId,
      backlogTags: backlogTags,
      reminderOverride: reminderOverride,
      createdAt: createdAt,
      updatedAt: updatedAt ?? createdAt,
    );
  }

  /// Return a copy with selected project defaults changed.
  ProjectProfile copyWith({
    String? id,
    String? name,
    String? colorKey,
    PriorityLevel? defaultPriority,
    RewardLevel? defaultReward,
    DifficultyLevel? defaultDifficulty,
    ReminderProfile? defaultReminderProfile,
    int? defaultDurationMinutes,
    bool clearDefaultDuration = false,
    DateTime? archivedAt,
    bool clearArchivedAt = false,
  }) {
    return ProjectProfile(
      id: id ?? this.id,
      name: name ?? this.name,
      colorKey: colorKey ?? this.colorKey,
      defaultPriority: defaultPriority ?? this.defaultPriority,
      defaultReward: defaultReward ?? this.defaultReward,
      defaultDifficulty: defaultDifficulty ?? this.defaultDifficulty,
      defaultReminderProfile:
          defaultReminderProfile ?? this.defaultReminderProfile,
      defaultDurationMinutes: clearDefaultDuration
          ? null
          : (defaultDurationMinutes ?? this.defaultDurationMinutes),
      archivedAt: clearArchivedAt ? null : (archivedAt ?? this.archivedAt),
    );
  }
}

/// Starter time range value used by scheduling helpers.
///
/// [TimeInterval] is the scheduler's neutral representation of a time span. It
/// is used for scheduled task slots, locked blocks, required visible blocks, and
/// candidate placements. The interval convention is start-inclusive and
/// end-exclusive, which avoids treating two back-to-back blocks as overlapping.
class TimeInterval {
  TimeInterval({
    required this.start,
    required this.end,
    this.label,
  }) {
    if (!start.isBefore(end)) {
      throw DomainValidationException(
        code: DomainValidationCode.invalidTimeInterval,
        invalidValue: {'start': start, 'end': end},
        name: 'TimeInterval',
        message: 'Interval end must be after interval start.',
      );
    }
  }

  /// Inclusive beginning of the interval.
  final DateTime start;

  /// Exclusive ending of the interval.
  final DateTime end;

  /// Optional debug/UI label, often a task id or locked block name.
  final String? label;

  /// Raw duration between [start] and [end]. Callers are responsible for only
  /// constructing meaningful positive intervals when required by a rule.
  Duration get duration => end.difference(start);

  /// Whether this interval shares any actual time with [other].
  ///
  /// Adjacent intervals do not overlap: `9:00-10:00` and `10:00-11:00` are safe
  /// to place back-to-back because the first interval's end is the second
  /// interval's start.
  bool overlaps(TimeInterval other) {
    return start.isBefore(other.end) && end.isAfter(other.start);
  }
}

String _requiredTrimmed(
  String value,
  String name,
  DomainValidationCode code,
  String message,
) {
  final trimmed = value.trim();
  if (trimmed.isEmpty) {
    throw DomainValidationException(
      code: code,
      invalidValue: value,
      name: name,
      message: message,
    );
  }
  return trimmed;
}

String? _nullableTrimmed(
  String? value,
  String name,
  DomainValidationCode code,
  String message,
) {
  if (value == null) {
    return null;
  }
  final trimmed = value.trim();
  if (trimmed.isEmpty) {
    throw DomainValidationException(
      code: code,
      invalidValue: value,
      name: name,
      message: message,
    );
  }
  return trimmed;
}

void _validatePositiveDuration(int? durationMinutes, String name) {
  if (durationMinutes == null) {
    return;
  }
  if (durationMinutes <= 0) {
    throw DomainValidationException(
      code: DomainValidationCode.nonPositiveDuration,
      invalidValue: durationMinutes,
      name: name,
      message: 'Duration must be positive when present.',
    );
  }
}

void _validateOptionalInterval({
  required DateTime? start,
  required DateTime? end,
  required DomainValidationCode partialCode,
  required DomainValidationCode invalidCode,
  required String name,
  required String partialMessage,
  required String invalidMessage,
}) {
  if ((start == null) != (end == null)) {
    throw DomainValidationException(
      code: partialCode,
      invalidValue: {'start': start, 'end': end},
      name: name,
      message: partialMessage,
    );
  }
  if (start != null && end != null && !start.isBefore(end)) {
    throw DomainValidationException(
      code: invalidCode,
      invalidValue: {'start': start, 'end': end},
      name: name,
      message: invalidMessage,
    );
  }
}
