// Adapter-neutral V1 document mapping helpers.
//
// These helpers use plain Dart map shapes only. They do not import database
// APIs, create clients, or perform database I/O.

import 'application_layer.dart';
import 'backlog.dart';
import 'locked_time.dart';
import 'models.dart';
import 'persistence_contract.dart';
import 'project_statistics.dart';
import 'repositories.dart';
import 'scheduling_engine.dart';
import 'task_lifecycle.dart';
import 'task_statistics.dart';
import 'time_contracts.dart';

/// Stable document mapping failure categories.
enum DocumentMappingFailureCode {
  missingField,
  wrongType,
  unknownCode,
  invalidSchemaVersion,
  invalidRevision,
  invalidValue,
}

/// Typed mapping failure for malformed V1 documents.
class DocumentMappingException implements Exception {
  const DocumentMappingException({
    required this.code,
    this.fieldName,
    this.detailCode,
  });

  /// Stable category for caller branching.
  final DocumentMappingFailureCode code;

  /// Field related to the failure, when known.
  final String? fieldName;

  /// Optional narrower reason.
  final String? detailCode;

  @override
  String toString() {
    return 'DocumentMappingException($code, field: $fieldName, detail: '
        '$detailCode)';
  }
}

/// Common metadata decoded from a V1 top-level document.
class DocumentMetadata {
  const DocumentMetadata({
    required this.id,
    required this.ownerId,
    required this.revision,
    required this.createdAt,
    required this.updatedAt,
  });

  final String id;
  final String ownerId;
  final int revision;
  final DateTime createdAt;
  final DateTime updatedAt;
}

/// Stable enum encode/decode helpers for V1 document maps.
abstract final class PersistenceEnumMapping {
  static String? encodeNullable(Enum? value) {
    return value == null ? null : encode(value);
  }

  static String encode(Enum value) {
    if (value is TaskType) return encodeTaskType(value);
    if (value is TaskStatus) return encodeTaskStatus(value);
    if (value is PriorityLevel) return encodePriority(value);
    if (value is RewardLevel) return encodeReward(value);
    if (value is DifficultyLevel) return encodeDifficulty(value);
    if (value is ReminderProfile) return encodeReminderProfile(value);
    if (value is BacklogTag) return encodeBacklogTag(value);
    if (value is LockedWeekday) return encodeLockedWeekday(value);
    if (value is LockedBlockOverrideType) {
      return encodeLockedBlockOverrideType(value);
    }
    if (value is TaskActivityCode) return encodeTaskActivityCode(value);
    if (value is SchedulingNoticeType) return encodeSchedulingNoticeType(value);
    if (value is SchedulingIssueCode) return encodeSchedulingIssueCode(value);
    if (value is SchedulingMovementCode) {
      return encodeSchedulingMovementCode(value);
    }
    if (value is SchedulingConflictCode) {
      return encodeSchedulingConflictCode(value);
    }
    if (value is ProjectCompletionTimeBucket) {
      return encodeProjectCompletionTimeBucket(value);
    }
    throw DocumentMappingException(
      code: DocumentMappingFailureCode.unknownCode,
      detailCode: value.runtimeType.toString(),
    );
  }

  static String encodeTaskType(TaskType value) => switch (value) {
        TaskType.flexible => 'flexible',
        TaskType.inflexible => 'inflexible',
        TaskType.critical => 'critical',
        TaskType.locked => 'locked',
        TaskType.surprise => 'surprise',
        TaskType.freeSlot => 'free_slot',
      };

  static TaskType decodeTaskType(String value) {
    return _decodeCode(_taskTypeByCode, value, 'TaskType');
  }

  static String encodeTaskStatus(TaskStatus value) => switch (value) {
        TaskStatus.planned => 'planned',
        TaskStatus.active => 'active',
        TaskStatus.completed => 'completed',
        TaskStatus.missed => 'missed',
        TaskStatus.cancelled => 'cancelled',
        TaskStatus.noLongerRelevant => 'no_longer_relevant',
        TaskStatus.backlog => 'backlog',
      };

  static TaskStatus decodeTaskStatus(String value) {
    return _decodeCode(_taskStatusByCode, value, 'TaskStatus');
  }

  static String encodePriority(PriorityLevel value) => switch (value) {
        PriorityLevel.veryLow => 'very_low',
        PriorityLevel.low => 'low',
        PriorityLevel.medium => 'medium',
        PriorityLevel.high => 'high',
        PriorityLevel.veryHigh => 'very_high',
      };

  static PriorityLevel? decodeNullablePriority(String? value) {
    return value == null
        ? null
        : _decodeCode(_priorityByCode, value, 'PriorityLevel');
  }

  static String encodeReward(RewardLevel value) => switch (value) {
        RewardLevel.notSet => 'not_set',
        RewardLevel.veryLow => 'very_low',
        RewardLevel.low => 'low',
        RewardLevel.medium => 'medium',
        RewardLevel.high => 'high',
        RewardLevel.veryHigh => 'very_high',
      };

  static RewardLevel decodeReward(String value) {
    return _decodeCode(_rewardByCode, value, 'RewardLevel');
  }

  static String encodeDifficulty(DifficultyLevel value) => switch (value) {
        DifficultyLevel.notSet => 'not_set',
        DifficultyLevel.veryEasy => 'very_easy',
        DifficultyLevel.easy => 'easy',
        DifficultyLevel.medium => 'medium',
        DifficultyLevel.hard => 'hard',
        DifficultyLevel.veryHard => 'very_hard',
      };

  static DifficultyLevel decodeDifficulty(String value) {
    return _decodeCode(_difficultyByCode, value, 'DifficultyLevel');
  }

  static String encodeReminderProfile(ReminderProfile value) => switch (value) {
        ReminderProfile.silent => 'silent',
        ReminderProfile.gentle => 'gentle',
        ReminderProfile.persistent => 'persistent',
        ReminderProfile.strict => 'strict',
      };

  static ReminderProfile? decodeNullableReminderProfile(String? value) {
    return value == null
        ? null
        : _decodeCode(_reminderProfileByCode, value, 'ReminderProfile');
  }

  static String encodeBacklogTag(BacklogTag value) => switch (value) {
        BacklogTag.wishlist => 'wishlist',
      };

  static BacklogTag decodeBacklogTag(String value) {
    return _decodeCode(_backlogTagByCode, value, 'BacklogTag');
  }

  static String encodeLockedWeekday(LockedWeekday value) => switch (value) {
        LockedWeekday.monday => 'monday',
        LockedWeekday.tuesday => 'tuesday',
        LockedWeekday.wednesday => 'wednesday',
        LockedWeekday.thursday => 'thursday',
        LockedWeekday.friday => 'friday',
        LockedWeekday.saturday => 'saturday',
        LockedWeekday.sunday => 'sunday',
      };

  static LockedWeekday decodeLockedWeekday(String value) {
    return _decodeCode(_lockedWeekdayByCode, value, 'LockedWeekday');
  }

  static String encodeLockedBlockOverrideType(
    LockedBlockOverrideType value,
  ) =>
      switch (value) {
        LockedBlockOverrideType.remove => 'remove',
        LockedBlockOverrideType.replace => 'replace',
        LockedBlockOverrideType.add => 'add',
      };

  static LockedBlockOverrideType decodeLockedBlockOverrideType(String value) {
    return _decodeCode(
      _lockedBlockOverrideTypeByCode,
      value,
      'LockedBlockOverrideType',
    );
  }

  static String encodeTaskActivityCode(TaskActivityCode value) =>
      switch (value) {
        TaskActivityCode.completed => 'completed',
        TaskActivityCode.missed => 'missed',
        TaskActivityCode.cancelled => 'cancelled',
        TaskActivityCode.noLongerRelevant => 'no_longer_relevant',
        TaskActivityCode.manuallyPushed => 'manually_pushed',
        TaskActivityCode.automaticallyPushed => 'automatically_pushed',
        TaskActivityCode.movedToBacklog => 'moved_to_backlog',
        TaskActivityCode.restoredFromBacklog => 'restored_from_backlog',
        TaskActivityCode.activated => 'activated',
      };

  static TaskActivityCode decodeTaskActivityCode(String value) {
    return _decodeCode(_taskActivityCodeByCode, value, 'TaskActivityCode');
  }

  static String encodeSchedulingNoticeType(SchedulingNoticeType value) =>
      switch (value) {
        SchedulingNoticeType.info => 'info',
        SchedulingNoticeType.moved => 'moved',
        SchedulingNoticeType.overlap => 'overlap',
        SchedulingNoticeType.noFit => 'no_fit',
        SchedulingNoticeType.overflow => 'overflow',
      };

  static SchedulingNoticeType decodeSchedulingNoticeType(String value) {
    return _decodeCode(
      _schedulingNoticeTypeByCode,
      value,
      'SchedulingNoticeType',
    );
  }

