import json
import mimetypes
from pathlib import Path
import re
import unicodedata

import requests

from app.keepedu_http import post_json_comprimido
from app.gerador_csv import TOTAL_PERGUNTAS, carregar_leituras, nome_arquivo_keepedu, resposta
from app.services import limpar_pos_importacao_keepedu
from app.services import (
    caminho_leituras,
    localizar_imagem_original,
)
from app.settings import (
    ID_PROVA_KEEPEDU,
    KEEPEDU_API_KEY,
)
from app.tenant import get_institute_id, resolve_keepedu_url

# O Cloudflare do KeepEdu permite requisições de até 125 segundos. Mantemos
# uma margem para que o cliente consiga registrar a falha antes do proxy.
KEEPEDU_IMPORTAR_TIMEOUT_SECONDS = 110
# O KeepEdu recebe os vetores em grupos; o progresso identifica todos os
# templates que compõem o lote em transmissão.
KEEPEDU_IMPORTAR_LOTE_TAMANHO = 10
from app.validacao_cadastral import (
    carregar_validacao_manual,
    gerar_validacao_cadastral,
    leitura_bloqueada_id_avaliacao,
    obter_validacao_cadastral_para_envio,
    resolver_validacao_cartao,
    limpar_pendencia_importacao,
    registrar_envio_keepedu,
    registrar_pendencia_importacao,
)

def caminho_relatorio_importacao(nome_processamento: str, modo: str = "importacao") -> Path:
    caminho_leitura = caminho_leituras(nome_processamento)
    pasta_processamento = caminho_leitura.parent
    nome_arquivo = "simulacao_keepedu.json" if modo == "simulacao" else "importacao_keepedu.json"
    return pasta_processamento / nome_arquivo


def _texto_limpo(valor) -> str:
    if valor is None:
        return ""
    return str(valor).strip()


def _montar_respostas_objetivas(dados_cartao: dict) -> list[str]:
    respostas = dados_cartao.get("respostas", {})
    return [resposta(respostas, numero) for numero in range(9, TOTAL_PERGUNTAS + 1)]


def _detectar_id_aval(dados_validacao: dict, id_aval_informado) -> str:
    id_aval = _texto_limpo(id_aval_informado)
    if id_aval:
        return id_aval
    resumo = dados_validacao.get("resumo", {})
    id_aval = _texto_limpo(resumo.get("id_prova_processamento"))
    if id_aval:
        return id_aval
    return _texto_limpo(ID_PROVA_KEEPEDU)


def _cabecalhos_keepedu() -> dict:
    return {
        "ApiKey": KEEPEDU_API_KEY,
        "Institute": get_institute_id(),
        "Content-Type": "application/json",
        "Accept": "application/json",
    }


def _cabecalhos_keepedu_upload() -> dict:
    return {
        "ApiKey": KEEPEDU_API_KEY,
        "Institute": get_institute_id(),
        "Accept": "application/json",
    }


def _mensagem_erro_rede(contexto: str, erro: BaseException) -> dict:
    return {
        "success": False,
        "mensagem": [f"{contexto}: {str(erro).strip() or 'falha de rede.'}"],
    }


def _payload_base(id_aval: str) -> dict:
    id_aval_payload = int(id_aval) if id_aval.isdigit() else id_aval
    return {"idAval": id_aval_payload}


def _montar_aluno_payload(
    nome_imagem: str,
    dados_cartao: dict,
    validacao: dict,
    id_aval: str,
    nome_arquivo: str,
    modelo_ia: str | None = None,
) -> dict:
    barcode = _texto_limpo(validacao.get("codigo_barras_final"))
    id_aluno = _texto_limpo(validacao.get("id_final"))

    if barcode and "A" in barcode:
        id_aval_barcode = barcode.split("A", 1)[0].strip()
        if id_aval and id_aval_barcode and id_aval_barcode != id_aval:
            raise ValueError(
                f"O barcode do cartão {nome_imagem} usa idAval {id_aval_barcode}, "
                f"mas o envio foi configurado com {id_aval}."
            )

    if not barcode and not id_aluno:
        raise ValueError(f"O cartão {nome_imagem} não possui barcode final nem idAluno final.")

    print("\n===== NOME ARQUIVO FINAL =====")
    print(nome_arquivo)
    print("=============================\n")


    respostas = dados_cartao.get("respostas", {})
    aluno = {
        "nomeArquivo": nome_arquivo,  # <--- Mude aqui para usar a variável corrigida!
        "lingua": resposta(respostas, 7),
        "respostas": _montar_respostas_objetivas(dados_cartao),
        "modelo_ia": modelo_ia or "READING",
    }

    if barcode:
        aluno["barcode"] = barcode
    else:
        aluno["idAluno"] = id_aluno

    return aluno


def _salvar_relatorio(nome_processamento: str, relatorio: dict, modo: str) -> str:
    caminho_saida = caminho_relatorio_importacao(nome_processamento, modo=modo)
    caminho_saida.parent.mkdir(parents=True, exist_ok=True)
    with open(caminho_saida, "w", encoding="utf-8") as arquivo:
        json.dump(relatorio, arquivo, ensure_ascii=False, indent=4)
    return str(caminho_saida)


def _normalizar_texto_busca(valor) -> str:
    texto = unicodedata.normalize("NFKD", str(valor or ""))
    texto = texto.encode("ascii", "ignore").decode("ascii")
    return " ".join(texto.lower().split())


def _formatar_stem_arquivo_keepedu(nome_original_real: str) -> str:
    """Nome base KeepEdu: stem do arquivo com hífens (ex.: 12763-ls-adaptad...-002)."""
    return nome_arquivo_keepedu(nome_original_real).replace("_", "-").lower()


def _formatar_nome_arquivo_imagem_keepedu(nome_original_real: str) -> str:
    nome = _formatar_stem_arquivo_keepedu(nome_original_real)
    if nome.endswith(".jpeg"):
        nome = re.sub(r"\.jpeg$", ".jpg", nome, flags=re.IGNORECASE)
    elif not nome.endswith(".jpg"):
        nome += ".jpg"
    return nome


