import ast
import secrets
from urllib.parse import quote, unquote
from app.gerador_csv import gerar_csv_final, caminho_csv_final
from app.bernoulli import transformar_vetor_keepedu_para_bernoulli, EXTENSOES_PLANILHA, PASTA_BERNOULLI
from app.keepedu_importacao import (
    contar_alunos_importacao,
    contar_imagens_importacao,
    enviar_cartao_keepedu,
    enviar_imagem_keepedu,
    enviar_todas_imagens_keepedu,
    importar_imagens_folha_resposta,
    importar_lista_frequencia_keepedu,
    salvar_lista_frequencia_pdf,
    importar_respostas_presenciais,
    processar_mock_importacao_keepedu,
    simular_importacao_respostas_presenciais,
)
from app.keepedu_auth import autenticar_keepedu
from app.validacao_cadastral import (
    alterar_id_prova_do_lote,
    carregar_envios_keepedu,
    gerar_validacao_cadastral,
    leitura_bloqueada_id_avaliacao,
    resumo_status_lote_keepedu,
    salvar_correcao_manual,
    remover_correcao_manual,
)
import json
import os
from html import escape
from app.auth import gerenciar_lembre_me, verificar_autologin_cookie, remover_token_banco
from app.auth import (
    salvar_sessao_usuario,
    obter_sessao_usuario,
    remover_sessao_usuario
)

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = os.path.dirname(BASE_DIR)

from app.settings import (
    APP_ENABLE_AUTH,
    ID_PROVA_KEEPEDU,
    KEEPEDU_LOGIN_SCHOOL,
    KEEPEDU_LOGIN_URL,
    PASTA_PROCESSAMENTOS,
    PASTA_UPLOADS_TEMP,
    MAX_UPLOAD_FILES,
    MAX_FILE_SIZE_MB,
    MAX_TOTAL_UPLOAD_SIZE_MB
)
from app.progresso import criar_job, obter_job, atualizar_job, registrar_evento_job
from app.services import processar_pasta_temporaria_com_progresso
from app.avaliacao_contexto import (
    limpar_contexto_na_sessao,
    aplicar_contexto_na_sessao,
    contexto_avaliacao_da_sessao,
    contexto_exibicao_avaliacao,
    contexto_para_processamento,
    descricao_avaliacao_para_exibicao,
    listar_simulados_disponiveis,
)
from app.keepedu_avaliacoes import buscar_listagem_avaliacoes
from app.keepedu_cadastros import (
    listar_series_por_unidade,
    listar_turmas_por_serie,
    listar_unidades_keepedu,
    listar_usuarios_keepedu,
)

from fastapi import APIRouter, Request, UploadFile, File, Form, Response, BackgroundTasks
from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse, JSONResponse, Response
from pydantic import BaseModel
from pathlib import Path

from app.services import (
    carregar_json,
    caminho_leituras,
    caminho_originais,
    caminho_pasta_processamento,
    listar_processamentos,
    listar_processamentos_recentes,
    listar_imagens_debug,
    localizar_imagem_original,
    resumo_processamento,
    processar_pasta_local,
    processar_uploads,
    caminho_imagem_debug,
    caminho_log,
    caminho_correcoes_web,
    reprocessar_imagem_processamento,
    deletar_leitura_processamento,
    deletar_processamento,
    reprocessar_lote_com_progresso,
    resolver_chave_leitura,
)
from app.historico_pos_processamento import (
    filtrar_e_paginar_lotes_historico,
    gerar_slug_historico_lote,
    gerar_visualizacao_cartao_temporaria,
    gerar_visualizacao_folha_keepedu_temporaria,
    montar_dados_pos_processamento,
    obter_identificacao_prova_lote,
    resolver_nome_lote_historico,
    resumo_estatisticas_globais_processamento,
)
from app.keepedu_folha_resposta import baixar_imagem_folha_resposta, ver_folha_resposta_keepedu
from app.alunos_client import buscar_aluno_por_ra
from app.validacao_cadastral import detectar_id_prova_do_processamento
from app.kanban import (
    automacao_envio_keepedu,
    automacao_inicio_vetores_keepedu,
    automacao_lote_aberto,
    automacao_processamento_iniciado,
    atualizar_card,
    atualizar_coluna,
    criar_coluna,
    excluir_card,
    excluir_coluna,
    historico_processamentos_do_card,
    historico_processamentos_por_referencia,
    pendencias_do_card,
    listar_etapas,
    montar_board,
    registrar_entrega,
)

router = APIRouter()
EXTENSOES_PERMITIDAS = {".jpg", ".jpeg", ".png"}

def descricao_avaliacao_exibicao(request: Request, nome_processamento: str = "") -> str:
    """Rótulo da avaliação exibido nas telas de lote (sessão do Painel ou meta do lote)."""
    return descricao_avaliacao_para_exibicao(request.session, nome_processamento)


def _contexto_avaliacao_ui(
    request: Request,
    nome_processamento: str = "",
    id_fallback: str = "",
) -> dict:
    exibicao = contexto_exibicao_avaliacao(
        request.session,
        nome_processamento=nome_processamento,
        id_fallback=id_fallback,
    )
    return {
        "id_avaliacao": exibicao.get("id_avaliacao") or id_fallback,
        "descricao_avaliacao": (
            exibicao.get("tipo_prova")
            or exibicao.get("nome_simulado")
            or descricao_avaliacao_exibicao(request, nome_processamento)
        ),
        "avaliacao_exibicao": exibicao,
    }


def _contexto_painel(request: Request) -> dict:
    listagem = buscar_listagem_avaliacoes()
    ctx_sessao = contexto_avaliacao_da_sessao(request.session)
    stats = resumo_estatisticas_globais_processamento()
    avaliacoes_total = int(listagem.get("total") or 0)
    return {
        "avaliacao_contexto": ctx_sessao,
        "avaliacao_exibicao": contexto_exibicao_avaliacao(request.session),
        "simulados_disponiveis": listar_simulados_disponiveis(),
        "listagem_avaliacoes_erro": listagem.get("erro", ""),
        "listagem_avaliacoes_total": avaliacoes_total,
        "listagem_avaliacoes_ano": listagem.get("ano_avaliacao", ""),
        "estatisticas_painel": {
            "avaliacoes_total": avaliacoes_total,
            "avaliacoes_processadas": stats["avaliacoes_processadas"],
            "cartoes_processados": stats["cartoes_processados"],
        },
    }


class VerFolhaRespostaPayload(BaseModel):
    aluno_id: str | int | None = None
    simulado_id: str | int | None = None
    ra: str = ""
    chave_imagem: str = ""


class CorrecoesPayload(BaseModel):
    correcoes: dict


class ReprocessarImagemPayload(BaseModel):
    nome_imagem: str
    pontos_cantos: dict | None = None


class KanbanEntregaPayload(BaseModel):
    titulo: str
    id_avaliacao: str
    responsavel_entrega: str
    responsavel_correcao: str = ""
    total_cartoes: int = 0
    data_entrega: str = ""
    data_previsao_entrega: str = ""
    unidade: str = ""
    serie: str = ""
    turma: str = ""


class KanbanCardPayload(BaseModel):
    etapa: str | None = None
    posicao: int | None = None
    titulo: str | None = None
    id_avaliacao: str | None = None
    responsavel_entrega: str | None = None
    responsavel_correcao: str | None = None
    total_cartoes: int | None = None
    data_entrega: str | None = None
    data_previsao_entrega: str | None = None
    unidade: str | None = None
    serie: str | None = None
    turma: str | None = None
    status_prazo: str | None = None


class KanbanColunaPayload(BaseModel):
    slug: str | None = None
    titulo: str | None = None
    cor: str | None = None
    ordem: int | None = None
    eh_inicial: bool | None = None
    eh_final: bool | None = None
    ativo: bool | None = None


class KanbanAutomacaoProcessamentoPayload(BaseModel):
    id_avaliacao: str
    nome_processamento: str = ""
    usuario: str = ""
    total_cartoes: int | None = None


class KanbanAutomacaoLotePayload(BaseModel):
    nome_processamento: str
    id_avaliacao: str = ""


class DeletarLeituraPayload(BaseModel):
    nome_imagem: str


class AlterarIdProvaPayload(BaseModel):
    id_prova_novo: str
    id_prova_antigo: str = ""


class ImportarImagemPayload(BaseModel):
    nome_imagem: str | None = None
    id_aluno: str | int | None = None
    id_aval: str | int | None = None
    modelo_ia: str | None = None


class ImportarTodasKeepEduPayload(BaseModel):
    id_aval: str | int | None = None
    modelo_ia: str | None = None


class ImportarRespostasPayload(BaseModel):
    idAval: str | int | None = None
    diaAval: str | int | None = None
    usuario_id: str | int | None = None
    idAluno: str | int | None = None
    nome_processamento: str | None = None
    modelo_ia : str | None = None


def listar_processamentos_local():
    return listar_processamentos()

@router.get("/historico", response_class=HTMLResponse)
@router.get("/historico/", response_class=HTMLResponse)
async def historico_processos(request: Request, q: str = "", page: int = 1):
    paginacao = filtrar_e_paginar_lotes_historico(query=q, page=page)
    return render_template(
        request,
        "historico_processos.html",
        {
            **paginacao,
            "avaliacoes": paginacao.get("avaliacoes") or paginacao.get("lotes") or [],
            **_contexto_avaliacao_ui(request),
        },
    )


def _resolver_lote_historico(identificador: str) -> str | None:
    ident = extrair_nome_lote_seguro(identificador)
    nome = resolver_nome_lote_historico(ident)
    if nome:
        return nome
    if montar_dados_pos_processamento(ident):
        return ident
    return None


@router.post("/historico/{nome_processamento}/excluir")
async def historico_excluir_lote(nome_processamento: str):
    nome_limpo = _resolver_lote_historico(nome_processamento)
    if not nome_limpo:
        return JSONResponse(
            {"status": "erro", "erro": "Lote não encontrado."},
            status_code=404,
        )
    try:
        return JSONResponse(deletar_processamento(nome_limpo))
    except FileNotFoundError:
        return JSONResponse(
            {"status": "erro", "erro": "Lote não encontrado."},
            status_code=404,
        )
    except ValueError as exc:
        return JSONResponse(
            {"status": "erro", "erro": str(exc)},
            status_code=400,
        )
    except Exception as exc:
        return JSONResponse(
            {"status": "erro", "erro": str(exc)},
            status_code=500,
        )


