import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shared_preferences/util/legacy_to_async_migration_util.dart';

/// Base class for services that use SharedPreferences singleton pattern.
///
/// This class handles the boilerplate for singleton initialization and
/// SharedPreferences lifecycle management. Subclasses should:
/// 1. Create a private named constructor (e.g., SettingsService._())
/// 2. Implement their own getInstance() method that calls BaseSharedPreferencesService.initializeInstance()
/// 3. Optionally override onInit() for post-initialization setup
abstract class BaseSharedPreferencesService {
  static final Map<Type, BaseSharedPreferencesService> _instances = {};
  // Single shared cache across all subclasses so writes from one service are
  // visible to reads from another without per-instance cache divergence.
  static Future<SharedPreferencesWithCache>? _cacheFuture;

  late SharedPreferencesWithCache _cache;

  BaseSharedPreferencesService();

  SharedPreferencesWithCache get prefs => _cache;

  /// Initialize the preferences instance.
  ///
  /// This method handles:
  /// - Singleton instance management
  /// - One-time migration from the legacy SharedPreferences API to the
  ///   SharedPreferencesAsync-backed cache (idempotent across launches)
  /// - Calling onInit() hook for subclass-specific setup
  static Future<T> initializeInstance<T extends BaseSharedPreferencesService>(T Function() constructor) async {
    if (_instances[T] == null) {
      final instance = constructor();
      _instances[T] = instance;
      instance._cache = await sharedCache();
      await instance.onInit();
    }
    return _instances[T] as T;
  }

  /// Shared preferences cache used app-wide. Runs the legacy → async
  /// migration on first call; subsequent calls return the same future.
  /// Use this from services that don't extend [BaseSharedPreferencesService].
  static Future<SharedPreferencesWithCache> sharedCache() {
    return _cacheFuture ??= () async {
      final legacy = await SharedPreferences.getInstance();
      await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary(
        legacySharedPreferencesInstance: legacy,
        sharedPreferencesAsyncOptions: const SharedPreferencesOptions(),
        migrationCompletedKey: 'plezy_legacy_prefs_migrated_v1',
      );
      return SharedPreferencesWithCache.create(cacheOptions: const SharedPreferencesWithCacheOptions());
    }();
  }

  /// Drop all cached singleton instances and the shared cache future so the
  /// next `getInstance()` call rebuilds against the current
  /// `SharedPreferences.setMockInitialValues(...)`. Test-only.
  @visibleForTesting
  static void resetForTesting() {
    _instances.clear();
    _cacheFuture = null;
  }

  /// Typed read helpers — return the stored value or [defaultValue] when missing.
  bool readBool(String key, {bool defaultValue = false}) => _cache.getBool(key) ?? defaultValue;
  int readInt(String key, {int defaultValue = 0}) => _cache.getInt(key) ?? defaultValue;
  double readDouble(String key, {double defaultValue = 0.0}) => _cache.getDouble(key) ?? defaultValue;
  String readString(String key, {String defaultValue = ''}) => _cache.getString(key) ?? defaultValue;
  List<String> readStringList(String key, {List<String> defaultValue = const []}) =>
      _cache.getStringList(key) ?? defaultValue;

  /// Typed write helpers — symmetric with the read helpers above; use these
  /// instead of `prefs.setX(...)` so call sites stay terse.
  Future<void> writeBool(String key, bool value) => _cache.setBool(key, value);
  Future<void> writeInt(String key, int value) => _cache.setInt(key, value);
  Future<void> writeDouble(String key, double value) => _cache.setDouble(key, value);
  Future<void> writeString(String key, String value) => _cache.setString(key, value);
  Future<void> writeStringList(String key, List<String> value) => _cache.setStringList(key, value);

