// Atomic V1 application commands.
//
// These use cases are the UI-facing mutation boundary. They load authoritative
// repository state, invoke pure domain services, and persist all resulting task,
// activity, project-statistic, and operation-record changes in one unit of work.

import 'application_layer.dart';
import 'child_tasks.dart';
import 'free_slots.dart';
import 'locked_time.dart';
import 'models.dart';
import 'project_statistics.dart';
import 'quick_capture.dart';
import 'scheduling_engine.dart';
import 'task_actions.dart';
import 'task_lifecycle.dart';
import 'time_contracts.dart';

/// Stable V1 application command identifiers.
enum ApplicationCommandCode {
  quickCaptureToBacklog,
  quickCaptureToNextAvailableSlot,
  scheduleBacklogItemToNextAvailableSlot,
  pushFlexibleToNextAvailableSlot,
  pushFlexibleToTomorrowTopOfQueue,
  moveFlexibleToBacklog,
  completeFlexibleTask,
  applyRequiredTaskAction,
  logSurpriseTask,
  createProtectedFreeSlot,
  updateProtectedFreeSlot,
  removeProtectedFreeSlot,
  breakUpTask,
  completeChildTask,
  completeParentTask,
  completeParentFromChild,
}

/// Draft row for creating one child task from an application command.
class ApplicationChildTaskDraft {
  const ApplicationChildTaskDraft({
    required this.title,
    this.priority,
    this.reward = RewardLevel.notSet,
    this.difficulty = DifficultyLevel.notSet,
    this.durationMinutes,
    this.projectId,
  });

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

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

  /// Optional reward estimate.
  final RewardLevel reward;

  /// Optional difficulty estimate.
  final DifficultyLevel difficulty;

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

  /// Optional project override. Null inherits from the parent.
  final String? projectId;
}

/// Hint for read models that should be refreshed after a command.
class ApplicationCommandReadHint {
  ApplicationCommandReadHint({
    CivilDate? localDate,
    Iterable<CivilDate> affectedDates = const <CivilDate>[],
    Iterable<String> affectedTaskIds = const <String>[],
    this.refreshToday = true,
  })  : localDate = localDate,
        affectedDates = List<CivilDate>.unmodifiable(
          {
            if (localDate != null) localDate,
            ...affectedDates,
          },
        ),
        affectedTaskIds = List<String>.unmodifiable(affectedTaskIds);

  /// Primary local date for a Today refresh, when known.
  final CivilDate? localDate;

  /// All local dates touched by the command.
  final List<CivilDate> affectedDates;

  /// Task ids changed or created by the command.
  final List<String> affectedTaskIds;

  /// Whether Today-style read models should be refreshed.
  final bool refreshToday;
}

/// Structured result returned by V1 application commands.
class ApplicationCommandResult {
  ApplicationCommandResult({
    required this.commandCode,
    required String operationId,
    required List<Task> changedTasks,
    required this.readHint,
    List<TaskActivity> activities = const <TaskActivity>[],
    List<ProjectStatistics> projectStatistics = const <ProjectStatistics>[],
    List<SchedulingNotice> notices = const <SchedulingNotice>[],
    List<SchedulingChange> changes = const <SchedulingChange>[],
    List<SchedulingOverlap> overlaps = const <SchedulingOverlap>[],
    List<ProtectedFreeSlotConflict> protectedFreeSlotConflicts =
        const <ProtectedFreeSlotConflict>[],
    List<String> childTaskIds = const <String>[],
    this.schedulingResult,
  })  : operationId = _requiredTrimmed(operationId, 'operationId'),
        changedTasks = List<Task>.unmodifiable(changedTasks),
        activities = List<TaskActivity>.unmodifiable(activities),
        projectStatistics = List<ProjectStatistics>.unmodifiable(
          projectStatistics,
        ),
        notices = List<SchedulingNotice>.unmodifiable(notices),
        changes = List<SchedulingChange>.unmodifiable(changes),
        overlaps = List<SchedulingOverlap>.unmodifiable(overlaps),
        protectedFreeSlotConflicts =
            List<ProtectedFreeSlotConflict>.unmodifiable(
          protectedFreeSlotConflicts,
        ),
        childTaskIds = List<String>.unmodifiable(childTaskIds);

  /// Command that produced this result.
  final ApplicationCommandCode commandCode;

  /// Exactly-once operation id.
  final String operationId;

  /// Tasks inserted or changed by the command.
  final List<Task> changedTasks;

  /// New internal activities persisted with the command.
  final List<TaskActivity> activities;

  /// Project aggregates updated by completion activities.
  final List<ProjectStatistics> projectStatistics;

  /// Scheduling notices returned by domain services.
  final List<SchedulingNotice> notices;

  /// Scheduling movement records returned by domain services.
  final List<SchedulingChange> changes;

  /// Scheduling overlaps returned by domain services.
  final List<SchedulingOverlap> overlaps;

  /// Protected rest conflicts caused by an explicit required placement.
  final List<ProtectedFreeSlotConflict> protectedFreeSlotConflicts;

  /// Child task ids created or affected by child-task commands.
  final List<String> childTaskIds;

