// 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(),
    this.timeZoneResolver = const FixedOffsetTimeZoneResolver.utc(),
    this.resolutionOptions = const TimeZoneResolutionOptions(),
    this.backlogBoardBucketPolicy = const BacklogBoardBucketPolicy(),
  });

  /// Atomic application boundary.
  final ApplicationUnitOfWork applicationStore;

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

  /// Resolver used for read-only planning previews.
  final TimeZoneResolver timeZoneResolver;

  /// DST/nonexistent/repeated local time policy for read-only previews.
  final TimeZoneResolutionOptions resolutionOptions;

  /// Canonical board bucket policy used by Backlog board reads.
  final BacklogBoardBucketPolicy backlogBoardBucketPolicy;

  /// 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 the Backlog board projection without mutating repositories.
  Future<ApplicationResult<BacklogBoardQueryResult>> getBacklogBoard(
    GetBacklogBoardRequest request,
  ) {
    return applicationStore.read(
      action: (repositories) async {
        final context = request.context;
        final ownerId = context.ownerTimeZone.ownerId;
        logger.debug(() => 'Loading backlog board. ownerId=$ownerId '
            'localDate=${request.localDate.toIsoString()} '
            'filterCount=${request.filters.length} sortKey=${request.sortKey.name} '
            'groupMode=${request.groupMode.name}');
        final settings = await _ownerSettingsOrDefault(
          repositories,
          context.ownerTimeZone,
        );
        final projects = await _loadActiveProjects(repositories, ownerId);
        final projectById = {
          for (final project in projects) project.id: project,
        };
        final candidates = await _loadBacklogCandidates(repositories, ownerId);
        final filtered = _filterBacklogTasks(
          tasks: candidates,
          request: request,
          settings: settings,
        );
        final searched = _searchBacklogTasks(
          tasks: filtered,
          searchText: request.searchText,
          projectById: projectById,
        );
        final sorted = _sortBoardTasks(
          tasks: searched,
          request: request,
          settings: settings,
          projectById: projectById,
        );
        final childrenByParentId = await _loadChildrenByParentId(
          repositories: repositories,
          ownerId: ownerId,
          parentTasks: sorted,
        );
        final items = [
          for (final task in sorted)
            _boardItemForTask(
              task: task,
              projectById: projectById,
              childTasks: childrenByParentId[task.id] ?? const <Task>[],
            ),
        ];

        logger.debug(() => 'Backlog board loaded. ownerId=$ownerId '
            'candidateCount=${candidates.length} itemCount=${items.length}');
        return ApplicationResult.success(
          BacklogBoardQueryResult(
            ownerId: ownerId,
            localDate: request.localDate,
            summary: _summaryForBoardItems(items),
            columns: _columnsForBoardItems(items),
            projectOptions: _projectOptionsFor(projects),
          ),
        );
      },
    );
  }

  /// Load one Backlog task drawer projection without mutating repositories.
  Future<ApplicationResult<BacklogTaskDetailReadModel>> getBacklogTaskDetail(
    GetBacklogTaskDetailRequest request,
  ) {
    return applicationStore.read(
      action: (repositories) async {
        final context = request.context;
        final ownerId = context.ownerTimeZone.ownerId;
        logger.debug(() => 'Loading backlog task detail. ownerId=$ownerId '
            'taskId=${request.taskId} localDate=${request.localDate.toIsoString()}');
        final record = await repositories.tasks.findRecordById(request.taskId);
        if (record == null ||
            record.ownerId != ownerId ||
            !record.value.isBacklog) {
          logger.warn(() => 'Backlog task detail not found. '
              'ownerId=$ownerId taskId=${request.taskId}');
          return _failure(
            ApplicationFailureCode.notFound,
            entityId: request.taskId,
          );
        }
        final task = record.value;
        final projects = await _loadActiveProjects(repositories, ownerId);
        final projectById = {
          for (final project in projects) project.id: project,
        };
        final childrenByParentId = await _loadChildrenByParentId(
          repositories: repositories,
          ownerId: ownerId,
          parentTasks: [task],
        );
        final item = _boardItemForTask(
          task: task,
          projectById: projectById,
          childTasks: childrenByParentId[task.id] ?? const <Task>[],
        );
        final suggestions = await _suggestedSlotsForTask(
          repositories: repositories,
          context: context,
          localDate: request.localDate,
          task: task,
        );

        logger.debug(() => 'Backlog task detail loaded. ownerId=$ownerId '
            'taskId=${task.id} suggestedSlotCount=${suggestions.length}');
        return ApplicationResult.success(
          BacklogTaskDetailReadModel(
            item: item,
            notes: null,
            addedLabel: _addedLabelFor(task, context.now),
            tags: _readOnlyTagLabelsFor(task),
            projectOptions: _projectOptionsFor(projects),
            suggestedSlots: suggestions,
            suggestedSlotUnavailableReason:
                _suggestedSlotUnavailableReasonFor(task, suggestions),
          ),
        );
      },
    );
  }

  /// Load all active owner projects used by Backlog board reads.
  Future<List<ProjectProfile>> _loadActiveProjects(
    ApplicationUnitOfWorkRepositories repositories,
    String ownerId,
  ) async {
    final projects = <ProjectProfile>[];
    String? cursor;
    do {
      final page = await repositories.projects.findByOwner(
        ownerId: ownerId,
        page: RepositoryPageRequest(cursor: cursor, limit: 200),
      );
      projects.addAll(page.items);
      cursor = page.nextCursor;
    } while (cursor != null);

    projects.sort((a, b) => a.name.compareTo(b.name));
    return List<ProjectProfile>.unmodifiable(projects);
  }

  /// Load all owner-scoped backlog candidates for the board.
  Future<List<Task>> _loadBacklogCandidates(
    ApplicationUnitOfWorkRepositories repositories,
    String ownerId,
  ) async {
    final tasks = <Task>[];
    String? cursor;
    do {
      final page = await repositories.tasks.findBacklogCandidates(
        BacklogCandidateQuery(
          ownerId: ownerId,
          page: RepositoryPageRequest(cursor: cursor, limit: 200),
        ),
      );
      tasks.addAll(page.items);
      cursor = page.nextCursor;
    } while (cursor != null);

    return List<Task>.unmodifiable(tasks);
  }

  /// Applies request filters using the existing Backlog view policy.
  List<Task> _filterBacklogTasks({
    required List<Task> tasks,
    required GetBacklogBoardRequest request,
    required OwnerSettings settings,
  }) {
    var filtered = tasks;
    for (final filter in request.filters) {
      filtered = BacklogView(
        tasks: filtered,
        now: request.context.now,
        stalenessSettings: settings.backlogStalenessSettings,
      ).filter(filter);
    }
    return List<Task>.unmodifiable(filtered);
  }

  /// Applies case-insensitive text search over task title, project, and derived tags.
  List<Task> _searchBacklogTasks({
    required List<Task> tasks,
    required String? searchText,
    required Map<String, ProjectProfile> projectById,
  }) {
    final query = searchText?.trim().toLowerCase();
    if (query == null || query.isEmpty) {
      return tasks;
    }
    return List<Task>.unmodifiable(
      tasks.where((task) {
        final project = projectById[task.projectId];
        final haystack = [
          task.title,
          task.projectId,
          if (project != null) project.name,
          ..._readOnlyTagLabelsFor(task),
        ].join(' ').toLowerCase();
        return haystack.contains(query);
      }),
    );
  }

  /// Sorts board tasks using the requested sort and optional project grouping.
  List<Task> _sortBoardTasks({
    required List<Task> tasks,
    required GetBacklogBoardRequest request,
    required OwnerSettings settings,
    required Map<String, ProjectProfile> projectById,
  }) {
    final sorted = BacklogView(
      tasks: tasks,
      now: request.context.now,
      stalenessSettings: settings.backlogStalenessSettings,
    ).sorted(request.sortKey);
    if (request.groupMode == BacklogBoardGroupMode.none) {
      return sorted;
    }

    final indexed = sorted.asMap().entries.toList(growable: false);
    indexed.sort((a, b) {
      final projectComparison = _projectNameFor(
        a.value.projectId,
        projectById,
      ).compareTo(
        _projectNameFor(b.value.projectId, projectById),
      );
      if (projectComparison != 0) {
        return projectComparison;
      }
      return a.key.compareTo(b.key);
    });
    return List<Task>.unmodifiable(indexed.map((entry) => entry.value));
  }

  /// Loads direct child task previews for [parentTasks].
  Future<Map<String, List<Task>>> _loadChildrenByParentId({
    required ApplicationUnitOfWorkRepositories repositories,
    required String ownerId,
    required List<Task> parentTasks,
  }) async {
    final result = <String, List<Task>>{};
    for (final parent in parentTasks) {
      final children = <Task>[];
      String? cursor;
      do {
        final page = await repositories.tasks.findByParentTaskId(
          ownerId: ownerId,
          parentTaskId: parent.id,
          page: RepositoryPageRequest(cursor: cursor, limit: 50),
        );
        children.addAll(page.items);
        cursor = page.nextCursor;
      } while (cursor != null);
      children.sort((a, b) => a.createdAt.compareTo(b.createdAt));
      result[parent.id] = List<Task>.unmodifiable(children);
    }
    return Map<String, List<Task>>.unmodifiable(result);
  }

  /// Maps one task into its board card read model.
  BacklogBoardItemReadModel _boardItemForTask({
    required Task task,
    required Map<String, ProjectProfile> projectById,
    required List<Task> childTasks,
  }) {
    final resolution = backlogBoardBucketPolicy.resolve(
      task: task,
      hasChildTasks: childTasks.isNotEmpty,
    );
    return BacklogBoardItemReadModel(
      taskId: task.id,
      title: task.title,
      subtitle: null,
      projectId: task.projectId,
      projectName: _projectNameFor(task.projectId, projectById),
      projectColorToken: _projectColorTokenFor(task.projectId, projectById),
      durationMinutes: task.durationMinutes,
      priority: task.priority,
      reward: task.reward,
      difficulty: task.difficulty,
      bucket: resolution.bucket,
      bucketReason: resolution.reason,
      createdAt: task.createdAt,
      updatedAt: task.updatedAt,
      hasChildren: childTasks.isNotEmpty,
      childPreview: [
        for (final child in childTasks.take(3))
          BacklogChildPreviewReadModel(
            taskId: child.id,
            title: child.title,
            durationMinutes: child.durationMinutes,
            isCompleted: child.status == TaskStatus.completed,
            statusLabel: _taskStatusLabel(child.status),
          ),
      ],
      tags: _readOnlyTagLabelsFor(task),
    );
  }

  /// Builds the stable four-column board from [items].
  List<BacklogBoardColumnReadModel> _columnsForBoardItems(
    List<BacklogBoardItemReadModel> items,
  ) {
    return [
      for (final bucket in _boardBucketOrder)
        BacklogBoardColumnReadModel(
          bucket: bucket,
          title: _bucketTitle(bucket),
          subtitle: _bucketSubtitle(bucket),
          accentToken: _bucketAccentToken(bucket),
          items: [
            for (final item in items)
              if (item.bucket == bucket) item,
          ],
        ),
    ];
  }

  /// Builds summary counts from the same filtered board [items].
  BacklogBoardSummaryReadModel _summaryForBoardItems(
    List<BacklogBoardItemReadModel> items,
  ) {
    final totalEstimatedMinutes = items.fold<int>(
      0,
      (sum, item) => sum + (item.durationMinutes ?? 0),
    );
    return BacklogBoardSummaryReadModel(
      totalTaskCount: items.length,
      totalEstimatedMinutes: totalEstimatedMinutes,
      priorityDistribution: _priorityDistributionFor(items),
      projectDistribution: _projectDistributionFor(items),
      durationDistribution: _durationDistributionFor(items),
      planningTip: _planningTipFor(items),
    );
  }

  /// Builds project picker options for the board and drawer.
  List<ProjectOptionReadModel> _projectOptionsFor(
    List<ProjectProfile> projects,
  ) {
    return List<ProjectOptionReadModel>.unmodifiable(
      projects.map(
        (project) => ProjectOptionReadModel(
          projectId: project.id,
          name: project.name,
          colorToken: project.colorKey,
          isArchived: project.isArchived,
        ),
      ),
    );
  }

  /// Builds non-mutating suggested slots for [task].
  Future<List<SuggestedSlotReadModel>> _suggestedSlotsForTask({
    required ApplicationUnitOfWorkRepositories repositories,
    required ApplicationOperationContext context,
    required CivilDate localDate,
    required Task task,
  }) async {
    final durationMinutes = task.durationMinutes;
    if (durationMinutes == null) {
      return const <SuggestedSlotReadModel>[];
    }
    final ownerId = context.ownerTimeZone.ownerId;
    final settings = await _ownerSettingsOrDefault(
      repositories,
      context.ownerTimeZone,
    );
    final window = _planningWindowFor(
      date: localDate,
      settings: settings.copyWith(timeZoneId: context.ownerTimeZone.timeZoneId),
    );
    final lockedExpansion = expandLockedBlocksForDay(
      blocks: (await repositories.lockedBlocks.findBlocksByOwner(
        ownerId: ownerId,
      ))
          .items,
      overrides: (await repositories.lockedBlocks.findOverridesForDate(
        ownerId: ownerId,
        date: localDate,
      ))
          .items,
      date: localDate,
      timeZoneId: context.ownerTimeZone.timeZoneId,
      timeZoneResolver: timeZoneResolver,
      resolutionOptions: resolutionOptions,
    );
    final tasks = (await repositories.tasks.findForLocalDay(
      ownerId: ownerId,
      localDate: localDate,
      window: window,
    ))
        .items;
    final input = SchedulingInput(
      tasks: tasks,
      window: window,
      lockedIntervals: lockedExpansion.schedulingIntervals,
    );
    final duration = Duration(minutes: durationMinutes);
    final windowStart = _previewStartFor(window, context.now);
    final primary = const SchedulingEngine().findFirstOpenInterval(
      windowStart: windowStart,
      windowEnd: window.end,
      duration: duration,
      blocked: input.blockedIntervals,
    );
    if (primary == null) {
      return const <SuggestedSlotReadModel>[];
    }
    final localDateLabel = _localDateLabelFor(localDate, window, context.now);
    final slots = <SuggestedSlotReadModel>[
      _suggestedSlotReadModel(
        id: '${task.id}:primary',
        interval: primary,
        localDateLabel: localDateLabel,
        durationMinutes: durationMinutes,
        fitLabel: 'Great Fit',
        fitSeverityToken: 'fit-great',
        isPrimary: true,
      ),
    ];
    final secondary = const SchedulingEngine().findFirstOpenInterval(
      windowStart: primary.end,
      windowEnd: window.end,
      duration: duration,
      blocked: input.blockedIntervals,
    );
    if (secondary != null && secondary.start != primary.start) {
      slots.add(
        _suggestedSlotReadModel(
          id: '${task.id}:secondary',
          interval: secondary,
          localDateLabel: localDateLabel,
          durationMinutes: durationMinutes,
          fitLabel: 'Okay',
          fitSeverityToken: 'fit-okay',
          isPrimary: false,
        ),
      );
    }
    return List<SuggestedSlotReadModel>.unmodifiable(slots);
  }

  /// Resolves the owner-local planning window for [date].
  SchedulingWindow _planningWindowFor({
    required CivilDate date,
    required OwnerSettings settings,
  }) {
    return SchedulingWindow(
      start: _resolve(
        date: date,
        wallTime: settings.dayStart,
        timeZoneId: settings.timeZoneId,
      ),
      end: _resolve(
        date: date,
        wallTime: settings.dayEnd,
        timeZoneId: settings.timeZoneId,
      ),
    );
  }

  /// Resolves [wallTime] on [date] through the configured preview resolver.
  DateTime _resolve({
    required CivilDate date,
    required WallTime wallTime,
    required String timeZoneId,
  }) {
    return timeZoneResolver
        .resolve(
          date: date,
          wallTime: wallTime,
          timeZoneId: timeZoneId,
          options: resolutionOptions,
        )
        .instant;
  }

  /// 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;
  }
}

