// Parent/child task ownership helpers.
//
// Child tasks are owned by one parent through `Task.parentTaskId`. This file
// keeps that relationship queryable without adding dependency scheduling, DAG
// traversal, or UI-specific state.

import 'models.dart';
import 'task_lifecycle.dart';
import 'time_contracts.dart';

/// Row-style input for creating one child task.
///
/// Priority is nullable on purpose. A null priority means the user did not set
/// one, so callers should preserve row insertion order instead of treating the
/// child as medium priority.
class ChildTaskEntry {
  const ChildTaskEntry({
    required this.id,
    required this.title,
    required this.createdAt,
    this.priority,
    this.reward = RewardLevel.notSet,
    this.difficulty = DifficultyLevel.notSet,
    this.durationMinutes,
    this.projectId,
    this.updatedAt,
  });

  /// Caller-generated child id.
  final String id;

  /// User-entered child title.
  final String title;

  /// Creation timestamp supplied by the caller.
  final DateTime createdAt;

  /// Optional explicit child priority.
  final PriorityLevel? priority;

  /// Optional explicit child reward. Missing reward stays `notSet`.
  final RewardLevel reward;

  /// Optional explicit child difficulty.
  final DifficultyLevel difficulty;

  /// Optional child duration estimate.
  final int? durationMinutes;

  /// Optional project override. Null means inherit the parent project.
  final String? projectId;

  /// Optional update timestamp.
  final DateTime? updatedAt;

  /// Convert this entry into a child [Task] owned by [parent].
  Task toTask({
    required Task parent,
  }) {
    final trimmedTitle = title.trim();
    if (trimmedTitle.isEmpty) {
      throw ArgumentError.value(title, 'title', 'Title is required.');
    }

    return Task(
      id: id,
      title: trimmedTitle,
      projectId: projectId ?? parent.projectId,
      type: TaskType.flexible,
      status: TaskStatus.backlog,
      priority: priority,
      reward: reward,
      difficulty: difficulty,
      durationMinutes: durationMinutes,
      parentTaskId: parent.id,
      createdAt: createdAt,
      updatedAt: updatedAt ?? createdAt,
    );
  }
}

/// Command input for breaking one parent task into direct children.
class ChildTaskBreakUpRequest {
  ChildTaskBreakUpRequest({
    required String parentTaskId,
    required List<ChildTaskEntry> entries,
    required String operationId,
  })  : parentTaskId = _requiredTrimmed(parentTaskId, 'parentTaskId'),
        entries = List<ChildTaskEntry>.unmodifiable(entries),
        operationId = _requiredTrimmed(operationId, 'operationId');

  /// Parent task that will own every created child.
  final String parentTaskId;

  /// Ordered child rows supplied by the caller.
  final List<ChildTaskEntry> entries;

  /// Idempotency key for the application command that creates the children.
  final String operationId;
}

/// Result from breaking a parent task into direct children.
class ChildTaskBreakUpResult {
  ChildTaskBreakUpResult({
    required List<Task> tasks,
    required this.parentTask,
    required List<Task> childTasks,
    required this.operationId,
    List<String> createdChildTaskIds = const <String>[],
    List<TaskActivity> activities = const <TaskActivity>[],
  })  : tasks = List<Task>.unmodifiable(tasks),
        childTasks = List<Task>.unmodifiable(childTasks),
        createdChildTaskIds = List<String>.unmodifiable(createdChildTaskIds),
        activities = List<TaskActivity>.unmodifiable(activities);

  /// Replacement task list including newly created children.
  final List<Task> tasks;

  /// Parent that owns the created children.
  final Task parentTask;

  /// Created children in request order.
  final List<Task> childTasks;

  /// Idempotency key for the command.
  final String operationId;

  /// Child ids created by this command.
  final List<String> createdChildTaskIds;

  /// Activity records produced by this command.
  ///
  /// V1 child creation is represented by task inserts, so this is currently
  /// empty while still giving application use cases one atomic mutation shape.
  final List<TaskActivity> activities;
}

/// Creates direct child tasks from ordered child-entry rows.
class ChildTaskBreakUpService {
  const ChildTaskBreakUpService();

