import 'dart:async';

import 'package:flutter/material.dart';
import 'package:plezy/widgets/app_icon.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';

import '../../connection/connection.dart';
import '../../exceptions/media_server_exceptions.dart';
import '../../focus/card_focus_scope.dart';
import '../../focus/focusable_button.dart';
import '../../focus/focusable_text_field.dart';
import '../../focus/focusable_wrapper.dart';
import '../../i18n/strings.g.dart';
import '../../mixins/controller_disposer_mixin.dart';
import '../../profiles/active_profile_binder.dart';
import '../../profiles/active_profile_provider.dart';
import '../../profiles/profile.dart';
import '../../profiles/profile_connection.dart';
import '../../services/jellyfin_auth_service.dart';
import '../../services/jellyfin_endpoint_discovery.dart';
import '../../services/jellyfin_lan_discovery_service.dart';
import '../../services/storage_service.dart';
import '../../theme/mono_tokens.dart';
import '../../utils/app_logger.dart';
import '../../utils/device_identity.dart';
import '../../utils/platform_detector.dart';
import '../../widgets/focused_scroll_scaffold.dart';
import '../profile/profile_switch_screen.dart';
import 'async_form_state_mixin.dart';
import 'connection_persistence.dart';
import '../../widgets/loading_indicator_box.dart';

@visibleForTesting
Future<String> resolveJellyfinClientVersion({Future<PackageInfo> Function()? packageInfoLoader}) async {
  const fallbackVersion = '1.0';
  try {
    final packageInfo = await (packageInfoLoader == null ? PackageInfo.fromPlatform() : packageInfoLoader());
    final version = packageInfo.version.trim();
    if (version.isNotEmpty) return version;
    appLogger.w('Package version is empty; using Jellyfin client version $fallbackVersion');
  } catch (error, stackTrace) {
    appLogger.w(
      'Failed to resolve package version; using Jellyfin client version $fallbackVersion',
      error: error,
      stackTrace: stackTrace,
    );
  }
  return fallbackVersion;
}

@visibleForTesting
bool shouldCreateLocalJellyfinProfile({
  required Profile? targetProfile,
  required Profile? activeProfile,
  required bool hasProfiles,
}) {
  return targetProfile == null && activeProfile == null && !hasProfiles;
}

@visibleForTesting
bool shouldPromptForJellyfinProfileSelection({
  required Profile? targetProfile,
  required Profile? activeProfile,
  required bool hasProfiles,
}) {
  return targetProfile == null && activeProfile == null && hasProfiles;
}

/// Three-step form to add a Jellyfin server:
///   1. Probe URL candidates (`/System/Info/Public`).
///   2. Username + password (`/Users/AuthenticateByName`) **or** Quick Connect
///      (`/QuickConnect/Initiate` → poll → `/Users/AuthenticateWithQuickConnect`).
///   3. Persist via [ConnectionRegistry] and create a [ProfileConnection]
///      row binding the server to [targetProfile] (or the active profile,
///      if not provided). When the target *is* the active profile we also
///      register the client with the manager so libraries refresh
///      immediately; otherwise the binder picks it up on the next switch.
class AddJellyfinScreen extends StatefulWidget {
  /// When set, the new Jellyfin connection is bound to this profile via a
  /// [ProfileConnection] row. When null, falls back to the currently active
  /// profile (typical for the global Connections screen entry point).
  final Profile? targetProfile;
  final FutureOr<JellyfinConnectionAuthService> Function()? _authServiceFactory;
  final FutureOr<List<DiscoveredJellyfinServer>> Function()? _localDiscoveryFactory;

  const AddJellyfinScreen({
    super.key,
    this.targetProfile,
    @visibleForTesting this._authServiceFactory,
    @visibleForTesting this._localDiscoveryFactory,
  });

  @override
  State<AddJellyfinScreen> createState() => _AddJellyfinScreenState();
}

