import 'dart:async';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:plezy/widgets/app_icon.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';

import '../../focus/focus_memory_tracker.dart';
import '../../focus/focusable_text_field.dart';
import '../../focus/input_mode_tracker.dart';
import '../../i18n/strings.g.dart';
import '../main_screen.dart';
import '../../mixins/mounted_set_state_mixin.dart';
import '../../mixins/refreshable.dart';
import '../../providers/hidden_libraries_provider.dart';
import '../../providers/download_provider.dart';
import '../../providers/libraries_provider.dart';
import '../../services/donation_service.dart';
import '../../services/download_storage_service.dart';
import '../../services/file_picker_service.dart';
import '../../services/saf_storage_service.dart';
import '../../services/settings_export_service.dart';
import '../../providers/theme_provider.dart';
import '../../providers/seerr_account_provider.dart';
import '../../services/keyboard_shortcuts_service.dart';
import '../../services/background_work_diagnostics_service.dart';
import '../../services/settings_service.dart' as settings;
import '../../widgets/background_download_warning_banner.dart';
import '../../services/update_service.dart';
import '../../utils/dialogs.dart';
import '../../utils/snackbar_helper.dart';
import '../../utils/platform_detector.dart';
import '../../utils/update_dialog.dart';
import '../../widgets/desktop_app_bar.dart';
import '../../widgets/dialog_action_button.dart';
import '../../widgets/focusable_list_tile.dart';
import '../../widgets/library_management_sheet.dart';
import '../../widgets/overlay_sheet.dart';
import '../../widgets/setting_tile.dart';
import '../../widgets/settings_builder.dart';
import '../../widgets/settings_section.dart';
import '../../profiles/active_profile_provider.dart';
import '../../profiles/profile.dart';
import '../../watch_together/services/watch_together_relay_endpoint.dart';
import 'about_screen.dart';
import 'add_connection_screen.dart';
import 'appearance_settings_screen.dart';
import 'keyboard_shortcuts_screen.dart';
import 'logs_screen.dart';
import 'playback_settings_screen.dart';
import '../profile/profile_switch_screen.dart';
import 'services_settings_screen.dart';
import 'settings_utils.dart';
import 'tracker_service_info.dart';
import '../../widgets/loading_indicator_box.dart';

class SettingsScreen extends StatefulWidget {
  const SettingsScreen({
    super.key,
    this.downloadDirectoryWritableChecker,
    this.settingsExporter,
    this.settingsImporter,
    this.backgroundWorkDiagnosticsService,
  });

  @visibleForTesting
  final Future<bool> Function(Directory directory)? downloadDirectoryWritableChecker;

  @visibleForTesting
  final Future<String?> Function()? settingsExporter;

  @visibleForTesting
  final Future<ImportResult?> Function()? settingsImporter;
  @visibleForTesting
  final BackgroundWorkDiagnosticsService? backgroundWorkDiagnosticsService;

  @override
  State<SettingsScreen> createState() => _SettingsScreenState();
}

class _SettingsScreenState extends State<SettingsScreen> with FocusableTab, MountedSetStateMixin {
  BackgroundWorkDiagnosticsService get _backgroundWorkDiagnostics =>
      widget.backgroundWorkDiagnosticsService ?? BackgroundWorkDiagnosticsService.instance;
  late final FocusMemoryTracker _focusTracker;

  // Focus tracking keys
  static const _kDonate = 'donate';
  static const _kAppearance = 'appearance';
  static const _kPlayback = 'playback';
  static const _kManageLibraries = 'manage_libraries';
  static const _kServices = 'services';
  static const _kDownloadLocation = 'download_location';
  static const _kDownloadOnWifiOnly = 'download_on_wifi_only';
  static const _kAutoRemoveWatchedDownloads = 'auto_remove_watched_downloads';
  static const _kBackgroundDownloads = 'background_downloads';
  static const _kVideoPlayerControls = 'video_player_controls';
  static const _kVideoPlayerNavigation = 'video_player_navigation';
  static const _kCrashReporting = 'crash_reporting';
  static const _kDebugLogging = 'debug_logging';
  static const _kViewLogs = 'view_logs';
  static const _kClearImageCache = 'clear_image_cache';
  static const _kResetSettings = 'reset_settings';
  static const _kCheckForUpdates = 'check_for_updates';
  static const _kAutoCheckUpdatesOnStartup = 'auto_check_updates_on_startup';
  static const _kAbout = 'about';
  static const _kWatchTogetherRelay = 'watch_together_relay';
  static const _kExportSettings = 'export_settings';
  static const _kImportSettings = 'import_settings';

