import 'dart:convert';

import '../models/plex/plex_home.dart';
import '../models/plex/plex_home_user.dart';
import '../profiles/profile.dart';
import '../profiles/profile_registry.dart';
import '../services/plex_auth_service.dart';
import '../services/server_registry.dart';
import '../services/storage_service.dart';
import '../utils/app_logger.dart';
import 'connection.dart';
import 'connection_registry.dart';

/// One-shot helpers that bridge between the legacy single-Plex-account
/// SharedPreferences state (`StorageService.plexToken` +
/// `currentUserUUID` + `homeUsersCache` + `ServerRegistry.getServers()`)
/// and the new [ConnectionRegistry] world.
///
/// Plex Home users are NOT persisted here — the bootstrap copies the
/// legacy `homeUsersCache` into the per-connection
/// `plex_home_users_{connectionId}` SharedPreferences slot so
/// [PlexHomeService] picks it up on cold start.
class ConnectionBootstrap {
  ConnectionBootstrap({
    required this.storage,
    required this.connectionRegistry,
    required this.serverRegistry,
    required this.profileRegistry,
    Future<List<PlexHomeUser>> Function(String accountToken)? plexHomeUserFetcher,
    Future<Map<String, dynamic>> Function(String accountToken)? plexUserInfoFetcher,
  }) : _plexHomeUserFetcher = plexHomeUserFetcher ?? _fetchPlexHomeUsers,
       _plexUserInfoFetcher = plexUserInfoFetcher ?? _fetchPlexUserInfo;

  final StorageService storage;
  final ConnectionRegistry connectionRegistry;
  final ServerRegistry serverRegistry;
  final ProfileRegistry profileRegistry;
  final Future<List<PlexHomeUser>> Function(String accountToken) _plexHomeUserFetcher;
  final Future<Map<String, dynamic>> Function(String accountToken) _plexUserInfoFetcher;

  static const String _keyProfileMigrationV1Done = 'profile_migration_v1_done';

  /// Run all idempotent boot-time migrations. Best-effort — errors are
  /// logged but never thrown.
  Future<void> run() async {
    await seedFromDevTokenDefine();
    final hadLegacyPlexToken = (storage.getPlexToken() ?? '').isNotEmpty;
    final hadLegacyProfileState = _hasLegacyProfileState();
    final migratedAccount = await migrateLegacyPlexAccount();
    final account = await _firstPlexAccount(migratedAccount);
    final alreadyMigrated = storage.prefs.getBool(_keyProfileMigrationV1Done) ?? false;
    if (!alreadyMigrated) {
      if (account == null && (hadLegacyPlexToken || hadLegacyProfileState)) {
        appLogger.w('Migration: legacy profile state present but Plex account was not migrated; will retry later');
        return;
      }
      // Drop any plex_home rows left over from the pre-refactor data
      // model — Plex Home users are now fetched live, never persisted.
      await profileRegistry.dropAllPlexHomeRows();
      if (account != null) {
        final prepared = await _preparePlexVirtualProfile(account);
        if (!prepared) {
          if (migratedAccount != null && hadLegacyPlexToken) {
            await connectionRegistry.remove(migratedAccount.id);
          }
          appLogger.w('Migration: could not hydrate Plex Home profiles for ${account.id}; will retry later');
          return;
        }
      }
      if (hadLegacyPlexToken) {
        await storage.clearLegacyPlexToken();
      }
      await storage.clearServersList();
      await storage.prefs.setBool(_keyProfileMigrationV1Done, true);
    } else {
      await storage.clearServersList();
      await _recoverLegacyProfilePromotionIfNeeded(account);
      await _migrateLegacyPlexHomeUsersCacheForExistingAccount(account);
    }
  }

  bool _hasLegacyProfileState() {
    return (storage.getCurrentUserUUID() ?? '').isNotEmpty ||
        (storage.prefs.getString('home_users_cache') ?? '').isNotEmpty;
  }