  static String encodeSchedulingIssueCode(SchedulingIssueCode value) =>
      switch (value) {
        SchedulingIssueCode.taskNotFound => 'task_not_found',
        SchedulingIssueCode.invalidTaskState => 'invalid_task_state',
        SchedulingIssueCode.missingDuration => 'missing_duration',
        SchedulingIssueCode.missingScheduledSlot => 'missing_scheduled_slot',
        SchedulingIssueCode.nonPositiveDuration => 'non_positive_duration',
        SchedulingIssueCode.noAvailableSlot => 'no_available_slot',
        SchedulingIssueCode.unfinishedTasksCouldNotFit =>
          'unfinished_tasks_could_not_fit',
        SchedulingIssueCode.noUnfinishedFlexibleTasks =>
          'no_unfinished_flexible_tasks',
        SchedulingIssueCode.duplicateSurpriseLog => 'duplicate_surprise_log',
      };

  static SchedulingIssueCode? decodeNullableSchedulingIssueCode(String? value) {
    return value == null
        ? null
        : _decodeCode(
            _schedulingIssueCodeByCode,
            value,
            'SchedulingIssueCode',
          );
  }

  static String encodeSchedulingMovementCode(SchedulingMovementCode value) =>
      switch (value) {
        SchedulingMovementCode.backlogTaskInserted => 'backlog_task_inserted',
        SchedulingMovementCode.flexibleTaskMovedToMakeRoom =>
          'flexible_task_moved_to_make_room',
        SchedulingMovementCode.flexibleTaskPushedToNextAvailableSlot =>
          'flexible_task_pushed_to_next_available_slot',
        SchedulingMovementCode.flexibleTaskMovedToTomorrow =>
          'flexible_task_moved_to_tomorrow',
        SchedulingMovementCode.unfinishedFlexibleTasksRolledOver =>
          'unfinished_flexible_tasks_rolled_over',
        SchedulingMovementCode.flexibleTaskMovedToBacklog =>
          'flexible_task_moved_to_backlog',
        SchedulingMovementCode.requiredCommitmentScheduled =>
          'required_commitment_scheduled',
      };

  static SchedulingMovementCode? decodeNullableSchedulingMovementCode(
    String? value,
  ) {
    return value == null
        ? null
        : _decodeCode(
            _schedulingMovementCodeByCode,
            value,
            'SchedulingMovementCode',
          );
  }

  static String encodeSchedulingConflictCode(SchedulingConflictCode value) =>
      switch (value) {
        SchedulingConflictCode.flexibleTaskOverlapsBlockedTime =>
          'flexible_task_overlaps_blocked_time',
        SchedulingConflictCode.surpriseTaskOverlapsRequiredVisibleTime =>
          'surprise_task_overlaps_required_visible_time',
        SchedulingConflictCode.requiredCommitmentOverlapsProtectedFreeSlot =>
          'required_commitment_overlaps_protected_free_slot',
      };

  static SchedulingConflictCode? decodeNullableSchedulingConflictCode(
    String? value,
  ) {
    return value == null
        ? null
        : _decodeCode(
            _schedulingConflictCodeByCode,
            value,
            'SchedulingConflictCode',
          );
  }

  static String encodeProjectCompletionTimeBucket(
    ProjectCompletionTimeBucket value,
  ) =>
      switch (value) {
        ProjectCompletionTimeBucket.overnight => 'overnight',
        ProjectCompletionTimeBucket.morning => 'morning',
        ProjectCompletionTimeBucket.afternoon => 'afternoon',
        ProjectCompletionTimeBucket.evening => 'evening',
        ProjectCompletionTimeBucket.night => 'night',
      };

  static ProjectCompletionTimeBucket decodeProjectCompletionTimeBucket(
    String value,
  ) {
    return _decodeCode(
      _projectCompletionTimeBucketByCode,
      value,
      'ProjectCompletionTimeBucket',
    );
  }
}

/// Document mapping for [Task].
abstract final class TaskDocumentMapping {
  static Map<String, Object?> toDocument(
    Task task, {
    required String ownerId,
    int revision = 1,
    bool clearSchedule = false,
    DateTime? backlogEnteredAt,
    String? backlogEnteredAtProvenance,
  }) {
    final scheduledStart = clearSchedule ? null : task.scheduledStart;
    final scheduledEnd = clearSchedule ? null : task.scheduledEnd;

    return {
      ..._commonFields(
        id: task.id,
        ownerId: ownerId,
        revision: revision,
        createdAt: task.createdAt,
        updatedAt: task.updatedAt,
      ),
      TaskDocumentFields.title: task.title,
      TaskDocumentFields.projectId: task.projectId,
      TaskDocumentFields.type: PersistenceEnumMapping.encodeTaskType(task.type),
      TaskDocumentFields.status:
          PersistenceEnumMapping.encodeTaskStatus(task.status),
      TaskDocumentFields.priority: task.priority == null
          ? null
          : PersistenceEnumMapping.encodePriority(task.priority!),
      TaskDocumentFields.reward:
          PersistenceEnumMapping.encodeReward(task.reward),
      TaskDocumentFields.difficulty:
          PersistenceEnumMapping.encodeDifficulty(task.difficulty),
      TaskDocumentFields.durationMinutes: task.durationMinutes,
      TaskDocumentFields.scheduledStart: _dateTimeToStored(scheduledStart),
      TaskDocumentFields.scheduledEnd: _dateTimeToStored(scheduledEnd),
      TaskDocumentFields.actualStart: _dateTimeToStored(task.actualStart),
      TaskDocumentFields.actualEnd: _dateTimeToStored(task.actualEnd),
      TaskDocumentFields.completedAt: _dateTimeToStored(task.completedAt),
      TaskDocumentFields.parentTaskId: task.parentTaskId,
      TaskDocumentFields.backlogTags: task.backlogTags
          .map(PersistenceEnumMapping.encodeBacklogTag)
          .toList(),
      TaskDocumentFields.reminderOverride: task.reminderOverride == null
          ? null
          : PersistenceEnumMapping.encodeReminderProfile(
              task.reminderOverride!),
      TaskDocumentFields.stats: TaskStatisticsDocumentMapping.toDocument(
        task.stats,
      ),
      TaskDocumentFields.backlogEnteredAt: _dateTimeToStored(backlogEnteredAt),
      TaskDocumentFields.backlogEnteredAtProvenance: backlogEnteredAtProvenance,
    };
  }

  static Task fromDocument(Map<String, Object?> document) {
    return _mapInvalidValues(() {
      _readCommon(document);
      return Task(
        id: _requiredString(document, TaskDocumentFields.id),
        title: _requiredString(document, TaskDocumentFields.title),
        projectId: _requiredString(document, TaskDocumentFields.projectId),
        type: PersistenceEnumMapping.decodeTaskType(
          _requiredString(document, TaskDocumentFields.type),
        ),
        status: PersistenceEnumMapping.decodeTaskStatus(
          _requiredString(document, TaskDocumentFields.status),
        ),
        priority: PersistenceEnumMapping.decodeNullablePriority(
          _requiredNullableString(document, TaskDocumentFields.priority),
        ),
        reward: PersistenceEnumMapping.decodeReward(
          _requiredString(document, TaskDocumentFields.reward),
        ),
        difficulty: PersistenceEnumMapping.decodeDifficulty(
          _requiredString(document, TaskDocumentFields.difficulty),
        ),
        durationMinutes:
            _requiredNullableInt(document, TaskDocumentFields.durationMinutes),
        scheduledStart: _requiredNullableDateTime(
          document,
          TaskDocumentFields.scheduledStart,
        ),
        scheduledEnd: _requiredNullableDateTime(
          document,
          TaskDocumentFields.scheduledEnd,
        ),
        actualStart: _requiredNullableDateTime(
          document,
          TaskDocumentFields.actualStart,
        ),
        actualEnd: _requiredNullableDateTime(
          document,
          TaskDocumentFields.actualEnd,
        ),
        completedAt: _requiredNullableDateTime(
          document,
          TaskDocumentFields.completedAt,
        ),
        parentTaskId: _requiredNullableString(
          document,
          TaskDocumentFields.parentTaskId,
        ),
        backlogTags: _backlogTagsFromDocument(document),
        reminderOverride: PersistenceEnumMapping.decodeNullableReminderProfile(
          _requiredNullableString(
            document,
            TaskDocumentFields.reminderOverride,
          ),
        ),
        createdAt: _requiredDateTime(document, TaskDocumentFields.createdAt),
        updatedAt: _requiredDateTime(document, TaskDocumentFields.updatedAt),
        stats: TaskStatisticsDocumentMapping.fromDocument(
          _requiredMap(document, TaskDocumentFields.stats),
        ),
      ).._validateTaskDocumentMetadata(document);
    });
  }

  static Set<BacklogTag> _backlogTagsFromDocument(
    Map<String, Object?> document,
  ) {
    final rawTags = _requiredList(document, TaskDocumentFields.backlogTags);
    return rawTags.map((tag) {
      if (tag is! String) {
        throw DocumentMappingException(
          code: DocumentMappingFailureCode.wrongType,
          fieldName: TaskDocumentFields.backlogTags,
        );
      }
      return PersistenceEnumMapping.decodeBacklogTag(tag);
    }).toSet();
  }
}

extension on Task {
  void _validateTaskDocumentMetadata(Map<String, Object?> document) {
    _requiredNullableDateTime(document, TaskDocumentFields.backlogEnteredAt);
    _requiredNullableString(
      document,
      TaskDocumentFields.backlogEnteredAtProvenance,
    );
  }
}

