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

part of '../../application_commands.dart';

/// UI-facing V1 command use cases.
class V1ApplicationCommandUseCases {
  /// Creates a `V1ApplicationCommandUseCases` 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 V1ApplicationCommandUseCases({
    required this.applicationStore,
    required this.timeZoneResolver,
    this.resolutionOptions = const TimeZoneResolutionOptions(),
    this.quickCaptureService = const QuickCaptureService(),
    this.flexibleTaskActionService = const FlexibleTaskActionService(),
    this.requiredTaskActionService = const RequiredTaskActionService(),
    this.surpriseTaskLogService = const SurpriseTaskLogService(),
    this.freeSlotService = const FreeSlotService(),
    this.childTaskBreakUpService = const ChildTaskBreakUpService(),
    this.childTaskCompletionService = const ChildTaskCompletionService(),
    this.projectStatisticsAggregationService =
        const ProjectStatisticsAggregationService(),
  });

  /// Atomic repository boundary.
  final ApplicationUnitOfWork applicationStore;

  /// Explicit local-time resolver for planning windows and locked expansion.
  final TimeZoneResolver timeZoneResolver;

  /// DST/nonexistent/repeated local time policy.
  final TimeZoneResolutionOptions resolutionOptions;

  /// Stores the `quickCaptureService` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final QuickCaptureService quickCaptureService;

  /// Stores the `flexibleTaskActionService` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final FlexibleTaskActionService flexibleTaskActionService;

  /// Stores the `requiredTaskActionService` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final RequiredTaskActionService requiredTaskActionService;

  /// Stores the `surpriseTaskLogService` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final SurpriseTaskLogService surpriseTaskLogService;

  /// Stores the `freeSlotService` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final FreeSlotService freeSlotService;

  /// Stores the `childTaskBreakUpService` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final ChildTaskBreakUpService childTaskBreakUpService;

  /// Stores the `childTaskCompletionService` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final ChildTaskCompletionService childTaskCompletionService;

  /// Stores the `projectStatisticsAggregationService` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final ProjectStatisticsAggregationService projectStatisticsAggregationService;

  /// Quick capture a task into backlog.
  Future<ApplicationResult<ApplicationCommandResult>> quickCaptureToBacklog({
    required ApplicationOperationContext context,
    required String title,
    String projectId = 'inbox',
    PriorityLevel priority = PriorityLevel.medium,
    RewardLevel reward = RewardLevel.notSet,
    DifficultyLevel difficulty = DifficultyLevel.notSet,
    TaskType type = TaskType.flexible,
    int? durationMinutes,
    Set<BacklogTag> backlogTags = const <BacklogTag>{},
  }) {
    const commandCode = ApplicationCommandCode.quickCaptureToBacklog;
    return applicationStore.run<ApplicationCommandResult>(
      context: context,
      operationName: commandCode.name,
      action: (repositories) async {
        logger.debug(() =>
            'Application command executing. command=${commandCode.name} '
            'operationId=${context.operationId} hasTitle=${title.trim().isNotEmpty} '
            'projectId=$projectId taskType=${type.name} durationMinutes=$durationMinutes');
        final result = quickCaptureService.capture(
          QuickCaptureRequest(
            id: context.idGenerator.nextId(),
            title: title,
            createdAt: context.now,
            addToNextAvailableSlot: false,
            projectId: projectId,
            priority: priority,
            reward: reward,
            difficulty: difficulty,
            type: type,
            durationMinutes: durationMinutes,
            backlogTags: backlogTags,
          ),
          updatedAt: context.now,
        );
        if (!result.isValid) {
          return _failure(
            ApplicationFailureCode.validation,
            detailCode: QuickCaptureStatus.validationError.name,
          );
        }

        return _persist(
          repositories: repositories,
          commandCode: commandCode,
          context: context,
          beforeTasks: const <Task>[],
          afterTasks: [result.task],
          activities: const <TaskActivity>[],
          readHint: ApplicationCommandReadHint(
            affectedTaskIds: [result.task.id],
            refreshToday: false,
          ),
        );
      },
    );
  }

  /// Quick capture a flexible task and schedule it into the next available slot.
  Future<ApplicationResult<ApplicationCommandResult>>
      quickCaptureToNextAvailableSlot({
    required ApplicationOperationContext context,
    required CivilDate localDate,
    required String title,
    required int durationMinutes,
    String projectId = 'inbox',
    PriorityLevel priority = PriorityLevel.medium,
    RewardLevel reward = RewardLevel.notSet,
    DifficultyLevel difficulty = DifficultyLevel.notSet,
  }) {
    const commandCode = ApplicationCommandCode.quickCaptureToNextAvailableSlot;
    return applicationStore.run<ApplicationCommandResult>(
      context: context,
      operationName: commandCode.name,
      action: (repositories) async {
        logger.debug(() =>
            'Application command executing. command=${commandCode.name} '
            'operationId=${context.operationId} localDate=${localDate.toIsoString()} '
            'hasTitle=${title.trim().isNotEmpty} projectId=$projectId '
            'durationMinutes=$durationMinutes');
        final state =
            await _loadPlanningState(repositories, context, localDate);
        final taskId = context.idGenerator.nextId();
        final result = quickCaptureService.capture(
          QuickCaptureRequest(
            id: taskId,
            title: title,
            createdAt: context.now,
            addToNextAvailableSlot: true,
            projectId: projectId,
            priority: priority,
            reward: reward,
            difficulty: difficulty,
            durationMinutes: durationMinutes,
          ),
          schedulingInput: state.input,
          updatedAt: context.now,
        );
        if (!result.isValid) {
          return _failureForQuickCapture(result);
        }

        final activities = _activitiesForSchedulingChanges(
          result: result.schedulingResult,
          beforeTasks: [result.task, ...state.tasks],
          operationId: context.operationId,
          occurredAt: context.now,
          selectedTaskId: result.task.id,
          selectedActivityCode: TaskActivityCode.restoredFromBacklog,
          movedActivityCode: TaskActivityCode.automaticallyPushed,
        );
        return _persist(
          repositories: repositories,
          commandCode: commandCode,
          context: context,
          beforeTasks: state.tasks,
          afterTasks: result.schedulingResult?.tasks ?? [result.task],
          activities: activities,
          schedulingResult: result.schedulingResult,
          readHint: ApplicationCommandReadHint(
            localDate: localDate,
            affectedTaskIds: _taskIdsFromTasks([result.task]),
          ),
        );
      },
    );
  }

  /// Schedule an existing backlog item into the next available flexible slot.
  Future<ApplicationResult<ApplicationCommandResult>>
      scheduleBacklogItemToNextAvailableSlot({
    required ApplicationOperationContext context,
    required CivilDate localDate,
    required String taskId,
    int? durationMinutes,
    DateTime? expectedUpdatedAt,
  }) {
    const commandCode =
        ApplicationCommandCode.scheduleBacklogItemToNextAvailableSlot;
    return applicationStore.run<ApplicationCommandResult>(
      context: context,
      operationName: commandCode.name,
      action: (repositories) async {
        logger.debug(() =>
            'Application command executing. command=${commandCode.name} '
            'operationId=${context.operationId} localDate=${localDate.toIsoString()} '
            'taskId=$taskId durationMinutes=$durationMinutes');
        var state = await _loadPlanningState(repositories, context, localDate);
        var task = _taskById(state.tasks, taskId);
        if (task == null) {
          logger.finer(() =>
              'Planning-state task lookup missed; reading by id. '
              'command=${commandCode.name} taskId=$taskId operationId=${context.operationId}');
          task = await repositories.tasks.findById(taskId);
          if (task != null) {
            state = state.withTask(task);
          }
        }
        if (task == null) {
          return _failure(ApplicationFailureCode.notFound, entityId: taskId);
        }
        final staleFailure = _staleFailure(task, expectedUpdatedAt);
        if (staleFailure != null) {
          return ApplicationResult.failure(staleFailure);
        }
        if (durationMinutes != null && durationMinutes <= 0) {
          return _failure(
            ApplicationFailureCode.validation,
            entityId: taskId,
            detailCode: DomainValidationCode.nonPositiveDuration.name,
          );
        }
        if (durationMinutes != null) {
          task = task.copyWith(
            durationMinutes: durationMinutes,
            updatedAt: context.now,
          );
          state = state.replaceTask(task);
        }

        final schedulingResult =
            const SchedulingEngine().insertBacklogTaskIntoNextAvailableSlot(
          input: state.input,
          taskId: taskId,
          updatedAt: context.now,
        );
        final schedulingFailure = _failureForSchedulingResult(
          schedulingResult,
          entityId: taskId,
        );
        if (schedulingFailure != null) {
          return ApplicationResult.failure(schedulingFailure);
        }

        final activities = _activitiesForSchedulingChanges(
          result: schedulingResult,
          beforeTasks: state.tasks,
          operationId: context.operationId,
          occurredAt: context.now,
          selectedTaskId: taskId,
          selectedActivityCode: TaskActivityCode.restoredFromBacklog,
          movedActivityCode: TaskActivityCode.automaticallyPushed,
        );
        return _persist(
          repositories: repositories,
          commandCode: commandCode,
          context: context,
          beforeTasks: state.tasks,
          afterTasks: schedulingResult.tasks,
          activities: activities,
          schedulingResult: schedulingResult,
          readHint: ApplicationCommandReadHint(
            localDate: localDate,
            affectedTaskIds: _taskIdsFromChanges(schedulingResult.changes),
          ),
        );
      },
    );
  }

  /// Push a planned flexible task later in the same local day.
  Future<ApplicationResult<ApplicationCommandResult>>
      pushFlexibleToNextAvailableSlot({
    required ApplicationOperationContext context,
    required CivilDate localDate,
    required String taskId,
    DateTime? expectedUpdatedAt,
  }) {
    return _pushFlexible(
      commandCode: ApplicationCommandCode.pushFlexibleToNextAvailableSlot,
      context: context,
      localDate: localDate,
      taskId: taskId,
      destination: PushDestination.nextAvailableSlot,
      expectedUpdatedAt: expectedUpdatedAt,
    );
  }

  /// Push a planned flexible task to the top of a future/tomorrow queue.
  Future<ApplicationResult<ApplicationCommandResult>>
      pushFlexibleToTomorrowTopOfQueue({
    required ApplicationOperationContext context,
    required CivilDate targetDate,
    required String taskId,
    DateTime? expectedUpdatedAt,
  }) {
    return _pushFlexible(
      commandCode: ApplicationCommandCode.pushFlexibleToTomorrowTopOfQueue,
      context: context,
      localDate: targetDate,
      taskId: taskId,
      destination: PushDestination.tomorrowTopOfQueue,
      expectedUpdatedAt: expectedUpdatedAt,
    );
  }

  /// Move a planned flexible task to backlog.
  Future<ApplicationResult<ApplicationCommandResult>> moveFlexibleToBacklog({
    required ApplicationOperationContext context,
    required String taskId,
    DateTime? expectedUpdatedAt,
  }) {
    const commandCode = ApplicationCommandCode.moveFlexibleToBacklog;
    return applicationStore.run<ApplicationCommandResult>(
      context: context,
      operationName: commandCode.name,
      action: (repositories) async {
        logger.debug(
            () => 'Application command executing. command=${commandCode.name} '
                'operationId=${context.operationId} taskId=$taskId');
        final task = await repositories.tasks.findById(taskId);
        if (task == null) {
          return _failure(ApplicationFailureCode.notFound, entityId: taskId);
        }
        final staleFailure = _staleFailure(task, expectedUpdatedAt);
        if (staleFailure != null) {
          return ApplicationResult.failure(staleFailure);
        }
        final existingActivities = await repositories.taskActivities.findAll();
        final actionResult = flexibleTaskActionService.apply(
          task: task,
          action: FlexibleTaskQuickAction.backlog,
          updatedAt: context.now,
          operationId: context.operationId,
          existingActivities: existingActivities,
        );
        if (actionResult.transitionOutcomeCode ==
            TaskTransitionOutcomeCode.invalidState) {
          return _failure(
            ApplicationFailureCode.conflict,
            entityId: taskId,
            detailCode: TaskTransitionOutcomeCode.invalidState.name,
          );
        }
        return _persist(
          repositories: repositories,
          commandCode: commandCode,
          context: context,
          beforeTasks: [task],
          afterTasks: [actionResult.task],
          activities: actionResult.activities,
          readHint: ApplicationCommandReadHint(
            affectedTaskIds: [taskId],
            refreshToday: true,
          ),
        );
      },
    );
  }

  /// Add the Someday/Wishlist backlog tag to an active Backlog task.
  Future<ApplicationResult<ApplicationCommandResult>> pushBacklogTaskToSomeday({
    required ApplicationOperationContext context,
    required String taskId,
    DateTime? expectedUpdatedAt,
  }) {
    const commandCode = ApplicationCommandCode.pushBacklogTaskToSomeday;
    return applicationStore.run<ApplicationCommandResult>(
      context: context,
      operationName: commandCode.name,
      action: (repositories) async {
        logger.debug(
            () => 'Application command executing. command=${commandCode.name} '
                'operationId=${context.operationId} taskId=$taskId');
        final task = await repositories.tasks.findById(taskId);
        if (task == null) {
          return _failure(ApplicationFailureCode.notFound, entityId: taskId);
        }
        final staleFailure = _staleFailure(task, expectedUpdatedAt);
        if (staleFailure != null) {
          return ApplicationResult.failure(staleFailure);
        }
        if (task.status != TaskStatus.backlog) {
          return _failure(
            ApplicationFailureCode.conflict,
            entityId: taskId,
            detailCode: 'notBacklogTask',
          );
        }
        final updatedTags = {...task.backlogTags, BacklogTag.wishlist};
        final updated = task.copyWith(
          backlogTags: updatedTags,
          updatedAt: context.now,
        );
        return _persist(
          repositories: repositories,
          commandCode: commandCode,
          context: context,
          beforeTasks: [task],
          afterTasks: [updated],
          activities: const <TaskActivity>[],
          readHint: ApplicationCommandReadHint(
            affectedTaskIds: [taskId],
            refreshToday: false,
          ),
        );
      },
    );
  }

  /// Hide an active Backlog task without deleting its persisted record.
  Future<ApplicationResult<ApplicationCommandResult>> archiveBacklogTask({
    required ApplicationOperationContext context,
    required String taskId,
    DateTime? expectedUpdatedAt,
  }) {
    const commandCode = ApplicationCommandCode.archiveBacklogTask;
    return applicationStore.run<ApplicationCommandResult>(
      context: context,
      operationName: commandCode.name,
      action: (repositories) async {
        logger.debug(
            () => 'Application command executing. command=${commandCode.name} '
                'operationId=${context.operationId} taskId=$taskId');
        final task = await repositories.tasks.findById(taskId);
        if (task == null) {
          return _failure(ApplicationFailureCode.notFound, entityId: taskId);
        }
        final staleFailure = _staleFailure(task, expectedUpdatedAt);
        if (staleFailure != null) {
          return ApplicationResult.failure(staleFailure);
        }
        if (task.status != TaskStatus.backlog) {
          return _failure(
            ApplicationFailureCode.conflict,
            entityId: taskId,
            detailCode: 'notBacklogTask',
          );
        }
        final archived = task.copyWith(
          status: TaskStatus.noLongerRelevant,
          updatedAt: context.now,
          clearSchedule: true,
        );
        return _persist(
          repositories: repositories,
          commandCode: commandCode,
          context: context,
          beforeTasks: [task],
          afterTasks: [archived],
          activities: const <TaskActivity>[],
          readHint: ApplicationCommandReadHint(
            affectedTaskIds: [taskId],
            refreshToday: false,
          ),
        );
      },
    );
  }

  /// Permanently remove a task from persistence.
  Future<ApplicationResult<ApplicationCommandResult>> removeTask({
    required ApplicationOperationContext context,
    required String taskId,
    DateTime? expectedUpdatedAt,
  }) {
    const commandCode = ApplicationCommandCode.removeTask;
    return applicationStore.run<ApplicationCommandResult>(
      context: context,
      operationName: commandCode.name,
      action: (repositories) async {
        logger.debug(
            () => 'Application command executing. command=${commandCode.name} '
                'operationId=${context.operationId} taskId=$taskId');
        final record = await repositories.tasks.findRecordById(taskId);
        if (record == null) {
          return _failure(ApplicationFailureCode.notFound, entityId: taskId);
        }
        final staleFailure = _staleFailure(record.value, expectedUpdatedAt);
        if (staleFailure != null) {
          return ApplicationResult.failure(staleFailure);
        }
        final deleteResult = await repositories.tasks.deleteIfRevision(
          taskId: taskId,
          ownerId: context.ownerTimeZone.ownerId,
          expectedRevision: record.revision,
        );
        final deleteFailure = deleteResult.failure;
        if (deleteFailure != null) {
          return ApplicationResult.failure(
            _failureForRepositoryMutation(deleteFailure),
          );
        }
        return ApplicationResult.success(
          ApplicationCommandResult(
            commandCode: commandCode,
            operationId: context.operationId,
            changedTasks: const <Task>[],
            readHint: ApplicationCommandReadHint(
              affectedTaskIds: [taskId],
              refreshToday: true,
            ),
          ),
        );
      },
    );
  }

  /// Complete a flexible task.
  Future<ApplicationResult<ApplicationCommandResult>> completeFlexibleTask({
    required ApplicationOperationContext context,
    required CivilDate localDate,
    required String taskId,
    DateTime? expectedUpdatedAt,
    DateTime? actualStart,
    DateTime? actualEnd,
  }) {
    const commandCode = ApplicationCommandCode.completeFlexibleTask;
    return applicationStore.run<ApplicationCommandResult>(
      context: context,
      operationName: commandCode.name,
      action: (repositories) async {
        logger.debug(() =>
            'Application command executing. command=${commandCode.name} '
            'operationId=${context.operationId} localDate=${localDate.toIsoString()} taskId=$taskId');
        var state = await _loadPlanningState(repositories, context, localDate);
        var task = _taskById(state.tasks, taskId);
        if (task == null) {
          logger.finer(() =>
              'Planning-state task lookup missed; reading by id. '
              'command=${commandCode.name} taskId=$taskId operationId=${context.operationId}');
          task = await repositories.tasks.findById(taskId);
          if (task != null) {
            state = state.withTask(task);
          }
        }
        if (task == null) {
          return _failure(ApplicationFailureCode.notFound, entityId: taskId);
        }
        final staleFailure = _staleFailure(task, expectedUpdatedAt);
        if (staleFailure != null) {
          return ApplicationResult.failure(staleFailure);
        }
        final existingActivities = await repositories.taskActivities.findAll();
        final actionResult = flexibleTaskActionService.apply(
          task: task,
          action: FlexibleTaskQuickAction.done,
          updatedAt: context.now,
          operationId: context.operationId,
          actualStart: actualStart,
          actualEnd: actualEnd,
          lockedIntervals: state.lockedIntervals,
          existingActivities: existingActivities,
        );
        if (actionResult.transitionOutcomeCode ==
            TaskTransitionOutcomeCode.invalidState) {
          return _failure(
            ApplicationFailureCode.conflict,
            entityId: taskId,
            detailCode: TaskTransitionOutcomeCode.invalidState.name,
          );
        }
        return _persist(
          repositories: repositories,
          commandCode: commandCode,
          context: context,
          beforeTasks: state.tasks,
          afterTasks: _replaceTask(state.tasks, actionResult.task),
          activities: actionResult.activities,
          readHint: ApplicationCommandReadHint(
            localDate: localDate,
            affectedTaskIds: [taskId],
          ),
        );
      },
    );
  }

  /// Mark a completed scheduled task as still needing to be done.
  Future<ApplicationResult<ApplicationCommandResult>> uncompleteTask({
    required ApplicationOperationContext context,
    required CivilDate localDate,
    required String taskId,
    DateTime? expectedUpdatedAt,
  }) {
    const commandCode = ApplicationCommandCode.uncompleteTask;
    return applicationStore.run<ApplicationCommandResult>(
      context: context,
      operationName: commandCode.name,
      action: (repositories) async {
        logger.debug(() =>
            'Application command executing. command=${commandCode.name} '
            'operationId=${context.operationId} localDate=${localDate.toIsoString()} taskId=$taskId');
        var state = await _loadPlanningState(repositories, context, localDate);
        var task = _taskById(state.tasks, taskId);
        if (task == null) {
          logger.finer(() =>
              'Planning-state task lookup missed; reading by id. '
              'command=${commandCode.name} taskId=$taskId operationId=${context.operationId}');
          task = await repositories.tasks.findById(taskId);
          if (task != null) {
            state = state.withTask(task);
          }
        }
        if (task == null) {
          return _failure(ApplicationFailureCode.notFound, entityId: taskId);
        }
        final staleFailure = _staleFailure(task, expectedUpdatedAt);
        if (staleFailure != null) {
          return ApplicationResult.failure(staleFailure);
        }
        if (task.status != TaskStatus.completed ||
            (!task.isFlexible && !task.isRequiredVisible)) {
          return _failure(
            ApplicationFailureCode.conflict,
            entityId: taskId,
            detailCode: TaskTransitionOutcomeCode.invalidState.name,
          );
        }
        final uncompleted = task.copyWith(
          status: TaskStatus.planned,
          updatedAt: context.now,
          clearActualInterval: true,
          clearCompletion: true,
        );
        return _persist(
          repositories: repositories,
          commandCode: commandCode,
          context: context,
          beforeTasks: state.tasks,
          afterTasks: _replaceTask(state.tasks, uncompleted),
          activities: const <TaskActivity>[],
          readHint: ApplicationCommandReadHint(
            localDate: localDate,
            affectedTaskIds: [taskId],
          ),
        );
      },
    );
  }

  /// Apply a required visible task lifecycle action.
  Future<ApplicationResult<ApplicationCommandResult>> applyRequiredTaskAction({
    required ApplicationOperationContext context,
    required CivilDate localDate,
    required String taskId,
    required RequiredTaskAction action,
    DateTime? expectedUpdatedAt,
    DateTime? actualStart,
    DateTime? actualEnd,
  }) {
    const commandCode = ApplicationCommandCode.applyRequiredTaskAction;
    return applicationStore.run<ApplicationCommandResult>(
      context: context,
      operationName: commandCode.name,
      action: (repositories) async {
        logger.debug(() =>
            'Application command executing. command=${commandCode.name} '
            'operationId=${context.operationId} localDate=${localDate.toIsoString()} taskId=$taskId '
            'action=${action.name}');
        var state = await _loadPlanningState(repositories, context, localDate);
        var task = _taskById(state.tasks, taskId);
        if (task == null) {
          logger.finer(() =>
              'Planning-state task lookup missed; reading by id. '
              'command=${commandCode.name} taskId=$taskId operationId=${context.operationId}');
          task = await repositories.tasks.findById(taskId);
          if (task != null) {
            state = state.withTask(task);
          }
        }
        if (task == null) {
          return _failure(ApplicationFailureCode.notFound, entityId: taskId);
        }
        final staleFailure = _staleFailure(task, expectedUpdatedAt);
        if (staleFailure != null) {
          return ApplicationResult.failure(staleFailure);
        }
        final existingActivities = await repositories.taskActivities.findAll();
        final actionResult = requiredTaskActionService.apply(
          task: task,
          action: action,
          updatedAt: context.now,
          operationId: context.operationId,
          actualStart: actualStart,
          actualEnd: actualEnd,
          lockedIntervals: state.lockedIntervals,
          existingActivities: existingActivities,
        );
        if (actionResult.transitionOutcomeCode ==
            TaskTransitionOutcomeCode.invalidState) {
          return _failure(
            ApplicationFailureCode.conflict,
            entityId: taskId,
            detailCode: TaskTransitionOutcomeCode.invalidState.name,
          );
        }
        return _persist(
          repositories: repositories,
          commandCode: commandCode,
          context: context,
          beforeTasks: state.tasks,
          afterTasks: _replaceTask(state.tasks, actionResult.task),
          activities: actionResult.activities,
          readHint: ApplicationCommandReadHint(
            localDate: localDate,
            affectedTaskIds: [taskId],
          ),
        );
      },
    );
  }

  /// Log completed surprise work and repair overlapping flexible work.
  Future<ApplicationResult<ApplicationCommandResult>> logSurpriseTask({
    required ApplicationOperationContext context,
    required CivilDate localDate,
    required String title,
    DateTime? startedAt,
    int? timeUsedMinutes,
    String projectId = 'inbox',
    PriorityLevel? priority,
    RewardLevel reward = RewardLevel.notSet,
    DifficultyLevel difficulty = DifficultyLevel.notSet,
    bool revealHiddenLockedDetails = false,
  }) {
    const commandCode = ApplicationCommandCode.logSurpriseTask;
    return applicationStore.run<ApplicationCommandResult>(
      context: context,
      operationName: commandCode.name,
      action: (repositories) async {
        logger.debug(() =>
            'Application command executing. command=${commandCode.name} '
            'operationId=${context.operationId} localDate=${localDate.toIsoString()} '
            'hasTitle=${title.trim().isNotEmpty} hasStartedAt=${startedAt != null} '
            'timeUsedMinutes=$timeUsedMinutes projectId=$projectId');
        final state =
            await _loadPlanningState(repositories, context, localDate);
        final logResult = surpriseTaskLogService.log(
          request: SurpriseTaskLogRequest(
            id: context.operationId,
            title: title,
            createdAt: context.now,
            startedAt: startedAt,
            timeUsedMinutes: timeUsedMinutes,
            projectId: projectId,
            priority: priority,
            reward: reward,
            difficulty: difficulty,
          ),
          input: state.input,
          updatedAt: context.now,
          revealHiddenLockedDetails: revealHiddenLockedDetails,
        );
        return _persist(
          repositories: repositories,
          commandCode: commandCode,
          context: context,
          beforeTasks: state.tasks,
          afterTasks: logResult.schedulingResult.tasks,
          activities: logResult.activities,
          schedulingResult: logResult.schedulingResult,
          protectedFreeSlotConflicts: const <ProtectedFreeSlotConflict>[],
          readHint: ApplicationCommandReadHint(
            localDate: localDate,
            affectedTaskIds: _changedTaskIds(
              state.tasks,
              logResult.schedulingResult.tasks,
            ),
          ),
        );
      },
    );
  }

  /// Create a protected Free Slot task.
  Future<ApplicationResult<ApplicationCommandResult>> createProtectedFreeSlot({
    required ApplicationOperationContext context,
    required CivilDate localDate,
    required String title,
    required DateTime start,
    required DateTime end,
    String projectId = 'inbox',
  }) {
    const commandCode = ApplicationCommandCode.createProtectedFreeSlot;
    return applicationStore.run<ApplicationCommandResult>(
      context: context,
      operationName: commandCode.name,
      action: (repositories) async {
        logger.debug(() =>
            'Application command executing. command=${commandCode.name} '
            'operationId=${context.operationId} localDate=${localDate.toIsoString()} '
            'start=${start.toIso8601String()} end=${end.toIso8601String()} '
            'projectId=$projectId hasTitle=${title.trim().isNotEmpty}');
        final freeSlot = freeSlotService.create(
          id: context.idGenerator.nextId(),
          title: title,
          start: start,
          end: end,
          createdAt: context.now,
          updatedAt: context.now,
          projectId: projectId,
        );
        return _persist(
          repositories: repositories,
          commandCode: commandCode,
          context: context,
          beforeTasks: const <Task>[],
          afterTasks: [freeSlot],
          activities: const <TaskActivity>[],
          readHint: ApplicationCommandReadHint(
            localDate: localDate,
            affectedTaskIds: [freeSlot.id],
          ),
        );
      },
    );
  }

  /// Update a protected Free Slot task.
  Future<ApplicationResult<ApplicationCommandResult>> updateProtectedFreeSlot({
    required ApplicationOperationContext context,
    required CivilDate localDate,
    required String taskId,
    required DateTime start,
    required DateTime end,
    DateTime? expectedUpdatedAt,
    String? title,
    String? projectId,
  }) {
    const commandCode = ApplicationCommandCode.updateProtectedFreeSlot;
    return applicationStore.run<ApplicationCommandResult>(
      context: context,
      operationName: commandCode.name,
      action: (repositories) async {
        logger.debug(() =>
            'Application command executing. command=${commandCode.name} '
            'operationId=${context.operationId} localDate=${localDate.toIsoString()} taskId=$taskId '
            'start=${start.toIso8601String()} end=${end.toIso8601String()} '
            'hasTitle=${title != null} projectId=$projectId');
        final task = await repositories.tasks.findById(taskId);
        if (task == null) {
          return _failure(ApplicationFailureCode.notFound, entityId: taskId);
        }
        final staleFailure = _staleFailure(task, expectedUpdatedAt);
        if (staleFailure != null) {
          return ApplicationResult.failure(staleFailure);
        }
        final updated = freeSlotService.update(
          freeSlot: task,
          start: start,
          end: end,
          updatedAt: context.now,
          title: title,
          projectId: projectId,
        );
        return _persist(
          repositories: repositories,
          commandCode: commandCode,
          context: context,
          beforeTasks: [task],
          afterTasks: [updated],
          activities: const <TaskActivity>[],
          readHint: ApplicationCommandReadHint(
            localDate: localDate,
            affectedTaskIds: [taskId],
          ),
        );
      },
    );
  }

  /// Remove a protected Free Slot from the active timeline.
  Future<ApplicationResult<ApplicationCommandResult>> removeProtectedFreeSlot({
    required ApplicationOperationContext context,
    required CivilDate localDate,
    required String taskId,
    DateTime? expectedUpdatedAt,
  }) {
    const commandCode = ApplicationCommandCode.removeProtectedFreeSlot;
    return applicationStore.run<ApplicationCommandResult>(
      context: context,
      operationName: commandCode.name,
      action: (repositories) async {
        logger.debug(() =>
            'Application command executing. command=${commandCode.name} '
            'operationId=${context.operationId} localDate=${localDate.toIsoString()} taskId=$taskId');
        final task = await repositories.tasks.findById(taskId);
        if (task == null) {
          return _failure(ApplicationFailureCode.notFound, entityId: taskId);
        }
        if (task.type != TaskType.freeSlot) {
          return _failure(
            ApplicationFailureCode.validation,
            entityId: taskId,
            detailCode: 'invalidFreeSlotType',
          );
        }
        final staleFailure = _staleFailure(task, expectedUpdatedAt);
        if (staleFailure != null) {
          return ApplicationResult.failure(staleFailure);
        }
        final removed = task.copyWith(
          status: TaskStatus.cancelled,
          updatedAt: context.now,
          clearSchedule: true,
        );
        return _persist(
          repositories: repositories,
          commandCode: commandCode,
          context: context,
          beforeTasks: [task],
          afterTasks: [removed],
          activities: const <TaskActivity>[],
          readHint: ApplicationCommandReadHint(
            localDate: localDate,
            affectedTaskIds: [taskId],
          ),
        );
      },
    );
  }

  /// Break a parent task into direct child tasks.
  Future<ApplicationResult<ApplicationCommandResult>> breakUpTask({
    required ApplicationOperationContext context,
    required String parentTaskId,
    required List<ApplicationChildTaskDraft> children,
    DateTime? expectedUpdatedAt,
  }) {
    const commandCode = ApplicationCommandCode.breakUpTask;
    return applicationStore.run<ApplicationCommandResult>(
      context: context,
      operationName: commandCode.name,
      action: (repositories) async {
        logger.debug(
            () => 'Application command executing. command=${commandCode.name} '
                'operationId=${context.operationId} parentTaskId=$parentTaskId '
                'childDraftCount=${children.length}');
        final tasks = await repositories.tasks.findAll();
        final parent = _taskById(tasks, parentTaskId);
        if (parent == null) {
          return _failure(
            ApplicationFailureCode.notFound,
            entityId: parentTaskId,
          );
        }
        final staleFailure = _staleFailure(parent, expectedUpdatedAt);
        if (staleFailure != null) {
          return ApplicationResult.failure(staleFailure);
        }
        final entries = children.map((draft) {
          return ChildTaskEntry(
            id: context.idGenerator.nextId(),
            title: draft.title,
            createdAt: context.now,
            priority: draft.priority,
            reward: draft.reward,
            difficulty: draft.difficulty,
            durationMinutes: draft.durationMinutes,
            projectId: draft.projectId,
            updatedAt: context.now,
          );
        }).toList(growable: false);
        final breakUpResult = childTaskBreakUpService.breakUp(
          tasks: tasks,
          request: ChildTaskBreakUpRequest(
            parentTaskId: parentTaskId,
            entries: entries,
            operationId: context.operationId,
          ),
        );
        return _persist(
          repositories: repositories,
          commandCode: commandCode,
          context: context,
          beforeTasks: tasks,
          afterTasks: breakUpResult.tasks,
          activities: breakUpResult.activities,
          childTaskIds: breakUpResult.createdChildTaskIds,
          readHint: ApplicationCommandReadHint(
            affectedTaskIds: [
              parentTaskId,
              ...breakUpResult.createdChildTaskIds
            ],
            refreshToday: false,
          ),
        );
      },
    );
  }

  /// Complete one child task and auto-complete its parent if appropriate.
  Future<ApplicationResult<ApplicationCommandResult>> completeChildTask({
    required ApplicationOperationContext context,
    required String childTaskId,
    DateTime? expectedUpdatedAt,
  }) {
    const commandCode = ApplicationCommandCode.completeChildTask;
    return _completeChildOrParent(
      commandCode: commandCode,
      context: context,
      taskId: childTaskId,
      expectedUpdatedAt: expectedUpdatedAt,
      apply: (tasks, existingActivities) =>
          childTaskCompletionService.completeChild(
        tasks: tasks,
        childTaskId: childTaskId,
        updatedAt: context.now,
        operationId: context.operationId,
        existingActivities: existingActivities,
      ),
    );
  }

  /// Complete a parent and force-complete unfinished direct children.
  Future<ApplicationResult<ApplicationCommandResult>> completeParentTask({
    required ApplicationOperationContext context,
    required String parentTaskId,
    DateTime? expectedUpdatedAt,
  }) {
    const commandCode = ApplicationCommandCode.completeParentTask;
    return _completeChildOrParent(
      commandCode: commandCode,
      context: context,
      taskId: parentTaskId,
      expectedUpdatedAt: expectedUpdatedAt,
      apply: (tasks, existingActivities) =>
          childTaskCompletionService.completeParent(
        tasks: tasks,
        parentTaskId: parentTaskId,
        updatedAt: context.now,
        operationId: context.operationId,
        existingActivities: existingActivities,
      ),
    );
  }

  /// Complete a parent from one of its direct children.
  Future<ApplicationResult<ApplicationCommandResult>> completeParentFromChild({
    required ApplicationOperationContext context,
    required String childTaskId,
    DateTime? expectedUpdatedAt,
  }) {
    const commandCode = ApplicationCommandCode.completeParentFromChild;
    return _completeChildOrParent(
      commandCode: commandCode,
      context: context,
      taskId: childTaskId,
      expectedUpdatedAt: expectedUpdatedAt,
      apply: (tasks, existingActivities) =>
          childTaskCompletionService.completeParentFromChild(
        tasks: tasks,
        childTaskId: childTaskId,
        updatedAt: context.now,
        operationId: context.operationId,
        existingActivities: existingActivities,
      ),
    );
  }

  /// Runs the `_pushFlexible` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  Future<ApplicationResult<ApplicationCommandResult>> _pushFlexible({
    required ApplicationCommandCode commandCode,
    required ApplicationOperationContext context,
    required CivilDate localDate,
    required String taskId,
    required PushDestination destination,
    DateTime? expectedUpdatedAt,
  }) {
    return applicationStore.run<ApplicationCommandResult>(
      context: context,
      operationName: commandCode.name,
      action: (repositories) async {
        logger.debug(() =>
            'Application command executing. command=${commandCode.name} '
            'operationId=${context.operationId} localDate=${localDate.toIsoString()} taskId=$taskId '
            'destination=${destination.name}');
        var state = await _loadPlanningState(repositories, context, localDate);
        var task = _taskById(state.tasks, taskId);
        if (task == null) {
          logger.finer(() =>
              'Planning-state task lookup missed; reading by id. '
              'command=${commandCode.name} taskId=$taskId operationId=${context.operationId}');
          task = await repositories.tasks.findById(taskId);
          if (task != null) {
            state = state.withTask(task);
          }
        }
        if (task == null) {
          return _failure(ApplicationFailureCode.notFound, entityId: taskId);
        }
        final staleFailure = _staleFailure(task, expectedUpdatedAt);
        if (staleFailure != null) {
          return ApplicationResult.failure(staleFailure);
        }
        final existingActivities = await repositories.taskActivities.findAll();
        final pushResult = flexibleTaskActionService.applyPushDestination(
          destination: destination,
          input: state.input,
          taskId: taskId,
          updatedAt: context.now,
          earliestStart: destination == PushDestination.nextAvailableSlot
              ? context.now
              : null,
          operationId: context.operationId,
          existingActivities: existingActivities,
        );
        final schedulingFailure = _failureForSchedulingResult(
          pushResult.schedulingResult,
          entityId: taskId,
        );
        if (schedulingFailure != null) {
          return ApplicationResult.failure(schedulingFailure);
        }
        return _persist(
          repositories: repositories,
          commandCode: commandCode,
          context: context,
          beforeTasks: state.tasks,
          afterTasks: pushResult.schedulingResult.tasks,
          activities: pushResult.activities,
          schedulingResult: pushResult.schedulingResult,
          readHint: ApplicationCommandReadHint(
            localDate: localDate,
            affectedTaskIds:
                _taskIdsFromChanges(pushResult.schedulingResult.changes),
          ),
        );
      },
    );
  }

  /// Runs the `_completeChildOrParent` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  Future<ApplicationResult<ApplicationCommandResult>> _completeChildOrParent({
    required ApplicationCommandCode commandCode,
    required ApplicationOperationContext context,
    required String taskId,
    required ChildTaskCompletionResult Function(
      List<Task> tasks,
      List<TaskActivity> existingActivities,
    ) apply,
    DateTime? expectedUpdatedAt,
  }) {
    return applicationStore.run<ApplicationCommandResult>(
      context: context,
      operationName: commandCode.name,
      action: (repositories) async {
        logger.debug(
            () => 'Application command executing. command=${commandCode.name} '
                'operationId=${context.operationId} taskId=$taskId');
        final tasks = await repositories.tasks.findAll();
        final task = _taskById(tasks, taskId);
        if (task == null) {
          return _failure(ApplicationFailureCode.notFound, entityId: taskId);
        }
        final staleFailure = _staleFailure(task, expectedUpdatedAt);
        if (staleFailure != null) {
          return ApplicationResult.failure(staleFailure);
        }
        final existingActivities = await repositories.taskActivities.findAll();
        final result = apply(tasks, existingActivities);
        return _persist(
          repositories: repositories,
          commandCode: commandCode,
          context: context,
          beforeTasks: tasks,
          afterTasks: result.tasks,
          activities: result.activities,
          childTaskIds: [
            if (result.completedChildId != null) result.completedChildId!,
            ...result.forceCompletedChildIds,
          ],
          readHint: ApplicationCommandReadHint(
            affectedTaskIds: result.changedTaskIds,
            refreshToday: true,
          ),
        );
      },
    );
  }

  /// Runs the `_loadPlanningState` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  Future<_PlanningState> _loadPlanningState(
    ApplicationUnitOfWorkRepositories repositories,
    ApplicationOperationContext context,
    CivilDate localDate,
  ) async {
    final ownerId = context.ownerTimeZone.ownerId;
    logger.debug(() => 'Loading planning state. ownerId=$ownerId '
        'localDate=${localDate.toIsoString()} operationId=${context.operationId}');
    final settings = await repositories.ownerSettings.findByOwnerId(ownerId) ??
        OwnerSettings(
          ownerId: ownerId,
          timeZoneId: context.ownerTimeZone.timeZoneId,
        );
    final timeZoneId = context.ownerTimeZone.timeZoneId;
    final effectiveSettings = settings.copyWith(timeZoneId: timeZoneId);
    final dayStart = _resolve(
      date: localDate,
      wallTime: effectiveSettings.dayStart,
      timeZoneId: timeZoneId,
    );
    final dayEnd = _resolve(
      date: localDate,
      wallTime: effectiveSettings.dayEnd,
      timeZoneId: timeZoneId,
    );
    final window = SchedulingWindow(start: dayStart, end: dayEnd);
    final tasks = (await repositories.tasks.findForLocalDay(
      ownerId: ownerId,
      localDate: localDate,
      window: window,
    ))
        .items;
    final lockedExpansion = expandLockedBlocksForDay(
      blocks: (await repositories.lockedBlocks.findBlocksByOwner(
        ownerId: ownerId,
      ))
          .items,
      overrides: (await repositories.lockedBlocks.findOverridesForDate(
        ownerId: ownerId,
        date: localDate,
      ))
          .items,
      date: localDate,
      timeZoneId: timeZoneId,
      timeZoneResolver: timeZoneResolver,
      resolutionOptions: resolutionOptions,
    );

    logger.debug(() =>
        'Planning state loaded. ownerId=$ownerId localDate=${localDate.toIsoString()} '
        'taskCount=${tasks.length} lockedIntervalCount=${lockedExpansion.schedulingIntervals.length} '
        'windowStart=${window.start.toIso8601String()} windowEnd=${window.end.toIso8601String()}');

    return _PlanningState(
      tasks: tasks,
      window: window,
      lockedIntervals: lockedExpansion.schedulingIntervals,
    );
  }

  /// Runs the `_resolve` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  DateTime _resolve({
    required CivilDate date,
    required WallTime wallTime,
    required String timeZoneId,
  }) {
    return timeZoneResolver
        .resolve(
          date: date,
          wallTime: wallTime,
          timeZoneId: timeZoneId,
          options: resolutionOptions,
        )
        .instant;
  }

  /// Runs the `_persist` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  Future<ApplicationResult<ApplicationCommandResult>> _persist({
    required ApplicationUnitOfWorkRepositories repositories,
    required ApplicationCommandCode commandCode,
    required ApplicationOperationContext context,
    required List<Task> beforeTasks,
    required List<Task> afterTasks,
    required List<TaskActivity> activities,
    required ApplicationCommandReadHint readHint,
    SchedulingResult? schedulingResult,
    List<ProtectedFreeSlotConflict> protectedFreeSlotConflicts =
        const <ProtectedFreeSlotConflict>[],
    List<String> childTaskIds = const <String>[],
  }) async {
    final changedTasks = _changedTasks(beforeTasks, afterTasks);
    logger.debug(() =>
        'Persisting application command result. command=${commandCode.name} '
        'operationId=${context.operationId} changedTaskCount=${changedTasks.length} '
        'activityCount=${activities.length} noticeCount=${schedulingResult?.notices.length ?? 0} '
        'changeCount=${schedulingResult?.changes.length ?? 0}');
    await repositories.tasks.saveAll(changedTasks);
    await repositories.taskActivities.saveAll(activities);
    final projectStats = await _saveProjectStatistics(
      repositories: repositories,
      activities: activities,
      tasks: afterTasks,
    );

    logger.debug(() =>
        'Application command persisted. command=${commandCode.name} '
        'operationId=${context.operationId} changedTaskCount=${changedTasks.length} '
        'projectStatisticsCount=${projectStats.length}');

    return ApplicationResult.success(
      ApplicationCommandResult(
        commandCode: commandCode,
        operationId: context.operationId,
        changedTasks: changedTasks,
        activities: activities,
        projectStatistics: projectStats,
        notices: schedulingResult?.notices ?? const <SchedulingNotice>[],
        changes: schedulingResult?.changes ?? const <SchedulingChange>[],
        overlaps: schedulingResult?.overlaps ?? const <SchedulingOverlap>[],
        protectedFreeSlotConflicts: protectedFreeSlotConflicts,
        childTaskIds: childTaskIds,
        schedulingResult: schedulingResult,
        readHint: readHint,
      ),
    );
  }

  /// Runs the `_saveProjectStatistics` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  Future<List<ProjectStatistics>> _saveProjectStatistics({
    required ApplicationUnitOfWorkRepositories repositories,
    required List<TaskActivity> activities,
    required List<Task> tasks,
  }) async {
    final projectIds = activities
        .where((activity) => activity.code == TaskActivityCode.completed)
        .map((activity) => activity.projectId)
        .toSet();
    logger.finer(() => 'Saving project statistics after command. '
        'projectCount=${projectIds.length} activityCount=${activities.length}');
    final updated = <ProjectStatistics>[];

    for (final projectId in projectIds) {
      final current = await repositories.projectStatistics.findByProjectId(
            projectId,
          ) ??
          ProjectStatistics(projectId: projectId);
      final result = projectStatisticsAggregationService.apply(
        statistics: current,
        activities: activities,
        tasks: tasks,
      );
      if (result.appliedActivityIds.isEmpty) {
        continue;
      }
      logger.finer(() => 'Project statistics updated. projectId=$projectId '
          'appliedActivityCount=${result.appliedActivityIds.length}');
      await repositories.projectStatistics.save(result.statistics);
      updated.add(result.statistics);
    }

    return List<ProjectStatistics>.unmodifiable(updated);
  }
}

/// Top-level helper that performs the `_failureForRepositoryMutation` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
ApplicationFailure _failureForRepositoryMutation(RepositoryFailure failure) {
  return switch (failure.code) {
    RepositoryFailureCode.notFound => ApplicationFailure(
        code: ApplicationFailureCode.notFound,
        entityId: failure.entityId,
        detailCode: failure.code.name,
      ),
    RepositoryFailureCode.staleRevision => ApplicationFailure(
        code: ApplicationFailureCode.staleRevision,
        entityId: failure.entityId,
        detailCode: failure.code.name,
      ),
    RepositoryFailureCode.invalidRevision => ApplicationFailure(
        code: ApplicationFailureCode.validation,
        entityId: failure.entityId,
        detailCode: failure.code.name,
      ),
    RepositoryFailureCode.ownerMismatch ||
    RepositoryFailureCode.duplicateId ||
    RepositoryFailureCode.duplicateOperation =>
      ApplicationFailure(
        code: ApplicationFailureCode.conflict,
        entityId: failure.entityId,
        detailCode: failure.code.name,
      ),
  };
}
