"""Armazenamento de lotes OMR: um JSON identificável para cada processamento."""
from __future__ import annotations

import json
import os
import re
import unicodedata
from contextlib import contextmanager
from contextvars import ContextVar
from pathlib import Path
from typing import Any

from app.settings import PASTA_PROCESSAMENTOS, nome_pasta_processamentos_para_escola
from app.tenant import get_database_name, get_school
from app.db import get_db_session, storage_usa_banco
from app.lote_estado import compactar_estado_lote
from app.lote_leitura import hidratar_dados_leitura, montar_dados_leitura, separar_dados_leitura

_cache_lotes: dict[Path, tuple[int, dict[str, Any]]] = {}
_cache_indice: tuple[Path, int, dict[str, Path]] | None = None
_cache_lotes_db: dict[str, dict[str, Any]] = {}
_cache_lotes_operacao: ContextVar[dict[str, dict[str, Any]] | None] = ContextVar(
    "cache_lotes_operacao",
    default=None,
)


def _copiar_lote(dados: dict[str, Any]) -> dict[str, Any]:
    return {
        "metadados": dict(dados.get("metadados") or {}),
        "leituras": dict(dados.get("leituras") or {}),
        "estado": dict(dados.get("estado") or {}),
    }


@contextmanager
def cache_lotes_durante_operacao():
    """Reutiliza lotes somente dentro de uma operação síncrona de leitura."""
    existente = _cache_lotes_operacao.get()
    if existente is not None:
        yield
        return

    token = _cache_lotes_operacao.set({})
    try:
        yield
    finally:
        _cache_lotes_operacao.reset(token)


def pasta_lotes() -> Path:
    pasta = PASTA_PROCESSAMENTOS / nome_pasta_processamentos_para_escola(get_school())
    if not storage_usa_banco():
        pasta.mkdir(parents=True, exist_ok=True)
    return pasta


def _nome_lote_seguro(nome_processamento: str) -> str:
    nome = str(nome_processamento or "").strip()
    if not re.fullmatch(r"[A-Za-z0-9_-]+", nome):
        raise ValueError("Identificador de processamento inválido.")
    return nome


def _slug_arquivo(texto: str) -> str:
    normalizado = unicodedata.normalize("NFKD", str(texto or ""))
    ascii_texto = normalizado.encode("ascii", "ignore").decode("ascii").lower()
    return re.sub(r"[^a-z0-9]+", "-", ascii_texto).strip("-")[:80]


def nome_arquivo_lote(nome_processamento: str, metadados: dict | None = None) -> str:
    """Nome legível, com sufixo técnico para não colidir em reprocessamentos."""
    nome = _nome_lote_seguro(nome_processamento)
    meta = dict(metadados or {})
    id_avaliacao = str(meta.get("id_avaliacao") or meta.get("id") or "").strip()
    nome_simulado = str(meta.get("nome_simulado") or meta.get("tipo_prova") or "").strip()
    partes = [parte for parte in (f"avaliacao-{id_avaliacao}" if id_avaliacao else "", _slug_arquivo(nome_simulado)) if parte]
    prefixo = "_".join(partes) or "avaliacao-sem-identificacao"
    return f"{prefixo}__{nome}.json"


def _carregar_arquivo(caminho: Path) -> dict[str, Any]:
    try:
        with open(caminho, "r", encoding="utf-8") as arquivo:
            dados = json.load(arquivo)
    except (OSError, json.JSONDecodeError):
        return {}
    if not isinstance(dados, dict):
        return {}
    if not isinstance(dados.get("metadados"), dict) or not isinstance(dados.get("leituras"), dict):
        return {}
    return dados


