// Canonical task lifecycle transitions and internal activity records.
//
// This module keeps task state changes separate from UI buttons and scheduling
// placement. Application use cases can persist the returned task and activities
// atomically in later blocks.

import 'models.dart';
import 'task_statistics.dart';
import 'time_contracts.dart';

/// Canonical task transition commands.
enum TaskTransitionCode {
  complete,
  miss,
  cancel,
  noLongerRelevant,
  manualPush,
  automaticPush,
  moveToBacklog,
  restoreFromBacklog,
  activate,
}

/// Stable internal activity categories derived from transitions.
enum TaskActivityCode {
  completed,
  missed,
  cancelled,
  noLongerRelevant,
  manuallyPushed,
  automaticallyPushed,
  movedToBacklog,
  restoredFromBacklog,
  activated,
}

/// Typed result categories for expected transition states.
enum TaskTransitionOutcomeCode {
  applied,
  noOp,
  duplicateOperation,
  invalidState,
}

/// Immutable internal activity fact.
///
/// Activities are backend/application data, not a visible per-task history UI.
/// They carry enough stable structure for idempotency, statistics, and future
/// persistence adapters.
class TaskActivity {
  TaskActivity({
    required String id,
    required String operationId,
    required this.code,
    required String taskId,
    required String projectId,
    required this.occurredAt,
    Map<String, Object?> metadata = const <String, Object?>{},
  })  : id = _requiredTrimmed(id, 'id'),
        operationId = _requiredTrimmed(operationId, 'operationId'),
        taskId = _requiredTrimmed(taskId, 'taskId'),
        projectId = _requiredTrimmed(projectId, 'projectId'),
        metadata = Map<String, Object?>.unmodifiable(metadata);

  /// Stable activity id supplied by the application boundary.
  final String id;

  /// Idempotency key for the user/application operation that created this fact.
  final String operationId;

  /// Stable activity category.
  final TaskActivityCode code;

  /// Task this activity belongs to.
  final String taskId;

  /// Project the task belonged to when the activity occurred.
  final String projectId;

  /// When the activity happened.
  final DateTime occurredAt;

  /// Small structured payload for later statistics and application use cases.
  final Map<String, Object?> metadata;
}

/// Result of applying one canonical transition.
class TaskTransitionResult {
  TaskTransitionResult({
    required this.transitionCode,
    required this.outcomeCode,
    required this.task,
    List<TaskActivity> activities = const <TaskActivity>[],
    this.duplicateOfActivityId,
  }) : activities = List<TaskActivity>.unmodifiable(activities);

  /// Requested transition.
  final TaskTransitionCode transitionCode;

  /// Typed outcome for callers.
  final TaskTransitionOutcomeCode outcomeCode;

  /// Original or updated task.
  final Task task;

  /// New internal activity records. Empty for no-op, invalid, and duplicates.
  final List<TaskActivity> activities;

  /// Existing activity id matched by a duplicate operation, when available.
  final String? duplicateOfActivityId;

  /// Convenience flag for mutation persistence.
  bool get applied => outcomeCode == TaskTransitionOutcomeCode.applied;
}

/// Result of applying activity-derived task statistics.
class TaskActivityApplicationResult {
  TaskActivityApplicationResult({
    required this.task,
    List<String> appliedActivityIds = const <String>[],
  }) : appliedActivityIds = List<String>.unmodifiable(appliedActivityIds);

  /// Task after all newly applied activity effects.
  final Task task;

  /// Activity ids that changed statistics in this application pass.
  final List<String> appliedActivityIds;
}

/// Applies task-statistic effects from internal activities exactly once.
class TaskActivityAccountingService {
  const TaskActivityAccountingService();