/// Document mapping for [TaskStatistics].
abstract final class TaskStatisticsDocumentMapping {
  static Map<String, Object?> toDocument(TaskStatistics stats) {
    return {
      TaskStatisticsDocumentFields.skippedDuringBurnoutCount:
          stats.skippedDuringBurnoutCount,
      TaskStatisticsDocumentFields.manuallyPushedCount:
          stats.manuallyPushedCount,
      TaskStatisticsDocumentFields.autoPushedCount: stats.autoPushedCount,
      TaskStatisticsDocumentFields.movedToBacklogCount:
          stats.movedToBacklogCount,
      TaskStatisticsDocumentFields.restoredFromBacklogCount:
          stats.restoredFromBacklogCount,
      TaskStatisticsDocumentFields.missedCount: stats.missedCount,
      TaskStatisticsDocumentFields.cancelledCount: stats.cancelledCount,
      TaskStatisticsDocumentFields.completedLateCount: stats.completedLateCount,
      TaskStatisticsDocumentFields.completedDuringLockedHoursCount:
          stats.completedDuringLockedHoursCount,
      TaskStatisticsDocumentFields.completedDuringLockedHoursMinutes:
          stats.completedDuringLockedHoursMinutes,
      TaskStatisticsDocumentFields.completedAfterShieldCount:
          stats.completedAfterShieldCount,
      TaskStatisticsDocumentFields.completedAfterPushCount:
          stats.completedAfterPushCount,
      TaskStatisticsDocumentFields.totalPushesBeforeCompletion:
          stats.totalPushesBeforeCompletion,
    };
  }

  static TaskStatistics fromDocument(Map<String, Object?> document) {
    return _mapInvalidValues(() {
      return TaskStatistics(
        skippedDuringBurnoutCount: _requiredInt(
          document,
          TaskStatisticsDocumentFields.skippedDuringBurnoutCount,
        ),
        manuallyPushedCount: _requiredInt(
          document,
          TaskStatisticsDocumentFields.manuallyPushedCount,
        ),
        autoPushedCount: _requiredInt(
          document,
          TaskStatisticsDocumentFields.autoPushedCount,
        ),
        movedToBacklogCount: _requiredInt(
          document,
          TaskStatisticsDocumentFields.movedToBacklogCount,
        ),
        restoredFromBacklogCount: _requiredInt(
          document,
          TaskStatisticsDocumentFields.restoredFromBacklogCount,
        ),
        missedCount: _requiredInt(
          document,
          TaskStatisticsDocumentFields.missedCount,
        ),
        cancelledCount: _requiredInt(
          document,
          TaskStatisticsDocumentFields.cancelledCount,
        ),
        completedLateCount: _requiredInt(
          document,
          TaskStatisticsDocumentFields.completedLateCount,
        ),
        completedDuringLockedHoursCount: _requiredInt(
          document,
          TaskStatisticsDocumentFields.completedDuringLockedHoursCount,
        ),
        completedDuringLockedHoursMinutes: _requiredInt(
          document,
          TaskStatisticsDocumentFields.completedDuringLockedHoursMinutes,
        ),
        completedAfterShieldCount: _requiredInt(
          document,
          TaskStatisticsDocumentFields.completedAfterShieldCount,
        ),
        completedAfterPushCount: _requiredInt(
          document,
          TaskStatisticsDocumentFields.completedAfterPushCount,
        ),
        totalPushesBeforeCompletion: _requiredInt(
          document,
          TaskStatisticsDocumentFields.totalPushesBeforeCompletion,
        ),
      );
    });
  }
}

/// Document mapping for [ProjectProfile].
abstract final class ProjectDocumentMapping {
  static Map<String, Object?> toDocument(
    ProjectProfile project, {
    required String ownerId,
    required DateTime createdAt,
    required DateTime updatedAt,
    int revision = 1,
  }) {
    return {
      ..._commonFields(
        id: project.id,
        ownerId: ownerId,
        revision: revision,
        createdAt: createdAt,
        updatedAt: updatedAt,
      ),
      ProjectDocumentFields.name: project.name,
      ProjectDocumentFields.colorKey: project.colorKey,
      ProjectDocumentFields.defaultPriority:
          PersistenceEnumMapping.encodePriority(project.defaultPriority),
      ProjectDocumentFields.defaultReward:
          PersistenceEnumMapping.encodeReward(project.defaultReward),
      ProjectDocumentFields.defaultDifficulty:
          PersistenceEnumMapping.encodeDifficulty(project.defaultDifficulty),
      ProjectDocumentFields.defaultReminderProfile:
          PersistenceEnumMapping.encodeReminderProfile(
        project.defaultReminderProfile,
      ),
      ProjectDocumentFields.defaultDurationMinutes:
          project.defaultDurationMinutes,
      ProjectDocumentFields.archivedAt: _dateTimeToStored(project.archivedAt),
    };
  }

  static ProjectProfile fromDocument(Map<String, Object?> document) {
    return _mapInvalidValues(() {
      _readCommon(document);
      return ProjectProfile(
        id: _requiredString(document, ProjectDocumentFields.id),
        name: _requiredString(document, ProjectDocumentFields.name),
        colorKey: _requiredString(document, ProjectDocumentFields.colorKey),
        defaultPriority: PersistenceEnumMapping.decodeNullablePriority(
          _requiredString(document, ProjectDocumentFields.defaultPriority),
        )!,
        defaultReward: PersistenceEnumMapping.decodeReward(
          _requiredString(document, ProjectDocumentFields.defaultReward),
        ),
        defaultDifficulty: PersistenceEnumMapping.decodeDifficulty(
          _requiredString(document, ProjectDocumentFields.defaultDifficulty),
        ),
        defaultReminderProfile:
            PersistenceEnumMapping.decodeNullableReminderProfile(
          _requiredString(
            document,
            ProjectDocumentFields.defaultReminderProfile,
          ),
        )!,
        defaultDurationMinutes: _requiredNullableInt(
          document,
          ProjectDocumentFields.defaultDurationMinutes,
        ),
        archivedAt: _requiredNullableDateTime(
          document,
          ProjectDocumentFields.archivedAt,
        ),
      );
    });
  }
}

/// Document mapping for [ProjectStatistics].
abstract final class ProjectStatisticsDocumentMapping {
  static Map<String, Object?> toDocument(
    ProjectStatistics statistics, {
    required String ownerId,
    required DateTime createdAt,
    required DateTime updatedAt,
    int revision = 1,
    Iterable<String> appliedActivityIds = const <String>[],
  }) {
    return {
      ..._commonFields(
        id: statistics.projectId,
        ownerId: ownerId,
        revision: revision,
        createdAt: createdAt,
        updatedAt: updatedAt,
      ),
      ProjectStatisticsDocumentFields.projectId: statistics.projectId,
      ProjectStatisticsDocumentFields.completedTaskCount:
          statistics.completedTaskCount,
      ProjectStatisticsDocumentFields.durationMinuteCounts:
          _intKeyCountMapToDocument(statistics.durationMinuteCounts),
      ProjectStatisticsDocumentFields.completionTimeBucketCounts:
          _enumCountMapToDocument(
        statistics.completionTimeBucketCounts,
        PersistenceEnumMapping.encodeProjectCompletionTimeBucket,
      ),
      ProjectStatisticsDocumentFields.totalPushesBeforeCompletion:
          statistics.totalPushesBeforeCompletion,
      ProjectStatisticsDocumentFields.completedAfterPushCount:
          statistics.completedAfterPushCount,
      ProjectStatisticsDocumentFields.rewardCounts: _enumCountMapToDocument(
        statistics.rewardCounts,
        PersistenceEnumMapping.encodeReward,
      ),
      ProjectStatisticsDocumentFields.difficultyCounts: _enumCountMapToDocument(
        statistics.difficultyCounts,
        PersistenceEnumMapping.encodeDifficulty,
      ),
      ProjectStatisticsDocumentFields.reminderProfileCounts:
          _enumCountMapToDocument(
        statistics.reminderProfileCounts,
        PersistenceEnumMapping.encodeReminderProfile,
      ),
      ProjectStatisticsDocumentFields.appliedActivityIds:
          appliedActivityIds.toList(growable: false),
    };
  }

  static ProjectStatistics fromDocument(Map<String, Object?> document) {
    return _mapInvalidValues(() {
      _readCommon(document);
      return ProjectStatistics(
        projectId: _requiredString(
          document,
          ProjectStatisticsDocumentFields.projectId,
        ),
        completedTaskCount: _requiredInt(
          document,
          ProjectStatisticsDocumentFields.completedTaskCount,
        ),
        durationMinuteCounts: _intKeyCountMapFromDocument(
          document,
          ProjectStatisticsDocumentFields.durationMinuteCounts,
        ),
        completionTimeBucketCounts: _enumCountMapFromDocument(
          document,
          ProjectStatisticsDocumentFields.completionTimeBucketCounts,
          PersistenceEnumMapping.decodeProjectCompletionTimeBucket,
        ),
        totalPushesBeforeCompletion: _requiredInt(
          document,
          ProjectStatisticsDocumentFields.totalPushesBeforeCompletion,
        ),
        completedAfterPushCount: _requiredInt(
          document,
          ProjectStatisticsDocumentFields.completedAfterPushCount,
        ),
        rewardCounts: _enumCountMapFromDocument(
          document,
          ProjectStatisticsDocumentFields.rewardCounts,
          PersistenceEnumMapping.decodeReward,
        ),
        difficultyCounts: _enumCountMapFromDocument(
          document,
          ProjectStatisticsDocumentFields.difficultyCounts,
          PersistenceEnumMapping.decodeDifficulty,
        ),
        reminderProfileCounts: _enumCountMapFromDocument(
          document,
          ProjectStatisticsDocumentFields.reminderProfileCounts,
          (value) =>
              PersistenceEnumMapping.decodeNullableReminderProfile(value)!,
        ),
      ).._validateProjectStatisticsDocumentMetadata(document);
    });
  }
}