  KeyboardShortcutsService? _keyboardService;
  late final bool _keyboardShortcutsSupported = KeyboardShortcutsService.isPlatformSupported();

  // Update checking state
  bool _isCheckingForUpdate = false;
  Map<String, dynamic>? _updateInfo;

  @override
  void initState() {
    super.initState();
    _focusTracker = FocusMemoryTracker(debugLabelPrefix: 'settings');
    if (_keyboardShortcutsSupported) {
      KeyboardShortcutsService.getInstance().then((s) {
        setStateIfMounted(() => _keyboardService = s);
      });
    }
  }

  @override
  void dispose() {
    _focusTracker.dispose();
    super.dispose();
  }

  @override
  void focusActiveTabIfReady() {
    if (InputModeTracker.isKeyboardMode(context, listen: false)) {
      _focusTracker.restoreFocus(fallbackKey: DonationService.isEnabled ? _kDonate : _kAppearance);
    }
  }

  void _navigateToSidebar() {
    MainScreenFocusScope.focusSidebarOf(context);
  }

  KeyEventResult _handleKeyEvent(FocusNode _, KeyEvent event) {
    if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.arrowLeft) {
      _navigateToSidebar();
      return KeyEventResult.handled;
    }
    return KeyEventResult.ignored;
  }

  settings.SettingsService get _settingsService => settings.SettingsService.instance;

  @override
  Widget build(BuildContext context) {
    final hasLibraries = context.select<LibrariesProvider, bool>((p) => p.libraries.isNotEmpty);

    if (OverlaySheetController.maybeOf(context) != null) {
      return _buildContent(context, hasLibraries: hasLibraries);
    }

    // Settings is hosted by MainScreen on side-navigation layouts, but it is a
    // separate pushed route on phones. Add a route-local host only for the
    // latter, and build its content from below the host so adaptive sheets do
    // not fall back to modal routes with a competing Android back path.
    return OverlaySheetHost(
      canPop: true,
      child: Builder(builder: (hostContext) => _buildContent(hostContext, hasLibraries: hasLibraries)),
    );
  }

  Widget _buildContent(BuildContext sheetContext, {required bool hasLibraries}) {
    return Scaffold(
      body: Focus(
        onKeyEvent: _handleKeyEvent,
        child: CustomScrollView(
          primary: false,
          slivers: [
            ExcludeFocus(child: CustomAppBar(title: Text(t.settings.title), pinned: true)),
            SliverList(
              delegate: SliverChildListDelegate([
                const SizedBox(height: 8),
                SettingsGroup(
                  children: [
                    if (DonationService.isEnabled) _buildDonateTile(),
                    _buildAppearanceTile(),
                    _buildPlaybackTile(),
                    if (hasLibraries) _buildManageLibrariesTile(sheetContext),
                    _buildServicesTile(),
                  ],
                ),

                _buildConnectionsSection(sheetContext),

                if (!PlatformDetector.isAppleTV()) _buildDownloadsSection(),

                if (_keyboardShortcutsSupported) ...[_buildKeyboardShortcutsSection()],

                _buildAdvancedSection(),

                if (UpdateService.isUpdateCheckEnabled) ...[_buildUpdateSection()],

                // Hidden on Android TV / tvOS (no document picker); desktop in
                // force-TV mode keeps it — FilePickerService works there.
                if (!PlatformDetector.isTV() || PlatformDetector.isDesktopOS()) _buildBackupSection(),

                const SizedBox(height: 24),
                SettingsGroup(
                  children: [
                    SettingNavigationTile(
                      focusNode: _focusTracker.get(_kAbout),
                      icon: Symbols.info_rounded,
                      title: t.settings.about,
                      subtitle: t.settings.aboutDescription,
                      destinationBuilder: (context) => const AboutScreen(),
                    ),
                  ],
                ),
                const SizedBox(height: 24),
              ]),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildDonateTile() {
    return SettingNavigationTile(
      focusNode: _focusTracker.get(_kDonate),
      icon: Symbols.favorite_rounded,
      title: t.settings.supportDeveloper,
      subtitle: t.settings.supportDeveloperDescription,
      trailingIcon: Symbols.open_in_new_rounded,
      onTap: () async {
        final url = Uri.parse(DonationService.donationUrl);
        if (await canLaunchUrl(url)) {
          await launchUrl(url, mode: LaunchMode.externalApplication);
        }
      },
    );
  }

  Widget _buildAppearanceTile() {
    return Consumer<ThemeProvider>(
      builder: (context, themeProvider, _) => SettingValueBuilder<int>(
        pref: settings.SettingsService.libraryDensity,
        builder: (context, libraryDensity, _) {
          final summary = '${themeModeLabel(themeProvider.themeMode)} · ${t.settings.libraryDensity} $libraryDensity';
          return SettingNavigationTile(
            focusNode: _focusTracker.get(_kAppearance),
            icon: Symbols.palette_rounded,
            title: t.settings.appearance,
            subtitle: summary,
            destinationBuilder: (context) => const AppearanceSettingsScreen(),
          );
        },
      ),
    );
  }

  Widget _buildPlaybackTile() {
    return SettingNavigationTile(
      focusNode: _focusTracker.get(_kPlayback),
      icon: Symbols.play_circle_rounded,
      title: t.settings.videoPlayback,
      subtitle: t.settings.videoPlaybackDescription,
      destinationBuilder: (context) => const PlaybackSettingsScreen(),
    );
  }

  Widget _buildManageLibrariesTile(BuildContext context) {
    return SettingNavigationTile(
      focusNode: _focusTracker.get(_kManageLibraries),
      icon: Symbols.video_library_rounded,
      title: t.libraries.manageLibraries,
      subtitle: t.settings.manageLibrariesDescription,
      onTap: () => showLibraryManagementSheet(context),
    );
  }

  Widget _buildServicesTile() {
    // The tracker account providers are watched through [TrackerServiceInfo].
    return Consumer<SeerrAccountProvider>(
      builder: (context, seerr, _) {
        final connectedNames = <String>[
          for (final info in TrackerServiceInfo.all)
            if (info.isConnected(context)) info.displayName,
          if (seerr.isConnected) t.services.names.seerr,
        ];
        final subtitle = connectedNames.isEmpty ? t.settings.servicesDescription : connectedNames.join(' · ');
        return SettingNavigationTile(
          focusNode: _focusTracker.get(_kServices),
          icon: Symbols.sync_rounded,
          title: t.settings.services,
          subtitle: subtitle,
          destinationBuilder: (_) => const ServicesSettingsScreen(),
        );
      },
    );
  }

  Widget _buildConnectionsSection(BuildContext context) {
    final active = context.select<ActiveProfileProvider, Profile?>((p) => p.active);
    final subtitle = active == null
        ? t.connections.addConnectionSubtitleNoProfile
        : t.connections.addConnectionSubtitleScoped(displayName: active.displayName);

    return SettingsGroup(
      title: t.connections.sectionTitle,
      children: [
        // Connections are managed per-profile (via the Profiles section
        // and each profile's detail screen). The shortcut here just opens
        // the picker scoped to the active profile so users can add a Plex
        // account, Jellyfin server, or borrow from another profile.
        SettingNavigationTile(
          icon: Symbols.add_link_rounded,
          title: t.connections.addConnection,
          subtitle: subtitle,
          onTap: () {
            final active = context.read<ActiveProfileProvider>().active;
            Navigator.push(context, MaterialPageRoute(builder: (_) => AddConnectionScreen(targetProfile: active)));
          },
        ),
        _buildProfilesTile(context),
      ],
    );
  }

  Widget _buildProfilesTile(BuildContext context) {
    // ActiveProfileProvider already merges local rows with virtual Plex
    // Home profiles — counting only the local DB rows made every Plex Home
    // household read as a single profile here. `context.select` keeps
    // rebuilds scoped to actual count/name changes (a StreamBuilder here
    // was also re-created on every settings rebuild).
    final count = context.select<ActiveProfileProvider, int>((p) => p.profiles.length);
    final activeName = context.select<ActiveProfileProvider, String?>((p) => p.active?.displayName);
    final subtitle = count <= 1
        ? t.profiles.summarySingle
        : (activeName != null
              ? t.profiles.summaryMultipleWithActive(count: count, activeName: activeName)
              : t.profiles.summaryMultiple(count: count));
    return SettingNavigationTile(
      icon: Symbols.group_rounded,
      title: t.profiles.sectionTitle,
      subtitle: subtitle,
      onTap: () => Navigator.of(
        context,
        rootNavigator: true,
      ).push(MaterialPageRoute(builder: (_) => const ProfileSwitchScreen())),
    );
  }

  Widget _buildDownloadsSection() {
    final storageService = DownloadStorageService.instance;
    final isCustom = storageService.isUsingCustomPath();

    return SettingsGroup(
      title: t.settings.downloads,
      children: [
        if (!Platform.isIOS)
          FutureBuilder<String>(
            future: storageService.getCurrentDownloadPathDisplay(),
            builder: (context, snapshot) {
              final currentPath = snapshot.data ?? '...';
              return FocusableListTile(
                focusNode: _focusTracker.get(_kDownloadLocation),
                leading: const AppIcon(Symbols.folder_rounded, fill: 1),
                title: Text(isCustom ? t.settings.downloadLocationCustom : t.settings.downloadLocationDefault),
                subtitle: Text(currentPath, maxLines: 2, overflow: .ellipsis),
                trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
                onTap: () => _showDownloadLocationDialog(),
              );
            },
          ),
        SettingSwitchTile(
          focusNode: _focusTracker.get(_kDownloadOnWifiOnly),
          pref: settings.SettingsService.downloadOnWifiOnly,
          icon: Symbols.wifi_rounded,
          title: t.settings.downloadOnWifiOnly,
          subtitle: t.settings.downloadOnWifiOnlyDescription,
        ),
        SettingSwitchTile(
          focusNode: _focusTracker.get(_kAutoRemoveWatchedDownloads),
          pref: settings.SettingsService.autoRemoveWatchedDownloads,
          icon: Symbols.delete_sweep_rounded,
          title: t.settings.autoRemoveWatchedDownloads,
          subtitle: t.settings.autoRemoveWatchedDownloadsDescription,
        ),
        if (_backgroundWorkDiagnostics.isSupported) _buildBackgroundDownloadsTile(),
      ],
    );
  }

  /// Standing answer to "why do my downloads stop when I leave the app" — also
  /// what support can ask a user to read out without needing a log upload.
  Widget _buildBackgroundDownloadsTile() {
    final diagnostics = _backgroundWorkDiagnostics;
    return ListenableBuilder(
      listenable: diagnostics,
      builder: (context, _) {
        final status = diagnostics.status;
        final scheme = Theme.of(context).colorScheme;
        final (icon, color, summary) = switch (status) {
          _ when !status.probed => (Symbols.help_rounded, null, t.downloads.backgroundWarning.statusUnknown),
          _ when status.isBlocked => (
            Symbols.battery_alert_rounded,
            scheme.error,
            t.downloads.backgroundWarning.statusBlocked,
          ),
          _ when !status.isHealthy => (
            Symbols.info_rounded,
            scheme.tertiary,
            t.downloads.backgroundWarning.statusDegraded,
          ),
          _ => (Symbols.check_circle_rounded, null, t.downloads.backgroundWarning.statusOk),
        };
        return FocusableListTile(
          focusNode: _focusTracker.get(_kBackgroundDownloads),
          leading: AppIcon(icon, fill: 1, color: color),
          title: Text(t.downloads.backgroundWarning.statusTile),
          subtitle: Text(summary),
          trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
          onTap: () async {
            await diagnostics.refresh();
            if (!context.mounted) return;
            if (diagnostics.status.isHealthy) {
              showAppSnackBar(context, t.downloads.backgroundWarning.statusOk);
              return;
            }
            await showBackgroundDownloadWarningDialog(context, service: diagnostics);
          },
        );
      },
    );
  }

  Widget _buildKeyboardShortcutsSection() {
    if (_keyboardService == null) return const SizedBox.shrink();

    return SettingsGroup(
      title: t.settings.keyboardShortcuts,
      children: [
        SettingNavigationTile(
          focusNode: _focusTracker.get(_kVideoPlayerControls),
          icon: Symbols.keyboard_rounded,
          title: t.settings.videoPlayerControls,
          subtitle: t.settings.keyboardShortcutsDescription,
          onTap: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => KeyboardShortcutsScreen(keyboardService: _keyboardService!)),
            );
          },
        ),
        SettingSwitchTile(
          focusNode: _focusTracker.get(_kVideoPlayerNavigation),
          pref: settings.SettingsService.videoPlayerNavigationEnabled,
          icon: Symbols.gamepad_rounded,
          title: t.settings.videoPlayerNavigation,
          subtitle: t.settings.videoPlayerNavigationDescription,
        ),
      ],
    );
  }

  Widget _buildAdvancedSection() {
    return SettingsGroup(
      title: t.settings.advanced,
      children: [
        SettingNavigationTile(
          focusNode: _focusTracker.get(_kWatchTogetherRelay),
          icon: Symbols.dns_rounded,
          title: t.settings.watchTogetherRelay,
          subtitle: t.settings.watchTogetherRelayDescription,
          onTap: () => _showRelayUrlDialog(),
        ),
        SettingSwitchTile(
          focusNode: _focusTracker.get(_kCrashReporting),
          pref: settings.SettingsService.crashReporting,
          icon: Symbols.monitoring_rounded,
          title: t.settings.crashReporting,
          subtitle: t.settings.crashReportingDescription,
        ),
        SettingSwitchTile(
          focusNode: _focusTracker.get(_kDebugLogging),
          pref: settings.SettingsService.enableDebugLogging,
          icon: Symbols.bug_report_rounded,
          title: t.settings.debugLogging,
          subtitle: t.settings.debugLoggingDescription,
        ),
        SettingNavigationTile(
          focusNode: _focusTracker.get(_kViewLogs),
          icon: Symbols.article_rounded,
          title: t.settings.viewLogs,
          subtitle: t.settings.viewLogsDescription,
          destinationBuilder: (context) => const LogsScreen(),
        ),
        SettingNavigationTile(
          focusNode: _focusTracker.get(_kClearImageCache),
          icon: Symbols.cleaning_services_rounded,
          title: t.settings.clearImageCache,
          subtitle: t.settings.clearImageCacheDescription,
          onTap: () => _showClearImageCacheDialog(),
        ),
        SettingNavigationTile(
          focusNode: _focusTracker.get(_kResetSettings),
          icon: Symbols.restore_rounded,
          title: t.settings.resetSettings,
          subtitle: t.settings.resetSettingsDescription,
          onTap: () => _showResetSettingsDialog(),
        ),
        if (kDebugMode)
          SettingNavigationTile(
            icon: Symbols.error_rounded,
            title: 'Test Sentry',
            subtitle: 'Send a test error',
            onTap: () {
              throw Exception("Example exception");
            },
          ),
        if (kDebugMode)
          SettingNavigationTile(
            icon: Symbols.timer_rounded,
            title: 'Test ANR',
            subtitle: 'Block the main thread for 10 seconds',
            onTap: () {
              showSnackBar(context, 'Blocking main thread...');
              final end = DateTime.now().add(const Duration(seconds: 10));
              while (DateTime.now().isBefore(end)) {}
            },
          ),
      ],
    );
  }

  Widget _buildBackupSection() {
    return SettingsGroup(
      title: t.settings.backup,
      children: [
        SettingNavigationTile(
          focusNode: _focusTracker.get(_kExportSettings),
          icon: Symbols.upload_rounded,
          title: t.settings.exportSettings,
          subtitle: t.settings.exportSettingsDescription,
          onTap: _handleExportSettings,
        ),
        SettingNavigationTile(
          focusNode: _focusTracker.get(_kImportSettings),
          icon: Symbols.download_rounded,
          title: t.settings.importSettings,
          subtitle: t.settings.importSettingsDescription,
          onTap: _showImportSettingsDialog,
        ),
      ],
    );
  }

  Widget _buildAutoCheckUpdatesOnStartupTile() => SettingSwitchTile(
    focusNode: _focusTracker.get(_kAutoCheckUpdatesOnStartup),
    pref: settings.SettingsService.autoCheckUpdatesOnStartup,
    icon: Symbols.notifications_active_rounded,
    title: t.settings.autoCheckUpdatesOnStartup,
    subtitle: t.settings.autoCheckUpdatesOnStartupDescription,
  );

  Widget _buildUpdateSection() {
    if (UpdateService.useNativeUpdater) {
      return SettingsGroup(
        title: t.settings.updates,
        children: [
          SettingNavigationTile(
            focusNode: _focusTracker.get(_kCheckForUpdates),
            icon: Symbols.system_update_rounded,
            title: t.settings.checkForUpdates,
            onTap: () => UpdateService.checkForUpdatesNative(inBackground: false),
          ),
          _buildAutoCheckUpdatesOnStartupTile(),
        ],
      );
    }

    final hasUpdate = _updateInfo != null && _updateInfo!['hasUpdate'] == true;

    return SettingsGroup(
      title: t.settings.updates,
      children: [
        FocusableListTile(
          focusNode: _focusTracker.get(_kCheckForUpdates),
          leading: AppIcon(
            hasUpdate ? Symbols.system_update_rounded : Symbols.check_circle_rounded,
            fill: 1,
            color: hasUpdate ? Colors.orange : null,
          ),
          title: Text(hasUpdate ? t.settings.updateAvailable : t.settings.checkForUpdates),
          subtitle: hasUpdate ? Text(t.update.versionAvailable(version: _updateInfo!['latestVersion'])) : null,
          trailing: _isCheckingForUpdate
              ? const LoadingIndicatorBox(size: 24)
              : const AppIcon(Symbols.chevron_right_rounded, fill: 1),
          onTap: _isCheckingForUpdate
              ? null
              : () {
                  if (hasUpdate) {
                    _showUpdateDialog();
                  } else {
                    _checkForUpdates();
                  }
                },
        ),
        _buildAutoCheckUpdatesOnStartupTile(),
      ],
    );
  }

  Future<void> _showDownloadLocationDialog() async {
    final storageService = DownloadStorageService.instance;
    final isCustom = storageService.isUsingCustomPath();

    await showScopedDialog<void>(
      context: context,
      builder: (dialogContext) => AlertDialog(
        title: Text(t.settings.downloads),
        content: Column(
          mainAxisSize: .min,
          crossAxisAlignment: .start,
          children: [
            Text(t.settings.downloadLocationDescription),
            const SizedBox(height: 16),
            FutureBuilder<String>(
              future: storageService.getCurrentDownloadPathDisplay(),
              builder: (context, snapshot) {
                return Text(
                  t.settings.currentPath(path: snapshot.data ?? '...'),
                  style: Theme.of(context).textTheme.bodySmall,
                );
              },
            ),
          ],
        ),
        actions: [
          if (isCustom)
            DialogActionButton(
              onPressed: () async {
                // Run the async work first, then pop — popping first leaves
                // setState inside _resetDownloadLocation racing against the
                // already-dismissed dialog (and any re-opened instance).
                await _resetDownloadLocation();
                if (dialogContext.mounted) Navigator.pop(dialogContext);
              },
              label: t.settings.resetToDefault,
            ),
          DialogActionButton(onPressed: () => Navigator.pop(dialogContext), label: t.common.cancel),
          DialogActionButton(
            onPressed: () async {
              final changed = await _selectDownloadLocation();
              if (changed && dialogContext.mounted) Navigator.pop(dialogContext);
            },
            label: t.settings.selectFolder,
            isPrimary: true,
          ),
        ],
      ),
    );
  }

  Future<bool> _selectDownloadLocation() async {
    final changed = await guardSettingsOperation<bool, DownloadStorageException>(
      context,
      operation: 'Download directory selection',
      body: () async {
        String? selectedPath;
        String pathType = 'file';

        if (Platform.isAndroid) {
          final safStorage = SafStorageService.instance;
          if (!safStorage.supportsDirectoryPicker) {
            showErrorSnackBar(context, t.settings.downloadLocationPickerUnavailable);
            return false;
          }
          selectedPath = await safStorage.pickDirectory();
          if (!mounted) return false;
          if (selectedPath != null) pathType = 'saf';
        } else {
          selectedPath = await FilePickerService.instance.getDirectoryPath(dialogTitle: t.settings.selectFolder);
          if (!mounted) return false;
        }
        if (selectedPath == null) return false;

        if (pathType == 'file') {
          final dir = Directory(selectedPath);
          final writableChecker =
              widget.downloadDirectoryWritableChecker ?? DownloadStorageService.instance.isDirectoryWritable;
          final isWritable = await writableChecker(dir);
          if (!mounted) return false;
          if (!isWritable) {
            showErrorSnackBar(context, t.settings.downloadLocationInvalid);
            return false;
          }
        }

        await context.read<DownloadProvider>().setDownloadLocation(path: selectedPath, pathType: pathType);
        if (!mounted) return false;

        // ignore: no-empty-block - setState triggers rebuild to reflect new download path
        setState(() {});
        showSuccessSnackBar(context, t.settings.downloadLocationChanged);
        return true;
      },
    );
    return changed ?? false;
  }

  Future<void> _resetDownloadLocation() async {
    await context.read<DownloadProvider>().resetDownloadLocation();

    if (mounted) {
      // ignore: no-empty-block - setState triggers rebuild to reflect reset path
      setState(() {});
      showAppSnackBar(context, t.settings.downloadLocationReset);
    }
  }

  Future<void> _showRelayUrlDialog() async {
    await showScopedDialog<void>(
      context: context,
      builder: (_) => _RelayUrlDialog(settingsService: _settingsService),
    );
  }

  Future<void> _showClearImageCacheDialog() async {
    final confirmed = await showConfirmDialog(
      context,
      title: t.settings.clearImageCache,
      message: t.settings.clearImageCacheDescription,
      confirmText: t.common.clear,
    );
    if (!confirmed) return;
    await _settingsService.clearImageCache();
    if (mounted) showSuccessSnackBar(context, t.settings.clearImageCacheSuccess);
  }

  Future<void> _showResetSettingsDialog() async {
    final confirmed = await showConfirmDialog(
      context,
      title: t.settings.resetSettings,
      message: t.settings.resetSettingsDescription,
      confirmText: t.common.reset,
      isDestructive: true,
    );
    if (!mounted || !confirmed) return;
    await context.read<DownloadProvider>().resetDownloadLocation();
    await _settingsService.resetAllSettings();
    await _keyboardService?.resetToDefaults();
    if (mounted) showSuccessSnackBar(context, t.settings.resetSettingsSuccess);
  }

  Future<void> _handleExportSettings() async {
    await guardSettingsOperation<void, SettingsExportException>(
      context,
      operation: 'Settings export',
      body: () async {
        final path = await (widget.settingsExporter ?? SettingsExportService.exportToFile)();
        if (!mounted || path == null) return;
        showSuccessSnackBar(context, t.settings.exportSettingsSuccess);
      },
    );
  }

  Future<void> _showImportSettingsDialog() async {
    final confirmed = await showConfirmDialog(
      context,
      title: t.settings.importSettings,
      message: t.settings.importSettingsConfirm,
      confirmText: t.settings.importSettings,
    );
    if (!mounted || !confirmed) return;
    await _handleImportSettings();
  }

  Future<void> _handleImportSettings() async {
    await guardSettingsOperation<void, SettingsExportException>(
      context,
      operation: 'Settings import',
      body: () async {
        // The two typed import failures carry their own message, so they are
        // handled here instead of falling through to the generic guard.
        try {
          final result = await (widget.settingsImporter ?? SettingsExportService.importFromFile)();
          if (!mounted) return;
          if (result == null) return; // user cancelled file picker

          final themeProvider = context.read<ThemeProvider>();
          final hiddenLibrariesProvider = context.read<HiddenLibrariesProvider>();
          final librariesProvider = context.read<LibrariesProvider>();

          // Import wrote directly to SharedPreferences, bypassing `write`. Push
          // fresh values into active listenables before providers re-read settings.
          _settingsService.refreshListenables();
          unawaited(LocaleSettings.setLocale(_settingsService.read(settings.SettingsService.appLocale)));
          await Future.wait([
            themeProvider.reload(),
            hiddenLibrariesProvider.refresh(),
            if (_keyboardService != null) _keyboardService!.refreshFromStorage(),
          ]);
          unawaited(librariesProvider.refresh());

          if (!mounted) return;
          showSuccessSnackBar(context, t.settings.importSettingsSuccess);
        } on NoUserSignedInException {
          if (mounted) showErrorSnackBar(context, t.settings.importSettingsNoUser);
        } on InvalidExportFileException {
          if (mounted) showErrorSnackBar(context, t.settings.importSettingsInvalidFile);
        }
      },
    );
  }

  Future<void> _checkForUpdates() async {
    setState(() => _isCheckingForUpdate = true);

    try {
      final updateInfo = await UpdateService.checkForUpdates();

      if (mounted) {
        setState(() {
          _updateInfo = updateInfo;
          _isCheckingForUpdate = false;
        });

        if (updateInfo == null || updateInfo['hasUpdate'] != true) {
          showAppSnackBar(context, t.update.latestVersion);
        }
      }
    } catch (e) {
      if (mounted) {
        setState(() => _isCheckingForUpdate = false);
        showErrorSnackBar(context, t.update.checkFailed);
      }
    }
  }

  void _showUpdateDialog() {
    final updateInfo = _updateInfo;
    if (updateInfo == null) return;
    unawaited(
      showUpdateAvailableDialog(context, updateInfo, title: t.settings.updateAvailable, dismissLabel: t.common.close),
    );
  }
}

class _RelayUrlDialog extends StatefulWidget {
  final settings.SettingsService settingsService;

  const _RelayUrlDialog({required this.settingsService});

  @override
  State<_RelayUrlDialog> createState() => _RelayUrlDialogState();
}

class _RelayUrlDialogState extends State<_RelayUrlDialog> {
  late final TextEditingController _controller;
  final _saveFocusNode = FocusNode(debugLabel: 'WatchTogetherRelaySave');
  bool _relayUrlInvalid = false;

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController(
      text: widget.settingsService.read(settings.SettingsService.customRelayUrl) ?? '',
    );
  }

  @override
  void dispose() {
    _saveFocusNode.dispose();
    _controller.dispose();
    super.dispose();
  }

  Future<void> _reset() async {
    _controller.clear();
    await widget.settingsService.write(settings.SettingsService.customRelayUrl, null);
    if (mounted) Navigator.pop(context);
  }

  Future<void> _save() async {
    final value = _controller.text;
    if (value.trim().isEmpty) {
      await widget.settingsService.write(settings.SettingsService.customRelayUrl, null);
      if (mounted) Navigator.pop(context);
      return;
    }

    final endpoint = WatchTogetherRelayEndpoint.tryParseCustom(value);
    if (endpoint == null) {
      setState(() => _relayUrlInvalid = true);
      return;
    }
    await widget.settingsService.write(settings.SettingsService.customRelayUrl, endpoint.canonicalBaseUrl);
    if (mounted) Navigator.pop(context);
  }

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text(t.settings.watchTogetherRelay),
      content: FocusableTextField(
        controller: _controller,
        decoration: InputDecoration(
          labelText: 'URL',
          hintText: t.settings.watchTogetherRelayHint,
          errorText: _relayUrlInvalid ? t.settings.watchTogetherRelayInvalid : null,
        ),
        autofocus: true,
        textInputAction: TextInputAction.done,
        onChanged: (_) {
          if (_relayUrlInvalid) {
            setState(() => _relayUrlInvalid = false);
          }
        },
        onEditingComplete: () => _saveFocusNode.requestFocus(),
        onNavigateDown: _saveFocusNode.requestFocus,
      ),
      actions: [
        DialogActionButton(onPressed: _reset, label: t.settings.resetToDefault),
        DialogActionButton(onPressed: () => Navigator.pop(context), label: t.common.cancel),
        DialogActionButton(focusNode: _saveFocusNode, onPressed: _save, label: t.common.save),
      ],
    );
  }
}