  /// Apply [activities] to [task], skipping ids already present in
  /// [alreadyAppliedActivityIds].
  ///
  /// Completion accounting uses explicit completion timestamps and actual
  /// intervals. If a completion has a timestamp but no actual interval, locked
  /// minutes are left at zero rather than inferred from the planned slot.
  TaskActivityApplicationResult apply({
    required Task task,
    required Iterable<TaskActivity> activities,
    Iterable<String> alreadyAppliedActivityIds = const <String>[],
    List<TimeInterval> lockedIntervals = const <TimeInterval>[],
  }) {
    final appliedIds = alreadyAppliedActivityIds.toSet();
    final newlyAppliedIds = <String>[];
    var current = task;

    for (final activity in activities) {
      if (activity.taskId != task.id || appliedIds.contains(activity.id)) {
        continue;
      }

      final nextStats = _applyActivityStats(
        task: current,
        activity: activity,
        lockedIntervals: lockedIntervals,
      );
      current = current.copyWith(stats: nextStats);
      appliedIds.add(activity.id);
      newlyAppliedIds.add(activity.id);
    }

    return TaskActivityApplicationResult(
      task: current,
      appliedActivityIds: newlyAppliedIds,
    );
  }
}

/// Canonical lifecycle transition service.
class TaskTransitionService {
  const TaskTransitionService({
    this.clock = const SystemClock(),
    this.accountingService = const TaskActivityAccountingService(),
  });

  /// Clock boundary used when no explicit occurrence time is supplied.
  final Clock clock;

  /// Activity-derived statistics updater.
  final TaskActivityAccountingService accountingService;

  /// Apply one transition to [task].
  ///
  /// [operationId] is the idempotency key. If an existing activity for the same
  /// operation and task is supplied, the command is treated as an exact duplicate
  /// and no additional changes are returned.
  TaskTransitionResult apply({
    required Task task,
    required TaskTransitionCode transitionCode,
    required String operationId,
    required String activityId,
    DateTime? occurredAt,
    DateTime? actualStart,
    DateTime? actualEnd,
    Iterable<TaskActivity> existingActivities = const <TaskActivity>[],
    Iterable<String> appliedActivityIds = const <String>[],
    List<TimeInterval> lockedIntervals = const <TimeInterval>[],
    Map<String, Object?> metadata = const <String, Object?>{},
  }) {
    final now = occurredAt ?? clock.now();
    final duplicate = _duplicateActivity(
      existingActivities: existingActivities,
      taskId: task.id,
      operationId: operationId,
    );
    if (duplicate != null) {
      return TaskTransitionResult(
        transitionCode: transitionCode,
        outcomeCode: TaskTransitionOutcomeCode.duplicateOperation,
        task: task,
        duplicateOfActivityId: duplicate.id,
      );
    }

    if (!_actualIntervalShapeIsValid(actualStart, actualEnd)) {
      return TaskTransitionResult(
        transitionCode: transitionCode,
        outcomeCode: TaskTransitionOutcomeCode.invalidState,
        task: task,
      );
    }

    if (_isNoOpTerminalRepeat(task, transitionCode)) {
      return TaskTransitionResult(
        transitionCode: transitionCode,
        outcomeCode: TaskTransitionOutcomeCode.noOp,
        task: task,
      );
    }

    if (_isBlockedByTerminalState(task, transitionCode)) {
      return TaskTransitionResult(
        transitionCode: transitionCode,
        outcomeCode: TaskTransitionOutcomeCode.invalidState,
        task: task,
      );
    }

    return switch (transitionCode) {
      TaskTransitionCode.complete => _complete(
          task: task,
          operationId: operationId,
          activityId: activityId,
          occurredAt: now,
          actualStart: actualStart,
          actualEnd: actualEnd,
          appliedActivityIds: appliedActivityIds,
          lockedIntervals: lockedIntervals,
          metadata: metadata,
        ),
      TaskTransitionCode.miss => _miss(
          task: task,
          operationId: operationId,
          activityId: activityId,
          occurredAt: now,
          appliedActivityIds: appliedActivityIds,
          metadata: metadata,
        ),
      TaskTransitionCode.cancel => _cancel(
          task: task,
          operationId: operationId,
          activityId: activityId,
          occurredAt: now,
          appliedActivityIds: appliedActivityIds,
          metadata: metadata,
        ),
      TaskTransitionCode.noLongerRelevant => _noLongerRelevant(
          task: task,
          operationId: operationId,
          activityId: activityId,
          occurredAt: now,
          appliedActivityIds: appliedActivityIds,
          metadata: metadata,
        ),
      TaskTransitionCode.manualPush => _movementOnly(
          task: task,
          transitionCode: transitionCode,
          activityCode: TaskActivityCode.manuallyPushed,
          operationId: operationId,
          activityId: activityId,
          occurredAt: now,
          metadata: metadata,
          appliedActivityIds: appliedActivityIds,
        ),
      TaskTransitionCode.automaticPush => _movementOnly(
          task: task,
          transitionCode: transitionCode,
          activityCode: TaskActivityCode.automaticallyPushed,
          operationId: operationId,
          activityId: activityId,
          occurredAt: now,
          metadata: metadata,
          appliedActivityIds: appliedActivityIds,
        ),
      TaskTransitionCode.moveToBacklog => _moveToBacklog(
          task: task,
          operationId: operationId,
          activityId: activityId,
          occurredAt: now,
          appliedActivityIds: appliedActivityIds,
          metadata: metadata,
        ),
      TaskTransitionCode.restoreFromBacklog => _restoreFromBacklog(
          task: task,
          operationId: operationId,
          activityId: activityId,
          occurredAt: now,
          appliedActivityIds: appliedActivityIds,
          metadata: metadata,
        ),
      TaskTransitionCode.activate => _activate(
          task: task,
          operationId: operationId,
          activityId: activityId,
          occurredAt: now,
          appliedActivityIds: appliedActivityIds,
          metadata: metadata,
        ),
    };
  }

