import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:file_picker/file_picker.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';

import '../utils/app_logger.dart';
import '../utils/formatters.dart';
import '../utils/platform_detector.dart';
import 'file_picker_service.dart';
import 'settings_service.dart';
import 'storage_service.dart';

class ImportResult {
  final int keysImported;
  final int keysSkipped;
  const ImportResult({required this.keysImported, required this.keysSkipped});
}

class SettingsExportException implements Exception {
  final String message;
  const SettingsExportException(this.message);
  @override
  String toString() => 'SettingsExportException: $message';
}

/// Thrown when an import is attempted without an active Plex user, since the
/// user prefix needed to re-scope library preferences is unavailable.
class NoUserSignedInException extends SettingsExportException {
  const NoUserSignedInException() : super('No user is signed in');
}

/// Thrown when the chosen file isn't a valid Plezy settings export.
class InvalidExportFileException extends SettingsExportException {
  const InvalidExportFileException(super.message);
}

/// Serializes / restores user-facing SharedPreferences to a JSON file.
///
/// Strategy is allow-by-default: every key is exported unless it matches an
/// exact denylist or a prefix denylist of auth/cache/internal keys. User-scoped
/// keys (prefixed with `user_{uuid}_`) have that prefix stripped on export and
/// re-applied with the current user's prefix on import, so preferences follow
/// whichever account is signed in on the target device.
class SettingsExportService {
  static const int formatVersion = 1;
  static const String fileExtension = 'json';

  // Type markers written into the export JSON. One per SharedPreferences setter.
  static const String _typeBool = 'bool';
  static const String _typeInt = 'int';
  static const String _typeDouble = 'double';
  static const String _typeString = 'string';
  static const String _typeStringList = 'stringList';

  /// Exact keys never included in the export. Matches the auth/account state
  /// tracked by [StorageService] plus multi-server and view-state keys.
  static const Set<String> _denyKeys = {
    // Credentials (from StorageService._credentialKeys)
    'server_url',
    'token',
    'plex_token',
    'server_data',
    'client_identifier',
    'user_profile',
    'current_user_uuid',
    'home_users_cache',
    'home_users_cache_expiry',
    'active_app_profile_id',
    // Multi-server routing
    'servers_list',
    'server_order',
    // CredentialVault encryption key for DB-stored connection tokens
    'credential_vault_key_v1',
    // View state, not settings
    'selected_library_index',
    'selected_library_key',
    // Internal migration flags
    'buffer_size_migrated_to_auto',
  };

  /// Prefix denylist. A key is excluded if it starts with any of these.
  /// The tracker prefixes (`trakt_`, `mal_`, `anilist_`, `simkl_`) cover
  /// OAuth session tokens and runtime sync queues. The `enable_*` feature
  /// toggles use a different prefix and stay exportable. Profile runtime
  /// caches are also excluded because they belong to local connection state.
  static const List<String> _denyPrefixes = [
    'server_endpoint_',
    'episode_count_',
    'watched_threshold_',
    'trakt_',
    'mal_',
    'anilist_',
    'simkl_',
    'plex_home_users_',
    'profile_last_used_',
  ];

  /// Literal prefix used by [StorageService._userPrefix] for any scoped key.
  static const String _userPrefixRoot = 'user_';

  static bool _isExportable(String strippedKey) {
    if (_denyKeys.contains(strippedKey)) return false;
    for (final prefix in _denyPrefixes) {
      if (strippedKey.startsWith(prefix)) return false;
    }
    return true;
  }

