import 'dart:convert';

import 'package:drift/drift.dart';

import '../../application/application.dart';
import '../../domain/domain.dart' as domain;
import 'app_database.dart' as db;

final class DriftExerciseRepository implements ExerciseRepository {
  const DriftExerciseRepository(this.database);

  final db.AppDatabase database;

  @override
  Future<domain.Exercise?> findById(String id) async {
    final row = await (database.select(
      database.exercises,
    )..where((table) => table.id.equals(id))).getSingleOrNull();
    return row == null
        ? null
        : _exerciseFromRow(
            row,
            await _imageMediaIdsForExercise(id),
            await _stepsForExercise(id),
            await _exerciseStarterMetadata(id),
          );
  }

  @override
  Future<List<domain.Exercise>> listActive() async {
    final rows =
        await (database.select(database.exercises)
              ..where(
                (table) => table.deletedAt.isNull() & table.archivedAt.isNull(),
              )
              ..orderBy([(table) => OrderingTerm.asc(table.name)]))
            .get();
    final exercises = <domain.Exercise>[];
    for (final row in rows) {
      exercises.add(
        _exerciseFromRow(
          row,
          await _imageMediaIdsForExercise(row.id),
          await _stepsForExercise(row.id),
          await _exerciseStarterMetadata(row.id),
        ),
      );
    }
    return exercises;
  }

  @override
  Future<bool> isReferencedByProgram(String id) async {
    final row =
        await (database.select(database.programExercises)
              ..where(
                (table) =>
                    table.deletedAt.isNull() &
                    table.sourceExerciseId.equals(id),
              )
              ..limit(1))
            .getSingleOrNull();
    return row != null;
  }

  @override
  Future<void> save(domain.Exercise exercise) async {
    await database.transaction(() async {
      await _upsertWithChangeLog(
        database: database,
        tableName: 'exercises',
        entityType: 'Exercise',
        metadata: exercise.metadata,
        write: () => database
            .into(database.exercises)
            .insertOnConflictUpdate(_exerciseCompanion(exercise)),
      );
      await _writeExerciseStarterMetadata(database, exercise);
      await _replaceExerciseImages(database, exercise);
      await _replaceExerciseSteps(database, exercise);
    });
  }

  Future<List<String>> _imageMediaIdsForExercise(String exerciseId) async {
    final rows =
        await (database.select(database.exerciseImages)
              ..where(
                (table) =>
                    table.exerciseId.equals(exerciseId) &
                    table.deletedAt.isNull(),
              )
              ..orderBy([(table) => OrderingTerm.asc(table.position)]))
            .get();
    return rows.map((row) => row.mediaAssetId).toList();
  }

  Future<List<domain.ExerciseStep>> _stepsForExercise(String exerciseId) async {
    final rows =
        await (database.select(database.exerciseSteps)
              ..where(
                (table) =>
                    table.exerciseId.equals(exerciseId) &
                    table.deletedAt.isNull(),
              )
              ..orderBy([(table) => OrderingTerm.asc(table.position)]))
            .get();
    return rows.map(_exerciseStepFromRow).toList();
  }

  Future<_ExerciseStarterMetadata> _exerciseStarterMetadata(
    String exerciseId,
  ) async {
    final row = await database
        .customSelect(
          'SELECT category, is_example FROM exercises WHERE id = ? LIMIT 1',
          variables: [Variable<String>(exerciseId)],
        )
        .getSingleOrNull();
    if (row == null) {
      return const _ExerciseStarterMetadata();
    }
    return _ExerciseStarterMetadata(
      category: _exerciseCategoryFromDb(
        row.readNullable<String>('category') ?? 'uncategorized',
      ),
      isExample: (row.readNullable<int>('is_example') ?? 0) == 1,
    );
  }
}

final class DriftStarterSeedRepository
    implements StarterSeedStateRepository, StarterContentRepository {
  const DriftStarterSeedRepository(this.database);

  static const _starterSeedKey = 'starter';

  final db.AppDatabase database;

  @override
  Future<int> readAppliedStarterSeedVersion() async {
    final row = await database
        .customSelect(
          'SELECT version FROM local_seed_metadata WHERE key = ? LIMIT 1',
          variables: [Variable<String>(_starterSeedKey)],
        )
        .getSingleOrNull();
    return row?.read<int>('version') ?? 0;
  }

  @override
  Future<void> writeAppliedStarterSeedVersion(
    int version,
    DateTime appliedAt,
  ) async {
    await database.customStatement(
      'INSERT INTO local_seed_metadata (key, version, applied_at) '
      'VALUES (?, ?, ?) '
      'ON CONFLICT(key) DO UPDATE SET '
      'version = excluded.version, applied_at = excluded.applied_at',
      [_starterSeedKey, version, appliedAt.toUtc().millisecondsSinceEpoch],
    );
  }

  @override
  Future<bool> isLocalContentEmpty() async {
    final exerciseCount = await _tableCount(database.exercises.actualTableName);
    final programCount = await _tableCount(database.programs.actualTableName);
    final templateCount = await _tableCount(
      database.workoutTemplates.actualTableName,
    );
    return exerciseCount == 0 && programCount == 0 && templateCount == 0;
  }

  @override
  Future<void> insertStarterContent(StarterContent content) async {
    await database.transaction(() async {
      for (final exercise in content.exercises) {
        await database
            .into(database.exercises)
            .insertOnConflictUpdate(_exerciseCompanion(exercise));
        await _writeExerciseStarterMetadata(database, exercise);
        await _replaceExerciseSteps(database, exercise);
      }
      await database
          .into(database.programs)
          .insertOnConflictUpdate(_programCompanion(content.program));
      await _writeProgramStarterMetadata(database, content.program);
      for (final exercise in content.program.exercises) {
        await database
            .into(database.programExercises)
            .insertOnConflictUpdate(_programExerciseCompanion(exercise));
      }
      await database
          .into(database.workoutTemplates)
          .insertOnConflictUpdate(
            _workoutTemplateCompanion(content.workoutTemplate),
          );
      await _writeWorkoutTemplateStarterMetadata(
        database,
        content.workoutTemplate,
      );
      for (final program in content.workoutTemplate.programs) {
        await database
            .into(database.workoutTemplatePrograms)
            .insertOnConflictUpdate(_workoutTemplateProgramCompanion(program));
      }
    });
  }

  Future<int> _tableCount(String tableName) async {
    final row = await database
        .customSelect('SELECT COUNT(*) AS count FROM $tableName')
        .getSingle();
    return row.read<int>('count');
  }
}

final class DriftMediaAssetRepository implements MediaAssetRepository {
  const DriftMediaAssetRepository(this.database);

  final db.AppDatabase database;

  @override
  Future<domain.MediaAsset?> findById(String id) async {
    final row = await (database.select(
      database.mediaAssets,
    )..where((table) => table.id.equals(id))).getSingleOrNull();
    return row == null ? null : _mediaAssetFromRow(row);
  }

  @override
  Future<List<domain.MediaAsset>> listActive() async {
    final rows = await (database.select(
      database.mediaAssets,
    )..where((table) => table.deletedAt.isNull())).get();
    return rows.map(_mediaAssetFromRow).toList();
  }

  @override
  Future<void> save(domain.MediaAsset mediaAsset) async {
    await _upsertWithChangeLog(
      database: database,
      tableName: 'media_assets',
      entityType: 'MediaAsset',
      metadata: mediaAsset.metadata,
      write: () => database
          .into(database.mediaAssets)
          .insertOnConflictUpdate(_mediaAssetCompanion(mediaAsset)),
    );
  }
}

final class DriftOnlineAccountRepository implements OnlineAccountRepository {
  const DriftOnlineAccountRepository(this.database);

  final db.AppDatabase database;

  @override
  Future<domain.UserAccountSession?> currentSession() async {
    final row =
        await (database.select(database.onlineAccountSessions)
              ..orderBy([(table) => OrderingTerm.desc(table.updatedAt)])
              ..limit(1))
            .getSingleOrNull();
    return row == null ? null : _userAccountSessionFromRow(row);
  }

  @override
  Future<void> saveSession(domain.UserAccountSession session) async {
    await database
        .into(database.onlineAccountSessions)
        .insertOnConflictUpdate(_userAccountSessionCompanion(session));
  }

  @override
  Future<void> clearSession() async {
    await database.delete(database.onlineAccountSessions).go();
  }
}

final class DriftSyncMetadataRepository implements SyncMetadataRepository {
  const DriftSyncMetadataRepository(this.database);

  static const _id = 'singleton';

  final db.AppDatabase database;

  @override
  Future<SyncMetadataSnapshot> read() async {
    final row = await (database.select(
      database.syncMetadataEntries,
    )..where((table) => table.id.equals(_id))).getSingleOrNull();
    if (row == null) {
      return const SyncMetadataSnapshot();
    }
    return SyncMetadataSnapshot(
      serverCursor: row.serverCursor,
      lastSuccessfulSyncAt: _utcOrNull(row.lastSuccessfulSyncAt),
      lastAttemptAt: _utcOrNull(row.lastAttemptAt),
      lastFailureAt: _utcOrNull(row.lastFailureAt),
      status: _onlineSyncStatusFromDb(row.status),
      pendingPushCount: row.pendingPushCount,
    );
  }

  @override
  Future<void> save(SyncMetadataSnapshot metadata) async {
    await database
        .into(database.syncMetadataEntries)
        .insertOnConflictUpdate(
          db.SyncMetadataEntriesCompanion.insert(
            id: const Value(_id),
            serverCursor: Value<String?>(metadata.serverCursor),
            lastSuccessfulSyncAt: Value<DateTime?>(
              _utcOrNull(metadata.lastSuccessfulSyncAt),
            ),
            lastAttemptAt: Value<DateTime?>(_utcOrNull(metadata.lastAttemptAt)),
            lastFailureAt: Value<DateTime?>(_utcOrNull(metadata.lastFailureAt)),
            status: Value(_onlineSyncStatusToDb(metadata.status)),
            pendingPushCount: Value<int?>(metadata.pendingPushCount),
          ),
        );
  }
}

final class DriftRemoteResourceMappingRepository
    implements RemoteResourceMappingRepository {
  const DriftRemoteResourceMappingRepository(this.database);

  final db.AppDatabase database;

  @override
  Future<RemoteResourceMapping?> find({
    required SyncResourceType resourceType,
    required String clientId,
  }) async {
    final row =
        await (database.select(database.remoteResourceMappings)..where(
              (table) =>
                  table.resourceType.equals(
                    _syncResourceTypeToDb(resourceType),
                  ) &
                  table.clientId.equals(clientId),
            ))
            .getSingleOrNull();
    return row == null ? null : _remoteResourceMappingFromRow(row);
  }

  @override
  Future<void> save(RemoteResourceMapping mapping) async {
    await database
        .into(database.remoteResourceMappings)
        .insertOnConflictUpdate(_remoteResourceMappingCompanion(mapping));
  }
}

final class DriftLocalSyncChangeRepository
    implements LocalSyncChangeRepository {
  const DriftLocalSyncChangeRepository(this.database);

  final db.AppDatabase database;

  @override
  Future<List<PendingSyncChange>> listPendingChanges() async {
    final rows =
        await (database.select(database.changeLogEntries)
              ..where(
                (table) =>
                    table.syncedAt.isNull() &
                    table.entityType.isIn(_syncableEntityTypes),
              )
              ..orderBy([(table) => OrderingTerm.asc(table.createdAt)]))
            .get();
    final grouped = <String, List<db.ChangeLogEntry>>{};
    for (final row in rows) {
      grouped
          .putIfAbsent('${row.entityType}:${row.entityId}', () => [])
          .add(row);
    }
    final changes = <PendingSyncChange>[];
    for (final group in grouped.values) {
      group.sort((left, right) => left.createdAt.compareTo(right.createdAt));
      final latest = group.last;
      final item = await _pushItemForChange(latest);
      if (item == null) {
        continue;
      }
      changes.add(
        PendingSyncChange(
          changeLogIds: group.map((row) => row.id).toList(),
          item: item,
        ),
      );
    }
    return changes;
  }

  @override
  Future<void> markChangesSynced(
    List<String> changeLogIds,
    DateTime syncedAt,
  ) async {
    if (changeLogIds.isEmpty) {
      return;
    }
    await (database.update(
      database.changeLogEntries,
    )..where((table) => table.id.isIn(changeLogIds))).write(
      db.ChangeLogEntriesCompanion(
        syncedAt: Value<DateTime?>(syncedAt.toUtc()),
      ),
    );
  }

  @override
  Future<bool> applyRemoteItem(RemoteSyncedItem item) async {
    final localUpdatedAt = await _localUpdatedAt(
      item.resourceType,
      item.clientId,
    );
    if (localUpdatedAt != null &&
        !item.clientUpdatedAt.isAfter(localUpdatedAt)) {
      return false;
    }
    if (item.deletedAt != null) {
      return _applyRemoteDelete(item);
    }
    return _applyRemotePayload(item);
  }

  Future<RemoteSyncPushItem?> _pushItemForChange(
    db.ChangeLogEntry change,
  ) async {
    final resourceType = _syncResourceTypeFromEntityType(change.entityType);
    if (resourceType == null) {
      return null;
    }
    final snapshot = await _localSnapshot(resourceType, change.entityId);
    return RemoteSyncPushItem(
      resourceType: resourceType,
      clientId: change.entityId,
      schemaVersion: snapshot.schemaVersion,
      clientUpdatedAt: snapshot.updatedAt,
      deletedAt: snapshot.deletedAt,
      payload: snapshot.payload,
    );
  }

  Future<_LocalSyncSnapshot> _localSnapshot(
    SyncResourceType resourceType,
    String id,
  ) async {
    return switch (resourceType) {
      SyncResourceType.exercise => _exerciseSnapshot(id),
      SyncResourceType.program => _programSnapshot(id),
      SyncResourceType.workoutTemplate => _workoutTemplateSnapshot(id),
      SyncResourceType.workoutHistory => _workoutHistorySnapshot(id),
      SyncResourceType.mediaAsset => _mediaAssetSnapshot(id),
    };
  }

  Future<DateTime?> _localUpdatedAt(
    SyncResourceType resourceType,
    String id,
  ) async {
    final tableName = _tableNameForResourceType(resourceType);
    final row = await database
        .customSelect(
          'SELECT updated_at FROM $tableName WHERE id = ? LIMIT 1',
          variables: [Variable<String>(id)],
        )
        .getSingleOrNull();
    final value = row?.data['updated_at'];
    return value is DateTime ? value.toUtc() : null;
  }

  Future<bool> _applyRemoteDelete(RemoteSyncedItem item) async {
    final tableName = _tableNameForResourceType(item.resourceType);
    final updated = await database.customUpdate(
      'UPDATE $tableName SET deleted_at = ?, updated_at = ?, '
      "sync_state = 'deleted' WHERE id = ?",
      variables: [
        Variable<DateTime>(item.deletedAt!.toUtc()),
        Variable<DateTime>(item.clientUpdatedAt.toUtc()),
        Variable<String>(item.clientId),
      ],
    );
    return updated > 0;
  }

  Future<bool> _applyRemotePayload(RemoteSyncedItem item) async {
    switch (item.resourceType) {
      case SyncResourceType.exercise:
        final exercise = _exerciseFromPayload(item);
        await database
            .into(database.exercises)
            .insertOnConflictUpdate(_exerciseCompanion(exercise));
        await _writeExerciseStarterMetadata(database, exercise);
        return true;
      case SyncResourceType.mediaAsset:
        await database
            .into(database.mediaAssets)
            .insertOnConflictUpdate(
              _mediaAssetCompanion(_mediaAssetFromPayload(item)),
            );
        return true;
      case SyncResourceType.program:
        final program = _programFromPayload(item);
        await database.transaction(() async {
          await database
              .into(database.programs)
              .insertOnConflictUpdate(_programCompanion(program));
          await _writeProgramStarterMetadata(database, program);
          for (final exercise in program.exercises) {
            await database
                .into(database.programExercises)
                .insertOnConflictUpdate(_programExerciseCompanion(exercise));
          }
        });
        return true;
      case SyncResourceType.workoutTemplate:
        final template = _workoutTemplateFromPayload(item);
        await database.transaction(() async {
          await database
              .into(database.workoutTemplates)
              .insertOnConflictUpdate(_workoutTemplateCompanion(template));
          await _writeWorkoutTemplateStarterMetadata(database, template);
          for (final program in template.programs) {
            await database
                .into(database.workoutTemplatePrograms)
                .insertOnConflictUpdate(
                  _workoutTemplateProgramCompanion(program),
                );
          }
          for (final override in template.overrides) {
            await database
                .into(database.workoutTemplateExerciseOverrides)
                .insertOnConflictUpdate(
                  _workoutTemplateExerciseOverrideCompanion(override),
                );
          }
        });
        return true;
      case SyncResourceType.workoutHistory:
        return false;
    }
  }

  Future<_LocalSyncSnapshot> _exerciseSnapshot(String id) async {
    final row = await (database.select(
      database.exercises,
    )..where((table) => table.id.equals(id))).getSingle();
    final exercise = _exerciseFromRow(
      row,
      await DriftExerciseRepository(database)._imageMediaIdsForExercise(id),
      await DriftExerciseRepository(database)._stepsForExercise(id),
      await DriftExerciseRepository(database)._exerciseStarterMetadata(id),
    );
    return _LocalSyncSnapshot.fromMetadata(
      exercise.metadata,
      _exercisePayload(exercise),
    );
  }

  Future<_LocalSyncSnapshot> _mediaAssetSnapshot(String id) async {
    final row = await (database.select(
      database.mediaAssets,
    )..where((table) => table.id.equals(id))).getSingle();
    final asset = _mediaAssetFromRow(row);
    return _LocalSyncSnapshot.fromMetadata(
      asset.metadata,
      _mediaAssetPayload(asset),
    );
  }

  Future<_LocalSyncSnapshot> _programSnapshot(String id) async {
    final program = await DriftProgramRepository(database).findById(id);
    if (program == null) {
      return _deletedFallbackSnapshot(SyncResourceType.program, id);
    }
    return _LocalSyncSnapshot.fromMetadata(
      program.metadata,
      _programPayload(program),
    );
  }

  Future<_LocalSyncSnapshot> _workoutTemplateSnapshot(String id) async {
    final template = await DriftWorkoutTemplateRepository(
      database,
    ).findById(id);
    if (template == null) {
      return _deletedFallbackSnapshot(SyncResourceType.workoutTemplate, id);
    }
    return _LocalSyncSnapshot.fromMetadata(
      template.metadata,
      _workoutTemplatePayload(template),
    );
  }

  Future<_LocalSyncSnapshot> _workoutHistorySnapshot(String id) async {
    final history = await DriftWorkoutHistoryRepository(database).findById(id);
    if (history == null) {
      return _deletedFallbackSnapshot(SyncResourceType.workoutHistory, id);
    }
    return _LocalSyncSnapshot.fromMetadata(
      history.metadata,
      _workoutHistoryPayload(history),
    );
  }

  Future<_LocalSyncSnapshot> _deletedFallbackSnapshot(
    SyncResourceType resourceType,
    String id,
  ) async {
    final tableName = _tableNameForResourceType(resourceType);
    final row = await database
        .customSelect(
          'SELECT updated_at, deleted_at, schema_version FROM $tableName '
          'WHERE id = ? LIMIT 1',
          variables: [Variable<String>(id)],
        )
        .getSingle();
    return _LocalSyncSnapshot(
      schemaVersion: row.data['schema_version'] as int? ?? 1,
      updatedAt: (row.data['updated_at'] as DateTime).toUtc(),
      deletedAt: (row.data['deleted_at'] as DateTime?)?.toUtc(),
      payload: {'id': id},
    );
  }
}

final class DriftShareInboxRepository implements ShareInboxRepository {
  const DriftShareInboxRepository(this.database);

  final db.AppDatabase database;

  @override
  Future<List<domain.ShareInboxItem>> listAll() async {
    final rows = await (database.select(
      database.shareInboxItems,
    )..orderBy([(table) => OrderingTerm.desc(table.createdAt)])).get();
    return rows.map(_shareInboxItemFromRow).toList(growable: false);
  }

  @override
  Future<domain.ShareInboxItem?> findByShareId(String shareId) async {
    final row = await (database.select(
      database.shareInboxItems,
    )..where((table) => table.shareId.equals(shareId))).getSingleOrNull();
    return row == null ? null : _shareInboxItemFromRow(row);
  }

  @override
  Future<void> upsert(domain.ShareInboxItem item) async {
    await database
        .into(database.shareInboxItems)
        .insertOnConflictUpdate(_shareInboxItemCompanion(item));
  }

  @override
  Future<void> upsertAll(List<domain.ShareInboxItem> items) async {
    await database.batch((batch) {
      batch.insertAllOnConflictUpdate(
        database.shareInboxItems,
        items.map(_shareInboxItemCompanion).toList(),
      );
    });
  }

  @override
  Future<void> markStatus(
    String shareId,
    domain.ShareInboxStatus status,
    DateTime respondedAt,
  ) async {
    await (database.update(
      database.shareInboxItems,
    )..where((table) => table.shareId.equals(shareId))).write(
      db.ShareInboxItemsCompanion(
        status: Value<String>(_shareInboxStatusToDb(status)),
        respondedAt: Value<DateTime?>(respondedAt.toUtc()),
        updatedAt: Value<DateTime>(respondedAt.toUtc()),
      ),
    );
  }
}

final class DriftPendingShareActionRepository
    implements PendingShareActionRepository {
  const DriftPendingShareActionRepository(this.database);

  final db.AppDatabase database;

  @override
  Future<List<domain.PendingShareAction>> listPending() async {
    final rows =
        await (database.select(database.pendingShareActions)
              ..where(
                (table) => table.status.isIn([
                  _pendingShareActionStatusToDb(
                    domain.PendingShareActionStatus.pending,
                  ),
                  _pendingShareActionStatusToDb(
                    domain.PendingShareActionStatus.failed,
                  ),
                ]),
              )
              ..orderBy([(table) => OrderingTerm.asc(table.createdAt)]))
            .get();
    return rows.map(_pendingShareActionFromRow).toList(growable: false);
  }

  @override
  Future<void> add(domain.PendingShareAction action) async {
    await database
        .into(database.pendingShareActions)
        .insertOnConflictUpdate(_pendingShareActionCompanion(action));
  }

  @override
  Future<void> markSucceeded(String id, DateTime attemptedAt) async {
    await _markActionAttempt(
      id,
      attemptedAt,
      domain.PendingShareActionStatus.succeeded,
    );
  }

  @override
  Future<void> markFailed(String id, DateTime attemptedAt) async {
    await _markActionAttempt(
      id,
      attemptedAt,
      domain.PendingShareActionStatus.failed,
    );
  }

  Future<void> _markActionAttempt(
    String id,
    DateTime attemptedAt,
    domain.PendingShareActionStatus status,
  ) async {
    final row = await (database.select(
      database.pendingShareActions,
    )..where((table) => table.id.equals(id))).getSingleOrNull();
    await (database.update(
      database.pendingShareActions,
    )..where((table) => table.id.equals(id))).write(
      db.PendingShareActionsCompanion(
        lastAttemptAt: Value<DateTime?>(attemptedAt.toUtc()),
        attemptCount: Value<int>((row?.attemptCount ?? 0) + 1),
        status: Value<String>(_pendingShareActionStatusToDb(status)),
      ),
    );
  }
}

final class DriftProgramRepository implements ProgramRepository {
  const DriftProgramRepository(this.database);

  final db.AppDatabase database;

  @override
  Future<domain.Program?> findById(String id) async {
    final query = database.select(database.programs)
      ..where((table) => table.id.equals(id) & table.deletedAt.isNull());
    final row = await query.getSingleOrNull();
    if (row == null) {
      return null;
    }
    final exercises =
        await (database.select(database.programExercises)
              ..where(
                (table) =>
                    table.programId.equals(id) & table.deletedAt.isNull(),
              )
              ..orderBy([(table) => OrderingTerm.asc(table.position)]))
            .get();
    return _programFromRow(
      row,
      exercises.map(_programExerciseFromRow).toList(),
      isExample: await _isProgramExample(id),
    );
  }

  @override
  Future<List<domain.Program>> listActive() async {
    final rows =
        await (database.select(database.programs)
              ..where((table) => table.deletedAt.isNull())
              ..orderBy([(table) => OrderingTerm.asc(table.name)]))
            .get();
    final programs = <domain.Program>[];
    for (final row in rows) {
      final exercises =
          await (database.select(database.programExercises)
                ..where(
                  (table) =>
                      table.programId.equals(row.id) & table.deletedAt.isNull(),
                )
                ..orderBy([(table) => OrderingTerm.asc(table.position)]))
              .get();
      programs.add(
        _programFromRow(
          row,
          exercises.map(_programExerciseFromRow).toList(),
          isExample: await _isProgramExample(row.id),
        ),
      );
    }
    return programs;
  }

  @override
  Future<void> save(domain.Program program) async {
    await database.transaction(() async {
      await _upsertWithChangeLog(
        database: database,
        tableName: 'programs',
        entityType: 'Program',
        metadata: program.metadata,
        write: () => database
            .into(database.programs)
            .insertOnConflictUpdate(_programCompanion(program)),
      );
      await _writeProgramStarterMetadata(database, program);
      for (final exercise in program.exercises) {
        await saveExercise(exercise);
      }
    });
  }