/// Canonical board bucket order for the V1 Backlog board.
const _boardBucketOrder = <BacklogBoardBucket>[
  BacklogBoardBucket.doNext,
  BacklogBoardBucket.needTimeBlock,
  BacklogBoardBucket.breakUpFirst,
  BacklogBoardBucket.notNow,
];

/// Resolves the display title for [bucket].
String _bucketTitle(BacklogBoardBucket bucket) {
  return switch (bucket) {
    BacklogBoardBucket.doNext => 'Do Next',
    BacklogBoardBucket.needTimeBlock => 'Need Time Block',
    BacklogBoardBucket.breakUpFirst => 'Break Up First',
    BacklogBoardBucket.notNow => 'Not Now',
  };
}

/// Resolves the display subtitle for [bucket].
String _bucketSubtitle(BacklogBoardBucket bucket) {
  return switch (bucket) {
    BacklogBoardBucket.doNext => 'Ready when you have a small opening.',
    BacklogBoardBucket.needTimeBlock => 'Give these a protected block.',
    BacklogBoardBucket.breakUpFirst => 'Split these before scheduling.',
    BacklogBoardBucket.notNow => 'Keep these out of today pressure.',
  };
}

/// Resolves the UI accent token for [bucket].
String _bucketAccentToken(BacklogBoardBucket bucket) {
  return switch (bucket) {
    BacklogBoardBucket.doNext => 'backlog-do-next',
    BacklogBoardBucket.needTimeBlock => 'backlog-time-block',
    BacklogBoardBucket.breakUpFirst => 'backlog-break-up',
    BacklogBoardBucket.notNow => 'backlog-not-now',
  };
}