def _resposta_indica_problema_folha_resposta(corpo_resposta: dict) -> bool:
    texto_resposta = _normalizar_texto_busca(json.dumps(corpo_resposta or {}, ensure_ascii=False))
    padroes = (
        "folha_de_resposta",
        "importe primeiro as respostas presenciais",
        "arquivo nao importado",
        "nome do arquivo deve coincidir",
    )
    return any(padrao in texto_resposta for padrao in padroes)


def _resposta_indica_aluno_fora_avaliacao(corpo_resposta: dict) -> bool:
    if _resposta_indica_problema_folha_resposta(corpo_resposta):
        return False
    texto_resposta = _normalizar_texto_busca(json.dumps(corpo_resposta or {}, ensure_ascii=False))
    padroes = (
        "nao pertence a avaliacao",
        "nao pertence a esta avaliacao",
        "nao esta vinculado a avaliacao",
        "nao esta vinculado nesta avaliacao",
        "nao esta incluido na avaliacao",
        "nao esta incluso na avaliacao",
        "nao participa da avaliacao",
        "aluno nao pertence a avaliacao",
        "aluno nao esta na avaliacao",
        "aluno nao cadastrado na avaliacao",
        "aluno nao localizado na avaliacao",
        "nao encontrado na avaliacao",
    )
    return any(padrao in texto_resposta for padrao in padroes)


def _mensagens_resposta(corpo_resposta: dict) -> list[str]:
    mensagens = []
    for chave in (
        "mensagem", "message", "error", "erro", "errors", "detail",
        "details", "arquivoErros",
    ):
        valor = (corpo_resposta or {}).get(chave)
        if isinstance(valor, list):
            mensagens.extend(str(item).strip() for item in valor if str(item).strip())
        elif isinstance(valor, dict):
            mensagens.extend(str(item).strip() for item in valor.values() if str(item).strip())
        elif valor:
            mensagens.append(str(valor).strip())
    return mensagens


def _mensagem_erro_do_item_keepedu(corpo_resposta: dict, nome_arquivo: str, aluno: dict) -> str:
    """Retorna somente o erro que menciona o cartão atual, se houver.

    A API pode responder 2xx com uma lista de recusas parciais. Nesse caso a
    resposta do lote não pode transformar todos os alunos em erro.
    """
    mensagens = _mensagens_resposta(corpo_resposta)
    identificadores = {
        _normalizar_texto_busca(nome_arquivo),
        _normalizar_texto_busca(Path(nome_arquivo).stem),
        _normalizar_texto_busca(str((aluno or {}).get("barcode") or "")),
        _normalizar_texto_busca(str((aluno or {}).get("idAluno") or "")),
    }
    identificadores.discard("")
    for mensagem in mensagens:
        texto = _normalizar_texto_busca(mensagem)
        if any(identificador in texto for identificador in identificadores):
            return str(mensagem).strip()
    return ""


def _resumir_motivo_falha_importacao(
    status_code: int | None = None,
    corpo_resposta: dict | None = None,
    exc: BaseException | None = None,
) -> str:
    if exc is not None:
        texto = str(exc).strip()
        return texto or "Erro inesperado durante o processamento."
    mensagens = _mensagens_resposta(corpo_resposta or {})
    if mensagens:
        return " | ".join(mensagens[:3])
    if status_code is not None and not (200 <= status_code < 300):
        return f"A API retornou HTTP {status_code}."
    if isinstance(corpo_resposta, dict) and corpo_resposta.get("success") is False:
        return "A API recusou o envio sem informar o motivo."
    return ""


def _descricao_resultado_item(
    nome_item: str,
    sucesso: bool,
    motivo_falha: str = "",
    *,
    texto_sucesso: str,
    prefixo_falha: str,
) -> str:
    if sucesso:
        return texto_sucesso.format(nome=nome_item)
    motivo = str(motivo_falha or "").strip()
    if motivo:
        return f"{prefixo_falha} {nome_item}: {motivo}"
    return f"{prefixo_falha} {nome_item}."


def _resumir_resposta_http_nao_json(status_code: int, resposta_api: requests.Response, contexto: str) -> str:
    texto = str(resposta_api.text or "").strip()
    texto_compacto = " ".join(texto.split())

    if not texto_compacto:
        return f"{contexto}: HTTP {status_code} sem corpo JSON."
    if "<html" in texto_compacto.lower() or "<!doctype" in texto_compacto.lower():
        return f"{contexto}: HTTP {status_code} retornou HTML em vez de JSON."

    texto_limpo = re.sub(r"<[^>]+>", " ", texto_compacto)
    texto_limpo = " ".join(texto_limpo.split())

    if not texto_limpo:
        return f"{contexto}: HTTP {status_code} sem JSON válido."
    if len(texto_limpo) > 220:
        return f"{texto_limpo[:217]}..."

    return f"{contexto}: HTTP {status_code} - {texto_limpo}"


def _post_keepedu(url_destino: str, payload_envio: dict, cabecalhos: dict) -> tuple[int, dict]:
    try:
        resposta_api = post_json_comprimido(
            url_destino,
            payload_envio,
            headers=cabecalhos,
            timeout=KEEPEDU_IMPORTAR_TIMEOUT_SECONDS,
        )
    except requests.RequestException as exc:
        return 0, _mensagem_erro_rede("Falha ao enviar respostas ao KeepEdu", exc)

    print("\n========== ENVIO VETOR KEEPEDU ==========")
    print("URL:", url_destino)

    print("\nPAYLOAD:")
    print(json.dumps(payload_envio, indent=4, ensure_ascii=False))

    print("=========================================\n")

    status_code = resposta_api.status_code
    try:
        corpo_resposta = resposta_api.json()
    except ValueError:
        corpo_resposta = {
            "success": False,
            "mensagem": [_resumir_resposta_http_nao_json(status_code, resposta_api, "Importação de respostas")],
        }

    print("\n========== RESPOSTA KEEPEDU ==========")
    print("STATUS:", status_code)
    print("BODY:", resposta_api.text)
    print("======================================\n")
    return status_code, corpo_resposta

    # print("\n===== IMAGEM =====")
    # print("nomeArquivo =", nome_arquivo_formatado)
    # print("==================\n")
