// Application-level Today read model.
//
// This module builds UI-independent state for the Today and compact views. It
// is read-only: it does not run rollover, create activities, update statistics,
// or persist operation records.

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

/// Source category for one Today timeline item.
enum TodayTimelineItemSource {
  task,
  lockedOccurrence,
}

/// Request for the V1 Today read model.
class GetTodayStateRequest {
  const GetTodayStateRequest({
    required this.context,
    required this.date,
    this.revealHiddenLockedBlocks = false,
    this.includeNextFlexibleItem = true,
    this.fullTimelineExpanded = false,
  });

  /// Operation/read context. [ApplicationOperationContext.now] is the read
  /// instant and is never recomputed during the query.
  final ApplicationOperationContext context;

  /// Owner-local day to read.
  final CivilDate date;

  /// Whether hidden locked-time overlays should be present in the item list.
  final bool revealHiddenLockedBlocks;

  /// Whether compact projection should expose a next flexible task.
  final bool includeNextFlexibleItem;

  /// Whether compact mode is currently showing the full timeline.
  final bool fullTimelineExpanded;
}

/// One item in the Today read model, with task status/source metadata.
class TodayTimelineItem {
  TodayTimelineItem({
    required this.item,
    required this.source,
    this.taskStatus,
    this.taskId,
    this.lockedBlockId,
    this.lockedOverrideId,
    this.hiddenByDefault = false,
    this.isCurrent = false,
    this.isNextRequired = false,
    this.isNextFlexible = false,
  });

  /// Display-ready framework-neutral item.
  final TimelineItem item;

  /// Repository/domain source for this item.
  final TodayTimelineItemSource source;

  /// Source task status. Null for locked occurrences.
  final TaskStatus? taskStatus;

  /// Source task id, when [source] is [TodayTimelineItemSource.task].
  final String? taskId;

  /// Source locked block id, when this is a locked occurrence.
  final String? lockedBlockId;

  /// Source override id, when this occurrence was replaced or added.
  final String? lockedOverrideId;

  /// Whether the locked source is normally hidden.
  final bool hiddenByDefault;

  /// Whether this item is the current selected item for the read instant.
  final bool isCurrent;

  /// Whether this item is the next required item.
  final bool isNextRequired;

  /// Whether this item is the optional next flexible item.
  final bool isNextFlexible;

  /// Stable item id.
  String get id => item.id;

  /// Timeline start.
  DateTime? get start => item.start;

  /// Timeline end.
  DateTime? get end => item.end;

  /// Source task behavior type.
  TaskType get taskType => item.taskType;

  /// Project/border color token.
  String get projectColorToken => item.projectColorToken;

  /// Structured quick actions available for this item.
  List<TimelineQuickAction> get quickActions => item.quickActions;

  /// Return a copy with selection flags changed.
  TodayTimelineItem copyWith({
    bool? isCurrent,
    bool? isNextRequired,
    bool? isNextFlexible,
  }) {
    return TodayTimelineItem(
      item: item,
      source: source,
      taskStatus: taskStatus,
      taskId: taskId,
      lockedBlockId: lockedBlockId,
      lockedOverrideId: lockedOverrideId,
      hiddenByDefault: hiddenByDefault,
      isCurrent: isCurrent ?? this.isCurrent,
      isNextRequired: isNextRequired ?? this.isNextRequired,
      isNextFlexible: isNextFlexible ?? this.isNextFlexible,
    );
  }
}

/// Compact projection derived from the same Today item list.
class TodayCompactState {
  const TodayCompactState({
    required this.manualCompactModeEnabled,
    required this.fullTimelineExpanded,
    this.currentItem,
    this.nextRequiredItem,
    this.nextFlexibleItem,
  });

  /// User-controlled compact mode flag.
  final bool manualCompactModeEnabled;

  /// Whether the full timeline is expanded while compact mode is active.
  final bool fullTimelineExpanded;

  /// Current visible item, if one occupies the read instant.
  final TodayTimelineItem? currentItem;

  /// Next critical or inflexible item.
  final TodayTimelineItem? nextRequiredItem;

  /// Optional next flexible item.
  final TodayTimelineItem? nextFlexibleItem;

  /// Whether callers should show the full timeline.
  bool get fullTimelineVisible {
    return !manualCompactModeEnabled || fullTimelineExpanded;
  }

  /// Items selected for compact rendering.
  List<TodayTimelineItem> get compactItems {
    if (!manualCompactModeEnabled) {
      return const [];
    }

    return [
      if (currentItem != null) currentItem!,
      if (nextRequiredItem != null && nextRequiredItem!.id != currentItem?.id)
        nextRequiredItem!,
      if (nextFlexibleItem != null &&
          nextFlexibleItem!.id != currentItem?.id &&
          nextFlexibleItem!.id != nextRequiredItem?.id)
        nextFlexibleItem!,
    ];
  }
}

