class Bolao {
  const Bolao({
    required this.id,
    required this.name,
    required this.competitionCode,
    required this.matchdayFrom,
    required this.matchdayTo,
    required this.totalMatches,
    this.openMatches = 0,
  });

  factory Bolao.doJson(Map<String, dynamic> j) => Bolao(
    id: j['id'] as String,
    name: j['name'] as String,
    competitionCode: j['competitionCode'] as String? ?? 'BSA',
    matchdayFrom: j['matchdayFrom'] as int,
    matchdayTo: j['matchdayTo'] as int,
    totalMatches: j['totalMatches'] as int? ?? 0,
    openMatches: j['openMatches'] as int? ?? 0,
  );

  final String id;
  final String name;
  final String competitionCode;
  final int matchdayFrom;
  final int matchdayTo;
  final int totalMatches;

  /// Quantos jogos ainda têm prazo aberto. **Zero é um bolão nascido morto:**
  /// ninguém palpita e o convite é recusado (a entrada fecha no primeiro jogo).
  /// O app tem que dizer isso na hora, não deixar o admin descobrir pelo grupo.
  final int openMatches;
}

/// Um bolão na lista da home. Traz o suficiente para o participante
/// reconhecer qual é o dele sem abrir cada um.
class BolaoResumo {
  const BolaoResumo({
    required this.id,
    required this.name,
    required this.matchdayFrom,
    required this.matchdayTo,
    required this.totalMembers,
    required this.isAdmin,
    required this.encerrado,
    this.arquivado = false,
  });

  factory BolaoResumo.doJson(Map<String, dynamic> j) => BolaoResumo(
    id: j['id'] as String,
    name: j['name'] as String,
    matchdayFrom: j['matchdayFrom'] as int,
    matchdayTo: j['matchdayTo'] as int,
    totalMembers: j['totalMembers'] as int? ?? 0,
    isAdmin: j['isAdmin'] as bool? ?? false,
    encerrado: j['closed'] as bool? ?? false,
    arquivado: j['archived'] as bool? ?? false,
  );

  /// Volta ao formato da API para o cache não precisar de um segundo mapeador.
  Map<String, dynamic> paraJson() => {
    'id': id,
    'name': name,
    'matchdayFrom': matchdayFrom,
    'matchdayTo': matchdayTo,
    'totalMembers': totalMembers,
    'isAdmin': isAdmin,
    'closed': encerrado,
    'archived': arquivado,
  };

  final String id;
  final String name;
  final int matchdayFrom;
  final int matchdayTo;
  final int totalMembers;
  final bool isAdmin;
  final bool encerrado;

  /// Arquivado pelo admin. Continua na lista, separado e somente leitura —
  /// omitir fazia o bolão desaparecer sem nada explicar.
  final bool arquivado;
}

class Competicao {
  const Competicao({
    required this.code,
    required this.nome,
    required this.rodadaAtual,
  });

  factory Competicao.doJson(Map<String, dynamic> j) => Competicao(
    code: j['code'] as String,
    nome: j['nome'] as String,
    rodadaAtual: j['rodadaAtual'] as int?,
  );

  final String code;
  final String nome;
  final int? rodadaAtual;
}

/// Um jogo da rodada, com o estado do prazo decidido pelo servidor.
class JogoDaRodada {
  const JogoDaRodada({
    required this.matchId,
    required this.homeTeam,
    required this.awayTeam,
    required this.kickoffAt,
    required this.deadlineOpen,
    this.palpiteCasa,
    this.palpiteFora,
    this.oficialCasa,
    this.oficialFora,
  });

  factory JogoDaRodada.doJson(Map<String, dynamic> j) {
    final meu = j['myPrediction'] as Map<String, dynamic>?;
    final oficial = j['officialScore'] as Map<String, dynamic>?;
    return JogoDaRodada(
      matchId: j['matchId'] as String,
      homeTeam: j['homeTeam'] as String,
      awayTeam: j['awayTeam'] as String,
      kickoffAt: DateTime.parse(j['kickoffAt'] as String),
      deadlineOpen: j['deadlineOpen'] as bool,
      palpiteCasa: meu?['homeScore'] as int?,
      palpiteFora: meu?['awayScore'] as int?,
      oficialCasa: oficial?['homeScore'] as int?,
      oficialFora: oficial?['awayScore'] as int?,
    );
  }

  final String matchId;
  final String homeTeam;
  final String awayTeam;
  final DateTime kickoffAt;

  /// Vem do servidor. O app **nunca** decide isso pelo relógio do device.
  final bool deadlineOpen;

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

  bool get temPalpite => palpiteCasa != null && palpiteFora != null;
}

class Rodada {
  const Rodada({required this.serverTime, required this.jogos});

