/// In-memory implementations of the scheduler persistence repository contracts.
///
/// These repositories are intended for core tests, local integration tests, and
/// adapter conformance checks. They model optimistic compare-and-set behavior
/// but do not provide durable storage.
library;

import 'dart:convert';

import 'package:scheduler_core/scheduler_core.dart' as core;
import 'package:scheduler_persistence/persistence.dart';

const _snapshotRetention = Duration(days: 30);

final _epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);

/// In-memory task repository with owner scoping and revision checks.
final class InMemoryTaskRepository implements TaskRepository {
  final Map<String, _Record<core.Task>> _records =
      <String, _Record<core.Task>>{};

  @override
  Future<RepoResult<core.Task?>> findById({
    required core.OwnerId ownerId,
    required String taskId,
  }) async {
    final record = _records[taskId];
    if (record == null || record.ownerId != ownerId) {
      return const Right(null);
    }
    return Right(_cloneTask(record.value, record.ownerId, record.revision));
  }

  @override
  Future<RepoResult<core.Page<core.Task>>> findByOwner({
    required core.OwnerId ownerId,
    core.PageRequest page = const core.PageRequest(),
  }) async {
    return Right(_pageRecords(
      _records.values.where((record) => record.ownerId == ownerId),
      page,
      (record) => _cloneTask(record.value, record.ownerId, record.revision),
    ));
  }

  @override
  Future<RepoResult<core.Page<core.Task>>> findByParentTaskId({
    required core.OwnerId ownerId,
    required String parentTaskId,
    core.PageRequest page = const core.PageRequest(),
  }) async {
    return Right(_pageRecords(
      _records.values.where(
        (record) =>
            record.ownerId == ownerId &&
            record.value.parentTaskId == parentTaskId,
      ),
      page,
      (record) => _cloneTask(record.value, record.ownerId, record.revision),
    ));
  }

  @override
  Future<RepoResult<core.Page<core.Task>>> findByProjectId({
    required core.OwnerId ownerId,
    required String projectId,
    core.PageRequest page = const core.PageRequest(),
  }) async {
    return Right(_pageRecords(
      _records.values.where(
        (record) =>
            record.ownerId == ownerId && record.value.projectId == projectId,
      ),
      page,
      (record) => _cloneTask(record.value, record.ownerId, record.revision),
    ));
  }

  @override
  Future<RepoResult<core.Page<core.Task>>> findByStatus({
    required core.OwnerId ownerId,
    required core.TaskStatus status,
    core.PageRequest page = const core.PageRequest(),
  }) async {
    return Right(_pageRecords(
      _records.values.where(
        (record) => record.ownerId == ownerId && record.value.status == status,
      ),
      page,
      (record) => _cloneTask(record.value, record.ownerId, record.revision),
    ));
  }

  @override
  Future<RepoResult<core.Revision>> insert({
    required core.OwnerId ownerId,
    required core.Task task,
  }) async {
    if (_records.containsKey(task.id)) {
      return Left(RepositoryDuplicateId(entityId: task.id));
    }
    _records[task.id] = _Record(
      id: task.id,
      ownerId: ownerId,
      value: _cloneTask(task, ownerId, core.Revision.initial),
      revision: core.Revision.initial,
    );
    return const Right(core.Revision.initial);
  }

  @override
  Future<RepoResult<core.Revision>> save({
    required core.OwnerId ownerId,
    required core.Task task,
    required core.Revision expectedRevision,
  }) async {
    final checked = _checkWrite(
      records: _records,
      id: task.id,
      ownerId: ownerId,
      expectedRevision: expectedRevision,
    );
    if (checked is Left<RepositoryFailure, _Record<core.Task>>) {
      return Left(checked.value);
    }
    final record = checked.right;
    final nextRevision = record.revision.next();
    _records[task.id] = record.copyWith(
      value: _cloneTask(task, ownerId, nextRevision),
      revision: nextRevision,
      updatedAt: task.updatedAt,
    );
    return Right(nextRevision);
  }

