import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:injectable/injectable.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tap_that/core/app_logger.dart';

/// An abstract class that provides a standardized interface for interacting
/// with `SharedPreferences`. It handles basic key-value storage as well
/// as more complex object caching with JSON serialization.
///
/// This abstraction is designed to be used with the `injectable` package
/// for easy dependency injection.
@lazySingleton
abstract class DataStorageSharedPref {
  /// Factory constructor for creating a `DataStorageSharedPref` instance.
  @factoryMethod
  const factory DataStorageSharedPref({required SharedPreferences storage}) =
      _DataStorageImpl;

  /// Writes a value to storage based on its type.
  ///
  /// Supports `int`, `bool`, `double`, `String`, and `List<String>`.
  /// Returns `true` on success and `false` on failure.
  Future<bool> write({required String key, required dynamic data});

  /// Reads a value from storage, casting it to the specified generic type [T].
  ///
  /// Returns the value if found and successfully cast, otherwise returns `null`.
  T? read<T>({required String key});

  /// Removes a value from storage by its [key].
  Future<bool> remove({required String key});

  /// Removes all data from storage.
  Future<bool> removeAll();

  /// Checks if a [key] exists in storage.
  Future<bool> containsKey({required String key});

  /// -- API Response Caching --

  /// Serializes and saves a response object to storage.
  ///
  /// The [key] can be a primitive type or an object with a `toJson()` method.
  /// The [value] can also be a primitive or an object with `toJson()`.
  /// The [path] is used as a prefix to create a unique storage key.
  Future<bool> saveResponse<K, V>({
    required K key,
    required V? value,
    String? path,
  });

  /// Retrieves and deserializes a response object from storage.
  ///
  /// The [key] is used to find the stored data. The [fromJson] function is
  /// required to convert the stored JSON map back into an object of type [V].
  Future<V> getResponse<K, V>({
    required K key,
    required V Function(Map<String, dynamic>) fromJson,
    String? path,
  });
}

class _DataStorageImpl implements DataStorageSharedPref {
  const _DataStorageImpl({required this.storage});
  final SharedPreferences storage;

  @override
  Future<bool> write({required String key, required dynamic data}) async {
    try {
      if (data is int) return await storage.setInt(key, data);
      if (data is bool) return await storage.setBool(key, data);
      if (data is double) return await storage.setDouble(key, data);
      if (data is String) return await storage.setString(key, data);
      if (data is List<String>) return await storage.setStringList(key, data);
    } on Exception catch (e, stackTrace) {
      AppLogger.logException(e,
          stackTrace: stackTrace,
          message: 'Failed to write to SharedPreferences for key: $key');
    }
    return false;
  }

  @override
  T? read<T>({required String key}) {
    try {
      final value = storage.get(key);
      if (value is T) {
        return value;
      }
    } on Exception catch (e, stackTrace) {
      AppLogger.logException(e,
          stackTrace: stackTrace,
          message: 'Failed to read from SharedPreferences for key: $key');
    }
    return null;
  }

  @override
  Future<bool> remove({required String key}) async {
    try {
      return await storage.remove(key);
    } on Exception catch (e, stackTrace) {
      AppLogger.logException(e,
          stackTrace: stackTrace,
          message: 'Failed to remove from SharedPreferences for key: $key');
      return false;
    }
  }

  @override
  Future<bool> removeAll() async {
    try {
      return await storage.clear();
    } on Exception catch (e, stackTrace) {
      AppLogger.logException(e,
          stackTrace: stackTrace, message: 'Failed to clear SharedPreferences');
      return false;
    }
  }

  @override
  Future<bool> containsKey({required String key}) async {
    try {
      return storage.containsKey(key);
    } on Exception catch (e, stackTrace) {
      AppLogger.logException(e,
          stackTrace: stackTrace,
          message: 'Failed containsKey check for key: $key');
      return false;
    }
  }

