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

part of '../../application_management.dart';

/// V1 management facade used by future Flutter state management.
class V1ApplicationManagementUseCases {
  /// Creates a `V1ApplicationManagementUseCases` instance with the values required by its invariants.
  /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code.
  const V1ApplicationManagementUseCases({
    required this.applicationStore,
    this.projectSuggestionService = const ProjectSuggestionService(),
  });

  /// Atomic application boundary.
  final ApplicationUnitOfWork applicationStore;

  /// Learned project suggestion policy.
  final ProjectSuggestionService projectSuggestionService;

  /// Load backlog candidates through the repository status filter, then apply
  /// existing filter/sort/staleness policies in memory.
  Future<ApplicationResult<BacklogQueryResult>> getBacklog(
    GetBacklogRequest request,
  ) {
    return applicationStore.read(
      action: (repositories) async {
        final ownerId = request.context.ownerTimeZone.ownerId;
        logger.debug(() => 'Loading backlog query. ownerId=$ownerId '
            'sortKey=${request.sortKey.name} hasFilter=${request.filter != null}');
        final settings = await _ownerSettingsOrDefault(
          repositories,
          request.context.ownerTimeZone,
        );
        final candidates = await repositories.tasks.findByStatus(
          TaskStatus.backlog,
        );
        final filtered = request.filter == null
            ? candidates
            : BacklogView(
                tasks: candidates,
                now: request.context.now,
                stalenessSettings: settings.backlogStalenessSettings,
              ).filter(request.filter!);
        final view = BacklogView(
          tasks: filtered,
          now: request.context.now,
          stalenessSettings: settings.backlogStalenessSettings,
        );
        final sorted = view.sorted(request.sortKey);
        logger.debug(() => 'Backlog query loaded. ownerId=$ownerId '
            'candidateCount=${candidates.length} filteredCount=${filtered.length} '
            'itemCount=${sorted.length}');

        return ApplicationResult.success(
          BacklogQueryResult(
            ownerId: ownerId,
            settings: settings,
            items: [
              for (final task in sorted)
                BacklogItemReadModel(
                  task: task,
                  stalenessMarker: view.stalenessMarkerFor(task),
                ),
            ],
          ),
        );
      },
    );
  }

  /// Load configured project defaults with learned suggestions kept separate.
  Future<ApplicationResult<ProjectDefaultsQueryResult>> getProjectDefaults(
    GetProjectDefaultsRequest request,
  ) {
    return applicationStore.read(
      action: (repositories) async {
        logger.debug(() =>
            'Loading project defaults. ownerId=${request.context.ownerTimeZone.ownerId} '
            'includeArchived=${request.includeArchived}');
        final projects = (await repositories.projects.findByOwner(
          ownerId: request.context.ownerTimeZone.ownerId,
          includeArchived: request.includeArchived,
        ))
            .items;
        final resolutions = <ProjectDefaultResolution>[];
        final sortedProjects = [...projects]
          ..sort((a, b) => a.name.compareTo(b.name));

        for (final project in sortedProjects) {
          if (project.isArchived && !request.includeArchived) {
            continue;
          }
          final statistics =
              await repositories.projectStatistics.findByProjectId(project.id);
          resolutions.add(
            projectSuggestionService.resolveDefaults(
              project: project,
              statistics:
                  statistics ?? ProjectStatistics(projectId: project.id),
            ),
          );
        }

        logger.debug(() =>
            'Project defaults loaded. ownerId=${request.context.ownerTimeZone.ownerId} '
            'projectCount=${projects.length} resolutionCount=${resolutions.length}');

        return ApplicationResult.success(
          ProjectDefaultsQueryResult(
            ownerId: request.context.ownerTimeZone.ownerId,
            projects: resolutions,
          ),
        );
      },
    );
  }