def _indice_lotes() -> dict[str, Path]:
    global _cache_indice
    pasta = pasta_lotes()
    try:
        mtime_ns = pasta.stat().st_mtime_ns
    except OSError:
        return {}
    if _cache_indice and _cache_indice[0] == pasta and _cache_indice[1] == mtime_ns:
        return _cache_indice[2]
    indice: dict[str, Path] = {}
    # Reconhece o layout legado (JSON na raiz) e o layout por lote
    # (``processamento_<id>/lote.json``).
    for arquivo in pasta.rglob("*.json"):
        dados = _carregar_arquivo(arquivo)
        nome = str((dados.get("metadados") or {}).get("id_processamento") or "").strip()
        if nome:
            indice[nome] = arquivo
    _cache_indice = (pasta, mtime_ns, indice)
    return indice


def _invalidar_indice() -> None:
    global _cache_indice
    _cache_indice = None


def caminho_lote(nome_processamento: str) -> Path:
    """Resolve o arquivo individual pelo ID estável do processamento."""
    nome = _nome_lote_seguro(nome_processamento)
    return _indice_lotes().get(nome, pasta_lotes() / nome / "lote.json")


def obter_lote(nome_processamento: str) -> dict[str, Any]:
    if storage_usa_banco():
        from app.db_models import LoteLeituraModel, LoteMarcacaoModel, LoteProcessamentoModel
        nome = _nome_lote_seguro(nome_processamento)
        chave_cache = f"{get_database_name()}:{get_school()}:{nome}"
        cache_operacao = _cache_lotes_operacao.get()
        if cache_operacao is not None and chave_cache in cache_operacao:
            return _copiar_lote(cache_operacao[chave_cache])
        # Não reutilize o cache em memória no backend MySQL. Em produção há
        # mais de um worker e cada processo mantém seu próprio dicionário; uma
        # exclusão feita por um worker não invalidava o lote já lido por outro,
        # fazendo a Validação ressuscitar cartões removidos na Correção.
        with get_db_session() as session:
            lote = session.query(LoteProcessamentoModel).filter_by(
                nome_processamento=nome, schoolname=get_school()
            ).one_or_none()
            if not lote:
                _cache_lotes_db.pop(chave_cache, None)
                if cache_operacao is not None:
                    cache_operacao[chave_cache] = {}
                return {}
            linhas = session.query(LoteLeituraModel).filter_by(lote_id=lote.id).all()
            marcacoes = {
                item.leitura_id: item.pontos_mapeados_zlib_v1
                for item in session.query(LoteMarcacaoModel).filter(
                    LoteMarcacaoModel.leitura_id.in_([linha.id for linha in linhas])
                ).all()
            } if linhas else {}
            leituras = {
                linha.chave: montar_dados_leitura({
                    "arquivo_original": linha.arquivo_original,
                    "registro_academico": linha.registro_academico,
                    "codigo_barras": linha.codigo_barras,
                    "respostas": linha.respostas,
                    "erros": linha.erros,
                    "pontos_cantos": linha.pontos_cantos,
                    "origem_cantos": linha.origem_cantos,
                }, linha.dados, marcacoes.get(linha.id, ""))
                for linha in linhas
            }
            dados = {"metadados": dict(lote.metadados or {}), "leituras": leituras, "estado": dict(lote.estado or {})}
            _cache_lotes_db.pop(chave_cache, None)
            if cache_operacao is not None:
                cache_operacao[chave_cache] = dados
            return _copiar_lote(dados)
    caminho = caminho_lote(nome_processamento)
    try:
        mtime_ns = caminho.stat().st_mtime_ns
    except OSError:
        return {}
    em_cache = _cache_lotes.get(caminho)
    if em_cache and em_cache[0] == mtime_ns:
        return em_cache[1]
    dados = _carregar_arquivo(caminho)
    _cache_lotes[caminho] = (mtime_ns, dados)
    return dados


def listar_lotes() -> list[str]:
    if storage_usa_banco():
        from app.db_models import LoteProcessamentoModel
        with get_db_session() as session:
            return [item[0] for item in session.query(LoteProcessamentoModel.nome_processamento).filter_by(
                schoolname=get_school()
            ).order_by(LoteProcessamentoModel.criado_em.desc()).all()]
    return sorted(_indice_lotes(), reverse=True)