  /// Full scheduling result when the command invoked the scheduler.
  final SchedulingResult? schedulingResult;

  /// Read-model refresh hint for UI/state management.
  final ApplicationCommandReadHint readHint;
}

/// UI-facing V1 command use cases.
class V1ApplicationCommandUseCases {
  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;

  final QuickCaptureService quickCaptureService;
  final FlexibleTaskActionService flexibleTaskActionService;
  final RequiredTaskActionService requiredTaskActionService;
  final SurpriseTaskLogService surpriseTaskLogService;
  final FreeSlotService freeSlotService;
  final ChildTaskBreakUpService childTaskBreakUpService;
  final ChildTaskCompletionService childTaskCompletionService;
  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 {
        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 {
        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,
    DateTime? expectedUpdatedAt,
  }) {
    const commandCode =
        ApplicationCommandCode.scheduleBacklogItemToNextAvailableSlot;
    return applicationStore.run<ApplicationCommandResult>(
      context: context,
      operationName: commandCode.name,
      action: (repositories) async {
        var state = await _loadPlanningState(repositories, context, localDate);
        var task = _taskById(state.tasks, taskId);
        if (task == null) {
          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 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 {
        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,
          ),
        );
      },
    );
  }

  /// 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 {
        var state = await _loadPlanningState(repositories, context, localDate);
        var task = _taskById(state.tasks, taskId);
        if (task == null) {
          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],
          ),
        );
      },
    );
  }

  /// 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 {
        var state = await _loadPlanningState(repositories, context, localDate);
        var task = _taskById(state.tasks, taskId);
        if (task == null) {
          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 {
        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 {
        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 {
        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 {
        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 {
        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,
      ),
    );
  }

  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 {
        var state = await _loadPlanningState(repositories, context, localDate);
        var task = _taskById(state.tasks, taskId);
        if (task == null) {
          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,
          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),
          ),
        );
      },
    );
  }

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

  Future<_PlanningState> _loadPlanningState(
    ApplicationUnitOfWorkRepositories repositories,
    ApplicationOperationContext context,
    CivilDate localDate,
  ) async {
    final ownerId = context.ownerTimeZone.ownerId;
    final settings = await repositories.ownerSettings.findByOwnerId(ownerId) ??
        OwnerSettings(
          ownerId: ownerId,
          timeZoneId: context.ownerTimeZone.timeZoneId,
        );
    final dayStart = _resolve(
      date: localDate,
      wallTime: settings.dayStart,
      timeZoneId: settings.timeZoneId,
    );
    final dayEnd = _resolve(
      date: localDate,
      wallTime: settings.dayEnd,
      timeZoneId: settings.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: settings.timeZoneId,
      timeZoneResolver: timeZoneResolver,
      resolutionOptions: resolutionOptions,
    );

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

  DateTime _resolve({
    required CivilDate date,
    required WallTime wallTime,
    required String timeZoneId,
  }) {
    return timeZoneResolver
        .resolve(
          date: date,
          wallTime: wallTime,
          timeZoneId: timeZoneId,
          options: resolutionOptions,
        )
        .instant;
  }

  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);
    await repositories.tasks.saveAll(changedTasks);
    await repositories.taskActivities.saveAll(activities);
    final projectStats = await _saveProjectStatistics(
      repositories: repositories,
      activities: activities,
      tasks: afterTasks,
    );

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

  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();
    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;
      }
      await repositories.projectStatistics.save(result.statistics);
      updated.add(result.statistics);
    }

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

class _PlanningState {
  _PlanningState({
    required List<Task> tasks,
    required this.window,
    required List<TimeInterval> lockedIntervals,
  })  : tasks = List<Task>.unmodifiable(tasks),
        lockedIntervals = List<TimeInterval>.unmodifiable(lockedIntervals);

  final List<Task> tasks;
  final SchedulingWindow window;
  final List<TimeInterval> lockedIntervals;

  SchedulingInput get input {
    return SchedulingInput(
      tasks: tasks,
      window: window,
      lockedIntervals: lockedIntervals,
    );
  }

  _PlanningState withTask(Task task) {
    if (tasks.any((existing) => existing.id == task.id)) {
      return this;
    }
    return _PlanningState(
      tasks: [task, ...tasks],
      window: window,
      lockedIntervals: lockedIntervals,
    );
  }
}

ApplicationResult<ApplicationCommandResult> _failure(
  ApplicationFailureCode code, {
  String? entityId,
  String? detailCode,
}) {
  return ApplicationResult.failure(
    ApplicationFailure(
      code: code,
      entityId: entityId,
      detailCode: detailCode,
    ),
  );
}

ApplicationResult<ApplicationCommandResult> _failureForQuickCapture(
  QuickCaptureResult result,
) {
  final schedulingResult = result.schedulingResult;
  if (schedulingResult != null) {
    final failure = _failureForSchedulingResult(
      schedulingResult,
      entityId: result.task.id,
    );
    if (failure != null) {
      return ApplicationResult.failure(failure);
    }
  }

  return _failure(
    ApplicationFailureCode.validation,
    entityId: result.task.id,
    detailCode: result.status.name,
  );
}