  /// Screenshot automation injects a Plex token via the `PLEX_TOKEN`
  /// dart-define so the app boots already-signed-in. Inserts a
  /// [PlexAccountConnection] directly when the env var is non-empty AND
  /// the registry doesn't already have a Plex account, fetching the user
  /// info + servers like the auth screen does. No-op in normal builds.
  Future<void> seedFromDevTokenDefine() async {
    const devToken = String.fromEnvironment('PLEX_TOKEN');
    if (devToken.isEmpty) return;
    final existing = await connectionRegistry.list();
    if (existing.whereType<PlexAccountConnection>().isNotEmpty) return;

    try {
      final auth = await PlexAuthService.create();
      try {
        final info = await auth.getUserInfo(devToken);
        final servers = await auth.fetchServers(devToken);
        final clientId = await storage.getOrCreateClientIdentifier();
        final conn = PlexAccountConnection(
          id: 'plex.$clientId',
          accountToken: devToken,
          clientIdentifier: clientId,
          accountLabel: (info['username'] as String?) ?? (info['email'] as String?) ?? 'Plex',
          servers: servers,
          createdAt: DateTime.now(),
          lastAuthenticatedAt: DateTime.now(),
        );
        await connectionRegistry.upsert(conn);
        appLogger.i('Seeded Plex account from PLEX_TOKEN dart-define as ${conn.id}');
      } finally {
        auth.dispose();
      }
    } catch (e, st) {
      appLogger.w('PLEX_TOKEN seed failed', error: e, stackTrace: st);
    }
  }

  /// If a legacy Plex token sits in [StorageService] without a corresponding
  /// row in [ConnectionRegistry], wrap it as a [PlexAccountConnection] and
  /// insert. Best-effort: errors are logged but never thrown.
  Future<PlexAccountConnection?> migrateLegacyPlexAccount() async {
    final token = storage.getPlexToken();
    if (token == null || token.isEmpty) return null;

    final existing = await connectionRegistry.list();
    final alreadyMigrated = existing
        .whereType<PlexAccountConnection>()
        .where((c) => c.accountToken == token)
        .firstOrNull;
    if (alreadyMigrated != null) {
      return alreadyMigrated;
    }

    try {
      final clientId = await storage.getOrCreateClientIdentifier();
      final servers = await serverRegistry.getServers();

      String accountLabel = 'Plex';
      String accountUuid = '';
      try {
        final info = await _plexUserInfoFetcher(token);
        accountLabel = (info['username'] as String?) ?? (info['email'] as String?) ?? 'Plex';
        accountUuid = (info['uuid'] as String?)?.trim() ?? '';
      } catch (e) {
        appLogger.d('Plex migration: account label lookup failed (using fallback): $e');
      }

      final conn = PlexAccountConnection(
        id: 'plex.${accountUuid.isNotEmpty ? accountUuid : clientId}',
        accountToken: token,
        clientIdentifier: clientId,
        accountLabel: accountLabel,
        servers: servers,
        createdAt: DateTime.now(),
        lastAuthenticatedAt: DateTime.now(),
      );
      await connectionRegistry.upsert(conn);
      appLogger.i('Migrated legacy Plex account into ConnectionRegistry as ${conn.id}');
      return conn;
    } catch (e, st) {
      appLogger.w('Plex account migration failed', error: e, stackTrace: st);
      return null;
    }
  }

  /// Hydrate Plex Home users for [account] and select a virtual Plex Home
  /// profile. Plex users are never persisted as local Plezy profiles.
  Future<bool> _preparePlexVirtualProfile(PlexAccountConnection account) async {
    final copied = await _migrateLegacyPlexHomeUsersCache(account.id);
    var users = copied ? _readPlexHomeUsersCache(account.id) : null;
    users ??= await _fetchAndCachePlexHomeUsers(account);
    final hydratedUsers = users;
    if (hydratedUsers.isEmpty) return false;

    final legacyActiveUuid = storage.getCurrentUserUUID();
    PlexHomeUser selected;
    if (legacyActiveUuid != null && legacyActiveUuid.isNotEmpty) {
      selected = hydratedUsers.firstWhere(
        (u) => u.uuid == legacyActiveUuid,
        orElse: () => _preferredPlexHomeUser(hydratedUsers),
      );
      if (selected.uuid != legacyActiveUuid) {
        appLogger.w('Migration: legacy Plex Home UUID $legacyActiveUuid not found; using ${selected.uuid} instead');
      }
    } else {
      selected = _preferredPlexHomeUser(hydratedUsers);
    }

    final activeProfileId = plexHomeProfileId(accountConnectionId: account.id, homeUserUuid: selected.uuid);
    await storage.setActiveProfileId(activeProfileId);
    await storage.clearCurrentUserUUID();
    await Future.wait([storage.prefs.remove('home_users_cache'), storage.prefs.remove('home_users_cache_expiry')]);
    appLogger.i('Migration: selected Plex Home profile ${selected.displayName} → $activeProfileId');
    return true;
  }

