// Persistence boundary interfaces for the scheduling core.
//
// These interfaces are intentionally database-client-free. A future MongoDB
// adapter can implement them with document storage while the domain layer and
// tests keep using the same pure Dart contracts.

import 'locked_time.dart';
import 'models.dart';
import 'scheduling_engine.dart';
import 'time_contracts.dart';

/// Stable repository mutation failure categories.
enum RepositoryFailureCode {
  notFound,
  staleRevision,
  invalidRevision,
  ownerMismatch,
  duplicateId,
  duplicateOperation,
}

/// Typed repository failure for optimistic writes and uniqueness checks.
class RepositoryFailure {
  const RepositoryFailure({
    required this.code,
    this.entityId,
    this.expectedRevision,
    this.actualRevision,
  });

  final RepositoryFailureCode code;
  final String? entityId;
  final int? expectedRevision;
  final int? actualRevision;
}

/// Mutation result that avoids throwing for expected repository conflicts.
class RepositoryMutationResult {
  const RepositoryMutationResult._({
    this.revision,
    this.failure,
  });

  factory RepositoryMutationResult.success({required int revision}) {
    return RepositoryMutationResult._(revision: revision);
  }

  factory RepositoryMutationResult.failure(RepositoryFailure failure) {
    return RepositoryMutationResult._(failure: failure);
  }

  final int? revision;
  final RepositoryFailure? failure;

  bool get isSuccess => failure == null;
}

/// Repository value plus adapter-neutral metadata.
class RepositoryRecord<T> {
  const RepositoryRecord({
    required this.value,
    required this.ownerId,
    required this.revision,
    this.archivedAt,
  });

  final T value;
  final String ownerId;
  final int revision;
  final DateTime? archivedAt;

  bool get isArchived => archivedAt != null;
}

/// Cursor contract for bounded repository queries.
class RepositoryPageRequest {
  const RepositoryPageRequest({
    this.cursor,
    this.limit = 100,
  }) : assert(limit > 0);

  /// Adapter-neutral cursor. In-memory repositories use an integer offset.
  final String? cursor;

  /// Maximum number of records to return.
  final int limit;
}

/// One bounded page of repository query results.
class RepositoryPage<T> {
  const RepositoryPage({
    required this.items,
    this.nextCursor,
  });

  final List<T> items;
  final String? nextCursor;

  bool get hasMore => nextCursor != null;
}

/// Status/project filters for backlog candidate queries.
class BacklogCandidateQuery {
  const BacklogCandidateQuery({
    required this.ownerId,
    this.projectId,
    this.tags = const <BacklogTag>{},
    this.page = const RepositoryPageRequest(),
  });

  final String ownerId;
  final String? projectId;
  final Set<BacklogTag> tags;
  final RepositoryPageRequest page;
}

/// Repository contract for task documents.
abstract interface class TaskRepository {
  /// Return a task by stable id, or null when it does not exist.
  Future<Task?> findById(String id);

  /// Return a task plus owner/revision metadata.
  Future<RepositoryRecord<Task>?> findRecordById(String id);

  /// Return all tasks currently known to the repository.
  Future<List<Task>> findAll();

  /// Return tasks with a lifecycle status.
  Future<List<Task>> findByStatus(TaskStatus status);

  /// Return owner-scoped tasks in deterministic id order.
  Future<RepositoryPage<Task>> findByOwner({
    required String ownerId,
    RepositoryPageRequest page = const RepositoryPageRequest(),
  });

  /// Return tasks assigned to [projectId].
  Future<RepositoryPage<Task>> findByProjectId({
    required String ownerId,
    required String projectId,
    RepositoryPageRequest page = const RepositoryPageRequest(),
  });

  /// Return direct children owned by [parentTaskId].
  Future<RepositoryPage<Task>> findByParentTaskId({
    required String ownerId,
    required String parentTaskId,
    RepositoryPageRequest page = const RepositoryPageRequest(),
  });

  /// Return backlog candidates ordered by backlog age, then id.
  Future<RepositoryPage<Task>> findBacklogCandidates(
    BacklogCandidateQuery query,
  );

  /// Return tasks that overlap [window] by scheduled start/end.
  Future<List<Task>> findScheduledInWindow(SchedulingWindow window);

