import '../../../core/api/api_client.dart';

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;
}

class BoloesRepository {
  BoloesRepository(this._api);

  final ApiClient _api;

  Future<List<BolaoResumo>> meus() async {
    final r = await _api.get<List<dynamic>>('/pools');
    return r.cast<Map<String, dynamic>>().map(BolaoResumo.doJson).toList();
  }

  Future<List<Competicao>> competicoes() async {
    final r = await _api.get<List<dynamic>>('/pools/competitions');
    return r.cast<Map<String, dynamic>>().map(Competicao.doJson).toList();
  }

  Future<Bolao> criar({
    required String nome,
    required String competitionCode,
    required int rodadaInicial,
    required int rodadaFinal,
    int? pontosExato,
    int? pontosVencedorSaldo,
    int? pontosVencedor,
    List<String>? ordemDesempate,
  }) async {
    // Regras omitidas caem no default do servidor (10/5/3). Mandar os valores
    // só quando o admin mexeu evita gravar "customizado" o que é o padrão.
    final regras = <String, dynamic>{
      'pointsExact': ?pontosExato,
      'pointsWinnerDiff': ?pontosVencedorSaldo,
      'pointsWinner': ?pontosVencedor,
      'tiebreakOrder': ?ordemDesempate,
    };

    final r = await _api.post<Map<String, dynamic>>(
      '/pools',
      body: {
        'name': nome,
        'competitionCode': competitionCode,
        'matchdayFrom': rodadaInicial,
        'matchdayTo': rodadaFinal,
        if (regras.isNotEmpty) 'rules': regras,
      },
    );
    return Bolao.doJson(r);
  }

  /// Altera as regras. O servidor recusa com 409 depois do primeiro palpite —
  /// e essa recusa é fluxo esperado, não erro.
  Future<void> alterarRegras(
    String poolId, {
    required int pontosExato,
    required int pontosVencedorSaldo,
    required int pontosVencedor,
    required List<String> ordemDesempate,
  }) async {
    await _api.patch<Map<String, dynamic>>(
      '/pools/$poolId/rules',
      body: {
        'pointsExact': pontosExato,
        'pointsWinnerDiff': pontosVencedorSaldo,
        'pointsWinner': pontosVencedor,
        'tiebreakOrder': ordemDesempate,
      },
    );
  }

  Future<Rodada> rodada(String poolId, int matchday) async {
    final r = await _api.get<Map<String, dynamic>>(
      '/pools/$poolId/matchdays/$matchday',
    );
    return Rodada.doJson(r);
  }

  Future<ResultadoEnvio> palpitar(
    String poolId,
    List<({String matchId, int casa, int fora})> palpites,
  ) async {
    final r = await _api.post<Map<String, dynamic>>(
      '/pools/$poolId/predictions',
      body: {
        'predictions': palpites
            .map(
              (p) => {
                'matchId': p.matchId,
                'homeScore': p.casa,
                'awayScore': p.fora,
              },
            )
            .toList(),
      },
    );
    return ResultadoEnvio.doJson(r);
  }

  Future<({String poolId, String poolName, bool jaParticipava})>
  entrarPorConvite(String token) async {
    final r = await _api.post<Map<String, dynamic>>(
      '/pools/join',
      body: {'token': token},
    );
    return (
      poolId: r['poolId'] as String,
      poolName: r['poolName'] as String,
      jaParticipava: r['jaParticipava'] as bool,
    );
  }

  Future<({String token, DateTime expiraEm})> gerarConvite(
    String poolId,
  ) async {
    final r = await _api.post<Map<String, dynamic>>('/pools/$poolId/invite');
    return (
      token: r['token'] as String,
      expiraEm: DateTime.parse(r['expiresAt'] as String),
    );
  }

  /// Palpites de todos num jogo (PP-7). Vem vazio enquanto o jogo não termina.
  Future<PalpitesDoJogo> palpitesDoJogo(String poolId, String matchId) async {
    final r = await _api.get<Map<String, dynamic>>(
      '/pools/$poolId/matches/$matchId/predictions',
    );
    return PalpitesDoJogo.doJson(r);
  }

  /// Correção manual do placar pelo admin (PR-10). Devolve o impacto para a
  /// tela poder dizer quantos foram reapurados e avisados.
  Future<({int afetados, int avisos})> corrigirPlacar(
    String poolId,
    String matchId,
    int casa,
    int fora,
  ) async {
    final r = await _api.patch<Map<String, dynamic>>(
      '/pools/$poolId/matches/$matchId/score',
      body: {'homeScore': casa, 'awayScore': fora},
    );
    return (
      afetados: r['afetados'] as int? ?? 0,
      avisos: r['avisos'] as int? ?? 0,
    );
  }

  /// Invalida o convite atual sem gerar outro — para quando o link caiu no
  /// lugar errado e o admin ainda não quer convidar mais ninguém.
  Future<void> invalidarConvite(String poolId) async {
    await _api.delete<void>('/pools/$poolId/invite');
  }
}
