import 'models.dart';

/// Weekday value using DateTime's Monday-first convention.
enum LockedWeekday {
  monday(DateTime.monday),
  tuesday(DateTime.tuesday),
  wednesday(DateTime.wednesday),
  thursday(DateTime.thursday),
  friday(DateTime.friday),
  saturday(DateTime.saturday),
  sunday(DateTime.sunday);

  const LockedWeekday(this.dateTimeValue);

  final int dateTimeValue;
}

/// Time of day without a calendar date.
class ClockTime {
  const ClockTime({
    required this.hour,
    required this.minute,
  })  : assert(hour >= 0 && hour < 24, 'hour must be 0-23'),
        assert(minute >= 0 && minute < 60, 'minute must be 0-59');

  final int hour;
  final int minute;

  DateTime onDate(DateTime date) {
    return DateTime(date.year, date.month, date.day, hour, minute);
  }
}

/// Recurrence rule for locked time.
class LockedBlockRecurrence {
  const LockedBlockRecurrence.weekly({
    required this.weekdays,
  });

  final Set<LockedWeekday> weekdays;

  bool occursOn(DateTime date) {
    return weekdays.any((weekday) => weekday.dateTimeValue == date.weekday);
  }
}

/// Scheduling constraint that reserves time without becoming a task card.
class LockedBlock {
  const LockedBlock({
    required this.id,
    required this.name,
    required this.startTime,
    required this.endTime,
    required this.createdAt,
    required this.updatedAt,
    this.date,
    this.recurrence,
    this.hiddenByDefault = true,
    this.projectId,
  }) : assert(
          recurrence != null || date != null,
          'date is required for one-off locked blocks',
        );

  final String id;
  final String name;
  final ClockTime startTime;
  final ClockTime endTime;

  /// Calendar date for one-off locked blocks. Recurring blocks leave this null.
  final DateTime? date;
  final LockedBlockRecurrence? recurrence;
  final bool hiddenByDefault;
  final String? projectId;
  final DateTime createdAt;
  final DateTime updatedAt;

  bool get isRecurring => recurrence != null;
}

/// Concrete locked-time occurrence for one calendar day.
class LockedBlockOccurrence {
  const LockedBlockOccurrence({
    required this.name,
    required this.interval,
    required this.hiddenByDefault,
    this.lockedBlockId,
    this.overrideId,
    this.projectId,
  });

  final String name;
  final TimeInterval interval;
  final bool hiddenByDefault;
  final String? lockedBlockId;
  final String? overrideId;
  final String? projectId;

  /// Scheduler-facing interval. Visibility only affects future UI overlays.
  TimeInterval get schedulingInterval {
    return TimeInterval(
      start: interval.start,
      end: interval.end,
      label: name,
    );
  }
}

/// Type of one-day override applied to locked time.
enum LockedBlockOverrideType {
  remove,
  replace,
  add,
}

/// One-day change to locked time that leaves the base recurrence unchanged.
class LockedBlockOverride {
  const LockedBlockOverride({
    required this.id,
    required this.date,
    required this.type,
    required this.createdAt,
    required this.updatedAt,
    this.lockedBlockId,
    this.name,
    this.startTime,
    this.endTime,
    this.hiddenByDefault = true,
    this.projectId,
  });

  /// Removes a recurring occurrence for one date.
  factory LockedBlockOverride.remove({
    required String id,
    required String lockedBlockId,
    required DateTime date,
    required DateTime createdAt,
    DateTime? updatedAt,
  }) {
    return LockedBlockOverride(
      id: id,
      lockedBlockId: lockedBlockId,
      date: date,
      type: LockedBlockOverrideType.remove,
      createdAt: createdAt,
      updatedAt: updatedAt ?? createdAt,
    );
  }

  /// Replaces a recurring occurrence with different times for one date.
  factory LockedBlockOverride.replace({
    required String id,
    required String lockedBlockId,
    required DateTime date,
    required ClockTime startTime,
    required ClockTime endTime,
    required DateTime createdAt,
    String? name,
    bool hiddenByDefault = true,
    String? projectId,
    DateTime? updatedAt,
  }) {
    return LockedBlockOverride(
      id: id,
      lockedBlockId: lockedBlockId,
      date: date,
      type: LockedBlockOverrideType.replace,
      name: name,
      startTime: startTime,
      endTime: endTime,
      hiddenByDefault: hiddenByDefault,
      projectId: projectId,
      createdAt: createdAt,
      updatedAt: updatedAt ?? createdAt,
    );
  }