/// Resolves the display project name for [projectId].
String _projectNameFor(
  String projectId,
  Map<String, ProjectProfile> projectById,
) {
  return projectById[projectId]?.name ?? _fallbackProjectName(projectId);
}

/// Resolves the UI project color token for [projectId].
String _projectColorTokenFor(
  String projectId,
  Map<String, ProjectProfile> projectById,
) {
  return projectById[projectId]?.colorKey ?? 'project-$projectId';
}

/// Builds a readable project fallback from a stable project id.
String _fallbackProjectName(String projectId) {
  final words = projectId
      .split(RegExp('[-_]'))
      .where((word) => word.trim().isNotEmpty)
      .toList(growable: false);
  if (words.isEmpty) {
    return projectId;
  }
  return words.map(_capitalized).join(' ');
}

/// Capitalizes [value] for fallback display labels.
String _capitalized(String value) {
  if (value.isEmpty) {
    return value;
  }
  return value.substring(0, 1).toUpperCase() + value.substring(1);
}

/// Resolves a compact task status label.
String _taskStatusLabel(TaskStatus status) {
  return switch (status) {
    TaskStatus.backlog => 'Backlog',
    TaskStatus.planned => 'Planned',
    TaskStatus.active => 'Active',
    TaskStatus.completed => 'Completed',
    TaskStatus.cancelled => 'Archived',
    TaskStatus.missed => 'Missed',
    TaskStatus.noLongerRelevant => 'Archived',
  };
}