  @override
  Future<void> saveExercise(domain.ProgramExercise exercise) async {
    await _upsertWithChangeLog(
      database: database,
      tableName: 'program_exercises',
      entityType: 'ProgramExercise',
      metadata: exercise.metadata,
      write: () => database
          .into(database.programExercises)
          .insertOnConflictUpdate(_programExerciseCompanion(exercise)),
    );
  }

  @override
  Future<void> replaceExercises(
    domain.Program program,
    DateTime deletedAt,
  ) async {
    await database.transaction(() async {
      await save(program);
      final activeIds = program.exercises
          .map((exercise) => exercise.metadata.id)
          .toSet();
      final staleQuery = database.select(database.programExercises);
      if (activeIds.isEmpty) {
        staleQuery.where(
          (table) =>
              table.programId.equals(program.metadata.id) &
              table.deletedAt.isNull(),
        );
      } else {
        staleQuery.where(
          (table) =>
              table.programId.equals(program.metadata.id) &
              table.deletedAt.isNull() &
              table.id.isNotIn(activeIds),
        );
      }
      await _softDeleteProgramExerciseRows(
        database,
        await staleQuery.get(),
        deletedAt,
      );
    });
  }

  Future<bool> _isProgramExample(String id) async {
    final row = await database
        .customSelect(
          'SELECT is_example FROM programs WHERE id = ? LIMIT 1',
          variables: [Variable<String>(id)],
        )
        .getSingleOrNull();
    return (row?.readNullable<int>('is_example') ?? 0) == 1;
  }
}

final class DriftWorkoutTemplateRepository
    implements WorkoutTemplateRepository {
  const DriftWorkoutTemplateRepository(this.database);

  final db.AppDatabase database;

  @override
  Future<domain.WorkoutTemplate?> findById(String id) async {
    final row =
        await (database.select(
              database.workoutTemplates,
            )..where((table) => table.id.equals(id) & table.deletedAt.isNull()))
            .getSingleOrNull();
    if (row == null) {
      return null;
    }
    final programs =
        await (database.select(database.workoutTemplatePrograms)
              ..where(
                (table) =>
                    table.workoutTemplateId.equals(id) &
                    table.deletedAt.isNull(),
              )
              ..orderBy([(table) => OrderingTerm.asc(table.position)]))
            .get();
    final programIds = programs.map((program) => program.id).toSet();
    final overrides = programIds.isEmpty
        ? <db.WorkoutTemplateExerciseOverride>[]
        : await (database.select(database.workoutTemplateExerciseOverrides)
                ..where(
                  (table) =>
                      table.workoutTemplateProgramId.isIn(programIds) &
                      table.deletedAt.isNull(),
                ))
              .get();
    return _workoutTemplateFromRow(
      row,
      programs.map(_workoutTemplateProgramFromRow).toList(),
      overrides.map(_workoutTemplateExerciseOverrideFromRow).toList(),
      isExample: await _isWorkoutTemplateExample(id),
    );
  }

  @override
  Future<List<domain.WorkoutTemplate>> listActive() async {
    final rows =
        await (database.select(database.workoutTemplates)
              ..where((table) => table.deletedAt.isNull())
              ..orderBy([(table) => OrderingTerm.asc(table.name)]))
            .get();
    final templates = <domain.WorkoutTemplate>[];
    for (final row in rows) {
      final template = await findById(row.id);
      if (template != null) {
        templates.add(template);
      }
    }
    return templates;
  }

  @override
  Future<void> save(domain.WorkoutTemplate template) async {
    await database.transaction(() async {
      await _upsertWithChangeLog(
        database: database,
        tableName: 'workout_templates',
        entityType: 'WorkoutTemplate',
        metadata: template.metadata,
        write: () => database
            .into(database.workoutTemplates)
            .insertOnConflictUpdate(_workoutTemplateCompanion(template)),
      );
      await _writeWorkoutTemplateStarterMetadata(database, template);
      for (final program in template.programs) {
        await saveProgram(program);
      }
      for (final override in template.overrides) {
        await saveOverride(override);
      }
    });
  }

  @override
  Future<void> saveProgram(domain.WorkoutTemplateProgram program) async {
    await _upsertWithChangeLog(
      database: database,
      tableName: 'workout_template_programs',
      entityType: 'WorkoutTemplateProgram',
      metadata: program.metadata,
      write: () => database
          .into(database.workoutTemplatePrograms)
          .insertOnConflictUpdate(_workoutTemplateProgramCompanion(program)),
    );
  }

  @override
  Future<void> saveOverride(
    domain.WorkoutTemplateExerciseOverride override,
  ) async {
    await _upsertWithChangeLog(
      database: database,
      tableName: 'workout_template_exercise_overrides',
      entityType: 'WorkoutTemplateExerciseOverride',
      metadata: override.metadata,
      write: () => database
          .into(database.workoutTemplateExerciseOverrides)
          .insertOnConflictUpdate(
            _workoutTemplateExerciseOverrideCompanion(override),
          ),
    );
  }

  @override
  Future<void> replaceComposition(
    domain.WorkoutTemplate template,
    DateTime deletedAt,
  ) async {
    await database.transaction(() async {
      final previousPrograms =
          await (database.select(database.workoutTemplatePrograms)..where(
                (table) =>
                    table.workoutTemplateId.equals(template.metadata.id) &
                    table.deletedAt.isNull(),
              ))
              .get();
      final previousProgramIds = previousPrograms
          .map((program) => program.id)
          .toSet();
      await save(template);
      final activeProgramIds = template.programs
          .map((program) => program.metadata.id)
          .toSet();
      final staleProgramsQuery = database.select(
        database.workoutTemplatePrograms,
      );
      if (activeProgramIds.isEmpty) {
        staleProgramsQuery.where(
          (table) =>
              table.workoutTemplateId.equals(template.metadata.id) &
              table.deletedAt.isNull(),
        );
      } else {
        staleProgramsQuery.where(
          (table) =>
              table.workoutTemplateId.equals(template.metadata.id) &
              table.deletedAt.isNull() &
              table.id.isNotIn(activeProgramIds),
        );
      }
      await _softDeleteWorkoutTemplateProgramRows(
        database,
        await staleProgramsQuery.get(),
        deletedAt,
      );

      final activeOverrideIds = template.overrides
          .map((override) => override.metadata.id)
          .toSet();
      final relevantProgramIds = {...previousProgramIds, ...activeProgramIds};
      if (relevantProgramIds.isEmpty) {
        return;
      }
      final staleOverridesQuery = database.select(
        database.workoutTemplateExerciseOverrides,
      );
      if (activeOverrideIds.isEmpty) {
        staleOverridesQuery.where(
          (table) =>
              table.workoutTemplateProgramId.isIn(relevantProgramIds) &
              table.deletedAt.isNull(),
        );
      } else {
        staleOverridesQuery.where(
          (table) =>
              table.workoutTemplateProgramId.isIn(relevantProgramIds) &
              table.deletedAt.isNull() &
              table.id.isNotIn(activeOverrideIds),
        );
      }
      await _softDeleteWorkoutTemplateExerciseOverrideRows(
        database,
        await staleOverridesQuery.get(),
        deletedAt,
      );
    });
  }

  Future<bool> _isWorkoutTemplateExample(String id) async {
    final row = await database
        .customSelect(
          'SELECT is_example FROM workout_templates WHERE id = ? LIMIT 1',
          variables: [Variable<String>(id)],
        )
        .getSingleOrNull();
    return (row?.readNullable<int>('is_example') ?? 0) == 1;
  }
}

final class DriftActiveSessionRepository implements ActiveSessionRepository {
  const DriftActiveSessionRepository(this.database);

  final db.AppDatabase database;

  @override
  Future<domain.ActiveWorkoutSession?> findById(String id) async {
    final row = await (database.select(
      database.activeWorkoutSessions,
    )..where((table) => table.id.equals(id))).getSingleOrNull();
    return row == null ? null : _activeWorkoutSessionFromRow(row);
  }

  @override
  Future<domain.ActiveWorkoutSession?> findOpen() async {
    final row =
        await (database.select(database.activeWorkoutSessions)
              ..where(
                (table) =>
                    table.deletedAt.isNull() &
                    table.status.isIn(['running', 'paused', 'savedExit']),
              )
              ..orderBy([(table) => OrderingTerm.desc(table.updatedAt)])
              ..limit(1))
            .getSingleOrNull();
    return row == null ? null : _activeWorkoutSessionFromRow(row);
  }

  @override
  Future<void> save(domain.ActiveWorkoutSession session) async {
    await _upsertWithChangeLog(
      database: database,
      tableName: 'active_workout_sessions',
      entityType: 'ActiveWorkoutSession',
      metadata: session.metadata,
      write: () => database
          .into(database.activeWorkoutSessions)
          .insertOnConflictUpdate(_activeWorkoutSessionCompanion(session)),
    );
  }

  @override
  Future<void> saveSetResult(domain.ActiveSetResult result) async {
    await _upsertWithChangeLog(
      database: database,
      tableName: 'active_set_results',
      entityType: 'ActiveSetResult',
      metadata: result.metadata,
      write: () => database
          .into(database.activeSetResults)
          .insertOnConflictUpdate(_activeSetResultCompanion(result)),
    );
  }

  @override
  Future<void> saveSetTimerState(domain.ActiveSetTimerState state) async {
    await _upsertWithChangeLog(
      database: database,
      tableName: 'active_set_timer_states',
      entityType: 'ActiveSetTimerState',
      metadata: state.metadata,
      write: () => _upsertActiveSetTimerState(database, state),
    );
  }

  @override
  Future<void> saveRestState(domain.ActiveRestState restState) async {
    await _upsertWithChangeLog(
      database: database,
      tableName: 'active_rest_states',
      entityType: 'ActiveRestState',
      metadata: restState.metadata,
      write: () async {
        await database
            .into(database.activeRestStates)
            .insertOnConflictUpdate(_activeRestStateCompanion(restState));
        await _updateActiveRestPauseFields(database, restState);
      },
    );
  }

  @override
  Future<domain.ActiveRestState?> findRestStateById(String id) async {
    final row = await database
        .customSelect(
          'SELECT * FROM active_rest_states WHERE id = ? LIMIT 1',
          variables: [Variable<String>(id)],
        )
        .getSingleOrNull();
    return row == null ? null : _activeRestStateFromCustomRow(row);
  }

  @override
  Future<void> saveScoreStopwatchState(
    domain.ActiveScoreStopwatchState state,
  ) async {
    await _upsertWithChangeLog(
      database: database,
      tableName: 'active_score_stopwatch_states',
      entityType: 'ActiveScoreStopwatchState',
      metadata: state.metadata,
      write: () => database
          .into(database.activeScoreStopwatchStates)
          .insertOnConflictUpdate(_activeScoreStopwatchStateCompanion(state)),
    );
  }

  @override
  Future<void> saveExerciseStepProgressState(
    domain.ActiveExerciseStepProgressState state,
  ) async {
    await _upsertWithChangeLog(
      database: database,
      tableName: 'active_exercise_step_progress_states',
      entityType: 'ActiveExerciseStepProgressState',
      metadata: state.metadata,
      write: () => database
          .into(database.activeExerciseStepProgressStates)
          .insertOnConflictUpdate(
            _activeExerciseStepProgressStateCompanion(state),
          ),
    );
  }

  @override
  Future<void> saveExerciseStepResult(
    domain.ActiveExerciseStepResult result,
  ) async {
    await _upsertWithChangeLog(
      database: database,
      tableName: 'active_exercise_step_results',
      entityType: 'ActiveExerciseStepResult',
      metadata: result.metadata,
      write: () => database
          .into(database.activeExerciseStepResults)
          .insertOnConflictUpdate(_activeExerciseStepResultCompanion(result)),
    );
  }

  @override
  Future<void> deleteScoreStopwatchState({
    required String sessionId,
    required int programIndex,
    required int exerciseIndex,
    required int setIndex,
    required DateTime deletedAt,
  }) async {
    final row =
        await (database.select(database.activeScoreStopwatchStates)..where(
              (table) =>
                  table.activeWorkoutSessionId.equals(sessionId) &
                  table.programIndex.equals(programIndex) &
                  table.exerciseIndex.equals(exerciseIndex) &
                  table.setIndex.equals(setIndex) &
                  table.deletedAt.isNull(),
            ))
            .getSingleOrNull();
    if (row == null) {
      return;
    }
    final revision = row.localRevision + 1;
    await (database.delete(
      database.activeScoreStopwatchStates,
    )..where((table) => table.id.equals(row.id))).go();
    await _writeChangeLog(
      database: database,
      entityType: 'ActiveScoreStopwatchState',
      entityId: row.id,
      operation: 'delete',
      localRevision: revision,
      originDeviceId: row.originDeviceId,
      createdAt: deletedAt,
    );
  }

  @override
  Future<domain.ActiveScoreStopwatchState?> findScoreStopwatchState({
    required String sessionId,
    required int programIndex,
    required int exerciseIndex,
    required int setIndex,
  }) async {
    final row =
        await (database.select(database.activeScoreStopwatchStates)..where(
              (table) =>
                  table.activeWorkoutSessionId.equals(sessionId) &
                  table.programIndex.equals(programIndex) &
                  table.exerciseIndex.equals(exerciseIndex) &
                  table.setIndex.equals(setIndex) &
                  table.deletedAt.isNull(),
            ))
            .getSingleOrNull();
    return row == null ? null : _activeScoreStopwatchStateFromRow(row);
  }

  @override
  Future<domain.ActiveSetTimerState?> findSetTimerState({
    required String sessionId,
    required int programIndex,
    required int exerciseIndex,
    required int setIndex,
  }) async {
    final row = await database
        .customSelect(
          'SELECT * FROM active_set_timer_states '
          'WHERE active_workout_session_id = ? '
          'AND program_index = ? '
          'AND exercise_index = ? '
          'AND set_index = ? '
          'AND deleted_at IS NULL '
          'LIMIT 1',
          variables: [
            Variable<String>(sessionId),
            Variable<int>(programIndex),
            Variable<int>(exerciseIndex),
            Variable<int>(setIndex),
          ],
        )
        .getSingleOrNull();
    return row == null ? null : _activeSetTimerStateFromCustomRow(row);
  }

  @override
  Future<domain.ActiveExerciseStepProgressState?>
  findExerciseStepProgressState({
    required String sessionId,
    required int programIndex,
    required int exerciseIndex,
    required int setIndex,
  }) async {
    final row =
        await (database.select(database.activeExerciseStepProgressStates)
              ..where(
                (table) =>
                    table.activeWorkoutSessionId.equals(sessionId) &
                    table.programIndex.equals(programIndex) &
                    table.exerciseIndex.equals(exerciseIndex) &
                    table.setIndex.equals(setIndex) &
                    table.deletedAt.isNull(),
              ))
            .getSingleOrNull();
    return row == null ? null : _activeExerciseStepProgressStateFromRow(row);
  }

  @override
  Future<List<domain.ActiveSetResult>> listSetResults(String sessionId) async {
    final rows =
        await (database.select(database.activeSetResults)
              ..where((table) => table.activeWorkoutSessionId.equals(sessionId))
              ..orderBy([
                (table) => OrderingTerm.asc(table.programIndex),
                (table) => OrderingTerm.asc(table.exerciseIndex),
                (table) => OrderingTerm.asc(table.setIndex),
              ]))
            .get();
    return rows.map(_activeSetResultFromRow).toList();
  }

  @override
  Future<List<domain.ActiveSetTimerState>> listSetTimerStates(
    String sessionId,
  ) async {
    final rows = await database
        .customSelect(
          'SELECT * FROM active_set_timer_states '
          'WHERE active_workout_session_id = ? '
          'AND deleted_at IS NULL '
          'ORDER BY program_index, exercise_index, set_index',
          variables: [Variable<String>(sessionId)],
        )
        .get();
    return rows.map(_activeSetTimerStateFromCustomRow).toList();
  }

  @override
  Future<List<domain.ActiveExerciseStepProgressState>>
  listExerciseStepProgressStates(String sessionId) async {
    final rows =
        await (database.select(database.activeExerciseStepProgressStates)
              ..where(
                (table) =>
                    table.activeWorkoutSessionId.equals(sessionId) &
                    table.deletedAt.isNull(),
              )
              ..orderBy([
                (table) => OrderingTerm.asc(table.programIndex),
                (table) => OrderingTerm.asc(table.exerciseIndex),
                (table) => OrderingTerm.asc(table.setIndex),
              ]))
            .get();
    return rows.map(_activeExerciseStepProgressStateFromRow).toList();
  }

  @override
  Future<List<domain.ActiveExerciseStepResult>> listExerciseStepResults(
    String sessionId,
  ) async {
    final rows =
        await (database.select(database.activeExerciseStepResults)
              ..where(
                (table) =>
                    table.activeWorkoutSessionId.equals(sessionId) &
                    table.deletedAt.isNull(),
              )
              ..orderBy([
                (table) => OrderingTerm.asc(table.programIndex),
                (table) => OrderingTerm.asc(table.exerciseIndex),
                (table) => OrderingTerm.asc(table.setIndex),
                (table) => OrderingTerm.asc(table.passageIndex),
                (table) => OrderingTerm.asc(table.stepIndex),
              ]))
            .get();
    return rows.map(_activeExerciseStepResultFromRow).toList();
  }

  @override
  Future<List<domain.ActiveRestState>> listRestStates(String sessionId) async {
    final rows = await database
        .customSelect(
          'SELECT * FROM active_rest_states '
          'WHERE active_workout_session_id = ? '
          'ORDER BY started_at',
          variables: [Variable<String>(sessionId)],
        )
        .get();
    return rows.map(_activeRestStateFromCustomRow).toList();
  }

  @override
  Future<List<domain.ActiveScoreStopwatchState>> listScoreStopwatchStates(
    String sessionId,
  ) async {
    final rows =
        await (database.select(database.activeScoreStopwatchStates)
              ..where(
                (table) =>
                    table.activeWorkoutSessionId.equals(sessionId) &
                    table.deletedAt.isNull(),
              )
              ..orderBy([
                (table) => OrderingTerm.asc(table.programIndex),
                (table) => OrderingTerm.asc(table.exerciseIndex),
                (table) => OrderingTerm.asc(table.setIndex),
              ]))
            .get();
    return rows.map(_activeScoreStopwatchStateFromRow).toList();
  }
}

final class DriftWorkoutHistoryRepository implements WorkoutHistoryRepository {
  const DriftWorkoutHistoryRepository(this.database);

  final db.AppDatabase database;

  @override
  Future<domain.WorkoutHistory?> findById(String id) async {
    final row =
        await (database.select(
              database.workoutHistories,
            )..where((table) => table.id.equals(id) & table.deletedAt.isNull()))
            .getSingleOrNull();
    if (row == null) {
      return null;
    }
    final results =
        await (database.select(database.workoutHistorySetResults)
              ..where(
                (table) =>
                    table.workoutHistoryId.equals(id) &
                    table.deletedAt.isNull(),
              )
              ..orderBy([
                (table) => OrderingTerm.asc(table.programIndex),
                (table) => OrderingTerm.asc(table.exerciseIndex),
                (table) => OrderingTerm.asc(table.setIndex),
              ]))
            .get();
    final stepResults =
        await (database.select(database.workoutHistoryStepResults)
              ..where(
                (table) =>
                    table.workoutHistoryId.equals(id) &
                    table.deletedAt.isNull(),
              )
              ..orderBy([
                (table) => OrderingTerm.asc(table.programIndex),
                (table) => OrderingTerm.asc(table.exerciseIndex),
                (table) => OrderingTerm.asc(table.setIndex),
                (table) => OrderingTerm.asc(table.passageIndex),
                (table) => OrderingTerm.asc(table.stepIndex),
              ]))
            .get();
    return _workoutHistoryFromRow(
      row,
      results.map(_workoutHistorySetResultFromRow).toList(),
      stepResults.map(_workoutHistoryStepResultFromRow).toList(),
    );
  }

  @override
  Future<List<domain.WorkoutHistory>> listActive() async {
    final rows =
        await (database.select(database.workoutHistories)
              ..where((table) => table.deletedAt.isNull())
              ..orderBy([(table) => OrderingTerm.desc(table.startedAt)]))
            .get();
    final histories = <domain.WorkoutHistory>[];
    for (final row in rows) {
      final history = await findById(row.id);
      if (history != null) {
        histories.add(history);
      }
    }
    return histories;
  }

  @override
  Future<void> save(domain.WorkoutHistory history) async {
    await database.transaction(() async {
      await _upsertWithChangeLog(
        database: database,
        tableName: 'workout_history',
        entityType: 'WorkoutHistory',
        metadata: history.metadata,
        write: () => database
            .into(database.workoutHistories)
            .insertOnConflictUpdate(_workoutHistoryCompanion(history)),
      );
      for (final result in history.results) {
        await saveSetResult(result);
      }
      for (final result in history.stepResults) {
        await saveStepResult(result);
      }
    });
  }

  @override
  Future<void> saveSetResult(domain.WorkoutHistorySetResult result) async {
    await _upsertWithChangeLog(
      database: database,
      tableName: 'workout_history_set_results',
      entityType: 'WorkoutHistorySetResult',
      metadata: result.metadata,
      write: () => database
          .into(database.workoutHistorySetResults)
          .insertOnConflictUpdate(_workoutHistorySetResultCompanion(result)),
    );
  }

  @override
  Future<void> saveStepResult(domain.WorkoutHistoryStepResult result) async {
    await _upsertWithChangeLog(
      database: database,
      tableName: 'workout_history_step_results',
      entityType: 'WorkoutHistoryStepResult',
      metadata: result.metadata,
      write: () => database
          .into(database.workoutHistoryStepResults)
          .insertOnConflictUpdate(_workoutHistoryStepResultCompanion(result)),
    );
  }

  @override
  Future<void> delete(String id, DateTime deletedAt) async {
    await database.transaction(() async {
      final history = await (database.select(
        database.workoutHistories,
      )..where((table) => table.id.equals(id))).getSingle();
      final revision = history.localRevision + 1;
      await (database.update(
        database.workoutHistories,
      )..where((table) => table.id.equals(id))).write(
        db.WorkoutHistoriesCompanion(
          deletedAt: Value(deletedAt.toUtc()),
          updatedAt: Value(deletedAt.toUtc()),
          localRevision: Value(revision),
          syncState: const Value('deleted'),
        ),
      );
      await _writeChangeLog(
        database: database,
        entityType: 'WorkoutHistory',
        entityId: id,
        operation: 'softDelete',
        localRevision: revision,
        originDeviceId: history.originDeviceId,
        createdAt: deletedAt,
      );
      final resultRows =
          await (database.select(database.workoutHistorySetResults)..where(
                (table) =>
                    table.workoutHistoryId.equals(id) &
                    table.deletedAt.isNull(),
              ))
              .get();
      await _softDeleteWorkoutHistorySetResultRows(
        database,
        resultRows,
        deletedAt,
      );
      final stepResultRows =
          await (database.select(database.workoutHistoryStepResults)..where(
                (table) =>
                    table.workoutHistoryId.equals(id) &
                    table.deletedAt.isNull(),
              ))
              .get();
      await _softDeleteWorkoutHistoryStepResultRows(
        database,
        stepResultRows,
        deletedAt,
      );
    });
  }
}