  @override
  Future<RepoResult<core.Revision>> delete({
    required core.OwnerId ownerId,
    required String taskId,
    required core.Revision expectedRevision,
  }) async {
    final checked = _checkWrite(
      records: _records,
      id: taskId,
      ownerId: ownerId,
      expectedRevision: expectedRevision,
    );
    if (checked is Left<RepositoryFailure, _Record<core.Task>>) {
      return Left(checked.value);
    }
    final nextRevision = checked.right.revision.next();
    _records.remove(taskId);
    return Right(nextRevision);
  }
}

/// In-memory project repository with owner scoping and revision checks.
final class InMemoryProjectRepository implements ProjectRepository {
  final Map<String, _Record<core.ProjectProfile>> _records =
      <String, _Record<core.ProjectProfile>>{};

  @override
  Future<RepoResult<core.ProjectProfile?>> findById({
    required core.OwnerId ownerId,
    required String projectId,
  }) async {
    final record = _records[projectId];
    if (record == null || record.ownerId != ownerId) {
      return const Right(null);
    }
    return Right(_cloneProject(record));
  }

  @override
  Future<RepoResult<core.Page<core.ProjectProfile>>> findByOwner({
    required core.OwnerId ownerId,
    bool includeArchived = false,
    core.PageRequest page = const core.PageRequest(),
  }) async {
    return Right(_pageRecords(
      _records.values.where(
        (record) =>
            record.ownerId == ownerId &&
            (includeArchived || !record.value.isArchived),
      ),
      page,
      _cloneProject,
    ));
  }

  @override
  Future<RepoResult<core.Revision>> insert({
    required core.OwnerId ownerId,
    required core.ProjectProfile project,
  }) async {
    if (_records.containsKey(project.id)) {
      return Left(RepositoryDuplicateId(entityId: project.id));
    }
    _records[project.id] = _Record(
      id: project.id,
      ownerId: ownerId,
      value: _cloneProjectValue(
        project,
        ownerId: ownerId,
        revision: core.Revision.initial,
        createdAt: _epoch,
        updatedAt: _epoch,
      ),
      revision: core.Revision.initial,
      createdAt: _epoch,
      updatedAt: _epoch,
    );
    return const Right(core.Revision.initial);
  }

  @override
  Future<RepoResult<core.Revision>> save({
    required core.OwnerId ownerId,
    required core.ProjectProfile project,
    required core.Revision expectedRevision,
  }) async {
    final checked = _checkWrite(
      records: _records,
      id: project.id,
      ownerId: ownerId,
      expectedRevision: expectedRevision,
    );
    if (checked is Left<RepositoryFailure, _Record<core.ProjectProfile>>) {
      return Left(checked.value);
    }
    final record = checked.right;
    final nextRevision = record.revision.next();
    _records[project.id] = record.copyWith(
      value: _cloneProjectValue(
        project,
        ownerId: ownerId,
        revision: nextRevision,
        createdAt: record.createdAt,
        updatedAt: _epoch,
      ),
      revision: nextRevision,
      updatedAt: _epoch,
    );
    return Right(nextRevision);
  }

  @override
  Future<RepoResult<core.Revision>> archive({
    required core.OwnerId ownerId,
    required String projectId,
    required core.Revision expectedRevision,
    required DateTime archivedAtUtc,
  }) async {
    final checked = _checkWrite(
      records: _records,
      id: projectId,
      ownerId: ownerId,
      expectedRevision: expectedRevision,
    );
    if (checked is Left<RepositoryFailure, _Record<core.ProjectProfile>>) {
      return Left(checked.value);
    }
    final record = checked.right;
    final nextRevision = record.revision.next();
    final archived = record.value.copyWith(archivedAt: archivedAtUtc);
    _records[projectId] = record.copyWith(
      value: _cloneProjectValue(
        archived,
        ownerId: ownerId,
        revision: nextRevision,
        createdAt: record.createdAt,
        updatedAt: _epoch,
      ),
      revision: nextRevision,
      updatedAt: _epoch,
    );
    return Right(nextRevision);
  }
}

/// In-memory locked-block repository with owner scoping and revision checks.
final class InMemoryLockedBlockRepository implements LockedBlockRepository {
  final Map<String, _Record<core.LockedBlock>> _blocks =
      <String, _Record<core.LockedBlock>>{};
  final Map<String, _Record<core.LockedBlockOverride>> _overrides =
      <String, _Record<core.LockedBlockOverride>>{};