class _AddJellyfinScreenState extends State<AddJellyfinScreen> with AsyncFormStateMixin, ControllerDisposerMixin {
  late final _urlController = createTextEditingController();
  late final _usernameController = createTextEditingController();
  late final _passwordController = createTextEditingController();
  final _urlFocus = FocusNode(debugLabel: 'AddJellyfin:Url');
  final _findServerFocus = FocusNode(debugLabel: 'AddJellyfin:FindServer');
  final _changeServerFocus = FocusNode(debugLabel: 'AddJellyfin:ChangeServer');
  final _usernameFocus = FocusNode(debugLabel: 'AddJellyfin:Username');
  // Owned so the username field can advance focus on Enter; mobile keyboards
  // act on `textInputAction: next` automatically but TV remotes / hardware
  // keyboards need the explicit `onFieldSubmitted` handler below.
  final _passwordFocus = FocusNode(debugLabel: 'AddJellyfin:Password');
  final _signInFocus = FocusNode(debugLabel: 'AddJellyfin:SignIn');
  final _quickConnectFocus = FocusNode(debugLabel: 'AddJellyfin:QuickConnect');
  final _cancelQuickConnectFocus = FocusNode(debugLabel: 'AddJellyfin:CancelQuickConnect');
  final _discoveredServerFocusNodes = <String, FocusNode>{};
  final _formKey = GlobalKey<FormState>();

  JellyfinServerInfo? _serverInfo;
  JellyfinEndpointRaceResult? _serverEndpoint;
  List<DiscoveredJellyfinServer> _localServers = const [];
  bool _isDiscoveringLocalServers = true;
  bool _quickConnectEnabled = false;
  JellyfinQuickConnectInitiation? _qcInitiation;
  bool _qcCancelled = false;
  int _qcAttemptId = 0;
  int _localDiscoveryAttemptId = 0;

  @override
  void initState() {
    super.initState();
    unawaited(_discoverLocalServers());
  }

  @override
  void dispose() {
    // Short-circuit any in-flight Quick Connect poll so it doesn't try to
    // setState after the widget is gone.
    _qcCancelled = true;
    _qcAttemptId++;
    _urlFocus.dispose();
    _findServerFocus.dispose();
    _changeServerFocus.dispose();
    _usernameFocus.dispose();
    _passwordFocus.dispose();
    _signInFocus.dispose();
    _quickConnectFocus.dispose();
    _cancelQuickConnectFocus.dispose();
    for (final node in _discoveredServerFocusNodes.values) {
      node.dispose();
    }
    super.dispose();
  }

  Future<void> _discoverLocalServers() async {
    final attemptId = ++_localDiscoveryAttemptId;
    try {
      final factory = widget._localDiscoveryFactory;
      final servers = factory != null
          ? await factory()
          : await JellyfinLanDiscoveryService().discover(responseWindow: const Duration(milliseconds: 1300));
      if (!mounted || attemptId != _localDiscoveryAttemptId) return;
      setState(() {
        _localServers = servers;
        _isDiscoveringLocalServers = false;
        _syncDiscoveredServerFocusNodes(servers);
      });
    } catch (e, st) {
      appLogger.w('Add Jellyfin local discovery failed', error: e, stackTrace: st);
      if (!mounted || attemptId != _localDiscoveryAttemptId) return;
      setState(() => _isDiscoveringLocalServers = false);
    }
  }

  void _syncDiscoveredServerFocusNodes(List<DiscoveredJellyfinServer> servers) {
    final ids = servers.map((server) => server.id).toSet();
    final removed = _discoveredServerFocusNodes.keys.where((id) => !ids.contains(id)).toList(growable: false);
    for (final id in removed) {
      _discoveredServerFocusNodes.remove(id)?.dispose();
    }
    for (final server in servers) {
      _discoveredServerFocusNodes.putIfAbsent(
        server.id,
        () => FocusNode(debugLabel: 'AddJellyfin:Discovered:${server.id}'),
      );
    }
  }

  void _clearResolvedServer() {
    _serverEndpoint = null;
    _serverInfo = null;
    _quickConnectEnabled = false;
  }

  Future<void> _useDiscoveredServer(DiscoveredJellyfinServer server) async {
    if (busy) return;
    setState(() {
      _urlController.text = server.address;
      _clearResolvedServer();
    });
    await _probe();
  }

