from dataclasses import dataclass

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import func
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from custos_precificacao import cadastro
from custos_precificacao.api.deps import get_session
from custos_precificacao.api.schemas import (
    AreaNo,
    AtividadeNo,
    DirecionadorNo,
    MacroprocessoNo,
    NoIn,
    ProcessoNo,
)
from custos_precificacao.despesas import _produtos_da_atividade
from custos_precificacao.models import (
    Area,
    Atividade,
    Despesa,
    Direcionador,
    Macroprocesso,
    Processo,
)

router = APIRouter(tags=["direcionadores"])

BASES_VALIDAS = ("tempo", "quantidade_produzida", "custo_primario")


@dataclass
class Nivel:
    model: type
    rotulo: str
    pai: str | None  # nível do pai (None = Área)
    fk_pai: str | None  # coluna FK no filho
    campos: tuple[str, ...]  # campos editáveis, além do pai


NIVEIS: dict[str, Nivel] = {
    "area": Nivel(Area, "Área", None, None, ("codigo", "nome")),
    "macroprocesso": Nivel(Macroprocesso, "Macroprocesso", "area", "area_id", ("codigo", "nome")),
    "processo": Nivel(Processo, "Processo", "macroprocesso", "macroprocesso_id", ("codigo", "nome")),
    "atividade": Nivel(Atividade, "Atividade", "processo", "processo_id", ("codigo", "nome")),
    "direcionador": Nivel(Direcionador, "Direcionador", "atividade", "atividade_id", ("codigo", "nome", "base")),
}

# nível → (rótulo do filho, modelo do filho, coluna FK do filho)
FILHOS = {
    "area": ("Macroprocesso", Macroprocesso, Macroprocesso.area_id),
    "macroprocesso": ("Processo", Processo, Processo.macroprocesso_id),
    "processo": ("Atividade", Atividade, Atividade.processo_id),
    "atividade": ("Direcionador", Direcionador, Direcionador.atividade_id),
}


def _nivel(nivel: str) -> Nivel:
    if nivel not in NIVEIS:
        raise HTTPException(status_code=404, detail=f"Nível {nivel!r} inválido. Opções: {', '.join(NIVEIS)}")
    return NIVEIS[nivel]


def _obter(session: Session, nivel: str, codigo: str):
    config = NIVEIS[nivel]
    instancia = session.query(config.model).filter_by(codigo=codigo).one_or_none()
    if instancia is None:
        raise HTTPException(status_code=404, detail=f"{config.rotulo} {codigo!r} não encontrado")
    return instancia


def _entidade(nivel: str) -> cadastro.Entidade:
    return cadastro.resolver_entidade(nivel)


def _erro_base() -> HTTPException:
    return HTTPException(status_code=422, detail=f"base deve ser um de: {', '.join(BASES_VALIDAS)}")


def _limpar(nivel: str, valores: dict[str, str | None], *, criando: bool) -> dict[str, str]:
    config = NIVEIS[nivel]
    permitidos = set(config.campos) | ({"pai_codigo"} if config.pai else set())
    desconhecidos = set(valores) - permitidos
    if desconhecidos:
        raise HTTPException(status_code=422, detail=f"Campos não editáveis: {', '.join(sorted(desconhecidos))}")
    limpos = {campo: (valor or "").strip() for campo, valor in valores.items()}
    if criando:
        faltando = [c for c in ("codigo", "nome") if not limpos.get(c)]
        if faltando:
            raise HTTPException(status_code=422, detail=f"Campos obrigatórios: {', '.join(faltando)}")
    elif "codigo" in limpos:
        raise HTTPException(status_code=422, detail="O código não pode ser alterado")
    if "nome" in limpos and not limpos["nome"]:
        raise HTTPException(status_code=422, detail="nome não pode ficar vazio")
    if nivel == "direcionador" and (criando or "base" in limpos) and limpos.get("base") not in BASES_VALIDAS:
        raise _erro_base()
    return limpos


def _resolver_pai(session: Session, nivel: str, limpos: dict[str, str], *, obrigatorio: bool) -> dict[str, str]:
    """Troca `pai_codigo` pelo id do pai na coluna FK do filho."""
    config = NIVEIS[nivel]
    pai_codigo = limpos.pop("pai_codigo", None)
    if not config.pai:
        return limpos
    if not pai_codigo:
        if obrigatorio:
            raise HTTPException(status_code=422, detail=f"pai_codigo é obrigatório para {config.rotulo}")
        return limpos
    limpos[config.fk_pai] = str(_obter(session, config.pai, pai_codigo).id)
    return limpos


