import 'dart:async';

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

import 'package:hl_lieferservice/data/network/auth_session_event.dart';
import 'package:hl_lieferservice/data/network/keycloak_oidc_token_provider.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_state.dart';
import 'package:hl_lieferservice/main.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';

/// AuthBloc bridge between der UI und dem [KeycloakOidcTokenProvider].
///
/// Eingehende UI-Events triggern den Provider (Login/Logout/Restore).
/// Provider-Ereignisse kommen über den `events`-Stream rein, werden zu
/// `ProviderSessionChanged` übersetzt und vom Bloc in Zustände überführt.
class AuthBloc extends Bloc<AuthEvent, AuthState> {
  AuthBloc({required this.tokenProvider, required this.operationBloc})
    : super(AuthBootstrapping()) {
    on<LoginRequested>(_handleLogin);
    on<LogoutRequested>(_handleLogout);
    on<RestoreSessionRequested>(_handleRestore);
    on<ProviderSessionChanged>(_handleProviderEvent);
    // Legacy: alte ERPframe-Repos rufen authBloc.add(SessionExpiredEvent())
    // bei jedem 401, weil ihr Cookie-Login serverseitig nicht mehr
    // existiert. Solange Phase D diese Repos nicht ersetzt hat, wäre
    // ein echter Logout daraus fatal: der erste TourBloc-Load nach
    // erfolgreicher Keycloak-Anmeldung würde die Session sofort wieder
    // wegwerfen. Daher hier nur loggen, **nicht** ausloggen — die
    // legitime SessionExpired-Quelle ist der Provider-Stream
    // (AuthSessionExpired bei Refresh-Failure).
    on<SessionExpiredEvent>((event, emit) async {
      debugPrint(
        '[AuthBloc] SessionExpiredEvent aus Legacy-Repo ignoriert — '
        'echter SessionExpired kommt vom KeycloakOidcTokenProvider.',
      );
    });

    _subscription = tokenProvider.events.listen(
      (event) => add(_toBlocEvent(event)),
    );
  }

  final KeycloakOidcTokenProvider tokenProvider;
  final OperationBloc operationBloc;

  late final StreamSubscription<AuthSessionEvent> _subscription;

  /// Timeout für den interaktiven Login. Schützt den Fahrer vor dem
  /// „Spinner dreht ewig"-Fall, wenn der native `flutter_appauth`-Aufruf
  /// (Browser-Tab + Token-Exchange) im Verbindungsabbruch / Issuer-Hang
  /// stecken bleibt. Bewusst etwas knapper als der Restore-Timeout
  /// (15 s im `_handleRestore`) — beim manuellen Login wartet der User
  /// aktiv vor dem Bildschirm.
  static const Duration _loginTimeout = Duration(seconds: 10);

  Future<void> _handleLogin(
    LoginRequested event,
    Emitter<AuthState> emit,
  ) async {
    try {
      emit(Authenticating());
      await tokenProvider.login().timeout(_loginTimeout);
      // Erfolg landet via Stream-Subscription als
      // ProviderSessionChanged.loggedIn → _handleProviderEvent.
    } on TimeoutException {
      // Native Login-Routine läuft im Hintergrund ggf. weiter. Sollte sie
      // doch noch erfolgreich durchlaufen, feuert der Provider-Stream
      // später ein `AuthLoggedIn` — der ProviderEvent-Handler hebt den
      // State dann auf `Authenticated`. Kein Schaden, nur „nachträgliche
      // Anmeldung". Bei späterer Exception passiert nichts; das Future
      // ist hier nicht mehr awaited.
      debugPrint('Login-Timeout nach ${_loginTimeout.inSeconds}s.');
      emit(Unauthenticated(loginTimedOut: true));
    } catch (err, st) {
      debugPrint('Login fehlgeschlagen: $err\n$st');
      emit(Unauthenticated());
      operationBloc.add(
        FailOperation(
          message: 'Login war nicht erfolgreich. Probieren Sie es erneut.',
        ),
      );
    }
  }

