// Central occupancy policy for V1 scheduling.
//
// This file keeps scheduler movement rules separate from UI timeline
// categories. A task can be hidden, visible, movable, historical, or
// non-occupying for scheduling purposes independent of how a view renders it.

import 'models.dart';

/// Scheduling-facing occupancy category for one task or external interval.
enum OccupancyCategory {
  /// Hidden locked time that constrains scheduling.
  hiddenLockedConstraint,

  /// Visible critical or inflexible commitment.
  requiredVisibleCommitment,

  /// Intentional protected rest.
  protectedFreeSlot,

  /// Planned flexible work that automatic scheduling may move.
  movablePlannedFlexible,

  /// Work currently in progress.
  activeWork,

  /// Known actual historical occupancy from completed work.
  completedActualOccupancy,

  /// Historical missed placement retained as context.
  retainedMissedHistoricalInterval,

  /// Record that should not occupy the scheduling window.
  nonOccupyingRecord,
}

/// Where an occupancy entry came from.
enum OccupancySource {
  /// A [Task] record.
  task,

  /// A hidden locked interval supplied outside the task list.
  lockedInterval,

  /// A visible required interval supplied outside the task list.
  requiredVisibleInterval,
}

/// Policy classification for one task or external interval.
class OccupancyEntry {
  const OccupancyEntry({
    required this.source,
    required this.category,
    required this.interval,
    required this.task,
    required this.isImmovable,
    required this.blocksAutomaticFlexiblePlacement,
    required this.mayBeExplicitlyOverlappedByRequiredCommitment,
    required this.reportsConflict,
    required this.hiddenByDefault,
  });

  /// Origin of the entry.
  final OccupancySource source;

  /// Scheduling-facing category.
  final OccupancyCategory category;

  /// Interval used by scheduling, or null when the record is unplaced.
  final TimeInterval? interval;

  /// Source task when this entry came from a task record.
  final Task? task;

  /// Stable task id when present.
  String? get taskId => task?.id;

  /// Whether automatic scheduling must leave this entry's placement unchanged.
  final bool isImmovable;

  /// Whether flexible placement must avoid [interval].
  final bool blocksAutomaticFlexiblePlacement;

  /// Whether an explicit critical/inflexible commitment may overlap this entry.
  final bool mayBeExplicitlyOverlappedByRequiredCommitment;

  /// Whether overlaps with this entry should be returned as conflicts.
  final bool reportsConflict;

  /// Whether entry details should be hidden by default in user-facing output.
  final bool hiddenByDefault;

  /// Whether the entry is planned flexible work movable by automatic scheduling.
  bool get isMovablePlannedFlexible {
    return category == OccupancyCategory.movablePlannedFlexible;
  }
}

/// UI-independent V1 occupancy policy.
class OccupancyPolicy {
  const OccupancyPolicy();

  /// Classify all task and external interval inputs for a scheduling operation.
  List<OccupancyEntry> classify({
    required Iterable<Task> tasks,
    Iterable<TimeInterval> lockedIntervals = const <TimeInterval>[],
    Iterable<TimeInterval> requiredVisibleIntervals = const <TimeInterval>[],
  }) {
    return List<OccupancyEntry>.unmodifiable([
      ...tasks.map(classifyTask),
      ...lockedIntervals.map(classifyLockedInterval),
      ...requiredVisibleIntervals.map(classifyRequiredVisibleInterval),
    ]);
  }

  /// Classify one task record.
  OccupancyEntry classifyTask(Task task) {
    final scheduledInterval = _scheduledIntervalFor(task);
    final actualInterval = _actualIntervalFor(task);

    if (_isNonOccupyingStatus(task.status)) {
      return _taskEntry(
        task: task,
        category: OccupancyCategory.nonOccupyingRecord,
        interval: null,
      );
    }

    if (task.status == TaskStatus.completed) {
      final completedInterval = actualInterval ??
          (task.type == TaskType.surprise ? scheduledInterval : null);

      if (completedInterval == null) {
        return _taskEntry(
          task: task,
          category: OccupancyCategory.nonOccupyingRecord,
          interval: null,
        );
      }

      return _taskEntry(
        task: task,
        category: OccupancyCategory.completedActualOccupancy,
        interval: completedInterval,
      );
    }

    if (task.status == TaskStatus.missed) {
      if (scheduledInterval == null) {
        return _taskEntry(
          task: task,
          category: OccupancyCategory.nonOccupyingRecord,
          interval: null,
        );
      }

      return _taskEntry(
        task: task,
        category: OccupancyCategory.retainedMissedHistoricalInterval,
        interval: scheduledInterval,
      );
    }

    if (task.type == TaskType.locked) {
      return _taskEntry(
        task: task,
        category: OccupancyCategory.hiddenLockedConstraint,
        interval: scheduledInterval,
      );
    }

    if (task.type == TaskType.freeSlot) {
      return _taskEntry(
        task: task,
        category: OccupancyCategory.protectedFreeSlot,
        interval: scheduledInterval,
      );
    }

    if (task.status == TaskStatus.active) {
      return _taskEntry(
        task: task,
        category: OccupancyCategory.activeWork,
        interval: actualInterval ?? scheduledInterval,
      );
    }

    if (task.isRequiredVisible) {
      return _taskEntry(
        task: task,
        category: OccupancyCategory.requiredVisibleCommitment,
        interval: scheduledInterval,
      );
    }

    if (task.isFlexible && task.status == TaskStatus.planned) {
      return _taskEntry(
        task: task,
        category: OccupancyCategory.movablePlannedFlexible,
        interval: scheduledInterval,
      );
    }

    // Surprise tasks should normally be completed records. If an unsupported
    // lifecycle combination appears, keep it out of automatic placement instead
    // of inventing movement behavior for it.
    return _taskEntry(
      task: task,
      category: OccupancyCategory.nonOccupyingRecord,
      interval: null,
    );
  }

