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

part of '../../timeline_state.dart';

/// Converts domain tasks into Today timeline items.
class TimelineItemMapper {
  /// Creates a `TimelineItemMapper` 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.
  TimelineItemMapper({
    Map<String, String> projectColorTokensById = const <String, String>{},
  }) : projectColorTokensById =
            Map<String, String>.unmodifiable(projectColorTokensById);

  /// Project id to border color token lookup.
  final Map<String, String> projectColorTokensById;

  /// Build a mapper from project profiles.
  factory TimelineItemMapper.fromProjects(Iterable<ProjectProfile> projects) {
    return TimelineItemMapper(
      projectColorTokensById: {
        for (final project in projects) project.id: project.colorKey,
      },
    );
  }

  /// Convert [task] into framework-neutral timeline view state.
  TimelineItem fromTask(Task task) {
    return TimelineItem(
      id: task.id,
      displayTitle: task.title,
      taskType: task.type,
      projectColorToken: _projectColorTokenFor(task.projectId),
      backgroundToken: _backgroundTokenFor(task.type),
      rewardIconToken: _rewardIconTokenFor(task.reward),
      difficultyIconToken: _difficultyIconTokenFor(task.difficulty),
      showsExplicitTime: _showsExplicitTime(task.type),
      start: task.scheduledStart,
      end: task.scheduledEnd,
      actualStart: task.actualStart,
      actualEnd: task.actualEnd,
      completedAt: task.completedAt,
      updatedAt: task.updatedAt,
      durationMinutes: _durationMinutesFor(task),
      quickActions: _quickActionsFor(task.type),
      category: task.isLocked
          ? TimelineItemCategory.overlay
          : TimelineItemCategory.taskCard,
    );
  }

  /// Convert a concrete locked-time occurrence into an overlay item.
  ///
  /// Hidden locked time returns null unless [revealHiddenLockedBlocks] is true.
  /// This keeps locked time available to scheduling while leaving UI visibility
  /// controlled by an explicit temporary reveal state.
  TimelineItem? fromLockedOccurrence(
    LockedBlockOccurrence occurrence, {
    required bool revealHiddenLockedBlocks,
    CivilDate? occurrenceDate,
  }) {
    if (occurrence.hiddenByDefault && !revealHiddenLockedBlocks) {
      return null;
    }

    return TimelineItem(
      id: _lockedOccurrenceId(occurrence, occurrenceDate: occurrenceDate),
      displayTitle: occurrence.name,
      taskType: TaskType.locked,
      projectColorToken: _projectColorTokenFor(occurrence.projectId),
      backgroundToken: TimelineBackgroundToken.locked,
      rewardIconToken: TimelineRewardIconToken.notSet,
      difficultyIconToken: TimelineDifficultyIconToken.notSet,
      showsExplicitTime: true,
      start: occurrence.interval.start,
      end: occurrence.interval.end,
      quickActions: const [],
      category: TimelineItemCategory.overlay,
    );
  }

  /// Build manual compact-mode state from scheduled task data.
  CompactTimelineState compactStateForTasks({
    required Iterable<Task> tasks,
    required DateTime now,
    required bool manualCompactModeEnabled,
    bool includeNextFlexibleTask = false,
    bool fullTimelineExpanded = false,
  }) {
    if (!manualCompactModeEnabled) {
      return CompactTimelineState(
        manualCompactModeEnabled: false,
        fullTimelineExpanded: fullTimelineExpanded,
      );
    }

    final timelineTasks = tasks.where(_canAppearInCompactMode).toList()
      ..sort(_compareTasksByStart);
    final currentTask = timelineTasks
        .where((task) {
          return task.status == TaskStatus.active || _containsTime(task, now);
        })
        .cast<Task?>()
        .firstWhere(
          (task) => task != null,
          orElse: () => null,
        );
    final nextRequiredTask = timelineTasks
        .where((task) {
          return task.isRequiredVisible && _startsAtOrAfter(task, now);
        })
        .cast<Task?>()
        .firstWhere(
          (task) => task != null,
          orElse: () => null,
        );
    final nextFlexibleTask = includeNextFlexibleTask
        ? timelineTasks
            .where((task) {
              return task.isFlexible &&
                  task.id != currentTask?.id &&
                  _startsAtOrAfter(task, now);
            })
            .cast<Task?>()
            .firstWhere(
              (task) => task != null,
              orElse: () => null,
            )
        : null;

    return CompactTimelineState(
      manualCompactModeEnabled: true,
      fullTimelineExpanded: fullTimelineExpanded,
      currentItem: currentTask == null ? null : fromTask(currentTask),
      nextRequiredItem:
          nextRequiredTask == null ? null : fromTask(nextRequiredTask),
      nextFlexibleItem:
          nextFlexibleTask == null ? null : fromTask(nextFlexibleTask),
    );
  }

