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

import '../../../core/api/api_client.dart';
import '../../../core/cache/cache_local.dart';
import '../../auth/presentation/sessao_controller.dart';
import '../data/ranking_repository.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 rankingRepositoryProvider;

/// Ranking geral do bolão.
///
/// Destaca a posição de quem está olhando: o participante não deve precisar
/// rolar a lista para se achar.
class RankingPage extends ConsumerStatefulWidget {
  const RankingPage({
    required this.poolId,
    required this.matchday,
    this.rodadaFinal,
    super.key,
  });

  final String poolId;

  /// Rodada inicial do bolão, usada como padrão da aba "rodada". Separar geral
  /// de rodada é pedido explícito: quem foi bem na rodada e mal no campeonato
  /// precisa ver as duas coisas.
  final int matchday;

  /// Última rodada do bolão. Presente = o participante escolhe qual rodada ver.
  final int? rodadaFinal;

  @override
  ConsumerState<RankingPage> createState() => _RankingPageState();
}

class _RankingPageState extends ConsumerState<RankingPage> {
  Ranking? _ranking;
  RankingRodada? _daRodada;
  String? _erro;
  bool _carregando = true;
  bool _mostrandoRodada = false;

  /// Rodada em exibição. Começa na primeira do bolão.
  late int _rodada = widget.matchday;

  /// Idade do dado quando veio do cache. Nulo = veio da rede agora.
  Duration? _idadeDoCache;

  /// Página seguinte sendo buscada. Bolão de empresa passa de 50 pessoas, e sem
  /// paginação quem está da metade para baixo simplesmente não aparece.
  bool _buscandoMais = false;
  static const _tamanhoDaPagina = 50;

  @override
  void initState() {
    super.initState();
    _carregar();
  }

  Future<void> _carregar() async {
    setState(() {
      _carregando = true;
      _erro = null;
    });
    try {
      final repo = ref.read(rankingRepositoryProvider);
      if (_mostrandoRodada) {
        final r = await repo.daRodada(widget.poolId, _rodada);
        if (mounted) setState(() => _daRodada = r);
      } else {
        final r = await repo.geral(widget.poolId, limit: _tamanhoDaPagina);
        if (!mounted) return;
        setState(() {
          _ranking = r;
          _idadeDoCache = null;
        });
        // Só o geral vai para o cache: é ele que vira o "ranking final" do bolão
        // encerrado ou arquivado, que é o que a pessoa quer rever meses depois
        // (CV-6 FE-4). Ranking de rodada específica não tem esse uso.
        await ref
            .read(cacheLocalProvider)
            .gravar('ranking.${widget.poolId}', r.paraJson());
      }
    } on ApiError catch (e) {
      if (!mounted) return;
      final guardado = _mostrandoRodada
          ? null
          : await ref.read(cacheLocalProvider).ler('ranking.${widget.poolId}');
      if (!mounted) return;
      if (guardado != null && guardado.valor is Map) {
        setState(() {
          _ranking = Ranking.doJson(
            (guardado.valor as Map).cast<String, dynamic>(),
          );
          _idadeDoCache = guardado.idade;
          _erro = null;
        });
      } else {
        setState(() => _erro = e.mensagem);
      }
    } finally {
      if (mounted) setState(() => _carregando = false);
    }
  }

  /// Busca a página seguinte do ranking geral e **acrescenta** ao que já existe.
  Future<void> _buscarMais() async {
    final atual = _ranking;
    if (atual == null || _buscandoMais) return;
    if (atual.linhas.length >= atual.total) return;

    setState(() => _buscandoMais = true);
    try {
      final proxima = await ref
          .read(rankingRepositoryProvider)
          .geral(
            widget.poolId,
            limit: _tamanhoDaPagina,
            offset: atual.linhas.length,
          );
      if (!mounted) return;
      setState(() {
        _ranking = Ranking(
          total: proxima.total,
          linhas: [...atual.linhas, ...proxima.linhas],
        );
      });
    } on ApiError catch (e) {
      // Falha ao paginar não pode apagar o que já está na tela: mostra o motivo
      // e deixa tentar de novo rolando outra vez.
      if (mounted) {
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(SnackBar(content: Text(e.mensagem)));
      }
    } finally {
      if (mounted) setState(() => _buscandoMais = false);
    }
  }

  void _trocar(bool rodada) {
    if (rodada == _mostrandoRodada) return;
    setState(() => _mostrandoRodada = rodada);
    _carregar();
  }