/// Builds read-only tag labels from existing narrow backlog flags.
List<String> _readOnlyTagLabelsFor(Task task) {
  final labels = [
    for (final tag in task.backlogTags)
      switch (tag) {
        BacklogTag.wishlist => 'someday',
      },
  ]..sort();
  return List<String>.unmodifiable(labels);
}

/// Builds the priority distribution for the summary panel.
BacklogDistributionReadModel _priorityDistributionFor(
  List<BacklogBoardItemReadModel> items,
) {
  final priorities = <PriorityLevel?>[
    PriorityLevel.veryHigh,
    PriorityLevel.high,
    PriorityLevel.medium,
    PriorityLevel.low,
    PriorityLevel.veryLow,
    null,
  ];
  final entries = <BacklogDistributionEntryReadModel>[];
  for (final priority in priorities) {
    final count = items.where((item) => item.priority == priority).length;
    if (count == 0) {
      continue;
    }
    entries.add(
      BacklogDistributionEntryReadModel(
        id: priority?.name ?? 'unset',
        label: _priorityLabel(priority),
        count: count,
        percentage: _percentage(count, items.length),
        colorToken: priority == null ? null : 'priority-${priority.name}',
      ),
    );
  }
  return BacklogDistributionReadModel(title: 'Priority', entries: entries);
}