  /// Return owner-scoped scheduled tasks that overlap [window].
  Future<RepositoryPage<Task>> findScheduledInWindowForOwner({
    required String ownerId,
    required SchedulingWindow window,
    RepositoryPageRequest page = const RepositoryPageRequest(),
  });

  /// Return tasks in a local-day window. Civil date is supplied for adapters
  /// that store date-derived keys; in-memory filtering uses [window].
  Future<RepositoryPage<Task>> findForLocalDay({
    required String ownerId,
    required CivilDate localDate,
    required SchedulingWindow window,
    RepositoryPageRequest page = const RepositoryPageRequest(),
  });

  /// Save or replace [task] by stable id.
  Future<void> save(Task task);

  /// Save or replace every task by stable id.
  Future<void> saveAll(Iterable<Task> tasks);

  /// Insert [task] if its id is unused.
  Future<RepositoryMutationResult> insert({
    required Task task,
    required String ownerId,
  });

  /// Compare-and-set save by expected repository revision.
  Future<RepositoryMutationResult> saveIfRevision({
    required Task task,
    required String ownerId,
    required int expectedRevision,
  });
}

/// Repository contract for project profile documents.
abstract interface class ProjectRepository {
  /// Return a project by stable id, or null when it does not exist.
  Future<ProjectProfile?> findById(String id);

  /// Return a project plus owner/revision metadata.
  Future<RepositoryRecord<ProjectProfile>?> findRecordById(String id);

  /// Return all projects currently known to the repository.
  Future<List<ProjectProfile>> findAll();

  /// Return owner-scoped projects, optionally including archived profiles.
  Future<RepositoryPage<ProjectProfile>> findByOwner({
    required String ownerId,
    bool includeArchived = false,
    RepositoryPageRequest page = const RepositoryPageRequest(),
  });

  /// Save or replace [project] by stable id.
  Future<void> save(ProjectProfile project);

  /// Insert [project] if its id is unused.
  Future<RepositoryMutationResult> insert({
    required ProjectProfile project,
    required String ownerId,
  });

  /// Compare-and-set save by expected repository revision.
  Future<RepositoryMutationResult> saveIfRevision({
    required ProjectProfile project,
    required String ownerId,
    required int expectedRevision,
  });

  /// Archive a project without deleting task history.
  Future<RepositoryMutationResult> archive({
    required String projectId,
    required String ownerId,
    required int expectedRevision,
    required DateTime archivedAt,
  });
}

/// Repository contract for locked-block and one-day override documents.
abstract interface class LockedBlockRepository {
  /// Return a locked block by stable id, or null when it does not exist.
  Future<LockedBlock?> findBlockById(String id);

  /// Return a locked block plus owner/revision metadata.
  Future<RepositoryRecord<LockedBlock>?> findBlockRecordById(String id);

  /// Return all locked block definitions.
  Future<List<LockedBlock>> findAllBlocks();

  /// Return owner-scoped locked blocks, optionally including archived blocks.
  Future<RepositoryPage<LockedBlock>> findBlocksByOwner({
    required String ownerId,
    bool includeArchived = false,
    RepositoryPageRequest page = const RepositoryPageRequest(),
  });

  /// Return an override by stable id, or null when it does not exist.
  Future<LockedBlockOverride?> findOverrideById(String id);

  /// Return an override plus owner/revision metadata.
  Future<RepositoryRecord<LockedBlockOverride>?> findOverrideRecordById(
    String id,
  );

  /// Return all one-day overrides.
  Future<List<LockedBlockOverride>> findAllOverrides();

  /// Return owner-scoped one-day overrides for [date].
  Future<RepositoryPage<LockedBlockOverride>> findOverridesForDate({
    required String ownerId,
    required CivilDate date,
    RepositoryPageRequest page = const RepositoryPageRequest(),
  });

  /// Save or replace [block] by stable id.
  Future<void> saveBlock(LockedBlock block);

  /// Save or replace [override] by stable id.
  Future<void> saveOverride(LockedBlockOverride override);

  Future<RepositoryMutationResult> insertBlock({
    required LockedBlock block,
    required String ownerId,
  });

  Future<RepositoryMutationResult> saveBlockIfRevision({
    required LockedBlock block,
    required String ownerId,
    required int expectedRevision,
  });

