import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../../core/api/api_client.dart';
import '../data/boloes_repository.dart';
import '../data/rascunho_store.dart';
import 'corrigir_placar_dialog.dart';
import 'palpites_do_jogo_page.dart';
import 'prazo.dart';
import '../../../core/tema/tema.dart';
import '../../../core/firebase/provedores.dart';

/// Os provedores mudaram de casa em `MG-7` (`core/firebase/provedores.dart`),
/// onde a escolha entre HTTP e Firebase é feita. Reexportados aqui para a
/// apresentação e os testes continuarem importando do mesmo lugar de sempre —
/// a troca de backend não tem por que aparecer na tela.
export '../../../core/firebase/provedores.dart' show boloesRepositoryProvider;

final rascunhoStoreProvider = Provider<RascunhoStore>((_) => RascunhoStore());

/// Tela de palpite da rodada.
///
/// Prioridade do casual: palpitar a rodada inteira em poucos toques, com o
/// prazo visível e confirmação de que salvou. A contagem vem do horário do
/// servidor, nunca do relógio do device.
class RodadaPage extends ConsumerStatefulWidget {
  const RodadaPage({
    required this.poolId,
    required this.matchday,
    this.encerrado = false,
    this.souAdmin = false,
    this.arquivado = false,
    this.relogioLocal,
    super.key,
  });

  final String poolId;
  final int matchday;

  /// Bolão encerrado: nenhum palpite mais entra, e o motivo fica visível.
  final bool encerrado;

  /// Habilita a correção manual de placar (PR-10), que é ação de admin.
  final bool souAdmin;

  /// Arquivado: mesma trava do encerrado, com texto próprio.
  final bool arquivado;

  /// Relógio local injetável, só para teste. A transição automática de "aberto"
  /// para "fechado" quando a contagem zera é comportamento de tempo — sem esta
  /// costura, a afirmação fica sem teste, porque `pump(Duration)` move os
  /// timers falsos mas não o `DateTime.now()` real.
  final DateTime Function()? relogioLocal;

  @override
  ConsumerState<RodadaPage> createState() => _RodadaPageState();
}

class _RodadaPageState extends ConsumerState<RodadaPage> {
  Rodada? _rodada;
  RelogioDoServidor? _relogio;
  String? _erro;
  bool _carregando = true;
  bool _enviando = false;
  Timer? _tique;

  /// Rascunho local do que foi digitado. Sobrevive a erro de envio **e ao
  /// fechamento do app**: perder palpite digitado é motivo declarado de sair.
  final Map<String, ({int? casa, int? fora})> _rascunho = {};
  final Map<String, String> _recusas = {};

  /// Jogos digitados que o servidor ainda não confirmou. É o que a etiqueta
  /// "não enviado" mostra e o que a fila retenta.
  final Set<String> _pendentes = {};

  /// Última tentativa falhou por falta de rede. Muda o texto: "erro" e "sem
  /// conexão" pedem ações diferentes do participante.
  bool _semConexao = false;

  @override
  void initState() {
    super.initState();
    _restaurarRascunho();
    _carregar();
    // Faz a contagem andar sem recarregar da rede.
    _tique = Timer.periodic(const Duration(seconds: 30), (_) {
      if (mounted) setState(() {});
    });
  }

  @override
  void dispose() {
    _tique?.cancel();
    super.dispose();
  }

  RascunhoStore get _store => ref.read(rascunhoStoreProvider);

  /// Recupera o que foi digitado antes de o app fechar. Roda antes da rede: o
  /// participante precisa ver o que digitou mesmo offline.
  Future<void> _restaurarRascunho() async {
    final salvos = await _store.ler(widget.poolId, widget.matchday);
    if (!mounted || salvos.isEmpty) return;
    setState(() {
      for (final p in salvos) {
        _rascunho[p.matchId] = (casa: p.casa, fora: p.fora);
        if (!p.enviado) _pendentes.add(p.matchId);
      }
    });
    // Havia palpite pendente de rodada anterior: tenta enviar ao abrir, que é
    // o momento em que a rede voltou.
    if (_pendentes.isNotEmpty) await _enviar(automatico: true);
  }

