import os
import csv
import json
import re
import shutil
from datetime import datetime
from pathlib import Path

from app.settings import PASTA_PROCESSAMENTOS, PASTA_UPLOADS_TEMP
from app.time_utils import agora_local, fromtimestamp_local
from omr_reader import (
    processar_imagens_omr,
    detectar_respostas_por_template,
    caminho_modelo_omr_padrao
)
from app.progresso import atualizar_job, registrar_evento_job


EXTENSOES_IMAGEM = [".jpg", ".jpeg", ".png"]


def _obter_data_hora_processamento(nome_pasta: str, pasta: Path) -> datetime:
    candidatos = [
        pasta / "log_leitura_omr.csv",
        pasta / "leituras_omr.json",
        pasta
    ]

    for candidato in candidatos:
        if candidato.exists():
            try:
                return fromtimestamp_local(candidato.stat().st_mtime)
            except OSError:
                pass

    match = re.match(r"^processamento_(\d{8})_(\d{6})$", str(nome_pasta or "").strip())

    if match:
        try:
            return datetime.strptime(f"{match.group(1)}_{match.group(2)}", "%Y%m%d_%H%M%S")
        except ValueError:
            pass

    return agora_local()


def _formatar_data_hora_processamento(nome_pasta: str, pasta: Path) -> tuple[str, str]:
    data_hora = _obter_data_hora_processamento(nome_pasta, pasta)
    return (
        data_hora.strftime("%d/%m/%Y"),
        data_hora.strftime("%H:%M:%S")
    )


def _rotulo_data_processamento(nome_pasta: str, pasta: Path) -> str:
    data_hora = _obter_data_hora_processamento(nome_pasta, pasta)
    hoje = agora_local().date()
    data_referencia = data_hora.date()

    if data_referencia == hoje:
        return "Hoje"
    if data_referencia == hoje.fromordinal(hoje.toordinal() - 1):
        return "Ontem"

    dias_semana = {
        0: "Segunda",
        1: "Terça",
        2: "Quarta",
        3: "Quinta",
        4: "Sexta",
        5: "Sábado",
        6: "Domingo",
    }
    return dias_semana.get(data_referencia.weekday(), "Lote")


from pathlib import Path
import os

def listar_processamentos():
    """
    Lista todos os lotes de cartões-resposta escaneados a partir
    do diretório configurado pelo aplicativo.
    """
    nomes_lotes = []

    if PASTA_PROCESSAMENTOS.exists() and PASTA_PROCESSAMENTOS.is_dir():
        for subpasta in PASTA_PROCESSAMENTOS.iterdir():
            if subpasta.is_dir() and not subpasta.name.startswith("."):
                nomes_lotes.append(subpasta.name)

    return sorted(nomes_lotes, reverse=True)


def listar_processamentos_recentes():
    """
    Monta os metadados exibidos na home para os lotes recentes.
    """
    lotes = []

    if not PASTA_PROCESSAMENTOS.exists() or not PASTA_PROCESSAMENTOS.is_dir():
        return lotes

    for subpasta in PASTA_PROCESSAMENTOS.iterdir():
        if not subpasta.is_dir() or subpasta.name.startswith("."):
            continue

        data_texto, hora_texto = _formatar_data_hora_processamento(subpasta.name, subpasta)
        lotes.append(
            {
                "nome": subpasta.name,
                "titulo": subpasta.name.replace("_", " "),
                "rotulo": _rotulo_data_processamento(subpasta.name, subpasta),
                "data": data_texto,
                "hora": hora_texto,
                "timestamp": _obter_data_hora_processamento(subpasta.name, subpasta),
            }
        )

    lotes.sort(key=lambda item: item["timestamp"], reverse=True)

    for item in lotes:
        item.pop("timestamp", None)

    return lotes


def resumo_processamento(nome_processamento: str):
    caminho_csv = caminho_log(nome_processamento)
    resumo = {
        "total_imagens": 0,
        "total_ok": 0,
        "total_erro": 0,
        "total_manual": 0,
        "total_pendencias": 0
    }

    if not caminho_csv.exists():
        return resumo

    with open(caminho_csv, "r", encoding="utf-8-sig", newline="") as f:
        leitor = csv.DictReader(f, delimiter=";")
        linhas = list(leitor)

    resumo["total_imagens"] = len(linhas)
    resumo["total_ok"] = sum(1 for linha in linhas if linha.get("status") == "OK")
    resumo["total_erro"] = sum(1 for linha in linhas if linha.get("status") == "ERRO")
    from app.log_omr_csv import linha_precisa_correcao_manual

    resumo["total_manual"] = sum(1 for linha in linhas if linha_precisa_correcao_manual(linha))

    caminho_json = caminho_leituras(nome_processamento)
    leituras = carregar_json(caminho_json, {})
    resumo["total_pendencias"] = sum(
        1
        for dados in leituras.values()
        if (dados.get("erros") or []) or int(dados.get("total_pendencias_confianca", 0) or 0) > 0
    )

    return resumo