final class DriftLocalDataBackupRepository
    implements LocalDataBackupRepository {
  const DriftLocalDataBackupRepository(this.database);

  final db.AppDatabase database;

  @override
  Future<LocalDataExportSnapshot> readExportSnapshot(
    DateTime exportedAt,
  ) async {
    final mediaAssets = await DriftMediaAssetRepository(database).listActive();
    final exercises = await DriftExerciseRepository(database).listActive();
    final programs = await DriftProgramRepository(database).listActive();
    final templates = await DriftWorkoutTemplateRepository(
      database,
    ).listActive();
    final histories = await DriftWorkoutHistoryRepository(
      database,
    ).listActive();
    return LocalDataExportSnapshot(
      exportedAt: exportedAt.toUtc(),
      appSchemaVersion: database.schemaVersion,
      originDeviceId: 'local-device',
      mediaAssets: [
        for (final asset in mediaAssets)
          _backupResource(asset.metadata, _mediaAssetPayload(asset)),
      ],
      exercises: [
        for (final exercise in exercises)
          _backupResource(exercise.metadata, _exercisePayload(exercise)),
      ],
      programs: [
        for (final program in programs)
          _backupResource(program.metadata, _programPayload(program)),
      ],
      workoutTemplates: [
        for (final template in templates)
          _backupResource(template.metadata, _workoutTemplatePayload(template)),
      ],
      workoutHistories: [
        for (final history in histories)
          _backupResource(
            history.metadata,
            _localWorkoutHistoryPayload(history),
          ),
      ],
    );
  }

  @override
  Future<bool> hasOpenActiveWorkoutSession() async {
    return DriftActiveSessionRepository(
      database,
    ).findOpen().then((session) => session != null);
  }

  @override
  Future<bool> hasAnyUserData() async {
    final count = await database.customSelect('''
SELECT
  (SELECT COUNT(*) FROM media_assets WHERE deleted_at IS NULL) +
  (SELECT COUNT(*) FROM exercises WHERE deleted_at IS NULL AND archived_at IS NULL) +
  (SELECT COUNT(*) FROM programs WHERE deleted_at IS NULL) +
  (SELECT COUNT(*) FROM workout_templates WHERE deleted_at IS NULL) +
  (SELECT COUNT(*) FROM workout_history WHERE deleted_at IS NULL) AS total
''').getSingle();
    return count.read<int>('total') > 0;
  }

  @override
  Future<LocalBackupImportResult> applyImportSnapshot({
    required LocalDataExportSnapshot snapshot,
    required LocalBackupImportMode mode,
    required DateTime importedAt,
  }) async {
    if (await hasOpenActiveWorkoutSession()) {
      throw const LocalBackupException(
        LocalBackupValidationError.activeWorkoutInProgress,
      );
    }
    var inserted = 0;
    var updated = 0;
    var ignored = 0;
    var deleted = 0;
    await database.transaction(() async {
      if (mode == LocalBackupImportMode.replaceAll) {
        deleted += await _softDeleteResourcesAbsentFromBackup(
          snapshot,
          importedAt,
        );
      }
      final force = mode == LocalBackupImportMode.replaceAll;
      final mediaResult = await _applyBackupResources(
        resources: snapshot.mediaAssets,
        resourceType: SyncResourceType.mediaAsset,
        tableName: 'media_assets',
        importedAt: importedAt,
        force: force,
      );
      inserted += mediaResult.insertedCount;
      updated += mediaResult.updatedCount;
      ignored += mediaResult.ignoredOlderCount;
      final exerciseResult = await _applyBackupResources(
        resources: snapshot.exercises,
        resourceType: SyncResourceType.exercise,
        tableName: 'exercises',
        importedAt: importedAt,
        force: force,
      );
      inserted += exerciseResult.insertedCount;
      updated += exerciseResult.updatedCount;
      ignored += exerciseResult.ignoredOlderCount;
      final programResult = await _applyBackupResources(
        resources: snapshot.programs,
        resourceType: SyncResourceType.program,
        tableName: 'programs',
        importedAt: importedAt,
        force: force,
      );
      inserted += programResult.insertedCount;
      updated += programResult.updatedCount;
      ignored += programResult.ignoredOlderCount;
      final templateResult = await _applyBackupResources(
        resources: snapshot.workoutTemplates,
        resourceType: SyncResourceType.workoutTemplate,
        tableName: 'workout_templates',
        importedAt: importedAt,
        force: force,
      );
      inserted += templateResult.insertedCount;
      updated += templateResult.updatedCount;
      ignored += templateResult.ignoredOlderCount;
      final historyResult = await _applyBackupResources(
        resources: snapshot.workoutHistories,
        resourceType: SyncResourceType.workoutHistory,
        tableName: 'workout_history',
        importedAt: importedAt,
        force: force,
      );
      inserted += historyResult.insertedCount;
      updated += historyResult.updatedCount;
      ignored += historyResult.ignoredOlderCount;
    });
    return LocalBackupImportResult(
      insertedCount: inserted,
      updatedCount: updated,
      ignoredOlderCount: ignored,
      deletedByReplaceCount: deleted,
      missingMediaCount: 0,
    );
  }

  Future<LocalBackupImportResult> _applyBackupResources({
    required List<LocalBackupResource> resources,
    required SyncResourceType resourceType,
    required String tableName,
    required DateTime importedAt,
    required bool force,
  }) async {
    var inserted = 0;
    var updated = 0;
    var ignored = 0;
    for (final resource in resources) {
      final local = await _localSyncRow(tableName, resource.id);
      if (!force &&
          local != null &&
          !resource.updatedAt.isAfter(local.updatedAt)) {
        ignored += 1;
        continue;
      }
      final revision = (local?.localRevision ?? 0) + 1;
      final payload = _payloadWithImportedMetadata(
        resource.payload,
        importedAt,
        revision,
        snapshotOriginDeviceId: resource.payload['originDeviceId'] as String?,
      );
      await _saveBackupResource(
        resourceType: resourceType,
        resource: resource.copyWith(payload: payload),
        importedAt: importedAt,
      );
      if (local == null || local.deletedAt != null) {
        inserted += 1;
      } else {
        updated += 1;
      }
    }
    return LocalBackupImportResult(
      insertedCount: inserted,
      updatedCount: updated,
      ignoredOlderCount: ignored,
      deletedByReplaceCount: 0,
      missingMediaCount: 0,
    );
  }

  Future<void> _saveBackupResource({
    required SyncResourceType resourceType,
    required LocalBackupResource resource,
    required DateTime importedAt,
  }) async {
    final remoteItem = _remoteItemFromBackupResource(resourceType, resource);
    switch (resourceType) {
      case SyncResourceType.mediaAsset:
        await DriftMediaAssetRepository(
          database,
        ).save(_mediaAssetFromPayload(remoteItem));
      case SyncResourceType.exercise:
        await DriftExerciseRepository(
          database,
        ).save(_exerciseFromPayload(remoteItem));
      case SyncResourceType.program:
        await DriftProgramRepository(
          database,
        ).replaceExercises(_programFromPayload(remoteItem), importedAt);
      case SyncResourceType.workoutTemplate:
        await DriftWorkoutTemplateRepository(database).replaceComposition(
          _workoutTemplateFromPayload(remoteItem),
          importedAt,
        );
      case SyncResourceType.workoutHistory:
        await _replaceWorkoutHistory(
          _workoutHistoryFromLocalBackupPayload(remoteItem),
          importedAt,
        );
    }
  }

  Future<void> _replaceWorkoutHistory(
    domain.WorkoutHistory history,
    DateTime deletedAt,
  ) async {
    await DriftWorkoutHistoryRepository(database).save(history);
    final activeResultIds = history.results
        .map((result) => result.metadata.id)
        .toSet();
    final resultRows =
        await (database.select(database.workoutHistorySetResults)..where(
              (table) =>
                  table.workoutHistoryId.equals(history.metadata.id) &
                  table.deletedAt.isNull() &
                  (activeResultIds.isEmpty
                      ? const Constant<bool>(true)
                      : table.id.isNotIn(activeResultIds)),
            ))
            .get();
    await _softDeleteWorkoutHistorySetResultRows(
      database,
      resultRows,
      deletedAt,
    );
    final activeStepResultIds = history.stepResults
        .map((result) => result.metadata.id)
        .toSet();
    final stepRows =
        await (database.select(database.workoutHistoryStepResults)..where(
              (table) =>
                  table.workoutHistoryId.equals(history.metadata.id) &
                  table.deletedAt.isNull() &
                  (activeStepResultIds.isEmpty
                      ? const Constant<bool>(true)
                      : table.id.isNotIn(activeStepResultIds)),
            ))
            .get();
    await _softDeleteWorkoutHistoryStepResultRows(
      database,
      stepRows,
      deletedAt,
    );
  }

  Future<int> _softDeleteResourcesAbsentFromBackup(
    LocalDataExportSnapshot snapshot,
    DateTime deletedAt,
  ) async {
    var deleted = 0;
    deleted += await _softDeleteMainRowsAbsent(
      tableName: 'media_assets',
      entityType: 'MediaAsset',
      keepIds: snapshot.mediaAssets.map((resource) => resource.id).toSet(),
      deletedAt: deletedAt,
    );
    deleted += await _softDeleteMainRowsAbsent(
      tableName: 'exercises',
      entityType: 'Exercise',
      keepIds: snapshot.exercises.map((resource) => resource.id).toSet(),
      deletedAt: deletedAt,
    );
    deleted += await _softDeleteMainRowsAbsent(
      tableName: 'programs',
      entityType: 'Program',
      keepIds: snapshot.programs.map((resource) => resource.id).toSet(),
      deletedAt: deletedAt,
    );
    deleted += await _softDeleteMainRowsAbsent(
      tableName: 'workout_templates',
      entityType: 'WorkoutTemplate',
      keepIds: snapshot.workoutTemplates.map((resource) => resource.id).toSet(),
      deletedAt: deletedAt,
    );
    deleted += await _softDeleteMainRowsAbsent(
      tableName: 'workout_history',
      entityType: 'WorkoutHistory',
      keepIds: snapshot.workoutHistories.map((resource) => resource.id).toSet(),
      deletedAt: deletedAt,
    );
    await _softDeleteAllActiveSessionRows(deletedAt);
    return deleted;
  }

  Future<int> _softDeleteMainRowsAbsent({
    required String tableName,
    required String entityType,
    required Set<String> keepIds,
    required DateTime deletedAt,
  }) async {
    final where = keepIds.isEmpty
        ? 'deleted_at IS NULL'
        : 'deleted_at IS NULL AND id NOT IN (${List.filled(keepIds.length, '?').join(', ')})';
    final rows = await database
        .customSelect(
          'SELECT id, local_revision, origin_device_id FROM $tableName WHERE $where',
          variables: [for (final id in keepIds) Variable<String>(id)],
        )
        .get();
    for (final row in rows) {
      final id = row.read<String>('id');
      final revision = row.read<int>('local_revision') + 1;
      await database.customUpdate(
        'UPDATE $tableName SET deleted_at = ?, updated_at = ?, '
        'local_revision = ?, sync_state = ? WHERE id = ?',
        variables: [
          Variable<DateTime>(deletedAt.toUtc()),
          Variable<DateTime>(deletedAt.toUtc()),
          Variable<int>(revision),
          const Variable<String>('deleted'),
          Variable<String>(id),
        ],
      );
      await _writeChangeLog(
        database: database,
        entityType: entityType,
        entityId: id,
        operation: 'softDelete',
        localRevision: revision,
        originDeviceId: row.read<String>('origin_device_id'),
        createdAt: deletedAt,
      );
    }
    return rows.length;
  }

  Future<void> _softDeleteAllActiveSessionRows(DateTime deletedAt) async {
    await database.customUpdate(
      'UPDATE active_workout_sessions SET deleted_at = ?, updated_at = ? '
      'WHERE deleted_at IS NULL',
      variables: [
        Variable<DateTime>(deletedAt.toUtc()),
        Variable<DateTime>(deletedAt.toUtc()),
      ],
      updates: {database.activeWorkoutSessions},
    );
  }

  Future<_BackupLocalRow?> _localSyncRow(String tableName, String id) async {
    final row = await database
        .customSelect(
          'SELECT updated_at, deleted_at, local_revision FROM $tableName '
          'WHERE id = ? LIMIT 1',
          variables: [Variable<String>(id)],
        )
        .getSingleOrNull();
    if (row == null) {
      return null;
    }
    return _BackupLocalRow(
      updatedAt: _dateTimeFromData(row.data, 'updated_at'),
      deletedAt: _dateTimeOrNullFromData(row.data, 'deleted_at'),
      localRevision: row.read<int>('local_revision'),
    );
  }
}

final class DriftExercisePerformanceReferenceRepository
    implements ExercisePerformanceReferenceRepository {
  const DriftExercisePerformanceReferenceRepository(this.database);

  final db.AppDatabase database;

  @override
  Future<bool> hasAnyCompletedHistoryForExercise(String exerciseId) async {
    final row = await database
        .customSelect(
          'SELECT 1 FROM workout_history_set_results AS result '
          'INNER JOIN workout_history AS history '
          'ON history.id = result.workout_history_id '
          'WHERE result.deleted_at IS NULL '
          'AND history.deleted_at IS NULL '
          'AND history.completed = 1 '
          'AND result.status = ? '
          'AND result.source_exercise_id_snapshot = ? '
          'LIMIT 1',
          variables: [
            const Variable<String>('completed'),
            Variable<String>(exerciseId),
          ],
        )
        .getSingleOrNull();
    return row != null;
  }

  @override
  Future<WorkoutHistorySetPerformance?> findLatestSetPerformance({
    required String exerciseId,
    required ActivePerformanceMeasures activeMeasures,
    required int currentSetIndex,
  }) async {
    final valuePredicate = _activeMeasuresPredicate(activeMeasures);
    if (valuePredicate == null) {
      return null;
    }
    final row = await database
        .customSelect(
          '''
WITH candidates AS (
  SELECT
    result.workout_history_id,
    history.started_at AS history_started_at,
    result.set_index,
    result.exercise_name_snapshot,
    result.score_input_mode_snapshot,
    result.actual_time_ms,
    result.actual_reps,
    result.actual_score,
    result.actual_score_time_ms,
    result.completed_at
  FROM workout_history_set_results AS result
  INNER JOIN workout_history AS history
    ON history.id = result.workout_history_id
  WHERE result.deleted_at IS NULL
    AND history.deleted_at IS NULL
    AND history.completed = 1
    AND result.status = ?
    AND result.source_exercise_id_snapshot = ?
    AND ($valuePredicate)
),
latest_history AS (
  SELECT workout_history_id
  FROM candidates
  ORDER BY history_started_at DESC
  LIMIT 1
)
SELECT *
FROM candidates
WHERE workout_history_id = (SELECT workout_history_id FROM latest_history)
ORDER BY
  CASE WHEN set_index = ? THEN 0 ELSE 1 END,
  set_index DESC
LIMIT 1
''',
          variables: [
            const Variable<String>('completed'),
            Variable<String>(exerciseId),
            Variable<int>(currentSetIndex),
          ],
        )
        .getSingleOrNull();
    return row == null ? null : _setPerformanceFromRow(row);
  }

  @override
  Future<WorkoutHistoryMetricPerformance?> findBestMetricPerformance({
    required String exerciseId,
    required PerformanceMetric metric,
    required domain.ScoreInputMode scoreInputMode,
  }) async {
    final metricSql = _metricSql(metric, scoreInputMode);
    final row = await database
        .customSelect(
          '''
SELECT
  result.workout_history_id,
  history.started_at AS history_started_at,
  result.set_index,
  result.exercise_name_snapshot,
  result.score_input_mode_snapshot,
  result.actual_time_ms,
  result.actual_reps,
  result.actual_score,
  result.actual_score_time_ms,
  result.completed_at
FROM workout_history_set_results AS result
INNER JOIN workout_history AS history
  ON history.id = result.workout_history_id
WHERE result.deleted_at IS NULL
  AND history.deleted_at IS NULL
  AND history.completed = 1
  AND result.status = ?
  AND result.source_exercise_id_snapshot = ?
  AND ${metricSql.predicate}
ORDER BY ${metricSql.ordering},
  history.started_at DESC,
  result.completed_at DESC
LIMIT 1
''',
          variables: [
            const Variable<String>('completed'),
            Variable<String>(exerciseId),
          ],
        )
        .getSingleOrNull();
    return row == null ? null : _metricPerformanceFromRow(row, metric: metric);
  }
}

final class DriftProgressionStatsRepository
    implements ProgressionStatsRepository {
  const DriftProgressionStatsRepository(this.database);

  final db.AppDatabase database;

  @override
  Future<ProgressionGlobalStatsData> readGlobalStats(
    ProgressionDateRange range,
  ) async {
    final variables = _rangeVariables(range);
    final row = await database.customSelect('''
SELECT
  COUNT(*) AS completed_count,
  COALESCE(SUM(total_active_ms), 0) AS total_active_ms,
  MIN(started_at) AS first_started_at
FROM workout_history
WHERE deleted_at IS NULL
  AND completed = 1
  AND started_at <= ?
  ${range.startedAt == null ? '' : 'AND started_at >= ?'}
''', variables: variables).getSingle();
    final anyRow = await database.customSelect('''
SELECT MIN(started_at) AS first_started_at
FROM workout_history
WHERE deleted_at IS NULL
  AND completed = 1
''').getSingle();
    final weekRows = await database.customSelect('''
SELECT started_at
FROM workout_history
WHERE deleted_at IS NULL
  AND completed = 1
  AND started_at <= ?
  ${range.startedAt == null ? '' : 'AND started_at >= ?'}
''', variables: variables).get();
    final weekStarts = {
      for (final row in weekRows)
        _startOfLocalWeekFromUtc(_dateTimeFromData(row.data, 'started_at')),
    }.toList()..sort();
    return ProgressionGlobalStatsData(
      completedSessionCount: row.data['completed_count'] as int,
      totalActiveMs: row.data['total_active_ms'] as int,
      activeWeekStarts: weekStarts,
      hasAnyCompletedHistory: anyRow.data['first_started_at'] != null,
      firstCompletedSessionAt: _dateTimeOrNullFromData(
        anyRow.data,
        'first_started_at',
      ),
    );
  }

  @override
  Future<List<ProgressionExerciseOptionData>> listExerciseOptions(
    ProgressionDateRange range,
  ) async {
    final rows = await database.customSelect('''
WITH result_projection AS (
  ${_progressionResultProjection(range)}
),
ranked AS (
  SELECT
    exercise_key,
    exercise_name_snapshot,
    source_exercise_id_snapshot,
    started_at,
    ROW_NUMBER() OVER (
      PARTITION BY exercise_key
      ORDER BY started_at DESC
    ) AS name_rank
  FROM result_projection
  WHERE has_graphable_measure = 1
)
SELECT
  ranked.exercise_key,
  MAX(CASE WHEN ranked.name_rank = 1 THEN ranked.exercise_name_snapshot END)
    AS exercise_name_snapshot,
  MAX(ranked.started_at) AS last_performed_at,
  CASE
    WHEN exercises.id IS NOT NULL
      AND exercises.deleted_at IS NULL
      AND exercises.archived_at IS NULL
    THEN 0
    ELSE 1
  END AS is_archived
FROM ranked
LEFT JOIN exercises
  ON exercises.id = ranked.source_exercise_id_snapshot
GROUP BY ranked.exercise_key
ORDER BY is_archived ASC,
  last_performed_at DESC,
  exercise_name_snapshot COLLATE NOCASE ASC,
  ranked.exercise_key ASC
''', variables: _projectionVariables(range)).get();
    return rows
        .map(
          (row) => ProgressionExerciseOptionData(
            exerciseKey: row.data['exercise_key'] as String,
            nameSnapshot: row.data['exercise_name_snapshot'] as String,
            isArchived: (row.data['is_archived'] as int) == 1,
            lastPerformedAt: _dateTimeFromData(row.data, 'last_performed_at'),
          ),
        )
        .toList();
  }

  @override
  Future<List<ProgressionMeasureOptionData>> listMeasureOptions({
    required ProgressionDateRange range,
    required String exerciseKey,
  }) async {
    final rows = await database
        .customSelect(
          '''
WITH result_projection AS (
  ${_progressionResultProjection(range)}
),
measure_rows AS (
  SELECT 0 AS sort_order, 'manualScore' AS measure, score_label_snapshot,
    score_unit_snapshot, started_at
  FROM result_projection
  WHERE exercise_key = ?
    AND score_input_mode_snapshot = 'manual'
    AND actual_score IS NOT NULL
  UNION ALL
  SELECT 1, 'stopwatchScore', NULL, NULL, started_at
  FROM result_projection
  WHERE exercise_key = ?
    AND score_input_mode_snapshot = 'stopwatch'
    AND actual_score_time_ms IS NOT NULL
  UNION ALL
  SELECT 2, 'reps', NULL, NULL, started_at
  FROM result_projection
  WHERE exercise_key = ?
    AND actual_reps IS NOT NULL
  UNION ALL
  SELECT 3, 'time', NULL, NULL, started_at
  FROM result_projection
  WHERE exercise_key = ?
    AND actual_time_ms IS NOT NULL
),
ranked AS (
  SELECT *,
    ROW_NUMBER() OVER (PARTITION BY measure ORDER BY started_at DESC)
      AS value_rank
  FROM measure_rows
)
SELECT
  measure,
  MAX(CASE WHEN value_rank = 1 THEN score_label_snapshot END) AS score_label,
  MAX(CASE WHEN value_rank = 1 THEN score_unit_snapshot END) AS score_unit,
  MIN(sort_order) AS sort_order
FROM ranked
GROUP BY measure
ORDER BY sort_order
''',
          variables: [
            ..._projectionVariables(range),
            for (var i = 0; i < 4; i++) Variable<String>(exerciseKey),
          ],
        )
        .get();
    return rows.map(_progressionMeasureOptionFromRow).toList();
  }

  @override
  Future<ProgressionExerciseSeriesData> readExerciseSeries({
    required ProgressionDateRange range,
    required String exerciseKey,
    required ProgressionMeasure measure,
  }) async {
    final measureOption =
        (await listMeasureOptions(
          range: range,
          exerciseKey: exerciseKey,
        )).where((option) => option.measure == measure).firstOrNull ??
        _defaultProgressionMeasureOption(measure);
    final points = await _readExercisePoints(
      range: range,
      exerciseKey: exerciseKey,
      measure: measure,
    );
    final allTimePoints = range.startedAt == null
        ? points
        : await _readExercisePoints(
            range: ProgressionDateRange(
              startedAt: null,
              endedAt: range.endedAt,
            ),
            exerciseKey: exerciseKey,
            measure: measure,
          );
    final exerciseName = points.isNotEmpty
        ? points.last.exerciseNameSnapshot
        : allTimePoints.isNotEmpty
        ? allTimePoints.last.exerciseNameSnapshot
        : exerciseKey;
    final hasAnyCompletedExerciseResult = await _hasAnyCompletedExerciseResult(
      range: range,
      exerciseKey: exerciseKey,
    );
    return ProgressionExerciseSeriesData(
      exerciseKey: exerciseKey,
      exerciseNameSnapshot: exerciseName,
      measure: measureOption,
      points: [
        for (final point in points)
          ProgressionPointData(
            workoutHistoryId: point.workoutHistoryId,
            workoutNameSnapshot: point.workoutNameSnapshot,
            startedAt: point.startedAt,
            value: point.value,
            rawValue: point.rawValue,
          ),
      ],
      hasAnyAllTimeData: allTimePoints.isNotEmpty,
      hasAnyCompletedExerciseResult: hasAnyCompletedExerciseResult,
    );
  }

  Future<List<_ProgressionPointRow>> _readExercisePoints({
    required ProgressionDateRange range,
    required String exerciseKey,
    required ProgressionMeasure measure,
  }) async {
    final aggregation = _progressionAggregationSql(measure);
    final rows = await database
        .customSelect(
          '''
WITH result_projection AS (
  ${_progressionResultProjection(range)}
)
SELECT
  workout_history_id,
  workout_name_snapshot,
  MAX(started_at) AS started_at,
  MAX(exercise_name_snapshot) AS exercise_name_snapshot,
  ${aggregation.expression} AS raw_value
FROM result_projection
WHERE exercise_key = ?
  AND ${aggregation.predicate}
GROUP BY workout_history_id, workout_name_snapshot
ORDER BY started_at ASC
''',
          variables: [
            ..._projectionVariables(range),
            Variable<String>(exerciseKey),
          ],
        )
        .get();
    return rows.map((row) => _progressionPointRow(row, measure)).toList();
  }

  Future<bool> _hasAnyCompletedExerciseResult({
    required ProgressionDateRange range,
    required String exerciseKey,
  }) async {
    final row = await database
        .customSelect(
          '''
WITH result_projection AS (
  ${_progressionResultProjection(range)}
)
SELECT 1
FROM result_projection
WHERE exercise_key = ?
LIMIT 1
''',
          variables: [
            ..._projectionVariables(range),
            Variable<String>(exerciseKey),
          ],
        )
        .getSingleOrNull();
    return row != null;
  }
}

List<Variable<Object>> _rangeVariables(ProgressionDateRange range) {
  return [
    Variable<DateTime>(range.endedAt.toUtc()),
    if (range.startedAt != null) Variable<DateTime>(range.startedAt!.toUtc()),
  ];
}

List<Variable<Object>> _projectionVariables(ProgressionDateRange range) {
  final variables = _rangeVariables(range);
  return [...variables, ...variables];
}

String _progressionResultProjection(ProgressionDateRange range) {
  final rangePredicate =
      'history.started_at <= ? '
      "${range.startedAt == null ? '' : 'AND history.started_at >= ?'}";
  final sharedWhere =
      'history.deleted_at IS NULL '
      'AND history.completed = 1 '
      'AND $rangePredicate';
  return '''
  SELECT
    history.id AS workout_history_id,
    history.name_snapshot AS workout_name_snapshot,
    history.started_at,
    COALESCE(result.source_exercise_id_snapshot, result.exercise_snapshot_id)
      AS exercise_key,
    result.source_exercise_id_snapshot,
    result.exercise_snapshot_id,
    result.exercise_name_snapshot,
    result.score_input_mode_snapshot,
    result.score_label_snapshot,
    result.score_unit_snapshot,
    result.actual_time_ms,
    result.actual_reps,
    result.actual_score,
    result.actual_score_time_ms,
    CASE
      WHEN result.actual_time_ms IS NOT NULL
        OR result.actual_reps IS NOT NULL
        OR result.actual_score IS NOT NULL
        OR result.actual_score_time_ms IS NOT NULL
      THEN 1 ELSE 0
    END AS has_graphable_measure
  FROM workout_history_set_results AS result
  INNER JOIN workout_history AS history
    ON history.id = result.workout_history_id
  WHERE result.deleted_at IS NULL
    AND result.status = 'completed'
    AND $sharedWhere
  UNION ALL
  SELECT
    history.id AS workout_history_id,
    history.name_snapshot AS workout_name_snapshot,
    history.started_at,
    COALESCE(step.source_exercise_id_snapshot, step.exercise_snapshot_id)
      AS exercise_key,
    step.source_exercise_id_snapshot,
    step.exercise_snapshot_id,
    COALESCE(parent.exercise_name_snapshot, step.step_name_snapshot)
      AS exercise_name_snapshot,
    step.score_input_mode_snapshot,
    step.score_label_snapshot,
    step.score_unit_snapshot,
    step.actual_time_ms,
    step.actual_reps,
    step.actual_score,
    step.actual_score_time_ms,
    CASE
      WHEN step.actual_time_ms IS NOT NULL
        OR step.actual_reps IS NOT NULL
        OR step.actual_score IS NOT NULL
        OR step.actual_score_time_ms IS NOT NULL
      THEN 1 ELSE 0
    END AS has_graphable_measure
  FROM workout_history_step_results AS step
  INNER JOIN workout_history AS history
    ON history.id = step.workout_history_id
  LEFT JOIN workout_history_set_results AS parent
    ON parent.workout_history_id = step.workout_history_id
    AND parent.program_index = step.program_index
    AND parent.exercise_index = step.exercise_index
    AND parent.set_index = step.set_index
    AND parent.deleted_at IS NULL
  WHERE step.deleted_at IS NULL
    AND step.status = 'completed'
    AND $sharedWhere
''';
}