  /// Builds the export map from the given prefs. Pure and testable.
  ///
  /// [currentUserUuid] — if set, keys prefixed with `user_{uuid}_` have that
  /// prefix stripped so they can be re-scoped on import. Keys belonging to any
  /// OTHER user are skipped (we only export the active user's prefs).
  static Map<String, dynamic> buildExportMap(
    SharedPreferencesWithCache prefs, {
    String? currentUserUuid,
    String appVersion = '',
  }) {
    final prefsOut = <String, Map<String, dynamic>>{};
    final currentUserPrefix = (currentUserUuid != null && currentUserUuid.isNotEmpty)
        ? '$_userPrefixRoot${currentUserUuid}_'
        : null;

    for (final fullKey in prefs.keys) {
      String baseKey;
      if (currentUserPrefix != null && fullKey.startsWith(currentUserPrefix)) {
        baseKey = fullKey.substring(currentUserPrefix.length);
      } else if (fullKey.startsWith(_userPrefixRoot)) {
        // Scoped to some other user — skip so we only export the active user.
        continue;
      } else {
        baseKey = fullKey;
      }

      if (!_isExportable(baseKey)) continue;

      final value = prefs.get(fullKey);
      final entry = _encodeValue(value);
      if (entry == null) continue;
      prefsOut[baseKey] = entry;
    }

    return {
      'formatVersion': formatVersion,
      'appVersion': appVersion,
      'exportedAt': DateTime.now().toUtc().toIso8601String(),
      'platform': Platform.operatingSystem,
      'prefs': prefsOut,
    };
  }

  static Map<String, dynamic>? _encodeValue(Object? value) {
    if (value is bool) return {'type': _typeBool, 'value': value};
    if (value is int) return {'type': _typeInt, 'value': value};
    if (value is double) return {'type': _typeDouble, 'value': value};
    if (value is String) return {'type': _typeString, 'value': value};
    if (value is List) {
      // SharedPreferences only supports List<String>.
      return {'type': _typeStringList, 'value': value.map((e) => e.toString()).toList()};
    }
    return null;
  }

  /// Applies a parsed export map to [prefs]. Pure and testable.
  ///
  /// Each key in the import overwrites whatever value currently exists at the
  /// same (possibly re-scoped) key. Keys not present in the import are left
  /// alone — this is a per-key replacement, not a global wipe.
  ///
  /// Throws [SettingsExportException] for structural problems.
  static Future<ImportResult> applyImportMap(
    Map<String, dynamic> data,
    SharedPreferencesWithCache prefs, {
    required String currentUserUuid,
  }) async {
    final version = data['formatVersion'];
    if (version is! int) {
      throw const InvalidExportFileException('Missing formatVersion');
    }
    if (version > formatVersion) {
      throw InvalidExportFileException('Unsupported formatVersion: $version');
    }

    final rawPrefs = data['prefs'];
    if (rawPrefs is! Map) {
      throw const InvalidExportFileException('Missing prefs object');
    }

    final userPrefix = 'user_${currentUserUuid}_';
    int imported = 0;
    int skipped = 0;

    for (final entry in rawPrefs.entries) {
      final baseKey = entry.key.toString();
      if (!_isExportable(baseKey)) {
        skipped++;
        continue;
      }

      final rawEntry = entry.value;
      if (rawEntry is! Map) {
        skipped++;
        continue;
      }

      final type = rawEntry['type'];
      final value = rawEntry['value'];
      if (type is! String) {
        skipped++;
        continue;
      }

      final targetKey = _isUserScopedBaseKey(baseKey) ? '$userPrefix$baseKey' : baseKey;

      final ok = await _writeTyped(prefs, targetKey, type, value);
      if (ok) {
        imported++;
      } else {
        skipped++;
        appLogger.w('Skipped import of $targetKey (type=$type)');
      }
    }

    return ImportResult(keysImported: imported, keysSkipped: skipped);
  }

  /// Base keys that [StorageService] persists under the user prefix. These need
  /// to be re-scoped to the current user on import.
  static bool _isUserScopedBaseKey(String baseKey) {
    const exact = {'hidden_libraries', 'library_filters', 'library_order'};
    if (exact.contains(baseKey)) return true;
    const prefixes = ['library_filters_', 'library_sort_', 'library_grouping_', 'library_tab_'];
    return prefixes.any(baseKey.startsWith);
  }

  static Future<bool> _writeTyped(SharedPreferencesWithCache prefs, String key, String type, Object? value) async {
    try {
      switch (type) {
        case _typeBool:
          if (value is! bool) return false;
          await prefs.setBool(key, value);
          return true;
        case _typeInt:
          if (value is! int) return false;
          await prefs.setInt(key, value);
          return true;
        case _typeDouble:
          if (value is num) {
            await prefs.setDouble(key, value.toDouble());
            return true;
          }
          return false;
        case _typeString:
          if (value is! String) return false;
          await prefs.setString(key, value);
          return true;
        case _typeStringList:
          if (value is! List) return false;
          await prefs.setStringList(key, value.map((e) => e.toString()).toList());
          return true;
      }
    } catch (e, st) {
      appLogger.e('Failed to import key $key', error: e, stackTrace: st);
    }
    return false;
  }