def listar_imagens_debug(nome_processamento: str):
    pasta_debug = PASTA_PROCESSAMENTOS / nome_processamento / "debug_omr"
    imagens = set()

    if pasta_debug.exists():
        for arquivo in pasta_debug.iterdir():
            if arquivo.suffix.lower() in EXTENSOES_IMAGEM and arquivo.name.startswith("template_"):
                imagens.add(arquivo.name)

    caminho_leitura = caminho_leituras(nome_processamento)
    leituras = carregar_json(caminho_leitura, {})

    for nome_imagem in leituras.keys():
        if str(nome_imagem).startswith("template_"):
            imagens.add(str(nome_imagem))

    return sorted(imagens)


def processar_pasta_local(pasta_origem: str):
    pasta_origem = pasta_origem.strip().strip('"')

    if not os.path.exists(pasta_origem):
        raise FileNotFoundError(f"Pasta não encontrada: {pasta_origem}")

    if not os.path.isdir(pasta_origem):
        raise NotADirectoryError(f"O caminho informado não é uma pasta: {pasta_origem}")

    resultado = processar_imagens_omr(
        pasta_origem,
        pasta_saida=str(PASTA_PROCESSAMENTOS)
    )

    return os.path.basename(resultado["pasta_processamento"])


async def processar_uploads(arquivos):
    timestamp = agora_local().strftime("%Y%m%d_%H%M%S")
    pasta_upload = PASTA_UPLOADS_TEMP / f"upload_{timestamp}"
    pasta_upload.mkdir(parents=True, exist_ok=True)

    try:
        for arquivo in arquivos:
            # Quando o navegador envia uma pasta, o filename pode vir com subpastas.
            # Aqui pegamos apenas o nome final do arquivo para evitar erro de diretório.
            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_IMAGEM:
                continue

            destino = pasta_upload / nome_arquivo

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

        resultado = processar_imagens_omr(
            str(pasta_upload),
            pasta_saida=str(PASTA_PROCESSAMENTOS)
        )

        return os.path.basename(resultado["pasta_processamento"])

    finally:
        shutil.rmtree(pasta_upload, ignore_errors=True)


def caminho_imagem_debug(nome_processamento: str, nome_imagem: str):
    return PASTA_PROCESSAMENTOS / nome_processamento / "debug_omr" / nome_imagem

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 = str(nome_avaliacao or "").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 caminho_leituras(nome_processamento: str):
    return PASTA_PROCESSAMENTOS / nome_processamento / "leituras_omr.json"


def caminho_log(nome_processamento: str):
    return PASTA_PROCESSAMENTOS / nome_processamento / "log_leitura_omr.csv"


def caminho_correcoes_web(nome_processamento: str):
    pasta_manual = PASTA_PROCESSAMENTOS / nome_processamento / "manual_omr"
    pasta_manual.mkdir(parents=True, exist_ok=True)

    return pasta_manual / "correcoes_web.json"


def caminho_pasta_processamento(nome_processamento: str):
    return PASTA_PROCESSAMENTOS / nome_processamento


def caminho_originais(nome_processamento: str):
    pasta = caminho_pasta_processamento(nome_processamento) / "originais"
    pasta.mkdir(parents=True, exist_ok=True)
    return pasta


def caminho_manual_arquivo(nome_processamento: str, nome_arquivo: str):
    pasta_manual = caminho_pasta_processamento(nome_processamento) / "manual_omr"
    pasta_manual.mkdir(parents=True, exist_ok=True)
    nome_json = f"{os.path.splitext(nome_arquivo)[0]}.json"
    return pasta_manual / nome_json


def normalizar_nome_arquivo_debug(nome_imagem: str):
    return nome_imagem[9:] if nome_imagem.startswith("template_") else nome_imagem


def carregar_json(caminho, padrao):
    if not caminho.exists():
        return padrao

    with open(caminho, "r", encoding="utf-8") as f:
        return json.load(f)


def salvar_json(caminho, dados):
    with open(caminho, "w", encoding="utf-8") as f:
        json.dump(dados, f, ensure_ascii=False, indent=4)


def pontos_cantos_para_lista(pontos_cantos):
    if not pontos_cantos:
        return None

    chaves = ["TOP_LEFT", "TOP_RIGHT", "BOTTOM_LEFT", "BOTTOM_RIGHT"]
    pontos = []

    for chave in chaves:
        ponto = pontos_cantos.get(chave) if isinstance(pontos_cantos, dict) else None

        if not ponto:
            return None

        pontos.append([float(ponto["x"]), float(ponto["y"])])

    return pontos