/// Builds the project distribution for the summary panel.
BacklogDistributionReadModel _projectDistributionFor(
  List<BacklogBoardItemReadModel> items,
) {
  final projectIds = items.map((item) => item.projectId).toSet().toList()
    ..sort((a, b) {
      final left = items.firstWhere((item) => item.projectId == a).projectName;
      final right = items.firstWhere((item) => item.projectId == b).projectName;
      return left.compareTo(right);
    });
  final entries = [
    for (final projectId in projectIds)
      BacklogDistributionEntryReadModel(
        id: projectId,
        label:
            items.firstWhere((item) => item.projectId == projectId).projectName,
        count: items.where((item) => item.projectId == projectId).length,
        percentage: _percentage(
          items.where((item) => item.projectId == projectId).length,
          items.length,
        ),
        colorToken: items
            .firstWhere((item) => item.projectId == projectId)
            .projectColorToken,
      ),
  ];
  return BacklogDistributionReadModel(title: 'Project', entries: entries);
}

/// Builds the duration distribution for the summary panel.
BacklogDistributionReadModel _durationDistributionFor(
  List<BacklogBoardItemReadModel> items,
) {
  final short = items.where((item) {
    final duration = item.durationMinutes;
    return duration != null && duration <= 30;
  }).length;
  final medium = items.where((item) {
    final duration = item.durationMinutes;
    return duration != null && duration > 30 && duration <= 60;
  }).length;
  final long = items.where((item) {
    final duration = item.durationMinutes;
    return duration != null && duration > 60 && duration <= 120;
  }).length;
  final veryLong = items.where((item) {
    final duration = item.durationMinutes;
    return duration != null && duration > 120;
  }).length;
  final missing = items.where((item) => item.durationMinutes == null).length;
  final entries = <BacklogDistributionEntryReadModel>[
    BacklogDistributionEntryReadModel(
      id: '0-30',
      label: '0-30',
      count: short,
      percentage: _percentage(short, items.length),
    ),
    BacklogDistributionEntryReadModel(
      id: '30-60',
      label: '30-60',
      count: medium,
      percentage: _percentage(medium, items.length),
    ),
    BacklogDistributionEntryReadModel(
      id: '60-120',
      label: '60-120',
      count: long,
      percentage: _percentage(long, items.length),
    ),
    BacklogDistributionEntryReadModel(
      id: '120+',
      label: '120+',
      count: veryLong,
      percentage: _percentage(veryLong, items.length),
    ),
    if (missing > 0)
      BacklogDistributionEntryReadModel(
        id: 'no-estimate',
        label: 'No estimate',
        count: missing,
        percentage: _percentage(missing, items.length),
      ),
  ];
  return BacklogDistributionReadModel(title: 'Duration', entries: entries);
}

