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

part of '../sqlite_repositories.dart';

/// SQLite implementation of the app-layer project repository.
final class _SqliteApplicationProjectRepository
    implements core.ProjectRepository {
  /// Creates a project repository for [db].
  _SqliteApplicationProjectRepository(
    this.db, {
    required this.defaultOwnerId,
  });

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

  /// Owner used for save methods without an owner parameter.
  final String defaultOwnerId;

  /// Returns a project by stable id.
  @override
  Future<core.ProjectProfile?> findById(String id) async {
    final row = await _projectById(id);
    return row == null ? null : _projectFromRow(row);
  }

  /// Returns a project plus owner/revision metadata.
  @override
  Future<core.RepositoryRecord<core.ProjectProfile>?> findRecordById(
    String id,
  ) async {
    final row = await _projectById(id);
    if (row == null) {
      return null;
    }
    return core.RepositoryRecord(
      value: _projectFromRow(row),
      ownerId: row.ownerId,
      revision: row.revision,
      archivedAt: _storedDateTime(row.archivedAtUtc),
    );
  }

  /// Returns every project in deterministic name/id order.
  @override
  Future<List<core.ProjectProfile>> findAll() async {
    final rows = await (db.select(db.projects)
          ..orderBy([
            (table) => drift.OrderingTerm.asc(table.name),
            (table) => drift.OrderingTerm.asc(table.id),
          ]))
        .get();
    return List<core.ProjectProfile>.unmodifiable(rows.map(_projectFromRow));
  }

  /// Returns owner-scoped projects.
  @override
  Future<core.RepositoryPage<core.ProjectProfile>> findByOwner({
    required String ownerId,
    bool includeArchived = false,
    core.RepositoryPageRequest page = const core.RepositoryPageRequest(),
  }) async {
    final rows = await (db.select(db.projects)
          ..where((table) {
            final ownerClause = table.ownerId.equals(ownerId);
            return includeArchived
                ? ownerClause
                : ownerClause & table.archivedAtUtc.isNull();
          })
          ..orderBy([
            (table) => drift.OrderingTerm.asc(table.name),
            (table) => drift.OrderingTerm.asc(table.id),
          ])
          ..limit(
            page.limit + 1,
            offset: _applicationCursorOffset(page.cursor),
          ))
        .get();
    return _applicationPageFromRows(rows, page, _projectFromRow);
  }

  /// Saves [project], inserting it when absent.
  @override
  Future<void> save(core.ProjectProfile project) async {
    final row = await _projectById(project.id);
    if (row == null) {
      await db.into(db.projects).insert(
            _projectCompanion(
              project,
              ownerId: core.OwnerId(defaultOwnerId),
              revision: core.Revision.initial,
              createdAt: _epoch,
              updatedAt: _epoch,
            ),
          );
      return;
    }
    final nextRevision = core.Revision(row.revision).next();
    await (db.update(db.projects)
          ..where((table) => table.id.equals(project.id)))
        .write(
      _projectCompanion(
        project,
        ownerId: core.OwnerId(row.ownerId),
        revision: nextRevision,
        createdAt: row.createdAtUtc,
        updatedAt: _epoch,
      ),
    );
  }

  /// Inserts [project] for [ownerId] when its id is unused.
  @override
  Future<core.RepositoryMutationResult> insert({
    required core.ProjectProfile project,
    required String ownerId,
  }) async {
    if (await _projectById(project.id) != null) {
      return _mutationFailure(
        core.RepositoryFailureCode.duplicateId,
        entityId: project.id,
      );
    }
    await db.into(db.projects).insert(
          _projectCompanion(
            project,
            ownerId: core.OwnerId(ownerId),
            revision: core.Revision.initial,
            createdAt: _epoch,
            updatedAt: _epoch,
          ),
        );
    return core.RepositoryMutationResult.success(revision: 1);
  }

  /// Saves [project] only when the repository revision matches [expectedRevision].
  @override
  Future<core.RepositoryMutationResult> saveIfRevision({
    required core.ProjectProfile project,
    required String ownerId,
    required int expectedRevision,
  }) async {
    final row = await _projectById(project.id);
    final failure = _compareForWrite(
      row: row,
      ownerId: ownerId,
      expectedRevision: expectedRevision,
      entityId: project.id,
      rowOwnerId: row?.ownerId,
      rowRevision: row?.revision,
    );
    if (failure != null) {
      return failure;
    }
    final nextRevision = core.Revision(expectedRevision).next();
    final updated = await (db.update(db.projects)
          ..where(
            (table) =>
                table.id.equals(project.id) &
                table.ownerId.equals(ownerId) &
                table.revision.equals(expectedRevision),
          ))
        .write(
      _projectCompanion(
        project,
        ownerId: core.OwnerId(ownerId),
        revision: nextRevision,
        createdAt: row!.createdAtUtc,
        updatedAt: _epoch,
      ),
    );
    if (updated != 1) {
      return _mutationFailure(
        core.RepositoryFailureCode.staleRevision,
        entityId: project.id,
        expectedRevision: expectedRevision,
        actualRevision: row.revision,
      );
    }
    return core.RepositoryMutationResult.success(revision: nextRevision.value);
  }

  /// Archives [projectId] without deleting task history.
  @override
  Future<core.RepositoryMutationResult> archive({
    required String projectId,
    required String ownerId,
    required int expectedRevision,
    required DateTime archivedAt,
  }) async {
    final project = await findById(projectId);
    if (project == null) {
      return _mutationFailure(
        core.RepositoryFailureCode.notFound,
        entityId: projectId,
      );
    }
    return saveIfRevision(
      project: project.copyWith(archivedAt: archivedAt),
      ownerId: ownerId,
      expectedRevision: expectedRevision,
    );
  }

  /// Returns a raw project row by id.
  Future<ProjectRow?> _projectById(String id) {
    return (db.select(db.projects)..where((table) => table.id.equals(id)))
        .getSingleOrNull();
  }
}