def listar_lotes_com_dados() -> list[tuple[str, dict[str, Any]]]:
    if storage_usa_banco():
        from app.db_models import LoteLeituraModel, LoteMarcacaoModel, LoteProcessamentoModel

        with get_db_session() as session:
            lotes = session.query(LoteProcessamentoModel).filter_by(
                schoolname=get_school()
            ).order_by(
                LoteProcessamentoModel.criado_em.desc()
            ).all()
            if not lotes:
                return []

            ids = [lote.id for lote in lotes]
            linhas = session.query(LoteLeituraModel).filter(
                LoteLeituraModel.lote_id.in_(ids)
            ).all()

            leituras_por_lote: dict[int, dict[str, Any]] = {lote_id: {} for lote_id in ids}
            for linha in linhas:
                leituras_por_lote.setdefault(linha.lote_id, {})[linha.chave] = (
                    hidratar_dados_leitura(linha.dados)
                )

            cache_operacao = _cache_lotes_operacao.get()
            resultado: list[tuple[str, dict[str, Any]]] = []
            banco = get_database_name()
            for lote in lotes:
                nome = str(lote.nome_processamento)
                dados = {
                    "metadados": dict(lote.metadados or {}),
                    "leituras": leituras_por_lote.get(lote.id, {}),
                    "estado": dict(lote.estado or {}),
                }
                if cache_operacao is not None:
                    cache_operacao[f"{banco}:{nome}"] = dados
                resultado.append((nome, _copiar_lote(dados)))
            return resultado

    return [(nome, obter_lote(nome)) for nome in listar_lotes()]


def listar_lotes_resumidos() -> list[tuple[str, dict[str, Any]]]:
    """Lista lotes sem carregar o JSON volumoso de cada leitura."""
    if storage_usa_banco():
        from sqlalchemy import func
        from app.db_models import LoteLeituraModel, LoteProcessamentoModel

        with get_db_session() as session:
            lotes = session.query(
                LoteProcessamentoModel.id,
                LoteProcessamentoModel.nome_processamento,
                LoteProcessamentoModel.metadados,
                LoteProcessamentoModel.estado,
                LoteProcessamentoModel.criado_em,
            ).filter_by(schoolname=get_school()).order_by(LoteProcessamentoModel.criado_em.desc()).all()
            contagens = {}
            if lotes:
                ids = [lote.id for lote in lotes]
                contagens = dict(
                    session.query(
                        LoteLeituraModel.lote_id,
                        func.count(LoteLeituraModel.id),
                    ).filter(LoteLeituraModel.lote_id.in_(ids)).group_by(
                        LoteLeituraModel.lote_id
                    ).all()
                )

        resultado = []
        for lote in lotes:
            estado = dict(lote.estado or {})
            resultado.append((
                str(lote.nome_processamento),
                {
                    "metadados": dict(lote.metadados or {}),
                    "estado": estado,
                    "total_leituras": int(contagens.get(lote.id, 0) or 0),
                    "criado_em": lote.criado_em,
                },
            ))
        return resultado

    from app.services import _calcular_resumo_leituras

    resultado = []
    for nome in listar_lotes():
        lote = obter_lote(nome)
        estado = dict(lote.get("estado") or {})
        # Só recalcula se o lote antigo não tiver resumo persistido.
        if not isinstance(estado.get("resumo_processamento"), dict):
            estado["resumo_processamento"] = _calcular_resumo_leituras(
                lote.get("leituras") or {}
            )
        resultado.append((
            nome,
            {
                "metadados": dict(lote.get("metadados") or {}),
                "estado": estado,
                "total_leituras": len(lote.get("leituras") or {}),
                "criado_em": None,
            },
        ))
    return resultado


