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

part of '../sqlite_repositories.dart';

/// Drift-backed locked-block and override repository.
final class SqliteLockedBlockRepository implements LockedBlockRepository {
  /// Creates a locked-time repository using [db].
  SqliteLockedBlockRepository(this.db);

  /// Shared SQLite database handle.
  final SchedulerDb db;

  /// Runs the `findBlockById` 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<RepoResult<core.LockedBlock?>> findBlockById({
    required core.OwnerId ownerId,
    required String blockId,
  }) async {
    final row = await _blockById(blockId);
    if (row == null || row.ownerId != ownerId.value) {
      return const Right(null);
    }
    return Right(_lockedBlockFromRow(row));
  }

  /// Runs the `findBlocksByOwner` 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<RepoResult<core.Page<core.LockedBlock>>> findBlocksByOwner({
    required core.OwnerId ownerId,
    bool includeArchived = false,
    core.PageRequest page = const core.PageRequest(),
  }) async {
    final offset = _cursorOffset(page.cursor);
    final query = db.select(db.lockedBlocks)
      ..where((table) {
        final ownerClause = table.ownerId.equals(ownerId.value);
        return includeArchived
            ? ownerClause
            : ownerClause & table.archivedAtUtc.isNull();
      })
      ..orderBy([(table) => drift.OrderingTerm.asc(table.id)])
      ..limit(page.limit + 1, offset: offset);
    final rows = await query.get();
    final pageRows = rows.take(page.limit).toList(growable: false);
    return Right(core.Page(
      items: pageRows.map(_lockedBlockFromRow),
      nextCursor: rows.length > page.limit ? '${offset + page.limit}' : null,
    ));
  }

  /// Runs the `findOverrideById` 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<RepoResult<core.LockedBlockOverride?>> findOverrideById({
    required core.OwnerId ownerId,
    required String overrideId,
  }) async {
    final row = await _overrideById(overrideId);
    if (row == null || row.ownerId != ownerId.value) {
      return const Right(null);
    }
    return Right(_lockedOverrideFromRow(row));
  }

  /// Runs the `findOverridesForDate` 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<RepoResult<core.Page<core.LockedBlockOverride>>> findOverridesForDate({
    required core.OwnerId ownerId,
    required core.CivilDate date,
    core.PageRequest page = const core.PageRequest(),
  }) async {
    final offset = _cursorOffset(page.cursor);
    final query = db.select(db.lockedOverrides)
      ..where(
        (table) =>
            table.ownerId.equals(ownerId.value) &
            table.date.equals(date.toIsoString()),
      )
      ..orderBy([(table) => drift.OrderingTerm.asc(table.id)])
      ..limit(page.limit + 1, offset: offset);
    final rows = await query.get();
    final pageRows = rows.take(page.limit).toList(growable: false);
    return Right(core.Page(
      items: pageRows.map(_lockedOverrideFromRow),
      nextCursor: rows.length > page.limit ? '${offset + page.limit}' : null,
    ));
  }

  /// Runs the `insertBlock` 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<RepoResult<core.Revision>> insertBlock({
    required core.OwnerId ownerId,
    required core.LockedBlock block,
  }) async {
    if (await _blockById(block.id) != null) {
      return Left(RepositoryDuplicateId(entityId: block.id));
    }
    await db.into(db.lockedBlocks).insert(_lockedBlockCompanion(
          block,
          ownerId: ownerId,
          revision: core.Revision.initial,
        ));
    return const Right(core.Revision.initial);
  }

  /// Runs the `saveBlock` 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<RepoResult<core.Revision>> saveBlock({
    required core.OwnerId ownerId,
    required core.LockedBlock block,
    required core.Revision expectedRevision,
  }) async {
    final checked = await _checkBlockWrite(
      block.id,
      ownerId,
      expectedRevision,
    );
    if (checked is Left<RepositoryFailure, LockedBlockRow>) {
      return Left(checked.value);
    }
    final nextRevision = core.Revision(checked.right.revision).next();
    final updated = await (db.update(db.lockedBlocks)
          ..where(
            (table) =>
                table.id.equals(block.id) &
                table.ownerId.equals(ownerId.value) &
                table.revision.equals(expectedRevision.value),
          ))
        .write(_lockedBlockCompanion(
      block,
      ownerId: ownerId,
      revision: nextRevision,
    ));
    if (updated != 1) {
      return Left(await _staleBlockFailure(block.id, expectedRevision));
    }
    return Right(nextRevision);
  }

  /// Runs the `archiveBlock` 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<RepoResult<core.Revision>> archiveBlock({
    required core.OwnerId ownerId,
    required String blockId,
    required core.Revision expectedRevision,
    required DateTime archivedAtUtc,
  }) async {
    final checked = await _checkBlockWrite(blockId, ownerId, expectedRevision);
    if (checked is Left<RepositoryFailure, LockedBlockRow>) {
      return Left(checked.value);
    }
    final row = checked.right;
    final nextRevision = core.Revision(row.revision).next();
    final archived =
        _lockedBlockFromRow(row).copyWith(archivedAt: archivedAtUtc);
    final updated = await (db.update(db.lockedBlocks)
          ..where(
            (table) =>
                table.id.equals(blockId) &
                table.ownerId.equals(ownerId.value) &
                table.revision.equals(expectedRevision.value),
          ))
        .write(_lockedBlockCompanion(
      archived,
      ownerId: ownerId,
      revision: nextRevision,
    ));
    if (updated != 1) {
      return Left(await _staleBlockFailure(blockId, expectedRevision));
    }
    return Right(nextRevision);
  }

  /// Runs the `insertOverride` 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<RepoResult<core.Revision>> insertOverride({
    required core.OwnerId ownerId,
    required core.LockedBlockOverride override,
  }) async {
    if (await _overrideById(override.id) != null) {
      return Left(RepositoryDuplicateId(entityId: override.id));
    }
    await db.into(db.lockedOverrides).insert(_lockedOverrideCompanion(
          override,
          ownerId: ownerId,
          revision: core.Revision.initial,
        ));
    return const Right(core.Revision.initial);
  }

  /// Runs the `saveOverride` 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<RepoResult<core.Revision>> saveOverride({
    required core.OwnerId ownerId,
    required core.LockedBlockOverride override,
    required core.Revision expectedRevision,
  }) async {
    final checked = await _checkOverrideWrite(
      override.id,
      ownerId,
      expectedRevision,
    );
    if (checked is Left<RepositoryFailure, LockedOverrideRow>) {
      return Left(checked.value);
    }
    final nextRevision = core.Revision(checked.right.revision).next();
    final updated = await (db.update(db.lockedOverrides)
          ..where(
            (table) =>
                table.id.equals(override.id) &
                table.ownerId.equals(ownerId.value) &
                table.revision.equals(expectedRevision.value),
          ))
        .write(_lockedOverrideCompanion(
      override,
      ownerId: ownerId,
      revision: nextRevision,
    ));
    if (updated != 1) {
      return Left(await _staleOverrideFailure(
        override.id,
        expectedRevision,
      ));
    }
    return Right(nextRevision);
  }

  /// Runs the `_blockById` 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<LockedBlockRow?> _blockById(String id) {
    return (db.select(db.lockedBlocks)..where((table) => table.id.equals(id)))
        .getSingleOrNull();
  }

  /// Runs the `_overrideById` 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<LockedOverrideRow?> _overrideById(String id) {
    return (db.select(db.lockedOverrides)
          ..where((table) => table.id.equals(id)))
        .getSingleOrNull();
  }

  /// Runs the `_checkBlockWrite` 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<Either<RepositoryFailure, LockedBlockRow>> _checkBlockWrite(
    String id,
    core.OwnerId ownerId,
    core.Revision expectedRevision,
  ) async {
    if (expectedRevision.value <= 0) {
      return Left(RepositoryInvalidRevision(
        entityId: id,
        expectedRevision: expectedRevision,
      ));
    }
    final row = await _blockById(id);
    if (row == null) return Left(RepositoryNotFound(entityId: id));
    if (row.ownerId != ownerId.value) {
      return Left(RepositoryOwnerMismatch(entityId: id));
    }
    if (row.revision != expectedRevision.value) {
      return Left(RepositoryStaleRevision(
        entityId: id,
        expectedRevision: expectedRevision,
        actualRevision: core.Revision(row.revision),
      ));
    }
    return Right(row);
  }

  /// Runs the `_checkOverrideWrite` 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<Either<RepositoryFailure, LockedOverrideRow>> _checkOverrideWrite(
    String id,
    core.OwnerId ownerId,
    core.Revision expectedRevision,
  ) async {
    if (expectedRevision.value <= 0) {
      return Left(RepositoryInvalidRevision(
        entityId: id,
        expectedRevision: expectedRevision,
      ));
    }
    final row = await _overrideById(id);
    if (row == null) return Left(RepositoryNotFound(entityId: id));
    if (row.ownerId != ownerId.value) {
      return Left(RepositoryOwnerMismatch(entityId: id));
    }
    if (row.revision != expectedRevision.value) {
      return Left(RepositoryStaleRevision(
        entityId: id,
        expectedRevision: expectedRevision,
        actualRevision: core.Revision(row.revision),
      ));
    }
    return Right(row);
  }

  /// Runs the `_staleBlockFailure` 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<RepositoryStaleRevision> _staleBlockFailure(
    String id,
    core.Revision expectedRevision,
  ) async {
    final row = await _blockById(id);
    return RepositoryStaleRevision(
      entityId: id,
      expectedRevision: expectedRevision,
      actualRevision:
          row == null ? expectedRevision.next() : core.Revision(row.revision),
    );
  }

  /// Runs the `_staleOverrideFailure` 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<RepositoryStaleRevision> _staleOverrideFailure(
    String id,
    core.Revision expectedRevision,
  ) async {
    final row = await _overrideById(id);
    return RepositoryStaleRevision(
      entityId: id,
      expectedRevision: expectedRevision,
      actualRevision:
          row == null ? expectedRevision.next() : core.Revision(row.revision),
    );
  }
}
