import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';

import '../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
import '../../../../extensions/date_time.dart';
import '../../../../state/app/modules/capabilities/bloc/capabilities_cubit.dart';
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../../state/app/modules/timetable/bloc/timetable_state.dart';
import '../../../../storage/timetable_settings.dart';
import '../data/arbitrary_appointment.dart';
import '../data/lesson_period_schedule.dart';
import '../data/timetable_appointment_factory.dart';
import 'custom_workweek_calendar.dart';
import 'special_regions_builder.dart';

/// Renders a weekly timetable from a [TimetableState]. Shared by the user's own
/// plan and the foreign-element view; the only differences are which custom
/// events to overlay (none for foreign plans) and whether tapping an empty slot
/// can create an event ([onCreateEvent] is null for read-only foreign plans).
///
/// The week navigation and appointment-tap callbacks are supplied by the host
/// page so each can route them to its own bloc.
class TimetableCalendarView extends StatefulWidget {
  final TimetableState state;
  final void Function(DateTime weekStart, DateTime weekEnd) onWeekChanged;
  final void Function(Appointment appointment) onAppointmentTap;
  final void Function(DateTime start, DateTime end)? onCreateEvent;
  final List<CustomTimetableEvent> customEvents;

  /// True for teacher plans — tiles then show the class instead of the
  /// teacher name (see [TimetableAppointmentFactory.showClassInsteadOfTeacher]).
  final bool showClassInsteadOfTeacher;

  const TimetableCalendarView({
    super.key,
    required this.state,
    required this.onWeekChanged,
    required this.onAppointmentTap,
    this.onCreateEvent,
    this.customEvents = const [],
    this.showClassInsteadOfTeacher = false,
  });

  @override
  State<TimetableCalendarView> createState() => TimetableCalendarViewState();
}

class TimetableCalendarViewState extends State<TimetableCalendarView> {
  final GlobalKey<CustomWorkWeekCalendarState> _calendarKey =
      GlobalKey<CustomWorkWeekCalendarState>();

  List<Appointment>? _cachedAppointments;
  // TimetableSettings and List define no `==`, so record equality degrades to
  // the same identity checks the cache always used.
  (int, TimetableSettings, List<CustomTimetableEvent>, bool)? _cacheKey;

  DateTime _initialDisplayDate() => DateTime.now().addDays(2);

  /// Snaps the calendar back to the current week. Exposed so host pages can
  /// wire it to a "today" AppBar action.
  void jumpToToday() {
    _calendarKey.currentState?.jumpToDate(_initialDisplayDate());
  }

  List<Appointment> _appointments(TimetableState state) {
    final timetableSettings = context
        .watch<SettingsCubit>()
        .val()
        .timetableSettings;
    final key = (
      state.dataVersion,
      timetableSettings,
      widget.customEvents,
      widget.showClassInsteadOfTeacher,
    );
    if (_cachedAppointments != null && _cacheKey == key) {
      return _cachedAppointments!;
    }
    _cacheKey = key;

    return _cachedAppointments = TimetableAppointmentFactory(
      lessons: state.getAllKnownLessons().toList(),
      customEvents: widget.customEvents,
      subjects: state.subjects?.result ?? const [],
      holidays: state.schoolHolidays?.result ?? const [],
      settings: timetableSettings,
      now: DateTime.now(),
      showClassInsteadOfTeacher: widget.showClassInsteadOfTeacher,
    ).build();
  }

  bool _isCrossedOut(Appointment appointment) {
    final id = appointment.id;
    if (id is LessonAppointment) return id.entry.status == 'CANCELLED';
    return false;
  }