  Future<RepositoryMutationResult> archiveBlock({
    required String blockId,
    required String ownerId,
    required int expectedRevision,
    required DateTime archivedAt,
  });

  Future<RepositoryMutationResult> insertOverride({
    required LockedBlockOverride override,
    required String ownerId,
  });

  Future<RepositoryMutationResult> saveOverrideIfRevision({
    required LockedBlockOverride override,
    required String ownerId,
    required int expectedRevision,
  });
}

/// Snapshot of one scheduling operation or persisted planning state.
class SchedulingStateSnapshot {
  SchedulingStateSnapshot({
    required this.id,
    required this.capturedAt,
    required this.window,
    required List<Task> tasks,
    List<TimeInterval> lockedIntervals = const <TimeInterval>[],
    List<TimeInterval> requiredVisibleIntervals = const <TimeInterval>[],
    List<SchedulingNotice> notices = const <SchedulingNotice>[],
    List<SchedulingChange> changes = const <SchedulingChange>[],
    List<SchedulingOverlap> overlaps = const <SchedulingOverlap>[],
    this.operationName,
  })  : tasks = List<Task>.unmodifiable(tasks),
        lockedIntervals = List<TimeInterval>.unmodifiable(lockedIntervals),
        requiredVisibleIntervals =
            List<TimeInterval>.unmodifiable(requiredVisibleIntervals),
        notices = List<SchedulingNotice>.unmodifiable(notices),
        changes = List<SchedulingChange>.unmodifiable(changes),
        overlaps = List<SchedulingOverlap>.unmodifiable(overlaps);

  /// Stable snapshot id.
  final String id;

  /// Time this snapshot was created.
  final DateTime capturedAt;

  /// Planning window represented by the snapshot.
  final SchedulingWindow window;

  /// Task documents visible to the operation.
  final List<Task> tasks;

  /// Locked intervals supplied to the scheduler.
  final List<TimeInterval> lockedIntervals;

  /// Extra visible required intervals supplied to the scheduler.
  final List<TimeInterval> requiredVisibleIntervals;

  /// Human-readable scheduling notices.
  final List<SchedulingNotice> notices;

  /// Machine-readable task movements.
  final List<SchedulingChange> changes;

  /// Analysis-only overlap findings.
  final List<SchedulingOverlap> overlaps;

  /// Optional operation label, such as `push` or `rollover`.
  final String? operationName;

  /// Rebuild scheduler input from the persisted state shape.
  SchedulingInput get input {
    return SchedulingInput(
      tasks: List<Task>.unmodifiable(tasks),
      window: window,
      lockedIntervals: List<TimeInterval>.unmodifiable(lockedIntervals),
      requiredVisibleIntervals:
          List<TimeInterval>.unmodifiable(requiredVisibleIntervals),
    );
  }

  /// Rebuild scheduler result details from the persisted state shape.
  SchedulingResult get result {
    return SchedulingResult(
      tasks: List<Task>.unmodifiable(tasks),
      notices: List<SchedulingNotice>.unmodifiable(notices),
      changes: List<SchedulingChange>.unmodifiable(changes),
      overlaps: List<SchedulingOverlap>.unmodifiable(overlaps),
    );
  }
}

/// Repository contract for scheduling operation/state snapshot documents.
abstract interface class SchedulingSnapshotRepository {
  /// Return a snapshot by stable id, or null when it does not exist.
  Future<SchedulingStateSnapshot?> findById(String id);

  /// Return all snapshots overlapping [window].
  Future<List<SchedulingStateSnapshot>> findInWindow(SchedulingWindow window);

  /// Save or replace [snapshot] by stable id.
  Future<void> save(SchedulingStateSnapshot snapshot);
}

