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

part of '../sqlite_repositories.dart';

/// SQLite implementation of the app-layer task repository.
final class _SqliteApplicationTaskRepository implements core.TaskRepository {
  /// Creates a task repository for [db].
  _SqliteApplicationTaskRepository(
    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 task by stable id.
  @override
  Future<core.Task?> findById(String id) async {
    final row = await _taskById(id);
    return row == null ? null : _taskFromRow(row);
  }

  /// Returns a task plus owner/revision metadata.
  @override
  Future<core.RepositoryRecord<core.Task>?> findRecordById(String id) async {
    final row = await _taskById(id);
    if (row == null) {
      return null;
    }
    return core.RepositoryRecord(
      value: _taskFromRow(row),
      ownerId: row.ownerId,
      revision: row.revision,
    );
  }

  /// Returns every task in deterministic id order.
  @override
  Future<List<core.Task>> findAll() async {
    final rows = await (db.select(db.tasks)
          ..orderBy([(table) => drift.OrderingTerm.asc(table.id)]))
        .get();
    return List<core.Task>.unmodifiable(rows.map(_taskFromRow));
  }

  /// Returns tasks with [status].
  @override
  Future<List<core.Task>> findByStatus(core.TaskStatus status) async {
    final rows = await (db.select(db.tasks)
          ..where(
            (table) => table.status.equals(
              core.PersistenceEnumMapping.encodeTaskStatus(status),
            ),
          )
          ..orderBy([(table) => drift.OrderingTerm.asc(table.id)]))
        .get();
    return List<core.Task>.unmodifiable(rows.map(_taskFromRow));
  }

  /// Returns owner-scoped tasks in deterministic id order.
  @override
  Future<core.RepositoryPage<core.Task>> findByOwner({
    required String ownerId,
    core.RepositoryPageRequest page = const core.RepositoryPageRequest(),
  }) async {
    final rows = await _pagedTaskRows(
      page: page,
      where: (table) => table.ownerId.equals(ownerId),
      orderBy: [(table) => drift.OrderingTerm.asc(table.id)],
    );
    return _applicationPageFromRows(rows, page, _taskFromRow);
  }

  /// Returns owner-scoped tasks assigned to [projectId].
  @override
  Future<core.RepositoryPage<core.Task>> findByProjectId({
    required String ownerId,
    required String projectId,
    core.RepositoryPageRequest page = const core.RepositoryPageRequest(),
  }) async {
    final rows = await _pagedTaskRows(
      page: page,
      where: (table) =>
          table.ownerId.equals(ownerId) & table.projectId.equals(projectId),
      orderBy: [(table) => drift.OrderingTerm.asc(table.id)],
    );
    return _applicationPageFromRows(rows, page, _taskFromRow);
  }

  /// Returns owner-scoped children of [parentTaskId].
  @override
  Future<core.RepositoryPage<core.Task>> findByParentTaskId({
    required String ownerId,
    required String parentTaskId,
    core.RepositoryPageRequest page = const core.RepositoryPageRequest(),
  }) async {
    final rows = await _pagedTaskRows(
      page: page,
      where: (table) =>
          table.ownerId.equals(ownerId) & table.parentId.equals(parentTaskId),
      orderBy: [(table) => drift.OrderingTerm.asc(table.id)],
    );
    return _applicationPageFromRows(rows, page, _taskFromRow);
  }

  /// Returns backlog candidates matching [query].
  @override
  Future<core.RepositoryPage<core.Task>> findBacklogCandidates(
    core.BacklogCandidateQuery query,
  ) async {
    final status = core.PersistenceEnumMapping.encodeTaskStatus(
      core.TaskStatus.backlog,
    );
    final rows = await (db.select(db.tasks)
          ..where((table) {
            final projectClause = query.projectId == null
                ? const drift.Constant(true)
                : table.projectId.equals(query.projectId!);
            return table.ownerId.equals(query.ownerId) &
                table.status.equals(status) &
                projectClause;
          })
          ..orderBy([
            (table) => drift.OrderingTerm.asc(table.createdAtUtc),
            (table) => drift.OrderingTerm.asc(table.id),
          ]))
        .get();
    final tasks = rows
        .map(_taskFromRow)
        .where((task) => query.tags.every(task.backlogTags.contains))
        .toList(growable: false);
    return _applicationPage(tasks, query.page);
  }

  /// Returns tasks scheduled in [window].
  @override
  Future<List<core.Task>> findScheduledInWindow(
    core.SchedulingWindow window,
  ) async {
    final rows = await _scheduledRowsInWindow(window);
    return List<core.Task>.unmodifiable(rows.map(_taskFromRow));
  }

  /// Returns owner-scoped tasks scheduled in [window].
  @override
  Future<core.RepositoryPage<core.Task>> findScheduledInWindowForOwner({
    required String ownerId,
    required core.SchedulingWindow window,
    core.RepositoryPageRequest page = const core.RepositoryPageRequest(),
  }) async {
    final rows = await _scheduledRowsInWindow(window, ownerId: ownerId);
    final tasks = rows.map(_taskFromRow).toList(growable: false);
    return _applicationPage(tasks, page);
  }

  /// Returns owner-scoped tasks for [localDate] in [window].
  @override
  Future<core.RepositoryPage<core.Task>> findForLocalDay({
    required String ownerId,
    required core.CivilDate localDate,
    required core.SchedulingWindow window,
    core.RepositoryPageRequest page = const core.RepositoryPageRequest(),
  }) {
    return findScheduledInWindowForOwner(
      ownerId: ownerId,
      window: window,
      page: page,
    );
  }

  /// Saves [task], inserting it when absent.
  @override
  Future<void> save(core.Task task) async {
    final row = await _taskById(task.id);
    if (row == null) {
      await db.into(db.tasks).insert(
            _taskCompanion(
              task,
              ownerId: core.OwnerId(defaultOwnerId),
              revision: core.Revision.initial,
            ),
          );
      return;
    }
    final nextRevision = core.Revision(row.revision).next();
    await (db.update(db.tasks)..where((table) => table.id.equals(task.id)))
        .write(
      _taskCompanion(
        task,
        ownerId: core.OwnerId(row.ownerId),
        revision: nextRevision,
      ),
    );
  }

  /// Saves every task in [tasks].
  @override
  Future<void> saveAll(Iterable<core.Task> tasks) async {
    for (final task in tasks) {
      await save(task);
    }
  }

  /// Inserts [task] for [ownerId] when its id is unused.
  @override
  Future<core.RepositoryMutationResult> insert({
    required core.Task task,
    required String ownerId,
  }) async {
    if (await _taskById(task.id) != null) {
      return _mutationFailure(
        core.RepositoryFailureCode.duplicateId,
        entityId: task.id,
      );
    }
    await db.into(db.tasks).insert(
          _taskCompanion(
            task,
            ownerId: core.OwnerId(ownerId),
            revision: core.Revision.initial,
          ),
        );
    return core.RepositoryMutationResult.success(
      revision: core.Revision.initial.value,
    );
  }

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

  /// Deletes [taskId] only when the repository revision matches [expectedRevision].
  @override
  Future<core.RepositoryMutationResult> deleteIfRevision({
    required String taskId,
    required String ownerId,
    required int expectedRevision,
  }) async {
    final row = await _taskById(taskId);
    final failure = _compareForWrite(
      row: row,
      ownerId: ownerId,
      expectedRevision: expectedRevision,
      entityId: taskId,
      rowOwnerId: row?.ownerId,
      rowRevision: row?.revision,
    );
    if (failure != null) {
      return failure;
    }
    final nextRevision = core.Revision(expectedRevision).next();
    final deleted = await (db.delete(db.tasks)
          ..where(
            (table) =>
                table.id.equals(taskId) &
                table.ownerId.equals(ownerId) &
                table.revision.equals(expectedRevision),
          ))
        .go();
    if (deleted != 1) {
      return _mutationFailure(
        core.RepositoryFailureCode.staleRevision,
        entityId: taskId,
        expectedRevision: expectedRevision,
        actualRevision: row?.revision,
      );
    }
    return core.RepositoryMutationResult.success(revision: nextRevision.value);
  }

  /// Returns a raw task row by id.
  Future<TaskRow?> _taskById(String id) {
    return (db.select(db.tasks)..where((table) => table.id.equals(id)))
        .getSingleOrNull();
  }

  /// Returns paged task rows for the supplied [where] clause.
  Future<List<TaskRow>> _pagedTaskRows({
    required core.RepositoryPageRequest page,
    required drift.Expression<bool> Function($TasksTable table) where,
    required List<drift.OrderingTerm Function($TasksTable table)> orderBy,
  }) {
    final offset = _applicationCursorOffset(page.cursor);
    return (db.select(db.tasks)
          ..where(where)
          ..orderBy(orderBy)
          ..limit(page.limit + 1, offset: offset))
        .get();
  }

  /// Returns raw scheduled task rows overlapping [window].
  Future<List<TaskRow>> _scheduledRowsInWindow(
    core.SchedulingWindow window, {
    String? ownerId,
  }) {
    final start = window.start.toUtc();
    final end = window.end.toUtc();
    return (db.select(db.tasks)
          ..where((table) {
            final ownerClause = ownerId == null
                ? const drift.Constant(true)
                : table.ownerId.equals(ownerId);
            return ownerClause &
                table.scheduledStartUtc.isNotNull() &
                table.scheduledEndUtc.isNotNull() &
                table.scheduledStartUtc.isSmallerThanValue(end) &
                table.scheduledEndUtc.isBiggerThanValue(start);
          })
          ..orderBy([
            (table) => drift.OrderingTerm.asc(table.scheduledStartUtc),
            (table) => drift.OrderingTerm.asc(table.id),
          ]))
        .get();
  }
}