  TaskTransitionResult _complete({
    required Task task,
    required String operationId,
    required String activityId,
    required DateTime occurredAt,
    required DateTime? actualStart,
    required DateTime? actualEnd,
    required Iterable<String> appliedActivityIds,
    required List<TimeInterval> lockedIntervals,
    required Map<String, Object?> metadata,
  }) {
    if (task.status != TaskStatus.planned && task.status != TaskStatus.active) {
      return _invalid(task, TaskTransitionCode.complete);
    }

    final completed = task.copyWith(
      status: TaskStatus.completed,
      updatedAt: occurredAt,
      completedAt: occurredAt,
      actualStart: actualStart,
      actualEnd: actualEnd,
    );
    final activities = [
      _activity(
        id: activityId,
        operationId: operationId,
        code: TaskActivityCode.completed,
        task: task,
        occurredAt: occurredAt,
        metadata: {
          ...metadata,
          'completedAt': occurredAt,
          'scheduledEnd': task.scheduledEnd,
          'actualStart': completed.actualStart,
          'actualEnd': completed.actualEnd,
          'durationMinutes': _knownDurationMinutes(
            completed,
            completed.actualStart,
            completed.actualEnd,
          ),
          'reward': task.reward.name,
          'difficulty': task.difficulty.name,
          if (task.reminderOverride != null)
            'reminderProfile': task.reminderOverride!.name,
          'pushesBeforeCompletion': _pushesBeforeCompletion(task),
        },
        nextStatus: completed.status,
      ),
    ];
    final accounted = accountingService.apply(
      task: completed,
      activities: activities,
      alreadyAppliedActivityIds: appliedActivityIds,
      lockedIntervals: lockedIntervals,
    );

    return _applied(
      transitionCode: TaskTransitionCode.complete,
      task: accounted.task,
      activities: activities,
    );
  }