  /// Runs the `createProject` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  Future<ApplicationResult<ProjectProfile>> createProject({
    required ApplicationOperationContext context,
    required ProjectProfileDraft draft,
  }) {
    return applicationStore.run(
      context: context,
      operationName: ApplicationManagementCommandCode.createProject.name,
      action: (repositories) async {
        final id = draft.id ?? context.idGenerator.nextId();
        logger.debug(() =>
            'Management command executing. command=${ApplicationManagementCommandCode.createProject.name} '
            'operationId=${context.operationId} projectId=$id hasName=${draft.name.trim().isNotEmpty} '
            'defaultDurationMinutes=${draft.defaultDurationMinutes}');
        final existing = await repositories.projects.findById(id);
        if (existing != null) {
          logger.warn(() => 'Create project failed: project already exists. '
              'operationId=${context.operationId} projectId=$id');
          return _failure(
            ApplicationFailureCode.conflict,
            entityId: id,
            detailCode: 'projectAlreadyExists',
          );
        }
        final project = ProjectProfile(
          id: id,
          name: draft.name,
          colorKey: draft.colorKey,
          defaultPriority: draft.defaultPriority,
          defaultReward: draft.defaultReward,
          defaultDifficulty: draft.defaultDifficulty,
          defaultReminderProfile: draft.defaultReminderProfile,
          defaultDurationMinutes: draft.defaultDurationMinutes,
        );
        await repositories.projects.save(project);
        logger.debug(() =>
            'Project created. operationId=${context.operationId} projectId=$id');
        return ApplicationResult.success(project);
      },
    );
  }

  /// Runs the `updateProject` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  Future<ApplicationResult<ProjectProfile>> updateProject({
    required ApplicationOperationContext context,
    required String projectId,
    required ProjectProfileUpdate update,
  }) {
    return applicationStore.run(
      context: context,
      operationName: ApplicationManagementCommandCode.updateProject.name,
      action: (repositories) async {
        logger.debug(() =>
            'Management command executing. command=${ApplicationManagementCommandCode.updateProject.name} '
            'operationId=${context.operationId} projectId=$projectId '
            'hasName=${update.name != null} hasColorKey=${update.colorKey != null}');
        final project = await _requireProject(repositories, projectId);
        final updated = project.copyWith(
          name: update.name,
          colorKey: update.colorKey,
          defaultPriority: update.defaultPriority,
          defaultReward: update.defaultReward,
          defaultDifficulty: update.defaultDifficulty,
          defaultReminderProfile: update.defaultReminderProfile,
          defaultDurationMinutes: update.defaultDurationMinutes,
          clearDefaultDuration: update.clearDefaultDuration,
        );
        await repositories.projects.save(updated);
        logger.debug(() =>
            'Project updated. operationId=${context.operationId} projectId=$projectId');
        return ApplicationResult.success(updated);
      },
    );
  }

  /// Runs the `archiveProject` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  Future<ApplicationResult<ProjectProfile>> archiveProject({
    required ApplicationOperationContext context,
    required String projectId,
  }) {
    return applicationStore.run(
      context: context,
      operationName: ApplicationManagementCommandCode.archiveProject.name,
      action: (repositories) async {
        logger.debug(() =>
            'Management command executing. command=${ApplicationManagementCommandCode.archiveProject.name} '
            'operationId=${context.operationId} projectId=$projectId');
        final project = await _requireProject(repositories, projectId);
        final archived = project.copyWith(archivedAt: context.now);
        await repositories.projects.save(archived);
        logger.debug(() =>
            'Project archived. operationId=${context.operationId} projectId=$projectId');
        return ApplicationResult.success(archived);
      },
    );
  }

  /// Runs the `createLockedBlock` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  Future<ApplicationResult<LockedBlock>> createLockedBlock({
    required ApplicationOperationContext context,
    required LockedBlockDraft draft,
  }) {
    return applicationStore.run(
      context: context,
      operationName: ApplicationManagementCommandCode.createLockedBlock.name,
      action: (repositories) async {
        final id = draft.id ?? context.idGenerator.nextId();
        logger.debug(() =>
            'Management command executing. command=${ApplicationManagementCommandCode.createLockedBlock.name} '
            'operationId=${context.operationId} lockedBlockId=$id date=${draft.date?.toIsoString()} '
            'hasRecurrence=${draft.recurrence != null} hiddenByDefault=${draft.hiddenByDefault}');
        final existing = await repositories.lockedBlocks.findBlockById(id);
        if (existing != null) {
          logger.warn(() => 'Create locked block failed: block already exists. '
              'operationId=${context.operationId} lockedBlockId=$id');
          return _failure(
            ApplicationFailureCode.conflict,
            entityId: id,
            detailCode: 'lockedBlockAlreadyExists',
          );
        }
        final block = LockedBlock(
          id: id,
          name: draft.name,
          startTime: draft.startTime,
          endTime: draft.endTime,
          date: draft.date,
          recurrence: draft.recurrence,
          hiddenByDefault: draft.hiddenByDefault,
          projectId: draft.projectId,
          createdAt: context.now,
          updatedAt: context.now,
        );
        await repositories.lockedBlocks.saveBlock(block);
        logger.debug(() =>
            'Locked block created. operationId=${context.operationId} lockedBlockId=$id');
        return ApplicationResult.success(block);
      },
    );
  }

  /// Runs the `updateLockedBlock` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  Future<ApplicationResult<LockedBlock>> updateLockedBlock({
    required ApplicationOperationContext context,
    required String lockedBlockId,
    required LockedBlockUpdate update,
  }) {
    return applicationStore.run(
      context: context,
      operationName: ApplicationManagementCommandCode.updateLockedBlock.name,
      action: (repositories) async {
        logger.debug(() =>
            'Management command executing. command=${ApplicationManagementCommandCode.updateLockedBlock.name} '
            'operationId=${context.operationId} lockedBlockId=$lockedBlockId '
            'hasName=${update.name != null} hasDate=${update.date != null} '
            'hasRecurrence=${update.recurrence != null}');
        final block = await _requireLockedBlock(repositories, lockedBlockId);
        final updated = block.copyWith(
          name: update.name,
          startTime: update.startTime,
          endTime: update.endTime,
          date: update.date,
          recurrence: update.recurrence,
          hiddenByDefault: update.hiddenByDefault,
          projectId: update.projectId,
          updatedAt: context.now,
        );
        await repositories.lockedBlocks.saveBlock(updated);
        logger.debug(() =>
            'Locked block updated. operationId=${context.operationId} lockedBlockId=$lockedBlockId');
        return ApplicationResult.success(updated);
      },
    );
  }

  /// Runs the `archiveLockedBlock` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  Future<ApplicationResult<LockedBlock>> archiveLockedBlock({
    required ApplicationOperationContext context,
    required String lockedBlockId,
  }) {
    return applicationStore.run(
      context: context,
      operationName: ApplicationManagementCommandCode.archiveLockedBlock.name,
      action: (repositories) async {
        logger.debug(() =>
            'Management command executing. command=${ApplicationManagementCommandCode.archiveLockedBlock.name} '
            'operationId=${context.operationId} lockedBlockId=$lockedBlockId');
        final block = await _requireLockedBlock(repositories, lockedBlockId);
        final archived = block.copyWith(
          updatedAt: context.now,
          archivedAt: context.now,
        );
        await repositories.lockedBlocks.saveBlock(archived);
        logger.debug(() =>
            'Locked block archived. operationId=${context.operationId} lockedBlockId=$lockedBlockId');
        return ApplicationResult.success(archived);
      },
    );
  }

  /// Runs the `addOneDayLockedOverride` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  Future<ApplicationResult<LockedBlockOverride>> addOneDayLockedOverride({
    required ApplicationOperationContext context,
    required CivilDate date,
    required String name,
    required ClockTime startTime,
    required ClockTime endTime,
    bool hiddenByDefault = true,
    String? projectId,
  }) {
    return applicationStore.run(
      context: context,
      operationName:
          ApplicationManagementCommandCode.addLockedBlockOverride.name,
      action: (repositories) async {
        logger.debug(() =>
            'Management command executing. command=${ApplicationManagementCommandCode.addLockedBlockOverride.name} '
            'operationId=${context.operationId} date=${date.toIsoString()} hiddenByDefault=$hiddenByDefault '
            'hasProjectId=${projectId != null}');
        final override = LockedBlockOverride.add(
          id: context.idGenerator.nextId(),
          date: date,
          name: name,
          startTime: startTime,
          endTime: endTime,
          hiddenByDefault: hiddenByDefault,
          projectId: projectId,
          createdAt: context.now,
        );
        await repositories.lockedBlocks.saveOverride(override);
        logger.debug(() =>
            'Locked block override added. operationId=${context.operationId} overrideId=${override.id}');
        return ApplicationResult.success(override);
      },
    );
  }

  /// Runs the `removeOneDayLockedOccurrence` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  Future<ApplicationResult<LockedBlockOverride>> removeOneDayLockedOccurrence({
    required ApplicationOperationContext context,
    required String lockedBlockId,
    required CivilDate date,
  }) {
    return applicationStore.run(
      context: context,
      operationName:
          ApplicationManagementCommandCode.removeLockedBlockOccurrence.name,
      action: (repositories) async {
        logger.debug(() =>
            'Management command executing. command=${ApplicationManagementCommandCode.removeLockedBlockOccurrence.name} '
            'operationId=${context.operationId} lockedBlockId=$lockedBlockId date=${date.toIsoString()}');
        await _requireLockedBlock(repositories, lockedBlockId);
        final override = LockedBlockOverride.remove(
          id: context.idGenerator.nextId(),
          lockedBlockId: lockedBlockId,
          date: date,
          createdAt: context.now,
        );
        await repositories.lockedBlocks.saveOverride(override);
        logger.debug(() =>
            'Locked block occurrence removed. operationId=${context.operationId} '
            'lockedBlockId=$lockedBlockId overrideId=${override.id}');
        return ApplicationResult.success(override);
      },
    );
  }

  /// Runs the `replaceOneDayLockedOccurrence` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  Future<ApplicationResult<LockedBlockOverride>> replaceOneDayLockedOccurrence({
    required ApplicationOperationContext context,
    required String lockedBlockId,
    required CivilDate date,
    required ClockTime startTime,
    required ClockTime endTime,
    String? name,
    bool hiddenByDefault = true,
    String? projectId,
  }) {
    return applicationStore.run(
      context: context,
      operationName:
          ApplicationManagementCommandCode.replaceLockedBlockOccurrence.name,
      action: (repositories) async {
        logger.debug(() =>
            'Management command executing. command=${ApplicationManagementCommandCode.replaceLockedBlockOccurrence.name} '
            'operationId=${context.operationId} lockedBlockId=$lockedBlockId date=${date.toIsoString()} '
            'hasName=${name != null} hiddenByDefault=$hiddenByDefault');
        await _requireLockedBlock(repositories, lockedBlockId);
        final override = LockedBlockOverride.replace(
          id: context.idGenerator.nextId(),
          lockedBlockId: lockedBlockId,
          date: date,
          startTime: startTime,
          endTime: endTime,
          name: name,
          hiddenByDefault: hiddenByDefault,
          projectId: projectId,
          createdAt: context.now,
        );
        await repositories.lockedBlocks.saveOverride(override);
        logger.debug(() =>
            'Locked block occurrence replaced. operationId=${context.operationId} '
            'lockedBlockId=$lockedBlockId overrideId=${override.id}');
        return ApplicationResult.success(override);
      },
    );
  }

  /// Runs the `updateOwnerSettings` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  Future<ApplicationResult<OwnerSettingsUpdateResult>> updateOwnerSettings({
    required ApplicationOperationContext context,
    required OwnerSettingsUpdate update,
  }) {
    return applicationStore.run(
      context: context,
      operationName: ApplicationManagementCommandCode.updateOwnerSettings.name,
      action: (repositories) async {
        logger.debug(() =>
            'Management command executing. command=${ApplicationManagementCommandCode.updateOwnerSettings.name} '
            'operationId=${context.operationId} ownerId=${context.ownerTimeZone.ownerId} '
            'hasTimeZone=${update.timeZoneId != null} hasDayStart=${update.dayStart != null} '
            'hasDayEnd=${update.dayEnd != null} hasStaleness=${update.backlogStalenessSettings != null}');
        final current = await _ownerSettingsOrDefault(
          repositories,
          context.ownerTimeZone,
        );
        final stalenessSettings = update.backlogStalenessSettings;
        if (stalenessSettings != null &&
            (stalenessSettings.greenMaxAge.isNegative ||
                stalenessSettings.blueMaxAge.isNegative ||
                stalenessSettings.blueMaxAge < stalenessSettings.greenMaxAge)) {
          logger.warn(() =>
              'Owner settings update failed: invalid staleness thresholds. '
              'operationId=${context.operationId} ownerId=${context.ownerTimeZone.ownerId}');
          return _failure(
            ApplicationFailureCode.validation,
            detailCode: 'invalidBacklogStalenessThresholds',
          );
        }
        final updated = current.copyWith(
          timeZoneId: update.timeZoneId,
          dayStart: update.dayStart,
          dayEnd: update.dayEnd,
          compactModeEnabled: update.compactModeEnabled,
          backlogStalenessSettings: update.backlogStalenessSettings,
        );
        final warnings = <OwnerSettingsWarningCode>[
          if (update.timeZoneId != null &&
              update.timeZoneId != current.timeZoneId)
            OwnerSettingsWarningCode.timeZoneChanged,
          if ((update.dayStart != null &&
                  update.dayStart != current.dayStart) ||
              (update.dayEnd != null && update.dayEnd != current.dayEnd))
            OwnerSettingsWarningCode.planningWindowChanged,
        ];
        await repositories.ownerSettings.save(updated);
        logger.debug(() =>
            'Owner settings updated. operationId=${context.operationId} '
            'ownerId=${context.ownerTimeZone.ownerId} warningCount=${warnings.length}');
        return ApplicationResult.success(
          OwnerSettingsUpdateResult(settings: updated, warnings: warnings),
        );
      },
    );
  }

  /// Runs the `acknowledgeNotice` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  Future<ApplicationResult<NoticeAcknowledgementResult>> acknowledgeNotice({
    required ApplicationOperationContext context,
    required String noticeId,
  }) {
    return applicationStore.run(
      context: context,
      operationName: ApplicationManagementCommandCode.acknowledgeNotice.name,
      action: (repositories) async {
        final ownerId = context.ownerTimeZone.ownerId;
        logger.debug(() =>
            'Management command executing. command=${ApplicationManagementCommandCode.acknowledgeNotice.name} '
            'operationId=${context.operationId} ownerId=$ownerId noticeId=$noticeId');
        final parsed = _parseNoticeId(noticeId);
        if (parsed == null) {
          logger.warn(() => 'Notice acknowledgement failed: invalid notice id. '
              'operationId=${context.operationId} noticeId=$noticeId');
          return _failure(
            ApplicationFailureCode.validation,
            entityId: noticeId,
            detailCode: 'invalidNoticeId',
          );
        }
        final snapshot =
            await repositories.schedulingSnapshots.findById(parsed.snapshotId);
        if (snapshot == null || parsed.noticeIndex >= snapshot.notices.length) {
          logger.warn(() => 'Notice acknowledgement failed: notice not found. '
              'operationId=${context.operationId} noticeId=$noticeId '
              'snapshotId=${parsed.snapshotId} noticeIndex=${parsed.noticeIndex}');
          return _failure(
            ApplicationFailureCode.notFound,
            entityId: noticeId,
            detailCode: 'noticeNotFound',
          );
        }
        final existing = await repositories.noticeAcknowledgements.findById(
          ownerId: ownerId,
          noticeId: noticeId,
        );
        if (existing != null) {
          logger.finer(() =>
              'Notice already acknowledged. operationId=${context.operationId} '
              'noticeId=$noticeId ownerId=$ownerId');
          return ApplicationResult.success(
            NoticeAcknowledgementResult(
              record: existing,
              alreadyAcknowledged: true,
            ),
          );
        }

        final record = NoticeAcknowledgementRecord(
          noticeId: noticeId,
          ownerId: ownerId,
          acknowledgedAt: context.now,
        );
        await repositories.noticeAcknowledgements.save(record);
        logger.debug(
            () => 'Notice acknowledged. operationId=${context.operationId} '
                'noticeId=$noticeId ownerId=$ownerId');
        return ApplicationResult.success(
          NoticeAcknowledgementResult(
            record: record,
            alreadyAcknowledged: false,
          ),
        );
      },
    );
  }

  /// Runs the `_requireProject` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  Future<ProjectProfile> _requireProject(
    ApplicationUnitOfWorkRepositories repositories,
    String projectId,
  ) async {
    final project = await repositories.projects.findById(projectId);
    if (project == null) {
      logger.warn(() => 'Required project not found. projectId=$projectId');
      throw ApplicationFailureException(
        ApplicationFailure(
          code: ApplicationFailureCode.notFound,
          entityId: projectId,
          detailCode: 'projectNotFound',
        ),
      );
    }
    return project;
  }

  /// Runs the `_requireLockedBlock` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  Future<LockedBlock> _requireLockedBlock(
    ApplicationUnitOfWorkRepositories repositories,
    String lockedBlockId,
  ) async {
    final block = await repositories.lockedBlocks.findBlockById(lockedBlockId);
    if (block == null) {
      logger.warn(() =>
          'Required locked block not found. lockedBlockId=$lockedBlockId');
      throw ApplicationFailureException(
        ApplicationFailure(
          code: ApplicationFailureCode.notFound,
          entityId: lockedBlockId,
          detailCode: 'lockedBlockNotFound',
        ),
      );
    }
    return block;
  }
}

