class LinhaRanking {
  const LinhaRanking({
    required this.userId,
    required this.displayName,
    required this.position,
    required this.points,
    required this.exactScores,
    required this.decidedBy,
    required this.sharedPosition,
  });

  factory LinhaRanking.doJson(Map<String, dynamic> j) => LinhaRanking(
    userId: j['userId'] as String,
    displayName: j['displayName'] as String,
    position: j['position'] as int,
    points: j['points'] as int,
    exactScores: j['exactScores'] as int,
    decidedBy: j['decidedBy'] as String,
    sharedPosition: j['sharedPosition'] as bool,
  );

  final String userId;
  final String displayName;
  final int position;
  final int points;
  final int exactScores;

  /// Critério que decidiu a posição em relação a quem está empatado em pontos —
  /// acima **ou** abaixo. Exibir isso é o que evita briga sobre a
  /// classificação, e vale principalmente para quem está na frente.
  final String decidedBy;

  /// Divide a posição com outro participante — co-campeões.
  final bool sharedPosition;
}

class Ranking {
  const Ranking({required this.total, required this.linhas});

  factory Ranking.doJson(Map<String, dynamic> j) => Ranking(
    total: j['total'] as int,
    linhas: (j['rows'] as List<dynamic>)
        .cast<Map<String, dynamic>>()
        .map(LinhaRanking.doJson)
        .toList(),
  );

  final int total;
  final List<LinhaRanking> linhas;

  /// Volta ao formato da API, para o cache reusar o mesmo mapeador.
  Map<String, dynamic> paraJson() => {
    'total': total,
    'rows': linhas
        .map(
          (l) => {
            'userId': l.userId,
            'displayName': l.displayName,
            'position': l.position,
            'points': l.points,
            'exactScores': l.exactScores,
            'decidedBy': l.decidedBy,
            'sharedPosition': l.sharedPosition,
          },
        )
        .toList(),
  };
}

/// Ranking de uma rodada só (PR-4). Separado do geral de propósito: quem foi
/// bem na rodada e mal no campeonato precisa ver as duas coisas.
class RankingRodada {
  const RankingRodada({
    required this.matchday,
    required this.totalJogos,
    required this.apurados,
    required this.parcial,
    required this.linhas,
  });

  factory RankingRodada.doJson(Map<String, dynamic> j) => RankingRodada(
    matchday: j['matchday'] as int,
    totalJogos: j['totalMatches'] as int,
    apurados: j['scoredMatches'] as int,
    parcial: j['partial'] as bool,
    linhas: (j['rows'] as List<dynamic>)
        .cast<Map<String, dynamic>>()
        .map(LinhaRanking.doJson)
        .toList(),
  );

  final int matchday;
  final int totalJogos;
  final int apurados;

  /// Rodada ainda não apurada por inteiro. Precisa ficar dito na tela: ranking
  /// parcial parecendo final gera comemoração e briga na sequência.
  final bool parcial;
  final List<LinhaRanking> linhas;
}

/// Alteração do placar de um jogo, para o extrato explicar ponto que mudou.
class Correcao {
  const Correcao({
    required this.anteriorCasa,
    required this.anteriorFora,
    required this.manual,
  });

  factory Correcao.doJson(Map<String, dynamic> j) => Correcao(
    anteriorCasa: j['previousHome'] as int?,
    anteriorFora: j['previousAway'] as int?,
    manual: j['manual'] as bool? ?? false,
  );

  final int? anteriorCasa;
  final int? anteriorFora;

  /// Corrigido à mão pelo administrador. Precisa ficar visível como
  /// intervenção humana — é a única mitigação prevista para esse poder.
  final bool manual;

  String get placarAnterior => anteriorCasa == null || anteriorFora == null
      ? 'sem placar'
      : '${anteriorCasa}x$anteriorFora';
}

class LinhaExtrato {
  const LinhaExtrato({
    required this.homeTeam,
    required this.awayTeam,
    required this.estado,
    required this.points,
    required this.band,
    required this.palpiteCasa,
    required this.palpiteFora,
    required this.oficialCasa,
    required this.oficialFora,
    this.matchId = '',
    this.correcao,
  });

  factory LinhaExtrato.doJson(Map<String, dynamic> j) {
    final meu = j['myPrediction'] as Map<String, dynamic>?;
    final oficial = j['officialScore'] as Map<String, dynamic>?;
    return LinhaExtrato(
      homeTeam: j['homeTeam'] as String,
      awayTeam: j['awayTeam'] as String,
      estado: j['estado'] as String,
      points: j['points'] as int?,
      band: j['band'] as String?,
      palpiteCasa: meu?['homeScore'] as int?,
      palpiteFora: meu?['awayScore'] as int?,
      oficialCasa: oficial?['homeScore'] as int?,
      oficialFora: oficial?['awayScore'] as int?,
      matchId: j['matchId'] as String? ?? '',
      correcao: j['correction'] == null
          ? null
          : Correcao.doJson(j['correction'] as Map<String, dynamic>),
    );
  }

  final String homeTeam;
  final String awayTeam;
  final String estado;
  final int? points;

  /// Faixa de acerto. É o "por quê" do ponto — sem isso o extrato não
  /// substitui o admin como árbitro.
  final String? band;

  final int? palpiteCasa;
  final int? palpiteFora;
  final int? oficialCasa;
  final int? oficialFora;
  final String matchId;

  /// Nulo quando o placar nunca mudou.
  final Correcao? correcao;
}

class Extrato {
  const Extrato({required this.total, required this.linhas});

  factory Extrato.doJson(Map<String, dynamic> j) => Extrato(
    total: j['total'] as int,
    linhas: (j['rows'] as List<dynamic>)
        .cast<Map<String, dynamic>>()
        .map(LinhaExtrato.doJson)
        .toList(),
  );

  /// Soma dos pontos da rodada. Precisa bater com o ranking.
  final int total;
  final List<LinhaExtrato> linhas;
}

abstract class RankingRepository {
  /// Página do ranking geral. O total vem junto, então quem chama sabe quando
  /// parou — sem isso a tela pediria página vazia para sempre.
  Future<Ranking> geral(String poolId, {int limit = 50, int offset = 0});

  Future<RankingRodada> daRodada(String poolId, int matchday);

  Future<Extrato> extrato(String poolId, int matchday);
}

/// Texto do critério de desempate, na voz do participante.
String descreverCriterio(String criterio) => switch (criterio) {
  'PONTOS' => 'por pontos',
  'EXACT_SCORES' => 'desempate: mais placares exatos',
  'WINNERS' => 'desempate: mais acertos de vencedor',
  'EMPATE_PERSISTE' => 'empate mantido — dividem a posição',
  _ => '',
};

/// Texto da faixa de acerto, explicando de onde veio o ponto.
String descreverFaixa(String? band) => switch (band) {
  'PLACAR_EXATO' => 'placar exato',
  'VENCEDOR_E_SALDO' => 'vencedor e saldo de gols',
  'SO_VENCEDOR' => 'só o vencedor',
  'ERROU' => 'errou o vencedor',
  _ => '',
};