  TaskTransitionResult _miss({
    required Task task,
    required String operationId,
    required String activityId,
    required DateTime occurredAt,
    required Iterable<String> appliedActivityIds,
    required Map<String, Object?> metadata,
  }) {
    if (task.status != TaskStatus.planned && task.status != TaskStatus.active) {
      return _invalid(task, TaskTransitionCode.miss);
    }
    if (!task.isFlexible && !task.isRequiredVisible) {
      return _invalid(task, TaskTransitionCode.miss);
    }

    final updatedTask = task.type == TaskType.critical
        ? task.copyWith(
            status: TaskStatus.backlog,
            updatedAt: occurredAt,
            clearSchedule: true,
          )
        : task.copyWith(
            status: TaskStatus.missed,
            updatedAt: occurredAt,
          );
    final activities = <TaskActivity>[
      _activity(
        id: activityId,
        operationId: operationId,
        code: TaskActivityCode.missed,
        task: task,
        occurredAt: occurredAt,
        metadata: metadata,
        nextStatus: updatedTask.status,
      ),
    ];

    if (task.type == TaskType.critical) {
      activities.add(
        _activity(
          id: '$activityId:movedToBacklog',
          operationId: operationId,
          code: TaskActivityCode.movedToBacklog,
          task: task,
          occurredAt: occurredAt,
          metadata: {
            ...metadata,
            'reason': 'criticalMissed',
          },
          nextStatus: updatedTask.status,
        ),
      );
    }

    final accounted = accountingService.apply(
      task: updatedTask,
      activities: activities,
      alreadyAppliedActivityIds: appliedActivityIds,
    );

    return _applied(
      transitionCode: TaskTransitionCode.miss,
      task: accounted.task,
      activities: activities,
    );
  }

  TaskTransitionResult _cancel({
    required Task task,
    required String operationId,
    required String activityId,
    required DateTime occurredAt,
    required Iterable<String> appliedActivityIds,
    required Map<String, Object?> metadata,
  }) {
    if (task.status == TaskStatus.cancelled) {
      return _noOp(task, TaskTransitionCode.cancel);
    }
    if (!_canRemoveFromActivePlan(task)) {
      return _invalid(task, TaskTransitionCode.cancel);
    }

    final cancelled = task.copyWith(
      status: TaskStatus.cancelled,
      updatedAt: occurredAt,
      clearSchedule: true,
    );
    final activities = [
      _activity(
        id: activityId,
        operationId: operationId,
        code: TaskActivityCode.cancelled,
        task: task,
        occurredAt: occurredAt,
        metadata: metadata,
        nextStatus: cancelled.status,
      ),
    ];
    final accounted = accountingService.apply(
      task: cancelled,
      activities: activities,
      alreadyAppliedActivityIds: appliedActivityIds,
    );

    return _applied(
      transitionCode: TaskTransitionCode.cancel,
      task: accounted.task,
      activities: activities,
    );
  }

  TaskTransitionResult _noLongerRelevant({
    required Task task,
    required String operationId,
    required String activityId,
    required DateTime occurredAt,
    required Iterable<String> appliedActivityIds,
    required Map<String, Object?> metadata,
  }) {
    if (task.status == TaskStatus.noLongerRelevant) {
      return _noOp(task, TaskTransitionCode.noLongerRelevant);
    }
    if (!_canRemoveFromActivePlan(task)) {
      return _invalid(task, TaskTransitionCode.noLongerRelevant);
    }

    final dismissed = task.copyWith(
      status: TaskStatus.noLongerRelevant,
      updatedAt: occurredAt,
      clearSchedule: true,
    );

    final activities = [
      _activity(
        id: activityId,
        operationId: operationId,
        code: TaskActivityCode.noLongerRelevant,
        task: task,
        occurredAt: occurredAt,
        metadata: metadata,
        nextStatus: dismissed.status,
      ),
    ];
    final accounted = accountingService.apply(
      task: dismissed,
      activities: activities,
      alreadyAppliedActivityIds: appliedActivityIds,
    );

    return _applied(
      transitionCode: TaskTransitionCode.noLongerRelevant,
      task: accounted.task,
      activities: activities,
    );
  }

