import 'dart:async';

import 'package:flutter/foundation.dart';

import '../connection/connection.dart';
import '../connection/connection_registry.dart';
import '../mixins/disposable_change_notifier_mixin.dart';
import '../models/plex/plex_home_user.dart';
import '../services/storage_service.dart';
import '../utils/app_logger.dart';
import 'plex_home_service.dart';
import 'profile.dart';
import 'profile_merge.dart';
import 'profile_registry.dart';

/// Holds the currently active [Profile] and a merged list of all available
/// profiles — local rows from [ProfileRegistry] plus virtual Plex Home
/// profiles built from [PlexHomeService]'s live cache.
///
/// The active id (in storage) can reference either a local row or a Plex
/// Home virtual id like `plex-home-{connId}-{uuid}`. Resolution checks
/// local profiles first, then live home users; if neither matches we fall
/// back to the first profile in the merged list.
class ActiveProfileProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
  ActiveProfileProvider({
    required ProfileRegistry registry,
    required PlexHomeService plexHome,
    required ConnectionRegistry connections,
    StorageService? storage,
  }) : _registry = registry,
       _plexHome = plexHome,
       _connections = connections,
       _storage = storage;

  final ProfileRegistry _registry;
  final PlexHomeService _plexHome;
  final ConnectionRegistry _connections;
  StorageService? _storage;

  Profile? _active;
  List<Profile> _profiles = const [];
  List<Profile> _localProfiles = const [];
  Map<String, List<PlexHomeUser>> _plexHomeUsers = const {};
  Map<String, Connection> _connectionsById = const {};

  StreamSubscription<List<Profile>>? _localSub;
  StreamSubscription<List<Connection>>? _connSub;
  StreamSubscription<Map<String, List<PlexHomeUser>>>? _plexHomeSub;
  Future<void>? _initializeFuture;
  bool _initialized = false;

  bool _isBinding = false;
  bool _lastBindingSucceeded = true;
  final List<Completer<bool>> _bindingSettleWaiters = [];

  Profile? get active => _active;
  String? get activeId => _active?.id;
  List<Profile> get profiles => _profiles;
  bool get hasMultipleProfiles => _profiles.length > 1;
  bool get isInitialized => _initialized;

  /// True while [ActiveProfileBinder] is wiring servers/tokens for the
  /// active profile. The picker reads this so it can stay open (and stay
  /// behind any PIN dialog the binder pops) until binding settles.
  bool get isBinding => _isBinding;

  /// Outcome of the most recent settled bind. False after a PIN cancel,
  /// failed `/home/users/{uuid}/switch`, or any error inside the binder.
  bool get lastBindingSucceeded => _lastBindingSucceeded;

  /// Called by [ActiveProfileBinder] at the start of a rebind cycle.
  /// Re-entrant — no-ops if already in [isBinding].
  void markBindingStarted() {
    if (_isBinding) return;
    _isBinding = true;
    _lastBindingSucceeded = true;
    safeNotifyListeners();
  }

  /// Called by [ActiveProfileBinder] when the rebind settles. [success]
  /// means the active profile was applied successfully. A local profile with
  /// no connections is successful and intentionally exposes no servers; false
  /// covers the "user cancelled the PIN" path and real binding errors.
  void markBindingFinished({required bool success}) {
    if (!_isBinding && _lastBindingSucceeded == success) return;
    _isBinding = false;
    _lastBindingSucceeded = success;
    safeNotifyListeners();
  }

  /// Resolves once the binder reports done. Returns immediately when no
  /// rebind is in flight. The boolean reflects [lastBindingSucceeded] at
  /// the moment binding settles, so the picker can decide whether to
  /// dismiss or surface an error.
  ///
  /// Pending completers are tracked so [dispose] can settle them — without
  /// that, awaiters (picker, user-profile refresh, post-bind hooks) hang
  /// indefinitely when the provider is torn down mid-rebind.
  Future<bool> awaitBindingSettle() {
    if (!_isBinding) return Future.value(_lastBindingSucceeded);
    final completer = Completer<bool>();
    _bindingSettleWaiters.add(completer);
    void listener() {
      if (_isBinding) return;
      removeListener(listener);
      if (_bindingSettleWaiters.remove(completer) && !completer.isCompleted) {
        completer.complete(_lastBindingSucceeded);
      }
    }

    addListener(listener);
    return completer.future;
  }

  Future<void> initialize() {
    if (_initialized) return Future.value();
    final pending = _initializeFuture;
    if (pending != null) return pending;

    final future = _initialize().catchError((Object error, StackTrace stackTrace) {
      _initializeFuture = null;
      Error.throwWithStackTrace(error, stackTrace);
    });
    _initializeFuture = future;
    return future;
  }

  /// Re-read connections, Plex Home cache, local profiles, and active id.
  ///
  /// Provider initialization starts before boot-time migration, so the first
  /// snapshot can legitimately miss the migrated connection/profile state.
  Future<void> reloadFromStorage() async {
    await initialize();
    await _plexHome.reloadFromStorage();
    await _reloadSnapshot();
    safeNotifyListeners();
  }

  Future<void> _initialize() async {
    await _reloadSnapshot();

    _localSub = _registry.watchProfiles().listen((list) {
      _localProfiles = list;
      _recomputeProfiles();
      _resolveActive();
      safeNotifyListeners();
    });
    _connSub = _connections.watchConnections().listen((list) {
      _connectionsById = {for (final c in list) c.id: c};
      _recomputeProfiles();
      _resolveActive();
      safeNotifyListeners();
    });
    _plexHomeSub = _plexHome.stream.listen((cache) {
      _plexHomeUsers = cache;
      _recomputeProfiles();
      _resolveActive();
      safeNotifyListeners();
    });

    _initialized = true;
    safeNotifyListeners();
  }

  Future<void> _reloadSnapshot() async {
    _storage ??= await StorageService.getInstance();
    // Hydrate the Plex Home cache before we read it — `_plexHome.current`
    // is only populated after start() finishes its disk-cache load.
    await _plexHome.start();

    _localProfiles = await _registry.list();
    final initialConns = await _connections.list();
    _connectionsById = {for (final c in initialConns) c.id: c};
    _plexHomeUsers = _plexHome.current;
    _recomputeProfiles();
    _resolveActive();
  }

  void _recomputeProfiles() {
    _profiles = mergeLocalWithPlexHome(
      locals: _localProfiles,
      plexHomeByConnectionId: _plexHomeUsers,
      connectionsById: _connectionsById,
      storage: _storage,
    );
  }

  void _resolveActive() {
    if (_profiles.isEmpty) {
      _active = null;
      return;
    }
    final id = _storage?.getActiveProfileId();
    // No saved id means "fresh state" — leave the active profile null so
    // MainScreen prompts the user via the picker. Auto-falling back to
    // `_profiles.first` would let the binder bind (and possibly PIN-prompt)
    // a profile the user never picked.
    if (id == null) {
      _active = null;
      return;
    }
    for (final p in _profiles) {
      if (p.id == id) {
        _active = p;
        return;
      }
    }
    // Saved id no longer matches anything (e.g. Plex admin removed the
    // home user). Clear it so storage-scoped settings do not keep reading
    // and writing under the removed profile's user scope, and let the UI
    // force an explicit profile choice.
    _active = null;
    final storage = _storage;
    if (storage != null) {
      unawaited(storage.clearActiveProfileId());
    }
  }

  /// Activate [profile]. PIN-protected local profiles must supply a matching
  /// PIN; for Plex Home profiles the binder enforces the PIN via
  /// `/home/users/{uuid}/switch` after activation.
  Future<bool> activate(Profile profile, {String? pin}) async {
    if (profile.isLocal && profile.isPinProtected) {
      final hash = profile.pinHash;
      if (pin == null || hash == null || !verifyPin(pin, hash)) {
        return false;
      }
    }
    final storage = _storage;
    if (storage == null) return false;
    await storage.setActiveProfileId(profile.id);
    final now = DateTime.now();
    await storage.markProfileUsed(profile.id, now);
    _active = profile.copyWith(lastUsedAt: now);
    safeNotifyListeners();
    appLogger.i('ActiveProfileProvider: activated ${profile.displayName} (${profile.id})');
    if (profile.isLocal) {
      // Local rows also bump the DB's lastUsedAt so the in-DB sortable column
      // stays accurate — the in-memory mark above keeps the picker snappy.
      unawaited(
        _registry.markUsed(profile.id, now).catchError((Object e, StackTrace s) {
          appLogger.w('markUsed failed for ${profile.id}', error: e, stackTrace: s);
        }),
      );
    }
    return true;
  }

  /// Clear the selected profile in both storage and memory so the picker
  /// can force an explicit choice on the next screen.
  Future<void> clearActiveProfile() async {
    final storage = _storage ??= await StorageService.getInstance();
    await storage.clearActiveProfileId();
    _active = null;
    safeNotifyListeners();
  }

  @visibleForTesting
  Future<void> resetForTesting() async {
    await _localSub?.cancel();
    await _connSub?.cancel();
    await _plexHomeSub?.cancel();
    _localSub = null;
    _connSub = null;
    _plexHomeSub = null;
    _profiles = const [];
    _localProfiles = const [];
    _plexHomeUsers = const {};
    _connectionsById = const {};
    _active = null;
    _initializeFuture = null;
    _initialized = false;
    _isBinding = false;
    _lastBindingSucceeded = true;
    for (final c in _bindingSettleWaiters) {
      if (!c.isCompleted) c.complete(_lastBindingSucceeded);
    }
    _bindingSettleWaiters.clear();
  }

  @override
  void dispose() {
    // Settle anyone awaiting binding before the listeners go away — leaving
    // them pending traps callers in a forever-await on app teardown.
    for (final c in _bindingSettleWaiters) {
      if (!c.isCompleted) c.complete(_lastBindingSucceeded);
    }
    _bindingSettleWaiters.clear();
    _initializeFuture = null;
    _localSub?.cancel();
    _connSub?.cancel();
    _plexHomeSub?.cancel();
    super.dispose();
  }
}