  Future<void> _probe() async {
    final input = JellyfinEndpointDiscovery.buildUserInputCandidates(_enteredUrls());
    if (input.probeBaseUrls.isEmpty) {
      setErrorText(t.addServer.enterJellyfinUrlError);
      return;
    }
    final autoStartQuickConnect = await runAsync<bool>(
      () async {
        final auth = await _buildAuthService();
        final endpoint = await auth.raceEndpoints(
          input.probeBaseUrls,
          baseUrlsToPersist: input.explicitBaseUrls,
          baseUrlValidationGroups: input.validationBaseUrlGroups,
        );
        final qcEnabled = await auth.isQuickConnectEnabled(endpoint.activeBaseUrl);
        if (!mounted) return false;
        setState(() {
          _serverEndpoint = endpoint;
          _serverInfo = endpoint.serverInfo;
          _quickConnectEnabled = qcEnabled;
          _urlController.text = endpoint.baseUrls.join('\n');
        });
        // On TV, typing a username/password with a remote is misery — auto-jump
        // to Quick Connect when the server supports it. Mirrors the
        // PlatformDetector.isTV() default in add_plex_account_screen.dart.
        final autoStart = qcEnabled && PlatformDetector.isTV();
        if (!autoStart) _requestFocusAfterFrame(_usernameFocus);
        return autoStart;
      },
      errorMapper: (e) =>
          e is MediaServerUrlException ? e.message : t.addServer.couldNotReachServer(error: e.toString()),
    );
    // Sequenced after the probe's runAsync so busy stays set straight through
    // /QuickConnect/Initiate. Started from inside the probe body, the probe's
    // `finally` cleared busy mid-initiate, re-enabling the form — the focus
    // fallback from the removed tile/button then landed on the URL field and
    // auto-opened the TV keyboard over the Quick Connect panel.
    if (autoStartQuickConnect == true && mounted) await _startQuickConnect();
  }

  Future<void> _signIn() async {
    if (!(_formKey.currentState?.validate() ?? false)) return;
    final info = _serverInfo;
    final endpoint = _serverEndpoint;
    if (info == null || endpoint == null) {
      await _probe();
      return;
    }
    await runAsync<void>(
      () async {
        final auth = await _buildAuthService();
        final storage = await StorageService.getInstance();
        final deviceId = await storage.getOrCreateClientIdentifier();

        final connection = await auth.authenticateByName(
          baseUrl: endpoint.activeBaseUrl,
          baseUrls: endpoint.baseUrls,
          username: _usernameController.text,
          password: _passwordController.text,
          deviceId: deviceId,
          serverInfo: info,
        );

        if (!mounted) return;
        await _persistAndExit(connection);
      },
      errorMapper: (e) {
        if (e is MediaServerAuthException) return e.message;
        appLogger.e('Add Jellyfin failed', error: e);
        return t.addServer.signInFailed(error: e.toString());
      },
    );
  }

  Future<void> _startQuickConnect() async {
    final info = _serverInfo;
    final endpoint = _serverEndpoint;
    if (info == null || endpoint == null) return;
    final attemptId = ++_qcAttemptId;
    setState(() => _qcCancelled = false);
    await runAsync<void>(
      () async {
        final auth = await _buildAuthService();
        final storage = await StorageService.getInstance();
        final deviceId = await storage.getOrCreateClientIdentifier();

        final initiation = await auth.initiateQuickConnect(baseUrl: endpoint.activeBaseUrl, deviceId: deviceId);
        if (!_isCurrentQuickConnectAttempt(attemptId)) return;
        // Show the waiting panel without a spinner — opt-out of busy mid-flow
        // so the user-visible state matches "we're polling, nothing for you to do".
        setState(() => _qcInitiation = initiation);
        _requestFocusAfterFrame(_cancelQuickConnectFocus);
        setBusy(false);

        final connection = await auth.authenticateByQuickConnect(
          baseUrl: endpoint.activeBaseUrl,
          baseUrls: endpoint.baseUrls,
          secret: initiation.secret,
          deviceId: deviceId,
          serverInfo: info,
          shouldCancel: () => _qcCancelled || attemptId != _qcAttemptId,
        );

        if (!_isCurrentQuickConnectAttempt(attemptId)) return;
        if (connection == null) {
          // Either user cancelled or the secret expired before approval.
          // Cancellation is silent; expiry surfaces an error.
          setState(() => _qcInitiation = null);
          if (!_qcCancelled) setErrorText(t.auth.quickConnectExpired);
          return;
        }
        await _persistAndExit(connection);
      },
      errorMapper: (e) {
        if (e is MediaServerAuthException) return e.message;
        appLogger.e('Jellyfin Quick Connect failed', error: e);
        return t.addServer.quickConnectFailed(error: e.toString());
      },
      shouldApplyState: () => attemptId == _qcAttemptId,
    );
    // Clear the QC panel after any error so the form re-shows.
    if (_isCurrentQuickConnectAttempt(attemptId) && errorText != null && _qcInitiation != null) {
      setState(() => _qcInitiation = null);
    }
  }