  TaskTransitionResult _movementOnly({
    required Task task,
    required TaskTransitionCode transitionCode,
    required TaskActivityCode activityCode,
    required String operationId,
    required String activityId,
    required DateTime occurredAt,
    required Map<String, Object?> metadata,
    required Iterable<String> appliedActivityIds,
  }) {
    if (!task.isFlexible || task.status != TaskStatus.planned) {
      return _invalid(task, transitionCode);
    }

    final updated = task.copyWith(updatedAt: occurredAt);
    final activities = [
      _activity(
        id: activityId,
        operationId: operationId,
        code: activityCode,
        task: task,
        occurredAt: occurredAt,
        metadata: metadata,
        nextStatus: updated.status,
      ),
    ];
    final accounted = accountingService.apply(
      task: updated,
      activities: activities,
      alreadyAppliedActivityIds: appliedActivityIds,
    );

    return _applied(
      transitionCode: transitionCode,
      task: accounted.task,
      activities: activities,
    );
  }

  TaskTransitionResult _moveToBacklog({
    required Task task,
    required String operationId,
    required String activityId,
    required DateTime occurredAt,
    required Iterable<String> appliedActivityIds,
    required Map<String, Object?> metadata,
  }) {
    final canMove = (task.isFlexible || task.type == TaskType.critical) &&
        !_isTerminal(task.status);
    if (!canMove) {
      return _invalid(task, TaskTransitionCode.moveToBacklog);
    }

    final moved = task.copyWith(
      status: TaskStatus.backlog,
      updatedAt: occurredAt,
      clearSchedule: true,
    );
    final activities = [
      _activity(
        id: activityId,
        operationId: operationId,
        code: TaskActivityCode.movedToBacklog,
        task: task,
        occurredAt: occurredAt,
        metadata: metadata,
        nextStatus: moved.status,
      ),
    ];
    final accounted = accountingService.apply(
      task: moved,
      activities: activities,
      alreadyAppliedActivityIds: appliedActivityIds,
    );

    return _applied(
      transitionCode: TaskTransitionCode.moveToBacklog,
      task: accounted.task,
      activities: activities,
    );
  }

  TaskTransitionResult _restoreFromBacklog({
    required Task task,
    required String operationId,
    required String activityId,
    required DateTime occurredAt,
    required Iterable<String> appliedActivityIds,
    required Map<String, Object?> metadata,
  }) {
    if (task.status != TaskStatus.backlog ||
        (!task.isFlexible && task.type != TaskType.critical)) {
      return _invalid(task, TaskTransitionCode.restoreFromBacklog);
    }

    final restored = task.copyWith(
      status: TaskStatus.planned,
      updatedAt: occurredAt,
    );
    final activities = [
      _activity(
        id: activityId,
        operationId: operationId,
        code: TaskActivityCode.restoredFromBacklog,
        task: task,
        occurredAt: occurredAt,
        metadata: metadata,
        nextStatus: restored.status,
      ),
    ];
    final accounted = accountingService.apply(
      task: restored,
      activities: activities,
      alreadyAppliedActivityIds: appliedActivityIds,
    );

    return _applied(
      transitionCode: TaskTransitionCode.restoreFromBacklog,
      task: accounted.task,
      activities: activities,
    );
  }

  TaskTransitionResult _activate({
    required Task task,
    required String operationId,
    required String activityId,
    required DateTime occurredAt,
    required Iterable<String> appliedActivityIds,
    required Map<String, Object?> metadata,
  }) {
    if (task.status != TaskStatus.planned ||
        task.isLocked ||
        task.type == TaskType.freeSlot ||
        task.type == TaskType.surprise) {
      return _invalid(task, TaskTransitionCode.activate);
    }

    final activated = task.copyWith(
      status: TaskStatus.active,
      updatedAt: occurredAt,
    );

    final activities = [
      _activity(
        id: activityId,
        operationId: operationId,
        code: TaskActivityCode.activated,
        task: task,
        occurredAt: occurredAt,
        metadata: metadata,
        nextStatus: activated.status,
      ),
    ];
    final accounted = accountingService.apply(
      task: activated,
      activities: activities,
      alreadyAppliedActivityIds: appliedActivityIds,
    );

    return _applied(
      transitionCode: TaskTransitionCode.activate,
      task: accounted.task,
      activities: activities,
    );
  }
}

