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

part of '../../repositories.dart';

/// In-memory task repository useful for tests and early application wiring.
class InMemoryTaskRepository implements TaskRepository {
  /// Creates a `InMemoryTaskRepository` 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.
  InMemoryTaskRepository([
    Iterable<Task> initialTasks = const [],
    this.defaultOwnerId = 'owner-1',
  ])  : _tasksById = {
          for (final task in initialTasks) task.id: task,
        },
        _ownersById = {
          for (final task in initialTasks) task.id: defaultOwnerId,
        },
        _revisionsById = {
          for (final task in initialTasks) task.id: 1,
        };

  /// Private state stored as `_tasksById` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  final Map<String, Task> _tasksById;

  /// Private state stored as `_ownersById` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  final Map<String, String> _ownersById;

  /// Private state stored as `_revisionsById` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  final Map<String, int> _revisionsById;

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

  /// Runs the `findById` 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.
  @override
  Future<Task?> findById(String id) async {
    return _tasksById[id];
  }

  /// Runs the `findRecordById` 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.
  @override
  Future<RepositoryRecord<Task>?> findRecordById(String id) async {
    final task = _tasksById[id];
    if (task == null) {
      return null;
    }
    return RepositoryRecord(
      value: task,
      ownerId: _ownerFor(id),
      revision: _revisionFor(id),
    );
  }

  /// Runs the `findAll` 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.
  @override
  Future<List<Task>> findAll() async {
    return List<Task>.unmodifiable(_tasksById.values);
  }

  /// Runs the `findByStatus` 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.
  @override
  Future<List<Task>> findByStatus(TaskStatus status) async {
    return List<Task>.unmodifiable(
      _tasksById.values.where((task) => task.status == status),
    );
  }

  /// Runs the `findByOwner` 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.
  @override
  Future<RepositoryPage<Task>> findByOwner({
    required String ownerId,
    RepositoryPageRequest page = const RepositoryPageRequest(),
  }) async {
    final tasks = _tasksById.values
        .where((task) => _ownerFor(task.id) == ownerId)
        .toList(growable: false)
      ..sort(_compareTaskIds);
    return _page(tasks, page);
  }

  /// Runs the `findByProjectId` 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.
  @override
  Future<RepositoryPage<Task>> findByProjectId({
    required String ownerId,
    required String projectId,
    RepositoryPageRequest page = const RepositoryPageRequest(),
  }) async {
    final tasks = _tasksById.values
        .where(
          (task) =>
              _ownerFor(task.id) == ownerId && task.projectId == projectId,
        )
        .toList(growable: false)
      ..sort(_compareTaskIds);
    return _page(tasks, page);
  }

  /// Runs the `findByParentTaskId` 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.
  @override
  Future<RepositoryPage<Task>> findByParentTaskId({
    required String ownerId,
    required String parentTaskId,
    RepositoryPageRequest page = const RepositoryPageRequest(),
  }) async {
    final tasks = _tasksById.values
        .where(
          (task) =>
              _ownerFor(task.id) == ownerId &&
              task.parentTaskId == parentTaskId,
        )
        .toList(growable: false)
      ..sort(_compareTaskIds);
    return _page(tasks, page);
  }

  /// Runs the `findBacklogCandidates` 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.
  @override
  Future<RepositoryPage<Task>> findBacklogCandidates(
    BacklogCandidateQuery query,
  ) async {
    final tasks = _tasksById.values
        .where(
          (task) =>
              _ownerFor(task.id) == query.ownerId &&
              task.status == TaskStatus.backlog &&
              (query.projectId == null || task.projectId == query.projectId) &&
              query.tags.every(task.backlogTags.contains),
        )
        .toList(growable: false)
      ..sort(_compareBacklogCandidates);
    return _page(tasks, query.page);
  }

  /// Runs the `findScheduledInWindow` 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.
  @override
  Future<List<Task>> findScheduledInWindow(SchedulingWindow window) async {
    return List<Task>.unmodifiable(
      _tasksById.values.where((task) {
        return _taskOverlapsWindow(task, window);
      }),
    );
  }

