/// Custom 5-day work-week calendar (replaces Syncfusion's `WorkWeek` view).
///
/// Implementation is split across `calendar/` for readability; everything
/// stays in this single library so private widgets and helpers (`_DayColumn`,
/// `_PeriodLayout`, `_isAllDayLike`, …) can remain library-private.
library;

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:intl/intl.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';

import '../../../../extensions/date_time.dart';
import '../../../../utils/haptics.dart';
import '../../../../widget/a11y/a11y_labels.dart';
import '../../../../widget/details_bottom_sheet.dart';
import '../data/calendar_layout.dart';
import '../data/calendar_logic.dart';
import '../data/lesson_period_schedule.dart';
import 'appointment_tile.dart';
import 'time_region_tile.dart';

part 'calendar/day_header.dart';
part 'calendar/outside_chips.dart';
part 'calendar/week_grid.dart';

class CustomWorkWeekCalendar extends StatefulWidget {
  final LessonPeriodSchedule schedule;
  final List<Appointment> appointments;
  final List<TimeRegion> timeRegions;
  final DateTime initialDate;
  final DateTime minDate;
  final DateTime maxDate;
  final void Function(Appointment appointment) onAppointmentTap;
  final void Function(DateTime weekStart, DateTime weekEnd) onWeekChanged;
  final bool Function(Appointment appointment) isCrossedOut;
  final void Function(DateTime start, DateTime end)? onCreateEvent;

  const CustomWorkWeekCalendar({
    super.key,
    required this.schedule,
    required this.appointments,
    required this.timeRegions,
    required this.initialDate,
    required this.minDate,
    required this.maxDate,
    required this.onAppointmentTap,
    required this.onWeekChanged,
    required this.isCrossedOut,
    this.onCreateEvent,
  });

  @override
  State<CustomWorkWeekCalendar> createState() => CustomWorkWeekCalendarState();
}

class CustomWorkWeekCalendarState extends State<CustomWorkWeekCalendar> {
  static const double _rulerWidth = 36;

  late PageController _pageController;
  late int _currentWeekIndex;
  late DateTime _firstMonday;
  late int _totalWeeks;
  late Timer _ticker;
  late ValueNotifier<DateTime> _nowNotifier;
  DateTime _today = _dateOnly(DateTime.now());

  @override
  void initState() {
    super.initState();
    _firstMonday = _mondayOf(widget.minDate);
    final lastMonday = _mondayOf(widget.maxDate);
    _totalWeeks = lastMonday.difference(_firstMonday).inDays ~/ 7 + 1;
    _currentWeekIndex =
        _mondayOf(widget.initialDate).difference(_firstMonday).inDays ~/ 7;
    _pageController = PageController(initialPage: _currentWeekIndex);
    _nowNotifier = ValueNotifier<DateTime>(DateTime.now());

    _ticker = Timer.periodic(const Duration(seconds: 30), (_) {
      if (!mounted) return;
      final now = DateTime.now();
      _nowNotifier.value = now;
      final newToday = _dateOnly(now);
      if (newToday != _today) setState(() => _today = newToday);
    });
  }