  @override
  Widget build(BuildContext context) {
    final sessao = ref.watch(sessaoProvider);
    final meuId = sessao is Autenticado ? sessao.conta.id : null;

    final atual = _mostrandoRodada ? _daRodada : _ranking;

    // `Material` transparente: a tela vive dentro do Scaffold de BolaoPage, mas
    // depender de ancestral é armadilha — o seletor de rodada quebra em quem
    // reusar a tela solta.
    return Material(
      type: MaterialType.transparency,
      child: Column(
        children: [
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            child: SegmentedButton<bool>(
              segments: [
                const ButtonSegment(value: false, label: Text('Geral')),
                const ButtonSegment(value: true, label: Text('Rodada')),
              ],
              selected: {_mostrandoRodada},
              onSelectionChanged: (s) => _trocar(s.first),
            ),
          ),
          // Seletor de rodada: o endpoint aceita qualquer uma, e quem quer ver a
          // rodada 7 no meio do campeonato não deveria depender de qual rodada o
          // bolão começou.
          if (_mostrandoRodada && widget.rodadaFinal != null)
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: Row(
                children: [
                  const Text('Rodada'),
                  const SizedBox(width: 12),
                  DropdownButton<int>(
                    value: _rodada,
                    items: [
                      for (
                        var r = widget.matchday;
                        r <= widget.rodadaFinal!;
                        r++
                      )
                        DropdownMenuItem(value: r, child: Text('$r')),
                    ],
                    onChanged: (v) {
                      if (v == null || v == _rodada) return;
                      setState(() => _rodada = v);
                      _carregar();
                    },
                  ),
                ],
              ),
            ),
          if (_idadeDoCache != null)
            Container(
              width: double.infinity,
              color: Theme.of(context).colorScheme.secondaryContainer,
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              child: Text(
                'Sem conexão — classificação guardada no aparelho, '
                '${descreverIdade(_idadeDoCache!)}.',
                style: const TextStyle(fontSize: 12),
              ),
            ),
          // Aviso de apuração parcial: ranking incompleto parecendo final produz
          // comemoração e depois briga.
          if (_mostrandoRodada && _daRodada != null && _daRodada!.parcial)
            Container(
              width: double.infinity,
              color: Theme.of(context).colorScheme.secondaryContainer,
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              child: Text(
                'Rodada parcial: ${_daRodada!.apurados} de '
                '${_daRodada!.totalJogos} jogos apurados.',
                style: const TextStyle(fontSize: 12),
              ),
            ),
          Expanded(child: _lista(atual, meuId)),
        ],
      ),
    );
  }

  Widget _lista(Object? atual, String? meuId) {
    if (_carregando && atual == null) {
      return const Center(child: CircularProgressIndicator());
    }
    if (_erro != null && atual == null) {
      return _ErroComRetry(mensagem: _erro!, onRetry: _carregar);
    }

    final linhas = atual is RankingRodada
        ? atual.linhas
        : (atual as Ranking?)?.linhas ?? const <LinhaRanking>[];
    if (linhas.isEmpty) {
      return _Mensagem(
        icone: Icons.emoji_events_outlined,
        titulo: _mostrandoRodada
            ? 'Rodada $_rodada sem classificação'
            : 'Ranking ainda vazio',
        detalhe: _mostrandoRodada
            ? 'Nenhum jogo desta rodada foi apurado ainda.'
            : 'Assim que a primeira rodada for apurada, a classificação aparece aqui.',
      );
    }

    final ninguemPontuou = linhas.every((l) => l.points == 0);

    final faltamLinhas =
        !_mostrandoRodada && atual is Ranking && linhas.length < atual.total;

    return RefreshIndicator(
      onRefresh: _carregar,
      child: NotificationListener<ScrollNotification>(
        onNotification: (aviso) {
          // Busca a próxima página um pouco antes do fim, para a rolagem não
          // parar em branco esperando rede.
          if (faltamLinhas &&
              aviso.metrics.pixels >= aviso.metrics.maxScrollExtent - 400) {
            _buscarMais();
          }
          return false;
        },
        child: ListView.separated(
          itemCount:
              linhas.length + (ninguemPontuou ? 1 : 0) + (faltamLinhas ? 1 : 0),
          separatorBuilder: (context, index) => const Divider(height: 1),
          itemBuilder: (context, i) {
            if (ninguemPontuou && i == 0) {
              return Padding(
                padding: const EdgeInsets.all(16),
                child: Text(
                  _mostrandoRodada
                      ? 'Nenhum jogo desta rodada apurado — todos com zero.'
                      : 'Nenhuma rodada apurada ainda — todos com zero.',
                  style: const TextStyle(fontStyle: FontStyle.italic),
                ),
              );
            }
            final indice = i - (ninguemPontuou ? 1 : 0);
            if (indice >= linhas.length) {
              // Rodapé de carregamento: também é o que diz ao participante que a
              // lista continua, em vez de parecer que acabou em 50.
              return const Padding(
                padding: EdgeInsets.all(16),
                child: Center(child: CircularProgressIndicator()),
              );
            }
            final linha = linhas[indice];
            return _LinhaRanking(linha: linha, souEu: linha.userId == meuId);
          },
        ),
      ),
    );
  }
}

