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

import '../../../../api/marianumconnect/models/mc_holiday.dart';
import '../../../../api/marianumconnect/queries/timetable_get_subjects/timetable_get_subjects_response.dart';
import '../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
import '../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
import '../../../../storage/timetable_settings.dart';
import 'arbitrary_appointment.dart';
import 'lesson_color.dart';
import 'lesson_labels.dart';
import 'lesson_merger.dart';
import 'lesson_status.dart';
import 'lesson_type_label.dart';
import 'rrule_with_exceptions.dart';
import 'subject_color_palette.dart';
import 'timetable_name_mode.dart';

class TimetableAppointmentFactory {
  final List<McTimetableEntry> lessons;
  final List<CustomTimetableEvent> customEvents;
  final List<McSubject> subjects;
  final List<McHoliday> holidays;
  final TimetableSettings settings;
  final DateTime now;

  /// Teacher plans (a teacher's own plan or a foreign teacher view) show the
  /// class on the tile instead of the teacher's own name.
  final bool showClassInsteadOfTeacher;

  TimetableAppointmentFactory({
    required this.lessons,
    required this.customEvents,
    required this.subjects,
    required this.settings,
    required this.now,
    this.holidays = const [],
    this.showClassInsteadOfTeacher = false,
  });

  List<Appointment> build() {
    final source = settings.connectDoubleLessons
        ? LessonMerger.merge(lessons)
        : lessons;
    return [
      ...source.map(_lessonToAppointment),
      ...customEvents.map(_customEventToAppointment),
    ];
  }

  Appointment _lessonToAppointment(McTimetableEntry lesson) {
    try {
      final startTime = lesson.startDateTime;
      final endTime = lesson.endDateTime;
      final subjectShortName = lesson.subjects.firstOrNull;
      // "Event"-Status nur, wenn auch kein bekannter Sonder-Lesson-Type vorliegt
      // — Aufsicht/Sprechstunde/etc. werden sonst grün statt eigenständig
      // eingefärbt.
      final isEvent = subjectShortName == null && lesson.lessonType == 'LESSON';
      final isDuty = lesson.lessonType != 'LESSON';
      final status = LessonStatusClassifier.classify(
        lesson,
        startTime,
        endTime,
        now,
        isEvent: isEvent,
        isDuty: isDuty,
      );

      return Appointment(
        id: LessonAppointment(lesson),
        startTime: startTime,
        endTime: endTime,
        subject: _subjectName(subjectShortName, lesson),
        location: _locationLabel(lesson),
        color: _lessonColor(status, subjectShortName),
      );
    } catch (_) {
      return Appointment(
        id: LessonAppointment(lesson),
        startTime: lesson.startDateTime,
        endTime: lesson.endDateTime,
        subject: 'Änderung',
        notes: lesson.infoText,
        location: 'Unbekannt',
        color: LessonColor.parseFallback,
        startTimeZone: '',
        endTimeZone: '',
      );
    }
  }

  Appointment _customEventToAppointment(CustomTimetableEvent event) {
    final allDay = isCustomEventAllDay(event);
    // rrule_generator stores excluded dates as a non-standard ";EXDATE=..."
    // suffix on the rule string. Strip it and feed the dates separately —
    // Syncfusion matches recurrence exceptions through this property, not
    // through anything inside `recurrenceRule`.
    final parsed = parseRRuleWithExceptions(event.rrule);
    final exceptionDates = parsed.exceptions
        .map(
          (d) => DateTime(
            d.year,
            d.month,
            d.day,
            event.startDate.hour,
            event.startDate.minute,
            event.startDate.second,
          ),
        )
        .toList();
    // Ferien ausblenden ist nur für Serien sinnvoll; Syncfusion matcht
    // Ausnahmen über recurrenceExceptionDates. Überzählige Ferientage, an denen
    // gar keine Occurrence liegt, ignoriert es folgenlos — daher genügt es, für
    // jeden Ferientag ein Ausnahmedatum zur Terminzeit einzustreuen, ohne die
    // RRULE selbst expandieren zu müssen.
    if (event.skipHolidays && parsed.rule.isNotEmpty) {
      exceptionDates.addAll(_holidayExceptionDates(event));
    }
    return Appointment(
      id: CustomAppointment(event),
      startTime: event.startDate,
      endTime: allDay
          ? DateTime(
              event.startDate.year,
              event.startDate.month,
              event.startDate.day,
              23,
              59,
            )
          : event.endDate,
      isAllDay: allDay,
      // Preserve user-entered newlines in descriptions; the tile soft-wraps to
      // fill the available height. For lessons we still collapse whitespace
      // so room/teacher stay on one line each.
      location: event.description.trim().isEmpty
          ? null
          : event.description.trim(),
      subject: collapseWhitespace(event.title) ?? event.title,
      recurrenceRule: parsed.rule,
      recurrenceExceptionDates: exceptionDates.isEmpty ? null : exceptionDates,
      color:
          SubjectColorPalette.colorFromName(event.color) ??
          SubjectColorPalette.displayOf(SubjectColorPalette.defaultColor).color,
      startTimeZone: '',
      endTimeZone: '',
    );
  }

