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 '../../../core/firebase/relogio.dart';
import 'boloes_repository.dart';

/// Bolões sobre Firestore.
///
/// A divisão do trabalho é a mesma do servidor: **leitura é direta**, e escrita
/// que depende de mais de um documento ou de dado invisível ao cliente é
/// callable. O palpite é a exceção que importa — ele é escrita direta, porque é
/// o caminho quente e uma Function no meio somaria cold start ao momento em que
/// todo mundo palpita junto.
class BoloesRepositoryFirebase implements BoloesRepository {
  BoloesRepositoryFirebase(
    this._relogio, {
    FirebaseFirestore? db,
    FirebaseAuth? auth,
    Callables? callables,
  }) : _db = db ?? FirebaseFirestore.instance,
       _auth = auth ?? FirebaseAuth.instance,
       _callables = callables ?? Callables();

  final RelogioDoServidor _relogio;
  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<List<BolaoResumo>> meus() async {
    // **Uma consulta, sem leitura extra por bolão.** O resumo mora no índice que
    // as Functions mantêm; ler cada bolão aqui seria uma leitura por linha da
    // tela inicial, todo dia, por participante.
    final docs = await _db
        .collection('usuarios/$_uid/boloes')
        .orderBy('joinedAt', descending: true)
        .get();

    return docs.docs.map((d) {
      final j = d.data();
      return BolaoResumo(
        id: d.id,
        name: j['name'] as String,
        matchdayFrom: j['matchdayFrom'] as int,
        matchdayTo: j['matchdayTo'] as int,
        totalMembers: j['activeMembers'] as int? ?? 0,
        isAdmin: j['isAdmin'] as bool? ?? false,
        encerrado: j['closed'] as bool? ?? false,
        arquivado: j['archived'] as bool? ?? false,
      );
    }).toList();
  }

  @override
  Future<List<Competicao>> competicoes() async {
    final r = await _callables.chamar<List<dynamic>>('listarCompeticoes');
    return r
        .map((e) => Competicao.doJson(Map<String, dynamic>.from(e as Map)))
        .toList();
  }

  @override
  Future<Bolao> criar({
    required String nome,
    required String competitionCode,
    required int rodadaInicial,
    required int rodadaFinal,
    int? pontosExato,
    int? pontosVencedorSaldo,
    int? pontosVencedor,
    List<String>? ordemDesempate,
  }) async {
    final regras = <String, dynamic>{
      'pointsExact': ?pontosExato,
      'pointsWinnerDiff': ?pontosVencedorSaldo,
      'pointsWinner': ?pontosVencedor,
      'tiebreakOrder': ?ordemDesempate,
    };

    final r = await _callables.chamar<Map<String, dynamic>>('criarBolao', {
      'name': nome,
      'competitionCode': competitionCode,
      'matchdayFrom': rodadaInicial,
      'matchdayTo': rodadaFinal,
      if (regras.isNotEmpty) 'rules': regras,
    });
    return Bolao.doJson(Map<String, dynamic>.from(r));
  }

  @override
  Future<void> alterarRegras(
    String poolId, {
    required int pontosExato,
    required int pontosVencedorSaldo,
    required int pontosVencedor,
    required List<String> ordemDesempate,
  }) async {
    await _callables.chamar<dynamic>('alterarRegras', {
      'poolId': poolId,
      'rules': {
        'pointsExact': pontosExato,
        'pointsWinnerDiff': pontosVencedorSaldo,
        'pointsWinner': pontosVencedor,
        'tiebreakOrder': ordemDesempate,
      },
    });
  }

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

    final jogos = await _db
        .collection('jogos')
        .where('competitionCode', isEqualTo: bolao.data()!['competitionCode'])
        .where('matchday', isEqualTo: matchday)
        .orderBy('kickoffAt')
        .get();

    final meus = <String, Map<String, dynamic>>{};
    for (final lote in _emLotes(jogos.docs.map((d) => d.id).toList())) {
      final palpites = await _db
          .collection('boloes/$poolId/palpites')
          .where('userId', isEqualTo: _uid)
          .where('matchId', whereIn: lote)
          .get();
      for (final p in palpites.docs) {
        meus[p.data()['matchId'] as String] = p.data();
      }
    }

    // Hora do servidor, não do aparelho: adiantar o celular não pode abrir
    // prazo fechado. A rule recusa de todo jeito; isto é para a tela não mentir.
    await _relogio.sincronizar();
    final agora = _relogio.agora;