def pontos_cantos_para_dict(pontos_cantos):
    chaves = ["TOP_LEFT", "TOP_RIGHT", "BOTTOM_LEFT", "BOTTOM_RIGHT"]

    def extrair_xy(ponto):
        if isinstance(ponto, dict):
            return float(ponto["x"]), float(ponto["y"])
        if isinstance(ponto, (list, tuple)) and len(ponto) >= 2:
            return float(ponto[0]), float(ponto[1])
        raise TypeError("Formato de ponto inválido")

    if isinstance(pontos_cantos, dict):
        saida = {}
        for chave in chaves:
            ponto = pontos_cantos.get(chave)
            if not ponto:
                continue
            try:
                x, y = extrair_xy(ponto)
                saida[chave] = {"x": x, "y": y}
            except (TypeError, ValueError, KeyError):
                continue
        return saida

    if isinstance(pontos_cantos, list) and len(pontos_cantos) == 4:
        saida = {}
        for chave, ponto in zip(chaves, pontos_cantos):
            try:
                x, y = extrair_xy(ponto)
                saida[chave] = {"x": x, "y": y}
            except (TypeError, ValueError, KeyError):
                continue
        return saida

    return {}


def resolver_chave_leitura(leituras: dict, nome_imagem: str) -> str:
    if not isinstance(leituras, dict) or not nome_imagem:
        return nome_imagem

    if nome_imagem in leituras:
        return nome_imagem

    stem_alvo = Path(normalizar_nome_arquivo_debug(nome_imagem)).stem.lower()

    for chave in leituras:
        if Path(normalizar_nome_arquivo_debug(chave)).stem.lower() == stem_alvo:
            return chave

    return nome_imagem


def _coletar_nomes_arquivo_imagem(nome_imagem: str, leitura_atual: dict | None = None) -> list[str]:
    leitura_atual = leitura_atual if isinstance(leitura_atual, dict) else {}
    nomes: list[str] = []

    def adicionar(valor):
        if not valor:
            return
        texto = os.path.basename(str(valor).strip())
        if texto and texto not in nomes:
            nomes.append(texto)

    adicionar(nome_imagem)
    adicionar(normalizar_nome_arquivo_debug(nome_imagem))
    adicionar(leitura_atual.get("arquivo_original"))
    adicionar(leitura_atual.get("nome_arquivo"))

    for nome in list(nomes):
        if nome and not nome.lower().startswith("template_"):
            adicionar(f"template_{nome}")

    return nomes


def _buscar_imagem_por_stem(pasta: Path, nome_arquivo: str):
    if not pasta.exists() or not pasta.is_dir():
        return None

    stem_alvo = Path(nome_arquivo).stem.lower()
    for arquivo in pasta.iterdir():
        if not arquivo.is_file():
            continue
        if arquivo.suffix.lower() not in EXTENSOES_IMAGEM:
            continue
        if arquivo.stem.lower() == stem_alvo:
            return arquivo

    return None


def localizar_imagem_original(nome_processamento: str, nome_imagem: str, leituras: dict):
    leitura_atual = leituras.get(nome_imagem, {}) if isinstance(leituras, dict) else {}
    nomes_arquivo = _coletar_nomes_arquivo_imagem(nome_imagem, leitura_atual)

    pasta_processamento = caminho_pasta_processamento(nome_processamento)
    pasta_originais = caminho_originais(nome_processamento)
    pasta_pendencias = pasta_processamento / "pendencias"
    pasta_debug = pasta_processamento / "debug_omr"

    candidatos = []

    caminho_salvo = leitura_atual.get("caminho_original_processamento", "")
    if caminho_salvo:
        candidatos.append(caminho_salvo)

    for nome_ref in nomes_arquivo:
        candidatos.extend([
            pasta_originais / nome_ref,
            pasta_pendencias / nome_ref,
            caminho_imagem_debug(nome_processamento, nome_ref),
        ])

        caminho_manual = caminho_manual_arquivo(nome_processamento, nome_ref)
        if caminho_manual.exists():
            dados_manual = carregar_json(caminho_manual, {})
            candidatos.extend([
                dados_manual.get("caminho_imagem_pendencia", ""),
                dados_manual.get("caminho_imagem", ""),
            ])

    for candidato in candidatos:
        if not candidato:
            continue

        caminho = candidato if hasattr(candidato, "exists") else Path(str(candidato))

        if caminho.exists():
            return caminho

    for nome_ref in nomes_arquivo:
        for pasta_busca in (pasta_originais, pasta_pendencias, pasta_debug):
            encontrado = _buscar_imagem_por_stem(pasta_busca, nome_ref)
            if encontrado:
                return encontrado

    return None