class _LinhaRanking extends StatelessWidget {
  const _LinhaRanking({required this.linha, required this.souEu});

  final LinhaRanking linha;
  final bool souEu;

  @override
  Widget build(BuildContext context) {
    final cores = Theme.of(context).colorScheme;
    final criterio = descreverCriterio(linha.decidedBy);

    return Container(
      color: souEu ? cores.primaryContainer.withValues(alpha: 0.4) : null,
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
      child: Row(
        children: [
          ConstrainedBox(
            // Largura mínima para os nomes alinharem, mas a coluna **cresce**
            // quando precisa: com largura fixa, "10º" + ícone de posição
            // dividida estourava, e o texto ainda cresce com a escala de fonte
            // do sistema — qualquer constante quebraria em algum device.
            constraints: const BoxConstraints(minWidth: 44),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(
                  '${linha.position}º',
                  style: Theme.of(context).textTheme.titleMedium?.copyWith(
                    fontWeight: souEu ? FontWeight.bold : null,
                  ),
                ),
                // Posição compartilhada é sinalizada: não se pode sugerir que
                // um venceu o outro.
                if (linha.sharedPosition)
                  Padding(
                    padding: const EdgeInsets.only(left: 2),
                    child: Icon(
                      Icons.people_outline,
                      size: 14,
                      color: cores.outline,
                    ),
                  ),
              ],
            ),
          ),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  linha.displayName + (souEu ? ' (você)' : ''),
                  style: TextStyle(fontWeight: souEu ? FontWeight.bold : null),
                ),
                // Aparece dos dois lados do empate, inclusive no 1º: quem está
                // na frente por desempate é justamente quem é questionado.
                // Filtrar por posição deixava o líder sem a justificativa.
                if (criterio.isNotEmpty && linha.decidedBy != 'PONTOS')
                  Text(
                    criterio,
                    style: Theme.of(
                      context,
                    ).textTheme.bodySmall?.copyWith(color: cores.outline),
                  ),
              ],
            ),
          ),
          Column(
            crossAxisAlignment: CrossAxisAlignment.end,
            children: [
              Text(
                '${linha.points} pts',
                style: Theme.of(context).textTheme.titleMedium,
              ),
              Text(
                '${linha.exactScores} exato(s)',
                style: Theme.of(context).textTheme.bodySmall,
              ),
            ],
          ),
        ],
      ),
    );
  }
}

/// Extrato do participante na rodada, com a conta por jogo.
class ExtratoPage extends ConsumerStatefulWidget {
  const ExtratoPage({required this.poolId, required this.matchday, super.key});

  final String poolId;
  final int matchday;

  @override
  ConsumerState<ExtratoPage> createState() => _ExtratoPageState();
}

class _ExtratoPageState extends ConsumerState<ExtratoPage> {
  Extrato? _extrato;
  String? _erro;
  bool _carregando = true;

  @override
  void initState() {
    super.initState();
    _carregar();
  }