@router.post("/historico/{nome_processamento}/ver-folha-resposta")
async def historico_ver_folha_resposta(
    nome_processamento: str,
    payload: VerFolhaRespostaPayload,
):
    nome_limpo = _resolver_lote_historico(nome_processamento)
    if not nome_limpo:
        return JSONResponse({"status": "erro", "erro": "Lote não encontrado."}, status_code=404)

    aluno_id = str(payload.aluno_id or "").strip()
    if not aluno_id or not str(aluno_id).isdigit():
        ra = str(payload.ra or "").strip()
        if ra and ra != "—":
            consulta = buscar_aluno_por_ra(ra)
            if consulta.get("encontrado") and consulta.get("id"):
                aluno_id = str(consulta["id"]).strip()

    simulado_id = str(payload.simulado_id or "").strip()
    if not simulado_id or not simulado_id.isdigit():
        identificacao = obter_identificacao_prova_lote(nome_limpo)
        simulado_id = str(identificacao.get("id_avaliacao") or "").strip()

    resultado = ver_folha_resposta_keepedu(aluno_id, simulado_id)
    if not resultado.get("ok"):
        return JSONResponse(
            {"status": "erro", "erro": resultado.get("erro") or "Falha ao consultar a folha."},
            status_code=400,
        )

    chave = str(payload.chave_imagem or "").strip()
    fallback_url = ""
    if chave:
        caminho_local = gerar_visualizacao_cartao_temporaria(nome_limpo, chave)
        if caminho_local and caminho_local.exists():
            fallback_url = f"/historico/{nome_limpo}/visualizar/{quote(chave, safe='')}"

    resposta: dict = {
        "status": "ok",
        "success": bool(resultado.get("success")),
        "imagem_url": "",
        "mensagem": resultado.get("mensagem") or "",
        "aluno_id": resultado.get("aluno_id"),
        "simulado_id": resultado.get("simulado_id"),
        "fonte": "",
        "fallback_url": fallback_url,
    }

    if resultado.get("success") and resultado.get("aluno_id") and resultado.get("simulado_id"):
        params = f"aluno_id={resultado['aluno_id']}&simulado_id={resultado['simulado_id']}"
        if chave:
            params += f"&chave={quote(chave, safe='')}"
        resposta["imagem_url"] = f"/historico/{nome_limpo}/folha-keepedu?{params}"
        resposta["fonte"] = "keepedu"
    elif fallback_url:
        resposta["fonte"] = "local"

    return JSONResponse(resposta)


@router.get("/historico/{nome_processamento}/folha-keepedu")
async def historico_folha_keepedu_imagem(
    nome_processamento: str,
    aluno_id: str = "",
    simulado_id: str = "",
    chave: str = "",
):
    nome_limpo = _resolver_lote_historico(nome_processamento)
    if not nome_limpo:
        return JSONResponse({"erro": "Lote não encontrado."}, status_code=404)

    chave_limpa = unquote(str(chave or "").strip())
    if chave_limpa:
        caminho = gerar_visualizacao_folha_keepedu_temporaria(
            nome_limpo,
            chave_limpa,
            aluno_id,
            simulado_id,
        )
        if caminho and caminho.exists():
            return FileResponse(
                str(caminho),
                media_type="image/jpeg",
                headers={"Cache-Control": "no-store, max-age=0"},
            )

    resultado = ver_folha_resposta_keepedu(aluno_id, simulado_id)
    if not resultado.get("ok"):
        return JSONResponse(
            {"erro": resultado.get("erro") or "Falha ao consultar a folha no KeepEdu."},
            status_code=400,
        )
    if not resultado.get("success") or not resultado.get("imagem_url"):
        return JSONResponse(
            {"erro": resultado.get("mensagem") or "Folha de resposta não encontrada no KeepEdu."},
            status_code=404,
        )

    try:
        conteudo, content_type = baixar_imagem_folha_resposta(resultado["imagem_url"])
    except ValueError as exc:
        return JSONResponse({"erro": str(exc)}, status_code=502)

    return Response(
        content=conteudo,
        media_type=content_type,
        headers={"Cache-Control": "no-store, max-age=0"},
    )


@router.get("/historico/{nome_processamento}/visualizar/{chave_imagem:path}")
async def historico_visualizar_cartao(nome_processamento: str, chave_imagem: str):
    nome_limpo = _resolver_lote_historico(nome_processamento)
    if not nome_limpo:
        return JSONResponse({"erro": "Lote não encontrado."}, status_code=404)
    caminho = gerar_visualizacao_cartao_temporaria(nome_limpo, chave_imagem)
    if not caminho or not caminho.exists():
        return JSONResponse({"erro": "Visualização não disponível para este cartão."}, status_code=404)
    return FileResponse(
        str(caminho),
        media_type="image/jpeg",
        headers={"Cache-Control": "no-store, max-age=0"},
    )


@router.get("/historico/{nome_processamento}", response_class=HTMLResponse)
async def historico_pos_processamento(request: Request, nome_processamento: str):
    nome_limpo = _resolver_lote_historico(nome_processamento)
    if not nome_limpo:
        return HTMLResponse(
            "<h1>Processamento não encontrado.</h1>"
            "<p>O lote solicitado não possui arquivos de histórico.</p>",
            status_code=404,
        )

    slug_canonico = gerar_slug_historico_lote(nome_limpo)
    ident_entrada = extrair_nome_lote_seguro(nome_processamento)
    if (
        not ident_entrada.startswith("processamento_")
        and ident_entrada != slug_canonico
    ):
        return RedirectResponse(url=f"/historico/{slug_canonico}", status_code=303)

    dados = montar_dados_pos_processamento(nome_limpo)
    if not dados:
        return HTMLResponse(
            "<h1>Processamento não encontrado.</h1>"
            "<p>O lote solicitado não possui arquivos de histórico.</p>",
            status_code=404,
        )

    identificacao = dados.get("identificacao") or {}
    return render_template(
        request,
        "historico_pos_processamento.html",
        {
            "nome_processamento": nome_limpo,
            "slug_historico": slug_canonico,
            "dados": dados,
            "historico_detalhe": True,
            **_contexto_avaliacao_ui(
                request,
                nome_limpo,
                identificacao.get("id_avaliacao") or "",
            ),
        },
    )


@router.get("/validacao/")
@router.get("/validacao")
async def tratar_validacao_vazia():
    lotes = listar_processamentos_local()
    if lotes:
        return RedirectResponse(url=f"/validacao/{lotes[0]}", status_code=303)
    return RedirectResponse(url="/historico", status_code=303)

def carregar_leitura(nome_avaliacao: str, em_json: bool = True):
    """
    Resolve o arquivo principal do lote a partir do diretório configurado.
    """
    caminho_principal = caminho_leituras(nome_avaliacao)
    if caminho_principal.exists():
        return caminho_principal

    nome_puro = nome_avaliacao.replace("processamento_", "")
    candidatos = [
        PASTA_PROCESSAMENTOS / nome_avaliacao / "leituras_omr.json",
        PASTA_PROCESSAMENTOS / nome_puro / "leituras_omr.json",
        PASTA_PROCESSAMENTOS / f"{nome_avaliacao}.json",
        PASTA_PROCESSAMENTOS / f"{nome_puro}.json",
    ]

    for candidato in candidatos:
        if candidato.exists() and candidato.is_file():
            return candidato

    return caminho_principal


def render_template(request: Request, nome_template: str, contexto: dict, status_code: int = 200):
    resposta = request.app.state.templates.TemplateResponse(
        request,
        nome_template,
        contexto,
        status_code=status_code
    )
    resposta.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
    resposta.headers["Pragma"] = "no-cache"
    resposta.headers["Expires"] = "0"
    return resposta


def _destino_pos_login(destino: str | None) -> str:
    texto = str(destino or "").strip()
    if not texto.startswith("/") or texto.startswith("//"):
        return "/"
    return texto


def _resultado_job_imagens(resultado: dict | None):
    if not isinstance(resultado, dict):
        return {}
    return {
        "status": resultado.get("status", ""),
        "total": int(resultado.get("total", 0) or 0),
        "importadas": int(resultado.get("importadas", 0) or 0),
        "erros": int(resultado.get("erros", 0) or 0),
        "mensagem": resultado.get("mensagem", []),
    }


def _executar_envio_imagens_em_background(
    job_id: str,
    nome_avaliacao: str,
    id_aval,
    id_aluno=None,
    executar_limpeza: bool = False,
):
    def progresso_callback(
        percentual,
        mensagem,
        arquivo_atual="",
        resultado=None,
        tipo=None,
        titulo=None,
        descricao="",
        atualizar_evento=False,
    ):
        atualizar_job(
            job_id,
            status="processando",
            percentual=percentual,
            mensagem=mensagem,
            arquivo_atual=arquivo_atual,
            resultado=_resultado_job_imagens(resultado),
        )
        if titulo:
            registrar_evento_job(
                job_id,
                tipo or "info",
                titulo,
                descricao or mensagem,
                arquivo=arquivo_atual,
                atualizar_por_arquivo=atualizar_evento,
            )

    try:
        atualizar_job(
            job_id,
            status="processando",
            percentual=1,
            mensagem="Preparando envio de imagens...",
        )
        resultado = importar_imagens_folha_resposta(
            nome_processamento=nome_avaliacao,
            id_aval=id_aval,
            id_aluno=id_aluno,
            progresso_callback=progresso_callback,
            executar_limpeza=executar_limpeza,
        )
        resultado_job = _resultado_job_imagens(resultado)
        status_resultado = resultado.get("status")

        if status_resultado == "erro" and not int(resultado.get("importadas") or 0):
            atualizar_job(
                job_id,
                status="erro",
                percentual=100,
                mensagem="Envio de imagens encerrado com erro.",
                erro=" | ".join((resultado.get("mensagem") or [])[:3]) or "Falha no envio de imagens.",
                resultado=resultado_job,
            )
            return

        atualizar_job(
            job_id,
            status="concluido",
            percentual=100,
            mensagem=f"Envio de imagens concluído ({resultado.get('importadas', 0)}/{resultado.get('total', 0)}).",
            resultado=resultado_job,
        )
        if int(resultado.get("importadas", 0) or 0) > 0:
            try:
                automacao_envio_keepedu(
                    nome_avaliacao,
                    str(id_aval or ""),
                )
            except Exception:
                pass
    except Exception as e:
        atualizar_job(
            job_id,
            status="erro",
            percentual=100,
            mensagem="Erro durante o envio de imagens.",
            erro=str(e),
        )