def _post_keepedu_folha_resposta(
    url_destino: str,
    caminho_imagem: Path,
    id_aval,
    cabecalhos: dict,
    nome_arquivo_limpo: str = None  # <--- ADICIONE ESTE PARÂMETRO
) -> tuple[int, dict]:
    content_type = mimetypes.guess_type(caminho_imagem.name)[0] or "application/octet-stream"
    # Se não passarmos o nome limpo, usa o do arquivo físico
    nome_final_envio = nome_arquivo_limpo or caminho_imagem.name

    try:
        with open(caminho_imagem, "rb") as arquivo_imagem:
            resposta_api = requests.post(
                url_destino,
                data={"idAval": str(id_aval)},
                files={
                    "imagem_folha_resposta": (
                        nome_final_envio,
                        arquivo_imagem,
                        content_type,
                    )
                },
                headers=cabecalhos,
                timeout=KEEPEDU_IMPORTAR_TIMEOUT_SECONDS,
            )
    except (OSError, requests.RequestException) as exc:
        return 0, _mensagem_erro_rede("Falha ao enviar imagem ao KeepEdu", exc)

        print("\n========== KEEPEDU UPLOAD ==========")
        print("URL:", url_destino)
        print("ID AVAL:", id_aval)
        print("ARQUIVO:", nome_final_envio)
        print("CAMINHO:", caminho_imagem)
        print("====================================\n")

        print("STATUS:", resposta_api.status_code)
        print("BODY:", resposta_api.text)

        print("\n===== DEBUG ENVIO IMAGEM =====")
        print("nome_final_envio =", nome_final_envio)
        print("caminho_imagem.name =", caminho_imagem.name)
        print("=============================\n")

        print("\n===== MULTIPART =====")
        print("field =", "imagem_folha_resposta")
        print("filename =", nome_final_envio)
        print("content_type =", content_type)
        print("=====================\n")

    # ... resto da função permanece igual
    status_code = resposta_api.status_code
    try:
        corpo_resposta = resposta_api.json()
    except ValueError:
        corpo_resposta = {
            "success": 200 <= status_code < 300,
            "mensagem": [_resumir_resposta_http_nao_json(status_code, resposta_api, "Upload da folha-resposta")],
        }
    return status_code, corpo_resposta


def _montar_url_folha_resposta(url_base: str, id_aval) -> str:
    url = str(url_base or "").strip()
    id_aval_texto = str(id_aval).strip()
    if not url or not id_aval_texto:
        return url
    return (
        url.replace("{idAval}", id_aval_texto)
        .replace("{id_aval}", id_aval_texto)
        .replace(":idAval", id_aval_texto)
        .replace(":id_digest", id_aval_texto)
    )


def _sucesso_resposta_generica(status_code: int, corpo_resposta: dict) -> bool:
    if not (200 <= status_code < 300):
        return False
    if isinstance(corpo_resposta, dict) and "success" in corpo_resposta:
        return bool(corpo_resposta.get("success"))
    return True


def _motivo_pendencia_avaliacao(corpo_resposta: dict, nome_arquivo: str) -> str:
    mensagens = _mensagens_resposta(corpo_resposta)
    if mensagens:
        return " | ".join(mensagens[:3])
    return f"O aluno do cartão {nome_arquivo} não pertence à avaliação informada."


def _emitir_progresso_importacao(
    progresso_callback,
    percentual: int,
    mensagem: str,
    arquivo_atual: str = "",
    resultado: dict | None = None,
    tipo: str | None = None,
    titulo: str | None = None,
    descricao: str = "",
    atualizar_evento: bool = False,
):
    if not progresso_callback:
        return
    progresso_callback(
        percentual,
        mensagem,
        arquivo_atual=arquivo_atual,
        resultado=resultado,
        tipo=tipo,
        titulo=titulo,
        descricao=descricao,
        atualizar_evento=atualizar_evento,
    )


def _selecionar_itens_importacao(
    leituras: dict,
    validacoes: dict,
    id_aluno=None,
    validacoes_manuais: dict | None = None,
) -> tuple[list[tuple[str, dict, dict]], str]:
    id_aluno_filtrado = _texto_limpo(id_aluno)
    manuais = validacoes_manuais if isinstance(validacoes_manuais, dict) else {}
    itens = []
    for nome_imagem, dados_cartao in leituras.items():
        validacao = resolver_validacao_cartao(validacoes, manuais, nome_imagem, dados_cartao)
        id_final = _texto_limpo(validacao.get("id_final"))
        if id_aluno_filtrado and id_final != id_aluno_filtrado:
            continue
        itens.append((nome_imagem, dados_cartao, validacao))
    return itens, id_aluno_filtrado


def contar_alunos_importacao(nome_processamento: str, id_aluno=None) -> int:
    leituras = carregar_leituras(nome_processamento)
    dados_validacao = obter_validacao_cadastral_para_envio(nome_processamento)
    validacoes = dados_validacao.get("validacoes", {})
    validacoes_manuais = carregar_validacao_manual(nome_processamento)
    itens, _ = _selecionar_itens_importacao(
        leituras,
        validacoes,
        id_aluno=id_aluno,
        validacoes_manuais=validacoes_manuais,
    )
    return len(itens)


def contar_imagens_importacao(nome_processamento: str, id_aluno=None) -> int:
    return contar_alunos_importacao(nome_processamento, id_aluno=id_aluno)


def processar_mock_importacao_keepedu(dados: dict) -> tuple[int, dict]:
    if not isinstance(dados, dict):
        return 400, {"success": False, "mensagem": ["Corpo inválido."], "arquivoErros": []}
    alunos = dados.get("alunos", [])
    return 200, {
        "success": True,
        "idAval": dados.get("idAval"),
        "total": len(alunos),
        "importadas": len(alunos),
        "erros": 0,
        "mensagem": ["Sucesso textual."],
        "arquivoErros": []
    }

