// Protected Free Slot helpers.
//
// Free slots are intentional rest, not empty capacity. They are represented as
// scheduled Task records so they can appear in read models, but scheduling
// treats them as protected occupancy.

import 'models.dart';
import 'occupancy_policy.dart';
import 'scheduling_engine.dart';

/// Typed outcome for explicitly placing a required visible commitment.
enum RequiredCommitmentScheduleStatus {
  /// Required task was scheduled without protected rest conflicts.
  scheduled,

  /// Required task was scheduled and overlaps protected rest.
  scheduledWithProtectedFreeSlotConflict,

  /// No task with the requested id was present.
  taskNotFound,

  /// The task is not critical or inflexible.
  invalidTaskType,
}

/// Typed overlap between an explicitly placed required commitment and rest.
class ProtectedFreeSlotConflict {
  const ProtectedFreeSlotConflict({
    required this.requiredTaskId,
    required this.freeSlotTaskId,
    required this.requiredInterval,
    required this.freeSlotInterval,
  });

  /// Critical or inflexible task that was explicitly placed.
  final String requiredTaskId;

  /// Protected Free Slot task that was interrupted.
  final String freeSlotTaskId;

  /// Required commitment interval.
  final TimeInterval requiredInterval;

  /// Protected rest interval.
  final TimeInterval freeSlotInterval;
}

/// Result from explicitly scheduling a required visible commitment.
class RequiredCommitmentScheduleResult {
  RequiredCommitmentScheduleResult({
    required this.status,
    required this.schedulingResult,
    required List<ProtectedFreeSlotConflict> protectedFreeSlotConflicts,
  }) : protectedFreeSlotConflicts =
            List<ProtectedFreeSlotConflict>.unmodifiable(
          protectedFreeSlotConflicts,
        );

  /// High-level typed outcome.
  final RequiredCommitmentScheduleStatus status;

  /// Replacement task list, notices, changes, and overlap details.
  final SchedulingResult schedulingResult;

  /// Protected rest interruptions caused by the explicit placement.
  final List<ProtectedFreeSlotConflict> protectedFreeSlotConflicts;

  /// Whether this placement interrupted protected rest.
  bool get hasProtectedFreeSlotConflict {
    return protectedFreeSlotConflicts.isNotEmpty;
  }
}

/// Creates and updates protected Free Slot task records.
class FreeSlotService {
  const FreeSlotService();

  /// Create a scheduled Free Slot record.
  Task create({
    required String id,
    required String title,
    required DateTime start,
    required DateTime end,
    required DateTime createdAt,
    String projectId = 'inbox',
    DateTime? updatedAt,
  }) {
    final interval = TimeInterval(start: start, end: end, label: id);

    return Task(
      id: id,
      title: title,
      projectId: projectId,
      type: TaskType.freeSlot,
      status: TaskStatus.planned,
      priority: PriorityLevel.medium,
      durationMinutes: interval.duration.inMinutes,
      scheduledStart: interval.start,
      scheduledEnd: interval.end,
      createdAt: createdAt,
      updatedAt: updatedAt ?? createdAt,
    );
  }

  /// Update an existing Free Slot record's schedule and optional metadata.
  Task update({
    required Task freeSlot,
    required DateTime start,
    required DateTime end,
    DateTime? updatedAt,
    String? title,
    String? projectId,
  }) {
    if (freeSlot.type != TaskType.freeSlot) {
      throw ArgumentError.value(
        freeSlot.type,
        'freeSlot.type',
        'Task must be a Free Slot.',
      );
    }

    final interval = TimeInterval(start: start, end: end, label: freeSlot.id);

    return freeSlot.copyWith(
      title: title,
      projectId: projectId,
      status: TaskStatus.planned,
      durationMinutes: interval.duration.inMinutes,
      scheduledStart: interval.start,
      scheduledEnd: interval.end,
      updatedAt: updatedAt ?? freeSlot.updatedAt,
    );
  }

