"""Custo dos Produtos do Planejamento: os custos **unitários** das cabeças da Tabela de Preços, por grupo, e a estrutura que os compõe.

Tudo é unitário (por unidade de rendimento do produto). O **total** é o unitário mais a ociosidade e é o `custo_producao_unitario` do
custeio (o que o preço, a DRE e o Planejamento usam). Os insumos só têm o unitário (matéria-prima, embalagem e intermediários); os
elementos de custo (mão de obra, instalações, máquinas, outros) têm unitário e ociosidade.
"""

from dataclasses import dataclass, field
from decimal import Decimal

from sqlalchemy.orm import Session

from custos_precificacao import unidades
from custos_precificacao.entradas import Entradas
from custos_precificacao.models import Atividade, CusteioExecucao, CusteioProduto, Insumo, Produto

# (chave, título, campo do custo absorvido do lote, campo da ociosidade do lote) na ordem de exibição
ELEMENTOS = (
    ("mao_de_obra", "Mão de obra", "custo_mao_de_obra", "ociosidade_mao_de_obra"),
    ("ambiente", "Instalações", "custo_ambiente", "ociosidade_ambiente"),
    ("maquina", "Máquinas", "custo_maquina", "ociosidade_maquina"),
    ("outros", "Outros", "custo_outros", "ociosidade_outros"),
)
PROFUNDIDADE_MAXIMA = 12


@dataclass
class ElementoUnitario:
    chave: str
    titulo: str
    unitario: Decimal
    ociosidade: Decimal


@dataclass
class CustoUnitario:
    insumos: Decimal  # inclui os intermediários a custo cheio (com a ociosidade deles)
    materia_prima: Decimal | None  # None = execução anterior à divisão do insumo
    embalagem: Decimal | None
    intermediarios: Decimal | None
    elementos: list[ElementoUnitario]

    @property
    def elementos_total(self) -> Decimal:
        return sum((e.unitario + e.ociosidade for e in self.elementos), Decimal(0))

    @property
    def total(self) -> Decimal:
        """Unitário + ociosidade: o `custo_producao_unitario` do custeio."""
        return self.insumos + self.elementos_total


def unitarios(r: CusteioProduto) -> CustoUnitario:
    """Os custos unitários de um resultado de custeio (os totais do lote ÷ o rendimento; sem rendimento, o unitário é o do lote)."""
    rendimento = r.rendimento or None

    def por_un(valor: Decimal) -> Decimal:
        valor = Decimal(valor or 0)
        return valor / rendimento if rendimento else valor

    insumos = por_un(r.custo_insumos)
    dividido = bool(r.custo_materia_prima or r.custo_embalagem or r.custo_intermediarios) or not insumos
    materia_prima = por_un(r.custo_materia_prima) if dividido else None
    embalagem = por_un(r.custo_embalagem) if dividido else None
    # intermediários = o que sobra do insumo (inclui a ociosidade que eles repassam)
    intermediarios = (insumos - materia_prima - embalagem) if dividido else None
    return CustoUnitario(
        insumos=insumos, materia_prima=materia_prima, embalagem=embalagem, intermediarios=intermediarios,
        elementos=[ElementoUnitario(chave, titulo, por_un(getattr(r, absorvido)), por_un(getattr(r, ocio))) for chave, titulo, absorvido, ocio in ELEMENTOS],
    )


@dataclass
class NoEstrutura:
    codigo: str
    nome: str
    tipo: str  # materia_prima | embalagem | intermediario | acabado | revenda
    unidade: str
    quantidade: Decimal  # consumida por unidade do pai (já com a perda da linha), na unidade base do item
    custo: CustoUnitario | None  # unitário do próprio item (None se não foi custeado nesta execução)
    participacao: Decimal | None  # quantidade × total unitário do item: o quanto ele pesa por unidade do pai
    filhos: list["NoEstrutura"] = field(default_factory=list)
    ciclo: bool = False
    etapa_codigo: str | None = None  # etapa (atividade) do pai em que o item é consumido
    etapa_nome: str | None = None
    produto_id: int | None = None  # intermediário/acabado/revenda
    insumo_id: int | None = None  # matéria-prima/embalagem