  List<DateTime> _holidayExceptionDates(CustomTimetableEvent event) {
    final result = <DateTime>[];
    for (final holiday in holidays) {
      var day = DateTime(
        holiday.startDate.year,
        holiday.startDate.month,
        holiday.startDate.day,
      );
      final last = DateTime(
        holiday.endDate.year,
        holiday.endDate.month,
        holiday.endDate.day,
      );
      while (!day.isAfter(last)) {
        result.add(
          DateTime(
            day.year,
            day.month,
            day.day,
            event.startDate.hour,
            event.startDate.minute,
            event.startDate.second,
          ),
        );
        day = DateTime(day.year, day.month, day.day + 1);
      }
    }
    return result;
  }

  /// All-day convention: a `CustomTimetableEvent` is treated as all-day when
  /// its `startDate` and `endDate` both land on midnight of the same day.
  /// Keeps the backend schema unchanged — the editor stores all-day events as
  /// `start == end == midnight(date)`.
  static bool isCustomEventAllDay(CustomTimetableEvent event) {
    final s = event.startDate;
    final e = event.endDate;
    return s.year == e.year &&
        s.month == e.month &&
        s.day == e.day &&
        s.hour == 0 &&
        s.minute == 0 &&
        s.second == 0 &&
        e.hour == 0 &&
        e.minute == 0 &&
        e.second == 0;
  }

  /// Regelstunden (regulär/laufend/vergangen) übernehmen die persönliche
  /// Fach-Farbe, sofern gesetzt. Sonderstatus (Entfall, Vertretung, Aufsicht,
  /// Event) behalten immer ihre Signalfarbe, damit die Abgrenzung erhalten
  /// bleibt.
  Color _lessonColor(LessonStatus status, String? subjectShort) {
    final isRegular =
        status == LessonStatus.regular ||
        status == LessonStatus.ongoing ||
        status == LessonStatus.past;
    if (isRegular) {
      final custom = SubjectColorPalette.colorFromName(
        _findSubject(subjectShort)?.color,
      );
      if (custom != null) return custom;
    }
    return LessonColor.forStatus(status);
  }

  McSubject? _findSubject(String? subjectShort) {
    if (subjectShort == null) return null;
    return subjects.where((s) => s.shortName == subjectShort).firstOrNull;
  }

  String _subjectName(String? subjectShort, McTimetableEntry lesson) {
    if (subjectShort != null) {
      final lookup = _findSubject(subjectShort);
      final name = switch (settings.timetableNameMode) {
        // Backend liefert nur shortName + longName; alternateName fällt auf
        // longName zurück.
        TimetableNameMode.name => subjectShort,
        TimetableNameMode.longName => lookup?.longName ?? subjectShort,
        TimetableNameMode.alternateName => lookup?.longName ?? subjectShort,
      };
      final collapsed = collapseWhitespace(name);
      if (collapsed != null) return collapsed;
    }
    // Subject leer → Titel aus dem Lesson-Type ableiten. Pausenaufsicht etc.
    // sollen nicht generisch als "Event" auftauchen, sondern ihren Zweck samt
    // Ort tragen.
    return LessonTypeLabel.forEntry(lesson);
  }

  String _locationLabel(McTimetableEntry lesson) {
    final roomName =
        collapseWhitespace(lesson.rooms.firstOrNull) ?? 'Unbekannt';
    // Klassenlose Einträge (Aufsichten etc.) fallen auf den Lehrer zurück.
    final secondLine =
        (showClassInsteadOfTeacher ? lesson.classLabel : null) ??
        _teacherLabel(lesson.teachers.firstOrNull) ??
        'Unbekannt';
    return '$roomName\n$secondLine';
  }

  /// Backend serves teachers with their full display name ("Stefan Müller"),
  /// which doesn't fit into a single calendar tile alongside the room. We
  /// reduce it to the last whitespace-separated token (the surname) for the
  /// overview; the detail sheet still renders the full name as a subtitle.
  static String? _teacherLabel(McTimetableTeacher? teacher) {
    if (teacher == null) return null;
    final display = collapseWhitespace(teacher.displayName);
    if (display != null && display.isNotEmpty) {
      final parts = display.split(' ');
      return parts.isEmpty ? display : parts.last;
    }
    return collapseWhitespace(teacher.shortName);
  }
}