def _resultado_job_frequencia(resultado: dict | None):
    if not isinstance(resultado, dict):
        return {}
    return {
        "status": resultado.get("status", ""),
        "idSimulado": resultado.get("idSimulado"),
        "nome_arquivo": resultado.get("nome_arquivo", ""),
        "mensagem": resultado.get("mensagem", ""),
    }


def _executar_envio_frequencia_em_background(
    job_id: str,
    nome_avaliacao: str,
    id_simulado: str,
    caminho_arquivo: str,
):
    def progresso_callback(
        percentual,
        mensagem,
        arquivo_atual="",
        resultado=None,
        tipo=None,
        titulo=None,
        descricao="",
        atualizar_evento=False,
    ):
        atualizar_job(
            job_id,
            status="processando",
            percentual=percentual,
            mensagem=mensagem,
            arquivo_atual=arquivo_atual,
            resultado=_resultado_job_frequencia(resultado),
        )
        if titulo:
            registrar_evento_job(
                job_id,
                tipo or "warning",
                titulo,
                descricao or mensagem,
                arquivo=arquivo_atual,
                atualizar_por_arquivo=atualizar_evento,
            )

    try:
        atualizar_job(
            job_id,
            status="processando",
            percentual=1,
            mensagem="Preparando envio de frequência...",
        )
        resultado = importar_lista_frequencia_keepedu(
            nome_avaliacao,
            id_simulado=id_simulado,
            caminho_arquivo=caminho_arquivo,
            progresso_callback=progresso_callback,
        )
        resultado_job = _resultado_job_frequencia(resultado)
        if resultado.get("status") == "erro":
            atualizar_job(
                job_id,
                status="erro",
                percentual=100,
                mensagem=resultado.get("mensagem") or "Falha no envio de frequência.",
                erro=resultado.get("mensagem") or "Falha no envio de frequência.",
                resultado=resultado_job,
            )
            return

        atualizar_job(
            job_id,
            status="concluido",
            percentual=100,
            mensagem=resultado.get("mensagem") or "Lista de frequência enviada com sucesso.",
            resultado=resultado_job,
        )
    except Exception as e:
        atualizar_job(
            job_id,
            status="erro",
            percentual=100,
            mensagem="Erro durante o envio de frequência.",
            erro=str(e),
        )


def _resultado_job_importacao(resultado: dict | None):
    if not isinstance(resultado, dict):
        return {}
    return {
        "status": resultado.get("status", ""),
        "success": bool(resultado.get("success")),
        "modo": resultado.get("modo", ""),
        "idAval": resultado.get("idAval"),
        "id_aluno": resultado.get("id_aluno"),
        "total": int(resultado.get("total", 0) or 0),
        "importadas": int(resultado.get("importadas", 0) or 0),
        "erros": int(resultado.get("erros", 0) or 0),
        "mensagem": resultado.get("mensagem", []),
        "pendencias_avaliacao": resultado.get("pendencias_avaliacao", []),
        "validacao_atualizada": bool(resultado.get("validacao_atualizada")),
        "arquivo_relatorio": resultado.get("arquivo_relatorio", ""),
    }


def _executar_importacao_respostas_em_background(
    job_id: str,
    nome_avaliacao: str,
    payload: ImportarRespostasPayload,
    modo: str,
):
    def progresso_callback(
        percentual,
        mensagem,
        arquivo_atual="",
        resultado=None,
        tipo=None,
        titulo=None,
        descricao="",
        atualizar_evento=False,
    ):
        atualizar_job(
            job_id,
            status="processando",
            percentual=percentual,
            mensagem=mensagem,
            arquivo_atual=arquivo_atual,
            resultado=_resultado_job_importacao(resultado),
        )
        if titulo:
            registrar_evento_job(
                job_id,
                tipo or "info",
                titulo,
                descricao or mensagem,
                arquivo=arquivo_atual,
                atualizar_por_arquivo=atualizar_evento,
            )

    try:
        atualizar_job(
            job_id,
            status="processando",
            percentual=1,
            mensagem="Preparando envio de Vetores...",
        )
        registrar_evento_job(
            job_id,
            "warning",
            "Envio iniciado",
            f"Lote {nome_avaliacao} em modo {modo}.",
        )

        if modo != "simulacao":
            try:
                automacao_inicio_vetores_keepedu(
                    nome_avaliacao,
                    str(payload.idAval or ""),
                )
            except Exception:
                pass

        # PASSAGEM POSICIONAL PURA: Passando 'nome_avaliacao' direto como primeiro argumento
        # Isso ignora se o nome interno é 'nome_processamento' ou 'nome_avaliacao' e preenche a posição correta.
        if modo == "simulacao":
            resultado = simular_importacao_respostas_presenciais(
                nome_avaliacao,  # <--- Sem chaves, direto na primeira posição
                id_aval=payload.idAval,
                dia_aval=payload.diaAval,
                usuario_id=payload.usuario_id,
                id_aluno=payload.idAluno,
                progresso_callback=progresso_callback,
                # modelo_ia=payload.modelo_ia,
            )
        else:
            resultado = importar_respostas_presenciais(
                nome_avaliacao,  # <--- Sem chaves, direto na primeira posição
                id_aval=payload.idAval,
                dia_aval=payload.diaAval,
                usuario_id=payload.usuario_id,
                id_aluno=payload.idAluno,
                progresso_callback=progresso_callback,
                # modelo_ia=payload.modelo_ia,
            )

        resultado_job = _resultado_job_importacao(resultado)
        status_resultado = resultado.get("status")

        if status_resultado == "erro":
            atualizar_job(
                job_id,
                status="erro",
                percentual=100,
                mensagem="Envio de Vetores encerrado com erro.",
                erro=" | ".join(resultado.get("mensagem", [])[:3]) or "Erro na importação.",
                resultado=resultado_job,
            )
            registrar_evento_job(
                job_id,
                "error",
                "Envio com erro",
                " | ".join(resultado.get("mensagem", [])[:3]) or "Falha na importação.",
            )
            return

        atualizar_job(
            job_id,
            status="concluido",
            percentual=100,
            mensagem=(
                "Simulação concluída."
                if modo == "simulacao" else
                "Progresso de envio de Vetores concluído."
            ),
            resultado=resultado_job,
        )
        registrar_evento_job(
            job_id,
            "success" if status_resultado == "ok" else "warning",
            "Envio finalizado",
            f"{resultado.get('importadas', 0)}/{resultado.get('total', 0)} importada(s), "
            f"{resultado.get('erros', 0)} erro(s).",
        )
    except Exception as erro:
        atualizar_job(
            job_id,
            status="erro",
            percentual=100,
            mensagem="Erro ao executar envio de Vetores.",
            erro=str(erro),
        )
        registrar_evento_job(
            job_id,
            "error",
            "Falha no envio",
            str(erro),
        )

async def salvar_uploads_em_pasta(arquivos: list[UploadFile], pasta_upload):
    if len(arquivos) > MAX_UPLOAD_FILES:
        raise ValueError(
            f"Limite excedido: envie no máximo {MAX_UPLOAD_FILES} arquivo(s) por processamento."
        )

    total_salvos = 0
    total_bytes = 0
    limite_total_bytes = MAX_TOTAL_UPLOAD_SIZE_MB * 1024 * 1024
    limite_arquivo_bytes = MAX_FILE_SIZE_MB * 1024 * 1024

    for arquivo in arquivos:
        nome_arquivo = os.path.basename(arquivo.filename)
        if not nome_arquivo:
            continue

        extensao = os.path.splitext(nome_arquivo)[1].lower()
        if extensao not in EXTENSOES_PERMITIDAS:
            continue

        conteudo = await arquivo.read()
        tamanho_arquivo = len(conteudo)

        if tamanho_arquivo > limite_arquivo_bytes:
            raise ValueError(
                f"O arquivo {nome_arquivo} excede o limite de {MAX_FILE_SIZE_MB} MB."
            )

        total_bytes += tamanho_arquivo
        if total_bytes > limite_total_bytes:
            raise ValueError(
                f"O envio excedeu o limite total de {MAX_TOTAL_UPLOAD_SIZE_MB} MB."
            )

        destino = pasta_upload / nome_arquivo
        with open(destino, "wb") as f:
            f.write(conteudo)

        total_salvos += 1
    return total_salvos


async def salvar_arquivo_upload(arquivo: UploadFile, pasta_destino):
    nome_arquivo = os.path.basename(arquivo.filename or "")
    if not nome_arquivo:
        raise ValueError("Arquivo enviado sem nome.")

    extensao = os.path.splitext(nome_arquivo)[1].lower()
    if extensao not in EXTENSOES_PLANILHA:
        raise ValueError(f"Formato não suportado: {nome_arquivo}")

    conteudo = await arquivo.read()
    destino = pasta_destino / nome_arquivo
    with open(destino, "wb") as f:
        f.write(conteudo)
    return destino


@router.get("/")
async def index():
    return RedirectResponse(
        url="/painel",
        status_code=303
    )


@router.get("/login", response_class=HTMLResponse)
async def login_page(request: Request, next: str = "/"):
    if not APP_ENABLE_AUTH:
        return RedirectResponse(url="/", status_code=303)
        
    usuario_id = request.session.get("usuario_id")
    session_id = request.session.get("session_id")

    if usuario_id and session_id:
        sessao_banco = obter_sessao_usuario(usuario_id)

        if sessao_banco and sessao_banco == session_id:
            return RedirectResponse(
                url=_destino_pos_login(next),
                status_code=303
            )

    # 2. FLUXO DO AUTO-LOGIN: Se não tiver sessão, checa o Cookie de "Lembre-me"
    usuario_id_valido = verificar_autologin_cookie(request)
    
    if usuario_id_valido:

        session_id = secrets.token_hex(32)

        request.session["authenticated"] = True
        request.session["session_id"] = session_id
        request.session["usuario_id"] = usuario_id_valido

        salvar_sessao_usuario(
            usuario_id_valido,
            session_id
        )

        request.session["auth_source"] = "keepedu_remember"

        return RedirectResponse(
            url=_destino_pos_login(next),
            status_code=303
        )

    # 3. Se não houver cookie ou o token estiver vencido, renderiza a tela normalmente
    return render_template(
        request,
        "login.html",
        {
            "erro_login": "",
            "email_login": "",
            "next_url": _destino_pos_login(next),
            "keepedu_login_habilitado": bool(KEEPEDU_LOGIN_URL),
            "keepedu_school": KEEPEDU_LOGIN_SCHOOL,
        },
    )