  PlexHomeUser _preferredPlexHomeUser(List<PlexHomeUser> users) {
    return users.firstWhere((u) => u.admin, orElse: () => users.first);
  }

  Future<bool> _migrateLegacyPlexHomeUsersCache(String connectionId) async {
    final raw = storage.prefs.getString('home_users_cache');
    if (raw == null || raw.isEmpty) return false;

    try {
      final decoded = jsonDecode(raw);
      if (decoded is! Map<String, dynamic>) return false;
      final home = PlexHome.fromJson(decoded);
      if (home.users.isEmpty) return false;
      await storage.savePlexHomeUsersCache(connectionId, home.users.map((u) => u.toJson()).toList());
      appLogger.i('Migration: copied ${home.users.length} Plex Home users into cache for $connectionId');
      return true;
    } catch (e, st) {
      appLogger.w('Plex Home cache migration failed', error: e, stackTrace: st);
      return false;
    }
  }

  List<PlexHomeUser>? _readPlexHomeUsersCache(String connectionId) {
    final raw = storage.getPlexHomeUsersCacheJson(connectionId);
    if (raw == null || raw.isEmpty) return null;
    try {
      final decoded = jsonDecode(raw);
      if (decoded is! List) return null;
      return decoded.whereType<Map<String, dynamic>>().map(PlexHomeUser.fromJson).toList();
    } catch (e, st) {
      appLogger.w('Migration: failed to read Plex Home cache for $connectionId', error: e, stackTrace: st);
      return null;
    }
  }

  Future<List<PlexHomeUser>> _fetchAndCachePlexHomeUsers(PlexAccountConnection account) async {
    try {
      final users = await _plexHomeUserFetcher(account.accountToken);
      if (users.isNotEmpty) {
        await storage.savePlexHomeUsersCache(account.id, users.map((u) => u.toJson()).toList());
        appLogger.i('Migration: fetched ${users.length} Plex Home users for ${account.id}');
      }
      return users;
    } catch (e, st) {
      appLogger.w('Migration: Plex Home fetch failed for ${account.id}', error: e, stackTrace: st);
      return const [];
    }
  }

  Future<void> _migrateLegacyPlexHomeUsersCacheForExistingAccount(PlexAccountConnection? account) async {
    if (storage.prefs.getString('home_users_cache') == null) return;
    var target = account;
    if (target == null) {
      final connections = await connectionRegistry.list();
      for (final conn in connections) {
        if (conn is PlexAccountConnection) {
          target = conn;
          break;
        }
      }
    }
    if (target == null) return;
    final copied = await _migrateLegacyPlexHomeUsersCache(target.id);
    if (copied) {
      await Future.wait([storage.prefs.remove('home_users_cache'), storage.prefs.remove('home_users_cache_expiry')]);
    }
  }

  Future<PlexAccountConnection?> _firstPlexAccount(PlexAccountConnection? preferred) async {
    if (preferred != null) return preferred;
    final connections = await connectionRegistry.list();
    for (final conn in connections) {
      if (conn is PlexAccountConnection) return conn;
    }
    return null;
  }

  Future<void> _recoverLegacyProfilePromotionIfNeeded(PlexAccountConnection? account) async {
    if (!_hasLegacyProfileState()) return;
    final target = await _firstPlexAccount(account);
    if (target == null) return;
    await _preparePlexVirtualProfile(target);
  }
}

Future<List<PlexHomeUser>> _fetchPlexHomeUsers(String accountToken) async {
  final auth = await PlexAuthService.create();
  try {
    final home = await auth.getHomeUsers(accountToken);
    return home.users;
  } finally {
    auth.dispose();
  }
}

Future<Map<String, dynamic>> _fetchPlexUserInfo(String accountToken) async {
  final auth = await PlexAuthService.create();
  try {
    return await auth.getUserInfo(accountToken);
  } finally {
    auth.dispose();
  }
}
