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

part of '../../child_tasks.dart';

/// 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 {
  /// Creates a `ChildTaskCompletionService` 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.
  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();
    logger.debug(() => 'Completing child task. childTaskId=$childTaskId '
        'taskCount=${tasks.length} operationId=$operationId '
        'updatedAt=${now.toIso8601String()}');
    final child = _taskById(tasks, childTaskId);
    if (child == null) {
      logger.warn(() => 'Child completion failed: task not found. '
          'childTaskId=$childTaskId operationId=$operationId');
      throw ArgumentError.value(childTaskId, 'childTaskId', 'Task not found.');
    }

    final parentId = child.parentTaskId;
    if (parentId == null) {
      logger.warn(() => 'Child completion failed: task has no parent. '
          'childTaskId=$childTaskId operationId=$operationId');
      throw ArgumentError.value(
          childTaskId, 'childTaskId', 'Task is not a child.');
    }

    final parent = _taskById(tasks, parentId);
    if (parent == null) {
      logger.warn(() => 'Child completion failed: parent not found. '
          'childTaskId=$childTaskId parentTaskId=$parentId operationId=$operationId');
      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) {
      logger.debug(() => 'Child task completed without parent auto-complete. '
          'childTaskId=${child.id} parentTaskId=${parent.id} '
          'childApplied=${childTransition.applied} operationId=$opId');
      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,
    ];

    logger.debug(() => 'Child task completion auto-completed parent. '
        'childTaskId=${child.id} parentTaskId=${parent.id} '
        'changedCount=${changedTaskIds.length} operationId=$opId');

    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>[],
  }) {
    logger.debug(() => 'Completing parent from child. childTaskId=$childTaskId '
        'taskCount=${tasks.length} operationId=$operationId');
    final child = _taskById(tasks, childTaskId);
    if (child == null) {
      logger.warn(() => 'Parent-from-child completion failed: task not found. '
          'childTaskId=$childTaskId operationId=$operationId');
      throw ArgumentError.value(childTaskId, 'childTaskId', 'Task not found.');
    }
    final parentId = child.parentTaskId;
    if (parentId == null) {
      logger.warn(
          () => 'Parent-from-child completion failed: task has no parent. '
              'childTaskId=$childTaskId operationId=$operationId');
      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();
    logger.debug(() => 'Completing parent task. parentTaskId=$parentTaskId '
        'taskCount=${tasks.length} initiatingChildTaskId=$initiatingChildTaskId '
        'operationId=$operationId updatedAt=${now.toIso8601String()}');
    final parent = _taskById(tasks, parentTaskId);
    if (parent == null) {
      logger.warn(() => 'Parent completion failed: parent not found. '
          'parentTaskId=$parentTaskId operationId=$operationId');
      throw ArgumentError.value(
          parentTaskId, 'parentTaskId', 'Parent not found.');
    }

    final directChildren = ChildTaskView(tasks: tasks).childrenOf(parent);
    if (initiatingChildTaskId != null &&
        !directChildren.any((child) => child.id == initiatingChildTaskId)) {
      logger.warn(
          () => 'Parent completion failed: initiating child is not direct. '
              'parentTaskId=${parent.id} childTaskId=$initiatingChildTaskId '
              'operationId=$operationId');
      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);
    }

    logger.debug(
        () => 'Parent task completion finished. parentTaskId=${parent.id} '
            'directChildCount=${directChildren.length} '
            'forceCompletedCount=${forceCompletedChildIds.length} '
            'changedCount=${changedTaskIds.length} operationId=$opId');

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

  /// Runs the `_completeWithTransition` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  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,
    );
  }
}
