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

part of '../../today_state.dart';

/// Builds Today state from application repositories.
class GetTodayStateQuery {
  /// Creates a `GetTodayStateQuery` 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 GetTodayStateQuery({
    required this.applicationStore,
    required this.timeZoneResolver,
    this.resolutionOptions = const TimeZoneResolutionOptions(),
  });

  /// Read boundary for repository access.
  final ApplicationUnitOfWork applicationStore;

  /// Explicit local-time resolver.
  final TimeZoneResolver timeZoneResolver;

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

  /// Execute the read-only Today query.
  Future<ApplicationResult<TodayState>> execute(
    GetTodayStateRequest request,
  ) {
    return applicationStore.read(
      action: (repositories) async {
        final context = request.context;
        final ownerId = context.ownerTimeZone.ownerId;
        logger.debug(() => 'Today state query started. '
            'ownerId=$ownerId date=${request.date.toIsoString()} '
            'revealHiddenLockedBlocks=${request.revealHiddenLockedBlocks} '
            'includeNextFlexibleItem=${request.includeNextFlexibleItem}');
        final storedSettings =
            await repositories.ownerSettings.findByOwnerId(ownerId);
        final settings = storedSettings ??
            OwnerSettings(
              ownerId: ownerId,
              timeZoneId: context.ownerTimeZone.timeZoneId,
            );
        if (storedSettings == null) {
          logger.finer(() =>
              'Today state query owner settings missing; using defaults. '
              'ownerId=$ownerId timeZoneId=${context.ownerTimeZone.timeZoneId}');
        }
        final timeZoneId = context.ownerTimeZone.timeZoneId;
        final effectiveSettings = settings.copyWith(timeZoneId: timeZoneId);
        final dayStart = _resolve(
          date: request.date,
          wallTime: effectiveSettings.dayStart,
          timeZoneId: timeZoneId,
        );
        final dayEnd = _resolve(
          date: request.date,
          wallTime: effectiveSettings.dayEnd,
          timeZoneId: timeZoneId,
        );
        final window = SchedulingWindow(start: dayStart, end: dayEnd);

        final projects = (await repositories.projects.findByOwner(
          ownerId: ownerId,
          includeArchived: true,
        ))
            .items;
        final tasks = (await repositories.tasks.findForLocalDay(
          ownerId: ownerId,
          localDate: request.date,
          window: window,
        ))
            .items;
        final blocks = (await repositories.lockedBlocks.findBlocksByOwner(
          ownerId: ownerId,
        ))
            .items;
        final overrides = (await repositories.lockedBlocks.findOverridesForDate(
          ownerId: ownerId,
          date: request.date,
        ))
            .items;
        final snapshots =
            await repositories.schedulingSnapshots.findInWindow(window);
        final acknowledgedNoticeIds =
            (await repositories.noticeAcknowledgements.findByOwnerId(ownerId))
                .map((record) => record.noticeId)
                .toSet();
        logger.debug(() => 'Today state query loaded persisted inputs. '
            'ownerId=$ownerId date=${request.date.toIsoString()} '
            'projectCount=${projects.length} taskCount=${tasks.length} '
            'lockedBlockCount=${blocks.length} overrideCount=${overrides.length} '
            'snapshotCount=${snapshots.length} '
            'acknowledgedNoticeCount=${acknowledgedNoticeIds.length}');

        final lockedExpansion = expandLockedBlocksForDay(
          blocks: blocks,
          overrides: overrides,
          date: request.date,
          timeZoneId: timeZoneId,
          timeZoneResolver: timeZoneResolver,
          resolutionOptions: resolutionOptions,
        );
        final mapper = TimelineItemMapper.fromProjects(projects);
        final baseItems = <TodayTimelineItem>[
          for (final task in tasks)
            if (_taskAppearsInToday(task, request.revealHiddenLockedBlocks))
              TodayTimelineItem(
                item: mapper.fromTask(task),
                source: TodayTimelineItemSource.task,
                taskStatus: task.status,
                taskId: task.id,
                hiddenByDefault: task.isLocked,
              ),
          for (final occurrence in lockedExpansion.occurrences)
            if (mapper.fromLockedOccurrence(
              occurrence,
              revealHiddenLockedBlocks: request.revealHiddenLockedBlocks,
              occurrenceDate: request.date,
            )
                case final lockedItem?)
              TodayTimelineItem(
                item: lockedItem,
                source: TodayTimelineItemSource.lockedOccurrence,
                lockedBlockId: occurrence.lockedBlockId,
                lockedOverrideId: occurrence.overrideId,
                hiddenByDefault: occurrence.hiddenByDefault,
              ),
        ]..sort(_compareTodayItems);

        final current = _currentItem(baseItems, context.now);
        final nextRequired = _nextRequiredItem(baseItems, context.now);
        final nextFlexible = request.includeNextFlexibleItem
            ? _nextFlexibleItem(
                baseItems,
                context.now,
                currentTaskId: current?.taskId,
              )
            : null;
        final selectedItems = baseItems
            .map(
              (item) => item.copyWith(
                isCurrent: item.id == current?.id,
                isNextRequired: item.id == nextRequired?.id,
                isNextFlexible: item.id == nextFlexible?.id,
              ),
            )
            .toList(growable: false);

        final selectedCurrent = _findById(selectedItems, current?.id);
        final selectedNextRequired = _findById(selectedItems, nextRequired?.id);
        final selectedNextFlexible = _findById(selectedItems, nextFlexible?.id);
        final pendingNotices = _pendingNoticesFromSnapshots(
          snapshots,
          acknowledgedNoticeIds: acknowledgedNoticeIds,
        );
        logger.debug(() => 'Today state query finished. '
            'ownerId=$ownerId date=${request.date.toIsoString()} '
            'timelineItemCount=${selectedItems.length} '
            'pendingNoticeCount=${pendingNotices.length} '
            'hasCurrent=${selectedCurrent != null} '
            'hasNextRequired=${selectedNextRequired != null} '
            'hasNextFlexible=${selectedNextFlexible != null}');

        return ApplicationResult.success(
          TodayState(
            date: request.date,
            ownerId: ownerId,
            timeZoneId: timeZoneId,
            readAt: context.now,
            dayStart: dayStart,
            dayEnd: dayEnd,
            ownerSettings: effectiveSettings,
            timelineItems: selectedItems,
            currentItem: selectedCurrent,
            nextRequiredItem: selectedNextRequired,
            nextFlexibleItem: selectedNextFlexible,
            compactState: TodayCompactState(
              manualCompactModeEnabled: effectiveSettings.compactModeEnabled,
              fullTimelineExpanded: request.fullTimelineExpanded,
              currentItem: selectedCurrent,
              nextRequiredItem: selectedNextRequired,
              nextFlexibleItem: selectedNextFlexible,
            ),
            pendingNotices: pendingNotices,
          ),
        );
      },
    );
  }

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

  /// Runs the `_taskAppearsInToday` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  static bool _taskAppearsInToday(Task task, bool revealHiddenLockedBlocks) {
    if (task.status == TaskStatus.backlog ||
        task.status == TaskStatus.cancelled ||
        task.status == TaskStatus.noLongerRelevant) {
      return false;
    }
    if (task.isLocked && !revealHiddenLockedBlocks) {
      return false;
    }

    return task.scheduledStart != null && task.scheduledEnd != null;
  }

  /// Runs the `_currentItem` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  static TodayTimelineItem? _currentItem(
    List<TodayTimelineItem> items,
    DateTime now,
  ) {
    final active = _firstOrNull(
      items.where((item) {
        return item.source == TodayTimelineItemSource.task &&
            item.taskStatus == TaskStatus.active;
      }),
    );
    if (active != null) {
      return active;
    }

    return _firstOrNull(
      items.where((item) {
        return item.source == TodayTimelineItemSource.task &&
            item.taskStatus == TaskStatus.planned &&
            _containsTime(item, now);
      }),
    );
  }

  /// Runs the `_nextRequiredItem` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  static TodayTimelineItem? _nextRequiredItem(
    List<TodayTimelineItem> items,
    DateTime now,
  ) {
    return _firstOrNull(
      items.where((item) {
        return item.source == TodayTimelineItemSource.task &&
            _isActionable(item.taskStatus) &&
            _isRequired(item.taskType) &&
            _startsAtOrAfter(item, now);
      }),
    );
  }

  /// Runs the `_nextFlexibleItem` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  static TodayTimelineItem? _nextFlexibleItem(
    List<TodayTimelineItem> items,
    DateTime now, {
    required String? currentTaskId,
  }) {
    return _firstOrNull(
      items.where((item) {
        return item.source == TodayTimelineItemSource.task &&
            item.taskId != currentTaskId &&
            _isActionable(item.taskStatus) &&
            item.taskType == TaskType.flexible &&
            _startsAtOrAfter(item, now);
      }),
    );
  }

  /// Runs the `_isActionable` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  static bool _isActionable(TaskStatus? status) {
    return status == TaskStatus.planned || status == TaskStatus.active;
  }

  /// Runs the `_isRequired` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  static bool _isRequired(TaskType type) {
    return type == TaskType.inflexible || type == TaskType.critical;
  }

  /// Runs the `_containsTime` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  static bool _containsTime(TodayTimelineItem item, DateTime now) {
    final start = item.start;
    final end = item.end;
    if (start == null || end == null) {
      return false;
    }

    return !now.isBefore(start) && now.isBefore(end);
  }

  /// Runs the `_startsAtOrAfter` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  static bool _startsAtOrAfter(TodayTimelineItem item, DateTime now) {
    final start = item.start;
    return start != null && !start.isBefore(now);
  }

  /// Runs the `_findById` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  static TodayTimelineItem? _findById(
    List<TodayTimelineItem> items,
    String? id,
  ) {
    if (id == null) {
      return null;
    }

    return _firstOrNull(items.where((item) => item.id == id));
  }

  /// Runs the `_pendingNoticesFromSnapshots` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  static List<TodayPendingNotice> _pendingNoticesFromSnapshots(
    List<SchedulingStateSnapshot> snapshots, {
    Set<String> acknowledgedNoticeIds = const <String>{},
  }) {
    final sortedSnapshots = [...snapshots]..sort((a, b) {
        final capturedComparison = a.capturedAt.compareTo(b.capturedAt);
        if (capturedComparison != 0) {
          return capturedComparison;
        }

        return a.id.compareTo(b.id);
      });
    final notices = <TodayPendingNotice>[];

    for (final snapshot in sortedSnapshots) {
      for (var index = 0; index < snapshot.notices.length; index += 1) {
        final noticeId = _noticeId(snapshot.id, index);
        if (acknowledgedNoticeIds.contains(noticeId)) {
          continue;
        }
        final notice = snapshot.notices[index];
        notices.add(
          TodayPendingNotice(
            noticeId: noticeId,
            snapshotId: snapshot.id,
            capturedAt: snapshot.capturedAt,
            message: notice.message,
            type: notice.type,
            taskId: notice.taskId,
            issueCode: notice.issueCode,
            movementCode: notice.movementCode,
            conflictCode: notice.conflictCode,
            parameters: notice.parameters,
          ),
        );
      }
    }

    return List<TodayPendingNotice>.unmodifiable(notices);
  }
}