  Future<void> _carregar() async {
    setState(() {
      _carregando = true;
      _erro = null;
    });
    try {
      final e = await ref
          .read(rankingRepositoryProvider)
          .extrato(widget.poolId, widget.matchday);
      if (mounted) setState(() => _extrato = e);
    } on ApiError catch (e) {
      if (mounted) setState(() => _erro = e.mensagem);
    } on Object {
      // Falha que não é do backend (configuração ausente, erro de plataforma)
      // ainda **precisa virar tela**. Sem este ramo, a exceção sumia daqui e a
      // tela tentava desenhar um extrato nulo — o participante via um crash em
      // vez de um "não deu, tente de novo".
      if (mounted) {
        setState(() => _erro = 'não foi possível carregar o extrato');
      }
    } finally {
      if (mounted) setState(() => _carregando = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_carregando && _extrato == null) {
      return const Center(child: CircularProgressIndicator());
    }
    final extrato = _extrato;
    if (extrato == null) {
      return _ErroComRetry(
        mensagem: _erro ?? 'não foi possível carregar o extrato',
        onRetry: _carregar,
      );
    }
    return RefreshIndicator(
      onRefresh: _carregar,
      child: ListView(
        children: [
          Container(
            width: double.infinity,
            color: Theme.of(context).colorScheme.primaryContainer,
            padding: const EdgeInsets.all(16),
            child: Text(
              'Total da rodada ${widget.matchday}: ${extrato.total} ponto(s)',
              // Cor **explícita** de par: `primaryContainer` passou a ser a cor
              // cheia da marca (#615FF6), e o texto padrão escuro dava contraste
              // 3.64 sobre ela — reprovando o mínimo de 4.5. Com o par correto,
              // 4.54.
              style: Theme.of(context).textTheme.titleMedium?.copyWith(
                color: Theme.of(context).colorScheme.onPrimaryContainer,
              ),
            ),
          ),
          for (final l in extrato.linhas) _LinhaExtrato(linha: l),
        ],
      ),
    );
  }
}

class _LinhaExtrato extends StatelessWidget {
  const _LinhaExtrato({required this.linha});
  final LinhaExtrato linha;

  @override
  Widget build(BuildContext context) {
    final cores = Theme.of(context).colorScheme;
    final palpite = linha.palpiteCasa != null
        ? '${linha.palpiteCasa}x${linha.palpiteFora}'
        : 'sem palpite';
    final oficial = linha.oficialCasa != null
        ? '${linha.oficialCasa}x${linha.oficialFora}'
        : null;

    return ListTile(
      title: Text('${linha.homeTeam} x ${linha.awayTeam}'),
      subtitle: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            'Seu palpite: $palpite${oficial != null ? '  ·  Oficial: $oficial' : ''}',
          ),
          // O motivo do ponto: é isto que dispensa "confia" como resposta.
          if (linha.band != null)
            Text(
              descreverFaixa(linha.band),
              style: TextStyle(color: cores.outline, fontSize: 12),
            )
          // Jogo terminado sem palpite: o resultado chegou, o palpite faltou.
          // Dizer "aguardando resultado" aqui é falso e esconde do
          // participante que ele esqueceu de palpitar.
          else if (linha.estado == 'SEM_PALPITE')
            Text(
              'você não palpitou neste jogo',
              style: TextStyle(color: cores.outline, fontSize: 12),
            )
          else if (linha.estado == 'AGUARDANDO_RESULTADO')
            Text(
              'aguardando resultado',
              style: TextStyle(color: cores.outline, fontSize: 12),
            )
          else if (linha.estado == 'PRAZO_ABERTO')
            Text(
              'prazo ainda aberto',
              style: TextStyle(color: cores.primary, fontSize: 12),
            ),
          // Placar corrigido: o participante precisa ver **o que mudou**, senão
          // ponto que cai é indistinguível de bug — ou de fraude, no manual.
          if (linha.correcao != null)
            Padding(
              padding: const EdgeInsets.only(top: 4),
              child: Text(
                'placar corrigido — antes era ${linha.correcao!.placarAnterior}'
                '${linha.correcao!.manual ? ', alterado pelo administrador' : ''}',
                style: TextStyle(
                  color: cores.error,
                  fontSize: 12,
                  fontWeight: FontWeight.w500,
                ),
              ),
            ),
        ],
      ),
      trailing: Text(
        linha.points != null ? '${linha.points} pts' : '—',
        style: Theme.of(context).textTheme.titleMedium,
      ),
    );
  }
}

class _ErroComRetry extends StatelessWidget {
  const _ErroComRetry({required this.mensagem, required this.onRetry});
  final String mensagem;
  final VoidCallback onRetry;

  @override
  Widget build(BuildContext context) => Center(
    child: Padding(
      padding: const EdgeInsets.all(32),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Icon(Icons.cloud_off, size: 48),
          const SizedBox(height: 12),
          Text(mensagem, textAlign: TextAlign.center),
          const SizedBox(height: 16),
          FilledButton(onPressed: onRetry, child: const Text('Tentar de novo')),
        ],
      ),
    ),
  );
}

class _Mensagem extends StatelessWidget {
  const _Mensagem({
    required this.icone,
    required this.titulo,
    required this.detalhe,
  });
  final IconData icone;
  final String titulo;
  final String detalhe;

  @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),
        ],
      ),
    ),
  );
}