  factory Rodada.doJson(Map<String, dynamic> j) => Rodada(
    serverTime: DateTime.parse(j['serverTime'] as String),
    jogos: (j['matches'] as List<dynamic>)
        .map((e) => JogoDaRodada.doJson(e as Map<String, dynamic>))
        .toList(),
  );

  /// Horário do servidor no momento da resposta. É a partir daqui que a
  /// contagem é calculada — o relógio do device pode ter sido alterado.
  final DateTime serverTime;
  final List<JogoDaRodada> jogos;
}

/// Palpite de outra pessoa num jogo, revelado só depois do fim.
class PalpiteDeTerceiro {
  const PalpiteDeTerceiro({
    required this.userId,
    required this.displayName,
    required this.casa,
    required this.fora,
    required this.pontos,
  });

  factory PalpiteDeTerceiro.doJson(Map<String, dynamic> j) => PalpiteDeTerceiro(
    userId: (j['user'] as Map<String, dynamic>)['id'] as String,
    displayName: (j['user'] as Map<String, dynamic>)['displayName'] as String,
    casa: j['homeScore'] as int,
    fora: j['awayScore'] as int,
    pontos: j['points'] as int?,
  );

  final String userId;
  final String displayName;
  final int casa;
  final int fora;
  final int? pontos;
}

/// Resposta do sigilo: `revelado` falso vem com lista **vazia**, porque o
/// filtro é na origem. Se o servidor mandasse e a tela escondesse, qualquer um
/// leria a API direto — inclusive o admin, que joga no próprio bolão.
class PalpitesDoJogo {
  const PalpitesDoJogo({required this.revelado, required this.palpites});

  factory PalpitesDoJogo.doJson(Map<String, dynamic> j) => PalpitesDoJogo(
    revelado: j['revealed'] as bool,
    palpites: ((j['predictions'] as List<dynamic>?) ?? const [])
        .cast<Map<String, dynamic>>()
        .map(PalpiteDeTerceiro.doJson)
        .toList(),
  );

  final bool revelado;
  final List<PalpiteDeTerceiro> palpites;
}

class ResultadoEnvio {
  const ResultadoEnvio({
    required this.salvos,
    required this.recusados,
    required this.motivos,
  });

  factory ResultadoEnvio.doJson(Map<String, dynamic> j) {
    final results = (j['results'] as List<dynamic>)
        .cast<Map<String, dynamic>>();
    return ResultadoEnvio(
      salvos: j['savedCount'] as int,
      recusados: j['rejectedCount'] as int,
      motivos: {
        for (final r in results.where((r) => r['saved'] == false))
          r['matchId'] as String: r['reason'] as String,
      },
    );
  }

  final int salvos;
  final int recusados;

  /// Motivo por jogo recusado. O backend distingue cada caso; a tela precisa
  /// dizer **qual** foi, não "erro".
  final Map<String, String> motivos;
}

/// Contrato dos bolões visto pela apresentação.
///
/// Os modelos acima **não mudam** entre implementações: eles são o formato que a
/// tela conhece, e mantê-los é o que faz a troca de backend não vazar para cima
/// (critério de `MG-7`).
abstract class BoloesRepository {
  Future<List<BolaoResumo>> meus();

  Future<List<Competicao>> competicoes();

  Future<Bolao> criar({
    required String nome,
    required String competitionCode,
    required int rodadaInicial,
    required int rodadaFinal,
    int? pontosExato,
    int? pontosVencedorSaldo,
    int? pontosVencedor,
    List<String>? ordemDesempate,
  });

  /// Altera as regras. Recusado depois do primeiro palpite — e essa recusa é
  /// fluxo esperado, não erro (CC-3).
  Future<void> alterarRegras(
    String poolId, {
    required int pontosExato,
    required int pontosVencedorSaldo,
    required int pontosVencedor,
    required List<String> ordemDesempate,
  });

  Future<Rodada> rodada(String poolId, int matchday);

  Future<ResultadoEnvio> palpitar(
    String poolId,
    List<({String matchId, int casa, int fora})> palpites,
  );

  Future<({String poolId, String poolName, bool jaParticipava})>
  entrarPorConvite(String token);

  Future<({String token, DateTime expiraEm})> gerarConvite(String poolId);

  /// Palpites de todos num jogo (PP-7). Vem vazio enquanto o jogo não termina.
  Future<PalpitesDoJogo> palpitesDoJogo(String poolId, String matchId);

  /// Correção manual do placar pelo admin (PR-10).
  Future<({int afetados, int avisos})> corrigirPlacar(
    String poolId,
    String matchId,
    int casa,
    int fora,
  );

  Future<void> invalidarConvite(String poolId);
}