def _bloqueio_exclusao(session: Session, nivel: str, instancia) -> str | None:
    """Mensagem de bloqueio se o registro tem filhos/uso; None se pode excluir."""
    rotulo = NIVEIS[nivel].rotulo
    if nivel in FILHOS:
        nome_filho, modelo, coluna = FILHOS[nivel]
        qtd = session.query(func.count(modelo.id)).filter(coluna == instancia.id).scalar()
        if qtd:
            return f"{rotulo} {instancia.codigo!r} tem {qtd} {nome_filho}(s) — exclua-os antes"
    if nivel == "atividade":
        qtd = len(_produtos_da_atividade(session, instancia.id))
        if qtd:
            return f"Atividade {instancia.codigo!r} é usada por {qtd} Produto(s) no roteiro — remova das linhas de roteiro antes"
    if nivel == "direcionador":
        qtd = session.query(func.count(Despesa.id)).filter(Despesa.direcionador_id == instancia.id).scalar()
        if qtd:
            return f"Direcionador {instancia.codigo!r} ratea {qtd} Despesa(s) — reatribua-as antes"
    return None


def _no_direcionador(session: Session, d: Direcionador) -> DirecionadorNo:
    qtd, total = (
        session.query(func.count(Despesa.id), func.coalesce(func.sum(Despesa.valor), 0))
        .filter(Despesa.direcionador_id == d.id)
        .one()
    )
    return DirecionadorNo(codigo=d.codigo, nome=d.nome, base=d.base, uso_despesas=qtd, uso_valor=total)


@router.get("/direcionadores/arvore", response_model=list[AreaNo])
def obter_arvore(session: Session = Depends(get_session)) -> list[AreaNo]:
    def por_pai(modelo, coluna: str) -> dict[int, list]:
        agrupado: dict[int, list] = {}
        for item in session.query(modelo).order_by(modelo.codigo).all():
            agrupado.setdefault(getattr(item, coluna), []).append(item)
        return agrupado

    macros = por_pai(Macroprocesso, "area_id")
    processos = por_pai(Processo, "macroprocesso_id")
    atividades = por_pai(Atividade, "processo_id")
    direcionadores = por_pai(Direcionador, "atividade_id")

    def atividade_no(a: Atividade) -> AtividadeNo:
        return AtividadeNo(
            codigo=a.codigo,
            nome=a.nome,
            uso_produtos=len(_produtos_da_atividade(session, a.id)),
            direcionadores=[_no_direcionador(session, d) for d in direcionadores.get(a.id, [])],
        )

    return [
        AreaNo(
            codigo=area.codigo,
            nome=area.nome,
            macroprocessos=[
                MacroprocessoNo(
                    codigo=m.codigo,
                    nome=m.nome,
                    processos=[
                        ProcessoNo(
                            codigo=p.codigo,
                            nome=p.nome,
                            atividades=[atividade_no(a) for a in atividades.get(p.id, [])],
                        )
                        for p in processos.get(m.id, [])
                    ],
                )
                for m in macros.get(area.id, [])
            ],
        )
        for area in session.query(Area).order_by(Area.codigo).all()
    ]


@router.post("/direcionadores/{nivel}", status_code=201)
def criar_no(nivel: str, corpo: NoIn, session: Session = Depends(get_session)) -> dict[str, str]:
    config = _nivel(nivel)
    limpos = _limpar(nivel, corpo.valores, criando=True)
    limpos = _resolver_pai(session, nivel, limpos, obrigatorio=True)
    try:
        cadastro.criar(session, _entidade(nivel), limpos)
    except IntegrityError as exc:
        session.rollback()
        raise HTTPException(
            status_code=409, detail=f"Já existe um(a) {config.rotulo} com código {limpos['codigo']!r}"
        ) from exc
    return {"codigo": limpos["codigo"]}


@router.patch("/direcionadores/{nivel}/{codigo}")
def atualizar_no(nivel: str, codigo: str, corpo: NoIn, session: Session = Depends(get_session)) -> dict[str, str]:
    _nivel(nivel)
    _obter(session, nivel, codigo)
    limpos = _limpar(nivel, corpo.valores, criando=False)
    limpos = _resolver_pai(session, nivel, limpos, obrigatorio=False)
    cadastro.atualizar(session, _entidade(nivel), codigo, limpos)
    return {"codigo": codigo}


@router.delete("/direcionadores/{nivel}/{codigo}", status_code=204)
def excluir_no(nivel: str, codigo: str, session: Session = Depends(get_session)) -> None:
    config = _nivel(nivel)
    instancia = _obter(session, nivel, codigo)
    bloqueio = _bloqueio_exclusao(session, nivel, instancia)
    if bloqueio:
        raise HTTPException(status_code=409, detail=bloqueio)
    try:
        cadastro.deletar(session, _entidade(nivel), codigo)
    except IntegrityError as exc:
        session.rollback()
        raise HTTPException(
            status_code=409, detail=f"{config.rotulo} {codigo!r} está em uso e não pode ser excluído"
        ) from exc