/// In-memory task repository useful for tests and early application wiring.
class InMemoryTaskRepository implements TaskRepository {
  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,
        };

  final Map<String, Task> _tasksById;
  final Map<String, String> _ownersById;
  final Map<String, int> _revisionsById;
  final String defaultOwnerId;

  @override
  Future<Task?> findById(String id) async {
    return _tasksById[id];
  }

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

  @override
  Future<List<Task>> findAll() async {
    return List<Task>.unmodifiable(_tasksById.values);
  }

  @override
  Future<List<Task>> findByStatus(TaskStatus status) async {
    return List<Task>.unmodifiable(
      _tasksById.values.where((task) => task.status == status),
    );
  }

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

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

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

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

  @override
  Future<List<Task>> findScheduledInWindow(SchedulingWindow window) async {
    return List<Task>.unmodifiable(
      _tasksById.values.where((task) {
        return _taskOverlapsWindow(task, window);
      }),
    );
  }

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

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

  @override
  Future<void> save(Task task) async {
    _tasksById[task.id] = task;
    _ownersById.putIfAbsent(task.id, () => defaultOwnerId);
    _revisionsById[task.id] = _revisionFor(task.id) + 1;
  }

  @override
  Future<void> saveAll(Iterable<Task> tasks) async {
    for (final task in tasks) {
      await save(task);
    }
  }

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

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

  String _ownerFor(String id) => _ownersById[id] ?? defaultOwnerId;

  int _revisionFor(String id) => _revisionsById[id] ?? 1;
}

/// In-memory project repository useful for tests and early application wiring.
class InMemoryProjectRepository implements ProjectRepository {
  InMemoryProjectRepository(
      [Iterable<ProjectProfile> initialProjects = const [],
      this.defaultOwnerId = 'owner-1'])
      : _projectsById = {
          for (final project in initialProjects) project.id: project,
        },
        _ownersById = {
          for (final project in initialProjects) project.id: defaultOwnerId,
        },
        _revisionsById = {
          for (final project in initialProjects) project.id: 1,
        };

  final Map<String, ProjectProfile> _projectsById;
  final Map<String, String> _ownersById;
  final Map<String, int> _revisionsById;
  final String defaultOwnerId;

  @override
  Future<ProjectProfile?> findById(String id) async {
    return _projectsById[id];
  }

  @override
  Future<RepositoryRecord<ProjectProfile>?> findRecordById(String id) async {
    final project = _projectsById[id];
    if (project == null) {
      return null;
    }
    return RepositoryRecord(
      value: project,
      ownerId: _ownerFor(id),
      revision: _revisionFor(id),
      archivedAt: project.archivedAt,
    );
  }

  @override
  Future<List<ProjectProfile>> findAll() async {
    return List<ProjectProfile>.unmodifiable(_projectsById.values);
  }

  @override
  Future<RepositoryPage<ProjectProfile>> findByOwner({
    required String ownerId,
    bool includeArchived = false,
    RepositoryPageRequest page = const RepositoryPageRequest(),
  }) async {
    final projects = _projectsById.values
        .where(
          (project) =>
              _ownerFor(project.id) == ownerId &&
              (includeArchived || !project.isArchived),
        )
        .toList(growable: false)
      ..sort(_compareProjects);
    return _page(projects, page);
  }

  @override
  Future<void> save(ProjectProfile project) async {
    _projectsById[project.id] = project;
    _ownersById.putIfAbsent(project.id, () => defaultOwnerId);
    _revisionsById[project.id] = _revisionFor(project.id) + 1;
  }

  @override
  Future<RepositoryMutationResult> insert({
    required ProjectProfile project,
    required String ownerId,
  }) async {
    if (_projectsById.containsKey(project.id)) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.duplicateId,
          entityId: project.id,
        ),
      );
    }
    _projectsById[project.id] = project;
    _ownersById[project.id] = ownerId;
    _revisionsById[project.id] = 1;
    return RepositoryMutationResult.success(revision: 1);
  }

  @override
  Future<RepositoryMutationResult> saveIfRevision({
    required ProjectProfile project,
    required String ownerId,
    required int expectedRevision,
  }) async {
    if (expectedRevision <= 0) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.invalidRevision,
          entityId: project.id,
          expectedRevision: expectedRevision,
        ),
      );
    }
    final current = _projectsById[project.id];
    if (current == null) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.notFound,
          entityId: project.id,
        ),
      );
    }
    if (_ownerFor(project.id) != ownerId) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.ownerMismatch,
          entityId: project.id,
        ),
      );
    }
    final actualRevision = _revisionFor(project.id);
    if (actualRevision != expectedRevision) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.staleRevision,
          entityId: project.id,
          expectedRevision: expectedRevision,
          actualRevision: actualRevision,
        ),
      );
    }
    final nextRevision = actualRevision + 1;
    _projectsById[project.id] = project;
    _ownersById[project.id] = ownerId;
    _revisionsById[project.id] = nextRevision;
    return RepositoryMutationResult.success(revision: nextRevision);
  }

  @override
  Future<RepositoryMutationResult> archive({
    required String projectId,
    required String ownerId,
    required int expectedRevision,
    required DateTime archivedAt,
  }) async {
    final project = _projectsById[projectId];
    if (project == null) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.notFound,
          entityId: projectId,
        ),
      );
    }
    return saveIfRevision(
      project: project.copyWith(archivedAt: archivedAt),
      ownerId: ownerId,
      expectedRevision: expectedRevision,
    );
  }

  String _ownerFor(String id) => _ownersById[id] ?? defaultOwnerId;

  int _revisionFor(String id) => _revisionsById[id] ?? 1;
}