def atualizar_log_processamento(nome_processamento: str, nome_arquivo: str, resultado: dict, detalhe: str, caminho_manual: str):
    from app.log_omr_csv import LOG_OMR_COLUNAS, montar_linha_log, normalizar_linha_log_bruta

    caminho = caminho_log(nome_processamento)

    if not caminho.exists():
        return

    with open(caminho, "r", encoding="utf-8-sig", newline="") as f:
        leitor = csv.DictReader(f, delimiter=";")
        linhas = [normalizar_linha_log_bruta(linha) for linha in leitor]

    from app.log_omr_csv import calcular_precisa_correcao_manual

    erros = resultado.get("erros", []) or []

    atualizacao = montar_linha_log(
        arquivo=nome_arquivo,
        status="OK",
        registro_academico=resultado.get("registro_academico", ""),
        codigo_barras=resultado.get("codigo_barras", ""),
        total_perguntas_modelo=resultado.get("total_perguntas_modelo", 0),
        total_respostas_lidas=resultado.get("total_respostas_lidas", 0),
        precisa_correcao_manual=calcular_precisa_correcao_manual(
            "OK",
            resultado,
            erros,
            tem_json_manual=bool(caminho_manual),
        ),
        detalhe=detalhe,
    )

    linha_encontrada = False

    for linha in linhas:
        if linha.get("arquivo") == nome_arquivo:
            linha.update(atualizacao)
            linha_encontrada = True
            break

    if not linha_encontrada:
        linhas.append(atualizacao)

    with open(caminho, "w", encoding="utf-8-sig", newline="") as f:
        escritor = csv.DictWriter(f, fieldnames=LOG_OMR_COLUNAS, delimiter=";")
        escritor.writeheader()
        escritor.writerows(linhas)


def reprocessar_imagem_processamento(nome_processamento: str, nome_imagem: str, pontos_cantos: dict | None = None):
    caminho_leitura = caminho_leituras(nome_processamento)
    leituras = carregar_json(caminho_leitura, {})
    chave_leitura = resolver_chave_leitura(leituras, nome_imagem)

    caminho_imagem = localizar_imagem_original(nome_processamento, chave_leitura, leituras)

    if caminho_imagem is None:
        raise FileNotFoundError(
            "Nao foi possivel localizar a imagem original deste processamento. "
            "Gere um novo processamento para habilitar o reprocessamento por cantos."
        )

    caminho_modelo = caminho_modelo_omr_padrao()

    if not os.path.exists(caminho_modelo):
        raise FileNotFoundError(f"Modelo .xtmpl nao encontrado: {caminho_modelo}")

    leitura_atual = leituras.get(chave_leitura, {})
    cantos_enviados = pontos_cantos_para_dict(pontos_cantos or {})
    cantos_informados = pontos_cantos is not None and len(cantos_enviados) > 0

    if len(cantos_enviados) < 4:
        cantos_salvos = pontos_cantos_para_dict(leitura_atual.get("pontos_cantos") or {})
        if len(cantos_salvos) == 4:
            cantos_enviados = cantos_salvos

    if len(cantos_enviados) == 4:
        pontos_override = pontos_cantos_para_lista(cantos_enviados)
    elif cantos_informados:
        raise ValueError("Defina os 4 cantos do cartão antes de reprocessar.")
    else:
        pontos_override = None

    pasta_debug = caminho_pasta_processamento(nome_processamento) / "debug_omr"
    pasta_debug.mkdir(parents=True, exist_ok=True)

    resultado, detalhe = detectar_respostas_por_template(
        str(caminho_imagem),
        caminho_modelo,
        str(pasta_debug),
        pontos_imagem_override=pontos_override
    )

    if not resultado:
        raise ValueError(detalhe)

    from app.avaliacao_contexto import (
        normalizar_codigo_barras_para_id_configurado,
        resolver_id_avaliacao_esperado_lote,
        validar_cartao_para_avaliacao_lote,
    )

    nome_arquivo = normalizar_nome_arquivo_debug(chave_leitura)
    cantos_finais = cantos_enviados or pontos_cantos_para_dict(resultado.get("pontos_cantos", {}))
    origem_cantos = "MANUAL" if pontos_override is not None else str(resultado.get("origem_cantos") or "AUTO")

    id_esperado = resolver_id_avaliacao_esperado_lote(nome_processamento, leituras)
    permitir_id_divergente = bool(id_esperado)
    codigo_barras_lido = str(resultado.get("codigo_barras") or "")
    erros_leitura = list(resultado.get("erros") or [])
    cartao_ok, motivo_bloqueio = validar_cartao_para_avaliacao_lote(
        nome_arquivo,
        codigo_barras_lido,
        id_esperado,
        permitir_id_impresso_divergente=permitir_id_divergente,
    )
    if cartao_ok and permitir_id_divergente and codigo_barras_lido:
        codigo_normalizado = normalizar_codigo_barras_para_id_configurado(
            codigo_barras_lido,
            id_esperado,
        )
        if codigo_normalizado != codigo_barras_lido.strip():
            resultado["codigo_barras"] = codigo_normalizado
            codigo_barras_lido = codigo_normalizado
    if not cartao_ok:
        erros_leitura = [motivo_bloqueio]
        detalhe = motivo_bloqueio

    leitura_atual.update({
        "arquivo_original": nome_arquivo,
        "caminho_original_processamento": str(caminho_imagem),
        "respostas": {} if not cartao_ok else resultado.get("respostas", {}),
        "pontos_mapeados": resultado.get("pontos_mapeados", {}),
        "pontos_cantos": cantos_finais,
        "origem_cantos": origem_cantos,
        "registro_academico": resultado.get("registro_academico", ""),
        "codigo_barras": resultado.get("codigo_barras", ""),
        "debug_bolhas": resultado.get("debug_bolhas", ""),
        "erros": erros_leitura,
        "confianca_questoes": resultado.get("confianca_questoes", {}),
        "pendencias_confianca": resultado.get("pendencias_confianca", []),
        "total_pendencias_confianca": resultado.get("total_pendencias_confianca", 0)
    })
    if chave_leitura != nome_imagem and nome_imagem in leituras:
        leituras.pop(nome_imagem, None)

    leituras[chave_leitura] = leitura_atual
    leitura_atual["status_revisao"] = "PENDENTE"
    salvar_json(caminho_leitura, leituras)

    caminho_manual = caminho_manual_arquivo(nome_processamento, nome_arquivo)
    erros = erros_leitura or []
    resultado_log = dict(resultado)
    resultado_log["erros"] = erros

    if erros:
        salvar_json(caminho_manual, {
            "arquivo": nome_arquivo,
            "caminho_imagem": str(caminho_imagem),
            "caminho_imagem_pendencia": str(caminho_pasta_processamento(nome_processamento) / "pendencias" / nome_arquivo),
            "registro_academico": resultado.get("registro_academico", ""),
            "codigo_barras": resultado.get("codigo_barras", ""),
            "respostas": resultado.get("respostas", {}),
            "erros": erros,
            "pontos_mapeados": resultado.get("pontos_mapeados", {}),
            "corrigido_manualmente": resultado.get("origem_cantos", "") == "MANUAL"
        })
        caminho_manual_str = str(caminho_manual)
    else:
        if caminho_manual.exists():
            caminho_manual.unlink()
        caminho_manual_str = ""

    atualizar_log_processamento(
        nome_processamento,
        nome_arquivo,
        resultado_log,
        detalhe,
        caminho_manual_str
    )

    return {
        "status": "ok",
        "nome_imagem": chave_leitura,
        "arquivo_original": nome_arquivo,
        "detalhe": detalhe,
        "pontos_cantos": cantos_finais,
        "origem_cantos": origem_cantos,
        "erros": erros,
        "bloqueado": not cartao_ok,
        "resultado": resultado_log
    }