extension on ProjectStatistics {
  void _validateProjectStatisticsDocumentMetadata(
    Map<String, Object?> document,
  ) {
    for (final value in _requiredList(
      document,
      ProjectStatisticsDocumentFields.appliedActivityIds,
    )) {
      if (value is! String) {
        throw const DocumentMappingException(
          code: DocumentMappingFailureCode.wrongType,
          fieldName: ProjectStatisticsDocumentFields.appliedActivityIds,
        );
      }
    }
  }
}

/// Document mapping for [LockedBlockRecurrence].
abstract final class LockedBlockRecurrenceDocumentMapping {
  static Map<String, Object?> toDocument(LockedBlockRecurrence recurrence) {
    return {
      LockedBlockRecurrenceDocumentFields.type: 'weekly',
      LockedBlockRecurrenceDocumentFields.weekdays: recurrence.weekdays
          .map(PersistenceEnumMapping.encodeLockedWeekday)
          .toList(growable: false),
    };
  }

  static LockedBlockRecurrence fromDocument(Map<String, Object?> document) {
    return _mapInvalidValues(() {
      final type = _requiredString(
        document,
        LockedBlockRecurrenceDocumentFields.type,
      );
      if (type != 'weekly') {
        throw DocumentMappingException(
          code: DocumentMappingFailureCode.unknownCode,
          fieldName: LockedBlockRecurrenceDocumentFields.type,
          detailCode: type,
        );
      }
      final weekdays = _requiredList(
        document,
        LockedBlockRecurrenceDocumentFields.weekdays,
      ).map((value) {
        if (value is! String) {
          throw const DocumentMappingException(
            code: DocumentMappingFailureCode.wrongType,
            fieldName: LockedBlockRecurrenceDocumentFields.weekdays,
          );
        }
        return PersistenceEnumMapping.decodeLockedWeekday(value);
      }).toSet();
      return LockedBlockRecurrence.weekly(weekdays: weekdays);
    });
  }
}

/// Document mapping for [LockedBlock].
abstract final class LockedBlockDocumentMapping {
  static Map<String, Object?> toDocument(
    LockedBlock block, {
    required String ownerId,
    int revision = 1,
  }) {
    return {
      ..._commonFields(
        id: block.id,
        ownerId: ownerId,
        revision: revision,
        createdAt: block.createdAt,
        updatedAt: block.updatedAt,
      ),
      LockedBlockDocumentFields.name: block.name,
      LockedBlockDocumentFields.startTime:
          PersistenceWallTimeConvention.toStoredString(block.startTime),
      LockedBlockDocumentFields.endTime:
          PersistenceWallTimeConvention.toStoredString(block.endTime),
      LockedBlockDocumentFields.date: _civilDateToStored(block.date),
      LockedBlockDocumentFields.recurrence: block.recurrence == null
          ? null
          : LockedBlockRecurrenceDocumentMapping.toDocument(
              block.recurrence!,
            ),
      LockedBlockDocumentFields.hiddenByDefault: block.hiddenByDefault,
      LockedBlockDocumentFields.projectId: block.projectId,
      LockedBlockDocumentFields.archivedAt: _dateTimeToStored(block.archivedAt),
    };
  }

  static LockedBlock fromDocument(Map<String, Object?> document) {
    return _mapInvalidValues(() {
      _readCommon(document);
      final recurrenceDocument = _requiredNullableMap(
        document,
        LockedBlockDocumentFields.recurrence,
      );
      return LockedBlock(
        id: _requiredString(document, LockedBlockDocumentFields.id),
        name: _requiredString(document, LockedBlockDocumentFields.name),
        startTime: _requiredWallTime(
          document,
          LockedBlockDocumentFields.startTime,
        ),
        endTime: _requiredWallTime(
          document,
          LockedBlockDocumentFields.endTime,
        ),
        date: _requiredNullableCivilDate(
          document,
          LockedBlockDocumentFields.date,
        ),
        recurrence: recurrenceDocument == null
            ? null
            : LockedBlockRecurrenceDocumentMapping.fromDocument(
                recurrenceDocument,
              ),
        hiddenByDefault: _requiredBool(
          document,
          LockedBlockDocumentFields.hiddenByDefault,
        ),
        projectId: _requiredNullableString(
          document,
          LockedBlockDocumentFields.projectId,
        ),
        createdAt:
            _requiredDateTime(document, LockedBlockDocumentFields.createdAt),
        updatedAt:
            _requiredDateTime(document, LockedBlockDocumentFields.updatedAt),
        archivedAt: _requiredNullableDateTime(
          document,
          LockedBlockDocumentFields.archivedAt,
        ),
      );
    });
  }
}

/// Document mapping for [LockedBlockOverride].
abstract final class LockedBlockOverrideDocumentMapping {
  static Map<String, Object?> toDocument(
    LockedBlockOverride override, {
    required String ownerId,
    int revision = 1,
  }) {
    return {
      ..._commonFields(
        id: override.id,
        ownerId: ownerId,
        revision: revision,
        createdAt: override.createdAt,
        updatedAt: override.updatedAt,
      ),
      LockedBlockOverrideDocumentFields.lockedBlockId: override.lockedBlockId,
      LockedBlockOverrideDocumentFields.date:
          PersistenceCivilDateConvention.toStoredString(override.date),
      LockedBlockOverrideDocumentFields.type:
          PersistenceEnumMapping.encodeLockedBlockOverrideType(override.type),
      LockedBlockOverrideDocumentFields.name: override.name,
      LockedBlockOverrideDocumentFields.startTime:
          _wallTimeToStored(override.startTime),
      LockedBlockOverrideDocumentFields.endTime:
          _wallTimeToStored(override.endTime),
      LockedBlockOverrideDocumentFields.hiddenByDefault:
          override.hiddenByDefault,
      LockedBlockOverrideDocumentFields.projectId: override.projectId,
    };
  }

  static LockedBlockOverride fromDocument(Map<String, Object?> document) {
    return _mapInvalidValues(() {
      _readCommon(document);
      return LockedBlockOverride(
        id: _requiredString(document, LockedBlockOverrideDocumentFields.id),
        date: _requiredCivilDate(
          document,
          LockedBlockOverrideDocumentFields.date,
        ),
        type: PersistenceEnumMapping.decodeLockedBlockOverrideType(
          _requiredString(document, LockedBlockOverrideDocumentFields.type),
        ),
        createdAt: _requiredDateTime(
          document,
          LockedBlockOverrideDocumentFields.createdAt,
        ),
        updatedAt: _requiredDateTime(
          document,
          LockedBlockOverrideDocumentFields.updatedAt,
        ),
        lockedBlockId: _requiredNullableString(
          document,
          LockedBlockOverrideDocumentFields.lockedBlockId,
        ),
        name: _requiredNullableString(
          document,
          LockedBlockOverrideDocumentFields.name,
        ),
        startTime: _requiredNullableWallTime(
          document,
          LockedBlockOverrideDocumentFields.startTime,
        ),
        endTime: _requiredNullableWallTime(
          document,
          LockedBlockOverrideDocumentFields.endTime,
        ),
        hiddenByDefault: _requiredBool(
          document,
          LockedBlockOverrideDocumentFields.hiddenByDefault,
        ),
        projectId: _requiredNullableString(
          document,
          LockedBlockOverrideDocumentFields.projectId,
        ),
      );
    });
  }
}

/// Document mapping for [TaskActivity].
abstract final class TaskActivityDocumentMapping {
  static Map<String, Object?> toDocument(
    TaskActivity activity, {
    required String ownerId,
    int revision = 1,
    DateTime? createdAt,
    DateTime? updatedAt,
  }) {
    final created = createdAt ?? activity.occurredAt;
    final updated = updatedAt ?? created;
    return {
      ..._commonFields(
        id: activity.id,
        ownerId: ownerId,
        revision: revision,
        createdAt: created,
        updatedAt: updated,
      ),
      TaskActivityDocumentFields.operationId: activity.operationId,
      TaskActivityDocumentFields.code:
          PersistenceEnumMapping.encodeTaskActivityCode(activity.code),
      TaskActivityDocumentFields.taskId: activity.taskId,
      TaskActivityDocumentFields.projectId: activity.projectId,
      TaskActivityDocumentFields.occurredAt:
          PersistenceDateTimeConvention.toStoredString(activity.occurredAt),
      TaskActivityDocumentFields.metadata: _plainDocument(activity.metadata),
    };
  }

