"""Plano de Contas e Centros de Custo em árvore (padrão APP32). Chave por id: o código muda quando o nó é movido."""

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.orm import Session

from custos_precificacao import arvore
from custos_precificacao.api.deps import get_session
from custos_precificacao.models import CentroDeCusto, Funcionario, PlanoDeContas

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

TIPOS_CONTA = ("receita", "custo", "despesa", "imposto")


class NoOut(BaseModel):
    id: int
    codigo: str
    sufixo: str | None
    nome: str
    pai_id: int | None
    nivel: int  # 1 = raiz
    sintetico: bool  # True = só agrupa; False = analítico (recebe lançamentos)
    ativo: bool
    usos: int  # lançamentos/recursos que usam o nó diretamente
    tem_filhos: bool
    chave: str | None  # código antigo/interno estável


class PlanoOut(NoOut):
    tipo: str
    codigo_reduzido: str | None
    codigo_externo: str | None
    observacoes: str | None


class CentroOut(NoOut):
    descricao: str | None
    gestor_id: int | None
    gestor_nome: str | None
    produtivo: bool  # marcador do próprio centro
    produtivo_efetivo: bool  # produtivo e sem ancestral não produtivo: o que vale para a ociosidade


class NoIn(BaseModel):
    """Só os campos enviados são aplicados (PATCH). `pai_id` nulo = raiz; `sufixo` omitido = próximo livre."""

    nome: str | None = None
    pai_id: int | None = None
    sufixo: str | None = None
    sintetico: bool | None = None
    ativo: bool | None = None


class PlanoIn(NoIn):
    tipo: str | None = None  # só na raiz; os filhos herdam
    codigo_reduzido: str | None = None
    codigo_externo: str | None = None
    observacoes: str | None = None


class CentroIn(NoIn):
    descricao: str | None = None
    gestor_id: int | None = None
    produtivo: bool | None = None


def _vazio_para_none(valor):
    return (valor.strip() or None) if isinstance(valor, str) else valor


def _montar(session: Session, modelo: type, extras) -> list[dict]:
    """Todos os nós em ordem de árvore, com nível, filhos e usos (consultas em massa)."""
    nos = session.query(modelo).all()
    por_id = {n.id: n for n in nos}
    com_filhos = {n.parent_id for n in nos if n.parent_id is not None}
    usos = arvore.usos_em_massa(session, modelo)

    def nivel(n) -> int:
        k, atual, vistos = 1, n, set()
        while atual.parent_id in por_id and atual.id not in vistos:
            vistos.add(atual.id)
            k, atual = k + 1, por_id[atual.parent_id]
        return k

    saida = []
    for n in sorted(nos, key=lambda x: arvore.chave_de_ordem(x.codigo)):
        saida.append(dict(
            id=n.id, codigo=n.codigo, sufixo=n.sufixo, nome=n.nome, pai_id=n.parent_id, nivel=nivel(n),
            sintetico=not n.aceita_lancamento, ativo=n.ativo, usos=usos.get(n.id, 0), tem_filhos=n.id in com_filhos,
            chave=n.chave, **extras(n),
        ))
    return saida


def _erro(exc: arvore.ErroDeArvore) -> HTTPException:
    return HTTPException(status_code=409 if isinstance(exc, arvore.ErroEmUso) else 422, detail=str(exc))


def _obter(session: Session, modelo: type, no_id: int):
    no = session.get(modelo, no_id)
    if no is None:
        raise HTTPException(status_code=404, detail=f"{arvore.entidade_de(modelo).nome} {no_id} não encontrado(a)")
    return no


def _aplicar_in(corpo: NoIn, extras: tuple[str, ...]) -> dict:
    enviados = corpo.model_dump(exclude_unset=True)
    mudancas = {k: _vazio_para_none(v) for k, v in enviados.items() if k in ("nome", "sufixo", "ativo") or k in extras}
    if "pai_id" in enviados:
        mudancas["parent_id"] = enviados["pai_id"]
    if "sintetico" in enviados and enviados["sintetico"] is not None:
        mudancas["aceita_lancamento"] = not enviados["sintetico"]
    return mudancas


# ---- Plano de Contas

def _extras_plano(n) -> dict:
    return dict(tipo=n.tipo, codigo_reduzido=n.codigo_reduzido, codigo_externo=n.codigo_externo, observacoes=n.observacoes)


def _plano_out(session: Session, no) -> PlanoOut:
    return next(PlanoOut(**d) for d in _montar(session, PlanoDeContas, _extras_plano) if d["id"] == no.id)


@router.get("/plano-de-contas", response_model=list[PlanoOut])
def listar_plano(session: Session = Depends(get_session)) -> list[PlanoOut]:
    return [PlanoOut(**d) for d in _montar(session, PlanoDeContas, _extras_plano)]


_EXTRAS_PLANO = ("tipo", "codigo_reduzido", "codigo_externo", "observacoes")