@router.post("/login", response_class=HTMLResponse)
async def login_submit(
    request: Request,
    response: Response, # <--- ADICIONADO PARA MANIPULAR OS COOKIES
    email: str = Form(""),
    senha: str = Form(""),
    lembre_me: bool = Form(False), # <--- CAPTURA O CHECKBOX DO HTML
    next_url: str = Form("/"),
):
    print("CHEGOU NO LOGIN SUBMIT")
    if not APP_ENABLE_AUTH:
        return RedirectResponse(url="/", status_code=303)

    sucesso, mensagem, dados_usuario = autenticar_keepedu(email, senha)
    
    print("DADOS_USUARIO:")
    print(dados_usuario)
    if not sucesso:
        return render_template(
            request,
            "login.html",
            {
                "erro_login": mensagem or "Não foi possível entrar.",
                "email_login": str(email or "").strip(),
                "next_url": _destino_pos_login(next_url),
                "keepedu_login_habilitado": bool(KEEPEDU_LOGIN_URL),
                "keepedu_school": KEEPEDU_LOGIN_SCHOOL,
            },
            status_code=401,
        )

    # Limpa a sessão local antiga
    # Limpa sessão antiga
    request.session.clear()

    # Gera session_id
    import secrets
    session_id = secrets.token_hex(32)

    # Obtém usuário
    usuario_id = str(dados_usuario.get("id") or "")

    # Salva sessão
    request.session["authenticated"] = True
    request.session["session_id"] = session_id
    request.session["usuario_id"] = usuario_id

    request.session["user_email"] = str(
        dados_usuario.get("email") or email or ""
    ).strip()

    request.session["user_name"] = str(
    dados_usuario.get("name") or ""
    ).strip()

    print("NOME FINAL:", request.session["user_name"])

    print("SESSION USER:")
    print(request.session.get("user_name"))
    
    print("SESSION USER:")
    print(request.session.get("user_name"))

    request.session["auth_source"] = str(
        dados_usuario.get("origem") or "keepedu"
    ).strip()


    # Salva no banco/cache
    salvar_sessao_usuario(
        usuario_id,
        session_id
    )
    
    redirecionamento = RedirectResponse(
        url=_destino_pos_login(next_url),
        status_code=303
    )

    if lembre_me and dados_usuario:
        usuario_id = str(dados_usuario.get("id") or "")
        gerenciar_lembre_me(redirecionamento, usuario_id)

    return redirecionamento


@router.get("/logout")
async def logout(request: Request, response: Response):
    # Verifica se existe o cookie de lembre-me e apaga do banco de dados
    token_cookie = request.cookies.get("remember_token")
    if token_cookie:
        remover_token_banco(token_cookie)
        
    # Limpa a sessão local da memória do FastAPI
    request.session.clear()
    
    # Cria a resposta limpando fisicamente o cookie do navegador
    resposta = RedirectResponse(url="/login", status_code=303)
    resposta.delete_cookie("remember_token")
    return resposta


@router.get("/kanban", response_class=HTMLResponse)
async def kanban(request: Request):
    board = montar_board()
    return render_template(
        request,
        "Kanban.html",
        {
            "board": board,
            "board_json": json.dumps(board, ensure_ascii=False),
        },
    )


@router.get("/api/kanban/board")
async def api_kanban_board():
    return JSONResponse(montar_board())


@router.get("/api/kanban/colunas")
async def api_kanban_listar_colunas():
    return JSONResponse({"status": "ok", "colunas": listar_etapas(incluir_inativas=True)})


@router.post("/api/kanban/colunas")
async def api_kanban_criar_coluna(payload: KanbanColunaPayload):
    try:
        if not payload.slug or not payload.titulo:
            return JSONResponse(
                {"status": "erro", "mensagem": "Informe slug e titulo da coluna."},
                status_code=400,
            )
        coluna = criar_coluna(
            slug=payload.slug,
            titulo=payload.titulo,
            cor=payload.cor or "#788394",
            ordem=payload.ordem,
            eh_inicial=bool(payload.eh_inicial),
            eh_final=bool(payload.eh_final),
        )
        return JSONResponse({"status": "ok", "coluna": coluna, "board": montar_board()})
    except ValueError as erro:
        return JSONResponse({"status": "erro", "mensagem": str(erro)}, status_code=400)
    except Exception as erro:
        return JSONResponse({"status": "erro", "mensagem": str(erro)}, status_code=500)


@router.patch("/api/kanban/colunas/{slug}")
async def api_kanban_atualizar_coluna(slug: str, payload: KanbanColunaPayload):
    try:
        alteracoes = payload.model_dump(exclude_unset=True)
        alteracoes.pop("slug", None)
        if not alteracoes:
            return JSONResponse(
                {"status": "erro", "mensagem": "Nenhum campo informado para atualização."},
                status_code=400,
            )
        coluna = atualizar_coluna(slug, **alteracoes)
        if not coluna:
            return JSONResponse({"status": "erro", "mensagem": "Coluna não encontrada."}, status_code=404)
        return JSONResponse({"status": "ok", "coluna": coluna, "board": montar_board()})
    except ValueError as erro:
        return JSONResponse({"status": "erro", "mensagem": str(erro)}, status_code=400)
    except Exception as erro:
        return JSONResponse({"status": "erro", "mensagem": str(erro)}, status_code=500)


@router.delete("/api/kanban/colunas/{slug}")
async def api_kanban_excluir_coluna(slug: str):
    try:
        removida = excluir_coluna(slug)
        if not removida:
            return JSONResponse({"status": "erro", "mensagem": "Coluna não encontrada."}, status_code=404)
        return JSONResponse({"status": "ok", "board": montar_board()})
    except ValueError as erro:
        return JSONResponse({"status": "erro", "mensagem": str(erro)}, status_code=400)
    except Exception as erro:
        return JSONResponse({"status": "erro", "mensagem": str(erro)}, status_code=500)


@router.post("/api/kanban/entrega")
async def api_kanban_entrega(payload: KanbanEntregaPayload):
    try:
        card = registrar_entrega(
            titulo=payload.titulo,
            id_avaliacao=payload.id_avaliacao,
            responsavel_entrega=payload.responsavel_entrega,
            responsavel_correcao=payload.responsavel_correcao,
            total_cartoes=payload.total_cartoes,
            data_entrega=payload.data_entrega,
            data_previsao_entrega=payload.data_previsao_entrega,
            unidade=payload.unidade,
            serie=payload.serie,
            turma=payload.turma,
        )
        return JSONResponse({"status": "ok", "card": card, "board": montar_board()})
    except ValueError as erro:
        return JSONResponse({"status": "erro", "mensagem": str(erro)}, status_code=400)
    except Exception as erro:
        return JSONResponse({"status": "erro", "mensagem": str(erro)}, status_code=500)


@router.delete("/api/kanban/cards/{card_id}")
async def api_kanban_excluir(card_id: str):
    try:
        if not excluir_card(card_id):
            return JSONResponse(
                {"status": "erro", "mensagem": "Card não encontrado."},
                status_code=404,
            )
        return JSONResponse({"status": "ok", "board": montar_board()})
    except Exception as erro:
        return JSONResponse({"status": "erro", "mensagem": str(erro)}, status_code=500)


@router.get("/api/kanban/historico-processamentos")
async def api_kanban_historico_processamentos_query(
    card_id: str = "",
    id_avaliacao: str = "",
):
    historico = historico_processamentos_por_referencia(
        card_id=card_id,
        id_avaliacao=id_avaliacao,
    )
    if historico is None:
        return JSONResponse(
            {"status": "erro", "mensagem": "Informe card_id ou id_avaliacao."},
            status_code=400,
        )
    return JSONResponse({"status": "ok", "historico_processamentos": historico})


@router.get("/api/kanban/cards/{card_id}/historico-processamentos")
async def api_kanban_historico_processamentos(card_id: str, id_avaliacao: str = ""):
    historico = historico_processamentos_do_card(card_id, id_avaliacao=id_avaliacao)
    if historico is None:
        return JSONResponse(
            {"status": "erro", "mensagem": "Card não encontrado."},
            status_code=404,
        )
    return JSONResponse({"status": "ok", "historico_processamentos": historico})


@router.get("/api/kanban/pendencias")
async def api_kanban_pendencias_query(card_id: str = "", id_avaliacao: str = ""):
    pendencias = pendencias_do_card(card_id, id_avaliacao=id_avaliacao)
    if pendencias is None:
        return JSONResponse(
            {"status": "erro", "mensagem": "Card não encontrado."},
            status_code=404,
        )
    return JSONResponse({"status": "ok", "pendencias": pendencias})


@router.get("/api/kanban/cards/{card_id}/pendencias")
async def api_kanban_pendencias(card_id: str, id_avaliacao: str = ""):
    pendencias = pendencias_do_card(card_id, id_avaliacao=id_avaliacao)
    if pendencias is None:
        return JSONResponse(
            {"status": "erro", "mensagem": "Card não encontrado."},
            status_code=404,
        )
    return JSONResponse({"status": "ok", "pendencias": pendencias})


@router.patch("/api/kanban/cards/{card_id}")
async def api_kanban_atualizar(card_id: str, payload: KanbanCardPayload):
    try:
        alteracoes = payload.model_dump(exclude_unset=True)
        if not alteracoes:
            return JSONResponse(
                {"status": "erro", "mensagem": "Nenhum campo informado para atualização."},
                status_code=400,
            )
        card = atualizar_card(card_id, **alteracoes)
        if not card:
            return JSONResponse({"status": "erro", "mensagem": "Card não encontrado."}, status_code=404)
        return JSONResponse({"status": "ok", "card": card, "board": montar_board()})
    except ValueError as erro:
        return JSONResponse({"status": "erro", "mensagem": str(erro)}, status_code=400)
    except Exception as erro:
        return JSONResponse({"status": "erro", "mensagem": str(erro)}, status_code=500)


@router.post("/api/kanban/automacao/processamento-iniciado")
async def api_kanban_automacao_processamento(
    request: Request,
    payload: KanbanAutomacaoProcessamentoPayload,
):
    usuario = str(payload.usuario or request.session.get("user_name") or "").strip()
    card = automacao_processamento_iniciado(
        payload.id_avaliacao,
        payload.nome_processamento,
        usuario=usuario,
        total_cartoes=payload.total_cartoes,
    )
    return JSONResponse({
        "status": "ok",
        "movido": bool(card),
        "card": card,
    })