TaskStatistics _applyActivityStats({
  required Task task,
  required TaskActivity activity,
  required List<TimeInterval> lockedIntervals,
}) {
  return switch (activity.code) {
    TaskActivityCode.manuallyPushed => task.stats.incrementManualPush(),
    TaskActivityCode.automaticallyPushed => task.stats.incrementAutoPush(),
    TaskActivityCode.movedToBacklog => task.stats.incrementMovedToBacklog(),
    TaskActivityCode.restoredFromBacklog =>
      task.stats.incrementRestoredFromBacklog(),
    TaskActivityCode.missed => task.stats.incrementMissed(),
    TaskActivityCode.cancelled => task.stats.incrementCancelled(),
    TaskActivityCode.completed => _applyCompletionStats(
        task: task,
        activity: activity,
        lockedIntervals: lockedIntervals,
      ),
    TaskActivityCode.noLongerRelevant ||
    TaskActivityCode.activated =>
      task.stats,
  };
}

TaskStatistics _applyCompletionStats({
  required Task task,
  required TaskActivity activity,
  required List<TimeInterval> lockedIntervals,
}) {
  var stats = task.stats;
  final scheduledEnd = task.scheduledEnd;
  final completedAt = task.completedAt ?? activity.occurredAt;

  if (scheduledEnd != null && completedAt.isAfter(scheduledEnd)) {
    stats = stats.incrementCompletedLate();
  }

  final lockedMinutes = _actualLockedOverlapMinutes(
    task: task,
    lockedIntervals: lockedIntervals,
  );
  if (lockedMinutes > 0) {
    stats = stats.incrementCompletedDuringLockedHours(lockedMinutes);
  }

  final pushesBeforeCompletion =
      _intMetadata(activity.metadata['pushesBeforeCompletion']) ??
          _pushesBeforeCompletion(task);

  return stats.recordPushesBeforeCompletion(pushesBeforeCompletion);
}

int _actualLockedOverlapMinutes({
  required Task task,
  required List<TimeInterval> lockedIntervals,
}) {
  final actualStart = task.actualStart;
  final actualEnd = task.actualEnd;
  if (actualStart == null ||
      actualEnd == null ||
      !actualStart.isBefore(actualEnd) ||
      lockedIntervals.isEmpty) {
    return 0;
  }

  final actualInterval = TimeInterval(start: actualStart, end: actualEnd);
  final intersections = <TimeInterval>[];

  for (final lockedInterval in lockedIntervals) {
    if (!actualInterval.overlaps(lockedInterval)) {
      continue;
    }
    intersections.add(
      TimeInterval(
        start: _latest(actualInterval.start, lockedInterval.start),
        end: _earliest(actualInterval.end, lockedInterval.end),
      ),
    );
  }

  if (intersections.isEmpty) {
    return 0;
  }

  intersections.sort((left, right) => left.start.compareTo(right.start));
  var total = Duration.zero;
  var current = intersections.first;
  for (final interval in intersections.skip(1)) {
    if (interval.start.isAfter(current.end)) {
      total += current.duration;
      current = interval;
      continue;
    }

    current = TimeInterval(
      start: current.start,
      end: _latest(current.end, interval.end),
    );
  }
  total += current.duration;

  return total.inMinutes;
}

int _pushesBeforeCompletion(Task task) {
  return task.stats.manuallyPushedCount + task.stats.autoPushedCount;
}