  @override
  Widget build(BuildContext context) {
    final state = widget.state;
    // Reference data is gated by the host's LoadableStateConsumer isReady
    // predicate, but guard the one hard dereference (schoolHolidays) so a
    // transient null can never crash the build.
    if (state.schoolHolidays == null) return const SizedBox.shrink();

    final schedule = LessonPeriodSchedule.fromState(state);
    final appointments = _appointments(state);
    final regions = SpecialRegionsBuilder(
      holidays: state.schoolHolidays!,
      schedule: schedule,
      colorScheme: Theme.of(context).colorScheme,
      disabledColor: Theme.of(context).disabledColor,
    ).build();

    final capabilities = context.watch<CapabilitiesCubit>();
    final (minDate, maxDate) = _scrollBounds(
      state,
      pastDays: capabilities.timetablePastDays,
      futureDays: capabilities.timetableFutureDays,
    );

    return CustomWorkWeekCalendar(
      key: _calendarKey,
      schedule: schedule,
      appointments: appointments,
      timeRegions: regions,
      initialDate: _initialDisplayDate(),
      minDate: minDate,
      maxDate: maxDate,
      onAppointmentTap: widget.onAppointmentTap,
      onWeekChanged: widget.onWeekChanged,
      isCrossedOut: _isCrossedOut,
      onCreateEvent: widget.onCreateEvent,
    );
  }

  /// Returns the (minDate, maxDate) the user is allowed to scroll between.
  /// Starts from the (server-narrowed) Webuntis school year — or a tight window
  /// when that hasn't loaded yet —, tightens by anything the bloc has learned
  /// from past denials, and finally clamps to the window Connect grants via
  /// [CapabilitiesCubit.timetablePastDays]/[CapabilitiesCubit.timetableFutureDays].
  ///
  /// The capability clamp mirrors the server: `null` means unlimited (no client
  /// clamp at all), and a given day count is widened to at least cover the
  /// current Mon–Sun week — so the two windows always coincide and the clamp
  /// can never invert.
  (DateTime, DateTime) _scrollBounds(
    TimetableState state, {
    required int? pastDays,
    required int? futureDays,
  }) {
    final year = state.schoolyear;
    final DateTime baseMin;
    final DateTime baseMax;
    if (year != null) {
      baseMin = year.startDate;
      baseMax = year.endDate;
    } else {
      final now = DateTime.now();
      baseMin = now.subtractDays(14);
      baseMax = now.addDays(7);
    }
    final effectiveMin = state.accessibleStartDate != null
        ? (state.accessibleStartDate!.isAfter(baseMin)
              ? state.accessibleStartDate!
              : baseMin)
        : baseMin;
    final effectiveMax = state.accessibleEndDate != null
        ? (state.accessibleEndDate!.isBefore(baseMax)
              ? state.accessibleEndDate!
              : baseMax)
        : baseMax;
    final today = _startOfDay(DateTime.now());
    final todayMonday = _mondayOf(today);
    final currentWeekEnd = todayMonday.addDays(DateTime.daysPerWeek - 1);
    final capMin = pastDays == null
        ? null
        : _earlier(today.subtractDays(pastDays), todayMonday);
    final capMax = futureDays == null
        ? null
        : _later(today.addDays(futureDays), currentWeekEnd);
    final cappedMin = capMin != null && effectiveMin.isBefore(capMin)
        ? capMin
        : effectiveMin;
    final cappedMax = capMax != null && effectiveMax.isAfter(capMax)
        ? capMax
        : effectiveMax;
    // When the resulting range does not cover the current week — the summer gap
    // between two school years, or a stale persisted bound — fall back to the
    // current week, widened to whatever the capabilities still allow. Otherwise
    // the PageView clamps the initial page to the last week before the holidays
    // (hiding the "Schulfrei" region) and forward scrolling collapses to the
    // current week only.
    final outsideRange =
        cappedMax.isBefore(todayMonday) || cappedMin.isAfter(currentWeekEnd);
    final finalMin = outsideRange ? (capMin ?? todayMonday) : cappedMin;
    final finalMax = outsideRange ? (capMax ?? currentWeekEnd) : cappedMax;
    final daysToMonday =
        (DateTime.monday - finalMin.weekday) % DateTime.daysPerWeek;
    final mondayMin = finalMin.addDays(daysToMonday);
    return (mondayMin, finalMax);
  }

  static DateTime _mondayOf(DateTime d) =>
      _startOfDay(d.subtractDays(d.weekday - 1));

  static DateTime _startOfDay(DateTime d) => DateTime(d.year, d.month, d.day);

  static DateTime _earlier(DateTime a, DateTime b) => a.isBefore(b) ? a : b;

  static DateTime _later(DateTime a, DateTime b) => a.isAfter(b) ? a : b;
}