/// In-memory locked-time repository useful for tests and early wiring.
class InMemoryLockedBlockRepository implements LockedBlockRepository {
  InMemoryLockedBlockRepository({
    Iterable<LockedBlock> initialBlocks = const [],
    Iterable<LockedBlockOverride> initialOverrides = const [],
    this.defaultOwnerId = 'owner-1',
  })  : _blocksById = {
          for (final block in initialBlocks) block.id: block,
        },
        _blockOwnersById = {
          for (final block in initialBlocks) block.id: defaultOwnerId,
        },
        _blockRevisionsById = {
          for (final block in initialBlocks) block.id: 1,
        },
        _overridesById = {
          for (final override in initialOverrides) override.id: override,
        },
        _overrideOwnersById = {
          for (final override in initialOverrides) override.id: defaultOwnerId,
        },
        _overrideRevisionsById = {
          for (final override in initialOverrides) override.id: 1,
        };

  final Map<String, LockedBlock> _blocksById;
  final Map<String, String> _blockOwnersById;
  final Map<String, int> _blockRevisionsById;
  final Map<String, LockedBlockOverride> _overridesById;
  final Map<String, String> _overrideOwnersById;
  final Map<String, int> _overrideRevisionsById;
  final String defaultOwnerId;

  @override
  Future<LockedBlock?> findBlockById(String id) async {
    return _blocksById[id];
  }

  @override
  Future<RepositoryRecord<LockedBlock>?> findBlockRecordById(String id) async {
    final block = _blocksById[id];
    if (block == null) {
      return null;
    }
    return RepositoryRecord(
      value: block,
      ownerId: _blockOwnerFor(id),
      revision: _blockRevisionFor(id),
      archivedAt: block.archivedAt,
    );
  }

  @override
  Future<List<LockedBlock>> findAllBlocks() async {
    return List<LockedBlock>.unmodifiable(_blocksById.values);
  }

  @override
  Future<RepositoryPage<LockedBlock>> findBlocksByOwner({
    required String ownerId,
    bool includeArchived = false,
    RepositoryPageRequest page = const RepositoryPageRequest(),
  }) async {
    final blocks = _blocksById.values
        .where(
          (block) =>
              _blockOwnerFor(block.id) == ownerId &&
              (includeArchived || !block.isArchived),
        )
        .toList(growable: false)
      ..sort(_compareLockedBlocks);
    return _page(blocks, page);
  }

  @override
  Future<LockedBlockOverride?> findOverrideById(String id) async {
    return _overridesById[id];
  }

  @override
  Future<RepositoryRecord<LockedBlockOverride>?> findOverrideRecordById(
    String id,
  ) async {
    final override = _overridesById[id];
    if (override == null) {
      return null;
    }
    return RepositoryRecord(
      value: override,
      ownerId: _overrideOwnerFor(id),
      revision: _overrideRevisionFor(id),
    );
  }

  @override
  Future<List<LockedBlockOverride>> findAllOverrides() async {
    return List<LockedBlockOverride>.unmodifiable(_overridesById.values);
  }

  @override
  Future<RepositoryPage<LockedBlockOverride>> findOverridesForDate({
    required String ownerId,
    required CivilDate date,
    RepositoryPageRequest page = const RepositoryPageRequest(),
  }) async {
    final overrides = _overridesById.values
        .where(
          (override) =>
              _overrideOwnerFor(override.id) == ownerId &&
              override.date == date,
        )
        .toList(growable: false)
      ..sort(_compareLockedOverrides);
    return _page(overrides, page);
  }