  @override
  Future<RepoResult<core.LockedBlock?>> findBlockById({
    required core.OwnerId ownerId,
    required String blockId,
  }) async {
    final record = _blocks[blockId];
    if (record == null || record.ownerId != ownerId) {
      return const Right(null);
    }
    return Right(
        _cloneLockedBlock(record.value, record.ownerId, record.revision));
  }

  @override
  Future<RepoResult<core.Page<core.LockedBlock>>> findBlocksByOwner({
    required core.OwnerId ownerId,
    bool includeArchived = false,
    core.PageRequest page = const core.PageRequest(),
  }) async {
    return Right(_pageRecords(
      _blocks.values.where(
        (record) =>
            record.ownerId == ownerId &&
            (includeArchived || !record.value.isArchived),
      ),
      page,
      (record) => _cloneLockedBlock(
        record.value,
        record.ownerId,
        record.revision,
      ),
    ));
  }

  @override
  Future<RepoResult<core.LockedBlockOverride?>> findOverrideById({
    required core.OwnerId ownerId,
    required String overrideId,
  }) async {
    final record = _overrides[overrideId];
    if (record == null || record.ownerId != ownerId) {
      return const Right(null);
    }
    return Right(
      _cloneLockedBlockOverride(record.value, record.ownerId, record.revision),
    );
  }

  @override
  Future<RepoResult<core.Page<core.LockedBlockOverride>>> findOverridesForDate({
    required core.OwnerId ownerId,
    required core.CivilDate date,
    core.PageRequest page = const core.PageRequest(),
  }) async {
    return Right(_pageRecords(
      _overrides.values.where(
        (record) => record.ownerId == ownerId && record.value.date == date,
      ),
      page,
      (record) => _cloneLockedBlockOverride(
        record.value,
        record.ownerId,
        record.revision,
      ),
    ));
  }

  @override
  Future<RepoResult<core.Revision>> insertBlock({
    required core.OwnerId ownerId,
    required core.LockedBlock block,
  }) async {
    if (_blocks.containsKey(block.id)) {
      return Left(RepositoryDuplicateId(entityId: block.id));
    }
    _blocks[block.id] = _Record(
      id: block.id,
      ownerId: ownerId,
      value: _cloneLockedBlock(block, ownerId, core.Revision.initial),
      revision: core.Revision.initial,
    );
    return const Right(core.Revision.initial);
  }

  @override
  Future<RepoResult<core.Revision>> saveBlock({
    required core.OwnerId ownerId,
    required core.LockedBlock block,
    required core.Revision expectedRevision,
  }) async {
    final checked = _checkWrite(
      records: _blocks,
      id: block.id,
      ownerId: ownerId,
      expectedRevision: expectedRevision,
    );
    if (checked is Left<RepositoryFailure, _Record<core.LockedBlock>>) {
      return Left(checked.value);
    }
    final record = checked.right;
    final nextRevision = record.revision.next();
    _blocks[block.id] = record.copyWith(
      value: _cloneLockedBlock(block, ownerId, nextRevision),
      revision: nextRevision,
      updatedAt: block.updatedAt,
    );
    return Right(nextRevision);
  }

  @override
  Future<RepoResult<core.Revision>> archiveBlock({
    required core.OwnerId ownerId,
    required String blockId,
    required core.Revision expectedRevision,
    required DateTime archivedAtUtc,
  }) async {
    final checked = _checkWrite(
      records: _blocks,
      id: blockId,
      ownerId: ownerId,
      expectedRevision: expectedRevision,
    );
    if (checked is Left<RepositoryFailure, _Record<core.LockedBlock>>) {
      return Left(checked.value);
    }
    final record = checked.right;
    final nextRevision = record.revision.next();
    final archived = record.value.copyWith(archivedAt: archivedAtUtc);
    _blocks[blockId] = record.copyWith(
      value: _cloneLockedBlock(archived, ownerId, nextRevision),
      revision: nextRevision,
      updatedAt: archived.updatedAt,
    );
    return Right(nextRevision);
  }