/// Resolves a display label for [priority].
String _priorityLabel(PriorityLevel? priority) {
  return switch (priority) {
    PriorityLevel.veryHigh => 'Very High',
    PriorityLevel.high => 'High',
    PriorityLevel.medium => 'Medium',
    PriorityLevel.low => 'Low',
    PriorityLevel.veryLow => 'Very Low',
    null => 'Unset',
  };
}

/// Calculates a distribution percentage.
double _percentage(int count, int total) {
  if (total == 0) {
    return 0;
  }
  return count / total;
}

/// Resolves the summary planning tip from the current board shape.
String _planningTipFor(List<BacklogBoardItemReadModel> items) {
  if (items.isEmpty) {
    return 'Backlog is clear.';
  }
  if (items.any((item) => item.bucket == BacklogBoardBucket.breakUpFirst)) {
    return 'Break up one large task before adding more to today.';
  }
  if (items.any((item) => item.bucket == BacklogBoardBucket.needTimeBlock)) {
    return 'Protect one focused block before choosing smaller tasks.';
  }
  return 'Pick one Do Next task that fits your current energy.';
}

/// Picks the preview search start within [window].
DateTime _previewStartFor(SchedulingWindow window, DateTime now) {
  if (now.isAfter(window.start) && now.isBefore(window.end)) {
    return now;
  }
  if (now.isAfter(window.end) || now.isAtSameMomentAs(window.end)) {
    return window.end;
  }
  return window.start;
}