  Future<void> _persistirRascunho() async {
    final lista = <PalpiteRascunho>[];
    for (final e in _rascunho.entries) {
      final casa = e.value.casa;
      final fora = e.value.fora;
      if (casa == null || fora == null) continue;
      lista.add(
        PalpiteRascunho(
          matchId: e.key,
          casa: casa,
          fora: fora,
          enviado: !_pendentes.contains(e.key),
        ),
      );
    }
    await _store.salvar(widget.poolId, widget.matchday, lista);
  }

  Future<void> _carregar() async {
    setState(() {
      _carregando = true;
      _erro = null;
    });
    try {
      final r = await ref
          .read(boloesRepositoryProvider)
          .rodada(widget.poolId, widget.matchday);
      if (!mounted) return;
      setState(() {
        _rodada = r;
        _relogio = RelogioDoServidor(
          serverTime: r.serverTime,
          relogioLocal: widget.relogioLocal,
        );
        for (final j in r.jogos) {
          if (j.temPalpite && !_rascunho.containsKey(j.matchId)) {
            _rascunho[j.matchId] = (casa: j.palpiteCasa, fora: j.palpiteFora);
          }
        }
      });
    } on ApiError catch (e) {
      if (mounted) setState(() => _erro = e.mensagem);
    } finally {
      if (mounted) setState(() => _carregando = false);
    }
  }

  /// Envia o que está pendente.
  ///
  /// `automatico` é a fila de PP-4 rodando ao abrir a tela: nesse caso o
  /// silêncio é proposital — o participante não pediu nada agora, e falhar sem
  /// rede é o esperado, não um erro para exibir.
  Future<void> _enviar({bool automatico = false}) async {
    final rodada = _rodada;
    final relogio = _relogio;
    if (widget.encerrado) return;

    final aEnviar = <({String matchId, int casa, int fora})>[];
    for (final e in _rascunho.entries) {
      final casa = e.value.casa;
      final fora = e.value.fora;
      if (casa == null || fora == null) continue;
      // Só o que está pendente, quando é a fila: reenviar tudo a cada abertura
      // desperdiçaria rede e poluiria o histórico de revisões.
      if (automatico && !_pendentes.contains(e.key)) continue;

      // Não monta envio para jogo fechado: o servidor recusaria, e tentar
      // seria fingir que ainda dá. Sem rodada carregada (offline no cold
      // start) manda tudo e deixa o servidor decidir — ele é a fonte do prazo.
      if (rodada != null && relogio != null) {
        final jogo = rodada.jogos.where((j) => j.matchId == e.key).firstOrNull;
        if (jogo == null || !relogio.aberto(jogo.kickoffAt)) continue;
      }
      aEnviar.add((matchId: e.key, casa: casa, fora: fora));
    }

    if (aEnviar.isEmpty) {
      if (!automatico) {
        _avisar(
          'Nada para enviar: preencha o placar de algum jogo com prazo aberto.',
        );
      }
      return;
    }

    setState(() {
      _enviando = true;
      if (!automatico) _recusas.clear();
    });
    try {
      final r = await ref
          .read(boloesRepositoryProvider)
          .palpitar(widget.poolId, aEnviar);
      if (!mounted) return;

      setState(() {
        _recusas.addAll(r.motivos);
        _semConexao = false;
        // Recusado por prazo: o que vale é o que está no servidor, não o que
        // acabou de ser digitado. Deixar o valor recusado na tela faria o
        // participante acreditar num palpite que não conta.
        for (final id in r.motivos.keys) {
          if (r.motivos[id] == 'PRAZO_FECHADO') _rascunho.remove(id);
        }
        // Recusado por prazo continua fora da fila: retentar não vai mudar o
        // resultado, e a etiqueta de pendente sugeriria que ainda vai salvar.
        for (final item in aEnviar) {
          _pendentes.remove(item.matchId);
        }
      });
      await _persistirRascunho();

      // Confirmação visível sem precisar procurar.
      _avisar(
        r.recusados == 0
            ? '${r.salvos} palpite(s) salvo(s)'
            : '${r.salvos} salvo(s), ${r.recusados} recusado(s) — veja os avisos',
        sucesso: r.recusados == 0,
      );
      await _carregar();
    } on ApiError catch (e) {
      // O rascunho permanece e volta para a fila: erro de rede não pode custar
      // o que foi digitado nem exigir digitar de novo.
      if (!mounted) return;
      setState(() {
        _semConexao = e.semConexao;
        for (final item in aEnviar) {
          _pendentes.add(item.matchId);
        }
      });
      await _persistirRascunho();
      if (!automatico) _avisar(e.mensagem);
    } finally {
      if (mounted) setState(() => _enviando = false);
    }
  }