/// Top-level helper that performs the `_ownerSettingsOrDefault` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
Future<OwnerSettings> _ownerSettingsOrDefault(
  ApplicationUnitOfWorkRepositories repositories,
  OwnerTimeZoneContext ownerTimeZone,
) async {
  final settings =
      await repositories.ownerSettings.findByOwnerId(ownerTimeZone.ownerId);
  if (settings != null) {
    return settings;
  }
  logger.finer(() => 'Owner settings missing; using defaults. '
      'ownerId=${ownerTimeZone.ownerId} timeZoneId=${ownerTimeZone.timeZoneId}');
  return OwnerSettings(
    ownerId: ownerTimeZone.ownerId,
    timeZoneId: ownerTimeZone.timeZoneId,
  );
}

/// Top-level helper that performs the `_failure<T>` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
ApplicationResult<T> _failure<T>(
  ApplicationFailureCode code, {
  String? entityId,
  String? detailCode,
}) {
  return ApplicationResult.failure(
    ApplicationFailure(
      code: code,
      entityId: entityId,
      detailCode: detailCode,
    ),
  );
}

/// Top-level helper that performs the `_parseNoticeId` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
_ParsedNoticeId? _parseNoticeId(String noticeId) {
  final separator = noticeId.lastIndexOf(':');
  if (separator <= 0 || separator == noticeId.length - 1) {
    return null;
  }
  final noticeIndex = int.tryParse(noticeId.substring(separator + 1));
  if (noticeIndex == null || noticeIndex < 0) {
    return null;
  }
  return _ParsedNoticeId(
    snapshotId: noticeId.substring(0, separator),
    noticeIndex: noticeIndex,
  );
}