ProgressionMeasureOptionData _progressionMeasureOptionFromRow(QueryRow row) {
  final measure = _progressionMeasureFromDb(row.data['measure'] as String);
  final defaults = _defaultProgressionMeasureOption(measure);
  return ProgressionMeasureOptionData(
    measure: measure,
    label: defaults.label,
    scoreLabel: row.data['score_label'] as String?,
    scoreUnit: row.data['score_unit'] as String?,
    lowerIsBetter: defaults.lowerIsBetter,
  );
}

ProgressionMeasure _progressionMeasureFromDb(String value) {
  switch (value) {
    case 'manualScore':
      return ProgressionMeasure.manualScore;
    case 'stopwatchScore':
      return ProgressionMeasure.stopwatchScore;
    case 'reps':
      return ProgressionMeasure.reps;
    case 'time':
      return ProgressionMeasure.time;
  }
  throw domain.DomainException('Unknown progression measure: $value');
}

ProgressionMeasureOptionData _defaultProgressionMeasureOption(
  ProgressionMeasure measure,
) {
  switch (measure) {
    case ProgressionMeasure.manualScore:
      return const ProgressionMeasureOptionData(
        measure: ProgressionMeasure.manualScore,
        label: 'Score',
        lowerIsBetter: false,
      );
    case ProgressionMeasure.stopwatchScore:
      return const ProgressionMeasureOptionData(
        measure: ProgressionMeasure.stopwatchScore,
        label: 'Temps réalisé',
        lowerIsBetter: true,
      );
    case ProgressionMeasure.reps:
      return const ProgressionMeasureOptionData(
        measure: ProgressionMeasure.reps,
        label: 'Répétitions',
        lowerIsBetter: false,
      );
    case ProgressionMeasure.time:
      return const ProgressionMeasureOptionData(
        measure: ProgressionMeasure.time,
        label: 'Temps',
        lowerIsBetter: false,
      );
  }
}

_ProgressionAggregationSql _progressionAggregationSql(
  ProgressionMeasure measure,
) {
  switch (measure) {
    case ProgressionMeasure.manualScore:
      return const _ProgressionAggregationSql(
        expression: 'MAX(actual_score)',
        predicate:
            "score_input_mode_snapshot = 'manual' AND actual_score IS NOT NULL",
      );
    case ProgressionMeasure.stopwatchScore:
      return const _ProgressionAggregationSql(
        expression: 'MIN(actual_score_time_ms)',
        predicate:
            "score_input_mode_snapshot = 'stopwatch' "
            'AND actual_score_time_ms IS NOT NULL',
      );
    case ProgressionMeasure.reps:
      return const _ProgressionAggregationSql(
        expression: 'SUM(actual_reps)',
        predicate: 'actual_reps IS NOT NULL',
      );
    case ProgressionMeasure.time:
      return const _ProgressionAggregationSql(
        expression: 'SUM(actual_time_ms)',
        predicate: 'actual_time_ms IS NOT NULL',
      );
  }
}

_ProgressionPointRow _progressionPointRow(
  QueryRow row,
  ProgressionMeasure measure,
) {
  final raw = row.data['raw_value'] as num;
  final rawValue = measure == ProgressionMeasure.manualScore
      ? raw.toDouble()
      : raw.toInt();
  return _ProgressionPointRow(
    workoutHistoryId: row.data['workout_history_id'] as String,
    workoutNameSnapshot: row.data['workout_name_snapshot'] as String,
    exerciseNameSnapshot: row.data['exercise_name_snapshot'] as String,
    startedAt: _dateTimeFromData(row.data, 'started_at'),
    value: raw.toDouble(),
    rawValue: rawValue,
  );
}

DateTime _startOfLocalWeekFromUtc(DateTime value) {
  final local = value.toLocal();
  final localDay = DateTime(local.year, local.month, local.day);
  return localDay.subtract(Duration(days: localDay.weekday - DateTime.monday));
}

final class _ProgressionAggregationSql {
  const _ProgressionAggregationSql({
    required this.expression,
    required this.predicate,
  });

  final String expression;
  final String predicate;
}

final class _ProgressionPointRow {
  const _ProgressionPointRow({
    required this.workoutHistoryId,
    required this.workoutNameSnapshot,
    required this.exerciseNameSnapshot,
    required this.startedAt,
    required this.value,
    required this.rawValue,
  });

  final String workoutHistoryId;
  final String workoutNameSnapshot;
  final String exerciseNameSnapshot;
  final DateTime startedAt;
  final double value;
  final Object rawValue;
}

Future<void> _upsertWithChangeLog({
  required db.AppDatabase database,
  required String tableName,
  required String entityType,
  required domain.EntityMetadata metadata,
  required Future<void> Function() write,
}) async {
  await database.transaction(() async {
    final operation = await _operationForMutation(
      database: database,
      tableName: tableName,
      entityId: metadata.id,
      deletedAt: metadata.deletedAt,
    );
    await write();
    await _writeChangeLog(
      database: database,
      entityType: entityType,
      entityId: metadata.id,
      operation: operation,
      localRevision: metadata.localRevision,
      originDeviceId: metadata.originDeviceId,
      createdAt: metadata.updatedAt,
    );
  });
}

Future<void> _replaceExerciseImages(
  db.AppDatabase database,
  domain.Exercise exercise,
) async {
  final rows = await (database.select(
    database.exerciseImages,
  )..where((table) => table.exerciseId.equals(exercise.metadata.id))).get();
  final activeRowsByMediaId = {
    for (final row in rows)
      if (row.deletedAt == null) row.mediaAssetId: row,
  };
  final desiredIds = exercise.imageMediaIds.toSet();
  final removedRows = activeRowsByMediaId.values
      .where((row) => !desiredIds.contains(row.mediaAssetId))
      .toList();
  await _softDeleteExerciseImageRows(
    database,
    removedRows,
    exercise.metadata.updatedAt,
  );

  for (var index = 0; index < exercise.imageMediaIds.length; index++) {
    final mediaId = exercise.imageMediaIds[index];
    final existing = activeRowsByMediaId[mediaId];
    final metadata = existing == null
        ? domain.EntityMetadata(
            id: _exerciseImageId(exercise.metadata.id, mediaId),
            createdAt: exercise.metadata.updatedAt,
            updatedAt: exercise.metadata.updatedAt,
            originDeviceId: exercise.metadata.originDeviceId,
          )
        : _metadataFromRow(existing).touch(exercise.metadata.updatedAt);
    await _upsertWithChangeLog(
      database: database,
      tableName: 'exercise_images',
      entityType: 'ExerciseImage',
      metadata: metadata,
      write: () => database
          .into(database.exerciseImages)
          .insertOnConflictUpdate(
            db.ExerciseImagesCompanion(
              id: Value(metadata.id),
              createdAt: Value(metadata.createdAt.toUtc()),
              updatedAt: Value(metadata.updatedAt.toUtc()),
              deletedAt: Value(_utcOrNull(metadata.deletedAt)),
              schemaVersion: Value(metadata.schemaVersion),
              syncState: Value(_syncStateToDb(metadata.syncState)),
              localRevision: Value(metadata.localRevision),
              originDeviceId: Value(metadata.originDeviceId),
              futureOwnerProfileId: Value(metadata.futureOwnerProfileId),
              lastSyncedAt: Value(_utcOrNull(metadata.lastSyncedAt)),
              remoteRevision: Value(metadata.remoteRevision),
              exerciseId: Value(exercise.metadata.id),
              mediaAssetId: Value(mediaId),
              position: Value(index),
            ),
          ),
    );
  }
}

Future<void> _replaceExerciseSteps(
  db.AppDatabase database,
  domain.Exercise exercise,
) async {
  final rows = await (database.select(
    database.exerciseSteps,
  )..where((table) => table.exerciseId.equals(exercise.metadata.id))).get();
  final activeRowsByStepId = {
    for (final row in rows)
      if (row.deletedAt == null) row.id: row,
  };
  final desiredIds = exercise.steps.map((step) => step.id).toSet();
  final removedRows = activeRowsByStepId.values
      .where((row) => !desiredIds.contains(row.id))
      .toList();
  await _softDeleteExerciseStepRows(
    database,
    removedRows,
    exercise.metadata.updatedAt,
  );

  for (final step in exercise.steps) {
    final existing = activeRowsByStepId[step.id];
    final metadata = existing == null
        ? domain.EntityMetadata(
            id: step.id,
            createdAt: exercise.metadata.updatedAt,
            updatedAt: exercise.metadata.updatedAt,
            originDeviceId: exercise.metadata.originDeviceId,
          )
        : _metadataFromRow(existing).touch(exercise.metadata.updatedAt);
    await _upsertWithChangeLog(
      database: database,
      tableName: 'exercise_steps',
      entityType: 'ExerciseStep',
      metadata: metadata,
      write: () => database
          .into(database.exerciseSteps)
          .insertOnConflictUpdate(
            db.ExerciseStepsCompanion(
              id: Value(metadata.id),
              createdAt: Value(metadata.createdAt.toUtc()),
              updatedAt: Value(metadata.updatedAt.toUtc()),
              deletedAt: Value(_utcOrNull(metadata.deletedAt)),
              schemaVersion: Value(metadata.schemaVersion),
              syncState: Value(_syncStateToDb(metadata.syncState)),
              localRevision: Value(metadata.localRevision),
              originDeviceId: Value(metadata.originDeviceId),
              futureOwnerProfileId: Value(metadata.futureOwnerProfileId),
              lastSyncedAt: Value(_utcOrNull(metadata.lastSyncedAt)),
              remoteRevision: Value(metadata.remoteRevision),
              exerciseId: Value(exercise.metadata.id),
              position: Value(step.position),
              name: Value(step.name),
              type: Value(_exerciseStepTypeToDb(step.type)),
              defaultTargetValue: Value(step.defaultTargetValue),
              hasScore: Value(step.hasScore),
              scoreInputMode: Value(
                step.hasScore ? _scoreInputModeToDb(step.scoreInputMode) : null,
              ),
              scoreLabel: Value(step.scoreLabel),
              scoreUnit: Value(step.scoreUnit),
              defaultTargetScore: Value(step.defaultTargetScore),
              defaultTargetScoreTimeMs: Value(step.defaultTargetScoreTimeMs),
            ),
          ),
    );
  }
}

Future<void> _softDeleteExerciseImageRows(
  db.AppDatabase database,
  List<db.ExerciseImage> rows,
  DateTime deletedAt,
) async {
  for (final row in rows) {
    final revision = row.localRevision + 1;
    await (database.update(
      database.exerciseImages,
    )..where((table) => table.id.equals(row.id))).write(
      db.ExerciseImagesCompanion(
        deletedAt: Value(deletedAt.toUtc()),
        updatedAt: Value(deletedAt.toUtc()),
        localRevision: Value(revision),
        syncState: const Value('deleted'),
      ),
    );
    await _writeChangeLog(
      database: database,
      entityType: 'ExerciseImage',
      entityId: row.id,
      operation: 'softDelete',
      localRevision: revision,
      originDeviceId: row.originDeviceId,
      createdAt: deletedAt,
    );
  }
}

Future<void> _softDeleteExerciseStepRows(
  db.AppDatabase database,
  List<db.ExerciseStep> rows,
  DateTime deletedAt,
) async {
  for (final row in rows) {
    final revision = row.localRevision + 1;
    await (database.update(
      database.exerciseSteps,
    )..where((table) => table.id.equals(row.id))).write(
      db.ExerciseStepsCompanion(
        deletedAt: Value(deletedAt.toUtc()),
        updatedAt: Value(deletedAt.toUtc()),
        localRevision: Value(revision),
        syncState: const Value('deleted'),
      ),
    );
    await _writeChangeLog(
      database: database,
      entityType: 'ExerciseStep',
      entityId: row.id,
      operation: 'softDelete',
      localRevision: revision,
      originDeviceId: row.originDeviceId,
      createdAt: deletedAt,
    );
  }
}

String _exerciseImageId(String exerciseId, String mediaId) {
  return 'exercise-image:$exerciseId:$mediaId';
}

final class _ExerciseStarterMetadata {
  const _ExerciseStarterMetadata({
    this.category = domain.ExerciseCategory.uncategorized,
    this.isExample = false,
  });

  final domain.ExerciseCategory category;
  final bool isExample;
}

Future<void> _writeExerciseStarterMetadata(
  db.AppDatabase database,
  domain.Exercise exercise,
) async {
  await database.customStatement(
    'UPDATE exercises SET category = ?, is_example = ? WHERE id = ?',
    [
      _exerciseCategoryToDb(exercise.category),
      exercise.isExample ? 1 : 0,
      exercise.metadata.id,
    ],
  );
}

Future<void> _writeProgramStarterMetadata(
  db.AppDatabase database,
  domain.Program program,
) async {
  await database.customStatement(
    'UPDATE programs SET is_example = ? WHERE id = ?',
    [program.isExample ? 1 : 0, program.metadata.id],
  );
}

Future<void> _writeWorkoutTemplateStarterMetadata(
  db.AppDatabase database,
  domain.WorkoutTemplate template,
) async {
  await database.customStatement(
    'UPDATE workout_templates SET is_example = ? WHERE id = ?',
    [template.isExample ? 1 : 0, template.metadata.id],
  );
}

Future<String> _operationForMutation({
  required db.AppDatabase database,
  required String tableName,
  required String entityId,
  required DateTime? deletedAt,
}) async {
  final existing = await database
      .customSelect(
        'SELECT deleted_at FROM $tableName WHERE id = ? LIMIT 1',
        variables: [Variable<String>(entityId)],
      )
      .getSingleOrNull();
  if (existing == null) {
    return 'insert';
  }
  final wasDeleted = existing.data['deleted_at'] != null;
  if (deletedAt != null) {
    return 'softDelete';
  }
  if (wasDeleted) {
    return 'restore';
  }
  return 'update';
}

Future<void> _writeChangeLog({
  required db.AppDatabase database,
  required String entityType,
  required String entityId,
  required String operation,
  required int localRevision,
  required String originDeviceId,
  required DateTime createdAt,
}) async {
  final id = [
    entityType,
    entityId,
    operation,
    localRevision,
    createdAt.microsecondsSinceEpoch,
  ].join(':');
  await database
      .into(database.changeLogEntries)
      .insertOnConflictUpdate(
        db.ChangeLogEntriesCompanion.insert(
          id: id,
          entityType: entityType,
          entityId: entityId,
          operation: operation,
          localRevision: localRevision,
          originDeviceId: originDeviceId,
          createdAt: createdAt.toUtc(),
        ),
      );
}

Future<void> _softDeleteProgramExerciseRows(
  db.AppDatabase database,
  List<db.ProgramExercise> rows,
  DateTime deletedAt,
) async {
  for (final row in rows) {
    final revision = row.localRevision + 1;
    await (database.update(
      database.programExercises,
    )..where((table) => table.id.equals(row.id))).write(
      db.ProgramExercisesCompanion(
        deletedAt: Value(deletedAt.toUtc()),
        updatedAt: Value(deletedAt.toUtc()),
        localRevision: Value(revision),
        syncState: const Value('deleted'),
      ),
    );
    await _writeChangeLog(
      database: database,
      entityType: 'ProgramExercise',
      entityId: row.id,
      operation: 'softDelete',
      localRevision: revision,
      originDeviceId: row.originDeviceId,
      createdAt: deletedAt,
    );
  }
}

Future<void> _softDeleteWorkoutTemplateProgramRows(
  db.AppDatabase database,
  List<db.WorkoutTemplateProgram> rows,
  DateTime deletedAt,
) async {
  for (final row in rows) {
    final revision = row.localRevision + 1;
    await (database.update(
      database.workoutTemplatePrograms,
    )..where((table) => table.id.equals(row.id))).write(
      db.WorkoutTemplateProgramsCompanion(
        deletedAt: Value(deletedAt.toUtc()),
        updatedAt: Value(deletedAt.toUtc()),
        localRevision: Value(revision),
        syncState: const Value('deleted'),
      ),
    );
    await _writeChangeLog(
      database: database,
      entityType: 'WorkoutTemplateProgram',
      entityId: row.id,
      operation: 'softDelete',
      localRevision: revision,
      originDeviceId: row.originDeviceId,
      createdAt: deletedAt,
    );
  }
}

Future<void> _softDeleteWorkoutTemplateExerciseOverrideRows(
  db.AppDatabase database,
  List<db.WorkoutTemplateExerciseOverride> rows,
  DateTime deletedAt,
) async {
  for (final row in rows) {
    final revision = row.localRevision + 1;
    await (database.update(
      database.workoutTemplateExerciseOverrides,
    )..where((table) => table.id.equals(row.id))).write(
      db.WorkoutTemplateExerciseOverridesCompanion(
        deletedAt: Value(deletedAt.toUtc()),
        updatedAt: Value(deletedAt.toUtc()),
        localRevision: Value(revision),
        syncState: const Value('deleted'),
      ),
    );
    await _writeChangeLog(
      database: database,
      entityType: 'WorkoutTemplateExerciseOverride',
      entityId: row.id,
      operation: 'softDelete',
      localRevision: revision,
      originDeviceId: row.originDeviceId,
      createdAt: deletedAt,
    );
  }
}

Future<void> _softDeleteWorkoutHistorySetResultRows(
  db.AppDatabase database,
  List<db.WorkoutHistorySetResult> rows,
  DateTime deletedAt,
) async {
  for (final row in rows) {
    final revision = row.localRevision + 1;
    await (database.update(
      database.workoutHistorySetResults,
    )..where((table) => table.id.equals(row.id))).write(
      db.WorkoutHistorySetResultsCompanion(
        deletedAt: Value(deletedAt.toUtc()),
        updatedAt: Value(deletedAt.toUtc()),
        localRevision: Value(revision),
        syncState: const Value('deleted'),
      ),
    );
    await _writeChangeLog(
      database: database,
      entityType: 'WorkoutHistorySetResult',
      entityId: row.id,
      operation: 'softDelete',
      localRevision: revision,
      originDeviceId: row.originDeviceId,
      createdAt: deletedAt,
    );
  }
}

Future<void> _softDeleteWorkoutHistoryStepResultRows(
  db.AppDatabase database,
  List<db.WorkoutHistoryStepResult> rows,
  DateTime deletedAt,
) async {
  for (final row in rows) {
    final revision = row.localRevision + 1;
    await (database.update(
      database.workoutHistoryStepResults,
    )..where((table) => table.id.equals(row.id))).write(
      db.WorkoutHistoryStepResultsCompanion(
        deletedAt: Value(deletedAt.toUtc()),
        updatedAt: Value(deletedAt.toUtc()),
        localRevision: Value(revision),
        syncState: const Value('deleted'),
      ),
    );
    await _writeChangeLog(
      database: database,
      entityType: 'WorkoutHistoryStepResult',
      entityId: row.id,
      operation: 'softDelete',
      localRevision: revision,
      originDeviceId: row.originDeviceId,
      createdAt: deletedAt,
    );
  }
}

DateTime _utc(DateTime value) => value.toUtc();

DateTime? _utcOrNull(DateTime? value) => value?.toUtc();

domain.EntityMetadata _metadataFromRow(dynamic row) {
  return domain.EntityMetadata(
    id: row.id as String,
    createdAt: _utc(row.createdAt as DateTime),
    updatedAt: _utc(row.updatedAt as DateTime),
    deletedAt: _utcOrNull(row.deletedAt as DateTime?),
    schemaVersion: row.schemaVersion as int,
    syncState: _syncStateFromDb(row.syncState as String),
    localRevision: row.localRevision as int,
    originDeviceId: row.originDeviceId as String,
    futureOwnerProfileId: row.futureOwnerProfileId as String?,
    lastSyncedAt: _utcOrNull(row.lastSyncedAt as DateTime?),
    remoteRevision: row.remoteRevision as String?,
  );
}

domain.EntityMetadata _metadataFromData(Map<String, dynamic> data) {
  return domain.EntityMetadata(
    id: data['id'] as String,
    createdAt: _dateTimeFromData(data, 'created_at'),
    updatedAt: _dateTimeFromData(data, 'updated_at'),
    deletedAt: _dateTimeOrNullFromData(data, 'deleted_at'),
    schemaVersion: data['schema_version'] as int,
    syncState: _syncStateFromDb(data['sync_state'] as String),
    localRevision: data['local_revision'] as int,
    originDeviceId: data['origin_device_id'] as String,
    futureOwnerProfileId: data['future_owner_profile_id'] as String?,
    lastSyncedAt: _dateTimeOrNullFromData(data, 'last_synced_at'),
    remoteRevision: data['remote_revision'] as String?,
  );
}

DateTime _dateTimeFromData(Map<String, dynamic> data, String key) {
  final value = data[key];
  if (value is DateTime) {
    return value.toUtc();
  }
  if (value is int) {
    if (value.abs() < 100000000000) {
      return DateTime.fromMillisecondsSinceEpoch(value * 1000, isUtc: true);
    }
    return DateTime.fromMillisecondsSinceEpoch(value, isUtc: true);
  }
  throw domain.DomainException('Invalid timestamp column: $key');
}

DateTime? _dateTimeOrNullFromData(Map<String, dynamic> data, String key) {
  final value = data[key];
  if (value == null) {
    return null;
  }
  if (value is DateTime) {
    return value.toUtc();
  }
  if (value is int) {
    if (value.abs() < 100000000000) {
      return DateTime.fromMillisecondsSinceEpoch(value * 1000, isUtc: true);
    }
    return DateTime.fromMillisecondsSinceEpoch(value, isUtc: true);
  }
  throw domain.DomainException('Invalid timestamp column: $key');
}

List<dynamic> _metadataValues(domain.EntityMetadata metadata) => [
  Value(metadata.id),
  Value(metadata.createdAt.toUtc()),
  Value(metadata.updatedAt.toUtc()),
  Value(_utcOrNull(metadata.deletedAt)),
  Value(metadata.schemaVersion),
  Value(_syncStateToDb(metadata.syncState)),
  Value(metadata.localRevision),
  Value(metadata.originDeviceId),
  Value(metadata.futureOwnerProfileId),
  Value(_utcOrNull(metadata.lastSyncedAt)),
  Value(metadata.remoteRevision),
];

db.ExercisesCompanion _exerciseCompanion(domain.Exercise exercise) {
  final values = _metadataValues(exercise.metadata);
  return db.ExercisesCompanion(
    id: values[0] as Value<String>,
    createdAt: values[1] as Value<DateTime>,
    updatedAt: values[2] as Value<DateTime>,
    deletedAt: values[3] as Value<DateTime?>,
    schemaVersion: values[4] as Value<int>,
    syncState: values[5] as Value<String>,
    localRevision: values[6] as Value<int>,
    originDeviceId: values[7] as Value<String>,
    futureOwnerProfileId: values[8] as Value<String?>,
    lastSyncedAt: values[9] as Value<DateTime?>,
    remoteRevision: values[10] as Value<String?>,
    name: Value(exercise.name),
    description: Value(exercise.description),
    iconMediaId: Value(exercise.iconMediaId),
    videoMediaId: Value(exercise.videoMediaId),
    hasTimeMeasure: Value(exercise.hasTimeMeasure),
    hasRepsMeasure: Value(exercise.hasRepsMeasure),
    hasScoreMeasure: Value(exercise.hasScoreMeasure),
    scoreInputMode: Value(_scoreInputModeToDb(exercise.scoreInputMode)),
    scoreLabel: Value(exercise.scoreLabel),
    scoreUnit: Value(exercise.scoreUnit),
    defaultTargetTimeSeconds: Value(exercise.defaultTargetTimeSeconds),
    defaultTargetReps: Value(exercise.defaultTargetReps),
    defaultTargetScore: Value(exercise.defaultTargetScore),
    defaultTargetScoreTimeMs: Value(exercise.defaultTargetScoreTimeMs),
    autoStartNextTimedStep: Value(exercise.autoStartNextTimedStep),
    tagsJson: Value(_encodeTags(exercise.tags)),
    archivedAt: Value(_utcOrNull(exercise.archivedAt)),
  );
}

