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

part of '../sqlite_repositories.dart';

/// SQLite implementation of the app-layer locked-time repository.
final class _SqliteApplicationLockedBlockRepository
    implements core.LockedBlockRepository {
  /// Creates a locked-time repository for [db].
  _SqliteApplicationLockedBlockRepository(
    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 locked block by stable id.
  @override
  Future<core.LockedBlock?> findBlockById(String id) async {
    final row = await _blockById(id);
    return row == null ? null : _lockedBlockFromRow(row);
  }

  /// Returns a locked block plus owner/revision metadata.
  @override
  Future<core.RepositoryRecord<core.LockedBlock>?> findBlockRecordById(
    String id,
  ) async {
    final row = await _blockById(id);
    if (row == null) {
      return null;
    }
    return core.RepositoryRecord(
      value: _lockedBlockFromRow(row),
      ownerId: row.ownerId,
      revision: row.revision,
      archivedAt: _storedDateTime(row.archivedAtUtc),
    );
  }

  /// Returns every locked block.
  @override
  Future<List<core.LockedBlock>> findAllBlocks() async {
    final rows = await (db.select(db.lockedBlocks)
          ..orderBy([
            (table) => drift.OrderingTerm.asc(table.name),
            (table) => drift.OrderingTerm.asc(table.id),
          ]))
        .get();
    return List<core.LockedBlock>.unmodifiable(rows.map(_lockedBlockFromRow));
  }

  /// Returns owner-scoped locked blocks.
  @override
  Future<core.RepositoryPage<core.LockedBlock>> findBlocksByOwner({
    required String ownerId,
    bool includeArchived = false,
    core.RepositoryPageRequest page = const core.RepositoryPageRequest(),
  }) async {
    final rows = await (db.select(db.lockedBlocks)
          ..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, _lockedBlockFromRow);
  }

  /// Returns a locked override by stable id.
  @override
  Future<core.LockedBlockOverride?> findOverrideById(String id) async {
    final row = await _overrideById(id);
    return row == null ? null : _lockedOverrideFromRow(row);
  }

  /// Returns a locked override plus owner/revision metadata.
  @override
  Future<core.RepositoryRecord<core.LockedBlockOverride>?>
      findOverrideRecordById(String id) async {
    final row = await _overrideById(id);
    if (row == null) {
      return null;
    }
    return core.RepositoryRecord(
      value: _lockedOverrideFromRow(row),
      ownerId: row.ownerId,
      revision: row.revision,
    );
  }

  /// Returns every one-day override.
  @override
  Future<List<core.LockedBlockOverride>> findAllOverrides() async {
    final rows = await (db.select(db.lockedOverrides)
          ..orderBy([
            (table) => drift.OrderingTerm.asc(table.date),
            (table) => drift.OrderingTerm.asc(table.createdAtUtc),
            (table) => drift.OrderingTerm.asc(table.id),
          ]))
        .get();
    return List<core.LockedBlockOverride>.unmodifiable(
      rows.map(_lockedOverrideFromRow),
    );
  }

  /// Returns owner-scoped overrides for [date].
  @override
  Future<core.RepositoryPage<core.LockedBlockOverride>> findOverridesForDate({
    required String ownerId,
    required core.CivilDate date,
    core.RepositoryPageRequest page = const core.RepositoryPageRequest(),
  }) async {
    final rows = await (db.select(db.lockedOverrides)
          ..where(
            (table) =>
                table.ownerId.equals(ownerId) &
                table.date.equals(date.toIsoString()),
          )
          ..orderBy([
            (table) => drift.OrderingTerm.asc(table.createdAtUtc),
            (table) => drift.OrderingTerm.asc(table.id),
          ])
          ..limit(
            page.limit + 1,
            offset: _applicationCursorOffset(page.cursor),
          ))
        .get();
    return _applicationPageFromRows(rows, page, _lockedOverrideFromRow);
  }

  /// Saves [block], inserting it when absent.
  @override
  Future<void> saveBlock(core.LockedBlock block) async {
    final row = await _blockById(block.id);
    if (row == null) {
      await db.into(db.lockedBlocks).insert(
            _lockedBlockCompanion(
              block,
              ownerId: core.OwnerId(defaultOwnerId),
              revision: core.Revision.initial,
            ),
          );
      return;
    }
    await (db.update(db.lockedBlocks)
          ..where((table) => table.id.equals(block.id)))
        .write(
      _lockedBlockCompanion(
        block,
        ownerId: core.OwnerId(row.ownerId),
        revision: core.Revision(row.revision).next(),
      ),
    );
  }

  /// Saves [override], inserting it when absent.
  @override
  Future<void> saveOverride(core.LockedBlockOverride override) async {
    final row = await _overrideById(override.id);
    if (row == null) {
      await db.into(db.lockedOverrides).insert(
            _lockedOverrideCompanion(
              override,
              ownerId: core.OwnerId(defaultOwnerId),
              revision: core.Revision.initial,
            ),
          );
      return;
    }
    await (db.update(db.lockedOverrides)
          ..where((table) => table.id.equals(override.id)))
        .write(
      _lockedOverrideCompanion(
        override,
        ownerId: core.OwnerId(row.ownerId),
        revision: core.Revision(row.revision).next(),
      ),
    );
  }

  /// Inserts [block] for [ownerId] when its id is unused.
  @override
  Future<core.RepositoryMutationResult> insertBlock({
    required core.LockedBlock block,
    required String ownerId,
  }) async {
    if (await _blockById(block.id) != null) {
      return _mutationFailure(
        core.RepositoryFailureCode.duplicateId,
        entityId: block.id,
      );
    }
    await db.into(db.lockedBlocks).insert(
          _lockedBlockCompanion(
            block,
            ownerId: core.OwnerId(ownerId),
            revision: core.Revision.initial,
          ),
        );
    return core.RepositoryMutationResult.success(revision: 1);
  }

  /// Saves [block] only when the repository revision matches [expectedRevision].
  @override
  Future<core.RepositoryMutationResult> saveBlockIfRevision({
    required core.LockedBlock block,
    required String ownerId,
    required int expectedRevision,
  }) async {
    final row = await _blockById(block.id);
    final failure = _compareForWrite(
      row: row,
      ownerId: ownerId,
      expectedRevision: expectedRevision,
      entityId: block.id,
      rowOwnerId: row?.ownerId,
      rowRevision: row?.revision,
    );
    if (failure != null) {
      return failure;
    }
    final nextRevision = core.Revision(expectedRevision).next();
    await (db.update(db.lockedBlocks)
          ..where(
            (table) =>
                table.id.equals(block.id) &
                table.ownerId.equals(ownerId) &
                table.revision.equals(expectedRevision),
          ))
        .write(
      _lockedBlockCompanion(
        block,
        ownerId: core.OwnerId(ownerId),
        revision: nextRevision,
      ),
    );
    return core.RepositoryMutationResult.success(revision: nextRevision.value);
  }

  /// Archives [blockId] without deleting history.
  @override
  Future<core.RepositoryMutationResult> archiveBlock({
    required String blockId,
    required String ownerId,
    required int expectedRevision,
    required DateTime archivedAt,
  }) async {
    final block = await findBlockById(blockId);
    if (block == null) {
      return _mutationFailure(
        core.RepositoryFailureCode.notFound,
        entityId: blockId,
      );
    }
    return saveBlockIfRevision(
      block: block.copyWith(updatedAt: archivedAt, archivedAt: archivedAt),
      ownerId: ownerId,
      expectedRevision: expectedRevision,
    );
  }

  /// Inserts [override] for [ownerId] when its id is unused.
  @override
  Future<core.RepositoryMutationResult> insertOverride({
    required core.LockedBlockOverride override,
    required String ownerId,
  }) async {
    if (await _overrideById(override.id) != null) {
      return _mutationFailure(
        core.RepositoryFailureCode.duplicateId,
        entityId: override.id,
      );
    }
    await db.into(db.lockedOverrides).insert(
          _lockedOverrideCompanion(
            override,
            ownerId: core.OwnerId(ownerId),
            revision: core.Revision.initial,
          ),
        );
    return core.RepositoryMutationResult.success(revision: 1);
  }

  /// Saves [override] only when the repository revision matches [expectedRevision].
  @override
  Future<core.RepositoryMutationResult> saveOverrideIfRevision({
    required core.LockedBlockOverride override,
    required String ownerId,
    required int expectedRevision,
  }) async {
    final row = await _overrideById(override.id);
    final failure = _compareForWrite(
      row: row,
      ownerId: ownerId,
      expectedRevision: expectedRevision,
      entityId: override.id,
      rowOwnerId: row?.ownerId,
      rowRevision: row?.revision,
    );
    if (failure != null) {
      return failure;
    }
    final nextRevision = core.Revision(expectedRevision).next();
    await (db.update(db.lockedOverrides)
          ..where(
            (table) =>
                table.id.equals(override.id) &
                table.ownerId.equals(ownerId) &
                table.revision.equals(expectedRevision),
          ))
        .write(
      _lockedOverrideCompanion(
        override,
        ownerId: core.OwnerId(ownerId),
        revision: nextRevision,
      ),
    );
    return core.RepositoryMutationResult.success(revision: nextRevision.value);
  }

  /// Returns a raw block row by id.
  Future<LockedBlockRow?> _blockById(String id) {
    return (db.select(db.lockedBlocks)..where((table) => table.id.equals(id)))
        .getSingleOrNull();
  }

  /// Returns a raw override row by id.
  Future<LockedOverrideRow?> _overrideById(String id) {
    return (db.select(db.lockedOverrides)
          ..where((table) => table.id.equals(id)))
        .getSingleOrNull();
  }
}
