import 'dart:developer';

import 'package:hydrated_bloc/hydrated_bloc.dart';

import '../../../../../api/errors/error_mapper.dart';
import '../../loadable_state/loadable_state.dart';
import '../../loadable_state/loading_error.dart';
import '../../repository/repository.dart';
import 'loadable_hydrated_bloc_event.dart';
import 'loadable_save_context.dart';

abstract class LoadableHydratedBloc<
  TEvent extends LoadableHydratedBlocEvent<TState>,
  TState,
  TRepository extends Repository<TState>
>
    extends
        HydratedBloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>> {
  late TRepository _repository;
  LoadableHydratedBloc()
    : super(
        const LoadableState(
          error: null,
          data: null,
          isLoading: true,
          lastFetch: null,
          reFetch: null,
        ),
      ) {
    on<Emit<TState>>((event, emit) {
      emit(
        LoadableState(
          isLoading: state.isLoading,
          data: event.state(innerState ?? fromNothing()),
          lastFetch: state.lastFetch,
          reFetch: retry,
          error: state.error,
          statusText: state.statusText,
        ),
      );
    });

    on<LoadingStatus<TState>>(
      (event, emit) => emit(
        LoadableState(
          isLoading: state.isLoading,
          data: innerState,
          lastFetch: state.lastFetch,
          reFetch: state.reFetch,
          error: state.error,
          statusText: event.statusText,
        ),
      ),
    );

    on<DataGathered<TState>>(
      (event, emit) => emit(
        LoadableState(
          isLoading: false,
          data: event.state(innerState ?? fromNothing()),
          lastFetch: DateTime.now().millisecondsSinceEpoch,
          reFetch: retry,
          error: null,
        ),
      ),
    );

    on<RefetchStarted<TState>>(
      (event, emit) => emit(
        LoadableState(
          isLoading: true,
          data: innerState,
          lastFetch: state.lastFetch,
          reFetch: null,
          error: null,
        ),
      ),
    );

    on<Error<TState>>(
      (event, emit) => emit(
        LoadableState(
          isLoading: false,
          data: innerState,
          lastFetch: state.lastFetch,
          reFetch: retry,
          error: event.error,
        ),
      ),
    );

    on<Reset<TState>>(
      (event, emit) => emit(
        const LoadableState(
          isLoading: false,
          data: null,
          lastFetch: null,
          reFetch: null,
          error: null,
        ),
      ),
    );

    _repository = repository();
    fetch();
  }

  /// Wipes this bloc's persisted state and resets the in-memory state to an
  /// empty, non-loading shell. Intended for logout: callers must trigger a
  /// fresh [fetch] (e.g. via [retry] or page-specific refresh) once the user
  /// is authenticated again, otherwise the UI would stay blank.
  Future<void> reset() async {
    await clear();
    add(Reset<TState>());
  }

  TState? get innerState => state.data;
  TRepository get repo => _repository;

  void retry() {
    log('Fetch retry triggered for ${TState.toString()}');
    add(RefetchStarted<TState>());
    fetch();
  }

  /// Maps [e] through the shared error mapper and emits it as an [Error] event.
  /// Does not guard [isClosed] — callers decide whether a late error still
  /// applies.
  void addLoadingError(Object e) => add(
    Error(
      LoadingError(
        message: errorToUserMessage(e),
        technicalDetails: errorToTechnicalDetails(e),
        allowRetry: errorAllowsRetry(e),
      ),
    ),
  );

  void fetch() {
    log('Fetching data for ${TState.toString()}');
    gatherData()
        .catchError((Object e) {
          log('Error while fetching ${TState.toString()}: ${e.toString()}');
          // The bloc may have been closed before this async error landed;
          // adding to a closed bloc throws, so swallow that case.
          if (isClosed) return;
          addLoadingError(e);
        })
        .then((value) {
          log('Fetch for ${TState.toString()} completed!');
        });
  }

  @override
  LoadableState<TState> fromJson(Map<String, dynamic> json) {
    var rawData = LoadableSaveContext.unwrap(json);
    return LoadableState(
      isLoading: true,
      data: fromStorage(rawData.data),
      lastFetch: rawData.meta.timestamp,
      reFetch: null,
      error: null,
    );
  }

  @override
  Map<String, dynamic>? toJson(LoadableState<TState> state) {
    Map<String, dynamic>? data;
    try {
      final stateData = state.data;
      data = stateData is TState ? toStorage(stateData) : null;
    } catch (e) {
      log('Failed to save state ${TState.toString()}: ${e.toString()}');
    }

    return LoadableSaveContext.wrap(
      data,
      state.lastFetch ?? DateTime.now().millisecondsSinceEpoch,
    );
  }

  Future<void> gatherData();
  TRepository repository();

  TState fromNothing();
  TState fromStorage(Map<String, dynamic> json);
  Map<String, dynamic>? toStorage(TState state);
}
