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

part of '../sqlite_repositories.dart';

/// Drift-backed project repository.
final class SqliteProjectRepository implements ProjectRepository {
  /// Creates a project repository using [db].
  SqliteProjectRepository(this.db);

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

  /// 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<RepoResult<core.ProjectProfile?>> findById({
    required core.OwnerId ownerId,
    required String projectId,
  }) async {
    final row = await _projectById(projectId);
    if (row == null || row.ownerId != ownerId.value) {
      return const Right(null);
    }
    return Right(_projectFromRow(row));
  }

  /// 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<RepoResult<core.Page<core.ProjectProfile>>> findByOwner({
    required core.OwnerId ownerId,
    bool includeArchived = false,
    core.PageRequest page = const core.PageRequest(),
  }) async {
    final offset = _cursorOffset(page.cursor);
    final query = db.select(db.projects)
      ..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(_projectFromRow),
      nextCursor: rows.length > page.limit ? '${offset + page.limit}' : null,
    ));
  }

  /// 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<RepoResult<core.Revision>> insert({
    required core.OwnerId ownerId,
    required core.ProjectProfile project,
  }) async {
    if (await _projectById(project.id) != null) {
      return Left(RepositoryDuplicateId(entityId: project.id));
    }
    await db.into(db.projects).insert(_projectCompanion(
          project,
          ownerId: ownerId,
          revision: core.Revision.initial,
          createdAt: _epoch,
          updatedAt: _epoch,
        ));
    return const Right(core.Revision.initial);
  }

  /// 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<RepoResult<core.Revision>> save({
    required core.OwnerId ownerId,
    required core.ProjectProfile project,
    required core.Revision expectedRevision,
  }) async {
    final checked = await _checkProjectWrite(
      project.id,
      ownerId,
      expectedRevision,
    );
    if (checked is Left<RepositoryFailure, ProjectRow>) {
      return Left(checked.value);
    }
    final row = checked.right;
    final nextRevision = core.Revision(row.revision).next();
    final updated = await (db.update(db.projects)
          ..where(
            (table) =>
                table.id.equals(project.id) &
                table.ownerId.equals(ownerId.value) &
                table.revision.equals(expectedRevision.value),
          ))
        .write(_projectCompanion(
      project,
      ownerId: ownerId,
      revision: nextRevision,
      createdAt: row.createdAtUtc,
      updatedAt: _epoch,
    ));
    if (updated != 1) {
      return Left(await _staleProjectFailure(project.id, expectedRevision));
    }
    return Right(nextRevision);
  }

  /// Runs the `archive` 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>> archive({
    required core.OwnerId ownerId,
    required String projectId,
    required core.Revision expectedRevision,
    required DateTime archivedAtUtc,
  }) async {
    final checked = await _checkProjectWrite(
      projectId,
      ownerId,
      expectedRevision,
    );
    if (checked is Left<RepositoryFailure, ProjectRow>) {
      return Left(checked.value);
    }
    final row = checked.right;
    final nextRevision = core.Revision(row.revision).next();
    final archived = _projectFromRow(row).copyWith(archivedAt: archivedAtUtc);
    final updated = await (db.update(db.projects)
          ..where(
            (table) =>
                table.id.equals(projectId) &
                table.ownerId.equals(ownerId.value) &
                table.revision.equals(expectedRevision.value),
          ))
        .write(_projectCompanion(
      archived,
      ownerId: ownerId,
      revision: nextRevision,
      createdAt: row.createdAtUtc,
      updatedAt: archivedAtUtc,
    ));
    if (updated != 1) {
      return Left(await _staleProjectFailure(projectId, expectedRevision));
    }
    return Right(nextRevision);
  }

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

  /// Runs the `_checkProjectWrite` 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, ProjectRow>> _checkProjectWrite(
    String id,
    core.OwnerId ownerId,
    core.Revision expectedRevision,
  ) async {
    if (expectedRevision.value <= 0) {
      return Left(RepositoryInvalidRevision(
        entityId: id,
        expectedRevision: expectedRevision,
      ));
    }
    final row = await _projectById(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 `_staleProjectFailure` 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> _staleProjectFailure(
    String id,
    core.Revision expectedRevision,
  ) async {
    final row = await _projectById(id);
    return RepositoryStaleRevision(
      entityId: id,
      expectedRevision: expectedRevision,
      actualRevision:
          row == null ? expectedRevision.next() : core.Revision(row.revision),
    );
  }
}