def _obter_nome_original_limpo(nome_imagem: str, dados_cartao: dict) -> str:
    """
    Garante o retorno do nome do arquivo original limpo,
    removendo 'template_' e forçando a extensão estrita para .jpg
    """
    if not isinstance(dados_cartao, dict):
        dados_cartao = {}

    nome_base = dados_cartao.get("arquivo_original") or dados_cartao.get("nome_arquivo")

    if not nome_base:
        nome_base = re.sub(r"^template_", "", nome_imagem, flags=re.IGNORECASE)

    # FORÇA A EXTENSÃO .JPG
    if nome_base.lower().endswith(".jpeg"):
        nome_base = re.sub(r"\.jpeg$", ".jpg", nome_base, flags=re.IGNORECASE)

    return nome_base


def _executar_importacao_respostas_presenciais(
    nome_processamento: str,
    url_destino: str,
    modo: str,
    id_aval=None,
    dia_aval=None,
    usuario_id=None,
    id_aluno=None,
    progresso_callback=None,
    modelo_ia=None,
    origem_envio: str = "validacao",
):
    if not url_destino:
        return {"status": "erro", "mensagem": "URL não configurada no .env."}
    if not KEEPEDU_API_KEY or not get_institute_id():
        return {
            "status": "erro",
            "mensagem": "Configure KEEPEDU_API_KEY e o instituto da escola ativa em KEEPEDU_INSTITUTES (ou KEEPEDU_INSTITUTE como fallback).",
        }

    leituras = carregar_leituras(nome_processamento)
    leituras_incompativeis = [
        nome_imagem
        for nome_imagem, dados_cartao in leituras.items()
        if leitura_bloqueada_id_avaliacao(dados_cartao)
    ]
    if leituras_incompativeis:
        return {
            "status": "erro",
            "mensagem": (
                "Existem cartoes com ID de avaliacao diferente do ID informado "
                "no processamento. Corrija ou reprocesse o lote antes de enviar vetores."
            ),
            "cartoes_bloqueados": leituras_incompativeis,
        }
    dados_validacao = obter_validacao_cadastral_para_envio(nome_processamento)

    validacoes = dados_validacao.get("validacoes", {})
    validacoes_manuais = carregar_validacao_manual(nome_processamento)
    itens_envio, id_aluno_filtrado = _selecionar_itens_importacao(
        leituras,
        validacoes,
        id_aluno=id_aluno,
        validacoes_manuais=validacoes_manuais,
    )
    total_itens = len(itens_envio)

    print("\n===== FILTRO IMPORTAÇÃO =====")
    print("id_aluno:", repr(id_aluno_filtrado))
    print("total itens:", len(itens_envio))

    for item in itens_envio:
        print("item:", item[0])

    print("=============================\n")

    id_aval_final = _detectar_id_aval(dados_validacao, id_aval)
    if not id_aval_final:
        return {"status": "erro", "mensagem": "Informe o ID da avaliação."}

    cabecalhos = _cabecalhos_keepedu()
    payload_base = _payload_base(id_aval_final)
    detalhes = []
    total, importadas = 0, 0
    mensagens, arquivos_erros, pendencias_avaliacao = [], [], []
    confirmacoes_pendentes = 0
    validacao_atualizada = False

    total_itens = len(itens_envio)

    _emitir_progresso_importacao(
        progresso_callback,
        percentual=0,
        mensagem="Preparando o envio das respostas...",
        resultado={"modo": modo, "id_aluno": id_aluno_filtrado},
        tipo="warning",
        titulo="Envio iniciado",
    )

    # Cada vetor é enviado individualmente para que o percentual represente
    # exatamente os itens já confirmados pela API externa.
    tamanho_lote_envio = 1
    for inicio_lote in range(0, total_itens, tamanho_lote_envio):
        lote = itens_envio[inicio_lote:inicio_lote + tamanho_lote_envio]
        preparados = []
        for deslocamento, (nome_imagem, dados_cartao, validacao) in enumerate(lote, start=1):
            indice = inicio_lote + deslocamento
            total += 1
            try:
                if not _texto_limpo(validacao.get("id_final")) and not _texto_limpo(validacao.get("codigo_barras_final")):
                    raise ValueError("Cartão sem ID de aluno validado para envio.")
                nome_original_real = _obter_nome_original_limpo(nome_imagem, dados_cartao)
                nome_arquivo = _formatar_stem_arquivo_keepedu(nome_original_real)
                aluno = _montar_aluno_payload(nome_imagem, dados_cartao, validacao, id_aval_final, nome_arquivo, modelo_ia)
                preparados.append((indice, nome_imagem, nome_original_real, nome_arquivo, aluno))
            except Exception as exc:
                motivo = _resumir_motivo_falha_importacao(exc=exc)
                mensagens.append(f"Erro ao processar {nome_imagem}: {motivo}")
                detalhes.append({"imagem": nome_imagem, "status_code": 0, "resposta": {}, "sucesso": False, "modelo_ia": modelo_ia})
                _emitir_progresso_importacao(progresso_callback, int(5 + (indice / total_itens) * 88), f"Cartão {indice} de {total_itens} não pôde ser enviado.", nome_imagem, tipo="error", titulo=f"Cartão {indice}/{total_itens} pendente", descricao=_descricao_resultado_item(nome_imagem, False, motivo, texto_sucesso="", prefixo_falha="Falha ao enviar respostas de"), atualizar_evento=True)

        if not preparados:
            continue

        primeiro_indice, _, primeiro_nome_real, _, _ = preparados[0]
        ultimo_indice = preparados[-1][0]
        _emitir_progresso_importacao(
            progresso_callback,
            round(((primeiro_indice - 1) / total_itens) * 100),
            f"Enviando respostas dos cartões {primeiro_indice} a {ultimo_indice} de {total_itens}...",
            primeiro_nome_real,
            resultado={
                "arquivos_lote": [nome_real for _, _, nome_real, _, _ in preparados],
                "inicio_lote": primeiro_indice,
                "fim_lote": ultimo_indice,
            },
            tipo="warning",
            titulo=f"Enviando cartões {primeiro_indice}/{total_itens}",
            descricao=f"Lote de {len(preparados)} vetores em envio.",
        )
        payload_envio = {**payload_base, "alunos": [aluno for _, _, _, _, aluno in preparados]}
        if modo == "simulacao":
            status_code, corpo_resposta = processar_mock_importacao_keepedu(payload_envio)
        else:
            status_code, corpo_resposta = _post_keepedu(url_destino, payload_envio, cabecalhos)
        sucesso_lote = _sucesso_resposta_generica(status_code, corpo_resposta)
        # Em timeout/erro de transporte não sabemos se o KeepEdu recebeu o lote.
        # Não classifique como erro definitivo nem reenvie automaticamente.
        confirmacao_pendente = status_code == 0
        motivo_lote = _resumir_motivo_falha_importacao(status_code, corpo_resposta)

        possui_falhas_individuais = bool(_mensagens_resposta(corpo_resposta))
        for indice, nome_imagem, nome_original_real, nome_arquivo, aluno in preparados:
            erro_individual = _mensagem_erro_do_item_keepedu(corpo_resposta, nome_arquivo, aluno)
            fora_avaliacao = bool(erro_individual) and _resposta_indica_aluno_fora_avaliacao({"mensagem": erro_individual})
            sucesso_item = (sucesso_lote or (200 <= status_code < 300 and possui_falhas_individuais and not erro_individual)) and not fora_avaliacao and not erro_individual
            motivo_item = ""
            if confirmacao_pendente:
                motivo_item = "Confirmação pendente: a conexão foi interrompida após o envio; verifique no KeepEdu antes de reenviar."
                confirmacoes_pendentes += 1
            elif fora_avaliacao:
                motivo_item = _motivo_pendencia_avaliacao(corpo_resposta, nome_arquivo)
                registrar_pendencia_importacao(nome_processamento, nome_imagem, "PENDENTE_ALUNO_FORA_AVALIACAO", motivo_item, detalhes={"idAval": id_aval_final, "resposta": corpo_resposta})
                validacao_atualizada = True
                pendencias_avaliacao.append({"imagem": nome_imagem, "nomeArquivo": nome_arquivo, "motivo": motivo_item})
            elif erro_individual:
                motivo_item = erro_individual
                mensagens.append(erro_individual)
            elif not sucesso_lote:
                motivo_item = motivo_lote
                mensagens.extend(_mensagens_resposta(corpo_resposta))
            elif limpar_pendencia_importacao(nome_processamento, nome_imagem):
                validacao_atualizada = True
            detalhes.append({"imagem": nome_imagem, "nomeArquivo": nome_arquivo, "barcode": aluno.get("barcode", ""), "idAluno": aluno.get("idAluno", ""), "payload": payload_envio, "status_code": status_code, "resposta": corpo_resposta, "sucesso": sucesso_item, "pendencia_avaliacao": fora_avaliacao, "modelo_ia": modelo_ia})
            if sucesso_item:
                importadas += 1
                registrar_envio_keepedu(nome_processamento, nome_imagem, vetores=True, origem=origem_envio, id_aval=id_aval_final)
            _emitir_progresso_importacao(
                progresso_callback,
                round((indice / total_itens) * 100),
                f"Cartão {indice} de {total_itens} processado.",
                nome_original_real,
                tipo="success" if sucesso_item else ("warning" if confirmacao_pendente else "error"),
                titulo=f"Cartão {indice}/{total_itens} concluído",
                descricao=_descricao_resultado_item(nome_original_real, sucesso_item, motivo_item, texto_sucesso="Respostas de {nome} enviadas com sucesso.", prefixo_falha="Confirmação necessária para" if confirmacao_pendente else "Falha ao enviar respostas de"),
                atualizar_evento=True,
                resultado=(
                    {"pendencias_avaliacao": [pendencias_avaliacao[-1]], "validacao_atualizada": True}
                    if fora_avaliacao else None
                ),
            )

    erros = total - importadas - confirmacoes_pendentes
    status = "ok" if erros == 0 and confirmacoes_pendentes == 0 else "parcial"

    relatorio = {
        "status": status,
        "success": status == "ok",
        "modo": modo,
        "url_destino": url_destino,
        "idAval": id_aval_final,
        "total": total,
        "importadas": importadas,
        "erros": erros,
        "confirmacoes_pendentes": confirmacoes_pendentes,
        "mensagem": mensagens,
        "arquivoErros": arquivos_erros,
        "pendencias_avaliacao": pendencias_avaliacao,
        "validacao_atualizada": validacao_atualizada,
        "detalhes": detalhes,
        "modelo_ia": modelo_ia,
    }

    _emitir_progresso_importacao(
        progresso_callback,
        percentual=100,
        mensagem=f"Envio concluído: {importadas} de {total} cartões enviados.",
        resultado=relatorio,
        tipo="success" if status == "ok" else "warning",
    )
    relatorio["arquivo_relatorio"] = _salvar_relatorio(nome_processamento, relatorio, modo=modo)
    return relatorio