  static TaskActivity fromDocument(Map<String, Object?> document) {
    return _mapInvalidValues(() {
      _readCommon(document);
      return TaskActivity(
        id: _requiredString(document, TaskActivityDocumentFields.id),
        operationId: _requiredString(
          document,
          TaskActivityDocumentFields.operationId,
        ),
        code: PersistenceEnumMapping.decodeTaskActivityCode(
          _requiredString(document, TaskActivityDocumentFields.code),
        ),
        taskId: _requiredString(document, TaskActivityDocumentFields.taskId),
        projectId:
            _requiredString(document, TaskActivityDocumentFields.projectId),
        occurredAt: _requiredDateTime(
          document,
          TaskActivityDocumentFields.occurredAt,
        ),
        metadata: _requiredMap(document, TaskActivityDocumentFields.metadata),
      );
    });
  }
}

/// Document mapping for [OwnerSettings].
abstract final class OwnerSettingsDocumentMapping {
  static Map<String, Object?> toDocument(
    OwnerSettings settings, {
    required DateTime createdAt,
    required DateTime updatedAt,
    int revision = 1,
  }) {
    return {
      ..._commonFields(
        id: settings.ownerId,
        ownerId: settings.ownerId,
        revision: revision,
        createdAt: createdAt,
        updatedAt: updatedAt,
      ),
      OwnerSettingsDocumentFields.timeZoneId: settings.timeZoneId,
      OwnerSettingsDocumentFields.dayStart:
          PersistenceWallTimeConvention.toStoredString(settings.dayStart),
      OwnerSettingsDocumentFields.dayEnd:
          PersistenceWallTimeConvention.toStoredString(settings.dayEnd),
      OwnerSettingsDocumentFields.compactModeEnabled:
          settings.compactModeEnabled,
      OwnerSettingsDocumentFields.backlogStaleness:
          BacklogStalenessDocumentMapping.toDocument(
        settings.backlogStalenessSettings,
      ),
    };
  }

  static OwnerSettings fromDocument(Map<String, Object?> document) {
    return _mapInvalidValues(() {
      _readCommon(document);
      return OwnerSettings(
        ownerId: _requiredString(document, OwnerSettingsDocumentFields.ownerId),
        timeZoneId: _requiredString(
          document,
          OwnerSettingsDocumentFields.timeZoneId,
        ),
        dayStart: _requiredWallTime(
          document,
          OwnerSettingsDocumentFields.dayStart,
        ),
        dayEnd: _requiredWallTime(
          document,
          OwnerSettingsDocumentFields.dayEnd,
        ),
        compactModeEnabled: _requiredBool(
          document,
          OwnerSettingsDocumentFields.compactModeEnabled,
        ),
        backlogStalenessSettings: BacklogStalenessDocumentMapping.fromDocument(
          _requiredMap(document, OwnerSettingsDocumentFields.backlogStaleness),
        ),
      );
    });
  }
}

/// Document mapping for [BacklogStalenessSettings].
abstract final class BacklogStalenessDocumentMapping {
  static Map<String, Object?> toDocument(BacklogStalenessSettings settings) {
    return {
      BacklogStalenessDocumentFields.greenMaxAgeDays:
          settings.greenMaxAge.inDays,
      BacklogStalenessDocumentFields.blueMaxAgeDays: settings.blueMaxAge.inDays,
    };
  }

  static BacklogStalenessSettings fromDocument(Map<String, Object?> document) {
    return _mapInvalidValues(() {
      final greenDays = _requiredInt(
        document,
        BacklogStalenessDocumentFields.greenMaxAgeDays,
      );
      final blueDays = _requiredInt(
        document,
        BacklogStalenessDocumentFields.blueMaxAgeDays,
      );
      if (greenDays < 0 || blueDays < 0 || blueDays < greenDays) {
        throw const DocumentMappingException(
          code: DocumentMappingFailureCode.invalidValue,
          fieldName: OwnerSettingsDocumentFields.backlogStaleness,
          detailCode: 'invalidBacklogStalenessThresholds',
        );
      }
      return BacklogStalenessSettings(
        greenMaxAge: Duration(days: greenDays),
        blueMaxAge: Duration(days: blueDays),
      );
    });
  }
}

/// Document mapping for [NoticeAcknowledgementRecord].
abstract final class NoticeAcknowledgementDocumentMapping {
  static Map<String, Object?> toDocument(
    NoticeAcknowledgementRecord record, {
    int revision = 1,
  }) {
    return {
      ..._commonFields(
        id: record.noticeId,
        ownerId: record.ownerId,
        revision: revision,
        createdAt: record.acknowledgedAt,
        updatedAt: record.acknowledgedAt,
      ),
      NoticeAcknowledgementDocumentFields.noticeId: record.noticeId,
      NoticeAcknowledgementDocumentFields.acknowledgedAt:
          PersistenceDateTimeConvention.toStoredString(record.acknowledgedAt),
    };
  }

  static NoticeAcknowledgementRecord fromDocument(
    Map<String, Object?> document,
  ) {
    return _mapInvalidValues(() {
      _readCommon(document);
      return NoticeAcknowledgementRecord(
        noticeId: _requiredString(
          document,
          NoticeAcknowledgementDocumentFields.noticeId,
        ),
        ownerId: _requiredString(
          document,
          NoticeAcknowledgementDocumentFields.ownerId,
        ),
        acknowledgedAt: _requiredDateTime(
          document,
          NoticeAcknowledgementDocumentFields.acknowledgedAt,
        ),
      );
    });
  }
}

/// Document mapping for [ApplicationOperationRecord].
abstract final class ApplicationOperationDocumentMapping {
  static Map<String, Object?> toDocument(
    ApplicationOperationRecord record, {
    int revision = 1,
  }) {
    return {
      ..._commonFields(
        id: record.operationId,
        ownerId: record.ownerId,
        revision: revision,
        createdAt: record.committedAt,
        updatedAt: record.committedAt,
      ),
      ApplicationOperationDocumentFields.operationId: record.operationId,
      ApplicationOperationDocumentFields.operationName: record.operationName,
      ApplicationOperationDocumentFields.committedAt:
          PersistenceDateTimeConvention.toStoredString(record.committedAt),
    };
  }

  static ApplicationOperationRecord fromDocument(
    Map<String, Object?> document,
  ) {
    return _mapInvalidValues(() {
      _readCommon(document);
      return ApplicationOperationRecord(
        operationId: _requiredString(
          document,
          ApplicationOperationDocumentFields.operationId,
        ),
        ownerId: _requiredString(
          document,
          ApplicationOperationDocumentFields.ownerId,
        ),
        operationName: _requiredString(
          document,
          ApplicationOperationDocumentFields.operationName,
        ),
        committedAt: _requiredDateTime(
          document,
          ApplicationOperationDocumentFields.committedAt,
        ),
      );
    });
  }
}

/// Document mapping for [SchedulingStateSnapshot].
abstract final class SchedulingSnapshotDocumentMapping {
  static Map<String, Object?> toDocument(
    SchedulingStateSnapshot snapshot, {
    required String ownerId,
    int revision = 1,
    CivilDate? sourceDate,
    CivilDate? targetDate,
    DateTime? retentionExpiresAt,
    bool truncated = false,
  }) {
    return {
      ..._commonFields(
        id: snapshot.id,
        ownerId: ownerId,
        revision: revision,
        createdAt: snapshot.capturedAt,
        updatedAt: snapshot.capturedAt,
      ),
      SchedulingSnapshotDocumentFields.capturedAt:
          PersistenceDateTimeConvention.toStoredString(snapshot.capturedAt),
      SchedulingSnapshotDocumentFields.sourceDate:
          _civilDateToStored(sourceDate),
      SchedulingSnapshotDocumentFields.targetDate:
          _civilDateToStored(targetDate),
      SchedulingSnapshotDocumentFields.window:
          SchedulingWindowDocumentMapping.toDocument(snapshot.window),
      SchedulingSnapshotDocumentFields.tasks: snapshot.tasks
          .map(
            (task) => TaskDocumentMapping.toDocument(
              task,
              ownerId: ownerId,
            ),
          )
          .toList(growable: false),
      SchedulingSnapshotDocumentFields.lockedIntervals: snapshot.lockedIntervals
          .map(TimeIntervalDocumentMapping.toDocument)
          .toList(growable: false),
      SchedulingSnapshotDocumentFields.requiredVisibleIntervals: snapshot
          .requiredVisibleIntervals
          .map(TimeIntervalDocumentMapping.toDocument)
          .toList(growable: false),
      SchedulingSnapshotDocumentFields.notices: snapshot.notices
          .map(SchedulingNoticeDocumentMapping.toDocument)
          .toList(growable: false),
      SchedulingSnapshotDocumentFields.changes: snapshot.changes
          .map(SchedulingChangeDocumentMapping.toDocument)
          .toList(growable: false),
      SchedulingSnapshotDocumentFields.overlaps: snapshot.overlaps
          .map(SchedulingOverlapDocumentMapping.toDocument)
          .toList(growable: false),
      SchedulingSnapshotDocumentFields.operationName: snapshot.operationName,
      SchedulingSnapshotDocumentFields.retentionExpiresAt:
          _dateTimeToStored(retentionExpiresAt),
      SchedulingSnapshotDocumentFields.truncated: truncated,
    };
  }

