import 'dart:developer';

import '../../../../../api/marianumconnect/queries/timetable_custom_events/custom_events_migration.dart';
import '../../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
import '../../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
import '../../../../../extensions/date_time.dart';
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart';
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
import '../repository/timetable_repository.dart';
import 'timetable_event.dart';
import 'timetable_state.dart';

class TimetableBloc
    extends
        LoadableHydratedBloc<
          TimetableEvent,
          TimetableState,
          TimetableRepository
        > {
  DateTime _lastWeekRequestStart = DateTime.fromMillisecondsSinceEpoch(0);

  /// Set by [retry] to force the next [gatherData] to bypass cache freshness
  /// checks and actually hit the network. Cleared at the top of [gatherData].
  bool _forceRenew = false;

  @override
  void retry() {
    _forceRenew = true;
    super.retry();
  }

  @override
  TimetableRepository repository() => TimetableRepository();

  @override
  TimetableState fromNothing() {
    final reference = DateTime.now().addDays(2);
    return TimetableState(
      startDate: _startOfWeek(reference),
      endDate: _endOfWeek(reference),
    );
  }

  /// Persisted state may carry a stale `startDate`/`endDate` from the user's
  /// last view. Reset on every cold start so the calendar always mounts on
  /// the current week, not on whatever week the user closed the app on.
  @override
  TimetableState fromStorage(Map<String, dynamic> json) {
    final stored = TimetableState.fromJson(json);
    final reference = DateTime.now().addDays(2);
    return stored.copyWith(
      startDate: _startOfWeek(reference),
      endDate: _endOfWeek(reference),
      accessibleStartDate: null,
      accessibleEndDate: null,
    );
  }

  @override
  Map<String, dynamic>? toStorage(TimetableState state) => state.toJson();

  @override
  Future<void> gatherData() async {
    final initial = innerState ?? fromNothing();
    final renew = _forceRenew;
    _forceRenew = false;

    Object? firstError;
    void recordError(Object e) {
      firstError ??= e;
    }

    await Future.wait([
      _loadCurrentWeek(
        initial.startDate,
        initial.endDate,
        onError: recordError,
        renew: renew,
      ),
      _loadStaticReferenceData(onError: recordError, renew: renew),
      _loadCustomEvents(onError: recordError, renew: renew),
    ]);

    if (firstError != null) throw firstError!;

    add(DataGathered((s) => s));
    _prefetchAdjacentWeeks(initial.startDate, initial.endDate);
  }

  void changeWeek(DateTime startDate, DateTime endDate) {
    final current = innerState ?? fromNothing();
    if (current.startDate == startDate && current.endDate == endDate) return;
    add(Emit((s) => s.copyWith(startDate: startDate, endDate: endDate)));
    _loadCurrentWeek(startDate, endDate);
    _prefetchAdjacentWeeks(startDate, endDate);
  }

  void resetWeek() {
    final reference = DateTime.now().addDays(2);
    changeWeek(_startOfWeek(reference), _endOfWeek(reference));
  }

  void refresh() => fetch();

  Future<void> addCustomEvent(CustomTimetableEvent event) async {
    await repo.data.addCustomEvent(event);
    await _refreshCustomEvents();
  }

  Future<void> updateCustomEvent(String id, CustomTimetableEvent event) async {
    await repo.data.updateCustomEvent(id, event);
    await _refreshCustomEvents();
  }

  Future<void> removeCustomEvent(String id) async {
    await repo.data.removeCustomEvent(id);
    await _refreshCustomEvents();
  }

  /// Setzt die persönliche Farbe eines Fachs (Schlüssel: Fach-Kürzel) und lädt
  /// die Fächer neu, damit die eingebettete Farbe im Kalender wirkt.
  Future<void> setSubjectColor(String subjectShort, String color) async {
    await repo.data.setSubjectColor(subjectShort, color);
    await _refreshSubjects();
  }

  Future<void> clearSubjectColor(String subjectShort) async {
    await repo.data.removeSubjectColor(subjectShort);
    await _refreshSubjects();
  }

  Future<void> _refreshSubjects() async {
    final subjects = await repo.data.getSubjects(renew: true);
    add(
      DataGathered(
        (s) => s.copyWith(subjects: subjects, dataVersion: s.dataVersion + 1),
      ),
    );
  }

  Future<void> _loadCurrentWeek(
    DateTime startDate,
    DateTime endDate, {
    void Function(Object)? onError,
    bool renew = false,
  }) async {
    final requestStart = DateTime.now();
    _lastWeekRequestStart = requestStart;
    try {
      final week = await repo.data.getWeek(
        startDate,
        endDate,
        onError: onError,
        renew: renew,
      );
      if (_lastWeekRequestStart.isAfter(requestStart)) return;
      _writeWeekToCache(startDate, week);
    } catch (e) {
      log('getWeek error for $startDate–$endDate: $e');
      onError?.call(e);
    }
  }

  Future<void> _loadStaticReferenceData({
    void Function(Object)? onError,
    bool renew = false,
  }) async {
    try {
      final (rooms, subjects, schoolHolidays, schoolyear) = await (
        repo.data.getRooms(onError: onError, renew: renew),
        repo.data.getSubjects(onError: onError, renew: renew),
        repo.data.getSchoolHolidays(onError: onError, renew: renew),
        repo.data.getCurrentSchoolyear(onError: onError, renew: renew),
      ).wait;

      add(
        Emit(
          (s) => s.copyWith(
            rooms: rooms,
            subjects: subjects,
            schoolHolidays: schoolHolidays,
            schoolyear: schoolyear,
            dataVersion: s.dataVersion + 1,
          ),
        ),
      );
    } catch (e) {
      onError?.call(e);
    }

    try {
      final timegrid = await repo.data.getTimegrid(renew: renew);
      add(
        Emit(
          (s) => s.copyWith(timegrid: timegrid, dataVersion: s.dataVersion + 1),
        ),
      );
    } catch (_) {
      // Timegrid load failure falls back to a hardcoded schedule in the UI layer.
    }
  }

  Future<void> _loadCustomEvents({
    void Function(Object)? onError,
    bool renew = false,
  }) async {
    try {
      await CustomEventsMigration.runOnce();
      final events = await repo.data.getCustomEvents(
        renew: renew,
        onError: onError,
      );
      add(
        Emit(
          (s) =>
              s.copyWith(customEvents: events, dataVersion: s.dataVersion + 1),
        ),
      );
    } catch (e) {
      onError?.call(e);
    }
  }

  Future<void> _refreshCustomEvents() async {
    final events = await repo.data.getCustomEvents(renew: true);
    add(
      DataGathered(
        (s) => s.copyWith(customEvents: events, dataVersion: s.dataVersion + 1),
      ),
    );
  }

  void _prefetchAdjacentWeeks(DateTime start, DateTime end) {
    _prefetchWeek(start.subtractDays(7), end.subtractDays(7));
    _prefetchWeek(start.addDays(7), end.addDays(7));
  }

  void _prefetchWeek(DateTime start, DateTime end) {
    repo.data
        .getWeek(start, end)
        .then((week) => _writeWeekToCache(start, week))
        .catchError((_) {});
  }

  void _writeWeekToCache(DateTime weekStart, TimetableGetWeekResponse week) {
    final key = weekStart.weekKey();
    add(
      Emit((s) {
        final updated = Map<String, TimetableGetWeekResponse>.of(s.weekCache);
        updated[key] = week;
        return s.copyWith(weekCache: updated, dataVersion: s.dataVersion + 1);
      }),
    );
  }

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

  static DateTime _endOfWeek(DateTime reference) {
    final friday = reference.addDays(
      DateTime.daysPerWeek - reference.weekday - 2,
    );
    return DateTime(friday.year, friday.month, friday.day);
  }
}
