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

part of '../application_recovery.dart';

/// Explicit app-open recovery facade.
class V1AppOpenRecoveryUseCases {
  /// Creates a `V1AppOpenRecoveryUseCases` 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 V1AppOpenRecoveryUseCases({
    required this.applicationStore,
    required this.timeZoneResolver,
    this.resolutionOptions = const TimeZoneResolutionOptions(),
    this.schedulingEngine = const SchedulingEngine(),
  });

  /// Atomic application boundary.
  final ApplicationUnitOfWork applicationStore;

  /// Explicit owner-local date resolver.
  final TimeZoneResolver timeZoneResolver;

  /// DST/nonexistent/repeated local time policy.
  final TimeZoneResolutionOptions resolutionOptions;

  /// Pure scheduling engine.
  final SchedulingEngine schedulingEngine;

  /// Run source-day rollover at most once for one owner/source-date key.
  Future<ApplicationResult<AppOpenRecoveryResult>> runAppOpenRecovery(
    RunAppOpenRecoveryRequest request,
  ) {
    return applicationStore.run(
      context: request.context,
      operationName: 'appOpenRecovery',
      action: (repositories) async {
        final context = request.context;
        final ownerId = context.ownerTimeZone.ownerId;
        final sourceDate = request.sourceDate;
        final targetDate = request.resolvedTargetDate;
        final snapshotId = _rolloverSnapshotId(
          ownerId: ownerId,
          sourceDate: sourceDate,
        );
        logger.info(() => 'App-open recovery started. ownerId=$ownerId '
            'operationId=${context.operationId} sourceDate=${sourceDate.toIsoString()} '
            'targetDate=${targetDate.toIsoString()} snapshotId=$snapshotId');
        final existingSnapshot =
            await repositories.schedulingSnapshots.findById(snapshotId);
        if (existingSnapshot != null) {
          logger.info(() =>
              'App-open recovery already processed. ownerId=$ownerId '
              'operationId=${context.operationId} snapshotId=$snapshotId '
              'noticeCount=${existingSnapshot.notices.length} changeCount=${existingSnapshot.changes.length}');
          return ApplicationResult.success(
            AppOpenRecoveryResult(
              outcome: AppOpenRecoveryOutcome.alreadyProcessed,
              sourceDate: sourceDate,
              targetDate: targetDate,
              snapshot: existingSnapshot,
              changedTasks: const <Task>[],
              activities: const <TaskActivity>[],
              notices: existingSnapshot.notices,
              changes: existingSnapshot.changes,
            ),
          );
        }

        final settings = await _ownerSettingsOrDefault(
          repositories,
          context.ownerTimeZone,
        );
        final effectiveSettings = settings.copyWith(
          timeZoneId: context.ownerTimeZone.timeZoneId,
        );
        final sourceWindow = _windowForDate(sourceDate, effectiveSettings);
        final targetWindow = _windowForDate(targetDate, effectiveSettings);
        final tasks = await _loadSourceAndTargetTasks(
          repositories,
          sourceWindow: sourceWindow,
          targetWindow: targetWindow,
        );
        final blocks = (await repositories.lockedBlocks.findBlocksByOwner(
          ownerId: context.ownerTimeZone.ownerId,
        ))
            .items;
        final overrides = (await repositories.lockedBlocks.findOverridesForDate(
          ownerId: context.ownerTimeZone.ownerId,
          date: targetDate,
        ))
            .items;
        final lockedExpansion = expandLockedBlocksForDay(
          blocks: blocks,
          overrides: overrides,
          date: targetDate,
          timeZoneId: effectiveSettings.timeZoneId,
          timeZoneResolver: timeZoneResolver,
          resolutionOptions: resolutionOptions,
        );
        logger.debug(() => 'App-open recovery loaded source and target state. '
            'ownerId=$ownerId taskCount=${tasks.length} blockCount=${blocks.length} '
            'overrideCount=${overrides.length} '
            'lockedIntervalCount=${lockedExpansion.schedulingIntervals.length}');
        final input = SchedulingInput(
          tasks: tasks,
          window: targetWindow,
          lockedIntervals: lockedExpansion.schedulingIntervals,
        );
        final schedulingResult =
            schedulingEngine.rollOverUnfinishedFlexibleTasks(
          input: input,
          sourceWindow: sourceWindow,
          updatedAt: context.now,
        );

        if (schedulingResult.outcomeCode == SchedulingOutcomeCode.overflow ||
            schedulingResult.outcomeCode == SchedulingOutcomeCode.noSlot) {
          logger.warn(() => 'App-open recovery could not place rollover tasks. '
              'ownerId=$ownerId operationId=${context.operationId} '
              'snapshotId=$snapshotId outcome=${schedulingResult.outcomeCode.name} '
              'noticeCount=${schedulingResult.notices.length}');
          return ApplicationResult.failure(
            ApplicationFailure(
              code: ApplicationFailureCode.noSlot,
              entityId: snapshotId,
              detailCode: schedulingResult.outcomeCode.name,
            ),
          );
        }

        final changedTasks = _changedTasks(tasks, schedulingResult.tasks);
        final activities = _activitiesForRolloverChanges(
          result: schedulingResult,
          beforeTasks: tasks,
          afterTasks: schedulingResult.tasks,
          operationId: context.operationId,
          occurredAt: context.now,
        );
        final rolloverNotice = _rolloverNotice(
          sourceWindow: sourceWindow,
          result: schedulingResult,
        );
        final snapshot = SchedulingStateSnapshot(
          id: snapshotId,
          capturedAt: context.now,
          window: targetWindow,
          tasks: schedulingResult.tasks,
          lockedIntervals: input.lockedIntervals,
          notices: [
            if (rolloverNotice != null) rolloverNotice,
          ],
          changes: schedulingResult.changes,
          operationName: 'appOpenRecovery',
        );

        logger.debug(() => 'Persisting app-open recovery. ownerId=$ownerId '
            'operationId=${context.operationId} snapshotId=$snapshotId '
            'changedTaskCount=${changedTasks.length} activityCount=${activities.length} '
            'noticeCount=${snapshot.notices.length} changeCount=${schedulingResult.changes.length}');
        await repositories.tasks.saveAll(changedTasks);
        await repositories.taskActivities.saveAll(activities);
        await repositories.schedulingSnapshots.save(snapshot);
        logger.info(() => 'App-open recovery completed. ownerId=$ownerId '
            'operationId=${context.operationId} outcome=${rolloverNotice == null ? AppOpenRecoveryOutcome.noUnfinishedFlexibleTasks.name : AppOpenRecoveryOutcome.rolledOver.name} '
            'changedTaskCount=${changedTasks.length} activityCount=${activities.length}');

        return ApplicationResult.success(
          AppOpenRecoveryResult(
            outcome: rolloverNotice == null
                ? AppOpenRecoveryOutcome.noUnfinishedFlexibleTasks
                : AppOpenRecoveryOutcome.rolledOver,
            sourceDate: sourceDate,
            targetDate: targetDate,
            snapshot: snapshot,
            changedTasks: changedTasks,
            activities: activities,
            notices: snapshot.notices,
            changes: schedulingResult.changes,
          ),
        );
      },
    );
  }

  /// Runs the `_windowForDate` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  SchedulingWindow _windowForDate(CivilDate date, OwnerSettings settings) {
    return SchedulingWindow(
      start: _resolve(
        date: date,
        wallTime: settings.dayStart,
        timeZoneId: settings.timeZoneId,
      ),
      end: _resolve(
        date: date,
        wallTime: settings.dayEnd,
        timeZoneId: settings.timeZoneId,
      ),
    );
  }

  /// Runs the `_resolve` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  DateTime _resolve({
    required CivilDate date,
    required WallTime wallTime,
    required String timeZoneId,
  }) {
    return timeZoneResolver
        .resolve(
          date: date,
          wallTime: wallTime,
          timeZoneId: timeZoneId,
          options: resolutionOptions,
        )
        .instant;
  }
}