  static SchedulingStateSnapshot fromDocument(Map<String, Object?> document) {
    return _mapInvalidValues(() {
      _readCommon(document);
      return SchedulingStateSnapshot(
        id: _requiredString(document, SchedulingSnapshotDocumentFields.id),
        capturedAt: _requiredDateTime(
          document,
          SchedulingSnapshotDocumentFields.capturedAt,
        ),
        window: SchedulingWindowDocumentMapping.fromDocument(
          _requiredMap(document, SchedulingSnapshotDocumentFields.window),
        ),
        tasks: _mappedList(
          document,
          SchedulingSnapshotDocumentFields.tasks,
          TaskDocumentMapping.fromDocument,
        ),
        lockedIntervals: _mappedList(
          document,
          SchedulingSnapshotDocumentFields.lockedIntervals,
          TimeIntervalDocumentMapping.fromDocument,
        ),
        requiredVisibleIntervals: _mappedList(
          document,
          SchedulingSnapshotDocumentFields.requiredVisibleIntervals,
          TimeIntervalDocumentMapping.fromDocument,
        ),
        notices: _mappedList(
          document,
          SchedulingSnapshotDocumentFields.notices,
          SchedulingNoticeDocumentMapping.fromDocument,
        ),
        changes: _mappedList(
          document,
          SchedulingSnapshotDocumentFields.changes,
          SchedulingChangeDocumentMapping.fromDocument,
        ),
        overlaps: _mappedList(
          document,
          SchedulingSnapshotDocumentFields.overlaps,
          SchedulingOverlapDocumentMapping.fromDocument,
        ),
        operationName: _requiredNullableString(
          document,
          SchedulingSnapshotDocumentFields.operationName,
        ),
      ).._validateSchedulingSnapshotDocumentMetadata(document);
    });
  }
}

extension on SchedulingStateSnapshot {
  void _validateSchedulingSnapshotDocumentMetadata(
    Map<String, Object?> document,
  ) {
    _requiredNullableCivilDate(
      document,
      SchedulingSnapshotDocumentFields.sourceDate,
    );
    _requiredNullableCivilDate(
      document,
      SchedulingSnapshotDocumentFields.targetDate,
    );
    _requiredNullableDateTime(
      document,
      SchedulingSnapshotDocumentFields.retentionExpiresAt,
    );
    _requiredBool(document, SchedulingSnapshotDocumentFields.truncated);
  }
}

/// Document mapping for [SchedulingWindow].
abstract final class SchedulingWindowDocumentMapping {
  static Map<String, Object?> toDocument(SchedulingWindow window) {
    return {
      SchedulingWindowDocumentFields.start:
          PersistenceDateTimeConvention.toStoredString(window.start),
      SchedulingWindowDocumentFields.end:
          PersistenceDateTimeConvention.toStoredString(window.end),
    };
  }

  static SchedulingWindow fromDocument(Map<String, Object?> document) {
    return _mapInvalidValues(() {
      return SchedulingWindow(
        start:
            _requiredDateTime(document, SchedulingWindowDocumentFields.start),
        end: _requiredDateTime(document, SchedulingWindowDocumentFields.end),
      );
    });
  }
}

/// Document mapping for [TimeInterval].
abstract final class TimeIntervalDocumentMapping {
  static Map<String, Object?> toDocument(TimeInterval interval) {
    return {
      TimeIntervalDocumentFields.start:
          PersistenceDateTimeConvention.toStoredString(interval.start),
      TimeIntervalDocumentFields.end:
          PersistenceDateTimeConvention.toStoredString(interval.end),
      TimeIntervalDocumentFields.label: interval.label,
    };
  }

  static TimeInterval fromDocument(Map<String, Object?> document) {
    return _mapInvalidValues(() {
      return TimeInterval(
        start: _requiredDateTime(document, TimeIntervalDocumentFields.start),
        end: _requiredDateTime(document, TimeIntervalDocumentFields.end),
        label: _requiredNullableString(
          document,
          TimeIntervalDocumentFields.label,
        ),
      );
    });
  }
}

/// Document mapping for [SchedulingNotice].
abstract final class SchedulingNoticeDocumentMapping {
  static Map<String, Object?> toDocument(SchedulingNotice notice) {
    return {
      SchedulingNoticeDocumentFields.message: notice.message,
      SchedulingNoticeDocumentFields.type:
          PersistenceEnumMapping.encodeSchedulingNoticeType(notice.type),
      SchedulingNoticeDocumentFields.taskId: notice.taskId,
      SchedulingNoticeDocumentFields.issueCode: notice.issueCode == null
          ? null
          : PersistenceEnumMapping.encodeSchedulingIssueCode(notice.issueCode!),
      SchedulingNoticeDocumentFields.movementCode: notice.movementCode == null
          ? null
          : PersistenceEnumMapping.encodeSchedulingMovementCode(
              notice.movementCode!,
            ),
      SchedulingNoticeDocumentFields.conflictCode: notice.conflictCode == null
          ? null
          : PersistenceEnumMapping.encodeSchedulingConflictCode(
              notice.conflictCode!,
            ),
      SchedulingNoticeDocumentFields.parameters:
          _plainDocument(notice.parameters),
    };
  }

  static SchedulingNotice fromDocument(Map<String, Object?> document) {
    return _mapInvalidValues(() {
      return SchedulingNotice(
        _requiredString(document, SchedulingNoticeDocumentFields.message),
        type: PersistenceEnumMapping.decodeSchedulingNoticeType(
          _requiredString(document, SchedulingNoticeDocumentFields.type),
        ),
        taskId: _requiredNullableString(
          document,
          SchedulingNoticeDocumentFields.taskId,
        ),
        issueCode: PersistenceEnumMapping.decodeNullableSchedulingIssueCode(
          _requiredNullableString(
            document,
            SchedulingNoticeDocumentFields.issueCode,
          ),
        ),
        movementCode:
            PersistenceEnumMapping.decodeNullableSchedulingMovementCode(
          _requiredNullableString(
            document,
            SchedulingNoticeDocumentFields.movementCode,
          ),
        ),
        conflictCode:
            PersistenceEnumMapping.decodeNullableSchedulingConflictCode(
          _requiredNullableString(
            document,
            SchedulingNoticeDocumentFields.conflictCode,
          ),
        ),
        parameters: _requiredMap(
          document,
          SchedulingNoticeDocumentFields.parameters,
        ),
      );
    });
  }
}

/// Document mapping for [SchedulingChange].
abstract final class SchedulingChangeDocumentMapping {
  static Map<String, Object?> toDocument(SchedulingChange change) {
    return {
      SchedulingChangeDocumentFields.taskId: change.taskId,
      SchedulingChangeDocumentFields.previousStart:
          _dateTimeToStored(change.previousStart),
      SchedulingChangeDocumentFields.previousEnd:
          _dateTimeToStored(change.previousEnd),
      SchedulingChangeDocumentFields.nextStart:
          _dateTimeToStored(change.nextStart),
      SchedulingChangeDocumentFields.nextEnd: _dateTimeToStored(change.nextEnd),
    };
  }

  static SchedulingChange fromDocument(Map<String, Object?> document) {
    return _mapInvalidValues(() {
      return SchedulingChange(
        taskId:
            _requiredString(document, SchedulingChangeDocumentFields.taskId),
        previousStart: _requiredNullableDateTime(
          document,
          SchedulingChangeDocumentFields.previousStart,
        ),
        previousEnd: _requiredNullableDateTime(
          document,
          SchedulingChangeDocumentFields.previousEnd,
        ),
        nextStart: _requiredNullableDateTime(
          document,
          SchedulingChangeDocumentFields.nextStart,
        ),
        nextEnd: _requiredNullableDateTime(
          document,
          SchedulingChangeDocumentFields.nextEnd,
        ),
      );
    });
  }
}

/// Document mapping for [SchedulingOverlap].
abstract final class SchedulingOverlapDocumentMapping {
  static Map<String, Object?> toDocument(SchedulingOverlap overlap) {
    return {
      SchedulingOverlapDocumentFields.taskId: overlap.taskId,
      SchedulingOverlapDocumentFields.taskInterval:
          TimeIntervalDocumentMapping.toDocument(overlap.taskInterval),
      SchedulingOverlapDocumentFields.blockedInterval:
          TimeIntervalDocumentMapping.toDocument(overlap.blockedInterval),
    };
  }

  static SchedulingOverlap fromDocument(Map<String, Object?> document) {
    return _mapInvalidValues(() {
      return SchedulingOverlap(
        taskId:
            _requiredString(document, SchedulingOverlapDocumentFields.taskId),
        taskInterval: TimeIntervalDocumentMapping.fromDocument(
          _requiredMap(document, SchedulingOverlapDocumentFields.taskInterval),
        ),
        blockedInterval: TimeIntervalDocumentMapping.fromDocument(
          _requiredMap(
            document,
            SchedulingOverlapDocumentFields.blockedInterval,
          ),
        ),
      );
    });
  }
}

/// Convenience extension for converting a [Task] into a V1 document map.
extension TaskDocumentExtension on Task {
  Map<String, Object?> toDocument({
    required String ownerId,
    int revision = 1,
    bool clearSchedule = false,
    DateTime? backlogEnteredAt,
    String? backlogEnteredAtProvenance,
  }) {
    return TaskDocumentMapping.toDocument(
      this,
      ownerId: ownerId,
      revision: revision,
      clearSchedule: clearSchedule,
      backlogEnteredAt: backlogEnteredAt,
      backlogEnteredAtProvenance: backlogEnteredAtProvenance,
    );
  }
}