  /// Runs the `_canAppearInCompactMode` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  bool _canAppearInCompactMode(Task task) {
    return !task.isLocked &&
        task.status != TaskStatus.backlog &&
        task.status != TaskStatus.completed &&
        task.status != TaskStatus.cancelled &&
        task.status != TaskStatus.noLongerRelevant &&
        task.scheduledStart != null &&
        task.scheduledEnd != null;
  }

  /// 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.
  bool _containsTime(Task task, DateTime now) {
    final start = task.scheduledStart;
    final end = task.scheduledEnd;
    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.
  bool _startsAtOrAfter(Task task, DateTime now) {
    final start = task.scheduledStart;
    return start != null && !start.isBefore(now);
  }

  /// Runs the `_compareTasksByStart` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  int _compareTasksByStart(Task left, Task right) {
    final leftStart = left.scheduledStart;
    final rightStart = right.scheduledStart;
    if (leftStart == null && rightStart == null) {
      return 0;
    }
    if (leftStart == null) {
      return 1;
    }
    if (rightStart == null) {
      return -1;
    }
    return leftStart.compareTo(rightStart);
  }

  /// Runs the `_lockedOccurrenceId` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  String _lockedOccurrenceId(
    LockedBlockOccurrence occurrence, {
    CivilDate? occurrenceDate,
  }) {
    final datePart =
        occurrenceDate == null ? null : ':${occurrenceDate.toIsoString()}';
    final lockedBlockId = occurrence.lockedBlockId;
    if (lockedBlockId != null) {
      return 'locked-block:$lockedBlockId${datePart ?? ''}';
    }
    final overrideId = occurrence.overrideId;
    if (overrideId != null) {
      return 'locked-override:$overrideId${datePart ?? ''}';
    }
    return 'locked:${occurrence.name}:'
        '${occurrence.interval.start.toIso8601String()}';
  }

  /// Runs the `_projectColorTokenFor` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  String _projectColorTokenFor(String? projectId) {
    if (projectId == null) {
      return 'locked';
    }
    return projectColorTokensById[projectId] ?? projectId;
  }

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

  /// Runs the `_durationMinutesFor` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  int? _durationMinutesFor(Task task) {
    if (!task.isFlexible) {
      return null;
    }
    if (task.durationMinutes != null) {
      return task.durationMinutes;
    }
    final start = task.scheduledStart;
    final end = task.scheduledEnd;
    if (start == null || end == null) {
      return null;
    }
    return end.difference(start).inMinutes;
  }

  /// Runs the `_quickActionsFor` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  List<TimelineQuickAction> _quickActionsFor(TaskType type) {
    switch (type) {
      case TaskType.flexible:
        return const [
          TimelineQuickAction.done,
          TimelineQuickAction.push,
          TimelineQuickAction.backlog,
          TimelineQuickAction.breakUp,
        ];
      case TaskType.inflexible:
      case TaskType.critical:
        return const [
          TimelineQuickAction.done,
          TimelineQuickAction.missed,
          TimelineQuickAction.cancel,
          TimelineQuickAction.noLongerRelevant,
        ];
      case TaskType.locked:
      case TaskType.surprise:
      case TaskType.freeSlot:
        return const [];
    }
  }

  /// Runs the `_backgroundTokenFor` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  TimelineBackgroundToken _backgroundTokenFor(TaskType type) {
    switch (type) {
      case TaskType.flexible:
        return TimelineBackgroundToken.flexible;
      case TaskType.inflexible:
        return TimelineBackgroundToken.inflexible;
      case TaskType.critical:
        return TimelineBackgroundToken.critical;
      case TaskType.locked:
        return TimelineBackgroundToken.locked;
      case TaskType.surprise:
        return TimelineBackgroundToken.surprise;
      case TaskType.freeSlot:
        return TimelineBackgroundToken.freeSlot;
    }
  }

  /// Runs the `_rewardIconTokenFor` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  TimelineRewardIconToken _rewardIconTokenFor(RewardLevel reward) {
    switch (reward) {
      case RewardLevel.notSet:
        return TimelineRewardIconToken.notSet;
      case RewardLevel.veryLow:
        return TimelineRewardIconToken.veryLow;
      case RewardLevel.low:
        return TimelineRewardIconToken.low;
      case RewardLevel.medium:
        return TimelineRewardIconToken.medium;
      case RewardLevel.high:
        return TimelineRewardIconToken.high;
      case RewardLevel.veryHigh:
        return TimelineRewardIconToken.veryHigh;
    }
  }

  /// Runs the `_difficultyIconTokenFor` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  TimelineDifficultyIconToken _difficultyIconTokenFor(
    DifficultyLevel difficulty,
  ) {
    switch (difficulty) {
      case DifficultyLevel.notSet:
        return TimelineDifficultyIconToken.notSet;
      case DifficultyLevel.veryEasy:
        return TimelineDifficultyIconToken.veryEasy;
      case DifficultyLevel.easy:
        return TimelineDifficultyIconToken.easy;
      case DifficultyLevel.medium:
        return TimelineDifficultyIconToken.medium;
      case DifficultyLevel.hard:
        return TimelineDifficultyIconToken.hard;
      case DifficultyLevel.veryHard:
        return TimelineDifficultyIconToken.veryHard;
    }
  }
}