@router.post("/api/kanban/automacao/lote-aberto")
async def api_kanban_automacao_lote(payload: KanbanAutomacaoLotePayload):
    card = automacao_lote_aberto(payload.nome_processamento, payload.id_avaliacao)
    return JSONResponse({
        "status": "ok",
        "movido": bool(card),
        "card": card,
    })


@router.post("/api/kanban/automacao/inicio-vetores-keepedu")
async def api_kanban_automacao_inicio_vetores(payload: KanbanAutomacaoLotePayload):
    card = automacao_inicio_vetores_keepedu(payload.nome_processamento, payload.id_avaliacao)
    return JSONResponse({
        "status": "ok",
        "movido": bool(card),
        "card": card,
    })


@router.post("/api/kanban/automacao/envio-keepedu")
async def api_kanban_automacao_envio_keepedu(payload: KanbanAutomacaoLotePayload):
    card = automacao_envio_keepedu(payload.nome_processamento, payload.id_avaliacao)
    return JSONResponse({
        "status": "ok",
        "movido": bool(card),
        "card": card,
    })


@router.get("/bernoulli", response_class=HTMLResponse)
async def bernoulli(request: Request):
    return render_template(request, "bernoulli.html", {})


def _rotulo_dia_bernoulli(dia: str) -> str:
    return "1 dia" if str(dia) == "1" else "2 dia"


def _montar_logs_bernoulli(resultado: dict, nome_arquivo_entrada: str, nome_arquivo_download: str):
    return [
        {"tipo": "info", "titulo": "Arquivo recebido", "descricao": nome_arquivo_entrada},
        {"tipo": "info", "titulo": "Modo selecionado", "descricao": f"{_rotulo_dia_bernoulli(resultado.get('dia', '1'))} | q{resultado.get('primeira_questao', 1)} a q{resultado.get('ultima_questao', 90)}"},
        {"tipo": "success", "titulo": "Estrutura validada", "descricao": f"{resultado['questoes_encontradas_no_keepedu']} coluna(s) de questao reconhecida(s)."},
        {"tipo": "success", "titulo": "Consulta Bernoulli concluida", "descricao": f"{resultado['rmb_encontrados']} RMB(s) localizado(s) e {resultado['sem_rmb']} pendencia(s) de inscricao."},
        {"tipo": "success", "titulo": "Arquivo pronto", "descricao": nome_arquivo_download}
    ]


def _processar_arquivo_bernoulli(caminho_keepedu, nome_arquivo_entrada: str, dia: str, progresso_callback=None):
    resultado = transformar_vetor_keepedu_para_bernoulli(
        caminho_arquivo_keepedu=caminho_keepedu, dia=dia, progresso_callback=progresso_callback
    )
    nome_saida_real = os.path.basename(str(resultado["caminho_saida"]))
    nome_arquivo_download = f"vetor_bernoulli_dia{resultado['dia']}_{resultado['linhas']}lin_q{resultado['primeira_questao']}-q{resultado['ultima_questao']}.xlsx"

    return {
        "resultado": resultado,
        "nome_arquivo_entrada": nome_arquivo_entrada,
        "nome_arquivo_real": nome_saida_real,
        "nome_arquivo_download": nome_arquivo_download,
        "download_url": f"/bernoulli/download/{nome_saida_real}",
        "logs": _montar_logs_bernoulli(resultado, nome_arquivo_entrada, nome_arquivo_download)
    }


def _resultado_job_bernoulli(dados: dict):
    return {
        "resultado": dados["resultado"],
        "nome_arquivo_entrada": dados["nome_arquivo_entrada"],
        "nome_arquivo_real": dados["nome_arquivo_real"],
        "nome_arquivo_download": dados["nome_arquivo_download"],
        "download_url": dados["download_url"]
    }


def processar_arquivo_bernoulli_em_background(job_id: str, caminho_keepedu, nome_arquivo_entrada: str, dia: str):
    pasta_tmp = caminho_keepedu.parent
    def progresso_callback(percentual, mensagem, tipo=None, titulo=None, descricao="", resultado=None):
        atualizar_job(job_id, status="processando", percentual=percentual, mensagem=mensagem, arquivo_atual=nome_arquivo_entrada, resultado=resultado)
        if titulo:
            registrar_evento_job(job_id, tipo or "info", titulo, descricao or mensagem, arquivo=nome_arquivo_entrada)

    try:
        atualizar_job(job_id, status="processando", percentual=5, mensagem=f"Arquivo recebido. Preparando processamento...", arquivo_atual=nome_arquivo_entrada)
        registrar_evento_job(job_id, "info", "Arquivo recebido", nome_arquivo_entrada, arquivo=nome_arquivo_entrada)

        dados = _processar_arquivo_bernoulli(caminho_keepedu=caminho_keepedu, nome_arquivo_entrada=nome_arquivo_entrada, dia=dia, progresso_callback=progresso_callback)

        registrar_evento_job(job_id, "success", "Arquivo pronto", dados["nome_arquivo_download"], arquivo=nome_arquivo_entrada)
        atualizar_job(job_id, status="concluido", percentual=100, mensagem="Simulado Bernoulli gerado com sucesso.", arquivo_atual=nome_arquivo_entrada, resultado=_resultado_job_bernoulli(dados))
    except Exception as erro:
        atualizar_job(job_id, status="erro", percentual=100, mensagem="Erro ao gerar o Simulado Bernoulli.", arquivo_atual=nome_arquivo_entrada, erro=str(erro))
        registrar_evento_job(job_id, "error", "Falha na conversao", str(erro), arquivo=nome_arquivo_entrada)
    finally:
        for arquivo in pasta_tmp.glob("*"):
            try: arquivo.unlink(missing_ok=True)
            except Exception: pass
        try: pasta_tmp.rmdir()
        except Exception: pass


@router.post("/bernoulli/processar")
async def bernoulli_processar(arquivo_keepedu: UploadFile = File(...), dia: str = Form("1")):
    try:
        dados = await executar_processamento_bernoulli(arquivo_keepedu, dia=dia)
        return FileResponse(
            str(PASTA_BERNOULLI / dados["nome_arquivo_real"]),
            filename=dados["nome_arquivo_download"],
            media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        )
    except Exception as e:
        return HTMLResponse(f"<h1>Erro</h1><p>{escape(str(e))}</p>", status_code=400)


@router.post("/bernoulli/processar-ajax")
async def bernoulli_processar_ajax(background_tasks: BackgroundTasks, arquivo_keepedu: UploadFile = File(...), dia: str = Form("1")):
    job_id = criar_job(total_arquivos=1)
    pasta_tmp = PASTA_UPLOADS_TEMP / f"bernoulli_{job_id}"
    pasta_tmp.mkdir(parents=True, exist_ok=True)
    try:
        caminho_keepedu = await salvar_arquivo_upload(arquivo_keepedu, pasta_tmp)
        nome_arquivo_entrada = os.path.basename(arquivo_keepedu.filename or caminho_keepedu.name)
        atualizar_job(job_id, status="processando", percentual=3, mensagem="Aguardando início...", arquivo_atual=nome_arquivo_entrada)
        background_tasks.add_task(processar_arquivo_bernoulli_em_background, job_id, caminho_keepedu, nome_arquivo_entrada, dia)
        return JSONResponse({"status": "ok", "job_id": job_id})
    except Exception as erro:
        atualizar_job(job_id, status="erro", percentual=100, mensagem="Erro", erro=str(erro))
        return JSONResponse({"status": "erro", "mensagem": str(erro)}, status_code=400)


@router.get("/bernoulli/download/{nome_arquivo}")
async def bernoulli_download(nome_arquivo: str):
    nome_seguro = os.path.basename(nome_arquivo or "")
    caminho_saida = (PASTA_BERNOULLI / nome_seguro).resolve()
    if not caminho_saida.exists():
        return HTMLResponse("<h1>Arquivo não encontrado.</h1>", status_code=404)
    return FileResponse(str(caminho_saida), filename=nome_seguro)


@router.post("/nova-avaliacao-pasta")
async def rota_nova_avaliacao_pasta(pasta_origem: str = Form(...)):
    try:
        nome_avaliacao = processar_pasta_local(pasta_origem)
        return RedirectResponse(url=f"/avaliacao/{nome_avaliacao}", status_code=303)
    except Exception as e:
        return HTMLResponse(f"<h1>Erro</h1><p>{escape(str(e))}</p>", status_code=400)


# CORREÇÃO AQUI: Esta rota agora responde com JSON Puro para alinhar com o JavaScript AJAX
@router.get("/api/keepedu/usuarios")
async def api_keepedu_usuarios(refresh: bool = False):
    resultado = listar_usuarios_keepedu(force=bool(refresh))
    status = 200 if resultado.get("success") else 502
    return JSONResponse(resultado, status_code=status)


@router.get("/api/keepedu/unidades")
async def api_keepedu_unidades(refresh: bool = False):
    resultado = listar_unidades_keepedu(force=bool(refresh))
    status = 200 if resultado.get("success") else 502
    return JSONResponse(resultado, status_code=status)


def _resolver_unidade_id(unidade_id: str = "", unidade: str = "") -> str:
    id_resolvido = str(unidade_id or "").strip()
    if id_resolvido:
        return id_resolvido

    unidade_nome = str(unidade or "").strip()
    if not unidade_nome:
        return ""

    cadastro = listar_unidades_keepedu()
    for item in cadastro.get("unidades") or []:
        if str(item.get("nome") or "").strip() == unidade_nome:
            return str(item.get("id") or "").strip()
    return ""


@router.get("/api/keepedu/series")
async def api_keepedu_series(
    unidade: str = "",
    unidade_id: str = "",
    refresh: bool = False,
):
    resultado = listar_series_por_unidade(
        unidade_id=_resolver_unidade_id(unidade_id, unidade),
        force=bool(refresh),
    )
    return JSONResponse(resultado, status_code=200)


@router.get("/api/keepedu/turmas")
async def api_keepedu_turmas(
    unidade: str = "",
    unidade_id: str = "",
    serie_id: str = "",
    refresh: bool = False,
):
    resultado = listar_turmas_por_serie(
        unidade_id=_resolver_unidade_id(unidade_id, unidade),
        serie_id=str(serie_id or "").strip(),
        force=bool(refresh),
    )
    return JSONResponse(resultado, status_code=200)