  @override
  Future<void> saveBlock(LockedBlock block) async {
    _blocksById[block.id] = block;
    _blockOwnersById.putIfAbsent(block.id, () => defaultOwnerId);
    _blockRevisionsById[block.id] = _blockRevisionFor(block.id) + 1;
  }

  @override
  Future<void> saveOverride(LockedBlockOverride override) async {
    _overridesById[override.id] = override;
    _overrideOwnersById.putIfAbsent(override.id, () => defaultOwnerId);
    _overrideRevisionsById[override.id] = _overrideRevisionFor(override.id) + 1;
  }

  @override
  Future<RepositoryMutationResult> insertBlock({
    required LockedBlock block,
    required String ownerId,
  }) async {
    if (_blocksById.containsKey(block.id)) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.duplicateId,
          entityId: block.id,
        ),
      );
    }
    _blocksById[block.id] = block;
    _blockOwnersById[block.id] = ownerId;
    _blockRevisionsById[block.id] = 1;
    return RepositoryMutationResult.success(revision: 1);
  }

  @override
  Future<RepositoryMutationResult> saveBlockIfRevision({
    required LockedBlock block,
    required String ownerId,
    required int expectedRevision,
  }) async {
    if (expectedRevision <= 0) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.invalidRevision,
          entityId: block.id,
          expectedRevision: expectedRevision,
        ),
      );
    }
    final current = _blocksById[block.id];
    if (current == null) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.notFound,
          entityId: block.id,
        ),
      );
    }
    if (_blockOwnerFor(block.id) != ownerId) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.ownerMismatch,
          entityId: block.id,
        ),
      );
    }
    final actualRevision = _blockRevisionFor(block.id);
    if (actualRevision != expectedRevision) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.staleRevision,
          entityId: block.id,
          expectedRevision: expectedRevision,
          actualRevision: actualRevision,
        ),
      );
    }
    final nextRevision = actualRevision + 1;
    _blocksById[block.id] = block;
    _blockOwnersById[block.id] = ownerId;
    _blockRevisionsById[block.id] = nextRevision;
    return RepositoryMutationResult.success(revision: nextRevision);
  }

  @override
  Future<RepositoryMutationResult> archiveBlock({
    required String blockId,
    required String ownerId,
    required int expectedRevision,
    required DateTime archivedAt,
  }) async {
    final block = _blocksById[blockId];
    if (block == null) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.notFound,
          entityId: blockId,
        ),
      );
    }
    return saveBlockIfRevision(
      block: block.copyWith(updatedAt: archivedAt, archivedAt: archivedAt),
      ownerId: ownerId,
      expectedRevision: expectedRevision,
    );
  }

  @override
  Future<RepositoryMutationResult> insertOverride({
    required LockedBlockOverride override,
    required String ownerId,
  }) async {
    if (_overridesById.containsKey(override.id)) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.duplicateId,
          entityId: override.id,
        ),
      );
    }
    _overridesById[override.id] = override;
    _overrideOwnersById[override.id] = ownerId;
    _overrideRevisionsById[override.id] = 1;
    return RepositoryMutationResult.success(revision: 1);
  }

  @override
  Future<RepositoryMutationResult> saveOverrideIfRevision({
    required LockedBlockOverride override,
    required String ownerId,
    required int expectedRevision,
  }) async {
    if (expectedRevision <= 0) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.invalidRevision,
          entityId: override.id,
          expectedRevision: expectedRevision,
        ),
      );
    }
    final current = _overridesById[override.id];
    if (current == null) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.notFound,
          entityId: override.id,
        ),
      );
    }
    if (_overrideOwnerFor(override.id) != ownerId) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.ownerMismatch,
          entityId: override.id,
        ),
      );
    }
    final actualRevision = _overrideRevisionFor(override.id);
    if (actualRevision != expectedRevision) {
      return RepositoryMutationResult.failure(
        RepositoryFailure(
          code: RepositoryFailureCode.staleRevision,
          entityId: override.id,
          expectedRevision: expectedRevision,
          actualRevision: actualRevision,
        ),
      );
    }
    final nextRevision = actualRevision + 1;
    _overridesById[override.id] = override;
    _overrideOwnersById[override.id] = ownerId;
    _overrideRevisionsById[override.id] = nextRevision;
    return RepositoryMutationResult.success(revision: nextRevision);
  }

  String _blockOwnerFor(String id) => _blockOwnersById[id] ?? defaultOwnerId;

  int _blockRevisionFor(String id) => _blockRevisionsById[id] ?? 1;

  String _overrideOwnerFor(String id) =>
      _overrideOwnersById[id] ?? defaultOwnerId;

  int _overrideRevisionFor(String id) => _overrideRevisionsById[id] ?? 1;
}