# Função de Upload Binário Corrigida para injetar o Form-Data sem o prefixo do OMR
def importar_imagens_folha_resposta(
    nome_processamento: str,
    id_aval=None,
    progresso_callback=None,
    id_aluno=None,
    executar_limpeza: bool = True,
    origem_envio: str = "validacao",
):
    url_folha = resolve_keepedu_url(
        "avaliacoes/importar-folha-resposta",
        "KEEPEDU_IMPORTAR_FOLHA_RESPOSTA_URL",
    )
    if not url_folha:
        return {"status": "erro", "mensagem": "URL de envio de imagem não configurada no .env."}

    leituras = carregar_leituras(nome_processamento)
    dados_validacao = obter_validacao_cadastral_para_envio(nome_processamento)
    validacoes = dados_validacao.get("validacoes", {})
    id_aval_final = _detectar_id_aval(dados_validacao, id_aval)
    validacoes_manuais = carregar_validacao_manual(nome_processamento)

    itens_para_importar, _ = _selecionar_itens_importacao(
        leituras,
        validacoes,
        id_aluno,
        validacoes_manuais=validacoes_manuais,
    )

    if not itens_para_importar:
        return {"status": "ok", "total": 0, "importadas": 0, "erros": 0, "mensagem": ["Nenhuma imagem selecionada para upload."]}

    cabecalhos_upload = _cabecalhos_keepedu_upload()
    url_folha_resposta = _montar_url_folha_resposta(url_folha, id_aval_final)

    total, importadas, erros = 0, 0, 0
    mensagens = []
    imagens_enviadas_com_sucesso = []

    _emitir_progresso_importacao(
        progresso_callback,
        percentual=5,
        mensagem=f"Iniciando upload de {len(itens_para_importar)} imagem(ns) original(is)...",
        tipo="warning",
        titulo="Envio de imagens iniciado",
        descricao=f"{len(itens_para_importar)} imagem(ns) na fila.",
    )

    total_itens = len(itens_para_importar)

    for indice, (nome_imagem, dados_cartao, _) in enumerate(itens_para_importar, start=1):
        # A API de folha-resposta recebe um arquivo por chamada. Organizamos as
        # chamadas em blocos de 10 para controlar o ritmo do envio e deixar o
        # progresso explícito no modal, sem disparar toda a fila de uma vez.
        if (indice - 1) % KEEPEDU_IMPORTAR_LOTE_TAMANHO == 0:
            fim_lote = min(indice + KEEPEDU_IMPORTAR_LOTE_TAMANHO - 1, total_itens)
            _emitir_progresso_importacao(
                progresso_callback,
                percentual=int(5 + ((indice - 1) / total_itens) * 88),
                mensagem=f"Enviando lote de imagens {indice} a {fim_lote} de {total_itens}...",
                tipo="warning",
                titulo=f"Lote de imagens {indice}-{fim_lote}/{total_itens}",
                descricao=f"Bloco de até {KEEPEDU_IMPORTAR_LOTE_TAMANHO} imagens em envio.",
            )
        total += 1
        sucesso_item = False
        motivo_item = ""

        if total_itens:
            percentual_item = int(5 + ((indice - 1) / total_itens) * 88)
            _emitir_progresso_importacao(
                progresso_callback,
                percentual=percentual_item,
                mensagem=f"Enviando imagem do cartão {indice} de {total_itens}...",
                arquivo_atual=_obter_nome_original_limpo(nome_imagem, dados_cartao),
                tipo="warning",
                titulo=f"Enviando imagem {indice}/{total_itens}",
                descricao="Enviando a imagem original. Aguarde a confirmação.",
            )

        nome_original_real = _obter_nome_original_limpo(
            nome_imagem,
            dados_cartao
        )

        nome_arquivo_formatado = _formatar_nome_arquivo_imagem_keepedu(nome_original_real)

        print("\n===== IMAGEM =====")
        print("nome_original_real:", nome_original_real)
        print("nome_arquivo_keepedu:", nome_arquivo_formatado)
        print("=================\n")

        caminho_imagem_original = localizar_imagem_original(
            nome_processamento,
            nome_imagem,
            leituras
        )

        if caminho_imagem_original and caminho_imagem_original.exists():

            status_code, corpo_resposta = _post_keepedu_folha_resposta(
                url_folha_resposta,
                caminho_imagem_original,
                id_aval_final,
                cabecalhos_upload,
                nome_arquivo_limpo=nome_arquivo_formatado
            )

            if _sucesso_resposta_generica(status_code, corpo_resposta):
                sucesso_item = True
                importadas += 1
                imagens_enviadas_com_sucesso.append(nome_original_real)
                registrar_envio_keepedu(
                    nome_processamento,
                    nome_imagem,
                    imagens=True,
                    origem=origem_envio,
                    id_aval=id_aval_final,
                )
            else:
                erros += 1
                mensagens.extend(_mensagens_resposta(corpo_resposta))
                motivo_item = _resumir_motivo_falha_importacao(status_code, corpo_resposta)

        else:
            erros += 1
            motivo_item = f"arquivo físico não localizado ({nome_arquivo_formatado})"
            mensagens.append(f"{nome_imagem}: {motivo_item}")

        if total_itens:
            percentual_item = int(5 + (indice / total_itens) * 88)
            tipo_item = "success" if sucesso_item else "error"
            _emitir_progresso_importacao(
                progresso_callback,
                percentual=percentual_item,
                mensagem=f"Imagem do cartão {indice} de {total_itens} processada.",
                arquivo_atual=nome_original_real,
                tipo=tipo_item,
                titulo=f"Imagem {indice}/{total_itens} concluída",
                descricao=_descricao_resultado_item(
                    nome_original_real,
                    sucesso_item,
                    motivo_item,
                    texto_sucesso="Upload de {nome} concluído com sucesso.",
                    prefixo_falha="Falha no upload de",
                ),
                atualizar_evento=True,
            )

    if executar_limpeza and imagens_enviadas_com_sucesso:
        _emitir_progresso_importacao(
            progresso_callback,
            percentual=95,
            mensagem="Finalizando envio e limpando arquivos locais...",
            tipo="warning",
        )
        limpar_pos_importacao_keepedu(nome_processamento, imagens_enviadas=imagens_enviadas_com_sucesso)

    _emitir_progresso_importacao(
        progresso_callback,
        percentual=100,
        mensagem=f"Envio de imagens concluído: {importadas} de {total} cartões enviados.",
        tipo="success" if erros == 0 else "warning",
    )
    return {"status": "ok" if erros == 0 else "parcial", "total": total, "importadas": importadas, "erros": erros, "mensagem": mensagens}