def remover_linha_log_processamento(nome_processamento: str, nomes_arquivo: list[str]):
    caminho = caminho_log(nome_processamento)
    if not caminho.exists():
        return

    stems_alvo = {Path(nome).stem.lower() for nome in nomes_arquivo if nome}

    with open(caminho, "r", encoding="utf-8-sig", newline="") as f:
        leitor = csv.DictReader(f, delimiter=";")
        linhas = list(leitor)
        cabecalho = leitor.fieldnames or []

    if not cabecalho:
        return

    linhas_filtradas = [
        linha for linha in linhas
        if Path(str(linha.get("arquivo", "")).strip()).stem.lower() not in stems_alvo
    ]

    with open(caminho, "w", encoding="utf-8-sig", newline="") as f:
        escritor = csv.DictWriter(f, fieldnames=cabecalho, delimiter=";")
        escritor.writeheader()
        escritor.writerows(linhas_filtradas)


def deletar_leitura_processamento(nome_processamento: str, nome_imagem: str):
    caminho_leitura = caminho_leituras(nome_processamento)
    leituras = carregar_json(caminho_leitura, {})

    chave_encontrada = None
    if nome_imagem in leituras:
        chave_encontrada = nome_imagem
    else:
        stem_alvo = Path(normalizar_nome_arquivo_debug(nome_imagem)).stem.lower()
        for chave in list(leituras.keys()):
            if Path(normalizar_nome_arquivo_debug(chave)).stem.lower() == stem_alvo:
                chave_encontrada = chave
                break

    if not chave_encontrada:
        raise FileNotFoundError("Leitura não encontrada neste lote.")

    leitura_atual = leituras.pop(chave_encontrada)
    salvar_json(caminho_leitura, leituras)

    nomes_arquivo = _coletar_nomes_arquivo_imagem(chave_encontrada, leitura_atual)
    pasta_processamento = caminho_pasta_processamento(nome_processamento)

    for nome_ref in nomes_arquivo:
        for subpasta in ("debug_omr", "originais", "pendencias"):
            caminho_arquivo = pasta_processamento / subpasta / nome_ref
            if caminho_arquivo.exists():
                caminho_arquivo.unlink(missing_ok=True)

        caminho_manual = caminho_manual_arquivo(nome_processamento, nome_ref)
        if caminho_manual.exists():
            caminho_manual.unlink(missing_ok=True)

        stem_ref = Path(nome_ref).stem
        caminho_manual_stem = pasta_processamento / "manual_omr" / f"{stem_ref}.json"
        if caminho_manual_stem.exists():
            caminho_manual_stem.unlink(missing_ok=True)

    caminho_correcoes = caminho_correcoes_web(nome_processamento)
    if caminho_correcoes.exists():
        correcoes = carregar_json(caminho_correcoes, {})
        if isinstance(correcoes, dict):
            for chave in list(correcoes.keys()):
                if Path(normalizar_nome_arquivo_debug(chave)).stem.lower() == Path(normalizar_nome_arquivo_debug(chave_encontrada)).stem.lower():
                    correcoes.pop(chave, None)
            salvar_json(caminho_correcoes, correcoes)

    remover_linha_log_processamento(nome_processamento, nomes_arquivo)

    return {
        "status": "ok",
        "nome_imagem": chave_encontrada,
        "total_restante": len(leituras),
    }