  @override
  Future<bool> saveResponse<K, V>({
    required K key,
    required V? value,
    String? path,
  }) async {
    try {
      final String storageKey = _encodeCacheKey(key, path);
      String encodedValue;

      // Encode the value to a JSON string.
      if (value is String || value is num || value is bool) {
        encodedValue = jsonEncode(value.toString());
      } else {
        // This will throw if the object doesn't have a toJson method.
        final jsonValueMap = (value as dynamic).toJson();
        encodedValue = jsonEncode(jsonValueMap);
      }

      if (kDebugMode) {
        _logWrite(storageKey, key, value, encodedValue);
      }

      return write(key: storageKey, data: encodedValue);
    } on Exception catch (e, stackTrace) {
      AppLogger.logException(
        e,
        stackTrace: stackTrace,
        message: 'Failed to save response. Ensure object has toJson() method.',
      );
      return false;
    }
  }

  @override
  Future<V> getResponse<K, V>({
    required K key,
    required V Function(Map<String, dynamic>) fromJson,
    String? path,
  }) {
    final String storageKey = _encodeCacheKey(key, path);
    final String? encodedValue = read<String>(key: storageKey);

    if (encodedValue == null) {
      debugPrint('| CACHE MISS: $storageKey');
      return Future.error(Exception('Cache miss: No data found for key.'));
    }

    try {
      final decodedJson = jsonDecode(encodedValue) as Map<String, dynamic>;
      final result = fromJson(decodedJson);
      if (kDebugMode) {
        _logRead(storageKey, key, result, encodedValue);
      }
      return Future.value(result);
    } on Exception catch (e, stackTrace) {
      AppLogger.logException(
        e,
        stackTrace: stackTrace,
        message: 'Failed to decode cached response for key: $storageKey',
      );
      // It's often better to not re-throw here unless the caller needs to handle it.
      // Depending on the use case, returning an error or a default value might be better.
      return Future.error(Exception('Error decoding cached response: $e'));
    }
  }

  /// Encodes a generic key [K] and an optional [path] into a unique string
  /// suitable for use as a storage key.
  String _encodeCacheKey<K>(K key, String? path) {
    String encodedKey;
    if (key is String || key is num || key is bool) {
      encodedKey = key.toString();
    } else {
      // ignore: avoid_dynamic_calls
      final jsonKeyMap = (key as dynamic).toJson();
      encodedKey = jsonEncode(jsonKeyMap);
    }
    return '${path ?? 'cache'}/$encodedKey';
  }

  // --- Logging Helpers ---

  void _logWrite<K, V>(String finalKey, K key, V value, String encodedValue) {
    if (!kDebugMode) return;
    final log = StringBuffer()
      ..writeln('|------------------ WRITE CACHE -------------------|')
      ..writeln('| PATH: $finalKey')
      ..writeln('| KEY TYPE: ${key.runtimeType}')
      ..writeln('| VALUE TYPE: ${value.runtimeType}')
      ..writeln('| SIZE: ${utf8.encode(encodedValue).length} bytes')
      ..writeln('|__________________________________________________|');
    debugPrint(log.toString());
  }

  void _logRead<K, V>(String finalKey, K key, V value, String encodedValue) {
    if (!kDebugMode) return;
    final log = StringBuffer()
      ..writeln('|------------------- READ CACHE -------------------|')
      ..writeln('| PATH: $finalKey')
      ..writeln('| KEY TYPE: ${key.runtimeType}')
      ..writeln('| VALUE TYPE: ${value.runtimeType}')
      ..writeln('| SIZE: ${utf8.encode(encodedValue).length} bytes')
      ..writeln('|__________________________________________________|');
    debugPrint(log.toString());
  }
}

// NOTE: To complete this setup with `injectable`, you would typically create
// a module to tell the dependency injector how to create third-party
// instances like `SharedPreferences`.
/*
  import 'package:injectable/injectable.dart';
  import 'package:shared_preferences/shared_preferences.dart';

  @module
  abstract class RegisterModule {
    @preResolve // Ensures SharedPreferences is initialized before the app starts
    Future<SharedPreferences> get prefs => SharedPreferences.getInstance();
  }
*/
optimize this