domain.Exercise _exerciseFromRow(
  db.Exercise row,
  List<String> imageMediaIds,
  List<domain.ExerciseStep> steps,
  _ExerciseStarterMetadata starterMetadata,
) {
  return domain.Exercise(
    metadata: _metadataFromRow(row),
    name: row.name,
    description: row.description,
    imageMediaIds: imageMediaIds,
    iconMediaId: row.iconMediaId,
    videoMediaId: row.videoMediaId,
    hasTimeMeasure: row.hasTimeMeasure,
    hasRepsMeasure: row.hasRepsMeasure,
    hasScoreMeasure: row.hasScoreMeasure,
    scoreInputMode: _scoreInputModeFromDb(row.scoreInputMode),
    scoreLabel: row.scoreLabel,
    scoreUnit: row.scoreUnit,
    defaultTargetTimeSeconds: row.defaultTargetTimeSeconds,
    defaultTargetReps: row.defaultTargetReps,
    defaultTargetScore: row.defaultTargetScore,
    defaultTargetScoreTimeMs: row.defaultTargetScoreTimeMs,
    autoStartNextTimedStep: row.autoStartNextTimedStep,
    category: starterMetadata.category,
    isExample: starterMetadata.isExample,
    tags: _decodeTags(row.tagsJson),
    steps: steps,
    archivedAt: _utcOrNull(row.archivedAt),
  );
}

domain.ExerciseStep _exerciseStepFromRow(db.ExerciseStep row) {
  return domain.ExerciseStep(
    id: row.id,
    position: row.position,
    name: row.name,
    type: _exerciseStepTypeFromDb(row.type),
    defaultTargetValue: row.defaultTargetValue,
    hasScore: row.hasScore,
    scoreInputMode: row.scoreInputMode == null
        ? domain.ScoreInputMode.manual
        : _scoreInputModeFromDb(row.scoreInputMode!),
    scoreLabel: row.scoreLabel,
    scoreUnit: row.scoreUnit,
    defaultTargetScore: row.defaultTargetScore,
    defaultTargetScoreTimeMs: row.defaultTargetScoreTimeMs,
  );
}

db.MediaAssetsCompanion _mediaAssetCompanion(domain.MediaAsset asset) {
  final values = _metadataValues(asset.metadata);
  return db.MediaAssetsCompanion(
    id: values[0] as Value<String>,
    createdAt: values[1] as Value<DateTime>,
    updatedAt: values[2] as Value<DateTime>,
    deletedAt: values[3] as Value<DateTime?>,
    schemaVersion: values[4] as Value<int>,
    syncState: values[5] as Value<String>,
    localRevision: values[6] as Value<int>,
    originDeviceId: values[7] as Value<String>,
    futureOwnerProfileId: values[8] as Value<String?>,
    lastSyncedAt: values[9] as Value<DateTime?>,
    remoteRevision: values[10] as Value<String?>,
    kind: Value(_mediaKindToDb(asset.kind)),
    localUri: Value(asset.localUri),
    mimeType: Value(asset.mimeType),
    sizeBytes: Value(asset.sizeBytes),
    width: Value(asset.width),
    height: Value(asset.height),
    durationMs: Value(asset.durationMs),
    checksum: Value(asset.checksum),
    remoteUri: Value(asset.remoteUri),
    thumbnailLocalUri: Value(asset.thumbnailLocalUri),
  );
}

domain.MediaAsset _mediaAssetFromRow(db.MediaAsset row) {
  return domain.MediaAsset(
    metadata: _metadataFromRow(row),
    kind: _mediaKindFromDb(row.kind),
    localUri: row.localUri,
    mimeType: row.mimeType,
    sizeBytes: row.sizeBytes,
    width: row.width,
    height: row.height,
    durationMs: row.durationMs,
    checksum: row.checksum,
    remoteUri: row.remoteUri,
    thumbnailLocalUri: row.thumbnailLocalUri,
  );
}

db.OnlineAccountSessionsCompanion _userAccountSessionCompanion(
  domain.UserAccountSession session,
) {
  return db.OnlineAccountSessionsCompanion(
    id: Value(session.id),
    serverUserId: Value(session.serverUserId),
    email: Value(session.email),
    displayName: Value<String?>(session.displayName),
    isLoggedIn: Value(session.isLoggedIn),
    createdAt: Value(session.createdAt.toUtc()),
    updatedAt: Value(session.updatedAt.toUtc()),
    lastAuthenticatedAt: Value<DateTime?>(
      _utcOrNull(session.lastAuthenticatedAt),
    ),
  );
}

domain.UserAccountSession _userAccountSessionFromRow(
  db.OnlineAccountSession row,
) {
  return domain.UserAccountSession(
    id: row.id,
    serverUserId: row.serverUserId,
    email: row.email,
    displayName: row.displayName,
    isLoggedIn: row.isLoggedIn,
    createdAt: _utc(row.createdAt),
    updatedAt: _utc(row.updatedAt),
    lastAuthenticatedAt: _utcOrNull(row.lastAuthenticatedAt),
  );
}

db.ShareInboxItemsCompanion _shareInboxItemCompanion(
  domain.ShareInboxItem item,
) {
  final updatedAt = item.respondedAt ?? item.createdAt;
  return db.ShareInboxItemsCompanion.insert(
    shareId: item.shareId,
    senderUserId: item.senderUserId,
    resourceType: _shareResourceTypeToDb(item.resourceType),
    payloadJson: item.payloadJson,
    status: _shareInboxStatusToDb(item.status),
    createdAt: item.createdAt.toUtc(),
    updatedAt: updatedAt.toUtc(),
    respondedAt: Value<DateTime?>(_utcOrNull(item.respondedAt)),
  );
}

domain.ShareInboxItem _shareInboxItemFromRow(db.ShareInboxItem row) {
  return domain.ShareInboxItem(
    shareId: row.shareId,
    senderUserId: row.senderUserId,
    resourceType: _shareResourceTypeFromDb(row.resourceType),
    payloadJson: row.payloadJson,
    status: _shareInboxStatusFromDb(row.status),
    createdAt: _utc(row.createdAt),
    respondedAt: _utcOrNull(row.respondedAt),
  );
}

db.PendingShareActionsCompanion _pendingShareActionCompanion(
  domain.PendingShareAction action,
) {
  return db.PendingShareActionsCompanion.insert(
    id: action.id,
    actionType: _pendingShareActionTypeToDb(action.actionType),
    shareId: Value<String?>(action.shareId),
    resourceType: Value<String?>(
      action.resourceType == null
          ? null
          : _shareResourceTypeToDb(action.resourceType!),
    ),
    payloadJson: Value<String?>(action.payloadJson),
    recipientEmailsJson: Value<String?>(action.recipientEmailsJson),
    createdAt: action.createdAt.toUtc(),
    lastAttemptAt: Value<DateTime?>(_utcOrNull(action.lastAttemptAt)),
    attemptCount: action.attemptCount,
    status: _pendingShareActionStatusToDb(action.status),
  );
}

domain.PendingShareAction _pendingShareActionFromRow(
  db.PendingShareAction row,
) {
  return domain.PendingShareAction(
    id: row.id,
    actionType: _pendingShareActionTypeFromDb(row.actionType),
    shareId: row.shareId,
    resourceType: row.resourceType == null
        ? null
        : _shareResourceTypeFromDb(row.resourceType!),
    payloadJson: row.payloadJson,
    recipientEmailsJson: row.recipientEmailsJson,
    createdAt: _utc(row.createdAt),
    lastAttemptAt: _utcOrNull(row.lastAttemptAt),
    attemptCount: row.attemptCount,
    status: _pendingShareActionStatusFromDb(row.status),
  );
}

db.ProgramsCompanion _programCompanion(domain.Program program) {
  final values = _metadataValues(program.metadata);
  return db.ProgramsCompanion(
    id: values[0] as Value<String>,
    createdAt: values[1] as Value<DateTime>,
    updatedAt: values[2] as Value<DateTime>,
    deletedAt: values[3] as Value<DateTime?>,
    schemaVersion: values[4] as Value<int>,
    syncState: values[5] as Value<String>,
    localRevision: values[6] as Value<int>,
    originDeviceId: values[7] as Value<String>,
    futureOwnerProfileId: values[8] as Value<String?>,
    lastSyncedAt: values[9] as Value<DateTime?>,
    remoteRevision: values[10] as Value<String?>,
    name: Value(program.name),
    defaultRestSeconds: Value(program.defaultRestSeconds),
    tagsJson: Value(_encodeTags(program.tags)),
  );
}

domain.Program _programFromRow(
  db.Program row,
  List<domain.ProgramExercise> exercises, {
  bool isExample = false,
}) {
  return domain.Program(
    metadata: _metadataFromRow(row),
    name: row.name,
    defaultRestSeconds: row.defaultRestSeconds,
    isExample: isExample,
    tags: _decodeTags(row.tagsJson),
    exercises: exercises,
  );
}

db.ProgramExercisesCompanion _programExerciseCompanion(
  domain.ProgramExercise exercise,
) {
  final values = _metadataValues(exercise.metadata);
  return db.ProgramExercisesCompanion(
    id: values[0] as Value<String>,
    createdAt: values[1] as Value<DateTime>,
    updatedAt: values[2] as Value<DateTime>,
    deletedAt: values[3] as Value<DateTime?>,
    schemaVersion: values[4] as Value<int>,
    syncState: values[5] as Value<String>,
    localRevision: values[6] as Value<int>,
    originDeviceId: values[7] as Value<String>,
    futureOwnerProfileId: values[8] as Value<String?>,
    lastSyncedAt: values[9] as Value<DateTime?>,
    remoteRevision: values[10] as Value<String?>,
    programId: Value(exercise.programId),
    sourceExerciseId: Value(exercise.sourceExerciseId),
    position: Value(exercise.position),
    exerciseNameSnapshot: Value(exercise.exerciseNameSnapshot),
    exerciseDescriptionSnapshot: Value(exercise.exerciseDescriptionSnapshot),
    exerciseImageMediaIdSnapshot: Value(exercise.exerciseImageMediaIdSnapshot),
    exerciseImageMediaIdsSnapshotJson: Value(
      _encodeImageMediaIdsSnapshot(exercise.exerciseImageMediaIdsSnapshot),
    ),
    exerciseStepsSnapshotJson: Value(
      _encodeExerciseStepsSnapshot(exercise.exerciseStepsSnapshot),
    ),
    autoStartNextTimedStepSnapshot: Value(
      exercise.autoStartNextTimedStepSnapshot,
    ),
    autoStartNextTimedStepOverride: Value(
      exercise.autoStartNextTimedStepOverride,
    ),
    exerciseVideoMediaIdSnapshot: Value(exercise.exerciseVideoMediaIdSnapshot),
    exerciseArchivedSnapshot: Value(exercise.exerciseArchivedSnapshot),
    availableTimeSnapshot: Value(exercise.availableTimeSnapshot),
    availableRepsSnapshot: Value(exercise.availableRepsSnapshot),
    availableScoreSnapshot: Value(exercise.availableScoreSnapshot),
    scoreInputModeSnapshot: Value(
      _scoreInputModeToDb(exercise.scoreInputModeSnapshot),
    ),
    scoreLabelSnapshot: Value(exercise.scoreLabelSnapshot),
    scoreUnitSnapshot: Value(exercise.scoreUnitSnapshot),
    setsCount: Value(exercise.setsCount),
    timeEnabled: Value(exercise.timeEnabled),
    repsEnabled: Value(exercise.repsEnabled),
    scoreEnabled: Value(exercise.scoreEnabled),
    targetTimeSeconds: Value(exercise.targetTimeSeconds),
    targetReps: Value(exercise.targetReps),
    targetScore: Value(exercise.targetScore),
    targetScoreTimeMs: Value(exercise.targetScoreTimeMs),
    restSecondsOverride: Value(exercise.restSecondsOverride),
  );
}

domain.ProgramExercise _programExerciseFromRow(db.ProgramExercise row) {
  return domain.ProgramExercise(
    metadata: _metadataFromRow(row),
    programId: row.programId,
    sourceExerciseId: row.sourceExerciseId,
    position: row.position,
    exerciseNameSnapshot: row.exerciseNameSnapshot,
    exerciseDescriptionSnapshot: row.exerciseDescriptionSnapshot,
    exerciseImageMediaIdSnapshot: row.exerciseImageMediaIdSnapshot,
    exerciseImageMediaIdsSnapshot: _decodeImageMediaIdsSnapshot(
      row.exerciseImageMediaIdsSnapshotJson,
      row.exerciseImageMediaIdSnapshot,
    ),
    exerciseStepsSnapshot: _decodeExerciseStepsSnapshot(
      row.exerciseStepsSnapshotJson,
    ),
    autoStartNextTimedStepSnapshot: row.autoStartNextTimedStepSnapshot,
    autoStartNextTimedStepOverride: row.autoStartNextTimedStepOverride,
    exerciseVideoMediaIdSnapshot: row.exerciseVideoMediaIdSnapshot,
    exerciseArchivedSnapshot: row.exerciseArchivedSnapshot,
    availableTimeSnapshot: row.availableTimeSnapshot,
    availableRepsSnapshot: row.availableRepsSnapshot,
    availableScoreSnapshot: row.availableScoreSnapshot,
    scoreInputModeSnapshot: _scoreInputModeFromDb(row.scoreInputModeSnapshot),
    scoreLabelSnapshot: row.scoreLabelSnapshot,
    scoreUnitSnapshot: row.scoreUnitSnapshot,
    setsCount: row.setsCount,
    timeEnabled: row.timeEnabled,
    repsEnabled: row.repsEnabled,
    scoreEnabled: row.scoreEnabled,
    targetTimeSeconds: row.targetTimeSeconds,
    targetReps: row.targetReps,
    targetScore: row.targetScore,
    targetScoreTimeMs: row.targetScoreTimeMs,
    restSecondsOverride: row.restSecondsOverride,
  );
}

db.WorkoutTemplatesCompanion _workoutTemplateCompanion(
  domain.WorkoutTemplate template,
) {
  final values = _metadataValues(template.metadata);
  return db.WorkoutTemplatesCompanion(
    id: values[0] as Value<String>,
    createdAt: values[1] as Value<DateTime>,
    updatedAt: values[2] as Value<DateTime>,
    deletedAt: values[3] as Value<DateTime?>,
    schemaVersion: values[4] as Value<int>,
    syncState: values[5] as Value<String>,
    localRevision: values[6] as Value<int>,
    originDeviceId: values[7] as Value<String>,
    futureOwnerProfileId: values[8] as Value<String?>,
    lastSyncedAt: values[9] as Value<DateTime?>,
    remoteRevision: values[10] as Value<String?>,
    name: Value(template.name),
    lastStartedAt: Value(_utcOrNull(template.lastStartedAt)),
    tagsJson: Value(_encodeTags(template.tags)),
  );
}

domain.WorkoutTemplate _workoutTemplateFromRow(
  db.WorkoutTemplate row,
  List<domain.WorkoutTemplateProgram> programs,
  List<domain.WorkoutTemplateExerciseOverride> overrides, {
  bool isExample = false,
}) {
  return domain.WorkoutTemplate(
    metadata: _metadataFromRow(row),
    name: row.name,
    lastStartedAt: _utcOrNull(row.lastStartedAt),
    isExample: isExample,
    tags: _decodeTags(row.tagsJson),
    programs: programs,
    overrides: overrides,
  );
}

db.WorkoutTemplateProgramsCompanion _workoutTemplateProgramCompanion(
  domain.WorkoutTemplateProgram program,
) {
  final values = _metadataValues(program.metadata);
  return db.WorkoutTemplateProgramsCompanion(
    id: values[0] as Value<String>,
    createdAt: values[1] as Value<DateTime>,
    updatedAt: values[2] as Value<DateTime>,
    deletedAt: values[3] as Value<DateTime?>,
    schemaVersion: values[4] as Value<int>,
    syncState: values[5] as Value<String>,
    localRevision: values[6] as Value<int>,
    originDeviceId: values[7] as Value<String>,
    futureOwnerProfileId: values[8] as Value<String?>,
    lastSyncedAt: values[9] as Value<DateTime?>,
    remoteRevision: values[10] as Value<String?>,
    workoutTemplateId: Value(program.workoutTemplateId),
    sourceProgramId: Value(program.sourceProgramId),
    position: Value(program.position),
    programNameSnapshot: Value(program.programNameSnapshot),
    defaultRestSecondsSnapshot: Value(program.defaultRestSecondsSnapshot),
    programSnapshotJson: Value(program.programSnapshotJson),
  );
}

domain.WorkoutTemplateProgram _workoutTemplateProgramFromRow(
  db.WorkoutTemplateProgram row,
) {
  return domain.WorkoutTemplateProgram(
    metadata: _metadataFromRow(row),
    workoutTemplateId: row.workoutTemplateId,
    sourceProgramId: row.sourceProgramId,
    position: row.position,
    programNameSnapshot: row.programNameSnapshot,
    defaultRestSecondsSnapshot: row.defaultRestSecondsSnapshot,
    programSnapshotJson: row.programSnapshotJson,
  );
}

db.WorkoutTemplateExerciseOverridesCompanion
_workoutTemplateExerciseOverrideCompanion(
  domain.WorkoutTemplateExerciseOverride override,
) {
  final values = _metadataValues(override.metadata);
  return db.WorkoutTemplateExerciseOverridesCompanion(
    id: values[0] as Value<String>,
    createdAt: values[1] as Value<DateTime>,
    updatedAt: values[2] as Value<DateTime>,
    deletedAt: values[3] as Value<DateTime?>,
    schemaVersion: values[4] as Value<int>,
    syncState: values[5] as Value<String>,
    localRevision: values[6] as Value<int>,
    originDeviceId: values[7] as Value<String>,
    futureOwnerProfileId: values[8] as Value<String?>,
    lastSyncedAt: values[9] as Value<DateTime?>,
    remoteRevision: values[10] as Value<String?>,
    workoutTemplateProgramId: Value(override.workoutTemplateProgramId),
    snapshotProgramExerciseId: Value(override.snapshotProgramExerciseId),
    setsCountOverride: Value(override.setsCountOverride),
    targetTimeSecondsOverride: Value(override.targetTimeSecondsOverride),
    targetRepsOverride: Value(override.targetRepsOverride),
    targetScoreOverride: Value(override.targetScoreOverride),
    targetScoreTimeMsOverride: Value(override.targetScoreTimeMsOverride),
    autoStartNextTimedStepOverride: Value(
      override.autoStartNextTimedStepOverride,
    ),
  );
}

domain.WorkoutTemplateExerciseOverride _workoutTemplateExerciseOverrideFromRow(
  db.WorkoutTemplateExerciseOverride row,
) {
  return domain.WorkoutTemplateExerciseOverride(
    metadata: _metadataFromRow(row),
    workoutTemplateProgramId: row.workoutTemplateProgramId,
    snapshotProgramExerciseId: row.snapshotProgramExerciseId,
    setsCountOverride: row.setsCountOverride,
    targetTimeSecondsOverride: row.targetTimeSecondsOverride,
    targetRepsOverride: row.targetRepsOverride,
    targetScoreOverride: row.targetScoreOverride,
    targetScoreTimeMsOverride: row.targetScoreTimeMsOverride,
    autoStartNextTimedStepOverride: row.autoStartNextTimedStepOverride,
  );
}

db.ActiveWorkoutSessionsCompanion _activeWorkoutSessionCompanion(
  domain.ActiveWorkoutSession session,
) {
  final values = _metadataValues(session.metadata);
  return db.ActiveWorkoutSessionsCompanion(
    id: values[0] as Value<String>,
    createdAt: values[1] as Value<DateTime>,
    updatedAt: values[2] as Value<DateTime>,
    deletedAt: values[3] as Value<DateTime?>,
    schemaVersion: values[4] as Value<int>,
    syncState: values[5] as Value<String>,
    localRevision: values[6] as Value<int>,
    originDeviceId: values[7] as Value<String>,
    futureOwnerProfileId: values[8] as Value<String?>,
    lastSyncedAt: values[9] as Value<DateTime?>,
    remoteRevision: values[10] as Value<String?>,
    sourceWorkoutTemplateId: Value(session.sourceWorkoutTemplateId),
    status: Value(_activeStatusToDb(session.status)),
    startedAt: Value(session.startedAt.toUtc()),
    pausedAt: Value(_utcOrNull(session.pausedAt)),
    endedAt: Value(_utcOrNull(session.endedAt)),
    lastPersistedAt: Value(session.lastPersistedAt.toUtc()),
    elapsedActiveMs: Value(session.elapsedActiveMs),
    currentProgramIndex: Value(session.currentProgramIndex),
    currentExerciseIndex: Value(session.currentExerciseIndex),
    currentSetIndex: Value(session.currentSetIndex),
    resolvedTemplateSnapshotJson: Value(session.resolvedTemplateSnapshotJson),
  );
}

domain.ActiveWorkoutSession _activeWorkoutSessionFromRow(
  db.ActiveWorkoutSession row,
) {
  return domain.ActiveWorkoutSession(
    metadata: _metadataFromRow(row),
    sourceWorkoutTemplateId: row.sourceWorkoutTemplateId,
    status: _activeStatusFromDb(row.status),
    startedAt: _utc(row.startedAt),
    pausedAt: _utcOrNull(row.pausedAt),
    endedAt: _utcOrNull(row.endedAt),
    lastPersistedAt: _utc(row.lastPersistedAt),
    elapsedActiveMs: row.elapsedActiveMs,
    currentProgramIndex: row.currentProgramIndex,
    currentExerciseIndex: row.currentExerciseIndex,
    currentSetIndex: row.currentSetIndex,
    resolvedTemplateSnapshotJson: row.resolvedTemplateSnapshotJson,
  );
}

db.ActiveSetResultsCompanion _activeSetResultCompanion(
  domain.ActiveSetResult result,
) {
  final values = _metadataValues(result.metadata);
  return db.ActiveSetResultsCompanion(
    id: values[0] as Value<String>,
    createdAt: values[1] as Value<DateTime>,
    updatedAt: values[2] as Value<DateTime>,
    deletedAt: values[3] as Value<DateTime?>,
    schemaVersion: values[4] as Value<int>,
    syncState: values[5] as Value<String>,
    localRevision: values[6] as Value<int>,
    originDeviceId: values[7] as Value<String>,
    futureOwnerProfileId: values[8] as Value<String?>,
    lastSyncedAt: values[9] as Value<DateTime?>,
    remoteRevision: values[10] as Value<String?>,
    activeWorkoutSessionId: Value(result.activeWorkoutSessionId),
    programSnapshotId: Value(result.programSnapshotId),
    exerciseSnapshotId: Value(result.exerciseSnapshotId),
    programIndex: Value(result.programIndex),
    exerciseIndex: Value(result.exerciseIndex),
    setIndex: Value(result.setIndex),
    startedAt: Value(_utcOrNull(result.startedAt)),
    completedAt: Value(_utcOrNull(result.completedAt)),
    actualTimeMs: Value(result.actualTimeMs),
    actualReps: Value(result.actualReps),
    actualScore: Value(result.actualScore),
    actualScoreTimeMs: Value(result.actualScoreTimeMs),
    scoreInputModeSnapshot: Value(
      _scoreInputModeToDb(result.scoreInputModeSnapshot),
    ),
    scoreLabelSnapshot: Value(result.scoreLabelSnapshot),
    scoreUnitSnapshot: Value(result.scoreUnitSnapshot),
    note: Value(result.note),
    status: Value(_setResultStatusToDb(result.status)),
  );
}

domain.ActiveSetResult _activeSetResultFromRow(db.ActiveSetResult row) {
  return domain.ActiveSetResult(
    metadata: _metadataFromRow(row),
    activeWorkoutSessionId: row.activeWorkoutSessionId,
    programSnapshotId: row.programSnapshotId,
    exerciseSnapshotId: row.exerciseSnapshotId,
    programIndex: row.programIndex,
    exerciseIndex: row.exerciseIndex,
    setIndex: row.setIndex,
    startedAt: _utcOrNull(row.startedAt),
    completedAt: _utcOrNull(row.completedAt),
    actualTimeMs: row.actualTimeMs,
    actualReps: row.actualReps,
    actualScore: row.actualScore,
    actualScoreTimeMs: row.actualScoreTimeMs,
    scoreInputModeSnapshot: _scoreInputModeFromDb(row.scoreInputModeSnapshot),
    scoreLabelSnapshot: row.scoreLabelSnapshot,
    scoreUnitSnapshot: row.scoreUnitSnapshot,
    note: row.note,
    status: _setResultStatusFromDb(row.status),
  );
}

db.ActiveRestStatesCompanion _activeRestStateCompanion(
  domain.ActiveRestState restState,
) {
  final values = _metadataValues(restState.metadata);
  return db.ActiveRestStatesCompanion(
    id: values[0] as Value<String>,
    createdAt: values[1] as Value<DateTime>,
    updatedAt: values[2] as Value<DateTime>,
    deletedAt: values[3] as Value<DateTime?>,
    schemaVersion: values[4] as Value<int>,
    syncState: values[5] as Value<String>,
    localRevision: values[6] as Value<int>,
    originDeviceId: values[7] as Value<String>,
    futureOwnerProfileId: values[8] as Value<String?>,
    lastSyncedAt: values[9] as Value<DateTime?>,
    remoteRevision: values[10] as Value<String?>,
    activeWorkoutSessionId: Value(restState.activeWorkoutSessionId),
    afterProgramIndex: Value(restState.afterProgramIndex),
    afterExerciseIndex: Value(restState.afterExerciseIndex),
    afterSetIndex: Value(restState.afterSetIndex),
    plannedRestSeconds: Value(restState.plannedRestSeconds),
    adjustedRestSeconds: Value(restState.adjustedRestSeconds),
    startedAt: Value(restState.startedAt.toUtc()),
    endedAt: Value(_utcOrNull(restState.endedAt)),
    skippedAt: Value(_utcOrNull(restState.skippedAt)),
  );
}

