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

part of '../scheduler_command_controller.dart';

/// Runs scheduler write commands and exposes command state to widgets.
class SchedulerCommandController extends ChangeNotifier {
  /// Creates a command controller from scheduler use cases and UI callbacks.
  SchedulerCommandController({
    required this.commands,
    required this.contextFor,
    required this.selectedDate,
    required this.refreshReads,
    required this.quickCaptureProjectId,
    this.operationIdPrefix = 'ui-command',
    SchedulerCurrentDateResolver? currentDateFor,
    DateTime Function()? now,
  }) : currentDateFor = currentDateFor ?? _utcCivilDateFor,
       now = now ?? _utcNow;

  /// Scheduler write use cases invoked by this controller.
  final V1ApplicationCommandUseCases commands;

  /// Factory for command-scoped application operation contexts.
  final OperationContextFactory contextFor;

  /// Reads the selected local date commands should operate against.
  final SchedulerSelectedDateReader selectedDate;

  /// Refresh callback invoked after commands that mutate read state.
  final ReadRefresh refreshReads;

  /// Project id used by quick-capture commands.
  final String quickCaptureProjectId;

  /// Prefix used to generate unique operation ids for scheduler commands.
  final String operationIdPrefix;

  /// Clock used by direct UI commands such as marking a task complete.
  final DateTime Function() now;

  /// Resolves command instants to the owner-local current civil date.
  final SchedulerCurrentDateResolver currentDateFor;

  /// Filler title used by the temporary timeline add-task action.
  static const _genericFlexibleTaskTitle = 'New flexible task';

  /// Filler duration used by the temporary timeline add-task action.
  static const _genericFlexibleTaskDurationMinutes = 30;

  /// Private state stored as `_sequence` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  var _sequence = 0;

  /// Private state stored as `_state` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  SchedulerCommandState _state = const SchedulerCommandIdle();

  /// Current command execution state.
  SchedulerCommandState get state => _state;

  /// Captures [title] as a backlog task.
  Future<void> quickCaptureToBacklog(String title) async {
    logger.debug(
      () =>
          'UI command requested: quick capture to backlog. '
          'projectId=$quickCaptureProjectId hasTitle=${title.trim().isNotEmpty}',
    );
    await _run(
      label: 'Capturing task',
      successMessage: 'Task captured',
      refreshAfterSuccess: true,
      action: (operationId) {
        final commandAt = now();
        return commands.quickCaptureToBacklog(
          context: contextFor(operationId, now: commandAt),
          title: title,
          projectId: quickCaptureProjectId,
        );
      },
    );
  }

  /// Adds a generic flexible task to the next available Today slot.
  Future<void> addGenericFlexibleTask() async {
    logger.debug(
      () =>
          'UI command requested: add generic flexible task. '
          'durationMinutes=$_genericFlexibleTaskDurationMinutes projectId=$quickCaptureProjectId',
    );
    await _run(
      label: 'Adding task',
      successMessage: 'Task added',
      refreshAfterSuccess: true,
      action: (operationId) {
        final commandAt = now();
        return commands.quickCaptureToNextAvailableSlot(
          context: contextFor(operationId, now: commandAt),
          localDate: selectedDate(),
          title: _genericFlexibleTaskTitle,
          durationMinutes: _genericFlexibleTaskDurationMinutes,
          projectId: quickCaptureProjectId,
        );
      },
    );
  }

  /// Schedules a backlog task into the next available slot.
  Future<void> scheduleBacklogItem({
    required String taskId,
    required int durationMinutes,
    required DateTime expectedUpdatedAt,
  }) async {
    logger.debug(
      () =>
          'UI command requested: schedule backlog item. '
          'taskId=$taskId durationMinutes=$durationMinutes '
          'expectedUpdatedAt=${expectedUpdatedAt.toIso8601String()}',
    );
    await _run(
      label: 'Scheduling task',
      successMessage: 'Task scheduled',
      refreshAfterSuccess: true,
      action: (operationId) {
        final commandAt = now();
        return commands.scheduleBacklogItemToNextAvailableSlot(
          context: contextFor(operationId, now: commandAt),
          localDate: selectedDate(),
          taskId: taskId,
          durationMinutes: durationMinutes,
          expectedUpdatedAt: expectedUpdatedAt,
        );
      },
    );
  }