  bool _isCurrentQuickConnectAttempt(int attemptId) => mounted && attemptId == _qcAttemptId;

  void _cancelQuickConnect() {
    _qcAttemptId++;
    setState(() {
      _qcCancelled = true;
      _qcInitiation = null;
    });
    setBusy(false);
  }

  void _requestFocusAfterFrame(FocusNode node) {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!mounted || !node.canRequestFocus) return;
      node.requestFocus();
    });
  }

  void _focusFirstDiscoveredServerOrFind() {
    if (_localServers.isEmpty) {
      _findServerFocus.requestFocus();
      return;
    }
    _discoveredServerFocusNodes[_localServers.first.id]?.requestFocus();
  }

  void _focusLastDiscoveredServerOrUrl() {
    if (_localServers.isEmpty) {
      _urlFocus.requestFocus();
      return;
    }
    _discoveredServerFocusNodes[_localServers.last.id]?.requestFocus();
  }

  List<String> _enteredUrls() => JellyfinEndpointDiscovery.parseUserEnteredUrls(_urlController.text);

  /// Shared persistence path for both username/password and Quick Connect:
  /// atomically provision the optional first-run profile, connection, and
  /// ownership row, then bind and pop only after durable success.
  Future<void> _persistAndExit(JellyfinConnection connection) async {
    if (!mounted) return;
    final activeProvider = context.read<ActiveProfileProvider>();
    await activeProvider.initialize();
    if (!mounted) return;
    final targetProfile = widget.targetProfile;
    var boundProfile = targetProfile ?? activeProvider.active;
    if (shouldPromptForJellyfinProfileSelection(
      targetProfile: targetProfile,
      activeProfile: activeProvider.active,
      hasProfiles: activeProvider.profiles.isNotEmpty,
    )) {
      await Navigator.of(
        context,
        rootNavigator: true,
      ).push<bool>(MaterialPageRoute(builder: (_) => const ProfileSwitchScreen(requireSelection: true)));
      if (!mounted) return;
      boundProfile = activeProvider.active;
      if (boundProfile == null) {
        setErrorText(t.messages.noProfilesAvailable);
        return;
      }
    }

    Profile? firstRunProfile;
    if (shouldCreateLocalJellyfinProfile(
      targetProfile: targetProfile,
      activeProfile: boundProfile,
      hasProfiles: activeProvider.profiles.isNotEmpty,
    )) {
      final now = DateTime.now();
      firstRunProfile = Profile.local(
        id: 'local-${const Uuid().v4()}',
        displayName: connection.userName.isNotEmpty ? connection.userName : connection.serverName,
        sortOrder: now.millisecondsSinceEpoch,
        createdAt: now,
      );
      boundProfile = firstRunProfile;
    }

    final bindProfile = boundProfile;
    if (bindProfile == null) {
      setErrorText(t.messages.noProfilesAvailable);
      return;
    }

    await persistAndBindConnection(
      context: context,
      connection: connection,
      bindToProfile: ProfileConnection(
        profileId: bindProfile.id,
        connectionId: connection.id,
        userToken: connection.accessToken,
        userIdentifier: connection.userId,
        tokenAcquiredAt: DateTime.now(),
      ),
      addToManager: null,
      firstRunProfile: firstRunProfile,
    );

    final boundToActive = bindProfile.id == activeProvider.activeId;
    if (!mounted) return;
    if (boundToActive) {
      await context.read<ActiveProfileBinder>().rebindIfActive(bindProfile.id);
    }

    if (!mounted) return;
    Navigator.of(context).pop(true);
  }

  Future<JellyfinConnectionAuthService> _buildAuthService() async {
    final authServiceFactory = widget._authServiceFactory;
    if (authServiceFactory != null) return await authServiceFactory();
    final clientVersion = await resolveJellyfinClientVersion();
    final deviceName = await _resolveDeviceName();
    return JellyfinConnectionAuthService(clientName: 'Plezy', clientVersion: clientVersion, deviceName: deviceName);
  }

  Future<String> _resolveDeviceName() async {
    final identity = await DeviceIdentityService.resolve();
    return sanitizeHeaderValue(identity.deviceName) ?? 'Plezy';
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return FocusedScrollScaffold(
      title: Text(t.addServer.addJellyfinTitle),
      slivers: [
        if (_qcInitiation != null)
          SliverFillRemaining(
            hasScrollBody: false,
            child: Padding(
              padding: const EdgeInsets.all(24),
              child: Center(child: _buildQuickConnectPanel(theme)),
            ),
          )
        else
          SliverPadding(
            padding: const EdgeInsets.all(16),
            sliver: SliverToBoxAdapter(
              child: Form(
                key: _formKey,
                child: Column(crossAxisAlignment: .stretch, children: _buildBodyChildren(theme)),
              ),
            ),
          ),
      ],
    );
  }

  List<Widget> _buildBodyChildren(ThemeData theme) {
    return [
      FocusableTextFormField(
        controller: _urlController,
        focusNode: _urlFocus,
        tvTextInputPresentation: PlatformDetector.isAppleTV()
            ? TvTextInputPresentation.platform
            : TvTextInputPresentation.automatic,
        autofocus: true,
        tvTextInputAutoOpenBehavior: TvTextInputAutoOpenBehavior.afterFirstFocus,
        keyboardType: TextInputType.url,
        minLines: 1,
        maxLines: 4,
        autocorrect: false,
        enableSuggestions: false,
        enabled: !busy,
        onChanged: (_) {
          if (_serverInfo == null && _serverEndpoint == null && !_quickConnectEnabled) return;
          setState(() {
            _clearResolvedServer();
          });
        },
        onNavigateDown: _serverInfo == null
            ? _focusFirstDiscoveredServerOrFind
            : () => _changeServerFocus.requestFocus(),
        textInputAction: TextInputAction.go,
        onFieldSubmitted: busy ? null : (_) => _probe(),
        decoration: InputDecoration(
          labelText: t.addServer.serverUrls,
          // URL example — intentionally not localized.
          hintText: 'https://jellyfin.example.com',
          helperText: _serverInfo == null ? t.addServer.serverUrlsHelper : null,
          prefixIcon: const AppIcon(Symbols.link_rounded, fill: 1),
        ),
        validator: (_) => _enteredUrls().isEmpty ? t.addServer.required : null,
      ),
      if (_serverInfo == null) ...[
        ..._buildLocalDiscoverySection(theme),
        const SizedBox(height: 16),
        FocusableButton(
          focusNode: _findServerFocus,
          useBackgroundFocus: true,
          onNavigateUp: _focusLastDiscoveredServerOrUrl,
          onPressed: busy ? null : _probe,
          child: FilledButton.icon(
            onPressed: busy ? null : _probe,
            icon: busy ? const LoadingIndicatorBox() : const AppIcon(Symbols.travel_explore_rounded, fill: 1),
            label: Text(t.addServer.findServer),
          ),
        ),
      ] else ...[
        const SizedBox(height: 16),
        _buildServerCard(theme),
        const SizedBox(height: 16),
        FocusableTextFormField(
          controller: _usernameController,
          focusNode: _usernameFocus,
          autocorrect: false,
          enableSuggestions: false,
          enabled: !busy,
          onNavigateUp: () => _changeServerFocus.requestFocus(),
          textInputAction: TextInputAction.next,
          onFieldSubmitted: busy ? null : (_) => _passwordFocus.requestFocus(),
          decoration: InputDecoration(
            labelText: t.addServer.username,
            prefixIcon: const AppIcon(Symbols.person_rounded, fill: 1),
          ),
          validator: (v) => v == null || v.trim().isEmpty ? t.addServer.required : null,
        ),
        const SizedBox(height: 12),
        FocusableTextFormField(
          controller: _passwordController,
          focusNode: _passwordFocus,
          obscureText: true,
          enabled: !busy,
          textInputAction: TextInputAction.done,
          onFieldSubmitted: busy ? null : (_) => _signIn(),
          decoration: InputDecoration(
            labelText: t.addServer.password,
            prefixIcon: const AppIcon(Symbols.lock_rounded, fill: 1),
          ),
          // Empty password is valid for some Jellyfin setups, so don't
          // require a value.
        ),
        const SizedBox(height: 16),
        FocusableButton(
          focusNode: _signInFocus,
          useBackgroundFocus: true,
          onPressed: busy ? null : _signIn,
          child: FilledButton.icon(
            onPressed: busy ? null : _signIn,
            icon: busy ? const LoadingIndicatorBox() : const AppIcon(Symbols.login_rounded, fill: 1),
            label: Text(t.addServer.signIn),
          ),
        ),
        if (_quickConnectEnabled) ...[
          const SizedBox(height: 12),
          FocusableButton(
            focusNode: _quickConnectFocus,
            useBackgroundFocus: true,
            onPressed: busy ? null : _startQuickConnect,
            child: OutlinedButton.icon(
              onPressed: busy ? null : _startQuickConnect,
              icon: const AppIcon(Symbols.tap_and_play_rounded, fill: 1),
              label: Text(t.auth.useQuickConnect),
            ),
          ),
        ],
      ],
      ...buildInlineError(theme),
    ];
  }

  Widget _buildServerCard(ThemeData theme) {
    return Container(
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: theme.colorScheme.surfaceContainerHighest,
        borderRadius: BorderRadius.circular(tokens(context).radiusMd),
      ),
      child: Row(
        children: [
          const AppIcon(Symbols.cloud_done_rounded, fill: 1),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: .start,
              children: [
                Text(_serverInfo!.serverName, style: theme.textTheme.titleSmall),
                Text(
                  'Jellyfin ${_serverInfo!.version}',
                  style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurface.withValues(alpha: 0.7)),
                ),
              ],
            ),
          ),
          FocusableButton(
            focusNode: _changeServerFocus,
            useBackgroundFocus: true,
            onNavigateUp: () => _urlFocus.requestFocus(),
            onNavigateDown: () => _usernameFocus.requestFocus(),
            onPressed: busy
                ? null
                : () => setState(() {
                    _clearResolvedServer();
                  }),
            child: TextButton(
              onPressed: busy
                  ? null
                  : () => setState(() {
                      _clearResolvedServer();
                    }),
              child: Text(t.addServer.change),
            ),
          ),
        ],
      ),
    );
  }

  List<Widget> _buildLocalDiscoverySection(ThemeData theme) {
    if (_isDiscoveringLocalServers) {
      return [
        const SizedBox(height: 16),
        Row(
          children: [
            const LoadingIndicatorBox(size: 16),
            const SizedBox(width: 10),
            Expanded(
              child: Text(
                t.addServer.searchingLocalServers,
                style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurface.withValues(alpha: 0.7)),
              ),
            ),
          ],
        ),
      ];
    }

    if (_localServers.isEmpty) return const [];
    final tokensRef = tokens(context);
    return [
      const SizedBox(height: 16),
      Text(t.addServer.localServers, style: theme.textTheme.titleSmall),
      const SizedBox(height: 8),
      for (final (i, server) in _localServers.indexed) ...[
        if (i > 0) SizedBox(height: tokensRef.groupGap),
        _DiscoveredJellyfinServerTile(
          server: server,
          borderRadius: groupItemRadii(context, i, _localServers.length),
          focusNode: _discoveredServerFocusNodes[server.id],
          onNavigateUp: () {
            final index = _localServers.indexOf(server);
            if (index <= 0) {
              _urlFocus.requestFocus();
              return;
            }
            _discoveredServerFocusNodes[_localServers[index - 1].id]?.requestFocus();
          },
          onNavigateDown: () {
            final index = _localServers.indexOf(server);
            if (index < 0 || index == _localServers.length - 1) {
              _findServerFocus.requestFocus();
              return;
            }
            _discoveredServerFocusNodes[_localServers[index + 1].id]?.requestFocus();
          },
          onTap: busy ? null : () => unawaited(_useDiscoveredServer(server)),
        ),
      ],
      const SizedBox(height: 8),
    ];
  }

  Widget _buildQuickConnectPanel(ThemeData theme) {
    final code = _qcInitiation!.code;
    final muted = theme.colorScheme.onSurface.withValues(alpha: 0.7);
    return ConstrainedBox(
      constraints: const BoxConstraints(maxWidth: 420),
      child: Column(
        mainAxisSize: .min,
        children: [
          Text(
            t.auth.quickConnectInstructions,
            textAlign: TextAlign.center,
            style: theme.textTheme.bodyLarge?.copyWith(color: muted),
          ),
          const SizedBox(height: 32),
          FittedBox(
            fit: BoxFit.scaleDown,
            child: Padding(
              // letterSpacing adds a trailing gap after the last glyph;
              // matching left padding keeps the code optically centered.
              padding: const EdgeInsets.only(left: 12),
              child: Text(
                code,
                style: theme.textTheme.displayLarge?.copyWith(
                  fontFamily: 'monospace',
                  fontWeight: .bold,
                  letterSpacing: 12,
                ),
              ),
            ),
          ),
          const SizedBox(height: 32),
          Row(
            mainAxisSize: .min,
            children: [
              const LoadingIndicatorBox(size: 16),
              const SizedBox(width: 10),
              Text(t.auth.quickConnectWaiting, style: theme.textTheme.bodyMedium?.copyWith(color: muted)),
            ],
          ),
          const SizedBox(height: 32),
          FocusableButton(
            focusNode: _cancelQuickConnectFocus,
            useBackgroundFocus: true,
            onPressed: _cancelQuickConnect,
            child: OutlinedButton.icon(
              onPressed: _cancelQuickConnect,
              icon: const AppIcon(Symbols.close_rounded, fill: 1),
              label: Text(t.auth.quickConnectCancel),
            ),
          ),
          ...buildInlineError(theme, gap: 16, center: true),
        ],
      ),
    );
  }
}