  /// Decode a JSON string to a Map with error handling.
  ///
  /// If [legacyStringOk] is true and the value is a plain string (not valid
  /// JSON), returns `{'key': jsonString, 'descending': false}` for legacy
  /// library sort compatibility.
  Map<String, dynamic> decodeJsonStringToMap(String jsonString, {bool legacyStringOk = false}) {
    try {
      return json.decode(jsonString) as Map<String, dynamic>;
    } catch (e) {
      if (legacyStringOk) {
        return {'key': jsonString, 'descending': false};
      }
      return {};
    }
  }

  /// Read a value typed by [pref]; falls back to its `defaultValue`.
  T read<T>(Pref<T> pref) => pref.readFrom(this);

  /// Write a value typed by [pref]. Pushes the post-transform value into any
  /// listenable previously vended for this key so widgets rebuild automatically.
  Future<void> write<T>(Pref<T> pref, T value) async {
    await pref.writeTo(this, value);
    final n = _listenables[pref.key];
    if (n != null) (n as ValueNotifier<T>).value = read(pref);
  }

  /// Lazy per-key [ValueNotifier]. Use with [ValueListenableBuilder] to rebuild
  /// when the value changes. Notifiers live for the app lifetime — do not
  /// dispose them.
  final Map<String, ValueNotifier<dynamic>> _listenables = {};
  final Map<String, Pref<dynamic>> _listenablePrefs = {};

  ValueNotifier<T> listenable<T>(Pref<T> pref) => pref.bindListenable(this);

  /// Type-erased [Listenable] accessor for combining multiple prefs into a
  /// `Listenable.merge`. Dispatches through [Pref.bindListenable] so the
  /// underlying notifier is created with the pref's concrete type.
  Listenable listenableOf(Pref<Object?> pref) => pref.bindListenable(this);

  /// Push current stored values into every active listenable. Used after bulk
  /// operations that bypass [write] (reset/import/direct SharedPreferences writes).
  void refreshActiveListenables() {
    for (final pref in _listenablePrefs.values.toList(growable: false)) {
      pref.refreshListenable(this);
    }
  }

  /// Hook for subclass-specific initialization after SharedPreferences is ready.
  ///
  /// Override this method to perform any setup that requires access to
  /// SharedPreferences (e.g., registering values with other services).
  Future<void> onInit() async {}
}

/// Typed preference declaration. Pair with [BaseSharedPreferencesService.read]
/// and [BaseSharedPreferencesService.write] to remove per-setting `?? default`
/// boilerplate.
abstract class Pref<T> {
  final String key;
  const Pref(this.key);

  /// Implementation hook — call [BaseSharedPreferencesService.read] instead.
  T readFrom(BaseSharedPreferencesService svc);

  /// Implementation hook — call [BaseSharedPreferencesService.write] instead.
  Future<void> writeTo(BaseSharedPreferencesService svc, T value);

  /// Get-or-create the [ValueNotifier] for this pref. Virtual-dispatched via
  /// the runtime [Pref] subclass so the notifier carries the concrete `T`,
  /// even when called through a `Pref<Object?>` reference (used by
  /// [BaseSharedPreferencesService.listenableOf]).
  ValueNotifier<T> bindListenable(BaseSharedPreferencesService svc) {
    final existing = svc._listenables[key];
    svc._listenablePrefs[key] = this;
    if (existing != null) return existing as ValueNotifier<T>;
    final notifier = ValueNotifier<T>(readFrom(svc));
    svc._listenables[key] = notifier;
    return notifier;
  }

  /// If a listenable exists for this key, push the current stored value into
  /// it. Used after bulk operations (reset, import) that bypass [writeTo].
  /// No-op when no listener has been registered.
  void refreshListenable(BaseSharedPreferencesService svc) {
    final n = svc._listenables[key];
    if (n != null) (n as ValueNotifier<T>).value = readFrom(svc);
  }
}

class BoolPref extends Pref<bool> {
  final bool defaultValue;
  final void Function(bool)? onWrite;
  const BoolPref(super.key, {this.defaultValue = false, this.onWrite});
  @override
  bool readFrom(BaseSharedPreferencesService svc) => svc.readBool(key, defaultValue: defaultValue);
  @override
  Future<void> writeTo(BaseSharedPreferencesService svc, bool value) async {
    await svc.writeBool(key, value);
    onWrite?.call(value);
  }
}