  /// Explicitly place a critical or inflexible task.
  ///
  /// This operation is allowed to overlap protected Free Slots because required
  /// commitments can interrupt rest. It does not move the Free Slot. Instead it
  /// returns typed conflict data for the application layer to surface calmly.
  RequiredCommitmentScheduleResult scheduleRequiredCommitment({
    required SchedulingInput input,
    required String taskId,
    required DateTime start,
    required DateTime end,
    DateTime? updatedAt,
  }) {
    final task = _taskById(input.tasks, taskId);
    if (task == null) {
      return _unchangedRequiredResult(
        input: input,
        status: RequiredCommitmentScheduleStatus.taskNotFound,
        notice: SchedulingNotice(
          'Task was not found.',
          type: SchedulingNoticeType.noFit,
          taskId: taskId,
          issueCode: SchedulingIssueCode.taskNotFound,
        ),
        outcomeCode: SchedulingOutcomeCode.notFound,
      );
    }

    if (!task.isRequiredVisible) {
      return _unchangedRequiredResult(
        input: input,
        status: RequiredCommitmentScheduleStatus.invalidTaskType,
        notice: SchedulingNotice(
          'Only critical or inflexible tasks can be explicitly scheduled here.',
          type: SchedulingNoticeType.noFit,
          taskId: task.id,
          issueCode: SchedulingIssueCode.invalidTaskState,
        ),
        outcomeCode: SchedulingOutcomeCode.invalidState,
      );
    }

    final requiredInterval =
        TimeInterval(start: start, end: end, label: taskId);
    final conflicts = _protectedFreeSlotConflicts(
      input: input,
      requiredTaskId: task.id,
      requiredInterval: requiredInterval,
    );
    final updatedTask = task.copyWith(
      status: TaskStatus.planned,
      durationMinutes: requiredInterval.duration.inMinutes,
      scheduledStart: requiredInterval.start,
      scheduledEnd: requiredInterval.end,
      updatedAt: updatedAt ?? task.updatedAt,
    );
    final scheduleChanged = !_sameDateTime(task.scheduledStart, start) ||
        !_sameDateTime(task.scheduledEnd, end) ||
        task.status != TaskStatus.planned;
    final updatedTasks = input.tasks.map((current) {
      if (current.id == task.id) {
        return updatedTask;
      }
      return current;
    }).toList(growable: false);
    final overlapDetails = conflicts.map((conflict) {
      return SchedulingOverlap(
        taskId: conflict.requiredTaskId,
        taskInterval: conflict.requiredInterval,
        blockedInterval: conflict.freeSlotInterval,
      );
    }).toList(growable: false);
    final notices = <SchedulingNotice>[
      SchedulingNotice(
        conflicts.isEmpty
            ? 'Required commitment scheduled.'
            : 'Required commitment overlaps protected rest.',
        type: conflicts.isEmpty
            ? SchedulingNoticeType.moved
            : SchedulingNoticeType.overlap,
        taskId: task.id,
        movementCode: conflicts.isEmpty
            ? SchedulingMovementCode.requiredCommitmentScheduled
            : null,
        conflictCode: conflicts.isEmpty
            ? null
            : SchedulingConflictCode
                .requiredCommitmentOverlapsProtectedFreeSlot,
        parameters: {
          'requiredStart': requiredInterval.start,
          'requiredEnd': requiredInterval.end,
          if (conflicts.isNotEmpty) 'freeSlotCount': conflicts.length,
        },
      ),
    ];
    final changes = <SchedulingChange>[
      if (scheduleChanged)
        SchedulingChange(
          taskId: task.id,
          previousStart: task.scheduledStart,
          previousEnd: task.scheduledEnd,
          nextStart: requiredInterval.start,
          nextEnd: requiredInterval.end,
        ),
    ];

    return RequiredCommitmentScheduleResult(
      status: conflicts.isEmpty
          ? RequiredCommitmentScheduleStatus.scheduled
          : RequiredCommitmentScheduleStatus
              .scheduledWithProtectedFreeSlotConflict,
      protectedFreeSlotConflicts: conflicts,
      schedulingResult: SchedulingResult(
        tasks: updatedTasks,
        operationCode: SchedulingOperationCode.scheduleRequiredCommitment,
        outcomeCode: conflicts.isEmpty
            ? SchedulingOutcomeCode.success
            : SchedulingOutcomeCode.conflict,
        notices: notices,
        changes: changes,
        overlaps: overlapDetails,
      ),
    );
  }
}

RequiredCommitmentScheduleResult _unchangedRequiredResult({
  required SchedulingInput input,
  required RequiredCommitmentScheduleStatus status,
  required SchedulingNotice notice,
  required SchedulingOutcomeCode outcomeCode,
}) {
  return RequiredCommitmentScheduleResult(
    status: status,
    protectedFreeSlotConflicts: const [],
    schedulingResult: SchedulingResult(
      tasks: input.tasks,
      operationCode: SchedulingOperationCode.scheduleRequiredCommitment,
      outcomeCode: outcomeCode,
      notices: [notice],
    ),
  );
}

List<ProtectedFreeSlotConflict> _protectedFreeSlotConflicts({
  required SchedulingInput input,
  required String requiredTaskId,
  required TimeInterval requiredInterval,
}) {
  final conflicts = <ProtectedFreeSlotConflict>[];

  for (final entry in input.occupancyEntries) {
    if (entry.category != OccupancyCategory.protectedFreeSlot) {
      continue;
    }

    final freeSlot = entry.task;
    final freeSlotInterval = entry.interval;
    if (freeSlot == null ||
        freeSlotInterval == null ||
        !requiredInterval.overlaps(freeSlotInterval)) {
      continue;
    }

    conflicts.add(
      ProtectedFreeSlotConflict(
        requiredTaskId: requiredTaskId,
        freeSlotTaskId: freeSlot.id,
        requiredInterval: requiredInterval,
        freeSlotInterval: freeSlotInterval,
      ),
    );
  }

  return List<ProtectedFreeSlotConflict>.unmodifiable(conflicts);
}

Task? _taskById(List<Task> tasks, String taskId) {
  for (final task in tasks) {
    if (task.id == taskId) {
      return task;
    }
  }

  return null;
}

bool _sameDateTime(DateTime? first, DateTime? second) {
  if (first == null || second == null) {
    return first == null && second == null;
  }

  return first.isAtSameMomentAs(second);
}