def deletar_processamento(nome_processamento: str):
    nome = str(nome_processamento or "").strip()
    if not nome or nome.startswith("."):
        raise ValueError("Nome de lote inválido.")

    pasta = caminho_pasta_processamento(nome)
    if not pasta.exists() or not pasta.is_dir():
        raise FileNotFoundError("Lote não encontrado.")

    shutil.rmtree(pasta)

    return {
        "status": "ok",
        "nome_processamento": nome,
    }


def reprocessar_lote_com_progresso(job_id: str, nome_processamento: str):
    from app.avaliacao_contexto import (
        extrair_id_avaliacao_do_nome_arquivo,
        resolver_id_avaliacao_esperado_lote,
    )
    from app.validacao_cadastral import leitura_bloqueada_id_avaliacao

    caminho_leitura = caminho_leituras(nome_processamento)
    leituras = carregar_json(caminho_leitura, {})
    pasta_originais = caminho_originais(nome_processamento)
    id_esperado = resolver_id_avaliacao_esperado_lote(nome_processamento, leituras)

    imagens = list(leituras.keys())
    if not imagens and pasta_originais.exists():
        imagens = [
            arquivo.name
            for arquivo in pasta_originais.iterdir()
            if arquivo.is_file() and arquivo.suffix.lower() in EXTENSOES_IMAGEM
        ]

    if id_esperado:
        for nome_imagem in imagens:
            id_nome = extrair_id_avaliacao_do_nome_arquivo(nome_imagem)
            if id_nome and id_nome != id_esperado:
                registrar_evento_job(
                    job_id,
                    tipo="warning",
                    titulo="ID impresso divergente no arquivo",
                    descricao=(
                        f"Arquivo com prefixo {id_nome}; o lote usa o ID configurado ({id_esperado}). "
                        "A leitura seguirá com o ID do painel."
                    ),
                    arquivo=normalizar_nome_arquivo_debug(nome_imagem),
                )

    total = len(imagens)
    if total == 0:
        raise ValueError("Nenhuma imagem restante para reprocessar neste lote.")

    registrar_evento_job(
        job_id,
        tipo="info",
        titulo="Reprocessamento iniciado",
        descricao=f"{total} cartão(ões) na fila de leitura OMR.",
    )
    atualizar_job(
        job_id,
        status="processando",
        percentual=5,
        mensagem="Preparando reprocessamento do lote...",
        nome_processamento=nome_processamento,
    )

    erros_total = 0

    for indice, nome_imagem in enumerate(imagens, start=1):
        percentual = 5 + int((indice / total) * 90)
        atualizar_job(
            job_id,
            status="processando",
            percentual=percentual,
            mensagem=f"Reprocessando cartão {indice} de {total}...",
            arquivo_atual=normalizar_nome_arquivo_debug(nome_imagem),
            nome_processamento=nome_processamento,
        )

        try:
            resultado_reprocess = reprocessar_imagem_processamento(
                nome_processamento=nome_processamento,
                nome_imagem=nome_imagem,
                pontos_cantos=None,
            )
            erros_cartao = resultado_reprocess.get("erros") or []
            if resultado_reprocess.get("bloqueado") or leitura_bloqueada_id_avaliacao({"erros": erros_cartao}):
                erros_total += 1
                registrar_evento_job(
                    job_id,
                    tipo="error",
                    titulo="Cartão bloqueado",
                    descricao=erros_cartao[0] if erros_cartao else "Cartão incompatível com a avaliação do lote.",
                    arquivo=normalizar_nome_arquivo_debug(nome_imagem),
                )
            else:
                registrar_evento_job(
                    job_id,
                    tipo="success",
                    titulo="Cartão reprocessado",
                    arquivo=normalizar_nome_arquivo_debug(nome_imagem),
                )
        except Exception as erro:
            erros_total += 1
            registrar_evento_job(
                job_id,
                tipo="error",
                titulo="Falha ao reprocessar cartão",
                descricao=str(erro),
                arquivo=normalizar_nome_arquivo_debug(nome_imagem),
            )

    resumo = resumo_processamento(nome_processamento)
    mensagem_final = "Reprocessamento do lote concluído."
    if erros_total:
        mensagem_final = f"Reprocessamento concluído com {erros_total} falha(s)."

    atualizar_job(
        job_id,
        status="concluido",
        percentual=100,
        mensagem=mensagem_final,
        nome_processamento=nome_processamento,
        resumo=resumo,
    )
    registrar_evento_job(
        job_id,
        tipo="success" if erros_total == 0 else "warning",
        titulo="Reprocessamento finalizado",
        descricao=mensagem_final,
    )

    return {
        "status": "ok",
        "nome_processamento": nome_processamento,
        "total": total,
        "erros": erros_total,
        "resumo": resumo,
    }