class _DiscoveredJellyfinServerTile extends StatelessWidget {
  final DiscoveredJellyfinServer server;
  final BorderRadius borderRadius;
  final FocusNode? focusNode;
  final VoidCallback? onNavigateUp;
  final VoidCallback? onNavigateDown;
  final VoidCallback? onTap;

  const _DiscoveredJellyfinServerTile({
    required this.server,
    required this.borderRadius,
    required this.focusNode,
    required this.onNavigateUp,
    required this.onNavigateDown,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return FocusableWrapper(
      focusNode: focusNode,
      disableScale: true,
      // Border drawn by CardFocusBorder so it paints over the opaque Material.
      delegateFocusBorder: true,
      descendantsAreFocusable: false,
      onSelect: onTap,
      onNavigateUp: onNavigateUp,
      onNavigateDown: onNavigateDown,
      child: CardFocusBorder(
        borderRadii: borderRadius,
        strokeAlign: BorderSide.strokeAlignInside,
        child: Material(
          color: theme.colorScheme.surfaceContainerHighest,
          borderRadius: borderRadius,
          child: InkWell(
            onTap: onTap,
            borderRadius: borderRadius,
            child: Padding(
              padding: const EdgeInsets.all(12),
              child: Row(
                children: [
                  const AppIcon(Symbols.dns_rounded, fill: 1),
                  const SizedBox(width: 12),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: .start,
                      mainAxisSize: .min,
                      children: [
                        Text(server.name, style: theme.textTheme.titleSmall),
                        const SizedBox(height: 2),
                        Text(
                          server.address,
                          maxLines: 1,
                          overflow: .ellipsis,
                          style: theme.textTheme.bodySmall?.copyWith(
                            color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
                          ),
                        ),
                      ],
                    ),
                  ),
                  const SizedBox(width: 12),
                  const AppIcon(Symbols.chevron_right_rounded, fill: 1),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}