def _validar_cartao_para_envio(nome_processamento: str, nome_imagem: str) -> tuple[dict | None, dict | None, str | None]:
    dados_validacao = obter_validacao_cadastral_para_envio(nome_processamento)
    validacao_imagem = dados_validacao.get("validacoes", {}).get(nome_imagem)

    if not validacao_imagem:
        return None, None, "Imagem não encontrada na validação cadastral."

    leituras = carregar_leituras(nome_processamento)
    leitura = leituras.get(nome_imagem) or {}
    if leitura_bloqueada_id_avaliacao(leitura):
        return (
            None,
            None,
            "Cartao com ID de avaliacao diferente do ID informado no processamento. "
            "Corrija ou reprocesse o lote antes de enviar.",
        )
    if not leitura.get("pontos_cantos"):
        return None, None, "A imagem precisa ser processada antes do envio."

    id_aluno = validacao_imagem.get("id_final")
    if not id_aluno:
        return None, None, "A imagem não possui um ID de aluno validado."

    return validacao_imagem, leitura, None


def _resposta_apenas_imagens(resultado_imagens: dict) -> dict:
    erros_imagens = int(resultado_imagens.get("erros") or 0)
    if erros_imagens > 0:
        mensagens = resultado_imagens.get("mensagem") or []
        if isinstance(mensagens, list):
            texto = " | ".join(str(item) for item in mensagens if item)
        else:
            texto = str(mensagens)
        return {
            "status": "erro",
            "mensagem": texto or "Ocorreram erros durante o envio da imagem.",
            "imagens": resultado_imagens,
        }

    return {
        "status": "ok" if resultado_imagens.get("status") != "erro" else "parcial",
        "imagens": resultado_imagens,
    }