def obter_estado_resumo_lote(nome_processamento: str) -> dict[str, Any]:
    """Metadados operacionais leves (sem hidratar todas as leituras)."""
    nome = _nome_lote_seguro(nome_processamento)
    if not nome:
        return {
            "metadados": {},
            "estado": {},
            "total_leituras": 0,
            "resumo_processamento": {},
        }

    if storage_usa_banco():
        from sqlalchemy import func
        from app.db_models import LoteLeituraModel, LoteProcessamentoModel

        with get_db_session() as session:
            lote = (
                session.query(
                    LoteProcessamentoModel.id,
                    LoteProcessamentoModel.metadados,
                    LoteProcessamentoModel.estado,
                )
                .filter_by(nome_processamento=nome, schoolname=get_school())
                .one_or_none()
            )
            if not lote:
                return {
                    "metadados": {},
                    "estado": {},
                    "total_leituras": 0,
                    "resumo_processamento": {},
                }
            total = int(
                session.query(func.count(LoteLeituraModel.id))
                .filter_by(lote_id=lote.id)
                .scalar()
                or 0
            )
            estado = dict(lote.estado or {})
            resumo = estado.get("resumo_processamento")
            if not isinstance(resumo, dict):
                resumo = {
                    "total_imagens": total,
                    "total_ok": total,
                    "total_erro": 0,
                    "total_manual": 0,
                    "total_pendencias": 0,
                }
            return {
                "metadados": dict(lote.metadados or {}),
                "estado": estado,
                "total_leituras": total,
                "resumo_processamento": dict(resumo),
            }

    lote = obter_lote(nome)
    if not lote:
        return {
            "metadados": {},
            "estado": {},
            "total_leituras": 0,
            "resumo_processamento": {},
        }
    estado = dict(lote.get("estado") or {})
    resumo = estado.get("resumo_processamento")
    total = len(lote.get("leituras") or {})
    if not isinstance(resumo, dict):
        from app.services import _calcular_resumo_leituras

        resumo = _calcular_resumo_leituras(lote.get("leituras") or {})
    return {
        "metadados": dict(lote.get("metadados") or {}),
        "estado": estado,
        "total_leituras": total,
        "resumo_processamento": dict(resumo),
    }


def salvar_lote(
    nome_processamento: str,
    metadados: dict,
    leituras: dict,
    estado: dict | None = None,
) -> None:
    nome = _nome_lote_seguro(nome_processamento)
    meta = {**dict(metadados or {}), "id_processamento": nome}
    estado_compacto = compactar_estado_lote(estado) if estado is not None else None
    if storage_usa_banco():
        from app.db_models import LoteLeituraModel, LoteProcessamentoModel
        with get_db_session() as session:
            lote = session.query(LoteProcessamentoModel).filter_by(
                nome_processamento=nome, schoolname=get_school()
            ).one_or_none()
            if not lote:
                lote = LoteProcessamentoModel(nome_processamento=nome, schoolname=get_school())
                session.add(lote)
                session.flush()
            lote.metadados = meta
            lote.leituras = {}
            lote.estado = (
                estado_compacto
                if estado_compacto is not None
                else compactar_estado_lote(lote.estado or {})
            )
            linhas_antigas = session.query(LoteLeituraModel).filter_by(lote_id=lote.id).all()
            if linhas_antigas:
                session.query(LoteMarcacaoModel).filter(
                    LoteMarcacaoModel.leitura_id.in_([linha.id for linha in linhas_antigas])
                ).delete(synchronize_session=False)
            session.query(LoteLeituraModel).filter_by(lote_id=lote.id).delete()
            for chave, dados in dict(leituras or {}).items():
                campos, residual, marcacoes = separar_dados_leitura(dados)
                leitura = LoteLeituraModel(
                    lote_id=lote.id, chave=str(chave), dados=residual,
                    arquivo_original=str(campos.get("arquivo_original") or ""),
                    registro_academico=str(campos.get("registro_academico") or ""),
                    codigo_barras=str(campos.get("codigo_barras") or ""),
                    respostas=campos.get("respostas") if isinstance(campos.get("respostas"), dict) else {},
                    erros=campos.get("erros") if isinstance(campos.get("erros"), list) else [],
                    pontos_cantos=campos.get("pontos_cantos") if isinstance(campos.get("pontos_cantos"), dict) else {},
                    origem_cantos=str(campos.get("origem_cantos") or ""),
                )
                session.add(leitura)
                session.flush()
                if marcacoes:
                    session.add(LoteMarcacaoModel(leitura_id=leitura.id, pontos_mapeados_zlib_v1=marcacoes))
        _cache_lotes_db.pop(f"{get_database_name()}:{get_school()}:{nome}", None)
        return
    caminho_anterior = caminho_lote(nome)
    caminho = pasta_lotes() / nome / "lote.json"
    if estado_compacto is None:
        estado_compacto = compactar_estado_lote(
            obter_lote(nome).get("estado") or {}
        )
    dados = {
        "metadados": meta,
        "leituras": dict(leituras or {}),
        "estado": estado_compacto,
    }
    caminho.parent.mkdir(parents=True, exist_ok=True)
    temporario = caminho.with_suffix(".json.tmp")
    with open(temporario, "w", encoding="utf-8") as arquivo:
        json.dump(dados, arquivo, ensure_ascii=False, indent=2)
    os.replace(temporario, caminho)
    if caminho_anterior != caminho and caminho_anterior.is_file():
        caminho_anterior.unlink()
        _cache_lotes.pop(caminho_anterior, None)
    _cache_lotes[caminho] = (caminho.stat().st_mtime_ns, dados)
    _invalidar_indice()