  /// Classify one hidden locked interval supplied outside the task list.
  OccupancyEntry classifyLockedInterval(TimeInterval interval) {
    return _entry(
      source: OccupancySource.lockedInterval,
      category: OccupancyCategory.hiddenLockedConstraint,
      interval: interval,
      task: null,
    );
  }

  /// Classify one visible required interval supplied outside the task list.
  OccupancyEntry classifyRequiredVisibleInterval(TimeInterval interval) {
    return _entry(
      source: OccupancySource.requiredVisibleInterval,
      category: OccupancyCategory.requiredVisibleCommitment,
      interval: interval,
      task: null,
    );
  }

  OccupancyEntry _taskEntry({
    required Task task,
    required OccupancyCategory category,
    required TimeInterval? interval,
  }) {
    return _entry(
      source: OccupancySource.task,
      category: category,
      interval: interval,
      task: task,
    );
  }

  OccupancyEntry _entry({
    required OccupancySource source,
    required OccupancyCategory category,
    required TimeInterval? interval,
    required Task? task,
  }) {
    final hasInterval = interval != null;

    return switch (category) {
      OccupancyCategory.nonOccupyingRecord => OccupancyEntry(
          source: source,
          category: category,
          interval: null,
          task: task,
          isImmovable: false,
          blocksAutomaticFlexiblePlacement: false,
          mayBeExplicitlyOverlappedByRequiredCommitment: false,
          reportsConflict: false,
          hiddenByDefault: false,
        ),
      OccupancyCategory.movablePlannedFlexible => OccupancyEntry(
          source: source,
          category: category,
          interval: interval,
          task: task,
          isImmovable: false,
          blocksAutomaticFlexiblePlacement: false,
          mayBeExplicitlyOverlappedByRequiredCommitment: false,
          reportsConflict: false,
          hiddenByDefault: false,
        ),
      OccupancyCategory.protectedFreeSlot => OccupancyEntry(
          source: source,
          category: category,
          interval: interval,
          task: task,
          isImmovable: true,
          blocksAutomaticFlexiblePlacement: hasInterval,
          mayBeExplicitlyOverlappedByRequiredCommitment: hasInterval,
          reportsConflict: hasInterval,
          hiddenByDefault: false,
        ),
      OccupancyCategory.hiddenLockedConstraint => OccupancyEntry(
          source: source,
          category: category,
          interval: interval,
          task: task,
          isImmovable: true,
          blocksAutomaticFlexiblePlacement: hasInterval,
          mayBeExplicitlyOverlappedByRequiredCommitment: false,
          reportsConflict: hasInterval,
          hiddenByDefault: true,
        ),
      OccupancyCategory.requiredVisibleCommitment ||
      OccupancyCategory.activeWork ||
      OccupancyCategory.completedActualOccupancy ||
      OccupancyCategory.retainedMissedHistoricalInterval =>
        OccupancyEntry(
          source: source,
          category: category,
          interval: interval,
          task: task,
          isImmovable: true,
          blocksAutomaticFlexiblePlacement: hasInterval,
          mayBeExplicitlyOverlappedByRequiredCommitment: false,
          reportsConflict: hasInterval,
          hiddenByDefault: false,
        ),
    };
  }
}

bool _isNonOccupyingStatus(TaskStatus status) {
  return status == TaskStatus.backlog ||
      status == TaskStatus.cancelled ||
      status == TaskStatus.noLongerRelevant;
}

TimeInterval? _scheduledIntervalFor(Task task) {
  final start = task.scheduledStart;
  final end = task.scheduledEnd;
  if (start == null || end == null) {
    return null;
  }

  return TimeInterval(start: start, end: end, label: task.id);
}

TimeInterval? _actualIntervalFor(Task task) {
  final start = task.actualStart;
  final end = task.actualEnd;
  if (start == null || end == null) {
    return null;
  }

  return TimeInterval(start: start, end: end, label: task.id);
}
