"""Estrutura explodida de uma Formulação: a árvore de etapas (produto › intermediários › insumos), com quantidade, perda e custo
de cada nó, num mês escolhido. Independe de Cenário (considera todos os recursos ligados), como a ficha."""

from dataclasses import dataclass, field
from datetime import date
from decimal import Decimal

from sqlalchemy.orm import Session

from custos_precificacao import historico, unidades
from custos_precificacao.custeio import LinhaCusto, RateioCenario, calcular_custo_produto
from custos_precificacao.entradas import Entradas
from custos_precificacao.models import Atividade, Produto

MAX_NIVEIS = 20  # trava de segurança (o custeio já detecta ciclo)


@dataclass
class NoEstrutura:
    codigo: str
    nome: str
    tipo: str  # acabado | semi_acabado
    unidade: str  # unidade base do produto
    nivel: int
    quantidade: Decimal  # consumida pelo pai (unidade base, já com a perda da linha); 1 na raiz
    perda_pct: Decimal  # perda da linha no pai
    custo_total: Decimal  # o que esse consumo custa no pai (quantidade × custo unitário); na raiz, o custo do lote
    custo_unitario: Decimal | None  # custo de produção por unidade base
    custo_lote: Decimal | None  # custo de uma bacia
    base_quantidade: Decimal | None
    perda_produto_pct: Decimal  # perda do processo (bacia → rendimento real)
    rendimento: Decimal | None  # rendimento real da bacia
    horas: dict[str, Decimal] = field(default_factory=dict)  # horas por tipo de recurso nesta etapa (por bacia)
    etapas: list[dict] = field(default_factory=list)  # linhas da etapa agrupadas por atividade
    filhos: list["NoEstrutura"] = field(default_factory=list)
    aviso: str | None = None


def _no(session: Session, produto: Produto, entradas: Entradas, nivel: int, quantidade: Decimal, perda: Decimal,
        custo_total: Decimal, atividades: dict[int, str], visitados: frozenset[int]) -> NoEstrutura:
    coletadas: list[LinhaCusto] = []
    no = NoEstrutura(
        codigo=produto.codigo, nome=produto.nome, tipo=produto.tipo, unidade=unidades.normalizar(produto.unidade), nivel=nivel,
        quantidade=quantidade, perda_pct=perda, custo_total=custo_total, custo_unitario=None, custo_lote=None,
        base_quantidade=produto.base_quantidade, perda_produto_pct=Decimal(produto.perda_pct or 0), rendimento=None,
    )
    if produto.id in visitados or nivel > MAX_NIVEIS:
        no.aviso = "Ciclo na formulação: este produto já aparece acima."
        return no
    try:
        custo = calcular_custo_produto(session, produto, RateioCenario(set(), set(), set()), entradas=entradas, _linhas=coletadas)
    except (ValueError, RuntimeError) as exc:
        no.aviso = str(exc)
        return no

    no.custo_lote = custo.custo_producao_lote
    no.rendimento = custo.rendimento
    un, kg = entradas.rendimentos(produto)
    base = unidades.normalizar(produto.unidade)
    rendimento_base = un if base == "un" else kg
    if rendimento_base:
        no.custo_unitario = custo.custo_producao_lote / rendimento_base

    por_etapa: dict[str, list[LinhaCusto]] = {}
    for l in coletadas:
        por_etapa.setdefault(atividades.get(l.atividade_id, ""), []).append(l)
        if l.tipo != "insumo":
            no.horas[l.tipo] = no.horas.get(l.tipo, Decimal(0)) + l.quantidade
    no.etapas = [
        {
            "atividade_codigo": codigo,
            "linhas": [
                {
                    "tipo": l.tipo, "descricao": l.descricao, "quantidade": l.quantidade, "unidade": l.unidade,
                    "quantidade_informada": l.quantidade_informada, "unidade_informada": l.unidade_informada,
                    "perda_pct": l.perda_pct, "valor_producao": l.valor_producao, "origem": l.produto_origem_id is not None,
                }
                for l in linhas
            ],
        }
        for codigo, linhas in sorted(por_etapa.items())
    ]
    for l in coletadas:
        if l.produto_origem_id is not None:
            filho = session.get(Produto, l.produto_origem_id)
            no.filhos.append(_no(session, filho, entradas, nivel + 1, l.quantidade, l.perda_pct, l.valor_producao, atividades, visitados | {produto.id}))
    return no


def montar_estrutura(session: Session, produto_codigo: str, competencia: date | str | None = None) -> NoEstrutura:
    produto = session.query(Produto).filter_by(codigo=produto_codigo).one_or_none()
    if produto is None:
        raise ValueError(f"Produto {produto_codigo!r} não encontrado")
    mes = historico.normalizar_mes(competencia)
    entradas = Entradas(session, mes)
    atividades = {a.id: a.codigo for a in session.query(Atividade)}
    raiz = _no(session, produto, entradas, 0, Decimal(1), Decimal(0), Decimal(0), atividades, frozenset())
    raiz.custo_total = raiz.custo_lote or Decimal(0)
    return raiz