def obter_estado_lote(nome_processamento: str) -> dict[str, Any]:
    """Dados mutaveis persistidos no mesmo JSON das leituras."""
    if storage_usa_banco():
        from app.db_models import LoteProcessamentoModel

        nome = _nome_lote_seguro(nome_processamento)
        with get_db_session() as session:
            lote = session.query(LoteProcessamentoModel).filter_by(
                nome_processamento=nome, schoolname=get_school()
            ).one_or_none()
            return dict(lote.estado or {}) if lote else {}

    lote = obter_lote(nome_processamento)
    return dict(lote.get("estado") or {})


def salvar_estado_lote(nome_processamento: str, estado: dict) -> None:
    estado_compacto = compactar_estado_lote(estado)
    if storage_usa_banco():
        from app.db_models import LoteProcessamentoModel

        nome = _nome_lote_seguro(nome_processamento)
        with get_db_session() as session:
            lote = session.query(LoteProcessamentoModel).filter_by(
                nome_processamento=nome, schoolname=get_school()
            ).one_or_none()
            if not lote:
                raise FileNotFoundError("Lote nao encontrado.")
            # Estado e leituras ficam separados no MySQL. Atualizar somente
            # esta coluna evita regravar todas as leituras do lote.
            lote.estado = estado_compacto
        _cache_lotes_db.pop(f"{get_database_name()}:{get_school()}:{nome}", None)
        return

    lote = obter_lote(nome_processamento)
    if not lote:
        raise FileNotFoundError("Lote nao encontrado.")
    salvar_lote(
        nome_processamento,
        lote.get("metadados") or {},
        lote.get("leituras") or {},
        estado_compacto,
    )


def remover_lote(nome_processamento: str) -> bool:
    if storage_usa_banco():
        from app.db_models import LoteProcessamentoModel
        with get_db_session() as session:
            lote = session.query(LoteProcessamentoModel).filter_by(
                nome_processamento=_nome_lote_seguro(nome_processamento), schoolname=get_school()
            ).one_or_none()
            if not lote:
                return False
            session.delete(lote)
        _cache_lotes_db.pop(
            f"{get_database_name()}:{get_school()}:{_nome_lote_seguro(nome_processamento)}",
            None,
        )
        return True
    caminho = caminho_lote(nome_processamento)
    if not caminho.exists():
        return False
    caminho.unlink()
    _cache_lotes.pop(caminho, None)
    _invalidar_indice()
    return True


def lote_existe(nome_processamento: str) -> bool:
    if storage_usa_banco():
        return bool(obter_lote(nome_processamento))
    return caminho_lote(nome_processamento).is_file()