  /// Runs the `findScheduledInWindowForOwner` 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.
  @override
  Future<RepositoryPage<Task>> findScheduledInWindowForOwner({
    required String ownerId,
    required SchedulingWindow window,
    RepositoryPageRequest page = const RepositoryPageRequest(),
  }) async {
    final tasks = _tasksById.values
        .where(
          (task) =>
              _ownerFor(task.id) == ownerId &&
              _taskOverlapsWindow(
                task,
                window,
              ),
        )
        .toList(growable: false)
      ..sort(_compareScheduledTasks);
    return _page(tasks, page);
  }

  /// Runs the `findForLocalDay` 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.
  @override
  Future<RepositoryPage<Task>> findForLocalDay({
    required String ownerId,
    required CivilDate localDate,
    required SchedulingWindow window,
    RepositoryPageRequest page = const RepositoryPageRequest(),
  }) {
    return findScheduledInWindowForOwner(
      ownerId: ownerId,
      window: window,
      page: page,
    );
  }

  /// Runs the `save` 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.
  @override
  Future<void> save(Task task) async {
    _tasksById[task.id] = task;
    _ownersById.putIfAbsent(task.id, () => defaultOwnerId);
    _revisionsById[task.id] = _revisionFor(task.id) + 1;
  }

  /// Runs the `saveAll` 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.
  @override
  Future<void> saveAll(Iterable<Task> tasks) async {
    for (final task in tasks) {
      await save(task);
    }
  }

  /// Runs the `insert` 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.
  @override
  Future<RepositoryMutationResult> insert({
    required Task task,
    required String ownerId,
  }) async {
    if (_tasksById.containsKey(task.id)) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.duplicateId,
          entityId: task.id,
        ),
      );
    }
    _tasksById[task.id] = task;
    _ownersById[task.id] = ownerId;
    _revisionsById[task.id] = 1;
    return RepositoryMutationResult.success(revision: 1);
  }

  /// Runs the `saveIfRevision` 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.
  @override
  Future<RepositoryMutationResult> saveIfRevision({
    required Task task,
    required String ownerId,
    required int expectedRevision,
  }) async {
    if (expectedRevision <= 0) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.invalidRevision,
          entityId: task.id,
          expectedRevision: expectedRevision,
        ),
      );
    }
    final current = _tasksById[task.id];
    if (current == null) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.notFound,
          entityId: task.id,
        ),
      );
    }
    if (_ownerFor(task.id) != ownerId) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.ownerMismatch,
          entityId: task.id,
        ),
      );
    }
    final actualRevision = _revisionFor(task.id);
    if (actualRevision != expectedRevision) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.staleRevision,
          entityId: task.id,
          expectedRevision: expectedRevision,
          actualRevision: actualRevision,
        ),
      );
    }
    final nextRevision = actualRevision + 1;
    _tasksById[task.id] = task;
    _ownersById[task.id] = ownerId;
    _revisionsById[task.id] = nextRevision;
    return RepositoryMutationResult.success(revision: nextRevision);
  }

  /// Runs the `deleteIfRevision` 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.
  @override
  Future<RepositoryMutationResult> deleteIfRevision({
    required String taskId,
    required String ownerId,
    required int expectedRevision,
  }) async {
    if (expectedRevision <= 0) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.invalidRevision,
          entityId: taskId,
          expectedRevision: expectedRevision,
        ),
      );
    }
    final current = _tasksById[taskId];
    if (current == null) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.notFound,
          entityId: taskId,
        ),
      );
    }
    if (_ownerFor(taskId) != ownerId) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.ownerMismatch,
          entityId: taskId,
        ),
      );
    }
    final actualRevision = _revisionFor(taskId);
    if (actualRevision != expectedRevision) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.staleRevision,
          entityId: taskId,
          expectedRevision: expectedRevision,
          actualRevision: actualRevision,
        ),
      );
    }
    final nextRevision = actualRevision + 1;
    _tasksById.remove(taskId);
    _ownersById.remove(taskId);
    _revisionsById.remove(taskId);
    return RepositoryMutationResult.success(revision: nextRevision);
  }

  /// Runs the `_ownerFor` 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 _ownerFor(String id) => _ownersById[id] ?? defaultOwnerId;

  /// Runs the `_revisionFor` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  int _revisionFor(String id) => _revisionsById[id] ?? 1;
}