@router.get("/api/avaliacoes/listagem")
async def api_avaliacoes_listagem(
    request: Request,
    ano: str | None = None,
    refresh: bool = False,
):
    """
    Lista avaliações da KeepEdu (mesma fonte do select do Painel).
    Query: ?ano=2026&refresh=1
    """
    resultado = buscar_listagem_avaliacoes(
        ano_avaliacao=ano,
        force=bool(refresh),
    )
    status = 200 if resultado.get("success") else 502
    return JSONResponse(resultado, status_code=status)


@router.post("/painel/configurar-avaliacao")
async def painel_configurar_avaliacao(request: Request, id_avaliacao: str = Form("")):
    try:
        id_texto = str(id_avaliacao or "").strip()
        if not id_texto:
            ctx = limpar_contexto_na_sessao(request.session)
            return JSONResponse({"status": "ok", "avaliacao": ctx})
        ctx = aplicar_contexto_na_sessao(request.session, id_avaliacao)
        return JSONResponse({"status": "ok", "avaliacao": ctx})
    except ValueError as erro:
        return JSONResponse({"status": "erro", "mensagem": str(erro)}, status_code=400)
    except Exception as erro:
        return JSONResponse({"status": "erro", "mensagem": str(erro)}, status_code=500)


@router.post("/processar-ajax")
async def rota_nova_avaliacao_upload(
    request: Request,
    background_tasks: BackgroundTasks,
    arquivos: list[UploadFile] = File(...),
):
    ctx_proc = contexto_para_processamento(request.session)
    if not ctx_proc.get("id_avaliacao_esperado"):
        return JSONResponse(
            {
                "status": "erro",
                "mensagem": "Configure o simulado/avaliação no Painel antes de iniciar o processamento.",
            },
            status_code=400,
        )

    job_id = criar_job(total_arquivos=len(arquivos))
    pasta_upload = PASTA_UPLOADS_TEMP / f"upload_{job_id}"
    pasta_upload.mkdir(parents=True, exist_ok=True)

    try:
        total_salvos = await salvar_uploads_em_pasta(arquivos, pasta_upload)
        if total_salvos == 0:
            atualizar_job(job_id, status="erro", percentual=100, mensagem="Nenhuma imagem válida.", erro="Sem arquivos válidos.")
            return JSONResponse({"status": "erro", "job_id": job_id, "mensagem": "Nenhuma imagem válida foi enviada."})

        atualizar_job(job_id, status="processando", percentual=5, mensagem=f"{total_salvos} imagem(ns) recebida(s). Lendo...")
        usuario_nome = str(request.session.get("user_name") or "").strip()
        try:
            automacao_processamento_iniciado(str(ctx_proc["id_avaliacao_esperado"]))
        except Exception:
            pass
        background_tasks.add_task(
            processar_pasta_temporaria_com_progresso,
            job_id,
            pasta_upload,
            ctx_proc["id_avaliacao_esperado"],
            ctx_proc.get("nome_simulado", ""),
            ctx_proc.get("meta_avaliacao"),
            usuario_nome,
        )

        return JSONResponse({"status": "ok", "job_id": job_id})
    except Exception as e:
        atualizar_job(job_id, status="erro", percentual=100, mensagem="Erro", erro=str(e))
        return JSONResponse({"status": "erro", "job_id": job_id, "mensagem": str(e)})


@router.get("/avaliacao/{nome_avaliacao}", response_class=HTMLResponse)
async def ver_avaliacao(request: Request, nome_avaliacao: str):
    imagens_debug = listar_imagens_debug(nome_avaliacao)
    resumo = resumo_processamento(nome_avaliacao)
    leituras = carregar_json(caminho_leituras(nome_avaliacao), {})
    id_avaliacao = detectar_id_prova_do_processamento(leituras, nome_avaliacao) or ""
    return render_template(
        request,
        "processamento.html",
        {
            "nome_processamento": nome_avaliacao,
            "imagens_debug": imagens_debug,
            "resumo": resumo,
            "status_lote": resumo_status_lote_keepedu(nome_avaliacao),
            **_contexto_avaliacao_ui(request, nome_avaliacao, id_avaliacao),
        },
    )


@router.get("/imagem/{nome_avaliacao}/{nome_imagem}")
async def servir_imagem(nome_avaliacao: str, nome_imagem: str):
    nome_limpo = extrair_nome_lote_seguro(nome_avaliacao)
    nome_imagem_limpo = str(nome_imagem or "").strip().lstrip("/")
    if not nome_limpo or not nome_imagem_limpo:
        return JSONResponse({"erro": "Imagem não encontrada."}, status_code=404)

    caminho = caminho_imagem_debug(nome_limpo, nome_imagem_limpo)
    if not caminho.exists():
        leituras = carregar_json(caminho_leituras(nome_limpo), {})
        caminho = localizar_imagem_original(nome_limpo, nome_imagem_limpo, leituras)
    if not caminho or not caminho.exists():
        caminho = gerar_visualizacao_cartao_temporaria(nome_limpo, nome_imagem_limpo)
    if not caminho or not caminho.exists():
        return JSONResponse({"erro": "Imagem não encontrada."}, status_code=404)

    caminho_texto = str(caminho).lower().replace("\\", "/")
    headers = {"Cache-Control": "no-store, max-age=0"} if "/_vis_temp/" in caminho_texto else None
    return FileResponse(
        str(caminho),
        media_type="image/jpeg" if caminho_texto.endswith((".jpg", ".jpeg")) else None,
        headers=headers,
    )


@router.get("/leituras/{nome_avaliacao}")
async def obter_leituras(nome_avaliacao: str):
    caminho = carregar_leitura(nome_avaliacao, em_json=True)
    if not caminho or not caminho.exists():
        return JSONResponse({})
    return FileResponse(str(caminho), media_type="application/json")


@router.get("/download-log/{nome_avaliacao}")
async def download_log(nome_avaliacao: str):
    caminho = caminho_log(nome_avaliacao)
    if not caminho.exists():
        return JSONResponse({"erro": "Log não encontrado."}, status_code=404)
    return FileResponse(str(caminho), filename=f"log_avaliacao_{nome_avaliacao}.csv")


@router.get("/correcoes/{nome_avaliacao}")
async def obter_correcoes(nome_avaliacao: str):
    caminho = caminho_correcoes_web(nome_avaliacao)
    if not caminho.exists():
        return JSONResponse({}, headers={"Cache-Control": "no-store"})
    return FileResponse(
        str(caminho),
        media_type="application/json",
        headers={"Cache-Control": "no-store"},
    )


@router.post("/correcoes/{nome_avaliacao}")
async def salvar_correcoes(nome_avaliacao: str, payload: CorrecoesPayload):
    caminho = caminho_correcoes_web(nome_avaliacao)
    with open(caminho, "w", encoding="utf-8") as f:
        json.dump(payload.correcoes, f, ensure_ascii=False, indent=4)
    return {"status": "ok", "arquivo": str(caminho)}


@router.post("/reprocessar-imagem/{nome_avaliacao}")
async def rota_reprocessar_imagem(nome_avaliacao: str, payload: ReprocessarImagemPayload):
    try:
        return JSONResponse(reprocessar_imagem_processamento(nome_processamento=nome_avaliacao, nome_imagem=payload.nome_imagem, pontos_cantos=payload.pontos_cantos))
    except Exception as e:
        return JSONResponse({"status": "erro", "mensagem": str(e)}, status_code=400)


@router.post("/deletar-leitura/{nome_avaliacao}")
async def rota_deletar_leitura(nome_avaliacao: str, payload: DeletarLeituraPayload):
    try:
        return JSONResponse(deletar_leitura_processamento(nome_avaliacao, payload.nome_imagem))
    except FileNotFoundError as e:
        return JSONResponse({"status": "erro", "mensagem": str(e)}, status_code=404)
    except Exception as e:
        return JSONResponse({"status": "erro", "mensagem": str(e)}, status_code=400)


@router.post("/reprocessar-lote-ajax/{nome_avaliacao:path}")
async def rota_reprocessar_lote_ajax(nome_avaliacao: str, background_tasks: BackgroundTasks):
    nome_limpo = extrair_nome_lote_seguro(nome_avaliacao)
    if not nome_limpo:
        return JSONResponse({"status": "erro", "mensagem": "Lote inválido."}, status_code=400)

    pasta_lote = caminho_pasta_processamento(nome_limpo)
    if not pasta_lote.exists():
        return JSONResponse({"status": "erro", "mensagem": "Lote não encontrado."}, status_code=404)

    leituras = carregar_json(caminho_leituras(nome_limpo), {})
    total_leituras = len(leituras) if isinstance(leituras, dict) else 0
    total_originais = sum(
        1
        for arquivo in caminho_originais(nome_limpo).iterdir()
        if arquivo.is_file() and arquivo.suffix.lower() in {".jpg", ".jpeg", ".png"}
    ) if caminho_originais(nome_limpo).exists() else 0

    if total_leituras <= 0 and total_originais <= 0:
        return JSONResponse(
            {"status": "erro", "mensagem": "Nenhuma imagem restante para reprocessar neste lote."},
            status_code=404,
        )

    job_id = criar_job(total_arquivos=max(total_leituras, total_originais, 1))
    try:
        atualizar_job(job_id, status="processando", percentual=1, mensagem="Iniciando reprocessamento...")
        background_tasks.add_task(reprocessar_lote_com_progresso, job_id, nome_limpo)
        return JSONResponse({"status": "ok", "job_id": job_id})
    except Exception as e:
        atualizar_job(job_id, status="erro", percentual=100, mensagem="Erro", erro=str(e))
        return JSONResponse({"status": "erro", "mensagem": str(e)}, status_code=400)


@router.post("/avaliacao/{nome_avaliacao}/alterar-id-prova-ajax")
async def rota_alterar_id_prova_ajax(
    request: Request,
    nome_avaliacao: str,
    payload: AlterarIdProvaPayload,
):
    nome_limpo = extrair_nome_lote_seguro(nome_avaliacao)
    if not nome_limpo:
        return JSONResponse({"status": "erro", "mensagem": "Lote inválido."}, status_code=400)

    try:
        resultado = alterar_id_prova_do_lote(
            nome_limpo,
            payload.id_prova_novo,
            payload.id_prova_antigo,
        )
        exibicao = contexto_exibicao_avaliacao(
            request.session,
            nome_processamento=nome_limpo,
            id_fallback=resultado.get("id_prova_novo", ""),
        )
        resultado["avaliacao_exibicao"] = exibicao
        return JSONResponse(resultado)
    except ValueError as erro:
        return JSONResponse({"status": "erro", "mensagem": str(erro)}, status_code=400)
    except Exception as erro:
        return JSONResponse(
            {"status": "erro", "mensagem": f"Não foi possível alterar o ID: {erro}"},
            status_code=500,
        )