  Future<void> _handleLogout(
    LogoutRequested event,
    Emitter<AuthState> emit,
  ) async {
    await tokenProvider.logout();
    // Provider feuert AuthLoggedOut → _handleProviderEvent setzt den State.
  }

  Future<void> _handleRestore(
    RestoreSessionRequested event,
    Emitter<AuthState> emit,
  ) async {
    try {
      // Timeout-Schutz: hängt der Restore (z. B. nativer flutter_appauth-
      // Token-Call nach Hot-Restart, nicht erreichbarer Issuer), darf der
      // Bootstrap NICHT ewig im Splash bleiben. Nach dem Timeout fallen wir
      // sauber auf die LoginPage zurück. Läuft der Restore später doch noch
      // erfolgreich durch, kommt der Login via Stream-Event (AuthLoggedIn)
      // nachträglich an und der State wird zu Authenticated.
      final restored = await tokenProvider.restoreSession().timeout(
        const Duration(seconds: 15),
        onTimeout: () => false,
      );
      if (!restored) {
        // Kein gespeicherter Refresh-Token oder Refresh fehlgeschlagen:
        // Vom Splash zur LoginPage übergehen. Kein Snackbar — das ist
        // der normale Cold-Start-Pfad.
        emit(Unauthenticated());
      }
      // Erfolg landet via Stream als ProviderSessionChanged.loggedIn,
      // der Handler emittiert Authenticated.
    } catch (err, st) {
      debugPrint('Restore-Session fehlgeschlagen: $err\n$st');
      emit(Unauthenticated());
    }
  }

  Future<void> _handleProviderEvent(
    ProviderSessionChanged event,
    Emitter<AuthState> emit,
  ) async {
    switch (event.kind) {
      case ProviderEventKind.loggedIn:
        try {
          final state = Authenticated.fromClaims(
            claims: event.claims ?? const <String, dynamic>{},
            accessToken: event.accessToken ?? '',
          );
          if (locator.isRegistered<Authenticated>()) {
            locator.unregister<Authenticated>();
          }
          locator.registerSingleton<Authenticated>(state);
          emit(state);
        } catch (err, st) {
          debugPrint('Konnte Claims nicht in Authenticated-State mappen: $err\n$st');
          emit(Unauthenticated());
          operationBloc.add(
            FailOperation(
              message:
                  'Login-Antwort vom Server unvollständig. Bitte erneut versuchen.',
            ),
          );
        }
      case ProviderEventKind.loggedOut:
        if (locator.isRegistered<Authenticated>()) {
          locator.unregister<Authenticated>();
        }
        emit(Unauthenticated());
      case ProviderEventKind.sessionExpired:
        if (locator.isRegistered<Authenticated>()) {
          locator.unregister<Authenticated>();
        }
        emit(Unauthenticated(sessionExpired: true));
    }
  }

  ProviderSessionChanged _toBlocEvent(AuthSessionEvent event) {
    return switch (event) {
      AuthLoggedIn(:final claims) => ProviderSessionChanged(
        ProviderEventKind.loggedIn,
        claims: claims,
        // Wir würden hier idealerweise den Access-Token mit übergeben.
        // Da der Provider den Token nicht im Stream-Event mitliefert,
        // bauen wir später eine Brücke. Für jetzt: leerer String —
        // Authenticated.sessionId wird mit Phase D ohnehin abgeschafft.
        accessToken: '',
      ),
      AuthLoggedOut() => const ProviderSessionChanged(
        ProviderEventKind.loggedOut,
      ),
      AuthSessionExpired() => const ProviderSessionChanged(
        ProviderEventKind.sessionExpired,
      ),
    };
  }

  @override
  Future<void> close() async {
    await _subscription.cancel();
    return super.close();
  }
}
