import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';

import '../../../core/erros/erro_do_app.dart';
import '../../../core/firebase/chamadas.dart';
import 'participantes_repository.dart';

/// Participantes, papéis, regras e avisos sobre Firestore.
///
/// A lista e as regras são leitura direta — `membros` e o próprio documento do
/// bolão, que todo participante já lê. Papel e saída são callables porque
/// nenhuma delas é decidível olhando só o documento que está sendo escrito: "o
/// bolão não pode ficar sem admin" exige **contar** os admins, e rule não conta
/// coleção.
class ParticipantesRepositoryFirebase implements ParticipantesRepository {
  ParticipantesRepositoryFirebase({
    FirebaseFirestore? db,
    FirebaseAuth? auth,
    Callables? callables,
  }) : _db = db ?? FirebaseFirestore.instance,
       _auth = auth ?? FirebaseAuth.instance,
       _callables = callables ?? Callables();

  final FirebaseFirestore _db;
  final FirebaseAuth _auth;
  final Callables _callables;

  String get _uid {
    final uid = _auth.currentUser?.uid;
    if (uid == null) throw SessaoExpirada();
    return uid;
  }

  @override
  Future<ListaParticipantes> listar(String poolId) async {
    final bolao = await _db.doc('boloes/$poolId').get();
    if (!bolao.exists) throw ApiError('bolão não encontrado', status: 404);
    final criador = bolao.data()!['createdBy'] as String;

    final docs = await _db.collection('boloes/$poolId/membros').get();
    final linhas = docs.docs.map((d) {
      final m = d.data();
      final saiu = m['leftAt'] as Timestamp?;
      return {
        'userId': d.id,
        'displayName': m['displayName'],
        'role': m['role'],
        'isCreator': d.id == criador,
        'joinedAt': (m['joinedAt'] as Timestamp).toDate().toIso8601String(),
        'leftAt': saiu?.toDate().toIso8601String(),
        'active': saiu == null,
      };
    }).toList();

    // Admin primeiro, depois por entrada — a mesma ordem do backend antigo. Quem
    // saiu **continua na lista, marcado**: sumir com a pessoa apagaria o
    // histórico dela do bolão aos olhos de quem ficou.
    linhas.sort((a, b) {
      final papel = (a['role'] as String).compareTo(b['role'] as String);
      if (papel != 0) return papel;
      return (a['joinedAt'] as String).compareTo(b['joinedAt'] as String);
    });

    return ListaParticipantes.doJson({'total': linhas.length, 'rows': linhas});
  }

  @override
  Future<StatusDaRodada> statusDaRodada(String poolId, int matchday) async {
    // Callable: é a única visão em que o admin recebe dado sobre palpite alheio,
    // e ela devolve **só o estado**. Fazer isso no cliente exigiria ler os
    // palpites dos outros, que é exatamente o que `PP-7` proíbe.
    final r = await _callables.chamar<Map<String, dynamic>>('statusDaRodada', {
      'poolId': poolId,
      'matchday': matchday,
    });
    return StatusDaRodada.doJson(mapaDaCallable(r));
  }

  @override
  Future<void> promover(String poolId, String userId) => _callables
      .chamar<dynamic>('promoverAdmin', {'poolId': poolId, 'userId': userId});

  @override
  Future<void> rebaixar(String poolId, String userId) => _callables
      .chamar<dynamic>('rebaixarAdmin', {'poolId': poolId, 'userId': userId});

  @override
  Future<void> remover(String poolId, String userId) =>
      _callables.chamar<dynamic>('removerParticipante', {
        'poolId': poolId,
        'userId': userId,
      });

  @override
  Future<void> sair(String poolId) =>
      _callables.chamar<dynamic>('sairDoBolao', {'poolId': poolId});

  @override
  Future<Regras> regras(String poolId) async {
    final bolao = await _db.doc('boloes/$poolId').get();
    if (!bolao.exists) throw ApiError('bolão não encontrado', status: 404);
    final b = bolao.data()!;

    // Próximo prazo: o primeiro jogo do bolão que ainda não começou. Sai dos
    // jogos, não do relógio do aparelho — quem compara é o servidor.
    final jogos = await _db
        .collection('jogos')
        .where('competitionCode', isEqualTo: b['competitionCode'])
        .where('matchday', isGreaterThanOrEqualTo: b['matchdayFrom'])
        .where('matchday', isLessThanOrEqualTo: b['matchdayTo'])
        .get();

    final agora = DateTime.now();
    final futuros =
        jogos.docs
            .map((d) => (d.data()['kickoffAt'] as Timestamp).toDate())
            .where((k) => k.isAfter(agora))
            .toList()
          ..sort();

    return Regras(
      pontosExato: b['pointsExact'] as int,
      pontosVencedorSaldo: b['pointsWinnerDiff'] as int,
      pontosVencedor: b['pointsWinner'] as int,
      ordemDesempate: (b['tiebreakOrder'] as List<dynamic>).cast<String>(),
      travadas: b['rulesLockedAt'] != null,
      proximoPrazo: futuros.isEmpty ? null : futuros.first,
    );
  }

  @override
  Future<List<Aviso>> avisos(String poolId) async {
    final docs = await _db
        .collection('usuarios/$_uid/avisos')
        .where('poolId', isEqualTo: poolId)
        .orderBy('createdAt', descending: true)
        .limit(50)
        .get();

    return docs.docs.map((d) {
      final a = d.data();
      return Aviso(
        id: d.id,
        kind: a['kind'] as String,
        titulo: a['title'] as String,
        corpo: a['body'] as String,
        criadoEm: (a['createdAt'] as Timestamp).toDate(),
        lido: a['readAt'] != null,
      );
    }).toList();
  }

  @override
  Future<void> marcarLido(String poolId, String avisoId) async {
    // Escrita direta, e a rule só deixa mexer em `readAt`: o texto do aviso é a
    // prova de que o ponto mudou e por quê — quem pudesse reescrevê-lo apagaria
    // a própria explicação.
    await _db.doc('usuarios/$_uid/avisos/$avisoId').update({
      'readAt': DateTime.now(),
    });
  }
}