  @override
  Future<RepoResult<core.Revision>> insertOverride({
    required core.OwnerId ownerId,
    required core.LockedBlockOverride override,
  }) async {
    if (_overrides.containsKey(override.id)) {
      return Left(RepositoryDuplicateId(entityId: override.id));
    }
    _overrides[override.id] = _Record(
      id: override.id,
      ownerId: ownerId,
      value: _cloneLockedBlockOverride(
        override,
        ownerId,
        core.Revision.initial,
      ),
      revision: core.Revision.initial,
    );
    return const Right(core.Revision.initial);
  }

  @override
  Future<RepoResult<core.Revision>> saveOverride({
    required core.OwnerId ownerId,
    required core.LockedBlockOverride override,
    required core.Revision expectedRevision,
  }) async {
    final checked = _checkWrite(
      records: _overrides,
      id: override.id,
      ownerId: ownerId,
      expectedRevision: expectedRevision,
    );
    if (checked is Left<RepositoryFailure, _Record<core.LockedBlockOverride>>) {
      return Left(checked.value);
    }
    final record = checked.right;
    final nextRevision = record.revision.next();
    _overrides[override.id] = record.copyWith(
      value: _cloneLockedBlockOverride(override, ownerId, nextRevision),
      revision: nextRevision,
      updatedAt: override.updatedAt,
    );
    return Right(nextRevision);
  }
}

/// In-memory owner settings repository with owner scoping and revision checks.
final class InMemorySettingsRepository implements SettingsRepository {
  final Map<String, _Record<core.OwnerSettings>> _records =
      <String, _Record<core.OwnerSettings>>{};

  @override
  Future<RepoResult<core.OwnerSettings?>> findByOwner({
    required core.OwnerId ownerId,
  }) async {
    final record = _records[ownerId.value];
    if (record == null || record.ownerId != ownerId) {
      return const Right(null);
    }
    return Right(_cloneSettings(record));
  }

  @override
  Future<RepoResult<core.Revision>> insert({
    required core.OwnerId ownerId,
    required core.OwnerSettings settings,
  }) async {
    if (_records.containsKey(ownerId.value)) {
      return Left(RepositoryDuplicateId(entityId: ownerId.value));
    }
    final ownedSettings = settings.ownerId == ownerId.value
        ? settings
        : settings.copyWith(ownerId: ownerId.value);
    _records[ownerId.value] = _Record(
      id: ownerId.value,
      ownerId: ownerId,
      value: _cloneSettingsValue(
        ownedSettings,
        revision: core.Revision.initial,
        createdAt: _epoch,
        updatedAt: _epoch,
      ),
      revision: core.Revision.initial,
      createdAt: _epoch,
      updatedAt: _epoch,
    );
    return const Right(core.Revision.initial);
  }

  @override
  Future<RepoResult<core.Revision>> save({
    required core.OwnerId ownerId,
    required core.OwnerSettings settings,
    required core.Revision expectedRevision,
  }) async {
    final checked = _checkWrite(
      records: _records,
      id: ownerId.value,
      ownerId: ownerId,
      expectedRevision: expectedRevision,
    );
    if (checked is Left<RepositoryFailure, _Record<core.OwnerSettings>>) {
      return Left(checked.value);
    }
    final record = checked.right;
    final nextRevision = record.revision.next();
    final ownedSettings = settings.ownerId == ownerId.value
        ? settings
        : settings.copyWith(ownerId: ownerId.value);
    _records[ownerId.value] = record.copyWith(
      value: _cloneSettingsValue(
        ownedSettings,
        revision: nextRevision,
        createdAt: record.createdAt,
        updatedAt: _epoch,
      ),
      revision: nextRevision,
      updatedAt: _epoch,
    );
    return Right(nextRevision);
  }
}

