import 'package:cloud_functions/cloud_functions.dart';
import 'package:firebase_auth/firebase_auth.dart';

import '../erros/erro_do_app.dart';
import 'firebase_config.dart';

/// Ponte entre os erros do Firebase e os erros que a tela conhece.
///
/// Cada recusa do servidor tem motivo distinguível — prazo fechado, convite
/// expirado, bolão já começado —, e a callable manda esse motivo em duas formas:
/// a mensagem, escrita para o participante ler, e `details.motivo`, para o app
/// decidir o que oferecer **sem depender do texto**. Perder isso aqui e mostrar
/// "erro" é o que faz o participante desistir e voltar ao WhatsApp.
class Callables {
  Callables([FirebaseFunctions? functions])
    : _functions =
          functions ??
          FirebaseFunctions.instanceFor(region: FirebaseConfig.regiao);

  final FirebaseFunctions _functions;

  Future<T> chamar<T>(String nome, [Map<String, dynamic>? dados]) async {
    try {
      final r = await _functions.httpsCallable(nome).call<dynamic>(dados);
      return r.data as T;
    } on FirebaseFunctionsException catch (e) {
      throw traduzir(e);
    } on FirebaseException catch (e) {
      throw traduzirFirestore(e);
    }
  }
}

/// Motivo estruturado que veio junto da recusa, quando veio.
String? motivoDe(Object erro) => erro is ApiError ? erro.motivo : null;

ApiError traduzir(FirebaseFunctionsException e) {
  final detalhes = e.details;
  final motivo = detalhes is Map ? detalhes['motivo'] as String? : null;

  if (e.code == 'unauthenticated') return SessaoExpirada();

  return ApiError(
    // A mensagem da callable já é escrita para o participante. Trocá-la por uma
    // genérica aqui jogaria fora o trabalho de escrever cada recusa.
    e.message?.isNotEmpty == true ? e.message! : 'não foi possível concluir',
    status: _statusDe(e.code),
    semConexao: e.code == 'unavailable' && e.message == null,
    motivo: motivo,
  );
}

ApiError traduzirFirestore(FirebaseException e) {
  if (e.code == 'permission-denied') {
    // **A rule não diz por quê, e não deve mesmo.** Quem sabe o motivo é quem
    // chamou, que conhece o estado da tela: prazo, bolão encerrado, saída.
    return ApiError('esta ação não é permitida agora', status: 403);
  }
  if (e.code == 'unavailable') {
    return ApiError('sem conexão com o servidor', semConexao: true);
  }
  return ApiError('não foi possível concluir a operação');
}

ApiError traduzirAuth(FirebaseAuthException e) => switch (e.code) {
  // O mesmo texto para os dois casos, de propósito: dizer "e-mail não existe"
  // transforma a tela de login num verificador de quem tem conta no app.
  'user-not-found' ||
  'wrong-password' ||
  'invalid-credential' => ApiError('e-mail ou senha inválidos', status: 401),
  'email-already-in-use' => ApiError(
    'não foi possível cadastrar com esses dados',
    status: 409,
  ),
  'invalid-email' => ApiError('informe um e-mail válido', status: 400),
  'weak-password' => ApiError(
    'a senha precisa de pelo menos 8 caracteres',
    status: 400,
  ),
  'too-many-requests' => ApiError(
    'muitas tentativas seguidas. Espere um pouco e tente de novo',
    status: 429,
  ),
  'network-request-failed' => ApiError(
    'sem conexão com o servidor',
    semConexao: true,
  ),
  _ => ApiError('não foi possível concluir'),
};

int? _statusDe(String code) => switch (code) {
  'not-found' => 404,
  'permission-denied' => 403,
  'unauthenticated' => 401,
  'invalid-argument' => 400,
  'failed-precondition' => 409,
  'unavailable' => 503,
  _ => null,
};

/// A callable devolve mapas com chaves dinâmicas; os mapeadores da tela esperam
/// `Map<String, dynamic>`. Converter aqui mantém os mapeadores intactos — eles
/// são o contrato que a apresentação já conhece.
Map<String, dynamic> mapaDaCallable(Map<dynamic, dynamic> bruto) {
  return bruto.map((k, v) => MapEntry(k as String, _valor(v)));
}

dynamic _valor(dynamic v) {
  if (v is Map) return mapaDaCallable(v);
  if (v is List) return v.map(_valor).toList();
  return v;
}