  /// Palpites de todos naquele jogo. Aberto a qualquer participante: o que
  /// protege o sigilo é o servidor, não esconder o caminho.
  void _verPalpites(JogoDaRodada jogo) {
    Navigator.of(context).push(
      MaterialPageRoute<void>(
        builder: (_) => PalpitesDoJogoPage(
          poolId: widget.poolId,
          matchId: jogo.matchId,
          timeCasa: jogo.homeTeam,
          timeFora: jogo.awayTeam,
          kickoffAt: jogo.kickoffAt,
        ),
      ),
    );
  }

  /// Abre a correção manual daquele jogo. Só faz sentido em jogo que já
  /// terminou — o servidor recusa antes disso, e oferecer seria mentir.
  Future<void> _corrigirPlacar(JogoDaRodada jogo) async {
    final mudou = await showDialog<bool>(
      context: context,
      builder: (_) => CorrigirPlacarDialog(
        poolId: widget.poolId,
        matchId: jogo.matchId,
        timeCasa: jogo.homeTeam,
        timeFora: jogo.awayTeam,
        oficialCasa: jogo.oficialCasa,
        oficialFora: jogo.oficialFora,
      ),
    );
    if (mudou == true) await _carregar();
  }

  void _avisar(String texto, {bool sucesso = false}) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(texto),
        backgroundColor: sucesso ? CoresDoApp.sucesso : null,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    // Sem Scaffold próprio: esta tela vive dentro das abas de BolaoPage.
    return Scaffold(
      body: RefreshIndicator(onRefresh: _carregar, child: _corpo()),
      // Sem ação de envio em bolão encerrado: botão que só sabe falhar.
      floatingActionButton: _rodada == null || widget.encerrado
          ? null
          : FloatingActionButton.extended(
              onPressed: _enviando ? null : _enviar,
              icon: _enviando
                  ? const SizedBox(
                      height: 18,
                      width: 18,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Icon(Icons.send),
              label: Text(_enviando ? 'Enviando…' : 'Enviar palpites'),
            ),
    );
  }

  Widget _corpo() {
    if (_carregando && _rodada == null) {
      return const Center(child: CircularProgressIndicator());
    }
    if (_erro != null && _rodada == null) {
      return _Vazio(
        icone: Icons.cloud_off,
        titulo: 'Não foi possível carregar',
        detalhe: _erro!,
        acao: FilledButton(
          onPressed: _carregar,
          child: const Text('Tentar de novo'),
        ),
      );
    }
    final rodada = _rodada!;
    if (rodada.jogos.isEmpty) {
      return const _Vazio(
        icone: Icons.event_busy,
        titulo: 'Nenhum jogo nesta rodada',
        detalhe: 'Escolha outra rodada do bolão.',
      );
    }

    return Column(
      children: [
        // Pendente + sem rede é o estado que o casual mais teme: ele digitou e
        // não sabe se salvou. Aqui ele fica sabendo, e tem o que fazer.
        if (_pendentes.isNotEmpty)
          Container(
            width: double.infinity,
            color: Theme.of(context).colorScheme.errorContainer,
            padding: const EdgeInsets.fromLTRB(16, 8, 8, 8),
            child: Row(
              children: [
                Expanded(
                  child: Text(
                    _semConexao
                        ? '${_pendentes.length} palpite(s) digitado(s) e ainda '
                              'não enviado(s) — sem conexão. Estão guardados no '
                              'aparelho e vão sozinhos quando a rede voltar.'
                        : '${_pendentes.length} palpite(s) ainda não enviado(s).',
                    style: const TextStyle(fontSize: 12),
                  ),
                ),
                TextButton(
                  onPressed: _enviando ? null : _enviar,
                  child: const Text('Enviar'),
                ),
              ],
            ),
          ),
        if (widget.encerrado)
          Container(
            width: double.infinity,
            color: Theme.of(context).colorScheme.secondaryContainer,
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
            child: Text(
              widget.arquivado
                  ? 'Bolão arquivado: só leitura. A classificação final e o '
                        'extrato continuam aqui, e o admin pode desarquivar no '
                        'histórico.'
                  : 'Bolão encerrado: os palpites estão fechados. A '
                        'classificação final está na aba Ranking.',
              style: const TextStyle(fontSize: 12),
            ),
          ),
        Expanded(child: _listaDeJogos(rodada)),
      ],
    );
  }