/// Pending notice data surfaced through Today state.
class TodayPendingNotice {
  TodayPendingNotice({
    required this.noticeId,
    required this.snapshotId,
    required this.capturedAt,
    required this.message,
    required this.type,
    required Map<String, Object?> parameters,
    this.taskId,
    this.issueCode,
    this.movementCode,
    this.conflictCode,
  }) : parameters = Map<String, Object?>.unmodifiable(parameters);

  /// Stable id for acknowledgement/consume commands.
  final String noticeId;

  /// Source snapshot id.
  final String snapshotId;

  /// Snapshot capture time.
  final DateTime capturedAt;

  /// Existing neutral scheduler message. UI may localize from codes later.
  final String message;

  /// Notice category.
  final SchedulingNoticeType type;

  /// Related task id, if any.
  final String? taskId;

  /// Stable issue code, if any.
  final SchedulingIssueCode? issueCode;

  /// Stable movement code, if any.
  final SchedulingMovementCode? movementCode;

  /// Stable conflict code, if any.
  final SchedulingConflictCode? conflictCode;

  /// Structured notice parameters.
  final Map<String, Object?> parameters;
}

/// Complete V1 Today state for one owner-local date.
class TodayState {
  TodayState({
    required this.date,
    required this.ownerId,
    required this.timeZoneId,
    required this.readAt,
    required this.dayStart,
    required this.dayEnd,
    required this.ownerSettings,
    required List<TodayTimelineItem> timelineItems,
    required this.compactState,
    required List<TodayPendingNotice> pendingNotices,
    this.currentItem,
    this.nextRequiredItem,
    this.nextFlexibleItem,
  })  : timelineItems = List<TodayTimelineItem>.unmodifiable(timelineItems),
        pendingNotices = List<TodayPendingNotice>.unmodifiable(pendingNotices);

  /// Owner-local date represented by this state.
  final CivilDate date;

  /// Authorization-neutral owner scope.
  final String ownerId;

  /// Time-zone id used for local-day expansion.
  final String timeZoneId;

  /// Read instant from the request context.
  final DateTime readAt;

  /// Resolved start instant for [date].
  final DateTime dayStart;

  /// Resolved end instant for [date].
  final DateTime dayEnd;

  /// Effective owner settings used by the query.
  final OwnerSettings ownerSettings;

  /// Full sorted UI-independent timeline projection.
  final List<TodayTimelineItem> timelineItems;

  /// Current item, if one occupies [readAt].
  final TodayTimelineItem? currentItem;

  /// Next required item after [readAt].
  final TodayTimelineItem? nextRequiredItem;

  /// Optional next flexible item after [readAt].
  final TodayTimelineItem? nextFlexibleItem;

  /// Compact-mode projection from [timelineItems].
  final TodayCompactState compactState;

  /// Pending scheduling notices stored for the day.
  final List<TodayPendingNotice> pendingNotices;
}

/// Builds Today state from application repositories.
class GetTodayStateQuery {
  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;
        final storedSettings =
            await repositories.ownerSettings.findByOwnerId(ownerId);
        final settings = storedSettings ??
            OwnerSettings(
              ownerId: ownerId,
              timeZoneId: context.ownerTimeZone.timeZoneId,
            );
        final timeZoneId = settings.timeZoneId;
        final dayStart = _resolve(
          date: request.date,
          wallTime: settings.dayStart,
          timeZoneId: timeZoneId,
        );
        final dayEnd = _resolve(
          date: request.date,
          wallTime: settings.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();

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

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

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

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

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

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

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

  static bool _isActionable(TaskStatus? status) {
    return status == TaskStatus.planned || status == TaskStatus.active;
  }

  static bool _isRequired(TaskType type) {
    return type == TaskType.inflexible || type == TaskType.critical;
  }

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

  static bool _startsAtOrAfter(TodayTimelineItem item, DateTime now) {
    final start = item.start;
    return start != null && !start.isBefore(now);
  }

  static TodayTimelineItem? _findById(
    List<TodayTimelineItem> items,
    String? id,
  ) {
    if (id == null) {
      return null;
    }

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

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

String schedulingNoticeId({
  required String snapshotId,
  required int noticeIndex,
}) {
  return _noticeId(snapshotId, noticeIndex);
}

String _noticeId(String snapshotId, int noticeIndex) {
  return '$snapshotId:$noticeIndex';
}

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

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

T? _firstOrNull<T>(Iterable<T> values) {
  final iterator = values.iterator;
  if (!iterator.moveNext()) {
    return null;
  }

  return iterator.current;
}