/// In-memory snapshot repository useful for tests and early application wiring.
class InMemorySchedulingSnapshotRepository
    implements SchedulingSnapshotRepository {
  InMemorySchedulingSnapshotRepository([
    Iterable<SchedulingStateSnapshot> initialSnapshots = const [],
  ]) : _snapshotsById = {
          for (final snapshot in initialSnapshots) snapshot.id: snapshot,
        };

  final Map<String, SchedulingStateSnapshot> _snapshotsById;

  @override
  Future<SchedulingStateSnapshot?> findById(String id) async {
    return _snapshotsById[id];
  }

  @override
  Future<List<SchedulingStateSnapshot>> findInWindow(
    SchedulingWindow window,
  ) async {
    return List<SchedulingStateSnapshot>.unmodifiable(
      _snapshotsById.values.where(
        (snapshot) => snapshot.window.interval.overlaps(window.interval),
      ),
    );
  }

  @override
  Future<void> save(SchedulingStateSnapshot snapshot) async {
    _snapshotsById[snapshot.id] = snapshot;
  }
}

RepositoryPage<T> _page<T>(List<T> sortedItems, RepositoryPageRequest request) {
  final offset = int.tryParse(request.cursor ?? '') ?? 0;
  final safeOffset = offset.clamp(0, sortedItems.length);
  final end = (safeOffset + request.limit).clamp(0, sortedItems.length);
  final items = sortedItems.sublist(safeOffset, end);
  final nextCursor = end < sortedItems.length ? end.toString() : null;
  return RepositoryPage<T>(
    items: List<T>.unmodifiable(items),
    nextCursor: nextCursor,
  );
}

bool _taskOverlapsWindow(Task task, SchedulingWindow window) {
  final start = task.scheduledStart;
  final end = task.scheduledEnd;
  if (start == null || end == null) {
    return false;
  }
  return TimeInterval(start: start, end: end).overlaps(window.interval);
}

int _compareTaskIds(Task left, Task right) => left.id.compareTo(right.id);

int _compareScheduledTasks(Task left, Task right) {
  final leftStart = left.scheduledStart;
  final rightStart = right.scheduledStart;
  if (leftStart != null && rightStart != null) {
    final startComparison = leftStart.compareTo(rightStart);
    if (startComparison != 0) {
      return startComparison;
    }
  }
  return left.id.compareTo(right.id);
}

int _compareBacklogCandidates(Task left, Task right) {
  final createdComparison = left.createdAt.compareTo(right.createdAt);
  if (createdComparison != 0) {
    return createdComparison;
  }
  return left.id.compareTo(right.id);
}

int _compareProjects(ProjectProfile left, ProjectProfile right) {
  final nameComparison = left.name.compareTo(right.name);
  if (nameComparison != 0) {
    return nameComparison;
  }
  return left.id.compareTo(right.id);
}

int _compareLockedBlocks(LockedBlock left, LockedBlock right) {
  final nameComparison = left.name.compareTo(right.name);
  if (nameComparison != 0) {
    return nameComparison;
  }
  return left.id.compareTo(right.id);
}

int _compareLockedOverrides(
  LockedBlockOverride left,
  LockedBlockOverride right,
) {
  final dateComparison = left.date.compareTo(right.date);
  if (dateComparison != 0) {
    return dateComparison;
  }
  final createdComparison = left.createdAt.compareTo(right.createdAt);
  if (createdComparison != 0) {
    return createdComparison;
  }
  return left.id.compareTo(right.id);
}