/// 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 during recovery; using defaults. '
      'ownerId=${ownerTimeZone.ownerId} timeZoneId=${ownerTimeZone.timeZoneId}');
  return OwnerSettings(
    ownerId: ownerTimeZone.ownerId,
    timeZoneId: ownerTimeZone.timeZoneId,
  );
}

/// Top-level helper that performs the `_loadSourceAndTargetTasks` 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<List<Task>> _loadSourceAndTargetTasks(
  ApplicationUnitOfWorkRepositories repositories, {
  required SchedulingWindow sourceWindow,
  required SchedulingWindow targetWindow,
}) async {
  logger.debug(() => 'Loading recovery source and target tasks. '
      'sourceStart=${sourceWindow.start.toIso8601String()} '
      'sourceEnd=${sourceWindow.end.toIso8601String()} '
      'targetStart=${targetWindow.start.toIso8601String()} '
      'targetEnd=${targetWindow.end.toIso8601String()}');
  final byId = <String, Task>{};
  for (final task in await repositories.tasks.findScheduledInWindow(
    sourceWindow,
  )) {
    byId[task.id] = task;
  }
  for (final task in await repositories.tasks.findScheduledInWindow(
    targetWindow,
  )) {
    byId[task.id] = task;
  }
  logger.debug(() =>
      'Recovery source and target tasks loaded. taskCount=${byId.length}');
  return List<Task>.unmodifiable(byId.values);
}