  /// Completes a planned flexible task from the Today view.
  Future<void> completeFlexibleTask({
    required String taskId,
    DateTime? expectedUpdatedAt,
  }) async {
    await completeTask(
      taskId: taskId,
      taskType: TaskType.flexible,
      expectedUpdatedAt: expectedUpdatedAt,
    );
  }

  /// Completes a planned task from the Today timeline.
  Future<void> completeTask({
    required String taskId,
    required TaskType taskType,
    DateTime? expectedUpdatedAt,
  }) async {
    final completedAt = now();
    logger.debug(
      () =>
          'UI command requested: complete task. '
          'taskId=$taskId taskType=${taskType.name} '
          'hasExpectedUpdatedAt=${expectedUpdatedAt != null} '
          'occurredAt=${completedAt.toIso8601String()}',
    );
    await _run(
      label: 'Completing task',
      successMessage: 'Task completed',
      refreshAfterSuccess: true,
      action: (operationId) {
        final context = contextFor(operationId, now: completedAt);
        return switch (taskType) {
          TaskType.flexible => commands.completeFlexibleTask(
            context: context,
            localDate: selectedDate(),
            taskId: taskId,
            expectedUpdatedAt: expectedUpdatedAt,
          ),
          TaskType.critical ||
          TaskType.inflexible => commands.applyRequiredTaskAction(
            context: context,
            localDate: selectedDate(),
            taskId: taskId,
            action: RequiredTaskAction.done,
            expectedUpdatedAt: expectedUpdatedAt,
          ),
          TaskType.locked ||
          TaskType.surprise ||
          TaskType.freeSlot => Future.value(
            ApplicationResult.failure(
              ApplicationFailure(
                code: ApplicationFailureCode.validation,
                entityId: taskId,
                detailCode: 'unsupportedTaskType',
              ),
            ),
          ),
        };
      },
    );
  }

  /// Returns a completed task to its planned state.
  Future<void> uncompleteTask({
    required String taskId,
    DateTime? expectedUpdatedAt,
  }) async {
    final occurredAt = now();
    logger.debug(
      () =>
          'UI command requested: uncomplete task. '
          'taskId=$taskId hasExpectedUpdatedAt=${expectedUpdatedAt != null} '
          'occurredAt=${occurredAt.toIso8601String()}',
    );
    await _run(
      label: 'Reopening task',
      successMessage: 'Task marked as not done',
      refreshAfterSuccess: true,
      action: (operationId) {
        return commands.uncompleteTask(
          context: contextFor(operationId, now: occurredAt),
          localDate: selectedDate(),
          taskId: taskId,
          expectedUpdatedAt: expectedUpdatedAt,
        );
      },
    );
  }

  /// Pushes a past flexible task to the next available slot after now.
  Future<void> pushTaskToNextAvailableSlot({
    required String taskId,
    DateTime? expectedUpdatedAt,
  }) async {
    final commandAt = now();
    logger.debug(
      () =>
          'UI command requested: push task to next available slot. '
          'taskId=$taskId hasExpectedUpdatedAt=${expectedUpdatedAt != null} '
          'occurredAt=${commandAt.toIso8601String()}',
    );
    await _run(
      label: 'Pushing task',
      successMessage: 'Task pushed',
      refreshAfterSuccess: true,
      action: (operationId) {
        return commands.pushFlexibleToNextAvailableSlot(
          context: contextFor(operationId, now: commandAt),
          localDate: currentDateFor(commandAt),
          taskId: taskId,
          expectedUpdatedAt: expectedUpdatedAt,
        );
      },
    );
  }

