class Participante {
  const Participante({
    required this.userId,
    required this.displayName,
    required this.isAdmin,
    required this.isCreator,
    required this.ativo,
  });

  factory Participante.doJson(Map<String, dynamic> j) => Participante(
    userId: j['userId'] as String,
    displayName: j['displayName'] as String,
    isAdmin: j['role'] == 'ADMIN',
    isCreator: j['isCreator'] as bool? ?? false,
    ativo: j['active'] as bool? ?? true,
  );

  final String userId;
  final String displayName;
  final bool isAdmin;

  /// Criador é admin permanente: não pode ser rebaixado nem removido.
  final bool isCreator;

  /// `false` = saiu do bolão. A linha continua na lista porque o histórico de
  /// jogos apurados dele continua existindo.
  final bool ativo;
}

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

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

  final int total;
  final List<Participante> linhas;
}

/// Estado de palpite de um participante na rodada. **Nunca** traz placar: o
/// sigilo de PP-7 vale até o jogo terminar, inclusive para o admin.
class StatusPalpite {
  const StatusPalpite({
    required this.userId,
    required this.displayName,
    required this.feitos,
    required this.estado,
  });

  factory StatusPalpite.doJson(Map<String, dynamic> j) => StatusPalpite(
    userId: j['userId'] as String,
    displayName: j['displayName'] as String,
    feitos: j['predictionsMade'] as int,
    estado: j['estado'] as String,
  );

  final String userId;
  final String displayName;
  final int feitos;
  final String estado;
}

class StatusDaRodada {
  const StatusDaRodada({
    required this.matchday,
    required this.totalJogos,
    required this.linhas,
  });

  factory StatusDaRodada.doJson(Map<String, dynamic> j) => StatusDaRodada(
    matchday: j['matchday'] as int,
    totalJogos: j['totalMatches'] as int,
    linhas: (j['rows'] as List<dynamic>)
        .cast<Map<String, dynamic>>()
        .map(StatusPalpite.doJson)
        .toList(),
  );

  final int matchday;
  final int totalJogos;
  final List<StatusPalpite> linhas;
}

class Regras {
  const Regras({
    required this.pontosExato,
    required this.pontosVencedorSaldo,
    required this.pontosVencedor,
    required this.ordemDesempate,
    required this.travadas,
    required this.proximoPrazo,
  });

  factory Regras.doJson(Map<String, dynamic> j) => Regras(
    pontosExato: j['pointsExact'] as int,
    pontosVencedorSaldo: j['pointsWinnerDiff'] as int,
    pontosVencedor: j['pointsWinner'] as int,
    ordemDesempate: (j['tiebreakOrder'] as List<dynamic>)
        .cast<String>()
        .toList(),
    travadas: j['rulesLockedAt'] != null,
    proximoPrazo: j['nextDeadlineAt'] == null
        ? null
        : DateTime.parse(j['nextDeadlineAt'] as String),
  );

  final int pontosExato;
  final int pontosVencedorSaldo;
  final int pontosVencedor;
  final List<String> ordemDesempate;

  /// Travadas pelo primeiro palpite (CC-3). Depois disso ninguém muda a
  /// pontuação no meio do jogo.
  final bool travadas;
  final DateTime? proximoPrazo;

  /// Volta ao formato da API, para o cache não precisar de um segundo mapeador.
  Map<String, dynamic> paraJson() => {
    'pointsExact': pontosExato,
    'pointsWinnerDiff': pontosVencedorSaldo,
    'pointsWinner': pontosVencedor,
    'tiebreakOrder': ordemDesempate,
    'rulesLockedAt': travadas ? DateTime.now().toIso8601String() : null,
    'nextDeadlineAt': proximoPrazo?.toIso8601String(),
  };
}

class Aviso {
  const Aviso({
    required this.id,
    required this.kind,
    required this.titulo,
    required this.corpo,
    required this.criadoEm,
    required this.lido,
  });

  factory Aviso.doJson(Map<String, dynamic> j) => Aviso(
    id: j['id'] as String,
    kind: j['kind'] as String,
    titulo: j['title'] as String,
    corpo: j['body'] as String,
    criadoEm: DateTime.parse(j['createdAt'] as String),
    lido: j['readAt'] != null,
  );

  final String id;
  final String kind;
  final String titulo;
  final String corpo;
  final DateTime criadoEm;
  final bool lido;
}

abstract class ParticipantesRepository {
  Future<ListaParticipantes> listar(String poolId);

  Future<StatusDaRodada> statusDaRodada(String poolId, int matchday);

  Future<void> promover(String poolId, String userId);

  Future<void> rebaixar(String poolId, String userId);

  Future<void> remover(String poolId, String userId);

  Future<void> sair(String poolId);

  Future<Regras> regras(String poolId);

  Future<List<Aviso>> avisos(String poolId);

  Future<void> marcarLido(String poolId, String avisoId);
}

/// Texto do critério de desempate para leitura, na ordem configurada.
String descreverOrdemDesempate(List<String> ordem) => ordem
    .map(
      (c) => switch (c) {
        'EXACT_SCORES' => 'mais placares exatos',
        'WINNERS' => 'mais acertos de vencedor',
        _ => c,
      },
    )
    .join(', depois ');