@router.get("/gerar-csv/{nome_avaliacao}")
async def rota_gerar_csv(request: Request, nome_avaliacao: str, forcar: bool = False):
    nome_limpo = extrair_nome_lote_seguro(nome_avaliacao)
    try:
        resultado = gerar_csv_final(nome_limpo, forcar=forcar)
    except FileNotFoundError as erro:
        return JSONResponse({"status": "erro", "mensagem": str(erro)}, status_code=404)

    if resultado.get("status") != "ok":
        return JSONResponse(resultado, status_code=400)

    aceita_json = "application/json" in (request.headers.get("accept") or "").lower()
    if aceita_json:
        return JSONResponse({
            "status": "ok",
            "download_url": f"/download-csv/{nome_limpo}",
            "gerado_com_pendencias": bool(resultado.get("gerado_com_pendencias")),
            "total_pendencias": int(resultado.get("total_pendencias") or 0),
        })

    return RedirectResponse(url=f"/download-csv/{nome_limpo}", status_code=303)


@router.get("/download-csv/{nome_avaliacao}")
async def rota_download_csv(nome_avaliacao: str):
    caminho = caminho_csv_final(nome_avaliacao)
    if not caminho.exists():
        return JSONResponse({"erro": "CSV não encontrado."}, status_code=404)
    return FileResponse(str(caminho), filename=f"{nome_avaliacao}_csv_final.csv", media_type="text/csv")


@router.post("/importar-respostas-presenciais")
async def rota_importar_respostas_presenciais(payload: ImportarRespostasPayload):

    # Pegamos o nome que veio no payload ou usamos o idAval como fallback seguro
    nome_lote = payload.nome_processamento or f"avaliacao_{payload.idAval}"

    # PASSO 1 — Importa vetores/respostas
    resultado = importar_respostas_presenciais(
        nome_lote,
        id_aval=payload.idAval,
        dia_aval=payload.diaAval,
        usuario_id=payload.usuario_id,
        id_aluno=payload.idAluno
    )

    if resultado.get("status") == "erro":
        return JSONResponse(resultado, status_code=400)

    return JSONResponse({
        "status": "ok",
        "vetores": resultado,
    })
    

@router.post("/importar-respostas-presenciais-ajax/{nome_avaliacao}")
async def rota_importar_respostas_presenciais_ajax(nome_avaliacao: str, payload: ImportarRespostasPayload, background_tasks: BackgroundTasks):
    total_arquivos = contar_alunos_importacao(nome_avaliacao, id_aluno=payload.idAluno)
    job_id = criar_job(total_arquivos=total_arquivos)
    background_tasks.add_task(_executar_importacao_respostas_em_background, job_id, nome_avaliacao, payload, "importacao")
    return JSONResponse({"status": "ok", "job_id": job_id, "mensagem": "Envio de Vetores iniciado."})


@router.post("/simular-importacao-respostas-presenciais/{nome_avaliacao}")
async def rota_simular_importacao_respostas_presenciais(nome_avaliacao: str, payload: ImportarRespostasPayload):
    resultado = simular_importacao_respostas_presenciais(nome_avaliacao=nome_avaliacao, id_aval=payload.idAval, dia_aval=payload.diaAval, usuario_id=payload.usuario_id, id_aluno=payload.idAluno)
    if resultado.get("status") == "erro": return JSONResponse(resultado, status_code=400)
    return JSONResponse(resultado)


@router.post("/simular-importacao-respostas-presenciais-ajax/{nome_avaliacao}")
async def rota_simular_importacao_respostas_presenciais_ajax(nome_avaliacao: str, payload: ImportarRespostasPayload, background_tasks: BackgroundTasks):
    total_arquivos = contar_alunos_importacao(nome_avaliacao, id_aluno=payload.idAluno)
    job_id = criar_job(total_arquivos=total_arquivos)
    background_tasks.add_task(_executar_importacao_respostas_em_background, job_id, nome_avaliacao, payload, "simulacao")
    return JSONResponse({"status": "ok", "job_id": job_id, "mensagem": "Simulação de Vetores iniciada."})


@router.post("/mock/importar-respostas-presenciais")
async def mock_importar_respostas_presenciais(request: Request):
    try: dados = await request.json()
    except Exception: return JSONResponse({"success": False, "mensagem": ["JSON inválido."]}, status_code=400)
    status_code, resposta = processar_mock_importacao_keepedu(dados)
    return JSONResponse(resposta, status_code=status_code)


@router.post("/validacao/{nome_avaliacao}/importar-frequencia-ajax")
async def rota_importar_frequencia_ajax(
    nome_avaliacao: str,
    background_tasks: BackgroundTasks,
    lista_de_frequencia: UploadFile = File(...),
    idSimulado: str = Form(...),
):
    nome_limpo = extrair_nome_lote_seguro(nome_avaliacao)
    nome_original = (lista_de_frequencia.filename or "").strip()

    if not nome_original:
        return JSONResponse({"status": "erro", "mensagem": "Nenhum arquivo selecionado."}, status_code=400)

    if not nome_original.lower().endswith(".pdf"):
        return JSONResponse({"status": "erro", "mensagem": "O arquivo deve estar em formato PDF."}, status_code=400)

    conteudo = await lista_de_frequencia.read()
    if not conteudo:
        return JSONResponse({"status": "erro", "mensagem": "O arquivo enviado está vazio."}, status_code=400)

    caminho = salvar_lista_frequencia_pdf(nome_limpo, conteudo)
    job_id = criar_job(total_arquivos=1)
    background_tasks.add_task(
        _executar_envio_frequencia_em_background,
        job_id,
        nome_limpo,
        idSimulado,
        str(caminho),
    )
    # Resposta imediata: o envio à KeepEdu roda em background com job_id para polling.
    return JSONResponse({
        "status": "ok",
        "job_id": job_id,
        "mensagem": "Envio de frequência iniciado.",
    })


@router.get("/validacao/{nome_avaliacao}/status-envios-ajax")
async def rota_status_envios_validacao_ajax(nome_avaliacao: str):
    nome_limpo = extrair_nome_lote_seguro(nome_avaliacao)
    dados_validacao = gerar_validacao_cadastral(nome_limpo)
    itens = {}
    for nome_imagem, validacao in (dados_validacao.get("validacoes") or {}).items():
        itens[nome_imagem] = {
            "rotulo_envio_keepedu": validacao.get("rotulo_envio_keepedu") or "",
            "enviado_keepedu_completo": bool(validacao.get("enviado_keepedu_completo")),
            "envio_keepedu": validacao.get("envio_keepedu") or {},
        }
    return JSONResponse({"status": "ok", "itens": itens})


@router.post("/validacao/manual/{nome_avaliacao}")
async def rota_salvar_validacao_manual(request: Request, nome_avaliacao: str, nome_imagem: str = Form(...), id_aluno_manual: str = Form(...)):
    try:
        registro = salvar_correcao_manual(nome_processamento=nome_avaliacao, nome_imagem=nome_imagem, valor_manual=id_aluno_manual)
        aceita_json = "application/json" in (request.headers.get("accept") or "").lower() or (request.headers.get("x-requested-with") or "").lower() == "fetch"
        if aceita_json: return JSONResponse({"status": "ok", "nome_imagem": nome_imagem, "registro": registro})
        return RedirectResponse(url=f"/validacao/{nome_avaliacao}", status_code=303)
    except Exception as e:
        return JSONResponse({"status": "erro", "mensagem": str(e)}, status_code=400)


@router.post("/validacao/manual/remover/{nome_avaliacao}")
async def rota_remover_validacao_manual(request: Request, nome_avaliacao: str, nome_imagem: str = Form(...)):
    try:
        resultado = remover_correcao_manual(nome_processamento=nome_avaliacao, nome_imagem=nome_imagem)
        aceita_json = "application/json" in (request.headers.get("accept") or "").lower() or (request.headers.get("x-requested-with") or "").lower() == "fetch"
        if aceita_json:
            return JSONResponse({
                "status": "ok",
                "nome_imagem": nome_imagem,
                "validacao": resultado.get("validacao", {}),
            })
        return RedirectResponse(url=f"/validacao/{nome_avaliacao}", status_code=303)
    except Exception as e:
        return JSONResponse({"status": "erro", "mensagem": str(e)}, status_code=400)


@router.get("/status-processamento/{job_id}")
async def status_processamento(job_id: str):
    return JSONResponse(obter_job(job_id))


@router.post("/nova-avaliacao-ajax")
async def rota_nova_avaliacao_ajax(
    request: Request,
    background_tasks: BackgroundTasks,
    arquivos: list[UploadFile] = File(...),
):
    ctx_proc = contexto_para_processamento(request.session)
    if not ctx_proc.get("id_avaliacao_esperado"):
        return JSONResponse(
            {
                "status": "erro",
                "mensagem": "Configure o simulado/avaliação no Painel antes de iniciar o processamento.",
            },
            status_code=400,
        )

    job_id = criar_job(total_arquivos=len(arquivos))
    pasta_upload = PASTA_UPLOADS_TEMP / f"upload_{job_id}"
    pasta_upload.mkdir(parents=True, exist_ok=True)
    try:
        total_salvos = await salvar_uploads_em_pasta(arquivos, pasta_upload)
        if total_salvos == 0:
            return JSONResponse({"status": "erro", "job_id": job_id, "mensagem": "Nenhuma imagem válida."})
        atualizar_job(job_id, status="processando", percentual=5, mensagem="Lendo imagens...")
        usuario_nome = str(request.session.get("user_name") or "").strip()
        try:
            automacao_processamento_iniciado(str(ctx_proc["id_avaliacao_esperado"]))
        except Exception:
            pass
        background_tasks.add_task(
            processar_pasta_temporaria_com_progresso,
            job_id,
            pasta_upload,
            ctx_proc["id_avaliacao_esperado"],
            ctx_proc.get("nome_simulado", ""),
            ctx_proc.get("meta_avaliacao"),
            usuario_nome,
        )
        return JSONResponse({"status": "ok", "job_id": job_id})
    except Exception as e:
        return JSONResponse({"status": "erro", "job_id": job_id, "mensagem": str(e)})
    
