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

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

/// Private implementation type for `_PlanningState` in this library.
/// It keeps supporting state and behavior scoped to the owning file so the public API stays focused on the scheduler concepts that callers use directly.
class _PlanningState {
  /// Creates a `_PlanningState` 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.
  _PlanningState({
    required List<Task> tasks,
    required this.window,
    required List<TimeInterval> lockedIntervals,
  })  : tasks = List<Task>.unmodifiable(tasks),
        lockedIntervals = List<TimeInterval>.unmodifiable(lockedIntervals);

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

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

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

  /// Returns the derived `input` value for this object.
  /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated.
  SchedulingInput get input {
    return SchedulingInput(
      tasks: tasks,
      window: window,
      lockedIntervals: lockedIntervals,
    );
  }

  /// Performs the `withTask` behavior for this scheduler component.
  /// The method provides a named entry point for this operation so callers do not need to know the lower-level state or persistence steps involved.
  _PlanningState withTask(Task task) {
    if (tasks.any((existing) => existing.id == task.id)) {
      return this;
    }
    return _PlanningState(
      tasks: [task, ...tasks],
      window: window,
      lockedIntervals: lockedIntervals,
    );
  }

  /// Performs the `replaceTask` behavior for this scheduler component.
  /// The method provides a named entry point for this operation so callers do not need to know the lower-level state or persistence steps involved.
  _PlanningState replaceTask(Task replacement) {
    return _PlanningState(
      tasks: _replaceTask(tasks, replacement),
      window: window,
      lockedIntervals: lockedIntervals,
    );
  }
}

/// Top-level helper that performs the `_failure` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
ApplicationResult<ApplicationCommandResult> _failure(
  ApplicationFailureCode code, {
  String? entityId,
  String? detailCode,
}) {
  return ApplicationResult.failure(
    ApplicationFailure(
      code: code,
      entityId: entityId,
      detailCode: detailCode,
    ),
  );
}

/// Top-level helper that performs the `_failureForQuickCapture` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
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,
  );
}

/// Top-level helper that performs the `_failureForSchedulingResult` 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? _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,
      ),
  };
}

/// Top-level helper that performs the `_staleFailure` 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? _staleFailure(Task task, DateTime? expectedUpdatedAt) {
  if (expectedUpdatedAt == null ||
      _sameNullableInstant(task.updatedAt, expectedUpdatedAt)) {
    return null;
  }

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

/// Top-level helper that performs the `_activitiesForSchedulingChanges` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
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);
}

/// Top-level helper that performs the `_nextStatusForChange` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
TaskStatus _nextStatusForChange(Task task, TaskActivityCode code) {
  if (code == TaskActivityCode.restoredFromBacklog) {
    return TaskStatus.planned;
  }
  if (code == TaskActivityCode.movedToBacklog) {
    return TaskStatus.backlog;
  }

  return task.status;
}

/// Top-level helper that performs the `_changedTasks` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
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);
}

/// Top-level helper that performs the `_changedTaskIds` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
List<String> _changedTaskIds(List<Task> beforeTasks, List<Task> afterTasks) {
  return _taskIdsFromTasks(_changedTasks(beforeTasks, afterTasks));
}

/// Top-level helper that performs the `_taskChanged` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
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;
}

/// Top-level helper that performs the `_sameNullableInstant` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
bool _sameNullableInstant(DateTime? first, DateTime? second) {
  if (first == null || second == null) {
    return first == null && second == null;
  }

  return first.isAtSameMomentAs(second);
}

/// Top-level helper that performs the `_taskById` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
Task? _taskById(List<Task> tasks, String taskId) {
  for (final task in tasks) {
    if (task.id == taskId) {
      return task;
    }
  }

  return null;
}

/// Top-level helper that performs the `_replaceTask` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
List<Task> _replaceTask(List<Task> tasks, Task replacement) {
  return tasks
      .map((task) => task.id == replacement.id ? replacement : task)
      .toList(growable: false);
}

/// Top-level helper that performs the `_taskIdsFromChanges` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
List<String> _taskIdsFromChanges(List<SchedulingChange> changes) {
  return List<String>.unmodifiable(
    changes.map((change) => change.taskId).toSet(),
  );
}

/// Top-level helper that performs the `_taskIdsFromTasks` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
List<String> _taskIdsFromTasks(List<Task> tasks) {
  return List<String>.unmodifiable(tasks.map((task) => task.id).toSet());
}

/// Top-level helper that performs the `_requiredTrimmed` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
String _requiredTrimmed(String value, String name) {
  final trimmed = value.trim();
  if (trimmed.isEmpty) {
    throw ArgumentError.value(value, name, 'Value is required.');
  }
  return trimmed;
}
