import 'dart:async';
import 'dart:convert';

import 'package:localstore/localstore.dart';

import 'api_response.dart';
import 'errors/parse_exception.dart';

abstract class RequestCache<T extends ApiResponse?> {
  static const int cacheNothing = 0;
  static const int cacheMinute = 60;
  static const int cacheHour = 60 * 60;
  static const int cacheDay = 60 * 60 * 24;

  static String collection = 'MarianumMobile';

  int maxCacheTime;
  void Function(T)? onUpdate;

  /// Called only when [start] finds a cached payload in localstore. Use this
  /// (instead of [onUpdate]) when callers need to distinguish stale-but-fast
  /// cache hits from authoritative network responses.
  void Function(T)? onCacheData;

  /// Called only when [start] receives a fresh payload from the network.
  void Function(T)? onNetworkData;

  void Function(Exception) onError;
  bool? renew;

  final Completer<void> _ready = Completer<void>();

  /// Resolves when [start] has finished, regardless of whether the network
  /// call succeeded, failed, or was skipped due to a fresh cache. Callers
  /// can await this to know when both the cache lookup and the network
  /// attempt have settled.
  Future<void> get ready => _ready.future;

  RequestCache(
    this.maxCacheTime,
    this.onUpdate, {
    this.onError = ignore,
    this.renew = false,
    this.onCacheData,
    this.onNetworkData,
  });

  static void ignore(Exception e) {}

  Future<void> start(String document) async {
    try {
      final tableData = await Localstore.instance
          .collection(collection)
          .doc(document)
          .get();
      if (tableData != null) {
        final cached = onLocalData(tableData['json'] as String);
        onUpdate?.call(cached);
        onCacheData?.call(cached);
      }

      final lastUpdate = (tableData?['lastupdate'] as num?) ?? 0;
      if (DateTime.now().millisecondsSinceEpoch - (maxCacheTime * 1000) <
          lastUpdate) {
        if (renew == null || !renew!) return;
      }

      try {
        final newValue = await onLoad();
        onUpdate?.call(newValue);
        onNetworkData?.call(newValue);
        unawaited(
          Localstore.instance.collection(collection).doc(document).set({
            'json': jsonEncode(newValue),
            'lastupdate': DateTime.now().millisecondsSinceEpoch,
          }),
        );
      } on Exception catch (e) {
        onError(e);
      }
    } finally {
      if (!_ready.isCompleted) _ready.complete();
    }
  }

  T onLocalData(String json);
  Future<T> onLoad();
}

/// Concrete [RequestCache] that takes the two overrides as constructor
/// callbacks instead of requiring a subclass per endpoint.
class SimpleCache<T extends ApiResponse?> extends RequestCache<T> {
  final Future<T> Function() _loader;
  final T Function(Map<String, dynamic> json) _fromJson;

  SimpleCache({
    required int cacheTime,
    required Future<T> Function() loader,
    required T Function(Map<String, dynamic> json) fromJson,
    void Function(T)? onUpdate,
    void Function(T)? onCacheData,
    void Function(T)? onNetworkData,
    void Function(Exception)? onError,
    bool? renew,
  }) : _loader = loader,
       _fromJson = fromJson,
       super(
         cacheTime,
         onUpdate,
         onError: onError ?? RequestCache.ignore,
         renew: renew,
         onCacheData: onCacheData,
         onNetworkData: onNetworkData,
       );

  @override
  Future<T> onLoad() => _loader();

  @override
  T onLocalData(String json) =>
      _fromJson(jsonDecode(json) as Map<String, dynamic>);
}

/// Captures the latest cache payload (cached or network) and rethrows the
/// captured network error if no payload arrived. Collapses the
/// `latest`/`capturedError`/`await ready` boilerplate that DataProviders
/// otherwise repeat per endpoint.
Future<T> resolveFromCache<T extends ApiResponse?>(
  RequestCache<T> Function(
    void Function(T) onUpdate,
    void Function(Exception) onError,
  )
  build, {
  void Function(Object)? onError,
  String? operationName,
}) async {
  T? latest;
  Object? capturedError;
  final cache = build((data) => latest = data, (e) {
    capturedError = e;
    onError?.call(e);
  });
  await cache.ready;
  if (latest != null) return latest as T;
  final err = capturedError;
  if (err != null) throw err;
  throw ParseException(
    technicalDetails: operationName != null
        ? 'No data and no error from $operationName'
        : null,
  );
}