def enviar_imagem_keepedu(nome_processamento: str, nome_imagem: str, id_aval=None) -> dict:
    validacao_imagem, _, erro = _validar_cartao_para_envio(nome_processamento, nome_imagem)
    if erro:
        return {"status": "erro", "mensagem": erro}

    id_aluno = validacao_imagem.get("id_final")

    resultado_imagens = importar_imagens_folha_resposta(
        nome_processamento=nome_processamento,
        id_aval=id_aval,
        id_aluno=id_aluno,
        executar_limpeza=True,
    )
    return _resposta_apenas_imagens(resultado_imagens)


def enviar_todas_imagens_keepedu(nome_processamento: str, id_aval=None) -> dict:
    leituras = carregar_leituras(nome_processamento)
    dados_validacao = obter_validacao_cadastral_para_envio(nome_processamento)
    validacoes = dados_validacao.get("validacoes", {})

    elegiveis = [
        nome_imagem
        for nome_imagem, leitura in leituras.items()
        if leitura.get("pontos_cantos") and validacoes.get(nome_imagem, {}).get("id_final")
    ]

    if not elegiveis:
        return {
            "status": "erro",
            "mensagem": "Nenhuma imagem processada com ID validado para envio.",
            "total": 0,
            "importadas": 0,
            "erros": 0,
        }

    resultado_imagens = importar_imagens_folha_resposta(
        nome_processamento=nome_processamento,
        id_aval=id_aval,
        id_aluno=None,
        executar_limpeza=True,
    )

    erros = int(resultado_imagens.get("erros") or 0)
    status = "ok" if erros == 0 else ("parcial" if int(resultado_imagens.get("importadas") or 0) > 0 else "erro")

    return {
        "status": status,
        "total": int(resultado_imagens.get("total") or 0),
        "importadas": int(resultado_imagens.get("importadas") or 0),
        "erros": erros,
        "mensagem": resultado_imagens.get("mensagem") or [],
        "imagens": resultado_imagens,
    }


def enviar_cartao_keepedu(nome_processamento: str, nome_imagem: str, id_aval=None, modelo_ia=None) -> dict:
    validacao_imagem, _, erro = _validar_cartao_para_envio(nome_processamento, nome_imagem)
    if erro:
        return {"status": "erro", "mensagem": erro}

    id_aluno = validacao_imagem.get("id_final")

    resultado_vetores = importar_respostas_presenciais(
        nome_processamento,
        id_aval=id_aval,
        id_aluno=id_aluno,
        modelo_ia=modelo_ia,
        origem_envio="correcao",
    )

    if resultado_vetores.get("status") == "erro":
        return {"status": "erro", "mensagem": resultado_vetores.get("mensagem"), "vetores": resultado_vetores}

    resultado_imagens = importar_imagens_folha_resposta(
        nome_processamento=nome_processamento,
        id_aval=id_aval,
        id_aluno=id_aluno,
        executar_limpeza=True,
        origem_envio="correcao",
    )

    erros_vetores = int(resultado_vetores.get("erros") or 0)
    erros_imagens = int(resultado_imagens.get("erros") or 0)
    if erros_vetores > 0 or erros_imagens > 0:
        mensagens = []
        for origem in (resultado_vetores, resultado_imagens):
            bloco = origem.get("mensagem") or []
            if isinstance(bloco, list):
                mensagens.extend(str(item) for item in bloco if item)
            elif bloco:
                mensagens.append(str(bloco))
        return {
            "status": "erro",
            "mensagem": " | ".join(mensagens) or "Ocorreram erros durante o envio.",
            "vetores": resultado_vetores,
            "imagens": resultado_imagens,
        }

    sucesso_geral = resultado_vetores.get("status") != "erro" and resultado_imagens.get("status") != "erro"
    return {
        "status": "ok" if sucesso_geral else "parcial",
        "vetores": resultado_vetores,
        "imagens": resultado_imagens,
    }


def importar_respostas_presenciais(
    nome_processamento: str,
    id_aval=None,
    dia_aval=None,
    usuario_id=None,
    id_aluno=None,
    progresso_callback=None,
    modelo_ia=None,
    origem_envio: str = "validacao",
):
    return _executar_importacao_respostas_presenciais(
        nome_processamento=nome_processamento,
        url_destino=resolve_keepedu_url(
            "avaliacoes/importar-respostas-presenciais",
            "KEEPEDU_IMPORTAR_RESPOSTAS_URL",
        ),
        modo="importacao",
        id_aval=id_aval, dia_aval=dia_aval, usuario_id=usuario_id, id_aluno=id_aluno, progresso_callback=progresso_callback, modelo_ia=modelo_ia,
        origem_envio=origem_envio,
    )