# Rota de compatibilidade para a tela de monitoramento/detalhes
@router.get("/processamento/{nome_avaliacao}")
async def redirecionar_processamento_antigo(nome_avaliacao: str):
    return RedirectResponse(url=f"/avaliacao/{nome_avaliacao}", status_code=301)

# Rota de compatibilidade caso o sistema tente monitorar o progresso com o nome antigo
@router.get("/processando/{job_id}", response_class=HTMLResponse)
async def tela_processando_compatibilidade(request: Request, job_id: str):
    return render_template(request, "processando.html", {"job_id": job_id})

def extrair_nome_lote_seguro(parametro: str) -> str:
    """
    Decodifica a URL e extrai o nome do lote mesmo se o frontend 
    enviar um dicionário Python formatado como string.
    """
    texto_puro = unquote(parametro).strip()
    
    # Se o frontend enviou um dicionário mascarado de string (ex: {'nome': '...'})
    if texto_puro.startswith("{") and ("['nome']" in texto_puro or "'nome'" in texto_puro):
        try:
            dados_dict = ast.literal_eval(texto_puro)
            if isinstance(dados_dict, dict) and "nome" in dados_dict:
                return str(dados_dict["nome"])
        except Exception:
            pass
            
    return texto_puro

@router.get("/validacao/{nome_processamento}/visualizar/{chave_imagem:path}")
async def validacao_visualizar_cartao(nome_processamento: str, chave_imagem: str):
    nome_limpo = extrair_nome_lote_seguro(nome_processamento)
    caminho = gerar_visualizacao_cartao_temporaria(nome_limpo, chave_imagem)
    if not caminho or not caminho.exists():
        return JSONResponse({"erro": "Visualização não disponível para este cartão."}, status_code=404)
    return FileResponse(
        str(caminho),
        media_type="image/jpeg",
        headers={"Cache-Control": "no-store, max-age=0"},
    )


@router.get("/validacao/{nome_avaliacao:path}", response_class=HTMLResponse)
async def validacao_cadastral(request: Request, nome_avaliacao: str):
    # Limpa a sujeira do link se ela existir
    nome_limpo = extrair_nome_lote_seguro(nome_avaliacao)
    
    # Se detectou que veio o dicionário no link, faz o redirecionamento limpo
    if nome_limpo != nome_avaliacao:
        return RedirectResponse(url=f"/validacao/{nome_limpo}", status_code=303)

    caminho = carregar_leitura(nome_limpo, em_json=True)
    if not caminho or not caminho.exists():
        return HTMLResponse("<h1>Avaliação não encontrada.</h1><p>O lote solicitado não existe no diretório.</p>", status_code=404)

    dados_validacao = gerar_validacao_cadastral(nome_limpo)
    resumo = dados_validacao["resumo"]
    leituras_lote = carregar_json(caminho_leituras(nome_limpo), {})
    id_avaliacao = (
        resumo.get("id_prova_processamento")
        or detectar_id_prova_do_processamento(leituras_lote, nome_limpo)
        or ""
    )
    id_aval_formulario = id_avaliacao or ID_PROVA_KEEPEDU
    return render_template(request, "validacao.html", {
        "nome_processamento": nome_limpo,
        "resumo": resumo,
        "validacoes": dados_validacao["validacoes"],
        "importacao_defaults": {"id_aval": id_aval_formulario},
        **_contexto_avaliacao_ui(request, nome_limpo, id_avaliacao),
    })


def _resolver_nome_imagem_keepedu(validacoes: dict, nome_imagem: str | None, id_aluno: str | int | None) -> str | None:
    if nome_imagem:
        return nome_imagem

    id_aluno_texto = str(id_aluno or "").strip()
    if not id_aluno_texto:
        return None

    for nome, validacao in validacoes.items():
        if str(validacao.get("id_final") or "").strip() == id_aluno_texto:
            return nome
    return None


def _resposta_bloqueio_leitura_incompativel(nome_avaliacao: str, nome_imagem: str) -> JSONResponse | None:
    leituras = carregar_json(caminho_leituras(nome_avaliacao), {})
    chave = resolver_chave_leitura(leituras, nome_imagem)
    dados = leituras.get(chave or nome_imagem) or {}
    if leitura_bloqueada_id_avaliacao(dados):
        return JSONResponse(
            {
                "status": "erro",
                "mensagem": (
                    "Cartão incompatível com a avaliação do lote. "
                    "Exclua a leitura e reprocesse o lote antes de enviar."
                ),
            },
            status_code=400,
        )
    return None


@router.post("/correcao/enviar-keepedu/{nome_avaliacao}")
async def rota_enviar_imagem_keepedu(nome_avaliacao: str, payload: ImportarImagemPayload):
    dados_validacao = gerar_validacao_cadastral(nome_avaliacao)
    validacoes = dados_validacao.get("validacoes", {})
    nome_imagem = _resolver_nome_imagem_keepedu(validacoes, payload.nome_imagem, payload.id_aluno)

    if not nome_imagem:
        return JSONResponse(
            {"status": "erro", "mensagem": "Informe a imagem ou um ID de aluno validado."},
            status_code=400,
        )

    bloqueio = _resposta_bloqueio_leitura_incompativel(nome_avaliacao, nome_imagem)
    if bloqueio:
        return bloqueio

    resultado = enviar_cartao_keepedu(
        nome_avaliacao,
        nome_imagem,
        id_aval=payload.id_aval,
        modelo_ia=payload.modelo_ia,
    )

    if resultado.get("status") == "erro":
        return JSONResponse(resultado, status_code=400)

    return JSONResponse(resultado)


@router.post("/correcao/enviar-imagens-keepedu/{nome_avaliacao}")
async def rota_enviar_imagem_keepedu_validacao(nome_avaliacao: str, payload: ImportarImagemPayload):
    dados_validacao = gerar_validacao_cadastral(nome_avaliacao)
    validacoes = dados_validacao.get("validacoes", {})
    nome_imagem = _resolver_nome_imagem_keepedu(validacoes, payload.nome_imagem, payload.id_aluno)

    if not nome_imagem:
        return JSONResponse(
            {"status": "erro", "mensagem": "Informe a imagem ou um ID de aluno validado."},
            status_code=400,
        )

    resultado = enviar_imagem_keepedu(
        nome_avaliacao,
        nome_imagem,
        id_aval=payload.id_aval,
    )

    if resultado.get("status") == "erro":
        return JSONResponse(resultado, status_code=400)

    return JSONResponse(resultado)


@router.post("/correcao/enviar-imagens-keepedu-todas/{nome_avaliacao}")
async def rota_enviar_todas_imagens_keepedu_validacao(nome_avaliacao: str, payload: ImportarTodasKeepEduPayload):
    resultado = enviar_todas_imagens_keepedu(
        nome_avaliacao,
        id_aval=payload.id_aval,
    )

    if resultado.get("status") == "erro" and not resultado.get("importadas"):
        return JSONResponse(resultado, status_code=400)

    return JSONResponse(resultado)


@router.post("/correcao/enviar-imagens-keepedu-ajax/{nome_avaliacao}")
async def rota_enviar_imagem_keepedu_ajax(
    nome_avaliacao: str,
    payload: ImportarImagemPayload,
    background_tasks: BackgroundTasks,
):
    dados_validacao = gerar_validacao_cadastral(nome_avaliacao)
    validacoes = dados_validacao.get("validacoes", {})
    nome_imagem = _resolver_nome_imagem_keepedu(validacoes, payload.nome_imagem, payload.id_aluno)

    if not nome_imagem:
        return JSONResponse(
            {"status": "erro", "mensagem": "Informe a imagem ou um ID de aluno validado."},
            status_code=400,
        )

    validacao_imagem = validacoes.get(nome_imagem, {})
    id_aluno = validacao_imagem.get("id_final")
    total_arquivos = contar_imagens_importacao(nome_avaliacao, id_aluno=id_aluno)
    job_id = criar_job(total_arquivos=max(total_arquivos, 1))
    background_tasks.add_task(
        _executar_envio_imagens_em_background,
        job_id,
        nome_avaliacao,
        payload.id_aval,
        id_aluno,
        False,
    )
    return JSONResponse({"status": "ok", "job_id": job_id, "mensagem": "Envio de imagem iniciado."})


@router.post("/correcao/enviar-imagens-keepedu-todas-ajax/{nome_avaliacao}")
async def rota_enviar_todas_imagens_keepedu_ajax(
    nome_avaliacao: str,
    payload: ImportarTodasKeepEduPayload,
    background_tasks: BackgroundTasks,
):
    total_arquivos = contar_imagens_importacao(nome_avaliacao)
    if total_arquivos <= 0:
        return JSONResponse(
            {
                "status": "erro",
                "mensagem": "Nenhuma imagem processada com ID validado para envio.",
            },
            status_code=400,
        )

    job_id = criar_job(total_arquivos=total_arquivos)
    background_tasks.add_task(
        _executar_envio_imagens_em_background,
        job_id,
        nome_avaliacao,
        payload.id_aval,
        None,
        True,
    )
    return JSONResponse({"status": "ok", "job_id": job_id, "mensagem": "Envio de imagens iniciado."})

@router.get("/correcao/{nome_avaliacao:path}", response_class=HTMLResponse)
async def correcao(request: Request, nome_avaliacao: str):
    # Limpa a sujeira do link se ela existir
    nome_limpo = extrair_nome_lote_seguro(nome_avaliacao)
    
    # Se detectou que veio o dicionário no link, faz o redirecionamento limpo
    if nome_limpo != nome_avaliacao:
        return RedirectResponse(url=f"/correcao/{nome_limpo}", status_code=303)

    imagens_debug = listar_imagens_debug(nome_limpo)
    leituras = carregar_json(caminho_leituras(nome_limpo), {})
    id_avaliacao = detectar_id_prova_do_processamento(leituras, nome_limpo) or ""
    try:
        automacao_lote_aberto(nome_limpo, id_avaliacao)
    except Exception:
        pass
    return render_template(request, "correcao.html", {
        "nome_processamento": nome_limpo,
        "imagens_debug": imagens_debug,
        "envios_keepedu": carregar_envios_keepedu(nome_limpo),
        **_contexto_avaliacao_ui(request, nome_limpo, id_avaliacao),
    })

@router.get("/painel", response_class=HTMLResponse)
async def painel(request: Request):
    processamentos = listar_processamentos_recentes()

    return render_template(
        request,
        "index.html",
        {
            "processamentos": processamentos,
            **_contexto_painel(request),
        },
    )