/// In-memory diagnostic schedule snapshot repository.
final class InMemoryScheduleSnapshotRepository
    implements ScheduleSnapshotRepository {
  final Map<String, _SnapshotRecord> _records = <String, _SnapshotRecord>{};

  @override
  Future<RepoResult<core.SchedulingStateSnapshot?>> findById({
    required core.OwnerId ownerId,
    required String snapshotId,
  }) async {
    final record = _records[snapshotId];
    if (record == null || record.ownerId != ownerId) {
      return const Right(null);
    }
    return Right(_cloneSnapshot(record.value, record.ownerId, record.revision));
  }

  @override
  Future<RepoResult<core.Page<core.SchedulingStateSnapshot>>> findInWindow({
    required core.OwnerId ownerId,
    required core.SchedulingWindow window,
    core.PageRequest page = const core.PageRequest(),
  }) async {
    return Right(_pageRecords(
      _records.values.where(
        (record) =>
            record.ownerId == ownerId &&
            _windowsOverlap(record.value.window, window),
      ),
      page,
      (record) => _cloneSnapshot(record.value, record.ownerId, record.revision),
    ));
  }

  @override
  Future<RepoResult<core.Revision>> insert({
    required core.OwnerId ownerId,
    required core.SchedulingStateSnapshot snapshot,
  }) async {
    if (_records.containsKey(snapshot.id)) {
      return Left(RepositoryDuplicateId(entityId: snapshot.id));
    }
    _records[snapshot.id] = _SnapshotRecord(
      id: snapshot.id,
      ownerId: ownerId,
      value: _cloneSnapshot(snapshot, ownerId, core.Revision.initial),
      revision: core.Revision.initial,
      retentionExpiresAt: snapshot.capturedAt.add(_snapshotRetention),
    );
    return const Right(core.Revision.initial);
  }

  @override
  Future<RepoResult<int>> deleteExpired({
    required core.OwnerId ownerId,
    required DateTime nowUtc,
  }) async {
    final expiredIds = _records.values
        .where(
          (record) =>
              record.ownerId == ownerId &&
              record.retentionExpiresAt.isBefore(nowUtc),
        )
        .map((record) => record.id)
        .toList(growable: false);
    for (final id in expiredIds) {
      _records.remove(id);
    }
    return Right(expiredIds.length);
  }
}

Either<RepositoryFailure, _Record<T>> _checkWrite<T>({
  required Map<String, _Record<T>> records,
  required String id,
  required core.OwnerId ownerId,
  required core.Revision expectedRevision,
}) {
  if (expectedRevision.value <= 0) {
    return Left(
      RepositoryInvalidRevision(
        entityId: id,
        expectedRevision: expectedRevision,
      ),
    );
  }
  final record = records[id];
  if (record == null) {
    return Left(RepositoryNotFound(entityId: id));
  }
  if (record.ownerId != ownerId) {
    return Left(RepositoryOwnerMismatch(entityId: id));
  }
  if (record.revision != expectedRevision) {
    return Left(
      RepositoryStaleRevision(
        entityId: id,
        expectedRevision: expectedRevision,
        actualRevision: record.revision,
      ),
    );
  }
  return Right(record);
}

core.Page<R> _pageRecords<T extends _IdentifiedRecord, R>(
  Iterable<T> records,
  core.PageRequest page,
  R Function(T record) clone,
) {
  final ordered = records.toList(growable: false)
    ..sort((left, right) => left.id.compareTo(right.id));
  final start = _cursorOffset(page.cursor);
  final end = (start + page.limit).clamp(0, ordered.length);
  final items = ordered.sublist(start, end).map(clone).toList(growable: false);
  final nextCursor = end < ordered.length ? end.toString() : null;
  return core.Page<R>(items: items, nextCursor: nextCursor);
}

int _cursorOffset(String? cursor) {
  if (cursor == null) {
    return 0;
  }
  final parsed = int.tryParse(cursor);
  if (parsed == null || parsed < 0) {
    return 0;
  }
  return parsed;
}

bool _windowsOverlap(core.SchedulingWindow left, core.SchedulingWindow right) {
  return left.start.isBefore(right.end) && right.start.isBefore(left.end);
}

core.Task _cloneTask(
  core.Task task,
  core.OwnerId ownerId,
  core.Revision revision,
) {
  return core.TaskDocumentMapping.fromDocument(_roundTrip(
    core.TaskDocumentMapping.toDocument(
      task,
      ownerId: ownerId.value,
      revision: revision.value,
    ),
  ));
}

core.ProjectProfile _cloneProject(_Record<core.ProjectProfile> record) {
  return _cloneProjectValue(
    record.value,
    ownerId: record.ownerId,
    revision: record.revision,
    createdAt: record.createdAt,
    updatedAt: record.updatedAt,
  );
}