  Widget _listaDeJogos(Rodada rodada) {
    return ListView.separated(
      padding: const EdgeInsets.only(bottom: 96),
      itemCount: rodada.jogos.length,
      separatorBuilder: (_, _) => const Divider(height: 1),
      itemBuilder: (_, i) => _CartaoJogo(
        jogo: rodada.jogos[i],
        relogio: _relogio!,
        // Encerrado fecha todos os campos, mesmo os de prazo aberto.
        bloqueado: widget.encerrado,
        motivoBloqueio: widget.arquivado ? 'arquivado' : 'bolão encerrado',
        rascunho: _rascunho[rodada.jogos[i].matchId],
        recusa: _recusas[rodada.jogos[i].matchId],
        onMudou: (casa, fora) {
          setState(() {
            final id = rodada.jogos[i].matchId;
            _rascunho[id] = (casa: casa, fora: fora);
            // Digitado é pendente até o servidor confirmar.
            if (casa != null && fora != null) _pendentes.add(id);
            _recusas.remove(id);
          });
          // Sem await: digitar não pode esperar disco.
          _persistirRascunho();
        },
        pendente: _pendentes.contains(rodada.jogos[i].matchId),
        onCorrigir: widget.souAdmin
            ? () => _corrigirPlacar(rodada.jogos[i])
            : null,
        onVerPalpites: () => _verPalpites(rodada.jogos[i]),
      ),
    );
  }
}

class _CartaoJogo extends StatelessWidget {
  const _CartaoJogo({
    required this.jogo,
    required this.relogio,
    required this.bloqueado,
    required this.motivoBloqueio,
    required this.rascunho,
    required this.recusa,
    required this.pendente,
    required this.onMudou,
    required this.onCorrigir,
    required this.onVerPalpites,
  });

  final JogoDaRodada jogo;
  final RelogioDoServidor relogio;
  final bool bloqueado;

  /// Texto da etiqueta quando o cartão está bloqueado pelo estado do bolão.
  /// "Encerrado" e "arquivado" são estados diferentes, e chamar um pelo nome do
  /// outro faz o participante procurar um encerramento que não houve.
  final String motivoBloqueio;
  final ({int? casa, int? fora})? rascunho;
  final String? recusa;

  /// Digitado e ainda não confirmado pelo servidor.
  final bool pendente;

  /// Nulo para quem não é admin: a correção é ação dele.
  final VoidCallback? onCorrigir;

  /// Abre os palpites de todos naquele jogo.
  final VoidCallback onVerPalpites;
  final void Function(int? casa, int? fora) onMudou;