def processar_pasta_temporaria_com_progresso(
    job_id,
    pasta_upload,
    id_avaliacao_esperado=None,
    nome_simulado=None,
    meta_avaliacao=None,
    usuario_nome: str = "",
):
    nome_processamento = None
    try:
        arquivos = [
            arquivo for arquivo in pasta_upload.iterdir()
            if arquivo.suffix.lower() in EXTENSOES_IMAGEM
        ]
        total = len(arquivos)

        registrar_evento_job(
            job_id,
            tipo="info",
            titulo="Processamento iniciado",
            descricao="Arquivos recebidos e fila de leitura preparada."
        )
        atualizar_job(
            job_id,
            status="processando",
            percentual=10,
            mensagem="Arquivos recebidos. Iniciando leitura OMR...",
            resumo={
                "info": 0,
                "warning": 0,
                "error": 0,
                "success": 0,
                "total_eventos": 0,
                "total_imagens": total,
                "total_ok": 0,
                "total_erro": 0,
                "total_manual": 0
            }
        )

        if total == 0:
            raise ValueError("Nenhuma imagem válida encontrada para processamento.")

        def progresso_callback(atual, total_arquivos, nome_arquivo=""):
            percentual = 10 + int((atual / total_arquivos) * 85)

            atualizar_job(
                job_id,
                status="processando",
                percentual=percentual,
                mensagem=f"Lendo cartão {atual} de {total_arquivos}...",
                arquivo_atual=nome_arquivo
            )

        def evento_callback(tipo, titulo, descricao="", arquivo=""):
            registrar_evento_job(
                job_id,
                tipo=tipo,
                titulo=titulo,
                descricao=descricao,
                arquivo=arquivo
            )

        kwargs_omr = {
            "pasta_saida": str(PASTA_PROCESSAMENTOS),
            "progresso_callback": progresso_callback,
            "evento_callback": evento_callback,
        }
        if id_avaliacao_esperado:
            kwargs_omr["id_avaliacao_esperado"] = id_avaliacao_esperado

        try:
            resultado = processar_imagens_omr(str(pasta_upload), **kwargs_omr)
        except TypeError:
            atualizar_job(
                job_id,
                status="processando",
                percentual=35,
                mensagem="Lendo cartões OMR..."
            )
            kwargs_fallback = {"pasta_saida": str(PASTA_PROCESSAMENTOS)}
            if id_avaliacao_esperado:
                kwargs_fallback["id_avaliacao_esperado"] = id_avaliacao_esperado
            resultado = processar_imagens_omr(str(pasta_upload), **kwargs_fallback)

        nome_processamento = os.path.basename(resultado["pasta_processamento"])

        if id_avaliacao_esperado:
            from app.avaliacao_contexto import salvar_meta_avaliacao_no_lote

            salvar_meta_avaliacao_no_lote(
                nome_processamento,
                str(id_avaliacao_esperado),
                str(nome_simulado or ""),
                meta_extra=meta_avaliacao if isinstance(meta_avaliacao, dict) else None,
            )
        resumo_atual = {
            "info": 0,
            "warning": 0,
            "error": 0,
            "success": 0,
            "total_eventos": 0,
            "total_imagens": int(resultado.get("total_imagens", 0)),
            "total_ok": int(resultado.get("total_ok", 0)),
            "total_erro": int(resultado.get("total_erro", 0)),
            "total_manual": int(resultado.get("total_manual", 0))
        }

        try:
            from app.progresso import obter_job
            resumo_atual.update(obter_job(job_id).get("resumo", {}))
            resumo_atual.update({
                "total_imagens": int(resultado.get("total_imagens", 0)),
                "total_ok": int(resultado.get("total_ok", 0)),
                "total_erro": int(resultado.get("total_erro", 0)),
                "total_manual": int(resultado.get("total_manual", 0))
            })
        except Exception:
            pass

        atualizar_job(
            job_id,
            status="concluido",
            percentual=100,
            mensagem="Processamento concluído.",
            nome_processamento=nome_processamento,
            resumo=resumo_atual
        )
        registrar_evento_job(
            job_id,
            tipo="success",
            titulo="Processamento concluído",
            descricao="Os arquivos já podem ser revisados no painel."
        )

        if id_avaliacao_esperado and nome_processamento:
            try:
                from app.kanban import automacao_processamento_iniciado

                automacao_processamento_iniciado(
                    str(id_avaliacao_esperado),
                    nome_processamento=nome_processamento,
                    usuario=str(usuario_nome or "").strip(),
                    total_cartoes=int(resultado.get("total_imagens", 0)),
                )
            except Exception:
                pass
    except Exception as e:
        registrar_evento_job(
            job_id,
            tipo="error",
            titulo="Erro durante o processamento",
            descricao=str(e)
        )
        atualizar_job(
            job_id,
            status="erro",
            percentual=100,
            mensagem="Erro durante o processamento.",
            erro=str(e)
        )

    finally:
        shutil.rmtree(pasta_upload, ignore_errors=True)
        