/// Top-level helper that performs the `_rolloverNotice` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
SchedulingNotice? _rolloverNotice({
  required SchedulingWindow sourceWindow,
  required SchedulingResult result,
}) {
  if (result.outcomeCode != SchedulingOutcomeCode.success) {
    return null;
  }
  final rolledTaskIds = result.changes
      .where(
        (change) =>
            change.previousStart != null &&
            change.previousEnd != null &&
            sourceWindow.contains(
              TimeInterval(
                  start: change.previousStart!, end: change.previousEnd!),
            ),
      )
      .map((change) => change.taskId)
      .toSet();
  if (rolledTaskIds.isEmpty) {
    logger.finer(
        () => 'No rollover notice needed; no tasks moved from source day.');
    return null;
  }
  logger.debug(
      () => 'Rollover notice created. rolledTaskCount=${rolledTaskIds.length}');

  return SchedulingNotice(
    '${rolledTaskIds.length} unfinished flexible tasks were moved to today.',
    type: SchedulingNoticeType.moved,
    movementCode: SchedulingMovementCode.unfinishedFlexibleTasksRolledOver,
    parameters: {
      'count': rolledTaskIds.length,
      'taskIds': List<String>.unmodifiable(rolledTaskIds),
    },
  );
}

/// Top-level helper that performs the `_activitiesForRolloverChanges` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
List<TaskActivity> _activitiesForRolloverChanges({
  required SchedulingResult result,
  required List<Task> beforeTasks,
  required List<Task> afterTasks,
  required String operationId,
  required DateTime occurredAt,
}) {
  if (result.outcomeCode != SchedulingOutcomeCode.success) {
    return const <TaskActivity>[];
  }
  final beforeById = {
    for (final task in beforeTasks) task.id: task,
  };
  final afterById = {
    for (final task in afterTasks) task.id: task,
  };
  final activities = <TaskActivity>[];

  for (final change in result.changes) {
    final before = beforeById[change.taskId];
    final after = afterById[change.taskId];
    if (before == null || after == null) {
      continue;
    }
    activities.add(
      TaskActivity(
        id: '$operationId:${change.taskId}:automaticPush',
        operationId: operationId,
        code: TaskActivityCode.automaticallyPushed,
        taskId: change.taskId,
        projectId: before.projectId,
        occurredAt: occurredAt,
        metadata: {
          'previousStatus': before.status.name,
          'nextStatus': after.status.name,
          'previousStart': change.previousStart,
          'previousEnd': change.previousEnd,
          'nextStart': change.nextStart,
          'nextEnd': change.nextEnd,
        },
      ),
    );
  }

  return List<TaskActivity>.unmodifiable(activities);
}

/// Top-level helper that performs the `_changedTasks` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
List<Task> _changedTasks(List<Task> beforeTasks, List<Task> afterTasks) {
  final beforeById = {
    for (final task in beforeTasks) task.id: task,
  };
  final changed = <Task>[];

  for (final task in afterTasks) {
    final before = beforeById[task.id];
    if (before == null || _taskChanged(before, task)) {
      changed.add(task);
    }
  }

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

/// Top-level helper that performs the `_taskChanged` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
bool _taskChanged(Task before, Task after) {
  return before.status != after.status ||
      before.scheduledStart != after.scheduledStart ||
      before.scheduledEnd != after.scheduledEnd ||
      before.updatedAt != after.updatedAt ||
      before.stats != after.stats;
}

/// Top-level helper that performs the `_rolloverSnapshotId` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
String _rolloverSnapshotId({
  required String ownerId,
  required CivilDate sourceDate,
}) {
  return 'rollover:$ownerId:${sourceDate.toIsoString()}';
}
