// UI-independent Today timeline view state.
//
// This file translates domain tasks into small display-ready value objects
// without importing Flutter or choosing visual assets. UI layers can map these
// tokens to colors, icons, text, or widgets later.

import 'locked_time.dart';
import 'models.dart';
import 'time_contracts.dart';

/// Broad display category for an item placed on the Today timeline.
enum TimelineItemCategory {
  /// Normal visible task card.
  taskCard,

  /// Non-task timeline layer such as locked time.
  overlay,
}

/// Background style token derived from the scheduling behavior type.
enum TimelineBackgroundToken {
  flexible,
  inflexible,
  critical,
  locked,
  surprise,
  freeSlot,
}

/// Reward icon token for the first timeline card icon.
enum TimelineRewardIconToken {
  notSet,
  veryLow,
  low,
  medium,
  high,
  veryHigh,
}

/// Difficulty icon token for the second timeline card icon.
enum TimelineDifficultyIconToken {
  notSet,
  veryEasy,
  easy,
  medium,
  hard,
  veryHard,
}

/// Quick actions the timeline can expose without depending on a UI framework.
enum TimelineQuickAction {
  done,
  push,
  backlog,
  breakUp,
  missed,
  cancel,
  noLongerRelevant,
}

/// Display-ready timeline item derived from a domain [Task].
class TimelineItem {
  TimelineItem({
    required this.id,
    required this.displayTitle,
    required this.taskType,
    required this.projectColorToken,
    required this.backgroundToken,
    required this.rewardIconToken,
    required this.difficultyIconToken,
    required this.showsExplicitTime,
    required List<TimelineQuickAction> quickActions,
    required this.category,
    this.start,
    this.end,
    this.durationMinutes,
  }) : quickActions = List<TimelineQuickAction>.unmodifiable(quickActions);

  /// Stable id of the source item.
  final String id;

  /// Text shown as the item name.
  final String displayTitle;

  /// Source scheduling behavior type.
  final TaskType taskType;

  /// Border/project color token. This is a key, not a raw color.
  final String projectColorToken;

  /// Translucent background token derived from [taskType].
  final TimelineBackgroundToken backgroundToken;

  /// First icon token.
  final TimelineRewardIconToken rewardIconToken;

  /// Second icon token.
  final TimelineDifficultyIconToken difficultyIconToken;

  /// Whether the item should render explicit start/end time text.
  final bool showsExplicitTime;

  /// Timeline placement start, if the source item has one.
  final DateTime? start;

  /// Timeline placement end, if the source item has one.
  final DateTime? end;

  /// Duration display value for flexible task cards.
  final int? durationMinutes;

  /// Quick actions available from this item.
  final List<TimelineQuickAction> quickActions;

  /// Whether this is a normal task card or a non-task overlay.
  final TimelineItemCategory category;
}

/// Reduced Today timeline state for manual compact mode.
class CompactTimelineState {
  const CompactTimelineState({
    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 task, if one is active or occupies the query time.
  final TimelineItem? currentItem;

  /// Next critical or inflexible task after the current moment.
  final TimelineItem? nextRequiredItem;

  /// Optional next flexible task after the current moment.
  final TimelineItem? nextFlexibleItem;

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

  /// Items selected for the compact view.
  List<TimelineItem> 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!,
    ];
  }
}

/// Converts domain tasks into Today timeline items.
class TimelineItemMapper {
  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,
      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),
    );
  }

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

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

  bool _startsAtOrAfter(Task task, DateTime now) {
    final start = task.scheduledStart;
    return start != null && !start.isBefore(now);
  }

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

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

  String _projectColorTokenFor(String? projectId) {
    if (projectId == null) {
      return 'locked';
    }
    return projectColorTokensById[projectId] ?? projectId;
  }

  bool _showsExplicitTime(TaskType type) {
    return type == TaskType.inflexible ||
        type == TaskType.critical ||
        type == TaskType.locked;
  }

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

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

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

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

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