  @override
  void didUpdateWidget(covariant CustomWorkWeekCalendar oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.minDate == oldWidget.minDate &&
        widget.maxDate == oldWidget.maxDate) {
      return;
    }
    // Boundaries changed (e.g. school-year payload finished loading after
    // the initial mount with the conservative fallback). Recompute the
    // range and snap the controller to the page that still represents the
    // currently visible week so the user doesn't get yanked around.
    final newFirstMonday = _mondayOf(widget.minDate);
    final newLastMonday = _mondayOf(widget.maxDate);
    final newTotalWeeks =
        newLastMonday.difference(newFirstMonday).inDays ~/ 7 + 1;
    final visibleWeekStart = _firstMonday.addDays(_currentWeekIndex * 7);
    final newIndex = visibleWeekStart.difference(newFirstMonday).inDays ~/ 7;
    final clampedIndex = newIndex.clamp(0, newTotalWeeks - 1);
    final oldController = _pageController;
    _pageController = PageController(initialPage: clampedIndex);
    oldController.dispose();
    setState(() {
      _firstMonday = newFirstMonday;
      _totalWeeks = newTotalWeeks;
      _currentWeekIndex = clampedIndex;
    });
  }

  @override
  void dispose() {
    _pageController.dispose();
    _ticker.cancel();
    _nowNotifier.dispose();
    super.dispose();
  }

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

  void jumpToDate(DateTime date) {
    final target = _mondayOf(date).difference(_firstMonday).inDays ~/ 7;
    if (target < 0 || target >= _totalWeeks) return;
    _pageController.animateToPage(
      target,
      duration: const Duration(milliseconds: 380),
      curve: Curves.easeInOutCubic,
    );
  }

  static DateTime _mondayOf(DateTime d) {
    final monday = d.subtractDays(d.weekday - 1);
    return DateTime(monday.year, monday.month, monday.day);
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final visibleWeekStart = _firstMonday.addDays(_currentWeekIndex * 7);

    return Column(
      children: [
        SizedBox(
          height: kCalendarViewHeaderHeight,
          child: AnimatedSwitcher(
            duration: const Duration(milliseconds: 220),
            switchInCurve: Curves.easeOut,
            switchOutCurve: Curves.easeIn,
            transitionBuilder: (child, animation) => FadeTransition(
              opacity: animation,
              child: SlideTransition(
                position: Tween<Offset>(
                  begin: const Offset(0, -0.08),
                  end: Offset.zero,
                ).animate(animation),
                child: child,
              ),
            ),
            // Die visuelle „MO/14"-Leiste ist für den Screenreader unbrauchbar;
            // die Tag-Info wird stattdessen als Header in jede Tagesspalte
            // eingehängt (siehe _DayColumn).
            child: ExcludeSemantics(
              child: _DayHeaderStrip(
                key: ValueKey(visibleWeekStart),
                weekStart: visibleWeekStart,
                today: _today,
                rulerWidth: _rulerWidth,
              ),
            ),
          ),
        ),
        ClipRect(
          child: AnimatedSize(
            duration: const Duration(milliseconds: 320),
            curve: Curves.easeOutCubic,
            alignment: Alignment.topCenter,
            child: AnimatedSwitcher(
              duration: const Duration(milliseconds: 280),
              switchInCurve: Curves.easeOutCubic,
              switchOutCurve: Curves.easeInCubic,
              transitionBuilder: (child, animation) =>
                  FadeTransition(opacity: animation, child: child),
              child: _OutsideHoursStrip(
                key: ValueKey(visibleWeekStart),
                weekStart: visibleWeekStart,
                appointments: widget.appointments,
                rulerWidth: _rulerWidth,
                onAppointmentTap: widget.onAppointmentTap,
                isCrossedOut: widget.isCrossedOut,
              ),
            ),
          ),
        ),
        Container(height: 0.5, color: theme.dividerColor.withAlpha(110)),
        Expanded(
          child: LayoutBuilder(
            builder: (context, constraints) {
              final periods = widget.schedule.periods;
              final lessonCount = periods.where((p) => !p.isBreak).length;
              final breakCount = periods.length - lessonCount;
              final available =
                  constraints.maxHeight - breakCount * kBreakBlockHeight;
              final fitLessonH = lessonCount > 0
                  ? available / lessonCount
                  : kLessonBlockMinHeight;
              final lessonH = fitLessonH < kLessonBlockMinHeight
                  ? kLessonBlockMinHeight
                  : fitLessonH;
              final layout = PeriodLayout(
                periods: periods,
                lessonHeight: lessonH,
                breakHeight: kBreakBlockHeight,
              );
              final gridHeight = layout.totalHeight;

              return SingleChildScrollView(
                physics: const AlwaysScrollableScrollPhysics(),
                child: SizedBox(
                  height: gridHeight,
                  child: PageView.builder(
                    controller: _pageController,
                    itemCount: _totalWeeks,
                    onPageChanged: (index) {
                      setState(() => _currentWeekIndex = index);
                      final weekStart = _firstMonday.addDays(index * 7);
                      widget.onWeekChanged(weekStart, weekStart.addDays(4));
                    },
                    itemBuilder: (_, weekIndex) {
                      final weekStart = _firstMonday.addDays(weekIndex * 7);
                      return _WeekGrid(
                        weekStart: weekStart,
                        schedule: widget.schedule,
                        appointments: widget.appointments,
                        timeRegions: widget.timeRegions,
                        onAppointmentTap: widget.onAppointmentTap,
                        isCrossedOut: widget.isCrossedOut,
                        onCreateEvent: widget.onCreateEvent,
                        today: _today,
                        nowNotifier: _nowNotifier,
                        rulerWidth: _rulerWidth,
                        layout: layout,
                      );
                    },
                  ),
                ),
              );
            },
          ),
        ),
      ],
    );
  }
}