/// Runs the `schedulingNoticeId` top-level operation.
/// This function is part of the file-level API and gives callers a direct way to use the associated scheduler behavior.
String schedulingNoticeId({
  required String snapshotId,
  required int noticeIndex,
}) {
  return _noticeId(snapshotId, noticeIndex);
}

/// Top-level helper that performs the `_noticeId` 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 _noticeId(String snapshotId, int noticeIndex) {
  return '$snapshotId:$noticeIndex';
}

/// Top-level helper that performs the `_compareTodayItems` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
int _compareTodayItems(TodayTimelineItem left, TodayTimelineItem right) {
  final startComparison = _compareNullableInstants(left.start, right.start);
  if (startComparison != 0) {
    return startComparison;
  }

  final endComparison = _compareNullableInstants(left.end, right.end);
  if (endComparison != 0) {
    return endComparison;
  }

  final sourceComparison = left.source.index.compareTo(right.source.index);
  if (sourceComparison != 0) {
    return sourceComparison;
  }

  return left.id.compareTo(right.id);
}

/// Top-level helper that performs the `_compareNullableInstants` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
int _compareNullableInstants(DateTime? left, DateTime? right) {
  if (left == null && right == null) {
    return 0;
  }
  if (left == null) {
    return 1;
  }
  if (right == null) {
    return -1;
  }

  return left.compareTo(right);
}

/// Top-level helper that performs the `_firstOrNull<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.
T? _firstOrNull<T>(Iterable<T> values) {
  final iterator = values.iterator;
  if (!iterator.moveNext()) {
    return null;
  }

  return iterator.current;
}