core.ProjectProfile _cloneProjectValue(
  core.ProjectProfile project, {
  required core.OwnerId ownerId,
  required core.Revision revision,
  required DateTime createdAt,
  required DateTime updatedAt,
}) {
  return core.ProjectDocumentMapping.fromDocument(_roundTrip(
    core.ProjectDocumentMapping.toDocument(
      project,
      ownerId: ownerId.value,
      revision: revision.value,
      createdAt: createdAt,
      updatedAt: updatedAt,
    ),
  ));
}

core.LockedBlock _cloneLockedBlock(
  core.LockedBlock block,
  core.OwnerId ownerId,
  core.Revision revision,
) {
  return core.LockedBlockDocumentMapping.fromDocument(_roundTrip(
    core.LockedBlockDocumentMapping.toDocument(
      block,
      ownerId: ownerId.value,
      revision: revision.value,
    ),
  ));
}

core.LockedBlockOverride _cloneLockedBlockOverride(
  core.LockedBlockOverride override,
  core.OwnerId ownerId,
  core.Revision revision,
) {
  return core.LockedBlockOverrideDocumentMapping.fromDocument(_roundTrip(
    core.LockedBlockOverrideDocumentMapping.toDocument(
      override,
      ownerId: ownerId.value,
      revision: revision.value,
    ),
  ));
}

core.OwnerSettings _cloneSettings(_Record<core.OwnerSettings> record) {
  return _cloneSettingsValue(
    record.value,
    revision: record.revision,
    createdAt: record.createdAt,
    updatedAt: record.updatedAt,
  );
}

core.OwnerSettings _cloneSettingsValue(
  core.OwnerSettings settings, {
  required core.Revision revision,
  required DateTime createdAt,
  required DateTime updatedAt,
}) {
  return core.OwnerSettingsDocumentMapping.fromDocument(_roundTrip(
    core.OwnerSettingsDocumentMapping.toDocument(
      settings,
      revision: revision.value,
      createdAt: createdAt,
      updatedAt: updatedAt,
    ),
  ));
}

core.SchedulingStateSnapshot _cloneSnapshot(
  core.SchedulingStateSnapshot snapshot,
  core.OwnerId ownerId,
  core.Revision revision,
) {
  return core.SchedulingSnapshotDocumentMapping.fromDocument(_roundTrip(
    core.SchedulingSnapshotDocumentMapping.toDocument(
      snapshot,
      ownerId: ownerId.value,
      revision: revision.value,
      retentionExpiresAt: snapshot.capturedAt.add(_snapshotRetention),
    ),
  ));
}

Map<String, Object?> _roundTrip(Map<String, Object?> document) {
  return Map<String, Object?>.from(
    jsonDecode(jsonEncode(document)) as Map<dynamic, dynamic>,
  );
}

abstract interface class _IdentifiedRecord {
  String get id;
}

final class _Record<T> implements _IdentifiedRecord {
  _Record({
    required this.id,
    required this.ownerId,
    required this.value,
    required this.revision,
    DateTime? createdAt,
    DateTime? updatedAt,
  })  : createdAt = createdAt ?? _epoch,
        updatedAt = updatedAt ?? _epoch;

  @override
  final String id;
  final core.OwnerId ownerId;
  final T value;
  final core.Revision revision;
  final DateTime createdAt;
  final DateTime updatedAt;

  _Record<T> copyWith({
    T? value,
    core.Revision? revision,
    DateTime? updatedAt,
  }) {
    return _Record<T>(
      id: id,
      ownerId: ownerId,
      value: value ?? this.value,
      revision: revision ?? this.revision,
      createdAt: createdAt,
      updatedAt: updatedAt ?? this.updatedAt,
    );
  }
}

final class _SnapshotRecord implements _IdentifiedRecord {
  _SnapshotRecord({
    required this.id,
    required this.ownerId,
    required this.value,
    required this.revision,
    required this.retentionExpiresAt,
  });

  @override
  final String id;
  final core.OwnerId ownerId;
  final core.SchedulingStateSnapshot value;
  final core.Revision revision;
  final DateTime retentionExpiresAt;
}