def _tipo_do_produto(produto: Produto) -> str:
    return {"semi_acabado": "intermediario", "revenda": "revenda"}.get(produto.tipo, "acabado")


def montar_estrutura(session: Session, produto: Produto, execucao: CusteioExecucao) -> NoEstrutura:
    """A composição de um produto na execução de custeio: matérias-primas e embalagens (custo de compra da competência) e, recursivamente,
    os intermediários/produtos que ele consome, cada um com os custos unitários da própria execução."""
    entradas = Entradas(session, execucao.competencia)
    resultados = {r.produto_id: r for r in session.query(CusteioProduto).filter_by(execucao_id=execucao.id)}
    etapas = {a.id: (a.codigo, a.nome) for a in session.query(Atividade)}

    def no(item: Produto, quantidade: Decimal, caminho: tuple[int, ...], nivel: int) -> NoEstrutura:
        r = resultados.get(item.id)
        custo = unitarios(r) if r is not None else None
        no_item = NoEstrutura(
            codigo=item.codigo, nome=item.nome, tipo=_tipo_do_produto(item), unidade=unidades.normalizar(item.unidade),
            quantidade=quantidade, custo=custo, participacao=(quantidade * custo.total) if custo is not None else None, produto_id=item.id,
        )
        if item.id in caminho:
            no_item.ciclo = True
            return no_item
        if nivel >= PROFUNDIDADE_MAXIMA or r is None:
            return no_item
        rendimento = r.rendimento or None
        for linha in sorted(entradas.roteiro(item, "insumo"), key=lambda l: etapas.get(l.atividade_id, ("", ""))[0]):
            etapa_codigo, etapa_nome = etapas.get(linha.atividade_id, (None, None))
            por_un = lambda q: q / rendimento if rendimento else q  # noqa: E731  (quantidade por lote → por unidade do item)
            fator = unidades.fator_de_perda(getattr(linha, "perda_pct", None))
            unidade_linha = unidades.normalizar(getattr(linha, "unidade", None))
            if linha.insumo_id is not None:
                insumo = session.get(Insumo, linha.insumo_id)
                base = unidades.normalizar(insumo.unidade)
                qtd = por_un(unidades.converter(session, insumo, linha.quantidade_kg, unidade_linha, base) * fator)
                preco = entradas.custo_final(insumo)
                embalagem = insumo.tipo == "embalagem"
                custo_insumo = CustoUnitario(
                    insumos=preco, materia_prima=None if embalagem else preco, embalagem=preco if embalagem else Decimal(0),
                    intermediarios=Decimal(0), elementos=[ElementoUnitario(c, t, Decimal(0), Decimal(0)) for c, t, _, _ in ELEMENTOS],
                )
                no_item.filhos.append(NoEstrutura(
                    codigo=insumo.codigo, nome=insumo.nome, tipo="embalagem" if embalagem else "materia_prima", unidade=base,
                    quantidade=qtd, custo=custo_insumo, participacao=qtd * preco, etapa_codigo=etapa_codigo, etapa_nome=etapa_nome, insumo_id=insumo.id,
                ))
            else:
                filho = session.get(Produto, linha.insumo_produto_id)
                base = unidades.normalizar(filho.unidade)
                qtd = por_un(unidades.converter(session, filho, linha.quantidade_kg, unidade_linha, base) * fator)
                no_filho = no(filho, qtd, caminho + (item.id,), nivel + 1)
                no_filho.etapa_codigo, no_filho.etapa_nome = etapa_codigo, etapa_nome
                no_item.filhos.append(no_filho)
        return no_item

    return no(produto, Decimal(1), (), 0)