  /// Adds surprise locked time for one date.
  factory LockedBlockOverride.add({
    required String id,
    required DateTime date,
    required String name,
    required ClockTime startTime,
    required ClockTime endTime,
    required DateTime createdAt,
    bool hiddenByDefault = true,
    String? projectId,
    DateTime? updatedAt,
  }) {
    return LockedBlockOverride(
      id: id,
      date: date,
      type: LockedBlockOverrideType.add,
      name: name,
      startTime: startTime,
      endTime: endTime,
      hiddenByDefault: hiddenByDefault,
      projectId: projectId,
      createdAt: createdAt,
      updatedAt: updatedAt ?? createdAt,
    );
  }

  final String id;
  final String? lockedBlockId;
  final DateTime date;
  final LockedBlockOverrideType type;
  final String? name;
  final ClockTime? startTime;
  final ClockTime? endTime;
  final bool hiddenByDefault;
  final String? projectId;
  final DateTime createdAt;
  final DateTime updatedAt;

  bool appliesTo({
    required String blockId,
    required DateTime occurrenceDate,
  }) {
    return lockedBlockId == blockId && _sameDate(date, occurrenceDate);
  }

  TimeInterval? intervalForDate(DateTime targetDate) {
    final start = startTime;
    final end = endTime;

    if (start == null || end == null || !_sameDate(date, targetDate)) {
      return null;
    }

    return TimeInterval(
      start: start.onDate(targetDate),
      end: end.onDate(targetDate),
      label: name,
    );
  }
}

/// Expands locked blocks and one-day overrides into concrete intervals.
class LockedScheduleExpansion {
  const LockedScheduleExpansion({
    required this.date,
    required this.occurrences,
  });

  final DateTime date;
  final List<LockedBlockOccurrence> occurrences;

  List<TimeInterval> get schedulingIntervals {
    return List<TimeInterval>.unmodifiable(
      occurrences.map((occurrence) => occurrence.schedulingInterval),
    );
  }
}

/// Returns concrete locked-time occurrences for [date].
LockedScheduleExpansion expandLockedBlocksForDay({
  required List<LockedBlock> blocks,
  required List<LockedBlockOverride> overrides,
  required DateTime date,
}) {
  final occurrences = <LockedBlockOccurrence>[];
  final sortedOverrides = [...overrides]..sort((a, b) {
      final createdComparison = a.createdAt.compareTo(b.createdAt);
      if (createdComparison != 0) {
        return createdComparison;
      }

      return a.id.compareTo(b.id);
    });

  for (final block in blocks) {
    if (!_blockOccursOn(block, date)) {
      continue;
    }

    final blockOverrides = sortedOverrides
        .where(
          (override) =>
              override.appliesTo(blockId: block.id, occurrenceDate: date),
        )
        .toList(growable: false);

    if (blockOverrides.any(
      (override) => override.type == LockedBlockOverrideType.remove,
    )) {
      continue;
    }

    final replacement = _lastReplacementOverride(blockOverrides);

    occurrences.add(
      replacement == null
          ? _occurrenceFromBlock(block, date)
          : _occurrenceFromReplacement(
              block: block,
              override: replacement,
              date: date,
            ),
    );
  }

  for (final override in sortedOverrides) {
    if (override.type != LockedBlockOverrideType.add ||
        !_sameDate(override.date, date)) {
      continue;
    }

    final occurrence = _occurrenceFromAddedOverride(override, date);
    if (occurrence != null) {
      occurrences.add(occurrence);
    }
  }

  occurrences.sort((a, b) {
    final startComparison = a.interval.start.compareTo(b.interval.start);
    if (startComparison != 0) {
      return startComparison;
    }

    return a.name.compareTo(b.name);
  });

  return LockedScheduleExpansion(
    date: DateTime(date.year, date.month, date.day),
    occurrences: List<LockedBlockOccurrence>.unmodifiable(occurrences),
  );
}

LockedBlockOverride? _lastReplacementOverride(
  List<LockedBlockOverride> overrides,
) {
  for (final override in overrides.reversed) {
    if (override.type == LockedBlockOverrideType.replace) {
      return override;
    }
  }

  return null;
}

List<TimeInterval> lockedSchedulingIntervalsForDay({
  required List<LockedBlock> blocks,
  required List<LockedBlockOverride> overrides,
  required DateTime date,
}) {
  return expandLockedBlocksForDay(
    blocks: blocks,
    overrides: overrides,
    date: date,
  ).schedulingIntervals;
}