Future<void> _updateActiveRestPauseFields(
  db.AppDatabase database,
  domain.ActiveRestState restState,
) async {
  await database.customUpdate(
    'UPDATE active_rest_states SET paused_at = ?, accumulated_paused_ms = ? '
    'WHERE id = ?',
    variables: [
      Variable<DateTime>(_utcOrNull(restState.pausedAt)),
      Variable<int>(restState.accumulatedPausedMs),
      Variable<String>(restState.metadata.id),
    ],
  );
}

Future<void> _upsertActiveSetTimerState(
  db.AppDatabase database,
  domain.ActiveSetTimerState state,
) async {
  final metadata = state.metadata;
  await database.customInsert(
    'INSERT INTO active_set_timer_states ('
    'id, created_at, updated_at, deleted_at, schema_version, sync_state, '
    'local_revision, origin_device_id, future_owner_profile_id, '
    'last_synced_at, remote_revision, active_workout_session_id, '
    'program_index, exercise_index, set_index, status, started_at, '
    'accumulated_ms, stopped_at, skipped_at'
    ') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) '
    'ON CONFLICT(active_workout_session_id, program_index, exercise_index, '
    'set_index) DO UPDATE SET '
    'id = excluded.id, '
    'created_at = excluded.created_at, '
    'updated_at = excluded.updated_at, '
    'deleted_at = excluded.deleted_at, '
    'schema_version = excluded.schema_version, '
    'sync_state = excluded.sync_state, '
    'local_revision = excluded.local_revision, '
    'origin_device_id = excluded.origin_device_id, '
    'future_owner_profile_id = excluded.future_owner_profile_id, '
    'last_synced_at = excluded.last_synced_at, '
    'remote_revision = excluded.remote_revision, '
    'active_workout_session_id = excluded.active_workout_session_id, '
    'program_index = excluded.program_index, '
    'exercise_index = excluded.exercise_index, '
    'set_index = excluded.set_index, '
    'status = excluded.status, '
    'started_at = excluded.started_at, '
    'accumulated_ms = excluded.accumulated_ms, '
    'stopped_at = excluded.stopped_at, '
    'skipped_at = excluded.skipped_at',
    variables: [
      Variable<String>(metadata.id),
      Variable<DateTime>(metadata.createdAt.toUtc()),
      Variable<DateTime>(metadata.updatedAt.toUtc()),
      Variable<DateTime>(_utcOrNull(metadata.deletedAt)),
      Variable<int>(metadata.schemaVersion),
      Variable<String>(_syncStateToDb(metadata.syncState)),
      Variable<int>(metadata.localRevision),
      Variable<String>(metadata.originDeviceId),
      Variable<String>(metadata.futureOwnerProfileId),
      Variable<DateTime>(_utcOrNull(metadata.lastSyncedAt)),
      Variable<String>(metadata.remoteRevision),
      Variable<String>(state.activeWorkoutSessionId),
      Variable<int>(state.programIndex),
      Variable<int>(state.exerciseIndex),
      Variable<int>(state.setIndex),
      Variable<String>(_setTimerStatusToDb(state.status)),
      Variable<DateTime>(_utcOrNull(state.startedAt)),
      Variable<int>(state.accumulatedMs),
      Variable<DateTime>(_utcOrNull(state.stoppedAt)),
      Variable<DateTime>(_utcOrNull(state.skippedAt)),
    ],
  );
}

domain.ActiveSetTimerState _activeSetTimerStateFromCustomRow(QueryRow row) {
  final data = row.data;
  return domain.ActiveSetTimerState(
    metadata: _metadataFromData(data),
    activeWorkoutSessionId: data['active_workout_session_id'] as String,
    programIndex: data['program_index'] as int,
    exerciseIndex: data['exercise_index'] as int,
    setIndex: data['set_index'] as int,
    status: _setTimerStatusFromDb(data['status'] as String),
    startedAt: _dateTimeOrNullFromData(data, 'started_at'),
    accumulatedMs: data['accumulated_ms'] as int,
    stoppedAt: _dateTimeOrNullFromData(data, 'stopped_at'),
    skippedAt: _dateTimeOrNullFromData(data, 'skipped_at'),
  );
}

domain.ActiveRestState _activeRestStateFromCustomRow(QueryRow row) {
  final data = row.data;
  return domain.ActiveRestState(
    metadata: _metadataFromData(data),
    activeWorkoutSessionId: data['active_workout_session_id'] as String,
    afterProgramIndex: data['after_program_index'] as int,
    afterExerciseIndex: data['after_exercise_index'] as int,
    afterSetIndex: data['after_set_index'] as int,
    plannedRestSeconds: data['planned_rest_seconds'] as int,
    adjustedRestSeconds: data['adjusted_rest_seconds'] as int,
    startedAt: _dateTimeFromData(data, 'started_at'),
    endedAt: _dateTimeOrNullFromData(data, 'ended_at'),
    skippedAt: _dateTimeOrNullFromData(data, 'skipped_at'),
    pausedAt: _dateTimeOrNullFromData(data, 'paused_at'),
    accumulatedPausedMs: data['accumulated_paused_ms'] as int? ?? 0,
  );
}

WorkoutHistorySetPerformance _setPerformanceFromRow(QueryRow row) {
  final data = row.data;
  return WorkoutHistorySetPerformance(
    workoutHistoryId: data['workout_history_id'] as String,
    startedAt: _dateTimeFromData(data, 'history_started_at'),
    setIndex: data['set_index'] as int,
    exerciseNameSnapshot: data['exercise_name_snapshot'] as String,
    scoreInputModeSnapshot: _scoreInputModeFromDb(
      data['score_input_mode_snapshot'] as String,
    ),
    actualTimeMs: data['actual_time_ms'] as int?,
    actualReps: data['actual_reps'] as int?,
    actualScore: (data['actual_score'] as num?)?.toDouble(),
    actualScoreTimeMs: data['actual_score_time_ms'] as int?,
    completedAt: _dateTimeOrNullFromData(data, 'completed_at'),
  );
}

WorkoutHistoryMetricPerformance _metricPerformanceFromRow(
  QueryRow row, {
  required PerformanceMetric metric,
}) {
  final set = _setPerformanceFromRow(row);
  return WorkoutHistoryMetricPerformance(
    workoutHistoryId: set.workoutHistoryId,
    startedAt: set.startedAt,
    setIndex: set.setIndex,
    exerciseNameSnapshot: set.exerciseNameSnapshot,
    metric: metric,
    scoreInputModeSnapshot: set.scoreInputModeSnapshot,
    actualTimeMs: set.actualTimeMs,
    actualReps: set.actualReps,
    actualScore: set.actualScore,
    actualScoreTimeMs: set.actualScoreTimeMs,
    completedAt: set.completedAt,
  );
}

String? _activeMeasuresPredicate(ActivePerformanceMeasures measures) {
  final predicates = <String>[];
  if (measures.scoreEnabled) {
    predicates.add(
      measures.scoreInputMode == domain.ScoreInputMode.stopwatch
          ? "(result.score_input_mode_snapshot = 'stopwatch' "
                'AND result.actual_score_time_ms IS NOT NULL)'
          : "(result.score_input_mode_snapshot = 'manual' "
                'AND result.actual_score IS NOT NULL)',
    );
  }
  if (measures.repsEnabled) {
    predicates.add('result.actual_reps IS NOT NULL');
  }
  if (measures.timeEnabled) {
    predicates.add('result.actual_time_ms IS NOT NULL');
  }
  return predicates.isEmpty ? null : predicates.join(' OR ');
}

({String predicate, String ordering}) _metricSql(
  PerformanceMetric metric,
  domain.ScoreInputMode scoreInputMode,
) {
  return switch (metric) {
    PerformanceMetric.score =>
      scoreInputMode == domain.ScoreInputMode.stopwatch
          ? (
              predicate:
                  "result.score_input_mode_snapshot = 'stopwatch' "
                  'AND result.actual_score_time_ms IS NOT NULL',
              ordering: 'result.actual_score_time_ms ASC',
            )
          : (
              predicate:
                  "result.score_input_mode_snapshot = 'manual' "
                  'AND result.actual_score IS NOT NULL',
              ordering: 'result.actual_score DESC',
            ),
    PerformanceMetric.reps => (
      predicate: 'result.actual_reps IS NOT NULL',
      ordering: 'result.actual_reps DESC',
    ),
    PerformanceMetric.time => (
      predicate: 'result.actual_time_ms IS NOT NULL',
      ordering: 'result.actual_time_ms DESC',
    ),
  };
}

db.ActiveScoreStopwatchStatesCompanion _activeScoreStopwatchStateCompanion(
  domain.ActiveScoreStopwatchState state,
) {
  final values = _metadataValues(state.metadata);
  return db.ActiveScoreStopwatchStatesCompanion(
    id: values[0] as Value<String>,
    createdAt: values[1] as Value<DateTime>,
    updatedAt: values[2] as Value<DateTime>,
    deletedAt: values[3] as Value<DateTime?>,
    schemaVersion: values[4] as Value<int>,
    syncState: values[5] as Value<String>,
    localRevision: values[6] as Value<int>,
    originDeviceId: values[7] as Value<String>,
    futureOwnerProfileId: values[8] as Value<String?>,
    lastSyncedAt: values[9] as Value<DateTime?>,
    remoteRevision: values[10] as Value<String?>,
    activeWorkoutSessionId: Value(state.activeWorkoutSessionId),
    programIndex: Value(state.programIndex),
    exerciseIndex: Value(state.exerciseIndex),
    setIndex: Value(state.setIndex),
    status: Value(_scoreStopwatchStatusToDb(state.status)),
    startedAt: Value(state.startedAt.toUtc()),
    accumulatedMs: Value(state.accumulatedMs),
    stoppedAt: Value(_utcOrNull(state.stoppedAt)),
  );
}

domain.ActiveScoreStopwatchState _activeScoreStopwatchStateFromRow(
  db.ActiveScoreStopwatchState row,
) {
  return domain.ActiveScoreStopwatchState(
    metadata: _metadataFromRow(row),
    activeWorkoutSessionId: row.activeWorkoutSessionId,
    programIndex: row.programIndex,
    exerciseIndex: row.exerciseIndex,
    setIndex: row.setIndex,
    status: _scoreStopwatchStatusFromDb(row.status),
    startedAt: _utc(row.startedAt),
    accumulatedMs: row.accumulatedMs,
    stoppedAt: _utcOrNull(row.stoppedAt),
  );
}

db.ActiveExerciseStepProgressStatesCompanion
_activeExerciseStepProgressStateCompanion(
  domain.ActiveExerciseStepProgressState state,
) {
  final values = _metadataValues(state.metadata);
  return db.ActiveExerciseStepProgressStatesCompanion(
    id: values[0] as Value<String>,
    createdAt: values[1] as Value<DateTime>,
    updatedAt: values[2] as Value<DateTime>,
    deletedAt: values[3] as Value<DateTime?>,
    schemaVersion: values[4] as Value<int>,
    syncState: values[5] as Value<String>,
    localRevision: values[6] as Value<int>,
    originDeviceId: values[7] as Value<String>,
    futureOwnerProfileId: values[8] as Value<String?>,
    lastSyncedAt: values[9] as Value<DateTime?>,
    remoteRevision: values[10] as Value<String?>,
    activeWorkoutSessionId: Value(state.activeWorkoutSessionId),
    programIndex: Value(state.programIndex),
    exerciseIndex: Value(state.exerciseIndex),
    setIndex: Value(state.setIndex),
    currentPassageIndex: Value(state.currentPassageIndex),
    currentStepIndex: Value(state.currentStepIndex),
    currentStepSnapshotId: Value(state.currentStepSnapshotId),
    status: Value(_exerciseStepProgressStatusToDb(state.status)),
    startedAt: Value<DateTime?>(_utcOrNull(state.startedAt)),
    accumulatedMs: Value(state.accumulatedMs),
    lastTransitionAt: Value(state.lastTransitionAt.toUtc()),
  );
}

domain.ActiveExerciseStepProgressState _activeExerciseStepProgressStateFromRow(
  db.ActiveExerciseStepProgressState row,
) {
  return domain.ActiveExerciseStepProgressState(
    metadata: _metadataFromRow(row),
    activeWorkoutSessionId: row.activeWorkoutSessionId,
    programIndex: row.programIndex,
    exerciseIndex: row.exerciseIndex,
    setIndex: row.setIndex,
    currentPassageIndex: row.currentPassageIndex,
    currentStepIndex: row.currentStepIndex,
    currentStepSnapshotId: row.currentStepSnapshotId,
    status: _exerciseStepProgressStatusFromDb(row.status),
    startedAt: _utcOrNull(row.startedAt),
    accumulatedMs: row.accumulatedMs,
    lastTransitionAt: _utc(row.lastTransitionAt),
  );
}

db.ActiveExerciseStepResultsCompanion _activeExerciseStepResultCompanion(
  domain.ActiveExerciseStepResult result,
) {
  final values = _metadataValues(result.metadata);
  return db.ActiveExerciseStepResultsCompanion(
    id: values[0] as Value<String>,
    createdAt: values[1] as Value<DateTime>,
    updatedAt: values[2] as Value<DateTime>,
    deletedAt: values[3] as Value<DateTime?>,
    schemaVersion: values[4] as Value<int>,
    syncState: values[5] as Value<String>,
    localRevision: values[6] as Value<int>,
    originDeviceId: values[7] as Value<String>,
    futureOwnerProfileId: values[8] as Value<String?>,
    lastSyncedAt: values[9] as Value<DateTime?>,
    remoteRevision: values[10] as Value<String?>,
    activeWorkoutSessionId: Value(result.activeWorkoutSessionId),
    programSnapshotId: Value(result.programSnapshotId),
    exerciseSnapshotId: Value(result.exerciseSnapshotId),
    programIndex: Value(result.programIndex),
    exerciseIndex: Value(result.exerciseIndex),
    setIndex: Value(result.setIndex),
    passageIndex: Value(result.passageIndex),
    stepIndex: Value(result.stepIndex),
    stepSnapshotId: Value(result.stepSnapshotId),
    stepNameSnapshot: Value(result.stepNameSnapshot),
    stepTypeSnapshot: Value(_exerciseStepTypeToDb(result.stepTypeSnapshot)),
    targetValueSnapshot: Value(result.targetValueSnapshot),
    hasScoreSnapshot: Value(result.hasScoreSnapshot),
    scoreInputModeSnapshot: Value<String?>(
      result.scoreInputModeSnapshot == null
          ? null
          : _scoreInputModeToDb(result.scoreInputModeSnapshot!),
    ),
    scoreLabelSnapshot: Value<String?>(result.scoreLabelSnapshot),
    scoreUnitSnapshot: Value<String?>(result.scoreUnitSnapshot),
    targetScoreSnapshot: Value<double?>(result.targetScoreSnapshot),
    targetScoreTimeMsSnapshot: Value<int?>(result.targetScoreTimeMsSnapshot),
    status: Value(_setResultStatusToDb(result.status)),
    startedAt: Value<DateTime?>(_utcOrNull(result.startedAt)),
    completedAt: Value<DateTime?>(_utcOrNull(result.completedAt)),
    actualTimeMs: Value<int?>(result.actualTimeMs),
    actualReps: Value<int?>(result.actualReps),
    actualScore: Value<double?>(result.actualScore),
    actualScoreTimeMs: Value<int?>(result.actualScoreTimeMs),
    note: Value<String?>(result.note),
  );
}

domain.ActiveExerciseStepResult _activeExerciseStepResultFromRow(
  db.ActiveExerciseStepResult row,
) {
  return domain.ActiveExerciseStepResult(
    metadata: _metadataFromRow(row),
    activeWorkoutSessionId: row.activeWorkoutSessionId,
    programSnapshotId: row.programSnapshotId,
    exerciseSnapshotId: row.exerciseSnapshotId,
    programIndex: row.programIndex,
    exerciseIndex: row.exerciseIndex,
    setIndex: row.setIndex,
    passageIndex: row.passageIndex,
    stepIndex: row.stepIndex,
    stepSnapshotId: row.stepSnapshotId,
    stepNameSnapshot: row.stepNameSnapshot,
    stepTypeSnapshot: _exerciseStepTypeFromDb(row.stepTypeSnapshot),
    targetValueSnapshot: row.targetValueSnapshot,
    hasScoreSnapshot: row.hasScoreSnapshot,
    scoreInputModeSnapshot: row.scoreInputModeSnapshot == null
        ? null
        : _scoreInputModeFromDb(row.scoreInputModeSnapshot!),
    scoreLabelSnapshot: row.scoreLabelSnapshot,
    scoreUnitSnapshot: row.scoreUnitSnapshot,
    targetScoreSnapshot: row.targetScoreSnapshot,
    targetScoreTimeMsSnapshot: row.targetScoreTimeMsSnapshot,
    status: _setResultStatusFromDb(row.status),
    startedAt: _utcOrNull(row.startedAt),
    completedAt: _utcOrNull(row.completedAt),
    actualTimeMs: row.actualTimeMs,
    actualReps: row.actualReps,
    actualScore: row.actualScore,
    actualScoreTimeMs: row.actualScoreTimeMs,
    note: row.note,
  );
}

db.WorkoutHistoriesCompanion _workoutHistoryCompanion(
  domain.WorkoutHistory history,
) {
  final values = _metadataValues(history.metadata);
  return db.WorkoutHistoriesCompanion(
    id: values[0] as Value<String>,
    createdAt: values[1] as Value<DateTime>,
    updatedAt: values[2] as Value<DateTime>,
    deletedAt: values[3] as Value<DateTime?>,
    schemaVersion: values[4] as Value<int>,
    syncState: values[5] as Value<String>,
    localRevision: values[6] as Value<int>,
    originDeviceId: values[7] as Value<String>,
    futureOwnerProfileId: values[8] as Value<String?>,
    lastSyncedAt: values[9] as Value<DateTime?>,
    remoteRevision: values[10] as Value<String?>,
    sourceWorkoutTemplateId: Value(history.sourceWorkoutTemplateId),
    sourceActiveWorkoutSessionId: Value(history.sourceActiveWorkoutSessionId),
    nameSnapshot: Value(history.nameSnapshot),
    startedAt: Value(history.startedAt.toUtc()),
    endedAt: Value(history.endedAt.toUtc()),
    totalActiveMs: Value(history.totalActiveMs),
    completed: Value(history.completed),
    historySnapshotJson: Value(history.historySnapshotJson),
  );
}

db.WorkoutHistorySetResultsCompanion _workoutHistorySetResultCompanion(
  domain.WorkoutHistorySetResult result,
) {
  final values = _metadataValues(result.metadata);
  return db.WorkoutHistorySetResultsCompanion(
    id: values[0] as Value<String>,
    createdAt: values[1] as Value<DateTime>,
    updatedAt: values[2] as Value<DateTime>,
    deletedAt: values[3] as Value<DateTime?>,
    schemaVersion: values[4] as Value<int>,
    syncState: values[5] as Value<String>,
    localRevision: values[6] as Value<int>,
    originDeviceId: values[7] as Value<String>,
    futureOwnerProfileId: values[8] as Value<String?>,
    lastSyncedAt: values[9] as Value<DateTime?>,
    remoteRevision: values[10] as Value<String?>,
    workoutHistoryId: Value(result.workoutHistoryId),
    programSnapshotId: Value(result.programSnapshotId),
    exerciseSnapshotId: Value(result.exerciseSnapshotId),
    programIndex: Value(result.programIndex),
    exerciseIndex: Value(result.exerciseIndex),
    setIndex: Value(result.setIndex),
    programNameSnapshot: Value(result.programNameSnapshot),
    exerciseNameSnapshot: Value(result.exerciseNameSnapshot),
    timeEnabledSnapshot: Value(result.timeEnabledSnapshot),
    repsEnabledSnapshot: Value(result.repsEnabledSnapshot),
    scoreEnabledSnapshot: Value(result.scoreEnabledSnapshot),
    scoreInputModeSnapshot: Value(
      _scoreInputModeToDb(result.scoreInputModeSnapshot),
    ),
    targetTimeSecondsSnapshot: Value(result.targetTimeSecondsSnapshot),
    targetRepsSnapshot: Value(result.targetRepsSnapshot),
    targetScoreSnapshot: Value(result.targetScoreSnapshot),
    targetScoreTimeMsSnapshot: Value(result.targetScoreTimeMsSnapshot),
    actualTimeMs: Value(result.actualTimeMs),
    actualReps: Value(result.actualReps),
    actualScore: Value(result.actualScore),
    actualScoreTimeMs: Value(result.actualScoreTimeMs),
    scoreLabelSnapshot: Value(result.scoreLabelSnapshot),
    scoreUnitSnapshot: Value(result.scoreUnitSnapshot),
    sourceExerciseIdSnapshot: Value(result.sourceExerciseIdSnapshot),
    startedAt: Value(_utcOrNull(result.startedAt)),
    completedAt: Value(_utcOrNull(result.completedAt)),
    status: Value(_setResultStatusToDb(result.status)),
  );
}

db.WorkoutHistoryStepResultsCompanion _workoutHistoryStepResultCompanion(
  domain.WorkoutHistoryStepResult result,
) {
  final values = _metadataValues(result.metadata);
  return db.WorkoutHistoryStepResultsCompanion(
    id: values[0] as Value<String>,
    createdAt: values[1] as Value<DateTime>,
    updatedAt: values[2] as Value<DateTime>,
    deletedAt: values[3] as Value<DateTime?>,
    schemaVersion: values[4] as Value<int>,
    syncState: values[5] as Value<String>,
    localRevision: values[6] as Value<int>,
    originDeviceId: values[7] as Value<String>,
    futureOwnerProfileId: values[8] as Value<String?>,
    lastSyncedAt: values[9] as Value<DateTime?>,
    remoteRevision: values[10] as Value<String?>,
    workoutHistoryId: Value(result.workoutHistoryId),
    programSnapshotId: Value(result.programSnapshotId),
    exerciseSnapshotId: Value(result.exerciseSnapshotId),
    programIndex: Value(result.programIndex),
    exerciseIndex: Value(result.exerciseIndex),
    setIndex: Value(result.setIndex),
    passageIndex: Value(result.passageIndex),
    stepIndex: Value(result.stepIndex),
    stepSnapshotId: Value(result.stepSnapshotId),
    stepNameSnapshot: Value(result.stepNameSnapshot),
    stepTypeSnapshot: Value(_exerciseStepTypeToDb(result.stepTypeSnapshot)),
    targetValueSnapshot: Value(result.targetValueSnapshot),
    hasScoreSnapshot: Value(result.hasScoreSnapshot),
    scoreInputModeSnapshot: Value<String?>(
      result.scoreInputModeSnapshot == null
          ? null
          : _scoreInputModeToDb(result.scoreInputModeSnapshot!),
    ),
    scoreLabelSnapshot: Value<String?>(result.scoreLabelSnapshot),
    scoreUnitSnapshot: Value<String?>(result.scoreUnitSnapshot),
    targetScoreSnapshot: Value<double?>(result.targetScoreSnapshot),
    targetScoreTimeMsSnapshot: Value<int?>(result.targetScoreTimeMsSnapshot),
    status: Value(_setResultStatusToDb(result.status)),
    startedAt: Value<DateTime?>(_utcOrNull(result.startedAt)),
    completedAt: Value<DateTime?>(_utcOrNull(result.completedAt)),
    actualTimeMs: Value<int?>(result.actualTimeMs),
    actualReps: Value<int?>(result.actualReps),
    actualScore: Value<double?>(result.actualScore),
    actualScoreTimeMs: Value<int?>(result.actualScoreTimeMs),
    note: Value<String?>(result.note),
    sourceExerciseIdSnapshot: Value<String?>(result.sourceExerciseIdSnapshot),
  );
}

domain.WorkoutHistory _workoutHistoryFromRow(
  db.WorkoutHistory row,
  List<domain.WorkoutHistorySetResult> results,
  List<domain.WorkoutHistoryStepResult> stepResults,
) {
  return domain.WorkoutHistory(
    metadata: _metadataFromRow(row),
    sourceWorkoutTemplateId: row.sourceWorkoutTemplateId,
    sourceActiveWorkoutSessionId: row.sourceActiveWorkoutSessionId,
    nameSnapshot: row.nameSnapshot,
    startedAt: _utc(row.startedAt),
    endedAt: _utc(row.endedAt),
    totalActiveMs: row.totalActiveMs,
    completed: row.completed,
    historySnapshotJson: row.historySnapshotJson,
    results: results,
    stepResults: stepResults,
  );
}