  static Future<String> _defaultFileName() async {
    final now = DateTime.now();
    final y = padNumber(now.year, 4);
    final m = padNumber(now.month, 2);
    final d = padNumber(now.day, 2);
    return 'plezy-settings-$y$m$d.$fileExtension';
  }

  /// Serializes the current user's settings and writes them to a location of
  /// the user's choosing. Returns the saved path, or `null` if the user
  /// cancelled the picker.
  ///
  /// Throws [SettingsExportException] on failure.
  static Future<String?> exportToFile() async {
    final prefs = (await SettingsService.getInstance()).prefs;
    final storage = await StorageService.getInstance();
    String appVersion = '';
    try {
      final info = await PackageInfo.fromPlatform();
      appVersion = info.version;
    } catch (_) {
      // best-effort; tolerate platforms without PackageInfo
    }

    final exportMap = buildExportMap(prefs, currentUserUuid: storage.activeUserScope(), appVersion: appVersion);
    final jsonString = const JsonEncoder.withIndent('  ').convert(exportMap);
    final bytes = Uint8List.fromList(utf8.encode(jsonString));
    final fileName = await _defaultFileName();

    // Android TV has no document picker — write to the app docs dir and let
    // the caller surface the path.
    if (Platform.isAndroid && TvDetectionService.isTVSync()) {
      return _writeToAppDocuments(fileName, bytes);
    }

    try {
      final savedPath = await FilePickerService.instance.saveFile(
        dialogTitle: 'Export Plezy settings',
        fileName: fileName,
        bytes: bytes,
        type: FileType.custom,
        allowedExtensions: const [fileExtension],
      );
      return savedPath;
    } catch (e, st) {
      appLogger.e('Settings export failed', error: e, stackTrace: st);
      throw const SettingsExportException('Could not write export file');
    }
  }

  static Future<String> _writeToAppDocuments(String fileName, Uint8List bytes) async {
    final dir = await getApplicationDocumentsDirectory();
    final file = File(p.join(dir.path, fileName));
    await file.writeAsBytes(bytes, flush: true);
    return file.path;
  }

  /// Prompts the user to pick a settings JSON and writes its contents into
  /// SharedPreferences. Requires a signed-in user.
  ///
  /// Returns `null` if the user cancelled. Throws [SettingsExportException] on
  /// malformed files or unsupported versions.
  static Future<ImportResult?> importFromFile() async {
    final storage = await StorageService.getInstance();
    final uuid = storage.activeUserScope();
    if (uuid == null || uuid.isEmpty) {
      throw const NoUserSignedInException();
    }

    final picked = await FilePickerService.instance.pickFiles(
      type: FileType.custom,
      allowedExtensions: const [fileExtension],
      withData: true,
    );
    if (picked == null || picked.files.isEmpty) return null;

    final file = picked.files.first;
    String contents;
    try {
      final bytes = file.bytes;
      if (bytes != null) {
        contents = utf8.decode(bytes);
      } else if (file.path != null) {
        contents = await File(file.path!).readAsString();
      } else {
        throw const InvalidExportFileException('Could not read the selected file');
      }
    } catch (e, st) {
      appLogger.e('Settings import read failed', error: e, stackTrace: st);
      throw const InvalidExportFileException('Could not read the selected file');
    }

    Map<String, dynamic> data;
    try {
      final decoded = json.decode(contents);
      if (decoded is! Map<String, dynamic>) {
        throw const InvalidExportFileException('Invalid export file');
      }
      data = decoded;
    } catch (_) {
      throw const InvalidExportFileException('Invalid export file');
    }

    final prefs = (await SettingsService.getInstance()).prefs;
    return applyImportMap(data, prefs, currentUserUuid: uuid);
  }
}
