import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import '../../../../../utils/haptics.dart';
import '../../../../../widget/conditional_wrapper.dart';
import '../../utility_widgets/bloc_module.dart';
import '../../utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
import '../bloc/loadable_state_bloc.dart';
import '../bloc/loadable_state_state.dart';
import '../loadable_state.dart';
import 'loadable_state_background_loading.dart';
import 'loadable_state_error_bar.dart';
import 'loadable_state_error_screen.dart';
import 'loadable_state_primary_loading.dart';

class LoadableStateConsumer<
  TController
      extends Bloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>>,
  TState
>
    extends StatelessWidget {
  final Widget Function(TState state, bool loading) child;
  final void Function(TState state)? onLoad;
  final bool wrapWithScrollView;
  final bool enablePullToRefresh;

  /// Optional predicate for callers whose [TState] always contains a non-null
  /// envelope but where actual content (e.g. a nested response) is loaded
  /// lazily. When provided, this overrides the default `data != null` check
  /// so primary loading / error screens / content visibility correctly reflect
  /// whether the inner content is ready.
  final bool Function(TState state)? isReady;

  const LoadableStateConsumer({
    required this.child,
    this.onLoad,
    this.wrapWithScrollView = false,
    this.enablePullToRefresh = true,
    this.isReady,
    super.key,
  });

  static Duration animationDuration = const Duration(milliseconds: 200);

  @override
  Widget build(BuildContext context) {
    var loadableState = context.watch<TController>().state;

    final loadedData = loadableState.data;
    if (!loadableState.isLoading && onLoad != null && loadedData is TState) {
      WidgetsBinding.instance.addPostFrameCallback(
        (timeStamp) => onLoad!(loadedData),
      );
    }

    final typedData = loadedData is TState ? loadedData : null;
    final hasContent =
        typedData != null &&
        (isReady?.call(typedData) ?? loadableState.showContent());
    final hasError = loadableState.error != null;
    final isLoading = loadableState.isLoading;

    final showPrimaryLoading = isLoading && !hasContent;
    final showBackgroundLoading = isLoading && hasContent;
    final showError = hasError && !hasContent;
    final showErrorBar = hasError && hasContent;

    // Keep the wrapper hierarchy stable across refresh cycles: reFetch flips to
    // null mid-refetch, and toggling the RefreshIndicator on that signal would
    // rebuild the tree under the ListView and reset its scroll position.
    final content = SizedBox(
      height: MediaQuery.of(context).size.height,
      child: hasContent
          ? child(typedData as TState, isLoading)
          : const SizedBox.shrink(),
    );
    final scrollable = ConditionalWrapper(
      condition: wrapWithScrollView,
      wrapper: (child) => SingleChildScrollView(
        physics: const AlwaysScrollableScrollPhysics(),
        child: child,
      ),
      child: content,
    );
    final childWidget = enablePullToRefresh
        ? RefreshIndicator(
            onRefresh: () {
              Haptics.refresh();
              loadableState.reFetch?.call();
              return Future.value();
            },
            child: scrollable,
          )
        : scrollable;

    return BlocModule<LoadableStateBloc, LoadableStateState>(
      create: (context) => LoadableStateBloc(),
      child: (context, bloc, state) {
        bloc.reFetch = loadableState.reFetch;
        return Column(
          children: [
            LoadableStateErrorBar(
              visible: showErrorBar,
              hasContent: hasContent,
              message: loadableState.error?.message,
              technicalDetails: loadableState.error?.technicalDetails,
              lastUpdated: loadableState.lastFetch,
            ),
            Expanded(
              child: Stack(
                children: [
                  LoadableStatePrimaryLoading(
                    visible: showPrimaryLoading,
                    statusText: loadableState.statusText,
                  ),
                  LoadableStateErrorScreen(
                    visible: showError,
                    message: loadableState.error?.message,
                    technicalDetails: loadableState.error?.technicalDetails,
                  ),

                  AnimatedOpacity(
                    opacity: hasContent ? 1.0 : 0.0,
                    duration: animationDuration,
                    curve: Curves.easeInOut,
                    child: childWidget,
                  ),

                  // Painted last so the top loading bar stays above the content
                  // instead of being covered by it (e.g. the chat message list).
                  LoadableStateBackgroundLoading(
                    visible: showBackgroundLoading,
                  ),
                ],
              ),
            ),
          ],
        );
      },
    );
  }
}
