"""Formulações: unidades e conversões, e a estrutura explodida de um produto."""

from decimal import Decimal, InvalidOperation

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

from custos_precificacao import formulacao, historico, unidades
from custos_precificacao.api.deps import get_session
from custos_precificacao.models import ConversaoUnidade, Insumo, Produto

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


class ConversaoIn(BaseModel):
    item_tipo: str  # insumo | produto
    item_codigo: str
    unidade: str
    fator: str  # 1 `unidade` = `fator` unidades-base do item


class ConversaoOut(BaseModel):
    id: int
    item_tipo: str
    item_codigo: str
    item_nome: str
    unidade_base: str
    unidade: str
    fator: Decimal


def _saida(c: ConversaoUnidade, item) -> ConversaoOut:
    return ConversaoOut(
        id=c.id, item_tipo="insumo" if c.insumo_id is not None else "produto", item_codigo=item.codigo, item_nome=item.nome,
        unidade_base=unidades.normalizar(item.unidade), unidade=c.unidade, fator=c.fator,
    )


@router.get("/unidades/conversoes", response_model=list[ConversaoOut])
def listar_conversoes(session: Session = Depends(get_session)) -> list[ConversaoOut]:
    insumos = {i.id: i for i in session.query(Insumo)}
    produtos = {p.id: p for p in session.query(Produto)}
    saida = [
        _saida(c, insumos[c.insumo_id] if c.insumo_id is not None else produtos[c.produto_id])
        for c in session.query(ConversaoUnidade).order_by(ConversaoUnidade.id)
    ]
    return sorted(saida, key=lambda c: (c.item_codigo, c.unidade))


@router.post("/unidades/conversoes", response_model=ConversaoOut, status_code=201)
def criar_conversao(corpo: ConversaoIn, session: Session = Depends(get_session)) -> ConversaoOut:
    if corpo.item_tipo not in ("insumo", "produto"):
        raise HTTPException(status_code=422, detail="item_tipo deve ser 'insumo' ou 'produto'")
    modelo = Insumo if corpo.item_tipo == "insumo" else Produto
    item = session.query(modelo).filter_by(codigo=(corpo.item_codigo or "").strip()).one_or_none()
    if item is None:
        raise HTTPException(status_code=404, detail=f"{corpo.item_tipo.capitalize()} {corpo.item_codigo!r} não encontrado")
    unidade = unidades.normalizar(corpo.unidade, "")
    if not unidade or len(unidade) > 10:
        raise HTTPException(status_code=422, detail="Informe a unidade (até 10 caracteres)")
    if unidade == unidades.normalizar(item.unidade):
        raise HTTPException(status_code=422, detail="Essa já é a unidade base do item")
    try:
        fator = Decimal((corpo.fator or "").strip().replace(",", "."))
    except InvalidOperation as exc:
        raise HTTPException(status_code=422, detail="O fator precisa ser um número") from exc
    if not fator.is_finite() or fator <= 0:
        raise HTTPException(status_code=422, detail="O fator deve ser maior que zero")
    coluna = "insumo_id" if modelo is Insumo else "produto_id"
    # o único do banco não vale com a outra coluna nula (NULL é sempre distinto): confere aqui
    if session.query(ConversaoUnidade).filter_by(unidade=unidade, **{coluna: item.id}).first() is not None:
        raise HTTPException(status_code=409, detail=f"Já existe a conversão de {unidade!r} para {item.codigo}")
    conversao = ConversaoUnidade(unidade=unidade, fator=fator, **{coluna: item.id})
    session.add(conversao)
    try:
        session.commit()
    except IntegrityError as exc:
        session.rollback()
        raise HTTPException(status_code=409, detail=f"Já existe a conversão de {unidade!r} para {item.codigo}") from exc
    return _saida(conversao, item)


@router.delete("/unidades/conversoes/{conversao_id}", status_code=204)
def excluir_conversao(conversao_id: int, session: Session = Depends(get_session)) -> None:
    conversao = session.get(ConversaoUnidade, conversao_id)
    if conversao is None:
        raise HTTPException(status_code=404, detail=f"Conversão {conversao_id} não encontrada")
    session.delete(conversao)
    session.commit()


# --- estrutura explodida ---------------------------------------------------------------------

class EtapaLinhaOut(BaseModel):
    tipo: str
    descricao: str
    quantidade: Decimal
    unidade: str
    quantidade_informada: Decimal | None
    unidade_informada: str | None
    perda_pct: Decimal
    valor_producao: Decimal
    origem: bool  # insumo que é outro produto (aparece também como filho)


class EtapaOut(BaseModel):
    atividade_codigo: str
    linhas: list[EtapaLinhaOut]


class EstruturaNoOut(BaseModel):
    codigo: str
    nome: str
    tipo: str
    unidade: str
    nivel: int
    quantidade: Decimal
    perda_pct: Decimal
    custo_total: Decimal
    custo_unitario: Decimal | None
    custo_lote: Decimal | None
    base_quantidade: Decimal | None
    perda_produto_pct: Decimal
    rendimento: Decimal | None
    horas: dict[str, Decimal]
    etapas: list[EtapaOut]
    filhos: list["EstruturaNoOut"]
    aviso: str | None


EstruturaNoOut.model_rebuild()


def _no_out(no: formulacao.NoEstrutura) -> EstruturaNoOut:
    dados = {k: getattr(no, k) for k in EstruturaNoOut.model_fields if k not in ("filhos", "etapas")}
    return EstruturaNoOut(**dados, etapas=[EtapaOut(**e) for e in no.etapas], filhos=[_no_out(f) for f in no.filhos])


@router.get("/produtos/{codigo}/estrutura", response_model=EstruturaNoOut)
def obter_estrutura(codigo: str, competencia: str | None = None, session: Session = Depends(get_session)) -> EstruturaNoOut:
    """Árvore explodida da formulação (todos os níveis) com quantidade, perda e custo de cada nó, no mês `competencia`."""
    try:
        return _no_out(formulacao.montar_estrutura(session, codigo, competencia))
    except historico.ErroDeHistorico as exc:
        raise HTTPException(status_code=422, detail=str(exc)) from exc
    except ValueError as exc:
        raise HTTPException(status_code=404, detail=str(exc)) from exc
