import 'dart:developer';

import '../../../../../api/marianumconnect/queries/timetable_get_element_week/timetable_element_type.dart';
import '../../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
import '../../../../../extensions/date_time.dart';
import '../../../infrastructure/loadable_state/loadable_state.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 '../../timetable/bloc/timetable_event.dart';
import '../../timetable/bloc/timetable_state.dart';
import '../repository/foreign_timetable_repository.dart';

/// Drives a foreign element's timetable. Mirrors `TimetableBloc`'s week-loading
/// and navigation but loads weeks from the element endpoint, carries no custom
/// events, and does not persist (page-scoped, recreated per element). Reuses
/// [TimetableState] verbatim so the render pipeline is unchanged; `customEvents`
/// stays null (the foreign view's `isReady` predicate ignores it).
class ForeignTimetableBloc
    extends
        LoadableHydratedBloc<
          TimetableEvent,
          TimetableState,
          ForeignTimetableRepository
        > {

  final TimetableElementType type;
  // Named `elementId` rather than `id` to avoid shadowing HydratedMixin's
  // `String get id` (the storage key), which a plain `int id` would illegally
  // override.
  final int elementId;
  final String title;

  DateTime _lastWeekRequestStart = DateTime.fromMillisecondsSinceEpoch(0);

  ForeignTimetableBloc({
    required this.type,
    required this.elementId,
    required this.title,
  });

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

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

  // Persistence disabled: page-scoped and element-specific, nothing worth
  // restoring. toJson returns null so nothing is written; fromJson starts fresh.
  @override
  Map<String, dynamic>? toJson(LoadableState<TimetableState> state) => null;

  @override
  LoadableState<TimetableState> fromJson(Map<String, dynamic> json) =>
      const LoadableState(
        isLoading: true,
        data: null,
        lastFetch: null,
        reFetch: null,
        error: null,
      );

  @override
  TimetableState fromStorage(Map<String, dynamic> json) => fromNothing();

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

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

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

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

    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> _loadCurrentWeek(
    DateTime startDate,
    DateTime endDate, {
    void Function(Object)? onError,
  }) async {
    final requestStart = DateTime.now();
    _lastWeekRequestStart = requestStart;
    try {
      final week = await repo.data.getElementWeek(
        type,
        elementId,
        startDate,
        endDate,
        onError: onError,
      );
      if (_lastWeekRequestStart.isAfter(requestStart)) return;
      _writeWeekToCache(startDate, week);
    } catch (e) {
      log('getElementWeek error for $startDate–$endDate: $e');
      onError?.call(e);
    }
  }

  Future<void> _loadStaticReferenceData({
    void Function(Object)? onError,
  }) async {
    try {
      final (rooms, subjects, schoolHolidays, schoolyear) = await (
        repo.data.getRooms(onError: onError),
        repo.data.getSubjects(onError: onError),
        repo.data.getSchoolHolidays(onError: onError),
        repo.data.getCurrentSchoolyear(onError: onError),
      ).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();
      add(
        Emit(
          (s) => s.copyWith(timegrid: timegrid, dataVersion: s.dataVersion + 1),
        ),
      );
    } catch (_) {
      // Timegrid load failure falls back to a hardcoded schedule in the UI.
    }
  }

  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
        .getElementWeek(type, elementId, 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);
  }
}