def limpar_pos_importacao_keepedu(nome_processamento: str, imagens_enviadas: list[str] | None = None):
    try:
        print(f"[LIMPEZA KEEPEDU] Iniciando limpeza para imagens enviadas em: {nome_processamento}")

        if not imagens_enviadas:
            print("[LIMPEZA KEEPEDU] Nenhuma imagem para limpar.")
            return

        pasta_processamento = caminho_pasta_processamento(nome_processamento)
        leituras = carregar_json(caminho_leituras(nome_processamento), {})

        print(f"[DEBUG LIMPEZA] Pasta encontrada: {pasta_processamento}")
        print(f"[DEBUG LIMPEZA] Existe? {pasta_processamento.exists()}")

        for nome_imagem_original in imagens_enviadas:
            nomes_para_limpar: set[str] = set()
            nome_limpo = str(nome_imagem_original or "").strip()
            if nome_limpo:
                nomes_para_limpar.add(nome_limpo)
                stem = Path(nome_limpo).stem
                nomes_para_limpar.add(f"template_{stem}.jpg")
                nomes_para_limpar.add(f"template_{stem}.jpeg")
                if not nome_limpo.lower().startswith("template_"):
                    nomes_para_limpar.add(f"template_{nome_limpo}")

            for chave, dados in (leituras or {}).items():
                if not isinstance(dados, dict):
                    continue
                arquivo = str(dados.get("arquivo_original") or "").strip()
                stem_chave = Path(str(chave)).stem
                stem_alvo = Path(nome_limpo).stem if nome_limpo else ""
                if (
                    arquivo == nome_limpo
                    or stem_chave == stem_alvo
                    or str(chave).strip() == nome_limpo
                ):
                    nomes_para_limpar.add(str(chave).strip())
                    if arquivo:
                        nomes_para_limpar.add(arquivo)
                    debug_bolhas = str(dados.get("debug_bolhas") or "").strip()
                    if debug_bolhas:
                        Path(debug_bolhas).unlink(missing_ok=True)

            for nome_arquivo in nomes_para_limpar:
                if not nome_arquivo:
                    continue

                caminho_debug = pasta_processamento / "debug_omr" / nome_arquivo
                if caminho_debug.exists():
                    caminho_debug.unlink(missing_ok=True)

                caminho_original = pasta_processamento / "originais" / nome_arquivo
                if caminho_original.exists():
                    caminho_original.unlink(missing_ok=True)

                caminho_pendencia = pasta_processamento / "pendencias" / nome_arquivo
                if caminho_pendencia.exists():
                    caminho_pendencia.unlink(missing_ok=True)

                nome_base_sem_ext = Path(nome_arquivo).stem
                caminho_manual = pasta_processamento / "manual_omr" / f"{nome_base_sem_ext}.json"
                if caminho_manual.exists():
                    caminho_manual.unlink(missing_ok=True)

        print(f"[LIMPEZA KEEPEDU] Limpeza de {len(imagens_enviadas)} imagem(ns) concluída.")
    except Exception:
        import traceback
        print("[ERRO LIMPEZA KEEPEDU]")
        traceback.print_exc()