ApplicationFailure? _failureForSchedulingResult(
  SchedulingResult result, {
  required String entityId,
}) {
  return switch (result.outcomeCode) {
    SchedulingOutcomeCode.success || SchedulingOutcomeCode.noOp => null,
    SchedulingOutcomeCode.conflict => null,
    SchedulingOutcomeCode.notFound => ApplicationFailure(
        code: ApplicationFailureCode.notFound,
        entityId: entityId,
        detailCode: result.outcomeCode.name,
      ),
    SchedulingOutcomeCode.noSlot ||
    SchedulingOutcomeCode.overflow =>
      ApplicationFailure(
        code: ApplicationFailureCode.noSlot,
        entityId: entityId,
        detailCode: result.outcomeCode.name,
      ),
    SchedulingOutcomeCode.invalidState => ApplicationFailure(
        code: ApplicationFailureCode.conflict,
        entityId: entityId,
        detailCode: result.outcomeCode.name,
      ),
  };
}

ApplicationFailure? _staleFailure(Task task, DateTime? expectedUpdatedAt) {
  if (expectedUpdatedAt == null ||
      _sameNullableInstant(task.updatedAt, expectedUpdatedAt)) {
    return null;
  }

  return ApplicationFailure(
    code: ApplicationFailureCode.staleRevision,
    entityId: task.id,
  );
}

List<TaskActivity> _activitiesForSchedulingChanges({
  required SchedulingResult? result,
  required List<Task> beforeTasks,
  required String operationId,
  required DateTime occurredAt,
  required String selectedTaskId,
  required TaskActivityCode selectedActivityCode,
  required TaskActivityCode movedActivityCode,
}) {
  if (result == null || result.outcomeCode != SchedulingOutcomeCode.success) {
    return const <TaskActivity>[];
  }

  final activities = <TaskActivity>[];
  for (final change in result.changes) {
    final original = _taskById(beforeTasks, change.taskId);
    if (original == null) {
      continue;
    }
    final code = change.taskId == selectedTaskId
        ? selectedActivityCode
        : movedActivityCode;
    activities.add(
      TaskActivity(
        id: '$operationId:${change.taskId}:${code.name}',
        operationId: operationId,
        code: code,
        taskId: change.taskId,
        projectId: original.projectId,
        occurredAt: occurredAt,
        metadata: {
          'previousStatus': original.status.name,
          'nextStatus': _nextStatusForChange(original, code).name,
          'previousStart': change.previousStart,
          'previousEnd': change.previousEnd,
          'nextStart': change.nextStart,
          'nextEnd': change.nextEnd,
        },
      ),
    );
  }

  return List<TaskActivity>.unmodifiable(activities);
}

TaskStatus _nextStatusForChange(Task task, TaskActivityCode code) {
  if (code == TaskActivityCode.restoredFromBacklog) {
    return TaskStatus.planned;
  }
  if (code == TaskActivityCode.movedToBacklog) {
    return TaskStatus.backlog;
  }

  return task.status;
}

List<Task> _changedTasks(List<Task> beforeTasks, List<Task> afterTasks) {
  final beforeById = {
    for (final task in beforeTasks) task.id: task,
  };
  final changed = <Task>[];

  for (final task in afterTasks) {
    final before = beforeById[task.id];
    if (before == null || _taskChanged(before, task)) {
      changed.add(task);
    }
  }

  return List<Task>.unmodifiable(changed);
}

List<String> _changedTaskIds(List<Task> beforeTasks, List<Task> afterTasks) {
  return _taskIdsFromTasks(_changedTasks(beforeTasks, afterTasks));
}

bool _taskChanged(Task before, Task after) {
  return before.title != after.title ||
      before.projectId != after.projectId ||
      before.type != after.type ||
      before.status != after.status ||
      before.priority != after.priority ||
      before.reward != after.reward ||
      before.difficulty != after.difficulty ||
      before.durationMinutes != after.durationMinutes ||
      !_sameNullableInstant(before.scheduledStart, after.scheduledStart) ||
      !_sameNullableInstant(before.scheduledEnd, after.scheduledEnd) ||
      !_sameNullableInstant(before.actualStart, after.actualStart) ||
      !_sameNullableInstant(before.actualEnd, after.actualEnd) ||
      !_sameNullableInstant(before.completedAt, after.completedAt) ||
      before.parentTaskId != after.parentTaskId ||
      !_sameNullableInstant(before.updatedAt, after.updatedAt) ||
      before.stats != after.stats;
}

bool _sameNullableInstant(DateTime? first, DateTime? second) {
  if (first == null || second == null) {
    return first == null && second == null;
  }

  return first.isAtSameMomentAs(second);
}

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

List<String> _taskIdsFromChanges(List<SchedulingChange> changes) {
  return List<String>.unmodifiable(
    changes.map((change) => change.taskId).toSet(),
  );
}

List<String> _taskIdsFromTasks(List<Task> tasks) {
  return List<String>.unmodifiable(tasks.map((task) => task.id).toSet());
}

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