// App-open/start-day recovery orchestration.
//
// This layer makes rollover explicit. Reading Today never runs recovery; a UI or
// app lifecycle boundary calls this use case when it wants to check one source
// day and persist any resulting V1 flexible-task rollover.

import 'application_layer.dart';
import 'locked_time.dart';
import 'models.dart';
import 'repositories.dart';
import 'scheduling_engine.dart';
import 'task_lifecycle.dart';
import 'time_contracts.dart';

/// Stable app-open recovery outcomes.
enum AppOpenRecoveryOutcome {
  rolledOver,
  noUnfinishedFlexibleTasks,
  alreadyProcessed,
}

/// Request for explicit app-open/start-day rollover recovery.
class RunAppOpenRecoveryRequest {
  const RunAppOpenRecoveryRequest({
    required this.context,
    required this.sourceDate,
    this.targetDate,
  });

  /// Atomic operation context.
  final ApplicationOperationContext context;

  /// Owner-local day to inspect for unfinished flexible work.
  final CivilDate sourceDate;

  /// Owner-local target day. Defaults to the next local day.
  final CivilDate? targetDate;

  /// Resolved target day.
  CivilDate get resolvedTargetDate => targetDate ?? sourceDate.addDays(1);
}

/// Result returned by app-open recovery.
class AppOpenRecoveryResult {
  AppOpenRecoveryResult({
    required this.outcome,
    required this.sourceDate,
    required this.targetDate,
    required this.snapshot,
    required List<Task> changedTasks,
    required List<TaskActivity> activities,
    required List<SchedulingNotice> notices,
    required List<SchedulingChange> changes,
  })  : changedTasks = List<Task>.unmodifiable(changedTasks),
        activities = List<TaskActivity>.unmodifiable(activities),
        notices = List<SchedulingNotice>.unmodifiable(notices),
        changes = List<SchedulingChange>.unmodifiable(changes);

  /// High-level deterministic outcome.
  final AppOpenRecoveryOutcome outcome;

  /// Owner-local source day that was checked.
  final CivilDate sourceDate;

  /// Owner-local target day used for placement and notice surfacing.
  final CivilDate targetDate;

  /// Persisted deterministic source-date snapshot.
  final SchedulingStateSnapshot snapshot;

  /// Tasks changed by this operation.
  final List<Task> changedTasks;

  /// Movement activities persisted with changed tasks.
  final List<TaskActivity> activities;

  /// Notices returned to the caller.
  final List<SchedulingNotice> notices;

  /// Machine-readable movement records.
  final List<SchedulingChange> changes;
}

/// Explicit app-open recovery facade.
class V1AppOpenRecoveryUseCases {
  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,
        );
        final existingSnapshot =
            await repositories.schedulingSnapshots.findById(snapshotId);
        if (existingSnapshot != null) {
          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 sourceWindow = _windowForDate(sourceDate, settings);
        final targetWindow = _windowForDate(targetDate, settings);
        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: settings.timeZoneId,
          timeZoneResolver: timeZoneResolver,
          resolutionOptions: resolutionOptions,
        );
        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) {
          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',
        );

        await repositories.tasks.saveAll(changedTasks);
        await repositories.taskActivities.saveAll(activities);
        await repositories.schedulingSnapshots.save(snapshot);

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

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

  DateTime _resolve({
    required CivilDate date,
    required WallTime wallTime,
    required String timeZoneId,
  }) {
    return timeZoneResolver
        .resolve(
          date: date,
          wallTime: wallTime,
          timeZoneId: timeZoneId,
          options: resolutionOptions,
        )
        .instant;
  }
}

Future<OwnerSettings> _ownerSettingsOrDefault(
  ApplicationUnitOfWorkRepositories repositories,
  OwnerTimeZoneContext ownerTimeZone,
) async {
  return await repositories.ownerSettings
          .findByOwnerId(ownerTimeZone.ownerId) ??
      OwnerSettings(
        ownerId: ownerTimeZone.ownerId,
        timeZoneId: ownerTimeZone.timeZoneId,
      );
}

Future<List<Task>> _loadSourceAndTargetTasks(
  ApplicationUnitOfWorkRepositories repositories, {
  required SchedulingWindow sourceWindow,
  required SchedulingWindow targetWindow,
}) async {
  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;
  }
  return List<Task>.unmodifiable(byId.values);
}

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) {
    return null;
  }

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

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

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

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

String _rolloverSnapshotId({
  required String ownerId,
  required CivilDate sourceDate,
}) {
  return 'rollover:$ownerId:${sourceDate.toIsoString()}';
}