int? _knownDurationMinutes(
  Task task,
  DateTime? actualStart,
  DateTime? actualEnd,
) {
  if (actualStart != null &&
      actualEnd != null &&
      actualStart.isBefore(actualEnd)) {
    final actualMinutes = actualEnd.difference(actualStart).inMinutes;
    if (actualMinutes > 0) {
      return actualMinutes;
    }
  }

  return task.durationMinutes;
}

int? _intMetadata(Object? value) {
  return value is int ? value : null;
}

DateTime _earliest(DateTime first, DateTime second) {
  return first.isBefore(second) ? first : second;
}

DateTime _latest(DateTime first, DateTime second) {
  return first.isAfter(second) ? first : second;
}

TaskTransitionResult _applied({
  required TaskTransitionCode transitionCode,
  required Task task,
  required List<TaskActivity> activities,
}) {
  return TaskTransitionResult(
    transitionCode: transitionCode,
    outcomeCode: TaskTransitionOutcomeCode.applied,
    task: task,
    activities: activities,
  );
}

TaskTransitionResult _invalid(Task task, TaskTransitionCode transitionCode) {
  return TaskTransitionResult(
    transitionCode: transitionCode,
    outcomeCode: TaskTransitionOutcomeCode.invalidState,
    task: task,
  );
}

TaskTransitionResult _noOp(Task task, TaskTransitionCode transitionCode) {
  return TaskTransitionResult(
    transitionCode: transitionCode,
    outcomeCode: TaskTransitionOutcomeCode.noOp,
    task: task,
  );
}

TaskActivity _activity({
  required String id,
  required String operationId,
  required TaskActivityCode code,
  required Task task,
  required DateTime occurredAt,
  required Map<String, Object?> metadata,
  required TaskStatus nextStatus,
}) {
  return TaskActivity(
    id: id,
    operationId: operationId,
    code: code,
    taskId: task.id,
    projectId: task.projectId,
    occurredAt: occurredAt,
    metadata: {
      ...metadata,
      'previousStatus': task.status.name,
      'nextStatus': nextStatus.name,
    },
  );
}

TaskActivity? _duplicateActivity({
  required Iterable<TaskActivity> existingActivities,
  required String taskId,
  required String operationId,
}) {
  for (final activity in existingActivities) {
    if (activity.taskId == taskId && activity.operationId == operationId) {
      return activity;
    }
  }

  return null;
}

bool _actualIntervalShapeIsValid(DateTime? actualStart, DateTime? actualEnd) {
  if (actualStart == null && actualEnd == null) {
    return true;
  }
  return actualStart != null &&
      actualEnd != null &&
      actualStart.isBefore(actualEnd);
}

bool _isNoOpTerminalRepeat(Task task, TaskTransitionCode transitionCode) {
  return (task.status == TaskStatus.completed &&
          transitionCode == TaskTransitionCode.complete) ||
      (task.status == TaskStatus.cancelled &&
          transitionCode == TaskTransitionCode.cancel) ||
      (task.status == TaskStatus.noLongerRelevant &&
          transitionCode == TaskTransitionCode.noLongerRelevant);
}

bool _isBlockedByTerminalState(Task task, TaskTransitionCode transitionCode) {
  if (!_isTerminal(task.status)) {
    return false;
  }
  return !_isNoOpTerminalRepeat(task, transitionCode);
}

bool _isTerminal(TaskStatus status) {
  return status == TaskStatus.completed ||
      status == TaskStatus.cancelled ||
      status == TaskStatus.noLongerRelevant;
}

bool _canRemoveFromActivePlan(Task task) {
  return task.status == TaskStatus.planned ||
      task.status == TaskStatus.active ||
      task.status == TaskStatus.backlog ||
      task.status == TaskStatus.missed;
}

String _requiredTrimmed(String value, String name) {
  final trimmed = value.trim();
  if (trimmed.isEmpty) {
    throw ArgumentError.value(value, name, 'Value is required.');
  }
  return trimmed;
}
