import 'dart:convert';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';

final cacheLocalProvider = Provider<CacheLocal>((_) => CacheLocal());

/// Guardado em cache, com o momento em que foi gravado.
class EmCache<T> {
  const EmCache({required this.valor, required this.gravadoEm});

  final T valor;
  final DateTime gravadoEm;

  /// Idade do dado. A tela decide o que fazer com ela — este cache **não**
  /// expira nada por conta própria: apagar o único dado disponível offline
  /// porque ele é velho deixaria o participante sem nada, o que é pior que
  /// mostrar um dado antigo **rotulado como antigo**.
  Duration get idade => DateTime.now().difference(gravadoEm);
}

/// Cache de leitura em disco, para o app continuar informando algo sem rede.
///
/// Regras que valem para todo uso:
/// - só guarda dado que o **próprio participante** pode ver — nunca palpite de
///   terceiro em jogo não terminado, que é o vazamento de `PP-7` por outro
///   caminho;
/// - nada expira sozinho: quem lê recebe a idade e decide;
/// - JSON inválido é descartado, não lançado — cache corrompido não pode
///   derrubar tela.
class CacheLocal {
  CacheLocal([SharedPreferences? prefs]) : _injetado = prefs;

  final SharedPreferences? _injetado;
  SharedPreferences? _cache;

  Future<SharedPreferences> get _prefs async =>
      _injetado ?? (_cache ??= await SharedPreferences.getInstance());

  String _chave(String nome) => 'bolao.cache.$nome';

  Future<void> gravar(String nome, Object valor) async {
    final envelope = {
      'gravadoEm': DateTime.now().toIso8601String(),
      'valor': valor,
    };
    await (await _prefs).setString(_chave(nome), jsonEncode(envelope));
  }

  Future<EmCache<Object?>?> ler(String nome) async {
    final bruto = (await _prefs).getString(_chave(nome));
    if (bruto == null) return null;
    try {
      final envelope = jsonDecode(bruto) as Map<String, dynamic>;
      return EmCache(
        valor: envelope['valor'],
        gravadoEm: DateTime.parse(envelope['gravadoEm'] as String),
      );
    } on FormatException {
      await remover(nome);
      return null;
    } on TypeError {
      await remover(nome);
      return null;
    }
  }

  Future<void> remover(String nome) async =>
      (await _prefs).remove(_chave(nome));
}

/// Texto da idade do dado em cache, na voz do participante.
///
/// Dizer "de 3 h atrás" é o que separa "dado velho" de "dado errado": sem isso,
/// o participante lê um ranking desatualizado como se fosse o de agora e briga
/// com quem tem o número certo.
String descreverIdade(Duration d) {
  if (d.inMinutes < 2) return 'agora mesmo';
  if (d.inMinutes < 60) return 'de ${d.inMinutes} min atrás';
  if (d.inHours < 24) return 'de ${d.inHours} h atrás';
  return 'de ${d.inDays} dia(s) atrás';
}