domain.WorkoutHistorySetResult _workoutHistorySetResultFromRow(
  db.WorkoutHistorySetResult row,
) {
  return domain.WorkoutHistorySetResult(
    metadata: _metadataFromRow(row),
    workoutHistoryId: row.workoutHistoryId,
    programSnapshotId: row.programSnapshotId,
    exerciseSnapshotId: row.exerciseSnapshotId,
    programIndex: row.programIndex,
    exerciseIndex: row.exerciseIndex,
    setIndex: row.setIndex,
    programNameSnapshot: row.programNameSnapshot,
    exerciseNameSnapshot: row.exerciseNameSnapshot,
    timeEnabledSnapshot: row.timeEnabledSnapshot,
    repsEnabledSnapshot: row.repsEnabledSnapshot,
    scoreEnabledSnapshot: row.scoreEnabledSnapshot,
    scoreInputModeSnapshot: _scoreInputModeFromDb(row.scoreInputModeSnapshot),
    targetTimeSecondsSnapshot: row.targetTimeSecondsSnapshot,
    targetRepsSnapshot: row.targetRepsSnapshot,
    targetScoreSnapshot: row.targetScoreSnapshot,
    targetScoreTimeMsSnapshot: row.targetScoreTimeMsSnapshot,
    actualTimeMs: row.actualTimeMs,
    actualReps: row.actualReps,
    actualScore: row.actualScore,
    actualScoreTimeMs: row.actualScoreTimeMs,
    scoreLabelSnapshot: row.scoreLabelSnapshot,
    scoreUnitSnapshot: row.scoreUnitSnapshot,
    sourceExerciseIdSnapshot: row.sourceExerciseIdSnapshot,
    startedAt: _utcOrNull(row.startedAt),
    completedAt: _utcOrNull(row.completedAt),
    status: _setResultStatusFromDb(row.status),
  );
}

domain.WorkoutHistoryStepResult _workoutHistoryStepResultFromRow(
  db.WorkoutHistoryStepResult row,
) {
  return domain.WorkoutHistoryStepResult(
    metadata: _metadataFromRow(row),
    workoutHistoryId: row.workoutHistoryId,
    programSnapshotId: row.programSnapshotId,
    exerciseSnapshotId: row.exerciseSnapshotId,
    programIndex: row.programIndex,
    exerciseIndex: row.exerciseIndex,
    setIndex: row.setIndex,
    passageIndex: row.passageIndex,
    stepIndex: row.stepIndex,
    stepSnapshotId: row.stepSnapshotId,
    stepNameSnapshot: row.stepNameSnapshot,
    stepTypeSnapshot: _exerciseStepTypeFromDb(row.stepTypeSnapshot),
    targetValueSnapshot: row.targetValueSnapshot,
    hasScoreSnapshot: row.hasScoreSnapshot,
    scoreInputModeSnapshot: row.scoreInputModeSnapshot == null
        ? null
        : _scoreInputModeFromDb(row.scoreInputModeSnapshot!),
    scoreLabelSnapshot: row.scoreLabelSnapshot,
    scoreUnitSnapshot: row.scoreUnitSnapshot,
    targetScoreSnapshot: row.targetScoreSnapshot,
    targetScoreTimeMsSnapshot: row.targetScoreTimeMsSnapshot,
    status: _setResultStatusFromDb(row.status),
    startedAt: _utcOrNull(row.startedAt),
    completedAt: _utcOrNull(row.completedAt),
    actualTimeMs: row.actualTimeMs,
    actualReps: row.actualReps,
    actualScore: row.actualScore,
    actualScoreTimeMs: row.actualScoreTimeMs,
    note: row.note,
    sourceExerciseIdSnapshot: row.sourceExerciseIdSnapshot,
  );
}

db.RemoteResourceMappingsCompanion _remoteResourceMappingCompanion(
  RemoteResourceMapping mapping,
) {
  return db.RemoteResourceMappingsCompanion.insert(
    id: _remoteResourceMappingId(mapping.resourceType, mapping.clientId),
    resourceType: _syncResourceTypeToDb(mapping.resourceType),
    clientId: mapping.clientId,
    serverId: mapping.serverId,
    serverUpdatedAt: mapping.serverUpdatedAt.toUtc(),
  );
}

RemoteResourceMapping _remoteResourceMappingFromRow(
  db.RemoteResourceMapping row,
) {
  return RemoteResourceMapping(
    resourceType: _syncResourceTypeFromDb(row.resourceType),
    clientId: row.clientId,
    serverId: row.serverId,
    serverUpdatedAt: _utc(row.serverUpdatedAt),
  );
}

String _remoteResourceMappingId(
  SyncResourceType resourceType,
  String clientId,
) {
  return '${_syncResourceTypeToDb(resourceType)}:$clientId';
}

final class _LocalSyncSnapshot {
  const _LocalSyncSnapshot({
    required this.schemaVersion,
    required this.updatedAt,
    required this.deletedAt,
    required this.payload,
  });

  factory _LocalSyncSnapshot.fromMetadata(
    domain.EntityMetadata metadata,
    Map<String, Object?> payload,
  ) {
    return _LocalSyncSnapshot(
      schemaVersion: metadata.schemaVersion,
      updatedAt: metadata.updatedAt.toUtc(),
      deletedAt: _utcOrNull(metadata.deletedAt),
      payload: payload,
    );
  }

  final int schemaVersion;
  final DateTime updatedAt;
  final DateTime? deletedAt;
  final Map<String, Object?> payload;
}

final class _BackupLocalRow {
  const _BackupLocalRow({
    required this.updatedAt,
    required this.deletedAt,
    required this.localRevision,
  });

  final DateTime updatedAt;
  final DateTime? deletedAt;
  final int localRevision;
}

Map<String, Object?> _metadataPayload(domain.EntityMetadata metadata) => {
  'id': metadata.id,
  'createdAt': metadata.createdAt.toUtc().toIso8601String(),
  'updatedAt': metadata.updatedAt.toUtc().toIso8601String(),
  'deletedAt': metadata.deletedAt?.toUtc().toIso8601String(),
  'schemaVersion': metadata.schemaVersion,
  'syncState': metadata.syncState.name,
  'localRevision': metadata.localRevision,
  'originDeviceId': metadata.originDeviceId,
};

Map<String, Object?> _exercisePayload(domain.Exercise exercise) => {
  'metadata': _metadataPayload(exercise.metadata),
  'id': exercise.metadata.id,
  'name': exercise.name,
  'description': exercise.description,
  'imageMediaIds': exercise.imageMediaIds,
  'iconMediaId': exercise.iconMediaId,
  'videoMediaId': exercise.videoMediaId,
  'hasTimeMeasure': exercise.hasTimeMeasure,
  'hasRepsMeasure': exercise.hasRepsMeasure,
  'hasScoreMeasure': exercise.hasScoreMeasure,
  'scoreInputMode': exercise.scoreInputMode.name,
  'scoreLabel': exercise.scoreLabel,
  'scoreUnit': exercise.scoreUnit,
  'defaultTargetTimeSeconds': exercise.defaultTargetTimeSeconds,
  'defaultTargetReps': exercise.defaultTargetReps,
  'defaultTargetScore': exercise.defaultTargetScore,
  'defaultTargetScoreTimeMs': exercise.defaultTargetScoreTimeMs,
  'autoStartNextTimedStep': exercise.autoStartNextTimedStep,
  'category': exercise.category.name,
  'isExample': exercise.isExample,
  'tags': exercise.tags,
  'steps': exercise.steps.map((step) => step.toSnapshotJson()).toList(),
  'archivedAt': exercise.archivedAt?.toUtc().toIso8601String(),
};

Map<String, Object?> _mediaAssetPayload(domain.MediaAsset asset) => {
  'metadata': _metadataPayload(asset.metadata),
  'id': asset.metadata.id,
  'kind': _mediaKindToDb(asset.kind),
  'localUri': asset.localUri,
  'mimeType': asset.mimeType,
  'sizeBytes': asset.sizeBytes,
  'width': asset.width,
  'height': asset.height,
  'durationMs': asset.durationMs,
  'checksum': asset.checksum,
  'remoteUri': asset.remoteUri,
  'thumbnailLocalUri': asset.thumbnailLocalUri,
};

Map<String, Object?> _programPayload(domain.Program program) => {
  'metadata': _metadataPayload(program.metadata),
  'id': program.metadata.id,
  'name': program.name,
  'defaultRestSeconds': program.defaultRestSeconds,
  'isExample': program.isExample,
  'tags': program.tags,
  'exercises': program.exercises
      .map((exercise) => exercise.toSnapshotJson())
      .toList(),
};

Map<String, Object?> _workoutTemplatePayload(domain.WorkoutTemplate template) =>
    {
      'metadata': _metadataPayload(template.metadata),
      'id': template.metadata.id,
      'name': template.name,
      'lastStartedAt': template.lastStartedAt?.toUtc().toIso8601String(),
      'isExample': template.isExample,
      'tags': template.tags,
      'programs': template.programs
          .map(
            (program) => {
              'id': program.metadata.id,
              'sourceProgramId': program.sourceProgramId,
              'position': program.position,
              'programNameSnapshot': program.programNameSnapshot,
              'defaultRestSecondsSnapshot': program.defaultRestSecondsSnapshot,
              'programSnapshotJson': program.programSnapshotJson,
            },
          )
          .toList(),
      'overrides': template.overrides
          .map(
            (override) => {
              'id': override.metadata.id,
              'workoutTemplateProgramId': override.workoutTemplateProgramId,
              'snapshotProgramExerciseId': override.snapshotProgramExerciseId,
              'setsCountOverride': override.setsCountOverride,
              'targetTimeSecondsOverride': override.targetTimeSecondsOverride,
              'targetRepsOverride': override.targetRepsOverride,
              'targetScoreOverride': override.targetScoreOverride,
              'targetScoreTimeMsOverride': override.targetScoreTimeMsOverride,
              'autoStartNextTimedStepOverride':
                  override.autoStartNextTimedStepOverride,
            },
          )
          .toList(),
    };

Map<String, Object?> _workoutHistoryPayload(domain.WorkoutHistory history) => {
  'metadata': _metadataPayload(history.metadata),
  'id': history.metadata.id,
  'sourceWorkoutTemplateId': history.sourceWorkoutTemplateId,
  'sourceActiveWorkoutSessionId': history.sourceActiveWorkoutSessionId,
  'nameSnapshot': history.nameSnapshot,
  'startedAt': history.startedAt.toUtc().toIso8601String(),
  'endedAt': history.endedAt.toUtc().toIso8601String(),
  'totalActiveMs': history.totalActiveMs,
  'completed': history.completed,
  'historySnapshotJson': history.historySnapshotJson,
};

Map<String, Object?> _localWorkoutHistoryPayload(
  domain.WorkoutHistory history,
) => {
  ..._workoutHistoryPayload(history),
  'results': history.results.map(_workoutHistorySetResultPayload).toList(),
  'stepResults': history.stepResults
      .map(_workoutHistoryStepResultPayload)
      .toList(),
};

Map<String, Object?> _workoutHistorySetResultPayload(
  domain.WorkoutHistorySetResult result,
) => {
  'metadata': _metadataPayload(result.metadata),
  'id': result.metadata.id,
  'workoutHistoryId': result.workoutHistoryId,
  'programSnapshotId': result.programSnapshotId,
  'exerciseSnapshotId': result.exerciseSnapshotId,
  'programIndex': result.programIndex,
  'exerciseIndex': result.exerciseIndex,
  'setIndex': result.setIndex,
  'programNameSnapshot': result.programNameSnapshot,
  'exerciseNameSnapshot': result.exerciseNameSnapshot,
  'timeEnabledSnapshot': result.timeEnabledSnapshot,
  'repsEnabledSnapshot': result.repsEnabledSnapshot,
  'scoreEnabledSnapshot': result.scoreEnabledSnapshot,
  'scoreInputModeSnapshot': result.scoreInputModeSnapshot.name,
  'targetTimeSecondsSnapshot': result.targetTimeSecondsSnapshot,
  'targetRepsSnapshot': result.targetRepsSnapshot,
  'targetScoreSnapshot': result.targetScoreSnapshot,
  'targetScoreTimeMsSnapshot': result.targetScoreTimeMsSnapshot,
  'actualTimeMs': result.actualTimeMs,
  'actualReps': result.actualReps,
  'actualScore': result.actualScore,
  'actualScoreTimeMs': result.actualScoreTimeMs,
  'scoreLabelSnapshot': result.scoreLabelSnapshot,
  'scoreUnitSnapshot': result.scoreUnitSnapshot,
  'sourceExerciseIdSnapshot': result.sourceExerciseIdSnapshot,
  'startedAt': result.startedAt?.toUtc().toIso8601String(),
  'completedAt': result.completedAt?.toUtc().toIso8601String(),
  'status': result.status.name,
};

Map<String, Object?> _workoutHistoryStepResultPayload(
  domain.WorkoutHistoryStepResult result,
) => {
  'metadata': _metadataPayload(result.metadata),
  'id': result.metadata.id,
  'workoutHistoryId': result.workoutHistoryId,
  'programSnapshotId': result.programSnapshotId,
  'exerciseSnapshotId': result.exerciseSnapshotId,
  'programIndex': result.programIndex,
  'exerciseIndex': result.exerciseIndex,
  'setIndex': result.setIndex,
  'passageIndex': result.passageIndex,
  'stepIndex': result.stepIndex,
  'stepSnapshotId': result.stepSnapshotId,
  'stepNameSnapshot': result.stepNameSnapshot,
  'stepTypeSnapshot': result.stepTypeSnapshot.name,
  'targetValueSnapshot': result.targetValueSnapshot,
  'hasScoreSnapshot': result.hasScoreSnapshot,
  'scoreInputModeSnapshot': result.scoreInputModeSnapshot?.name,
  'scoreLabelSnapshot': result.scoreLabelSnapshot,
  'scoreUnitSnapshot': result.scoreUnitSnapshot,
  'targetScoreSnapshot': result.targetScoreSnapshot,
  'targetScoreTimeMsSnapshot': result.targetScoreTimeMsSnapshot,
  'status': result.status.name,
  'startedAt': result.startedAt?.toUtc().toIso8601String(),
  'completedAt': result.completedAt?.toUtc().toIso8601String(),
  'actualTimeMs': result.actualTimeMs,
  'actualReps': result.actualReps,
  'actualScore': result.actualScore,
  'actualScoreTimeMs': result.actualScoreTimeMs,
  'note': result.note,
  'sourceExerciseIdSnapshot': result.sourceExerciseIdSnapshot,
};

LocalBackupResource _backupResource(
  domain.EntityMetadata metadata,
  Map<String, Object?> payload,
) {
  return LocalBackupResource(
    id: metadata.id,
    updatedAt: metadata.updatedAt.toUtc(),
    payload: payload,
  );
}

RemoteSyncedItem _remoteItemFromBackupResource(
  SyncResourceType resourceType,
  LocalBackupResource resource,
) {
  final metadata = resource.payload['metadata'];
  final metadataMap = metadata is Map
      ? Map<String, Object?>.from(metadata)
      : const <String, Object?>{};
  return RemoteSyncedItem(
    resourceType: resourceType,
    clientId: resource.id,
    serverId: '',
    schemaVersion: metadataMap['schemaVersion'] as int? ?? 1,
    clientUpdatedAt: resource.updatedAt.toUtc(),
    serverUpdatedAt: resource.updatedAt.toUtc(),
    deletedAt: _dateTimeFromPayload(metadataMap['deletedAt']),
    payload: resource.payload,
  );
}

Map<String, Object?> _payloadWithImportedMetadata(
  Map<String, Object?> payload,
  DateTime importedAt,
  int localRevision, {
  String? snapshotOriginDeviceId,
}) {
  final metadata = payload['metadata'];
  final metadataMap = metadata is Map
      ? Map<String, Object?>.from(metadata)
      : <String, Object?>{};
  return {
    ...payload,
    'metadata': {
      ...metadataMap,
      'updatedAt': importedAt.toUtc().toIso8601String(),
      'deletedAt': null,
      'syncState': domain.SyncState.dirty.name,
      'localRevision': localRevision,
      'originDeviceId':
          metadataMap['originDeviceId'] as String? ??
          snapshotOriginDeviceId ??
          'local-device',
    },
  };
}

domain.WorkoutHistory _workoutHistoryFromLocalBackupPayload(
  RemoteSyncedItem item,
) {
  final payload = item.payload;
  final metadata = _metadataFromPayload(item);
  return domain.WorkoutHistory(
    metadata: metadata,
    sourceWorkoutTemplateId: payload['sourceWorkoutTemplateId'] as String?,
    sourceActiveWorkoutSessionId:
        payload['sourceActiveWorkoutSessionId'] as String?,
    nameSnapshot: _stringFromPayload(payload, 'nameSnapshot', item.clientId),
    startedAt:
        _dateTimeFromPayload(payload['startedAt']) ?? item.clientUpdatedAt,
    endedAt: _dateTimeFromPayload(payload['endedAt']) ?? item.clientUpdatedAt,
    totalActiveMs: payload['totalActiveMs'] as int? ?? 0,
    completed: payload['completed'] as bool? ?? false,
    historySnapshotJson:
        payload['historySnapshotJson'] as String? ?? '{"programs":[]}',
    results: _workoutHistorySetResultsFromPayload(payload['results'], metadata),
    stepResults: _workoutHistoryStepResultsFromPayload(
      payload['stepResults'],
      metadata,
    ),
  );
}

domain.Exercise _exerciseFromPayload(RemoteSyncedItem item) {
  final payload = item.payload;
  return domain.Exercise(
    metadata: _metadataFromPayload(item),
    name: _stringFromPayload(payload, 'name', item.clientId),
    description: payload['description'] as String?,
    imageMediaIds: _stringListFromPayload(payload['imageMediaIds']),
    iconMediaId: payload['iconMediaId'] as String?,
    videoMediaId: payload['videoMediaId'] as String?,
    hasTimeMeasure: payload['hasTimeMeasure'] == true,
    hasRepsMeasure: payload['hasRepsMeasure'] == true,
    hasScoreMeasure: payload['hasScoreMeasure'] == true,
    scoreInputMode: _scoreInputModeFromDb(
      payload['scoreInputMode'] as String? ?? 'manual',
    ),
    scoreLabel: payload['scoreLabel'] as String?,
    scoreUnit: payload['scoreUnit'] as String?,
    defaultTargetTimeSeconds: payload['defaultTargetTimeSeconds'] as int?,
    defaultTargetReps: payload['defaultTargetReps'] as int?,
    defaultTargetScore: (payload['defaultTargetScore'] as num?)?.toDouble(),
    defaultTargetScoreTimeMs: payload['defaultTargetScoreTimeMs'] as int?,
    autoStartNextTimedStep: payload['autoStartNextTimedStep'] as bool? ?? true,
    category: _exerciseCategoryFromDb(
      payload['category'] as String? ?? 'uncategorized',
    ),
    isExample: payload['isExample'] as bool? ?? false,
    tags: _stringListFromPayload(payload['tags']),
    steps: _stepsFromPayload(payload['steps']),
    archivedAt: _dateTimeFromPayload(payload['archivedAt']),
  );
}

domain.MediaAsset _mediaAssetFromPayload(RemoteSyncedItem item) {
  final payload = item.payload;
  return domain.MediaAsset(
    metadata: _metadataFromPayload(item),
    kind: _mediaKindFromDb(payload['kind'] as String? ?? 'image'),
    localUri: _stringFromPayload(payload, 'localUri', ''),
    mimeType: payload['mimeType'] as String?,
    sizeBytes: payload['sizeBytes'] as int?,
    width: payload['width'] as int?,
    height: payload['height'] as int?,
    durationMs: payload['durationMs'] as int?,
    checksum: payload['checksum'] as String?,
    remoteUri: payload['remoteUri'] as String?,
    thumbnailLocalUri: payload['thumbnailLocalUri'] as String?,
  );
}

domain.Program _programFromPayload(RemoteSyncedItem item) {
  final payload = item.payload;
  final metadata = _metadataFromPayload(item);
  return domain.Program(
    metadata: metadata,
    name: _stringFromPayload(payload, 'name', item.clientId),
    defaultRestSeconds: payload['defaultRestSeconds'] as int? ?? 0,
    isExample: payload['isExample'] as bool? ?? false,
    tags: _stringListFromPayload(payload['tags']),
    exercises: _programExercisesFromPayload(payload['exercises'], metadata),
  );
}

domain.WorkoutTemplate _workoutTemplateFromPayload(RemoteSyncedItem item) {
  final payload = item.payload;
  final metadata = _metadataFromPayload(item);
  return domain.WorkoutTemplate(
    metadata: metadata,
    name: _stringFromPayload(payload, 'name', item.clientId),
    lastStartedAt: _dateTimeFromPayload(payload['lastStartedAt']),
    isExample: payload['isExample'] as bool? ?? false,
    tags: _stringListFromPayload(payload['tags']),
    programs: _workoutTemplateProgramsFromPayload(
      payload['programs'],
      metadata,
    ),
    overrides: _workoutTemplateOverridesFromPayload(
      payload['overrides'],
      metadata,
    ),
  );
}

domain.EntityMetadata _metadataFromPayload(RemoteSyncedItem item) {
  final metadata = item.payload['metadata'];
  final map = metadata is Map ? Map<String, Object?>.from(metadata) : null;
  return domain.EntityMetadata(
    id: item.clientId,
    createdAt: _dateTimeFromPayload(map?['createdAt']) ?? item.clientUpdatedAt,
    updatedAt: item.clientUpdatedAt,
    deletedAt: item.deletedAt,
    schemaVersion: item.schemaVersion,
    syncState: map?['syncState'] is String
        ? _syncStateFromDb(map!['syncState'] as String)
        : item.deletedAt == null
        ? domain.SyncState.synced
        : domain.SyncState.deleted,
    localRevision: map?['localRevision'] as int? ?? 0,
    originDeviceId: map?['originDeviceId'] as String? ?? 'remote',
  );
}

List<domain.ProgramExercise> _programExercisesFromPayload(
  Object? value,
  domain.EntityMetadata parentMetadata,
) {
  if (value is! List) {
    return const [];
  }
  return value
      .whereType<Map>()
      .map((entry) {
        final map = Map<String, Object?>.from(entry);
        final id = _stringFromPayload(map, 'id', 'program-exercise');
        return domain.ProgramExercise(
          metadata: _childMetadataFromPayload(map, id, parentMetadata),
          programId: parentMetadata.id,
          sourceExerciseId: map['sourceExerciseId'] as String?,
          position: map['position'] as int? ?? 0,
          exerciseNameSnapshot: _stringFromPayload(
            map,
            'exerciseNameSnapshot',
            id,
          ),
          exerciseDescriptionSnapshot:
              map['exerciseDescriptionSnapshot'] as String?,
          exerciseImageMediaIdSnapshot:
              map['exerciseImageMediaIdSnapshot'] as String?,
          exerciseImageMediaIdsSnapshot: _stringListFromPayload(
            map['exerciseImageMediaIdsSnapshot'] ??
                map['imageMediaIdsSnapshot'],
          ),
          exerciseVideoMediaIdSnapshot:
              map['exerciseVideoMediaIdSnapshot'] as String?,
          exerciseStepsSnapshot: _stepsFromPayload(
            map['exerciseStepsSnapshot'],
          ),
          autoStartNextTimedStepSnapshot:
              map['autoStartNextTimedStepSnapshot'] as bool? ?? true,
          autoStartNextTimedStepOverride:
              map['autoStartNextTimedStepOverride'] as bool?,
          exerciseArchivedSnapshot: map['exerciseArchivedSnapshot'] == true,
          availableTimeSnapshot: map['availableTimeSnapshot'] == true,
          availableRepsSnapshot: map['availableRepsSnapshot'] == true,
          availableScoreSnapshot: map['availableScoreSnapshot'] == true,
          scoreInputModeSnapshot: _scoreInputModeFromDb(
            map['scoreInputModeSnapshot'] as String? ?? 'manual',
          ),
          scoreLabelSnapshot: map['scoreLabelSnapshot'] as String?,
          scoreUnitSnapshot: map['scoreUnitSnapshot'] as String?,
          setsCount: map['setsCount'] as int? ?? 1,
          timeEnabled: map['timeEnabled'] == true,
          repsEnabled: map['repsEnabled'] == true,
          scoreEnabled: map['scoreEnabled'] == true,
          targetTimeSeconds: map['targetTimeSeconds'] as int?,
          targetReps: map['targetReps'] as int?,
          targetScore: (map['targetScore'] as num?)?.toDouble(),
          targetScoreTimeMs: map['targetScoreTimeMs'] as int?,
          restSecondsOverride: map['restSecondsOverride'] as int?,
        );
      })
      .toList(growable: false);
}

List<domain.WorkoutTemplateProgram> _workoutTemplateProgramsFromPayload(
  Object? value,
  domain.EntityMetadata parentMetadata,
) {
  if (value is! List) {
    return const [];
  }
  return value
      .whereType<Map>()
      .map((entry) {
        final map = Map<String, Object?>.from(entry);
        final id = _stringFromPayload(map, 'id', 'template-program');
        return domain.WorkoutTemplateProgram(
          metadata: _childMetadataFromPayload(map, id, parentMetadata),
          workoutTemplateId: parentMetadata.id,
          sourceProgramId: map['sourceProgramId'] as String?,
          position: map['position'] as int? ?? 0,
          programNameSnapshot: _stringFromPayload(
            map,
            'programNameSnapshot',
            id,
          ),
          defaultRestSecondsSnapshot:
              map['defaultRestSecondsSnapshot'] as int? ?? 0,
          programSnapshotJson:
              map['programSnapshotJson'] as String? ?? '{"exercises":[]}',
        );
      })
      .toList(growable: false);
}