/// Builds the display label for a suggested slot date.
String _localDateLabelFor(
  CivilDate localDate,
  SchedulingWindow window,
  DateTime now,
) {
  if ((now.isAfter(window.start) || now.isAtSameMomentAs(window.start)) &&
      now.isBefore(window.end)) {
    return 'Today';
  }
  return localDate.toIsoString();
}

/// Builds one suggested slot read model from [interval].
SuggestedSlotReadModel _suggestedSlotReadModel({
  required String id,
  required TimeInterval interval,
  required String localDateLabel,
  required int durationMinutes,
  required String fitLabel,
  required String fitSeverityToken,
  required bool isPrimary,
}) {
  return SuggestedSlotReadModel(
    id: id,
    localDateLabel: localDateLabel,
    startText: _clockText(interval.start),
    endText: _clockText(interval.end),
    durationMinutes: durationMinutes,
    fitLabel: fitLabel,
    fitSeverityToken: fitSeverityToken,
    isPrimary: isPrimary,
  );
}

/// Formats [instant] as compact 12-hour clock text.
String _clockText(DateTime instant) {
  final hour12 = instant.hour % 12 == 0 ? 12 : instant.hour % 12;
  final minute = instant.minute.toString().padLeft(2, '0');
  final period = instant.hour < 12 ? 'AM' : 'PM';
  return '$hour12:$minute $period';
}

/// Returns a display reason when suggested slots are unavailable.
String? _suggestedSlotUnavailableReasonFor(
  Task task,
  List<SuggestedSlotReadModel> suggestions,
) {
  if (task.durationMinutes == null) {
    return 'Add an estimate to see suggested slots.';
  }
  if (suggestions.isEmpty) {
    return 'No open slot fits this estimate today.';
  }
  return null;
}

/// Builds the drawer added-age label for [task].
String _addedLabelFor(Task task, DateTime now) {
  final days = now.difference(task.backlogFreshnessAnchorAt).inDays;
  if (days <= 0) {
    return 'Added today';
  }
  if (days == 1) {
    return 'Added yesterday';
  }
  return 'Added $days days ago';
}

/// 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,
  );
}