/// Convenience extension for converting [TaskStatistics] into an embedded map.
extension TaskStatisticsDocumentExtension on TaskStatistics {
  Map<String, Object?> toDocument() {
    return TaskStatisticsDocumentMapping.toDocument(this);
  }
}

/// Convenience extension for converting [ProjectStatistics] into a document.
extension ProjectStatisticsDocumentExtension on ProjectStatistics {
  Map<String, Object?> toDocument({
    required String ownerId,
    required DateTime createdAt,
    required DateTime updatedAt,
    int revision = 1,
    Iterable<String> appliedActivityIds = const <String>[],
  }) {
    return ProjectStatisticsDocumentMapping.toDocument(
      this,
      ownerId: ownerId,
      createdAt: createdAt,
      updatedAt: updatedAt,
      revision: revision,
      appliedActivityIds: appliedActivityIds,
    );
  }
}

const _taskTypeByCode = <String, TaskType>{
  'flexible': TaskType.flexible,
  'inflexible': TaskType.inflexible,
  'critical': TaskType.critical,
  'locked': TaskType.locked,
  'surprise': TaskType.surprise,
  'free_slot': TaskType.freeSlot,
};

const _taskStatusByCode = <String, TaskStatus>{
  'planned': TaskStatus.planned,
  'active': TaskStatus.active,
  'completed': TaskStatus.completed,
  'missed': TaskStatus.missed,
  'cancelled': TaskStatus.cancelled,
  'no_longer_relevant': TaskStatus.noLongerRelevant,
  'backlog': TaskStatus.backlog,
};

const _priorityByCode = <String, PriorityLevel>{
  'very_low': PriorityLevel.veryLow,
  'low': PriorityLevel.low,
  'medium': PriorityLevel.medium,
  'high': PriorityLevel.high,
  'very_high': PriorityLevel.veryHigh,
};

const _rewardByCode = <String, RewardLevel>{
  'not_set': RewardLevel.notSet,
  'very_low': RewardLevel.veryLow,
  'low': RewardLevel.low,
  'medium': RewardLevel.medium,
  'high': RewardLevel.high,
  'very_high': RewardLevel.veryHigh,
};

const _difficultyByCode = <String, DifficultyLevel>{
  'not_set': DifficultyLevel.notSet,
  'very_easy': DifficultyLevel.veryEasy,
  'easy': DifficultyLevel.easy,
  'medium': DifficultyLevel.medium,
  'hard': DifficultyLevel.hard,
  'very_hard': DifficultyLevel.veryHard,
};

const _reminderProfileByCode = <String, ReminderProfile>{
  'silent': ReminderProfile.silent,
  'gentle': ReminderProfile.gentle,
  'persistent': ReminderProfile.persistent,
  'strict': ReminderProfile.strict,
};

const _backlogTagByCode = <String, BacklogTag>{
  'wishlist': BacklogTag.wishlist,
};

const _lockedWeekdayByCode = <String, LockedWeekday>{
  'monday': LockedWeekday.monday,
  'tuesday': LockedWeekday.tuesday,
  'wednesday': LockedWeekday.wednesday,
  'thursday': LockedWeekday.thursday,
  'friday': LockedWeekday.friday,
  'saturday': LockedWeekday.saturday,
  'sunday': LockedWeekday.sunday,
};

const _lockedBlockOverrideTypeByCode = <String, LockedBlockOverrideType>{
  'remove': LockedBlockOverrideType.remove,
  'replace': LockedBlockOverrideType.replace,
  'add': LockedBlockOverrideType.add,
};

const _taskActivityCodeByCode = <String, TaskActivityCode>{
  'completed': TaskActivityCode.completed,
  'missed': TaskActivityCode.missed,
  'cancelled': TaskActivityCode.cancelled,
  'no_longer_relevant': TaskActivityCode.noLongerRelevant,
  'manually_pushed': TaskActivityCode.manuallyPushed,
  'automatically_pushed': TaskActivityCode.automaticallyPushed,
  'moved_to_backlog': TaskActivityCode.movedToBacklog,
  'restored_from_backlog': TaskActivityCode.restoredFromBacklog,
  'activated': TaskActivityCode.activated,
};

const _schedulingNoticeTypeByCode = <String, SchedulingNoticeType>{
  'info': SchedulingNoticeType.info,
  'moved': SchedulingNoticeType.moved,
  'overlap': SchedulingNoticeType.overlap,
  'no_fit': SchedulingNoticeType.noFit,
  'overflow': SchedulingNoticeType.overflow,
};

const _schedulingIssueCodeByCode = <String, SchedulingIssueCode>{
  'task_not_found': SchedulingIssueCode.taskNotFound,
  'invalid_task_state': SchedulingIssueCode.invalidTaskState,
  'missing_duration': SchedulingIssueCode.missingDuration,
  'missing_scheduled_slot': SchedulingIssueCode.missingScheduledSlot,
  'non_positive_duration': SchedulingIssueCode.nonPositiveDuration,
  'no_available_slot': SchedulingIssueCode.noAvailableSlot,
  'unfinished_tasks_could_not_fit':
      SchedulingIssueCode.unfinishedTasksCouldNotFit,
  'no_unfinished_flexible_tasks': SchedulingIssueCode.noUnfinishedFlexibleTasks,
  'duplicate_surprise_log': SchedulingIssueCode.duplicateSurpriseLog,
};

const _schedulingMovementCodeByCode = <String, SchedulingMovementCode>{
  'backlog_task_inserted': SchedulingMovementCode.backlogTaskInserted,
  'flexible_task_moved_to_make_room':
      SchedulingMovementCode.flexibleTaskMovedToMakeRoom,
  'flexible_task_pushed_to_next_available_slot':
      SchedulingMovementCode.flexibleTaskPushedToNextAvailableSlot,
  'flexible_task_moved_to_tomorrow':
      SchedulingMovementCode.flexibleTaskMovedToTomorrow,
  'unfinished_flexible_tasks_rolled_over':
      SchedulingMovementCode.unfinishedFlexibleTasksRolledOver,
  'flexible_task_moved_to_backlog':
      SchedulingMovementCode.flexibleTaskMovedToBacklog,
  'required_commitment_scheduled':
      SchedulingMovementCode.requiredCommitmentScheduled,
};

const _schedulingConflictCodeByCode = <String, SchedulingConflictCode>{
  'flexible_task_overlaps_blocked_time':
      SchedulingConflictCode.flexibleTaskOverlapsBlockedTime,
  'surprise_task_overlaps_required_visible_time':
      SchedulingConflictCode.surpriseTaskOverlapsRequiredVisibleTime,
  'required_commitment_overlaps_protected_free_slot':
      SchedulingConflictCode.requiredCommitmentOverlapsProtectedFreeSlot,
};

const _projectCompletionTimeBucketByCode =
    <String, ProjectCompletionTimeBucket>{
  'overnight': ProjectCompletionTimeBucket.overnight,
  'morning': ProjectCompletionTimeBucket.morning,
  'afternoon': ProjectCompletionTimeBucket.afternoon,
  'evening': ProjectCompletionTimeBucket.evening,
  'night': ProjectCompletionTimeBucket.night,
};

T _decodeCode<T>(Map<String, T> valuesByCode, String code, String fieldName) {
  final value = valuesByCode[code];
  if (value == null) {
    throw DocumentMappingException(
      code: DocumentMappingFailureCode.unknownCode,
      fieldName: fieldName,
      detailCode: code,
    );
  }
  return value;
}

Map<String, Object?> _commonFields({
  required String id,
  required String ownerId,
  required int revision,
  required DateTime createdAt,
  required DateTime updatedAt,
}) {
  if (revision <= 0) {
    throw const DocumentMappingException(
      code: DocumentMappingFailureCode.invalidRevision,
      fieldName: DocumentFields.revision,
    );
  }
  return {
    DocumentFields.schemaVersion: v1SchemaVersion,
    DocumentFields.id: id,
    DocumentFields.ownerId: ownerId,
    DocumentFields.revision: revision,
    DocumentFields.createdAt:
        PersistenceDateTimeConvention.toStoredString(createdAt),
    DocumentFields.updatedAt:
        PersistenceDateTimeConvention.toStoredString(updatedAt),
  };
}

DocumentMetadata _readCommon(Map<String, Object?> document) {
  final schemaVersion = _requiredInt(document, DocumentFields.schemaVersion);
  if (schemaVersion != v1SchemaVersion) {
    throw DocumentMappingException(
      code: DocumentMappingFailureCode.invalidSchemaVersion,
      fieldName: DocumentFields.schemaVersion,
      detailCode: schemaVersion.toString(),
    );
  }
  final revision = _requiredInt(document, DocumentFields.revision);
  if (revision <= 0) {
    throw DocumentMappingException(
      code: DocumentMappingFailureCode.invalidRevision,
      fieldName: DocumentFields.revision,
      detailCode: revision.toString(),
    );
  }
  return DocumentMetadata(
    id: _requiredString(document, DocumentFields.id),
    ownerId: _requiredString(document, DocumentFields.ownerId),
    revision: revision,
    createdAt: _requiredDateTime(document, DocumentFields.createdAt),
    updatedAt: _requiredDateTime(document, DocumentFields.updatedAt),
  );
}