  /// Break [request.parentTaskId] into direct child tasks.
  ///
  /// This service validates the full requested set before returning any
  /// mutations. Persistence and transaction wiring are handled by later blocks.
  ChildTaskBreakUpResult breakUp({
    required List<Task> tasks,
    required ChildTaskBreakUpRequest request,
  }) {
    final parent = _taskById(tasks, request.parentTaskId);
    if (parent == null) {
      throw ArgumentError.value(
        request.parentTaskId,
        'parentTaskId',
        'Parent not found.',
      );
    }
    if (request.entries.isEmpty) {
      throw ArgumentError.value(
        request.entries,
        'entries',
        'At least one child task is required.',
      );
    }

    final existingIds = tasks.map((task) => task.id).toSet();
    final requestedIds = <String>{};
    for (final entry in request.entries) {
      final childId = entry.id.trim();
      if (childId.isEmpty) {
        throw ArgumentError.value(entry.id, 'id', 'Child id is required.');
      }
      if (childId == parent.id) {
        throw ArgumentError.value(
          entry.id,
          'id',
          'Child id cannot match the parent id.',
        );
      }
      if (!requestedIds.add(childId)) {
        throw ArgumentError.value(
          entry.id,
          'id',
          'Child ids must be unique within one break-up command.',
        );
      }
      if (existingIds.contains(childId)) {
        throw ArgumentError.value(
          entry.id,
          'id',
          'Child id is already used by another task.',
        );
      }
    }

    final childTasks = request.entries
        .map((entry) => entry.toTask(parent: parent))
        .toList(growable: false);

    return ChildTaskBreakUpResult(
      tasks: List<Task>.unmodifiable([...tasks, ...childTasks]),
      parentTask: parent,
      childTasks: childTasks,
      operationId: request.operationId,
      createdChildTaskIds:
          childTasks.map((child) => child.id).toList(growable: false),
    );
  }
}

/// Read-only parent/child projection over a task list.
///
/// This is intentionally not a scheduler. It answers ownership questions such
/// as "which tasks belong to this parent?" while leaving task placement and
/// completion rules to other domain services.
class ChildTaskView {
  ChildTaskView({
    required List<Task> tasks,
  }) : tasks = List<Task>.unmodifiable(tasks);

  /// Source task list supplied by the caller.
  final List<Task> tasks;

  /// Direct children owned by [parent], preserving source-list order.
  List<Task> childrenOf(Task parent) {
    return List<Task>.unmodifiable(
      tasks.where((task) => task.parentTaskId == parent.id),
    );
  }