List<domain.WorkoutTemplateExerciseOverride>
_workoutTemplateOverridesFromPayload(
  Object? value,
  domain.EntityMetadata parentMetadata,
) {
  if (value is! List) {
    return const [];
  }
  return value
      .whereType<Map>()
      .map((entry) {
        final map = Map<String, Object?>.from(entry);
        final id = _stringFromPayload(map, 'id', 'template-override');
        return domain.WorkoutTemplateExerciseOverride(
          metadata: _childMetadataFromPayload(map, id, parentMetadata),
          workoutTemplateProgramId: _stringFromPayload(
            map,
            'workoutTemplateProgramId',
            '',
          ),
          snapshotProgramExerciseId: _stringFromPayload(
            map,
            'snapshotProgramExerciseId',
            '',
          ),
          setsCountOverride: map['setsCountOverride'] as int?,
          targetTimeSecondsOverride: map['targetTimeSecondsOverride'] as int?,
          targetRepsOverride: map['targetRepsOverride'] as int?,
          targetScoreOverride: (map['targetScoreOverride'] as num?)?.toDouble(),
          targetScoreTimeMsOverride: map['targetScoreTimeMsOverride'] as int?,
          autoStartNextTimedStepOverride:
              map['autoStartNextTimedStepOverride'] as bool?,
        );
      })
      .toList(growable: false);
}

List<domain.WorkoutHistorySetResult> _workoutHistorySetResultsFromPayload(
  Object? value,
  domain.EntityMetadata parentMetadata,
) {
  if (value is! List) {
    return const [];
  }
  return value
      .whereType<Map>()
      .map((entry) {
        final map = Map<String, Object?>.from(entry);
        final id = _stringFromPayload(map, 'id', 'history-set-result');
        return domain.WorkoutHistorySetResult(
          metadata: _childMetadataFromPayload(map, id, parentMetadata),
          workoutHistoryId: parentMetadata.id,
          programSnapshotId: _stringFromPayload(map, 'programSnapshotId', ''),
          exerciseSnapshotId: _stringFromPayload(map, 'exerciseSnapshotId', ''),
          programIndex: map['programIndex'] as int? ?? 0,
          exerciseIndex: map['exerciseIndex'] as int? ?? 0,
          setIndex: map['setIndex'] as int? ?? 0,
          programNameSnapshot: _stringFromPayload(
            map,
            'programNameSnapshot',
            '',
          ),
          exerciseNameSnapshot: _stringFromPayload(
            map,
            'exerciseNameSnapshot',
            '',
          ),
          timeEnabledSnapshot: map['timeEnabledSnapshot'] == true,
          repsEnabledSnapshot: map['repsEnabledSnapshot'] == true,
          scoreEnabledSnapshot: map['scoreEnabledSnapshot'] == true,
          scoreInputModeSnapshot: _scoreInputModeFromDb(
            map['scoreInputModeSnapshot'] as String? ?? 'manual',
          ),
          targetTimeSecondsSnapshot: map['targetTimeSecondsSnapshot'] as int?,
          targetRepsSnapshot: map['targetRepsSnapshot'] as int?,
          targetScoreSnapshot: (map['targetScoreSnapshot'] as num?)?.toDouble(),
          targetScoreTimeMsSnapshot: map['targetScoreTimeMsSnapshot'] as int?,
          actualTimeMs: map['actualTimeMs'] as int?,
          actualReps: map['actualReps'] as int?,
          actualScore: (map['actualScore'] as num?)?.toDouble(),
          actualScoreTimeMs: map['actualScoreTimeMs'] as int?,
          scoreLabelSnapshot: map['scoreLabelSnapshot'] as String?,
          scoreUnitSnapshot: map['scoreUnitSnapshot'] as String?,
          sourceExerciseIdSnapshot: map['sourceExerciseIdSnapshot'] as String?,
          startedAt: _dateTimeFromPayload(map['startedAt']),
          completedAt: _dateTimeFromPayload(map['completedAt']),
          status: _setResultStatusFromDb(
            map['status'] as String? ?? 'completed',
          ),
        );
      })
      .toList(growable: false);
}

List<domain.WorkoutHistoryStepResult> _workoutHistoryStepResultsFromPayload(
  Object? value,
  domain.EntityMetadata parentMetadata,
) {
  if (value is! List) {
    return const [];
  }
  return value
      .whereType<Map>()
      .map((entry) {
        final map = Map<String, Object?>.from(entry);
        final id = _stringFromPayload(map, 'id', 'history-step-result');
        return domain.WorkoutHistoryStepResult(
          metadata: _childMetadataFromPayload(map, id, parentMetadata),
          workoutHistoryId: parentMetadata.id,
          programSnapshotId: _stringFromPayload(map, 'programSnapshotId', ''),
          exerciseSnapshotId: _stringFromPayload(map, 'exerciseSnapshotId', ''),
          programIndex: map['programIndex'] as int? ?? 0,
          exerciseIndex: map['exerciseIndex'] as int? ?? 0,
          setIndex: map['setIndex'] as int? ?? 0,
          passageIndex: map['passageIndex'] as int? ?? 0,
          stepIndex: map['stepIndex'] as int? ?? 0,
          stepSnapshotId: _stringFromPayload(map, 'stepSnapshotId', ''),
          stepNameSnapshot: _stringFromPayload(map, 'stepNameSnapshot', ''),
          stepTypeSnapshot: _exerciseStepTypeFromDb(
            map['stepTypeSnapshot'] as String? ?? 'reps',
          ),
          targetValueSnapshot: map['targetValueSnapshot'] as int? ?? 0,
          hasScoreSnapshot: map['hasScoreSnapshot'] == true,
          scoreInputModeSnapshot: map['scoreInputModeSnapshot'] == null
              ? null
              : _scoreInputModeFromDb(map['scoreInputModeSnapshot'] as String),
          scoreLabelSnapshot: map['scoreLabelSnapshot'] as String?,
          scoreUnitSnapshot: map['scoreUnitSnapshot'] as String?,
          targetScoreSnapshot: (map['targetScoreSnapshot'] as num?)?.toDouble(),
          targetScoreTimeMsSnapshot: map['targetScoreTimeMsSnapshot'] as int?,
          status: _setResultStatusFromDb(
            map['status'] as String? ?? 'completed',
          ),
          startedAt: _dateTimeFromPayload(map['startedAt']),
          completedAt: _dateTimeFromPayload(map['completedAt']),
          actualTimeMs: map['actualTimeMs'] as int?,
          actualReps: map['actualReps'] as int?,
          actualScore: (map['actualScore'] as num?)?.toDouble(),
          actualScoreTimeMs: map['actualScoreTimeMs'] as int?,
          note: map['note'] as String?,
          sourceExerciseIdSnapshot: map['sourceExerciseIdSnapshot'] as String?,
        );
      })
      .toList(growable: false);
}

domain.EntityMetadata _childMetadataFromPayload(
  Map<String, Object?> payload,
  String id,
  domain.EntityMetadata parentMetadata,
) {
  final metadata = payload['metadata'];
  final map = metadata is Map ? Map<String, Object?>.from(metadata) : null;
  return domain.EntityMetadata(
    id: id,
    createdAt:
        _dateTimeFromPayload(map?['createdAt']) ?? parentMetadata.createdAt,
    updatedAt:
        _dateTimeFromPayload(map?['updatedAt']) ?? parentMetadata.updatedAt,
    deletedAt: _dateTimeFromPayload(map?['deletedAt']),
    schemaVersion:
        map?['schemaVersion'] as int? ?? parentMetadata.schemaVersion,
    syncState: parentMetadata.syncState,
    localRevision: map?['localRevision'] as int? ?? 0,
    originDeviceId:
        map?['originDeviceId'] as String? ?? parentMetadata.originDeviceId,
  );
}

String _stringFromPayload(
  Map<String, Object?> payload,
  String key,
  String fallback,
) {
  final value = payload[key];
  return value is String && value.trim().isNotEmpty ? value : fallback;
}

List<String> _stringListFromPayload(Object? value) {
  if (value is! List) {
    return const [];
  }
  return value.whereType<String>().toList(growable: false);
}

String _encodeTags(List<String> tags) => jsonEncode(domain.normalizeTags(tags));

List<String> _decodeTags(String? encoded) {
  if (encoded == null || encoded.trim().isEmpty) {
    return const [];
  }
  try {
    final decoded = jsonDecode(encoded);
    if (decoded is! List) {
      return const [];
    }
    return domain.normalizeTags(decoded.whereType<String>());
  } on Object {
    return const [];
  }
}

List<domain.ExerciseStep> _stepsFromPayload(Object? value) {
  if (value is! List) {
    return const [];
  }
  return value
      .map((raw) {
        final json = Map<String, Object?>.from(raw as Map);
        return domain.ExerciseStep(
          id: _requiredString(json, 'id'),
          position: _requiredInt(json, 'position'),
          name: _requiredString(json, 'name'),
          type: _exerciseStepTypeFromDb(_requiredString(json, 'type')),
          defaultTargetValue: _requiredInt(json, 'defaultTargetValue'),
          hasScore: _requiredBool(json, 'hasScore'),
          scoreInputMode: _scoreInputModeFromDb(
            _optionalString(json, 'scoreInputMode') ?? 'manual',
          ),
          scoreLabel: _optionalString(json, 'scoreLabel'),
          scoreUnit: _optionalString(json, 'scoreUnit'),
          defaultTargetScore: _optionalDouble(json, 'defaultTargetScore'),
          defaultTargetScoreTimeMs: _optionalInt(
            json,
            'defaultTargetScoreTimeMs',
          ),
        );
      })
      .toList(growable: false);
}

DateTime? _dateTimeFromPayload(Object? value) {
  return value is String ? DateTime.tryParse(value)?.toUtc() : null;
}

const _syncableEntityTypes = [
  'Exercise',
  'Program',
  'WorkoutTemplate',
  'WorkoutHistory',
  'MediaAsset',
];

SyncResourceType? _syncResourceTypeFromEntityType(String entityType) {
  return switch (entityType) {
    'Exercise' => SyncResourceType.exercise,
    'Program' => SyncResourceType.program,
    'WorkoutTemplate' => SyncResourceType.workoutTemplate,
    'WorkoutHistory' => SyncResourceType.workoutHistory,
    'MediaAsset' => SyncResourceType.mediaAsset,
    _ => null,
  };
}

String _tableNameForResourceType(SyncResourceType type) => switch (type) {
  SyncResourceType.exercise => 'exercises',
  SyncResourceType.program => 'programs',
  SyncResourceType.workoutTemplate => 'workout_templates',
  SyncResourceType.workoutHistory => 'workout_history',
  SyncResourceType.mediaAsset => 'media_assets',
};

String _syncStateToDb(domain.SyncState state) => switch (state) {
  domain.SyncState.localOnly => 'localOnly',
  domain.SyncState.dirty => 'dirty',
  domain.SyncState.synced => 'synced',
  domain.SyncState.deleted => 'deleted',
};

domain.SyncState _syncStateFromDb(String value) => switch (value) {
  'localOnly' => domain.SyncState.localOnly,
  'dirty' => domain.SyncState.dirty,
  'synced' => domain.SyncState.synced,
  'deleted' => domain.SyncState.deleted,
  _ => throw domain.DomainException('Unknown sync state: $value'),
};

String _syncResourceTypeToDb(SyncResourceType type) => switch (type) {
  SyncResourceType.exercise => 'exercise',
  SyncResourceType.program => 'program',
  SyncResourceType.workoutTemplate => 'workoutTemplate',
  SyncResourceType.workoutHistory => 'workoutHistory',
  SyncResourceType.mediaAsset => 'mediaAsset',
};

SyncResourceType _syncResourceTypeFromDb(String value) => switch (value) {
  'exercise' => SyncResourceType.exercise,
  'program' => SyncResourceType.program,
  'workoutTemplate' => SyncResourceType.workoutTemplate,
  'workoutHistory' => SyncResourceType.workoutHistory,
  'mediaAsset' => SyncResourceType.mediaAsset,
  _ => throw domain.DomainException('Unknown sync resource type: $value'),
};

String _onlineSyncStatusToDb(OnlineSyncStatus status) => switch (status) {
  OnlineSyncStatus.idle => 'idle',
  OnlineSyncStatus.syncing => 'syncing',
  OnlineSyncStatus.success => 'success',
  OnlineSyncStatus.failure => 'failure',
};

OnlineSyncStatus _onlineSyncStatusFromDb(String value) => switch (value) {
  'idle' => OnlineSyncStatus.idle,
  'syncing' => OnlineSyncStatus.syncing,
  'success' => OnlineSyncStatus.success,
  'failure' => OnlineSyncStatus.failure,
  _ => throw domain.DomainException('Unknown sync status: $value'),
};

String _shareResourceTypeToDb(domain.ShareResourceType type) => switch (type) {
  domain.ShareResourceType.program => 'program',
  domain.ShareResourceType.workoutTemplate => 'workoutTemplate',
};

domain.ShareResourceType _shareResourceTypeFromDb(String value) =>
    switch (value) {
      'program' => domain.ShareResourceType.program,
      'workoutTemplate' => domain.ShareResourceType.workoutTemplate,
      _ => throw domain.DomainException('Unknown share resource type: $value'),
    };

String _shareInboxStatusToDb(domain.ShareInboxStatus status) =>
    switch (status) {
      domain.ShareInboxStatus.pending => 'pending',
      domain.ShareInboxStatus.accepted => 'accepted',
      domain.ShareInboxStatus.declined => 'declined',
      domain.ShareInboxStatus.revoked => 'revoked',
    };

domain.ShareInboxStatus _shareInboxStatusFromDb(String value) =>
    switch (value) {
      'pending' => domain.ShareInboxStatus.pending,
      'accepted' => domain.ShareInboxStatus.accepted,
      'declined' => domain.ShareInboxStatus.declined,
      'revoked' => domain.ShareInboxStatus.revoked,
      _ => throw domain.DomainException('Unknown share inbox status: $value'),
    };

String _pendingShareActionTypeToDb(domain.PendingShareActionType type) =>
    switch (type) {
      domain.PendingShareActionType.send => 'send',
      domain.PendingShareActionType.accept => 'accept',
      domain.PendingShareActionType.decline => 'decline',
      domain.PendingShareActionType.revoke => 'revoke',
    };

domain.PendingShareActionType _pendingShareActionTypeFromDb(String value) =>
    switch (value) {
      'send' => domain.PendingShareActionType.send,
      'accept' => domain.PendingShareActionType.accept,
      'decline' => domain.PendingShareActionType.decline,
      'revoke' => domain.PendingShareActionType.revoke,
      _ => throw domain.DomainException('Unknown share action type: $value'),
    };

String _pendingShareActionStatusToDb(domain.PendingShareActionStatus status) =>
    switch (status) {
      domain.PendingShareActionStatus.pending => 'pending',
      domain.PendingShareActionStatus.succeeded => 'succeeded',
      domain.PendingShareActionStatus.failed => 'failed',
    };

domain.PendingShareActionStatus _pendingShareActionStatusFromDb(String value) =>
    switch (value) {
      'pending' => domain.PendingShareActionStatus.pending,
      'succeeded' => domain.PendingShareActionStatus.succeeded,
      'failed' => domain.PendingShareActionStatus.failed,
      _ => throw domain.DomainException('Unknown share action status: $value'),
    };

String _activeStatusToDb(domain.ActiveWorkoutStatus status) => switch (status) {
  domain.ActiveWorkoutStatus.running => 'running',
  domain.ActiveWorkoutStatus.paused => 'paused',
  domain.ActiveWorkoutStatus.savedExit => 'savedExit',
  domain.ActiveWorkoutStatus.completed => 'completed',
  domain.ActiveWorkoutStatus.abandoned => 'abandoned',
};

String _mediaKindToDb(domain.MediaKind kind) => switch (kind) {
  domain.MediaKind.image => 'image',
  domain.MediaKind.video => 'video',
};

domain.MediaKind _mediaKindFromDb(String value) => switch (value) {
  'image' => domain.MediaKind.image,
  'video' => domain.MediaKind.video,
  _ => throw domain.DomainException('Unknown media kind: $value'),
};

String _scoreInputModeToDb(domain.ScoreInputMode mode) => switch (mode) {
  domain.ScoreInputMode.manual => 'manual',
  domain.ScoreInputMode.stopwatch => 'stopwatch',
};

domain.ScoreInputMode _scoreInputModeFromDb(String value) => switch (value) {
  'manual' => domain.ScoreInputMode.manual,
  'stopwatch' => domain.ScoreInputMode.stopwatch,
  _ => throw domain.DomainException('Unknown score input mode: $value'),
};

String _exerciseStepTypeToDb(domain.ExerciseStepType type) => switch (type) {
  domain.ExerciseStepType.time => 'time',
  domain.ExerciseStepType.reps => 'reps',
};

domain.ExerciseStepType _exerciseStepTypeFromDb(String value) =>
    switch (value) {
      'time' => domain.ExerciseStepType.time,
      'reps' => domain.ExerciseStepType.reps,
      _ => throw domain.DomainException('Unknown exercise step type: $value'),
    };

String _exerciseCategoryToDb(domain.ExerciseCategory category) =>
    switch (category) {
      domain.ExerciseCategory.shoot => 'shoot',
      domain.ExerciseCategory.freeThrows => 'freeThrows',
      domain.ExerciseCategory.dribble => 'dribble',
      domain.ExerciseCategory.finishing => 'finishing',
      domain.ExerciseCategory.conditioning => 'conditioning',
      domain.ExerciseCategory.defense => 'defense',
      domain.ExerciseCategory.mobility => 'mobility',
      domain.ExerciseCategory.uncategorized => 'uncategorized',
    };

domain.ExerciseCategory _exerciseCategoryFromDb(String value) =>
    switch (value) {
      'shoot' => domain.ExerciseCategory.shoot,
      'freeThrows' => domain.ExerciseCategory.freeThrows,
      'dribble' => domain.ExerciseCategory.dribble,
      'finishing' => domain.ExerciseCategory.finishing,
      'conditioning' => domain.ExerciseCategory.conditioning,
      'defense' => domain.ExerciseCategory.defense,
      'mobility' => domain.ExerciseCategory.mobility,
      'uncategorized' => domain.ExerciseCategory.uncategorized,
      _ => throw domain.DomainException('Unknown exercise category: $value'),
    };

String _setResultStatusToDb(domain.SetResultStatus status) => switch (status) {
  domain.SetResultStatus.completed => 'completed',
  domain.SetResultStatus.skipped => 'skipped',
};

domain.SetResultStatus _setResultStatusFromDb(String value) => switch (value) {
  'completed' => domain.SetResultStatus.completed,
  'skipped' => domain.SetResultStatus.skipped,
  _ => throw domain.DomainException('Unknown set result status: $value'),
};

String _scoreStopwatchStatusToDb(domain.ActiveScoreStopwatchStatus status) =>
    switch (status) {
      domain.ActiveScoreStopwatchStatus.running => 'running',
      domain.ActiveScoreStopwatchStatus.paused => 'paused',
      domain.ActiveScoreStopwatchStatus.stopped => 'stopped',
    };

domain.ActiveScoreStopwatchStatus _scoreStopwatchStatusFromDb(String value) =>
    switch (value) {
      'running' => domain.ActiveScoreStopwatchStatus.running,
      'paused' => domain.ActiveScoreStopwatchStatus.paused,
      'stopped' => domain.ActiveScoreStopwatchStatus.stopped,
      _ => throw domain.DomainException(
        'Unknown active score stopwatch status: $value',
      ),
    };

String _setTimerStatusToDb(domain.ActiveSetTimerStatus status) =>
    switch (status) {
      domain.ActiveSetTimerStatus.running => 'running',
      domain.ActiveSetTimerStatus.paused => 'paused',
      domain.ActiveSetTimerStatus.stopped => 'stopped',
      domain.ActiveSetTimerStatus.skipped => 'skipped',
    };

domain.ActiveSetTimerStatus _setTimerStatusFromDb(String value) =>
    switch (value) {
      'running' => domain.ActiveSetTimerStatus.running,
      'paused' => domain.ActiveSetTimerStatus.paused,
      'stopped' => domain.ActiveSetTimerStatus.stopped,
      'skipped' => domain.ActiveSetTimerStatus.skipped,
      _ => throw domain.DomainException(
        'Unknown active set timer status: $value',
      ),
    };

String _exerciseStepProgressStatusToDb(
  domain.ActiveExerciseStepProgressStatus status,
) => switch (status) {
  domain.ActiveExerciseStepProgressStatus.notStarted => 'notStarted',
  domain.ActiveExerciseStepProgressStatus.waitingManual => 'waitingManual',
  domain.ActiveExerciseStepProgressStatus.runningTimer => 'runningTimer',
  domain.ActiveExerciseStepProgressStatus.pausedTimer => 'pausedTimer',
  domain.ActiveExerciseStepProgressStatus.stoppedTimer => 'stoppedTimer',
  domain.ActiveExerciseStepProgressStatus.sequenceComplete =>
    'sequenceComplete',
};

domain.ActiveExerciseStepProgressStatus _exerciseStepProgressStatusFromDb(
  String value,
) => switch (value) {
  'notStarted' => domain.ActiveExerciseStepProgressStatus.notStarted,
  'waitingManual' => domain.ActiveExerciseStepProgressStatus.waitingManual,
  'runningTimer' => domain.ActiveExerciseStepProgressStatus.runningTimer,
  'pausedTimer' => domain.ActiveExerciseStepProgressStatus.pausedTimer,
  'stoppedTimer' => domain.ActiveExerciseStepProgressStatus.stoppedTimer,
  'sequenceComplete' =>
    domain.ActiveExerciseStepProgressStatus.sequenceComplete,
  _ => throw domain.DomainException(
    'Unknown active exercise step progress status: $value',
  ),
};

String? _encodeImageMediaIdsSnapshot(List<String> imageMediaIds) {
  return imageMediaIds.isEmpty ? null : jsonEncode(imageMediaIds);
}

List<String> _decodeImageMediaIdsSnapshot(
  String? encoded,
  String? fallbackImageMediaId,
) {
  if (encoded == null || encoded.trim().isEmpty) {
    return [?fallbackImageMediaId];
  }
  final decoded = jsonDecode(encoded);
  if (decoded is! List) {
    throw const domain.DomainException(
      'Invalid exercise image media ids snapshot.',
    );
  }
  return decoded
      .whereType<String>()
      .where((id) => id.trim().isNotEmpty)
      .toList(growable: false);
}

String? _encodeExerciseStepsSnapshot(List<domain.ExerciseStep> steps) {
  return steps.isEmpty
      ? null
      : jsonEncode(steps.map((step) => step.toSnapshotJson()).toList());
}

List<domain.ExerciseStep> _decodeExerciseStepsSnapshot(String? encoded) {
  if (encoded == null || encoded.trim().isEmpty) {
    return const [];
  }
  final decoded = jsonDecode(encoded);
  if (decoded is! List) {
    throw const domain.DomainException('Invalid exercise steps snapshot.');
  }
  return decoded
      .map((item) {
        if (item is! Map) {
          throw const domain.DomainException('Invalid exercise step snapshot.');
        }
        final json = Map<String, Object?>.from(item);
        return domain.ExerciseStep(
          id: _requiredString(json, 'id'),
          position: _requiredInt(json, 'position'),
          name: _requiredString(json, 'name'),
          type: _exerciseStepTypeFromDb(_requiredString(json, 'type')),
          defaultTargetValue: _requiredInt(json, 'defaultTargetValue'),
          hasScore: _requiredBool(json, 'hasScore'),
          scoreInputMode: _scoreInputModeFromDb(
            _optionalString(json, 'scoreInputMode') ?? 'manual',
          ),
          scoreLabel: _optionalString(json, 'scoreLabel'),
          scoreUnit: _optionalString(json, 'scoreUnit'),
          defaultTargetScore: _optionalDouble(json, 'defaultTargetScore'),
          defaultTargetScoreTimeMs: _optionalInt(
            json,
            'defaultTargetScoreTimeMs',
          ),
        );
      })
      .toList(growable: false);
}

String _requiredString(Map<String, Object?> json, String key) {
  final value = json[key];
  if (value is String && value.trim().isNotEmpty) {
    return value;
  }
  throw domain.DomainException('$key must be a non-empty string.');
}

String? _optionalString(Map<String, Object?> json, String key) {
  final value = json[key];
  if (value == null) {
    return null;
  }
  return value is String ? value : null;
}

int _requiredInt(Map<String, Object?> json, String key) {
  final value = json[key];
  if (value is int) {
    return value;
  }
  throw domain.DomainException('$key must be an integer.');
}

int? _optionalInt(Map<String, Object?> json, String key) {
  final value = json[key];
  return value is int ? value : null;
}

double? _optionalDouble(Map<String, Object?> json, String key) {
  final value = json[key];
  return (value as num?)?.toDouble();
}

bool _requiredBool(Map<String, Object?> json, String key) {
  final value = json[key];
  if (value is bool) {
    return value;
  }
  throw domain.DomainException('$key must be a boolean.');
}

domain.ActiveWorkoutStatus _activeStatusFromDb(String value) => switch (value) {
  'running' => domain.ActiveWorkoutStatus.running,
  'paused' => domain.ActiveWorkoutStatus.paused,
  'savedExit' => domain.ActiveWorkoutStatus.savedExit,
  'completed' => domain.ActiveWorkoutStatus.completed,
  'abandoned' => domain.ActiveWorkoutStatus.abandoned,
  _ => throw domain.DomainException('Unknown active workout status: $value'),
};