T _mapInvalidValues<T>(T Function() read) {
  try {
    return read();
  } on DocumentMappingException {
    rethrow;
  } on DomainValidationException catch (error) {
    throw DocumentMappingException(
      code: DocumentMappingFailureCode.invalidValue,
      fieldName: error.name,
      detailCode: error.code.name,
    );
  } on FormatException catch (error) {
    throw DocumentMappingException(
      code: DocumentMappingFailureCode.invalidValue,
      detailCode: error.message,
    );
  } on ArgumentError catch (error) {
    throw DocumentMappingException(
      code: DocumentMappingFailureCode.invalidValue,
      fieldName: error.name,
    );
  }
}

String? _dateTimeToStored(DateTime? value) {
  return value == null
      ? null
      : PersistenceDateTimeConvention.toStoredString(value);
}

String? _civilDateToStored(CivilDate? value) {
  return value == null
      ? null
      : PersistenceCivilDateConvention.toStoredString(value);
}

String? _wallTimeToStored(WallTime? value) {
  return value == null
      ? null
      : PersistenceWallTimeConvention.toStoredString(value);
}

String _requiredString(Map<String, Object?> document, String fieldName) {
  if (!document.containsKey(fieldName)) {
    throw DocumentMappingException(
      code: DocumentMappingFailureCode.missingField,
      fieldName: fieldName,
    );
  }
  final value = document[fieldName];
  if (value is String) {
    return value;
  }
  throw DocumentMappingException(
    code: DocumentMappingFailureCode.wrongType,
    fieldName: fieldName,
  );
}

String? _requiredNullableString(
  Map<String, Object?> document,
  String fieldName,
) {
  if (!document.containsKey(fieldName)) {
    throw DocumentMappingException(
      code: DocumentMappingFailureCode.missingField,
      fieldName: fieldName,
    );
  }
  final value = document[fieldName];
  if (value == null || value is String) {
    return value as String?;
  }
  throw DocumentMappingException(
    code: DocumentMappingFailureCode.wrongType,
    fieldName: fieldName,
  );
}

int _requiredInt(Map<String, Object?> document, String fieldName) {
  if (!document.containsKey(fieldName)) {
    throw DocumentMappingException(
      code: DocumentMappingFailureCode.missingField,
      fieldName: fieldName,
    );
  }
  final value = document[fieldName];
  if (value is int) {
    return value;
  }
  throw DocumentMappingException(
    code: DocumentMappingFailureCode.wrongType,
    fieldName: fieldName,
  );
}

int? _requiredNullableInt(
  Map<String, Object?> document,
  String fieldName,
) {
  if (!document.containsKey(fieldName)) {
    throw DocumentMappingException(
      code: DocumentMappingFailureCode.missingField,
      fieldName: fieldName,
    );
  }
  final value = document[fieldName];
  if (value == null || value is int) {
    return value as int?;
  }
  throw DocumentMappingException(
    code: DocumentMappingFailureCode.wrongType,
    fieldName: fieldName,
  );
}

bool _requiredBool(Map<String, Object?> document, String fieldName) {
  if (!document.containsKey(fieldName)) {
    throw DocumentMappingException(
      code: DocumentMappingFailureCode.missingField,
      fieldName: fieldName,
    );
  }
  final value = document[fieldName];
  if (value is bool) {
    return value;
  }
  throw DocumentMappingException(
    code: DocumentMappingFailureCode.wrongType,
    fieldName: fieldName,
  );
}

DateTime _requiredDateTime(Map<String, Object?> document, String fieldName) {
  return PersistenceDateTimeConvention.fromStoredString(
    _requiredString(document, fieldName),
  );
}

DateTime? _requiredNullableDateTime(
  Map<String, Object?> document,
  String fieldName,
) {
  final value = _requiredNullableString(document, fieldName);
  return value == null
      ? null
      : PersistenceDateTimeConvention.fromStoredString(value);
}

CivilDate _requiredCivilDate(Map<String, Object?> document, String fieldName) {
  return PersistenceCivilDateConvention.fromStoredString(
    _requiredString(document, fieldName),
  );
}

CivilDate? _requiredNullableCivilDate(
  Map<String, Object?> document,
  String fieldName,
) {
  final value = _requiredNullableString(document, fieldName);
  return value == null
      ? null
      : PersistenceCivilDateConvention.fromStoredString(value);
}

WallTime _requiredWallTime(Map<String, Object?> document, String fieldName) {
  return PersistenceWallTimeConvention.fromStoredString(
    _requiredString(document, fieldName),
  );
}

WallTime? _requiredNullableWallTime(
  Map<String, Object?> document,
  String fieldName,
) {
  final value = _requiredNullableString(document, fieldName);
  return value == null
      ? null
      : PersistenceWallTimeConvention.fromStoredString(value);
}

Map<String, Object?> _requiredMap(
  Map<String, Object?> document,
  String fieldName,
) {
  if (!document.containsKey(fieldName)) {
    throw DocumentMappingException(
      code: DocumentMappingFailureCode.missingField,
      fieldName: fieldName,
    );
  }
  final value = document[fieldName];
  if (value is Map<String, Object?>) {
    return value;
  }
  if (value is Map) {
    return Map<String, Object?>.from(value);
  }
  throw DocumentMappingException(
    code: DocumentMappingFailureCode.wrongType,
    fieldName: fieldName,
  );
}

Map<String, Object?>? _requiredNullableMap(
  Map<String, Object?> document,
  String fieldName,
) {
  if (!document.containsKey(fieldName)) {
    throw DocumentMappingException(
      code: DocumentMappingFailureCode.missingField,
      fieldName: fieldName,
    );
  }
  final value = document[fieldName];
  if (value == null) {
    return null;
  }
  if (value is Map<String, Object?>) {
    return value;
  }
  if (value is Map) {
    return Map<String, Object?>.from(value);
  }
  throw DocumentMappingException(
    code: DocumentMappingFailureCode.wrongType,
    fieldName: fieldName,
  );
}

List<Object?> _requiredList(Map<String, Object?> document, String fieldName) {
  if (!document.containsKey(fieldName)) {
    throw DocumentMappingException(
      code: DocumentMappingFailureCode.missingField,
      fieldName: fieldName,
    );
  }
  final value = document[fieldName];
  if (value is List) {
    return List<Object?>.unmodifiable(value);
  }
  throw DocumentMappingException(
    code: DocumentMappingFailureCode.wrongType,
    fieldName: fieldName,
  );
}

List<T> _mappedList<T>(
  Map<String, Object?> document,
  String fieldName,
  T Function(Map<String, Object?> document) decode,
) {
  return _requiredList(document, fieldName).map((value) {
    if (value is Map<String, Object?>) {
      return decode(value);
    }
    if (value is Map) {
      return decode(Map<String, Object?>.from(value));
    }
    throw DocumentMappingException(
      code: DocumentMappingFailureCode.wrongType,
      fieldName: fieldName,
    );
  }).toList(growable: false);
}

Map<String, Object?> _intKeyCountMapToDocument(Map<int, int> counts) {
  return {
    for (final entry in counts.entries) entry.key.toString(): entry.value,
  };
}

Map<String, Object?> _enumCountMapToDocument<T extends Enum>(
  Map<T, int> counts,
  String Function(T value) encode,
) {
  return {
    for (final entry in counts.entries) encode(entry.key): entry.value,
  };
}

Map<int, int> _intKeyCountMapFromDocument(
  Map<String, Object?> document,
  String fieldName,
) {
  final raw = _requiredMap(document, fieldName);
  return {
    for (final entry in raw.entries)
      int.parse(entry.key): _countValue(entry.value, fieldName),
  };
}

Map<T, int> _enumCountMapFromDocument<T extends Enum>(
  Map<String, Object?> document,
  String fieldName,
  T Function(String value) decode,
) {
  final raw = _requiredMap(document, fieldName);
  return {
    for (final entry in raw.entries)
      decode(entry.key): _countValue(entry.value, fieldName),
  };
}

int _countValue(Object? value, String fieldName) {
  if (value is int) {
    return value;
  }
  throw DocumentMappingException(
    code: DocumentMappingFailureCode.wrongType,
    fieldName: fieldName,
  );
}

Map<String, Object?> _plainDocument(Map<String, Object?> value) {
  return Map<String, Object?>.unmodifiable(
    value.map((key, value) => MapEntry(key, _plainDocumentValue(value))),
  );
}

Object? _plainDocumentValue(Object? value) {
  if (value == null ||
      value is String ||
      value is num ||
      value is bool ||
      value is DateTime) {
    return value is DateTime
        ? PersistenceDateTimeConvention.toStoredString(value)
        : value;
  }
  if (value is CivilDate) {
    return PersistenceCivilDateConvention.toStoredString(value);
  }
  if (value is WallTime) {
    return PersistenceWallTimeConvention.toStoredString(value);
  }
  if (value is Enum) {
    return PersistenceEnumMapping.encode(value);
  }
  if (value is List) {
    return value.map(_plainDocumentValue).toList(growable: false);
  }
  if (value is Map<String, Object?>) {
    return _plainDocument(value);
  }
  if (value is Map) {
    return _plainDocument(Map<String, Object?>.from(value));
  }
  throw DocumentMappingException(
    code: DocumentMappingFailureCode.wrongType,
    detailCode: value.runtimeType.toString(),
  );
}