  @override
  Widget build(BuildContext context) {
    // O prazo vem do servidor; a contagem é derivada do desvio.
    final aberto = !bloqueado && relogio.aberto(jogo.kickoffAt);
    final restante = relogio.restanteAte(jogo.kickoffAt);
    final cores = Theme.of(context).colorScheme;

    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      '${jogo.homeTeam} x ${jogo.awayTeam}',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    // Horário no fuso do aparelho, com o fuso escrito: só a
                    // contagem não diz **quando**, e UTC faria o participante
                    // achar que tem horas a mais.
                    Text(
                      formatarHorarioLocal(jogo.kickoffAt),
                      style: TextStyle(fontSize: 11, color: cores.outline),
                    ),
                  ],
                ),
              ),
              // "Não enviado" precisa aparecer por jogo: saber que digitou não
              // é saber que salvou, e é essa confusão que custa a rodada.
              if (pendente) ...[
                _Etiqueta(texto: 'não enviado', cor: cores.error),
                const SizedBox(width: 6),
              ],
              // Motivo explícito do bloqueio, nunca só um campo cinza.
              _Etiqueta(
                texto: bloqueado
                    ? motivoBloqueio
                    : aberto
                    ? formatarRestante(restante)
                    : 'prazo fechado',
                cor: aberto ? cores.primary : cores.outline,
              ),
            ],
          ),
          const SizedBox(height: 8),
          Row(
            children: [
              _CampoPlacar(
                valor: rascunho?.casa,
                habilitado: aberto,
                rotulo: jogo.homeTeam,
                onMudou: (v) => onMudou(v, rascunho?.fora),
              ),
              const Padding(
                padding: EdgeInsets.symmetric(horizontal: 12),
                child: Text('x'),
              ),
              _CampoPlacar(
                valor: rascunho?.fora,
                habilitado: aberto,
                rotulo: jogo.awayTeam,
                onMudou: (v) => onMudou(rascunho?.casa, v),
              ),
              const Spacer(),
              if (jogo.oficialCasa != null)
                Text(
                  'oficial ${jogo.oficialCasa}x${jogo.oficialFora}',
                  style: Theme.of(context).textTheme.bodySmall,
                )
              // Jogo passou do horário e o placar não chegou: o tier gratuito
              // atrasa, e isso não pode parecer zero nem tela travada.
              else if (!aberto)
                Text(
                  'aguardando resultado',
                  style: Theme.of(context).textTheme.bodySmall,
                ),
              IconButton(
                tooltip: 'Palpites de todos',
                visualDensity: VisualDensity.compact,
                icon: const Icon(Icons.groups_outlined, size: 20),
                onPressed: onVerPalpites,
              ),
              // Corrigir só depois do apito: antes disso o servidor recusa.
              if (onCorrigir != null && !relogio.aberto(jogo.kickoffAt))
                IconButton(
                  tooltip: 'Corrigir placar',
                  visualDensity: VisualDensity.compact,
                  icon: const Icon(Icons.edit_note, size: 20),
                  onPressed: onCorrigir,
                ),
            ],
          ),
          if (recusa != null) ...[
            const SizedBox(height: 6),
            Text(
              _mensagemDaRecusa(recusa!),
              style: TextStyle(color: cores.error, fontSize: 12),
            ),
          ],
        ],
      ),
    );
  }

  /// Cada motivo tem texto próprio: "erro" genérico faz o participante desistir.
  String _mensagemDaRecusa(String motivo) => switch (motivo) {
    'PRAZO_FECHADO' =>
      'o prazo deste jogo fechou antes do envio — vale o palpite que '
          'estava salvo',
    'JOGO_FORA_DO_BOLAO' => 'este jogo não pertence ao bolão',
    _ => 'não foi possível salvar este palpite',
  };
}

class _CampoPlacar extends StatelessWidget {
  const _CampoPlacar({
    required this.valor,
    required this.habilitado,
    required this.rotulo,
    required this.onMudou,
  });

  final int? valor;
  final bool habilitado;
  final String rotulo;
  final ValueChanged<int?> onMudou;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 64,
      child: Semantics(
        label: 'Gols de $rotulo',
        child: TextFormField(
          initialValue: valor?.toString() ?? '',
          enabled: habilitado,
          textAlign: TextAlign.center,
          // Teclado numérico: o casual digita com pressa.
          keyboardType: TextInputType.number,
          inputFormatters: [
            FilteringTextInputFormatter.digitsOnly,
            LengthLimitingTextInputFormatter(2),
          ],
          decoration: const InputDecoration(isDense: true),
          onChanged: (t) {
            final n = int.tryParse(t);
            // Mesmo limite do servidor: divergir mostraria erro sem explicação.
            onMudou(n != null && n >= 0 && n <= 20 ? n : null);
          },
        ),
      ),
    );
  }
}

class _Etiqueta extends StatelessWidget {
  const _Etiqueta({required this.texto, required this.cor});
  final String texto;
  final Color cor;

  @override
  Widget build(BuildContext context) => Container(
    padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
    decoration: BoxDecoration(
      border: Border.all(color: cor),
      borderRadius: BorderRadius.circular(12),
    ),
    child: Text(texto, style: TextStyle(color: cor, fontSize: 11)),
  );
}

class _Vazio extends StatelessWidget {
  const _Vazio({
    required this.icone,
    required this.titulo,
    required this.detalhe,
    this.acao,
  });

  final IconData icone;
  final String titulo;
  final String detalhe;
  final Widget? acao;

  @override
  Widget build(BuildContext context) => Center(
    child: Padding(
      padding: const EdgeInsets.all(32),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(icone, size: 48),
          const SizedBox(height: 12),
          Text(titulo, style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 6),
          Text(detalhe, textAlign: TextAlign.center),
          if (acao != null) ...[const SizedBox(height: 16), acao!],
        ],
      ),
    ),
  );
}