@router.post("/plano-de-contas", response_model=PlanoOut, status_code=201)
def criar_conta(corpo: PlanoIn, session: Session = Depends(get_session)) -> PlanoOut:
    mudancas = _aplicar_in(corpo, _EXTRAS_PLANO)
    if mudancas.get("tipo") is not None and mudancas["tipo"] not in TIPOS_CONTA:
        raise HTTPException(status_code=422, detail=f"Tipo inválido. Opções: {', '.join(TIPOS_CONTA)}")
    try:
        no = arvore.criar_no(
            session, PlanoDeContas, nome=mudancas.pop("nome", None) or "", pai_id=mudancas.pop("parent_id", None),
            sufixo=mudancas.pop("sufixo", None), aceita_lancamento=mudancas.pop("aceita_lancamento", True),
            ativo=mudancas.pop("ativo", True), **mudancas,
        )
        session.commit()
    except arvore.ErroDeArvore as exc:
        session.rollback()
        raise _erro(exc) from exc
    return _plano_out(session, no)


@router.patch("/plano-de-contas/{no_id}", response_model=PlanoOut)
def atualizar_conta(no_id: int, corpo: PlanoIn, session: Session = Depends(get_session)) -> PlanoOut:
    no = _obter(session, PlanoDeContas, no_id)
    mudancas = _aplicar_in(corpo, _EXTRAS_PLANO)
    if mudancas.get("tipo") is not None and mudancas["tipo"] not in TIPOS_CONTA:
        raise HTTPException(status_code=422, detail=f"Tipo inválido. Opções: {', '.join(TIPOS_CONTA)}")
    try:
        arvore.atualizar_no(session, no, mudancas)
        session.commit()
    except arvore.ErroDeArvore as exc:
        session.rollback()
        raise _erro(exc) from exc
    return _plano_out(session, no)


@router.delete("/plano-de-contas/{no_id}", status_code=204)
def excluir_conta(no_id: int, session: Session = Depends(get_session)) -> None:
    no = _obter(session, PlanoDeContas, no_id)
    try:
        arvore.excluir_no(session, no)
        session.commit()
    except arvore.ErroDeArvore as exc:
        session.rollback()
        raise _erro(exc) from exc


# ---- Centros de Custo

def _extras_centro(session: Session):
    nomes = {f.id: f.nome for f in session.query(Funcionario)}
    efetivos = arvore.centros_produtivos(session)

    def extras(n) -> dict:
        return dict(descricao=n.descricao, gestor_id=n.gestor_id, gestor_nome=nomes.get(n.gestor_id), produtivo=n.produtivo, produtivo_efetivo=n.id in efetivos)

    return extras


def _centro_out(session: Session, no) -> CentroOut:
    return next(CentroOut(**d) for d in _montar(session, CentroDeCusto, _extras_centro(session)) if d["id"] == no.id)


def _validar_gestor(session: Session, mudancas: dict) -> None:
    if mudancas.get("gestor_id") is not None and session.get(Funcionario, mudancas["gestor_id"]) is None:
        raise HTTPException(status_code=422, detail="Gestor não encontrado entre os funcionários")


@router.get("/centros-de-custo", response_model=list[CentroOut])
def listar_centros(session: Session = Depends(get_session)) -> list[CentroOut]:
    return [CentroOut(**d) for d in _montar(session, CentroDeCusto, _extras_centro(session))]


@router.post("/centros-de-custo", response_model=CentroOut, status_code=201)
def criar_centro(corpo: CentroIn, session: Session = Depends(get_session)) -> CentroOut:
    mudancas = _aplicar_in(corpo, ("descricao", "gestor_id", "produtivo"))
    mudancas = {k: v for k, v in mudancas.items() if not (k == "produtivo" and v is None)}
    _validar_gestor(session, mudancas)
    try:
        no = arvore.criar_no(
            session, CentroDeCusto, nome=mudancas.pop("nome", None) or "", pai_id=mudancas.pop("parent_id", None),
            sufixo=mudancas.pop("sufixo", None), aceita_lancamento=mudancas.pop("aceita_lancamento", True),
            ativo=mudancas.pop("ativo", True), **mudancas,
        )
        session.commit()
    except arvore.ErroDeArvore as exc:
        session.rollback()
        raise _erro(exc) from exc
    return _centro_out(session, no)


@router.patch("/centros-de-custo/{no_id}", response_model=CentroOut)
def atualizar_centro(no_id: int, corpo: CentroIn, session: Session = Depends(get_session)) -> CentroOut:
    no = _obter(session, CentroDeCusto, no_id)
    mudancas = _aplicar_in(corpo, ("descricao", "gestor_id", "produtivo"))
    mudancas = {k: v for k, v in mudancas.items() if not (k == "produtivo" and v is None)}
    _validar_gestor(session, mudancas)
    try:
        arvore.atualizar_no(session, no, mudancas)
        session.commit()
    except arvore.ErroDeArvore as exc:
        session.rollback()
        raise _erro(exc) from exc
    return _centro_out(session, no)


@router.delete("/centros-de-custo/{no_id}", status_code=204)
def excluir_centro(no_id: int, session: Session = Depends(get_session)) -> None:
    no = _obter(session, CentroDeCusto, no_id)
    try:
        arvore.excluir_no(session, no)
        session.commit()
    except arvore.ErroDeArvore as exc:
        session.rollback()
        raise _erro(exc) from exc
