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

part of '../../task_actions.dart';

/// Applies low-friction quick actions for flexible task cards.
///
/// This service is the adapter between small UI button presses and domain logic.
/// It intentionally only accepts flexible tasks; required/locked/surprise items
/// should have their own action rules so the UI cannot accidentally apply a
/// flexible-only behavior to a fixed commitment.
class FlexibleTaskActionService {
  /// Creates a `FlexibleTaskActionService` 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 FlexibleTaskActionService({
    this.schedulingEngine = const SchedulingEngine(),
    this.transitionService = const TaskTransitionService(),
    this.clock = const SystemClock(),
  });

  /// Scheduling dependency used for actions that need timeline changes.
  final SchedulingEngine schedulingEngine;

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

  /// Clock boundary used when an action timestamp is not supplied.
  final Clock clock;

  /// Apply the first-stage quick action.
  ///
  /// Direct actions (`done`, `backlog`) return a changed task. `push` returns the
  /// list of destinations the UI should present. `breakUp` signals that the UI
  /// should start a child-task flow rather than changing the task immediately.
  FlexibleTaskActionResult apply({
    required Task task,
    required FlexibleTaskQuickAction action,
    DateTime? updatedAt,
    String? operationId,
    DateTime? actualStart,
    DateTime? actualEnd,
    List<TimeInterval> lockedIntervals = const <TimeInterval>[],
    Iterable<TaskActivity> existingActivities = const <TaskActivity>[],
  }) {
    if (!task.isFlexible) {
      logger.error(() => 'Flexible action rejected for non-flexible task. '
          'action=${action.name} taskId=${task.id} '
          'taskType=${task.type.name} taskStatus=${task.status.name}');
      throw ArgumentError.value(
          task.type, 'task.type', 'Task must be flexible.');
    }

    logger.debug(() => 'Applying flexible quick action. '
        'action=${action.name} taskId=${task.id} '
        'taskStatus=${task.status.name} hasExplicitUpdatedAt=${updatedAt != null} '
        'hasOperationId=${operationId != null}');
    logger.finest(() => 'Flexible quick action input dump. '
        'action=${action.name} task=$task '
        'updatedAt=${updatedAt?.toIso8601String()} operationId=$operationId '
        'actualStart=${actualStart?.toIso8601String()} '
        'actualEnd=${actualEnd?.toIso8601String()} '
        'lockedIntervals=$lockedIntervals '
        'existingActivityCount=${existingActivities.length}');

    switch (action) {
      case FlexibleTaskQuickAction.done:
        final now = updatedAt ?? clock.now();
        final opId =
            operationId ?? _defaultOperationId(action.name, task.id, now);
        logger.finer(() => 'Completing flexible task. taskId=${task.id} '
            'operationId=$opId occurredAt=${now.toIso8601String()} '
            'hasActualStart=${actualStart != null} '
            'hasActualEnd=${actualEnd != null} '
            'lockedIntervalCount=${lockedIntervals.length}');
        final transition = transitionService.apply(
          task: task,
          transitionCode: TaskTransitionCode.complete,
          operationId: opId,
          activityId: _activityId(
            opId,
            task.id,
            TaskActivityCode.completed,
          ),
          occurredAt: now,
          actualStart: actualStart,
          actualEnd: actualEnd,
          lockedIntervals: lockedIntervals,
          existingActivities: existingActivities,
        );
        logger.debug(() => 'Flexible task completion transition finished. '
            'taskId=${task.id} outcome=${transition.outcomeCode.name} '
            'nextStatus=${transition.task.status.name} '
            'activityCount=${transition.activities.length}');
        if (transition.outcomeCode != TaskTransitionOutcomeCode.applied) {
          logger
              .fine(() => 'Possible issue: flexible completion did not apply. '
                  'taskId=${task.id} outcome=${transition.outcomeCode.name} '
                  'previousStatus=${task.status.name} '
                  'nextStatus=${transition.task.status.name}');
        }
        return FlexibleTaskActionResult(
          action: action,
          task: transition.task,
          activities: transition.activities,
          transitionOutcomeCode: transition.outcomeCode,
        );
      case FlexibleTaskQuickAction.push:
        logger.debug(() => 'Flexible push destination menu requested. '
            'taskId=${task.id} taskStatus=${task.status.name}');
        logger.finer(() => 'Flexible push destinations prepared. '
            'taskId=${task.id} destinationCount=3');
        return FlexibleTaskActionResult(
          action: action,
          task: task,
          pushDestinations: const [
            PushDestination.nextAvailableSlot,
            PushDestination.tomorrowTopOfQueue,
            PushDestination.backlog,
          ],
        );
      case FlexibleTaskQuickAction.backlog:
        final now = updatedAt ?? clock.now();
        final opId =
            operationId ?? _defaultOperationId(action.name, task.id, now);
        logger.debug(() => 'Moving flexible task to backlog from quick action. '
            'taskId=${task.id} operationId=$opId '
            'occurredAt=${now.toIso8601String()} '
            'previousStatus=${task.status.name}');
        final transition = transitionService.apply(
          task: task,
          transitionCode: TaskTransitionCode.moveToBacklog,
          operationId: opId,
          activityId: _activityId(
            opId,
            task.id,
            TaskActivityCode.movedToBacklog,
          ),
          occurredAt: now,
          existingActivities: existingActivities,
        );
        logger.debug(() => 'Flexible task backlog transition finished. '
            'taskId=${task.id} outcome=${transition.outcomeCode.name} '
            'nextStatus=${transition.task.status.name} '
            'activityCount=${transition.activities.length}');
        if (transition.outcomeCode != TaskTransitionOutcomeCode.applied) {
          logger.fine(() =>
              'Possible issue: move-to-backlog quick action did not apply. '
              'taskId=${task.id} outcome=${transition.outcomeCode.name} '
              'previousStatus=${task.status.name} '
              'nextStatus=${transition.task.status.name}');
        }
        return FlexibleTaskActionResult(
          action: action,
          task: transition.task,
          activities: transition.activities,
          transitionOutcomeCode: transition.outcomeCode,
        );
      case FlexibleTaskQuickAction.breakUp:
        logger.debug(() => 'Flexible break-up flow requested. '
            'taskId=${task.id} taskStatus=${task.status.name}');
        return FlexibleTaskActionResult(
          action: action,
          task: task,
          startsChildTaskFlow: true,
        );
    }
  }

  /// Apply the second-stage destination selected after the `push` action.
  ///
  /// This needs the full [SchedulingInput] because pushing can shift other
  /// flexible tasks and must avoid locked/required intervals.
  PushDestinationResult applyPushDestination({
    required PushDestination destination,
    required SchedulingInput input,
    required String taskId,
    DateTime? updatedAt,
    DateTime? earliestStart,
    String? operationId,
    Iterable<TaskActivity> existingActivities = const <TaskActivity>[],
  }) {
    final now = updatedAt ?? clock.now();
    final opId = operationId ??
        _defaultOperationId('push-${destination.name}', taskId, now);
    logger.debug(() => 'Applying flexible push destination. '
        'destination=${destination.name} taskId=$taskId operationId=$opId '
        'occurredAt=${now.toIso8601String()} taskCount=${input.tasks.length} '
        'windowStart=${input.window.start.toIso8601String()} '
        'windowEnd=${input.window.end.toIso8601String()}');
    logger.finest(() => 'Flexible push destination input dump. '
        'destination=${destination.name} taskId=$taskId input=$input '
        'existingActivityCount=${existingActivities.length}');
    if (_hasDuplicateActivity(
      existingActivities: existingActivities,
      operationId: opId,
      taskId: taskId,
    )) {
      logger.warn(() => 'Duplicate flexible push operation ignored. '
          'destination=${destination.name} taskId=$taskId operationId=$opId');
      return PushDestinationResult(
        destination: destination,
        schedulingResult: SchedulingResult(
          tasks: input.tasks,
          operationCode: _operationCodeForPushDestination(destination),
          outcomeCode: SchedulingOutcomeCode.noOp,
          notices: [
            SchedulingNotice('Push operation already applied.'),
          ],
        ),
      );
    }

    logger.finer(() => 'Dispatching flexible push destination to scheduler. '
        'destination=${destination.name} taskId=$taskId');
    final result = switch (destination) {
      PushDestination.nextAvailableSlot =>
        schedulingEngine.pushFlexibleTaskToNextAvailableSlot(
          input: input,
          taskId: taskId,
          updatedAt: now,
          earliestStart: earliestStart,
        ),
      PushDestination.tomorrowTopOfQueue =>
        schedulingEngine.pushFlexibleTaskToTomorrowTopOfQueue(
          input: input,
          taskId: taskId,
          updatedAt: now,
        ),
      PushDestination.backlog => _moveTaskToBacklog(
          input: input,
          taskId: taskId,
          updatedAt: now,
        ),
    };
    logger.debug(() => 'Flexible push destination scheduling finished. '
        'destination=${destination.name} taskId=$taskId '
        'outcome=${result.outcomeCode.name} changeCount=${result.changes.length} '
        'noticeCount=${result.notices.length}');
    if (result.outcomeCode != SchedulingOutcomeCode.success) {
      logger.warn(() => 'Handled flexible push issue. '
          'destination=${destination.name} taskId=$taskId '
          'outcome=${result.outcomeCode.name} notices=${result.notices}');
    }
    logger.fine(() => 'Flexible push result details. '
        'destination=${destination.name} taskId=$taskId '
        'changes=${result.changes} notices=${result.notices}');
    logger.finest(() => 'Flexible push destination result dump. '
        'destination=${destination.name} taskId=$taskId result=$result');

    final activities = _activitiesForPushDestination(
      destination: destination,
      input: input,
      result: result,
      taskId: taskId,
      operationId: opId,
      occurredAt: now,
      existingActivities: existingActivities,
    );
    logger.debug(() => 'Flexible push destination activities prepared. '
        'destination=${destination.name} taskId=$taskId '
        'activityCount=${activities.length}');

    return PushDestinationResult(
      destination: destination,
      schedulingResult: result,
      activities: activities,
    );
  }

  /// Move one planned flexible task to backlog inside a scheduling result.
  ///
  /// This mirrors the shape of other push destination results so callers can
  /// handle every destination through the same `SchedulingResult` interface.
  SchedulingResult _moveTaskToBacklog({
    required SchedulingInput input,
    required String taskId,
    DateTime? updatedAt,
  }) {
    logger.debug(() => 'Moving flexible task to backlog from push destination. '
        'taskId=$taskId taskCount=${input.tasks.length} '
        'hasExplicitUpdatedAt=${updatedAt != null}');
    logger.finest(() => 'Move-to-backlog input dump. '
        'taskId=$taskId tasks=${input.tasks}');
    final task = _taskById(input.tasks, taskId);
    if (task == null) {
      logger.warn(() => 'Handled move-to-backlog issue: task not found. '
          'taskId=$taskId');
      return SchedulingResult(
        tasks: input.tasks,
        operationCode: SchedulingOperationCode.moveFlexibleTaskToBacklog,
        outcomeCode: SchedulingOutcomeCode.notFound,
        notices: [
          SchedulingNotice(
            'Task was not found.',
            type: SchedulingNoticeType.noFit,
            taskId: taskId,
            issueCode: SchedulingIssueCode.taskNotFound,
          ),
        ],
      );
    }

    if (!task.isFlexible || task.status != TaskStatus.planned) {
      logger.warn(() => 'Handled move-to-backlog issue: invalid task state. '
          'taskId=${task.id} taskType=${task.type.name} '
          'taskStatus=${task.status.name}');
      return SchedulingResult(
        tasks: input.tasks,
        operationCode: SchedulingOperationCode.moveFlexibleTaskToBacklog,
        outcomeCode: SchedulingOutcomeCode.invalidState,
        notices: [
          SchedulingNotice(
            'Only planned flexible tasks can be moved to backlog.',
            type: SchedulingNoticeType.noFit,
            taskId: task.id,
            issueCode: SchedulingIssueCode.invalidTaskState,
          ),
        ],
      );
    }

    final movedTask = schedulingEngine.moveToBacklog(
      task,
      updatedAt: updatedAt,
    );
    logger.debug(() => 'Flexible task moved to backlog. '
        'taskId=${task.id} previousStart=${task.scheduledStart?.toIso8601String()} '
        'previousEnd=${task.scheduledEnd?.toIso8601String()} '
        'updatedAt=${updatedAt?.toIso8601String()}');

    return SchedulingResult(
      tasks: List<Task>.unmodifiable(
        input.tasks.map((current) {
          if (current.id == task.id) {
            return movedTask;
          }

          return current;
        }),
      ),
      operationCode: SchedulingOperationCode.moveFlexibleTaskToBacklog,
      outcomeCode: SchedulingOutcomeCode.success,
      notices: [
        SchedulingNotice(
          'Flexible task moved to backlog.',
          type: SchedulingNoticeType.moved,
          taskId: task.id,
          movementCode: SchedulingMovementCode.flexibleTaskMovedToBacklog,
          parameters: {
            'previousStart': task.scheduledStart,
            'previousEnd': task.scheduledEnd,
            'nextStart': null,
            'nextEnd': null,
          },
        ),
      ],
      changes: [
        SchedulingChange(
          taskId: task.id,
          previousStart: task.scheduledStart,
          previousEnd: task.scheduledEnd,
          nextStart: null,
          nextEnd: null,
        ),
      ],
    );
  }
}