def simular_importacao_respostas_presenciais(
    nome_processamento: str,
    id_aval=None,
    dia_aval=None,
    usuario_id=None,
    id_aluno=None,
    progresso_callback=None,
    modelo_ia=None,
    origem_envio: str = "validacao",
):
    return _executar_importacao_respostas_presenciais(
        nome_processamento=nome_processamento,
        url_destino=resolve_keepedu_url(
            "avaliacoes/importar-respostas-presenciais",
            "KEEPEDU_IMPORTAR_RESPOSTAS_URL",
        ),
        modo="simulacao",
        id_aval=id_aval, dia_aval=dia_aval, usuario_id=usuario_id, id_aluno=id_aluno, progresso_callback=progresso_callback, modelo_ia=modelo_ia,
        origem_envio=origem_envio,
    )


def caminho_lista_frequencia(nome_processamento: str) -> Path:
    caminho_leitura = caminho_leituras(nome_processamento)
    return caminho_leitura.parent / "lista_frequencia_keepedu.pdf"


def salvar_lista_frequencia_pdf(nome_processamento: str, conteudo: bytes) -> Path:
    caminho = caminho_lista_frequencia(nome_processamento)
    caminho.parent.mkdir(parents=True, exist_ok=True)
    with open(caminho, "wb") as arquivo:
        arquivo.write(conteudo)
    return caminho


def _post_keepedu_lista_frequencia(
    url_destino: str,
    caminho_arquivo: Path,
    id_simulado,
    cabecalhos: dict,
) -> tuple[int, dict]:
    content_type = mimetypes.guess_type(caminho_arquivo.name)[0] or "application/pdf"

    with open(caminho_arquivo, "rb") as arquivo:
        resposta_api = requests.post(
            url_destino,
            data={"idSimulado": str(id_simulado)},
            files={
                "lista_de_frequencia": (
                    caminho_arquivo.name,
                    arquivo,
                    content_type,
                )
            },
            headers=cabecalhos,
            timeout=KEEPEDU_IMPORTAR_TIMEOUT_SECONDS,
        )

    print("\n========== KEEPEDU FREQUÊNCIA ==========")
    print("URL:", url_destino)
    print("ID SIMULADO:", id_simulado)
    print("ARQUIVO:", caminho_arquivo.name)
    print("STATUS:", resposta_api.status_code)
    print("BODY:", resposta_api.text)
    print("========================================\n")

    status_code = resposta_api.status_code
    try:
        corpo_resposta = resposta_api.json()
    except ValueError:
        corpo_resposta = {
            "success": 200 <= status_code < 300,
            "mensagem": [_resumir_resposta_http_nao_json(status_code, resposta_api, "Importação de frequência")],
        }
    return status_code, corpo_resposta


def importar_lista_frequencia_keepedu(
    nome_processamento: str,
    id_simulado=None,
    caminho_arquivo: str | Path | None = None,
    progresso_callback=None,
) -> dict:
    url_lista = resolve_keepedu_url(
        "avaliacoes/importar-lista-de-frequencia",
        "KEEPEDU_IMPORTAR_LISTA_FREQUENCIA_URL",
    )
    if not url_lista:
        return {
            "status": "erro",
            "mensagem": "KEEPEDU_IMPORTAR_LISTA_FREQUENCIA_URL não está configurada.",
        }

    _emitir_progresso_importacao(
        progresso_callback,
        percentual=8,
        mensagem="Validando arquivo de frequência...",
        tipo="warning",
        titulo="Envio de frequência iniciado",
        descricao="Preparando o PDF para transmissão.",
    )

    dados_validacao = obter_validacao_cadastral_para_envio(nome_processamento)
    id_simulado_final = _detectar_id_aval(dados_validacao, id_simulado)

    if not id_simulado_final:
        return {
            "status": "erro",
            "mensagem": "Não foi possível identificar o ID do simulado para o envio de frequências.",
        }

    if not caminho_arquivo:
        return {
            "status": "erro",
            "mensagem": "Informe o arquivo PDF da lista de frequência.",
        }

    caminho_path = Path(caminho_arquivo)
    if not caminho_path.exists():
        return {
            "status": "erro",
            "mensagem": "Arquivo de frequência não encontrado.",
        }

    if caminho_path.suffix.lower() != ".pdf":
        return {
            "status": "erro",
            "mensagem": "A lista de frequência deve estar em formato PDF.",
        }

    _emitir_progresso_importacao(
        progresso_callback,
        percentual=35,
        mensagem="Enviando PDF para a KeepEdu...",
        arquivo_atual=caminho_path.name,
        tipo="warning",
        titulo="Transmitindo lista",
        descricao=f"Enviando {caminho_path.name} ao simulado {id_simulado_final}.",
    )

    status_code, corpo_resposta = _post_keepedu_lista_frequencia(
        url_lista,
        caminho_path,
        id_simulado_final,
        _cabecalhos_keepedu_upload(),
    )

    sucesso = _sucesso_resposta_generica(status_code, corpo_resposta)
    mensagens = _mensagens_resposta(corpo_resposta)
    mensagem_final = mensagens[0] if mensagens else (
        f"Lista de frequência ({caminho_path.name}) enviada com sucesso."
        if sucesso
        else "A KeepEdu recusou o envio da lista de frequência."
    )
    _emitir_progresso_importacao(
        progresso_callback,
        percentual=100,
        mensagem=mensagem_final,
        arquivo_atual=caminho_path.name,
        tipo="success" if sucesso else "error",
        titulo="Envio concluído" if sucesso else "Falha no envio",
        descricao=mensagem_final,
    )
    relatorio = {
        "status": "ok" if sucesso else "erro",
        "idSimulado": id_simulado_final,
        "arquivo": str(caminho_path),
        "nome_arquivo": caminho_path.name,
        "http_status": status_code,
        "resposta_keepedu": corpo_resposta,
        "mensagem": mensagem_final,
    }

    return relatorio