  /// Direct children sorted by explicit priority while preserving row order ties.
  ///
  /// Children without priority sort after explicit priorities. Within each
  /// priority group, including the no-priority group, source-list order is kept.
  List<Task> childrenOfSortedByPriority(Task parent) {
    final indexedChildren = childrenOf(parent)
        .asMap()
        .entries
        .map(
          (entry) => _IndexedChild(
            task: entry.value,
            originalIndex: entry.key,
          ),
        )
        .toList(growable: false);

    indexedChildren.sort((a, b) {
      final priorityComparison = _priorityRank(b.task.priority)
          .compareTo(_priorityRank(a.task.priority));
      if (priorityComparison != 0) {
        return priorityComparison;
      }

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

    return List<Task>.unmodifiable(
      indexedChildren.map((child) => child.task),
    );
  }

  /// Parent task for [child], or null when the parent is not in [tasks].
  Task? parentOf(Task child) {
    final parentId = child.parentTaskId;
    if (parentId == null) {
      return null;
    }

    for (final task in tasks) {
      if (task.id == parentId) {
        return task;
      }
    }

    return null;
  }

  /// Whether [child] is directly owned by [parent].
  bool isChildOf({
    required Task child,
    required Task parent,
  }) {
    return child.parentTaskId == parent.id;
  }

  /// Aggregate direct child status counts for [parent].
  ChildTaskSummary summaryFor(Task parent) {
    return ChildTaskSummary.fromChildren(childrenOf(parent));
  }

  /// Whether this helper would auto-complete [parent].
  bool parentShouldAutoComplete(Task parent) {
    return summaryFor(parent).allChildrenCompleted;
  }
}

/// Result from child/parent completion propagation.
class ChildTaskCompletionResult {
  ChildTaskCompletionResult({
    required List<Task> tasks,
    required List<String> changedTaskIds,
    this.completedChildId,
    this.completedParentId,
    List<String> forceCompletedChildIds = const <String>[],
    List<TaskActivity> activities = const <TaskActivity>[],
    List<TaskTransitionOutcomeCode> transitionOutcomeCodes =
        const <TaskTransitionOutcomeCode>[],
    this.autoCompletedParent = false,
    this.canCompleteParentExplicitly = false,
  })  : tasks = List<Task>.unmodifiable(tasks),
        changedTaskIds = List<String>.unmodifiable(changedTaskIds),
        forceCompletedChildIds =
            List<String>.unmodifiable(forceCompletedChildIds),
        activities = List<TaskActivity>.unmodifiable(activities),
        transitionOutcomeCodes = List<TaskTransitionOutcomeCode>.unmodifiable(
            transitionOutcomeCodes);

  /// Replacement task list after the completion action.
  final List<Task> tasks;

  /// Task ids whose completion state changed.
  final List<String> changedTaskIds;

  /// Child id completed by the initial child-complete action, when applicable.
  final String? completedChildId;

  /// Parent id completed by the action, when applicable.
  final String? completedParentId;

  /// Direct child ids completed by explicit parent-complete behavior.
  final List<String> forceCompletedChildIds;

  /// Internal activities produced by parent/child completion propagation.
  final List<TaskActivity> activities;

  /// Transition outcomes returned while applying completion propagation.
  final List<TaskTransitionOutcomeCode> transitionOutcomeCodes;

  /// Whether the parent was completed because all direct children are complete.
  final bool autoCompletedParent;

  /// Whether callers may offer an explicit parent-complete action.
  final bool canCompleteParentExplicitly;
}

/// Applies direct parent/child completion rules.
///
/// This service intentionally handles only one ownership level. It does not walk
/// dependency graphs, and it only completes sibling children when the caller
/// explicitly completes the parent.
class ChildTaskCompletionService {
  const ChildTaskCompletionService({
    this.clock = const SystemClock(),
    this.transitionService = const TaskTransitionService(),
  });

  /// Clock boundary used when a completion timestamp is not supplied.
  final Clock clock;

  /// Canonical lifecycle transition dependency.
  final TaskTransitionService transitionService;

  /// Complete one child and auto-complete its parent only when all children are complete.
  ChildTaskCompletionResult completeChild({
    required List<Task> tasks,
    required String childTaskId,
    DateTime? updatedAt,
    String? operationId,
    Iterable<TaskActivity> existingActivities = const <TaskActivity>[],
    Iterable<String> appliedActivityIds = const <String>[],
  }) {
    final now = updatedAt ?? clock.now();
    final child = _taskById(tasks, childTaskId);
    if (child == null) {
      throw ArgumentError.value(childTaskId, 'childTaskId', 'Task not found.');
    }

    final parentId = child.parentTaskId;
    if (parentId == null) {
      throw ArgumentError.value(
          childTaskId, 'childTaskId', 'Task is not a child.');
    }

    final parent = _taskById(tasks, parentId);
    if (parent == null) {
      throw ArgumentError.value(parentId, 'parentTaskId', 'Parent not found.');
    }

    final opId =
        operationId ?? _defaultOperationId('complete-child', child.id, now);
    final childTransition = _completeWithTransition(
      task: child,
      operationId: opId,
      occurredAt: now,
      existingActivities: existingActivities,
      appliedActivityIds: appliedActivityIds,
      metadata: {
        'completionSource': 'child',
        'parentTaskId': parent.id,
      },
    );
    final withChildCompleted = _replaceTask(tasks, childTransition.task);
    final view = ChildTaskView(tasks: withChildCompleted);
    final shouldCompleteParent = parent.status != TaskStatus.completed &&
        view.parentShouldAutoComplete(parent);
    final activities = <TaskActivity>[...childTransition.activities];
    final outcomes = <TaskTransitionOutcomeCode>[childTransition.outcomeCode];

    if (!shouldCompleteParent) {
      return ChildTaskCompletionResult(
        tasks: List<Task>.unmodifiable(withChildCompleted),
        changedTaskIds: childTransition.applied ? [child.id] : const <String>[],
        completedChildId: child.id,
        canCompleteParentExplicitly: parent.status != TaskStatus.completed,
        activities: activities,
        transitionOutcomeCodes: outcomes,
      );
    }

    final parentTransition = _completeWithTransition(
      task: parent,
      operationId: opId,
      occurredAt: now,
      existingActivities: existingActivities,
      appliedActivityIds: appliedActivityIds,
      metadata: {
        'completionSource': 'autoParentAfterLastChild',
        'completedChildTaskId': child.id,
      },
    );
    activities.addAll(parentTransition.activities);
    outcomes.add(parentTransition.outcomeCode);

    final updatedParent = parentTransition.task;
    final completedTasks = _replaceTask(withChildCompleted, updatedParent);
    final changedTaskIds = <String>[
      if (childTransition.applied) child.id,
      if (parentTransition.applied) parent.id,
    ];

    return ChildTaskCompletionResult(
      tasks: List<Task>.unmodifiable(completedTasks),
      changedTaskIds: List<String>.unmodifiable(changedTaskIds),
      completedChildId: child.id,
      completedParentId: parent.id,
      autoCompletedParent: true,
      activities: activities,
      transitionOutcomeCodes: outcomes,
    );
  }

  /// Complete a parent from one of its direct children and force-complete siblings.
  ChildTaskCompletionResult completeParentFromChild({
    required List<Task> tasks,
    required String childTaskId,
    DateTime? updatedAt,
    String? operationId,
    Iterable<TaskActivity> existingActivities = const <TaskActivity>[],
    Iterable<String> appliedActivityIds = const <String>[],
  }) {
    final child = _taskById(tasks, childTaskId);
    if (child == null) {
      throw ArgumentError.value(childTaskId, 'childTaskId', 'Task not found.');
    }
    final parentId = child.parentTaskId;
    if (parentId == null) {
      throw ArgumentError.value(
        childTaskId,
        'childTaskId',
        'Task is not a child.',
      );
    }

    return completeParent(
      tasks: tasks,
      parentTaskId: parentId,
      updatedAt: updatedAt,
      operationId: operationId,
      initiatingChildTaskId: child.id,
      existingActivities: existingActivities,
      appliedActivityIds: appliedActivityIds,
    );
  }

  /// Explicitly complete a parent and any direct children not already completed.
  ChildTaskCompletionResult completeParent({
    required List<Task> tasks,
    required String parentTaskId,
    DateTime? updatedAt,
    String? operationId,
    String? initiatingChildTaskId,
    Iterable<TaskActivity> existingActivities = const <TaskActivity>[],
    Iterable<String> appliedActivityIds = const <String>[],
  }) {
    final now = updatedAt ?? clock.now();
    final parent = _taskById(tasks, parentTaskId);
    if (parent == null) {
      throw ArgumentError.value(
          parentTaskId, 'parentTaskId', 'Parent not found.');
    }

    final directChildren = ChildTaskView(tasks: tasks).childrenOf(parent);
    if (initiatingChildTaskId != null &&
        !directChildren.any((child) => child.id == initiatingChildTaskId)) {
      throw ArgumentError.value(
        initiatingChildTaskId,
        'initiatingChildTaskId',
        'Initiating task must be a direct child of the parent.',
      );
    }

    final opId =
        operationId ?? _defaultOperationId('complete-parent', parent.id, now);
    final forceCompletedChildIds = <String>[];
    final changedTaskIds = <String>[];
    final activities = <TaskActivity>[];
    final outcomes = <TaskTransitionOutcomeCode>[];
    final updatedTasks = <Task>[];

    for (final task in tasks) {
      if (task.id == parent.id) {
        final transition = _completeWithTransition(
          task: task,
          operationId: opId,
          occurredAt: now,
          existingActivities: existingActivities,
          appliedActivityIds: appliedActivityIds,
          metadata: {
            'completionSource':
                initiatingChildTaskId == null ? 'parent' : 'parentFromChild',
            if (initiatingChildTaskId != null)
              'initiatingChildTaskId': initiatingChildTaskId,
          },
        );
        final completedParent = transition.task;
        updatedTasks.add(completedParent);
        activities.addAll(transition.activities);
        outcomes.add(transition.outcomeCode);
        if (transition.applied) {
          changedTaskIds.add(task.id);
        }
        continue;
      }

      final isDirectChild = directChildren.any((child) => child.id == task.id);
      if (isDirectChild && task.status != TaskStatus.completed) {
        final transition = _completeWithTransition(
          task: task,
          operationId: opId,
          occurredAt: now,
          existingActivities: existingActivities,
          appliedActivityIds: appliedActivityIds,
          metadata: {
            'completionSource': initiatingChildTaskId == null
                ? 'parentForce'
                : 'childForceParent',
            'parentTaskId': parent.id,
            if (initiatingChildTaskId != null)
              'initiatingChildTaskId': initiatingChildTaskId,
          },
        );
        updatedTasks.add(transition.task);
        activities.addAll(transition.activities);
        outcomes.add(transition.outcomeCode);
        if (transition.applied) {
          forceCompletedChildIds.add(task.id);
          changedTaskIds.add(task.id);
        }
        continue;
      }

      updatedTasks.add(task);
    }

    return ChildTaskCompletionResult(
      tasks: List<Task>.unmodifiable(updatedTasks),
      changedTaskIds: List<String>.unmodifiable(changedTaskIds),
      completedParentId: parent.id,
      forceCompletedChildIds: List<String>.unmodifiable(forceCompletedChildIds),
      activities: List<TaskActivity>.unmodifiable(activities),
      transitionOutcomeCodes: List<TaskTransitionOutcomeCode>.unmodifiable(
        outcomes,
      ),
    );
  }

  TaskTransitionResult _completeWithTransition({
    required Task task,
    required String operationId,
    required DateTime occurredAt,
    required Iterable<TaskActivity> existingActivities,
    required Iterable<String> appliedActivityIds,
    required Map<String, Object?> metadata,
  }) {
    return transitionService.apply(
      task: task,
      transitionCode: TaskTransitionCode.complete,
      operationId: operationId,
      activityId: _activityId(
        operationId,
        task.id,
        TaskActivityCode.completed,
      ),
      occurredAt: occurredAt,
      existingActivities: existingActivities,
      appliedActivityIds: appliedActivityIds,
      metadata: metadata,
    );
  }
}

/// Status counts for a parent's direct children.
class ChildTaskSummary {
  const ChildTaskSummary({
    required this.totalCount,
    required this.plannedCount,
    required this.activeCount,
    required this.completedCount,
    required this.missedCount,
    required this.cancelledCount,
    required this.noLongerRelevantCount,
    required this.backlogCount,
  });

  /// Build counts from [children].
  factory ChildTaskSummary.fromChildren(List<Task> children) {
    var plannedCount = 0;
    var activeCount = 0;
    var completedCount = 0;
    var missedCount = 0;
    var cancelledCount = 0;
    var noLongerRelevantCount = 0;
    var backlogCount = 0;

    for (final child in children) {
      switch (child.status) {
        case TaskStatus.planned:
          plannedCount += 1;
        case TaskStatus.active:
          activeCount += 1;
        case TaskStatus.completed:
          completedCount += 1;
        case TaskStatus.missed:
          missedCount += 1;
        case TaskStatus.cancelled:
          cancelledCount += 1;
        case TaskStatus.noLongerRelevant:
          noLongerRelevantCount += 1;
        case TaskStatus.backlog:
          backlogCount += 1;
      }
    }

    return ChildTaskSummary(
      totalCount: children.length,
      plannedCount: plannedCount,
      activeCount: activeCount,
      completedCount: completedCount,
      missedCount: missedCount,
      cancelledCount: cancelledCount,
      noLongerRelevantCount: noLongerRelevantCount,
      backlogCount: backlogCount,
    );
  }

  /// Total direct children counted.
  final int totalCount;

  /// Children in planned status.
  final int plannedCount;

  /// Children in active status.
  final int activeCount;

  /// Children in completed status.
  final int completedCount;

  /// Children in missed status.
  final int missedCount;

  /// Children in cancelled status.
  final int cancelledCount;

  /// Children marked no longer relevant.
  final int noLongerRelevantCount;

  /// Children currently in backlog.
  final int backlogCount;

  /// Whether at least one direct child exists.
  bool get hasChildren => totalCount > 0;

  /// Whether every direct child is completed.
  bool get allChildrenCompleted => hasChildren && completedCount == totalCount;
}

class _IndexedChild {
  const _IndexedChild({
    required this.task,
    required this.originalIndex,
  });

  final Task task;
  final int originalIndex;
}

int _priorityRank(PriorityLevel? priority) {
  return switch (priority) {
    PriorityLevel.veryLow => 0,
    PriorityLevel.low => 1,
    PriorityLevel.medium => 2,
    PriorityLevel.high => 3,
    PriorityLevel.veryHigh => 4,
    null => -1,
  };
}

Task? _taskById(List<Task> tasks, String taskId) {
  for (final task in tasks) {
    if (task.id == taskId) {
      return task;
    }
  }

  return null;
}

List<Task> _replaceTask(List<Task> tasks, Task replacement) {
  return tasks
      .map((task) => task.id == replacement.id ? replacement : task)
      .toList(growable: false);
}

String _defaultOperationId(String actionName, String taskId, DateTime at) {
  return '$actionName:$taskId:${at.toIso8601String()}';
}

String _activityId(
  String operationId,
  String taskId,
  TaskActivityCode activityCode,
) {
  return '$operationId:$taskId:${activityCode.name}';
}

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