class IntPref extends Pref<int> {
  final int defaultValue;
  final int Function(int)? transform;
  const IntPref(super.key, {this.defaultValue = 0, this.transform});
  @override
  int readFrom(BaseSharedPreferencesService svc) {
    final raw = svc.readInt(key, defaultValue: defaultValue);
    return transform == null ? raw : transform!(raw);
  }

  @override
  Future<void> writeTo(BaseSharedPreferencesService svc, int value) =>
      svc.writeInt(key, transform == null ? value : transform!(value));
}

class DoublePref extends Pref<double> {
  final double defaultValue;
  final double Function(double)? transform;
  const DoublePref(super.key, {this.defaultValue = 0.0, this.transform});
  @override
  double readFrom(BaseSharedPreferencesService svc) {
    final raw = svc.readDouble(key, defaultValue: defaultValue);
    return transform == null ? raw : transform!(raw);
  }

  @override
  Future<void> writeTo(BaseSharedPreferencesService svc, double value) =>
      svc.writeDouble(key, transform == null ? value : transform!(value));
}

class StringPref extends Pref<String> {
  final String defaultValue;
  const StringPref(super.key, {this.defaultValue = ''});
  @override
  String readFrom(BaseSharedPreferencesService svc) => svc.readString(key, defaultValue: defaultValue);
  @override
  Future<void> writeTo(BaseSharedPreferencesService svc, String value) => svc.writeString(key, value);
}

/// Like [StringPref] but null = key absent. [transform] runs on write; if it
/// returns null the key is removed.
class NullableStringPref extends Pref<String?> {
  final String? Function(String?)? transform;
  const NullableStringPref(super.key, {this.transform});
  @override
  String? readFrom(BaseSharedPreferencesService svc) => svc.prefs.getString(key);
  @override
  Future<void> writeTo(BaseSharedPreferencesService svc, String? value) async {
    final normalized = transform == null ? value : transform!(value);
    if (normalized == null) {
      await svc.prefs.remove(key);
    } else {
      await svc.writeString(key, normalized);
    }
  }
}

class StringListPref extends Pref<List<String>> {
  final List<String> defaultValue;
  const StringListPref(super.key, {this.defaultValue = const []});
  @override
  List<String> readFrom(BaseSharedPreferencesService svc) => svc.readStringList(key, defaultValue: defaultValue);
  @override
  Future<void> writeTo(BaseSharedPreferencesService svc, List<String> value) => svc.writeStringList(key, value);
}

/// Stores an enum by its [Enum.name]; falls back to [defaultValue] when the
/// stored string doesn't match any value in [values].
class EnumPref<T extends Enum> extends Pref<T> {
  final List<T> values;
  final T defaultValue;
  const EnumPref(super.key, {required this.values, required this.defaultValue});
  @override
  T readFrom(BaseSharedPreferencesService svc) {
    final stored = svc.prefs.getString(key);
    if (stored == null) return defaultValue;
    return values.firstWhere((v) => v.name == stored, orElse: () => defaultValue);
  }

  @override
  Future<void> writeTo(BaseSharedPreferencesService svc, T value) => svc.writeString(key, value.name);
}

/// Stores an arbitrary value as a JSON-encoded string. Decode failures and
/// missing keys both fall back to [defaultValue].
class JsonPref<T> extends Pref<T> {
  final T defaultValue;
  final String Function(T) encode;
  final T Function(dynamic) decode;
  JsonPref(super.key, {required this.defaultValue, required this.encode, required this.decode});

  @override
  T readFrom(BaseSharedPreferencesService svc) {
    final s = svc.prefs.getString(key);
    if (s == null) return defaultValue;
    try {
      return decode(json.decode(s));
    } catch (_) {
      return defaultValue;
    }
  }

  @override
  Future<void> writeTo(BaseSharedPreferencesService svc, T value) => svc.writeString(key, encode(value));
}