  /// Pushes a past flexible task to the first available slot tomorrow.
  Future<void> pushTaskToTomorrow({
    required String taskId,
    DateTime? expectedUpdatedAt,
  }) async {
    final commandAt = now();
    logger.debug(
      () =>
          'UI command requested: push task to tomorrow. '
          'taskId=$taskId hasExpectedUpdatedAt=${expectedUpdatedAt != null} '
          'occurredAt=${commandAt.toIso8601String()}',
    );
    await _run(
      label: 'Pushing task',
      successMessage: 'Task moved to tomorrow',
      refreshAfterSuccess: true,
      action: (operationId) {
        return commands.pushFlexibleToTomorrowTopOfQueue(
          context: contextFor(operationId, now: commandAt),
          targetDate: currentDateFor(commandAt).addDays(1),
          taskId: taskId,
          expectedUpdatedAt: expectedUpdatedAt,
        );
      },
    );
  }

  /// Pushes a past flexible task back to Backlog.
  Future<void> pushTaskToBacklog({
    required String taskId,
    DateTime? expectedUpdatedAt,
  }) async {
    final commandAt = now();
    logger.debug(
      () =>
          'UI command requested: push task to backlog. '
          'taskId=$taskId hasExpectedUpdatedAt=${expectedUpdatedAt != null} '
          'occurredAt=${commandAt.toIso8601String()}',
    );
    await _run(
      label: 'Moving task to backlog',
      successMessage: 'Task moved to backlog',
      refreshAfterSuccess: true,
      action: (operationId) {
        return commands.moveFlexibleToBacklog(
          context: contextFor(operationId, now: commandAt),
          taskId: taskId,
          expectedUpdatedAt: expectedUpdatedAt,
        );
      },
    );
  }

  /// Permanently removes a task from persistence.
  Future<void> removeTask({
    required String taskId,
    DateTime? expectedUpdatedAt,
  }) async {
    final commandAt = now();
    logger.debug(
      () =>
          'UI command requested: remove task. '
          'taskId=$taskId hasExpectedUpdatedAt=${expectedUpdatedAt != null} '
          'occurredAt=${commandAt.toIso8601String()}',
    );
    await _run(
      label: 'Removing task',
      successMessage: 'Task removed',
      refreshAfterSuccess: true,
      action: (operationId) {
        return commands.removeTask(
          context: contextFor(operationId, now: commandAt),
          taskId: taskId,
          expectedUpdatedAt: expectedUpdatedAt,
        );
      },
    );
  }

  /// Runs the `_run` 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<void> _run({
    required String label,
    required String successMessage,
    required bool refreshAfterSuccess,
    required Future<ApplicationResult<ApplicationCommandResult>> Function(
      String operationId,
    )
    action,
  }) async {
    final operationId = _nextOperationId();
    logger.debug(
      () => 'Scheduler command started. operationId=$operationId label=$label',
    );
    _setState(SchedulerCommandRunning(label));
    try {
      final result = await action(operationId);
      final failure = result.failure;
      if (failure != null) {
        logger.warn(
          () =>
              'Scheduler command returned failure. '
              'operationId=$operationId code=${failure.code.name} '
              'detailCode=${failure.detailCode} entityId=${failure.entityId}',
        );
        _setState(SchedulerCommandFailure(failure: failure));
        return;
      }
      logger.debug(
        () =>
            'Scheduler command succeeded. operationId=$operationId '
            'refreshAfterSuccess=$refreshAfterSuccess',
      );
      if (refreshAfterSuccess) {
        logger.finer(
          () =>
              'Refreshing reads after scheduler command. '
              'operationId=$operationId',
        );
        await refreshReads();
      }
      _setState(SchedulerCommandSuccess(successMessage));
    } on Object catch (error, stackTrace) {
      logger.error(
        () =>
            'Scheduler command threw unexpectedly. operationId=$operationId label=$label',
        error: error,
        stackTrace: stackTrace,
      );
      _setState(SchedulerCommandFailure(error: error));
    }
  }

  /// Runs the `_nextOperationId` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  String _nextOperationId() {
    _sequence += 1;
    return '$operationIdPrefix-$_sequence';
  }

  /// Runs the `_setState` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  void _setState(SchedulerCommandState next) {
    _state = next;
    notifyListeners();
  }

  /// Runs the `_utcNow` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  static DateTime _utcNow() => DateTime.now().toUtc();

  /// Runs the `_utcCivilDateFor` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  static CivilDate _utcCivilDateFor(DateTime instant) {
    final utc = instant.toUtc();
    return CivilDate(utc.year, utc.month, utc.day);
  }
}