    return Rodada(
      serverTime: agora,
      jogos: jogos.docs.map((d) {
        final j = d.data();
        final meu = meus[d.id];
        final inicio = (j['kickoffAt'] as Timestamp).toDate();
        return JogoDaRodada(
          matchId: d.id,
          homeTeam: j['homeTeam'] as String,
          awayTeam: j['awayTeam'] as String,
          kickoffAt: inicio,
          deadlineOpen: inicio.isAfter(agora) && !_fechado(bolao.data()!),
          palpiteCasa: meu?['homeScore'] as int?,
          palpiteFora: meu?['awayScore'] as int?,
          oficialCasa: j['homeScore'] as int?,
          oficialFora: j['awayScore'] as int?,
        );
      }).toList(),
    );
  }

  bool _fechado(Map<String, dynamic> bolao) =>
      bolao['closedAt'] != null || bolao['archivedAt'] != null;

  @override
  Future<ResultadoEnvio> palpitar(
    String poolId,
    List<({String matchId, int casa, int fora})> palpites,
  ) async {
    final motivos = <String, String>{};
    var salvos = 0;

    for (final p in palpites) {
      final agora = DateTime.now();
      try {
        // Id determinístico: reenvio da fila offline **sobrescreve** em vez de
        // duplicar, e a rule sabe de quem é o palpite sem ler o documento.
        await _db.doc('boloes/$poolId/palpites/${p.matchId}_$_uid').set({
          'userId': _uid,
          'matchId': p.matchId,
          'homeScore': p.casa,
          'awayScore': p.fora,
          'createdAt': agora,
          'updatedAt': agora,
          'matchFinished': false,
          'points': null,
          'band': null,
          'scoredAt': null,
        });
        salvos++;
      } on FirebaseException catch (e) {
        if (e.code != 'permission-denied') rethrow;
        // **A rule não diz por quê** — e não deve, porque explicar a recusa é
        // dar mapa de como contorná-la. Quem sabe o motivo é o app, que acabou
        // de ler o jogo e o bolão.
        motivos[p.matchId] = await _motivoDaRecusa(poolId, p.matchId);
      }
    }

    return ResultadoEnvio(
      salvos: salvos,
      recusados: motivos.length,
      motivos: motivos,
    );
  }

  Future<String> _motivoDaRecusa(String poolId, String matchId) async {
    try {
      final jogo = await _db.doc('jogos/$matchId').get();
      final bolao = await _db.doc('boloes/$poolId').get();
      if (bolao.exists && bolao.data()!['archivedAt'] != null) {
        return 'este bolão foi arquivado e está somente para leitura';
      }
      if (bolao.exists && bolao.data()!['closedAt'] != null) {
        return 'este bolão já foi encerrado';
      }
      if (jogo.exists) {
        final inicio = (jogo.data()!['kickoffAt'] as Timestamp).toDate();
        if (!inicio.isAfter(_relogio.agora)) {
          return 'o prazo deste jogo fechou no apito inicial';
        }
      }
      return 'não foi possível salvar este palpite';
    } on FirebaseException {
      return 'não foi possível salvar este palpite';
    }
  }

  @override
  Future<({String poolId, String poolName, bool jaParticipava})>
  entrarPorConvite(String token) async {
    final r = await _callables.chamar<Map<String, dynamic>>(
      'entrarPorConvite',
      {'token': token},
    );
    return (
      poolId: r['poolId'] as String,
      poolName: r['poolName'] as String,
      jaParticipava: r['jaParticipava'] as bool,
    );
  }

  @override
  Future<({String token, DateTime expiraEm})> gerarConvite(
    String poolId,
  ) async {
    final r = await _callables.chamar<Map<String, dynamic>>('gerarConvite', {
      'poolId': poolId,
    });
    return (
      token: r['token'] as String,
      expiraEm: DateTime.parse(r['expiresAt'] as String),
    );
  }

  @override
  Future<PalpitesDoJogo> palpitesDoJogo(String poolId, String matchId) async {
    final jogo = await _db.doc('jogos/$matchId').get();
    final terminou =
        jogo.exists &&
        jogo.data()!['status'] == 'FINISHED' &&
        jogo.data()!['homeScore'] != null;

    // Enquanto o jogo não termina, **nem se pergunta**: a rule recusa a consulta
    // inteira, e recusa é o certo — mas perguntar por algo que se sabe proibido
    // vira erro na tela em vez de "ainda é segredo" (`PP-7`).
    if (!terminou) {
      return const PalpitesDoJogo(revelado: false, palpites: []);
    }

    final docs = await _db
        .collection('boloes/$poolId/palpites')
        .where('matchId', isEqualTo: matchId)
        // O filtro **prova o acesso**: rule não filtra resultado de consulta,
        // então é a consulta que precisa provar que só pede o que é público.
        .where('matchFinished', isEqualTo: true)
        .get();

    // O nome vem de `membros`, onde ele está copiado. O palpite não carrega
    // nome de propósito: dado repetido em toda escrita é dado que envelhece em
    // toda escrita.
    final membros = await _db.collection('boloes/$poolId/membros').get();
    final nomes = {
      for (final m in membros.docs) m.id: m.data()['displayName'] as String,
    };

    return PalpitesDoJogo(
      revelado: true,
      palpites: docs.docs.map((d) {
        final p = d.data();
        return PalpiteDeTerceiro(
          userId: p['userId'] as String,
          displayName: nomes[p['userId']] ?? 'participante',
          casa: p['homeScore'] as int,
          fora: p['awayScore'] as int,
          pontos: p['points'] as int?,
        );
      }).toList(),
    );
  }

  @override
  Future<({int afetados, int avisos})> corrigirPlacar(
    String poolId,
    String matchId,
    int casa,
    int fora,
  ) async {
    final r = await _callables.chamar<Map<String, dynamic>>('corrigirPlacar', {
      'poolId': poolId,
      'matchId': matchId,
      'homeScore': casa,
      'awayScore': fora,
    });
    // **Avisos não voltam mais da chamada**: quem apura é gatilho, e esperar por
    // ele seria segurar o admin numa tela por causa de um número. O que dá para
    // afirmar é quantos palpites serão reapurados.
    return (afetados: r['afetados'] as int? ?? 0, avisos: 0);
  }

  @override
  Future<void> invalidarConvite(String poolId) async {
    await _callables.chamar<dynamic>('invalidarConvite', {'poolId': poolId});
  }
}

/// O `whereIn` do Firestore aceita 30 valores por consulta.
List<List<String>> _emLotes(List<String> itens) {
  final lotes = <List<String>>[];
  for (var i = 0; i < itens.length; i += 30) {
    lotes.add(itens.sublist(i, i + 30 > itens.length ? itens.length : i + 30));
  }
  return lotes;
}