/// Returns [task] with locked-hour completion statistics applied when relevant.
Task trackCompletedDuringLockedHours({
  required Task task,
  required List<TimeInterval> lockedIntervals,
}) {
  final overlapMinutes = completedDuringLockedHoursMinutes(
    task: task,
    lockedIntervals: lockedIntervals,
  );

  if (overlapMinutes == 0) {
    return task;
  }

  return task.copyWith(
    stats: task.stats.incrementCompletedDuringLockedHours(overlapMinutes),
  );
}

/// Calculates how many scheduled task minutes overlap locked intervals.
int completedDuringLockedHoursMinutes({
  required Task task,
  required List<TimeInterval> lockedIntervals,
}) {
  if (!_shouldTrackLockedHourCompletion(task)) {
    return 0;
  }

  final taskInterval = _scheduledIntervalForTask(task);
  if (taskInterval == null) {
    return 0;
  }

  final intersections = <TimeInterval>[];
  for (final lockedInterval in lockedIntervals) {
    if (!taskInterval.overlaps(lockedInterval)) {
      continue;
    }

    intersections.add(
      TimeInterval(
        start: _latest(taskInterval.start, lockedInterval.start),
        end: _earliest(taskInterval.end, lockedInterval.end),
      ),
    );
  }

  if (intersections.isEmpty) {
    return 0;
  }

  intersections.sort((a, b) => a.start.compareTo(b.start));

  var total = Duration.zero;
  var current = intersections.first;
  for (final interval in intersections.skip(1)) {
    if (interval.start.isAfter(current.end)) {
      total += current.duration;
      current = interval;
      continue;
    }

    current = TimeInterval(
      start: current.start,
      end: _latest(current.end, interval.end),
    );
  }

  total += current.duration;

  return total.inMinutes;
}

bool _blockOccursOn(LockedBlock block, DateTime date) {
  final recurrence = block.recurrence;
  if (recurrence != null) {
    return recurrence.occursOn(date);
  }

  return _sameDate(block.date!, date);
}

LockedBlockOccurrence _occurrenceFromBlock(
  LockedBlock block,
  DateTime date,
) {
  return LockedBlockOccurrence(
    lockedBlockId: block.id,
    name: block.name,
    interval: TimeInterval(
      start: block.startTime.onDate(date),
      end: block.endTime.onDate(date),
      label: block.name,
    ),
    hiddenByDefault: block.hiddenByDefault,
    projectId: block.projectId,
  );
}

LockedBlockOccurrence _occurrenceFromReplacement({
  required LockedBlock block,
  required LockedBlockOverride override,
  required DateTime date,
}) {
  final startTime = override.startTime ?? block.startTime;
  final endTime = override.endTime ?? block.endTime;
  final name = override.name ?? block.name;

  return LockedBlockOccurrence(
    lockedBlockId: block.id,
    overrideId: override.id,
    name: name,
    interval: TimeInterval(
      start: startTime.onDate(date),
      end: endTime.onDate(date),
      label: name,
    ),
    hiddenByDefault: override.hiddenByDefault,
    projectId: override.projectId ?? block.projectId,
  );
}

LockedBlockOccurrence? _occurrenceFromAddedOverride(
  LockedBlockOverride override,
  DateTime date,
) {
  final interval = override.intervalForDate(date);
  final name = override.name;

  if (interval == null || name == null) {
    return null;
  }

  return LockedBlockOccurrence(
    overrideId: override.id,
    name: name,
    interval: TimeInterval(
      start: interval.start,
      end: interval.end,
      label: name,
    ),
    hiddenByDefault: override.hiddenByDefault,
    projectId: override.projectId,
  );
}

bool _shouldTrackLockedHourCompletion(Task task) {
  return task.status == TaskStatus.completed || task.type == TaskType.surprise;
}

TimeInterval? _scheduledIntervalForTask(Task task) {
  final start = task.scheduledStart;
  final end = task.scheduledEnd;

  if (start == null || end == null || !start.isBefore(end)) {
    return null;
  }

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

DateTime _earliest(DateTime first, DateTime second) {
  return first.isBefore(second) ? first : second;
}

DateTime _latest(DateTime first, DateTime second) {
  return first.isAfter(second) ? first : second;
}

bool _sameDate(DateTime first, DateTime second) {
  return first.year == second.year &&
      first.month == second.month &&
      first.day == second.day;
}
