"""Volume de produção de cada produto, incluindo o que os intermediários (PI) precisam produzir para abastecer os acabados.

O usuário informa a produção só do que está na tabela de preços (`Produto.qtd_estimada_producao_mes`, por Planejamento). O volume
dos intermediários é **calculado pela estrutura**: cada linha de formulação que consome um produto (`RoteiroInsumo.insumo_produto_id`)
pede, por lote do pai, a quantidade da linha (convertida para a unidade do filho, com a perda da linha). Então

    total(p) = informado(p) + Σ_pai  total(pai) ÷ rendimento(pai) × quantidade da linha por lote do pai

— o que o PI também vende direto (informado) soma com o que os acabados consomem. É a quantidade em que se baseiam a capacidade
dos elementos, a ineficiência/ociosidade, o rateio de despesas e os créditos de imposto.
"""

from dataclasses import dataclass, field
from decimal import Decimal

from sqlalchemy import event
from sqlalchemy.orm import Session, object_session

from custos_precificacao import unidades
from custos_precificacao.models import Produto, RoteiroInsumo

CHAVE_CACHE = "volumes_de_producao"


def rendimento_atual(produto: Produto) -> Decimal | None:
    """Rendimento por lote (un ou kg) com os valores atuais — o mesmo de `Entradas.rendimentos`: com bacia, o rendimento real
    (base × (1 − perda)) na unidade do produto; sem ela, o cadastrado."""
    un, kg = produto.rendimento_un, produto.rendimento_kg
    if produto.base_quantidade:
        real = produto.base_quantidade * (1 - (produto.perda_pct or 0))
        if unidades.normalizar(produto.unidade) == "un":
            un = real
        else:
            kg = real
    return un or kg


@dataclass
class VolumeProduto:
    informado: Decimal
    calculado: Decimal  # o que os produtos que o consomem pedem (0 nos acabados)

    @property
    def total(self) -> Decimal:
        return self.informado + self.calculado


@dataclass
class Volumes:
    por_produto: dict[int, VolumeProduto] = field(default_factory=dict)
    avisos: list[str] = field(default_factory=list)

    def total(self, produto_id: int) -> Decimal:
        volume = self.por_produto.get(produto_id)
        return volume.total if volume else Decimal(0)


def calcular_volumes(session: Session) -> Volumes:
    """Explode a produção informada pela estrutura (sem cache). Ciclos na estrutura e pais sem rendimento viram avisos."""
    produtos = {p.id: p for p in session.query(Produto)}
    avisos: list[str] = []

    # filho_id -> [(pai_id, quantidade do filho por lote do pai, na unidade base do filho, com a perda da linha)]
    consumo: dict[int, list[tuple[int, Decimal]]] = {}
    for linha in session.query(RoteiroInsumo).filter(RoteiroInsumo.insumo_produto_id.isnot(None)).order_by(RoteiroInsumo.id):
        filho = produtos.get(linha.insumo_produto_id)
        if filho is None or linha.produto_id == filho.id:
            continue
        quantidade = unidades.converter(
            session, filho, linha.quantidade_kg, unidades.normalizar(getattr(linha, "unidade", None)), unidades.normalizar(filho.unidade)
        ) * unidades.fator_de_perda(getattr(linha, "perda_pct", None))
        consumo.setdefault(filho.id, []).append((linha.produto_id, quantidade))

    memo: dict[int, VolumeProduto] = {}
    sem_rendimento: set[int] = set()

    def volume(produto_id: int, pilha: frozenset[int]) -> VolumeProduto:
        if produto_id in memo:
            return memo[produto_id]
        produto = produtos[produto_id]
        calculado = Decimal(0)
        for pai_id, quantidade in consumo.get(produto_id, []):
            if pai_id in pilha or pai_id == produto_id:
                avisos.append(f"Estrutura circular entre {produto.codigo} e {produtos[pai_id].codigo}: o consumo de {produtos[pai_id].codigo} foi ignorado")
                continue
            pai = produtos.get(pai_id)
            if pai is None:
                continue
            total_pai = volume(pai_id, pilha | {produto_id}).total
            if not total_pai:
                continue
            rend = rendimento_atual(pai)
            if not rend:
                if pai_id not in sem_rendimento:
                    sem_rendimento.add(pai_id)
                    avisos.append(f"{pai.codigo} não tem rendimento: o volume que ele pede de {produto.codigo} não foi calculado")
                continue
            calculado += total_pai / rend * quantidade
        informado = (produto.qtd_estimada_producao_mes or Decimal(0)) if produto.tabela_de_precos else Decimal(0)  # só a cabeça tem produção informada
        resultado = VolumeProduto(informado=informado, calculado=calculado)
        memo[produto_id] = resultado
        return resultado

    for produto_id in produtos:
        volume(produto_id, frozenset())
    return Volumes(memo, list(dict.fromkeys(avisos)))


def volumes_de_producao(session: Session) -> Volumes:
    """Como `calcular_volumes`, guardado na sessão até algo mudar (flush, rollback ou troca da produção informada)."""
    volumes = session.info.get(CHAVE_CACHE)
    if volumes is None:
        volumes = session.info[CHAVE_CACHE] = calcular_volumes(session)
    return volumes


def volume_de_producao(session: Session, produto: Produto) -> Decimal | None:
    """Volume de produção do mês (informado + calculado pela estrutura); None se não há produção."""
    total = volumes_de_producao(session).total(produto.id)
    return total or None


def _descartar_cache(session: Session, *_args) -> None:
    session.info.pop(CHAVE_CACHE, None)


event.listen(Session, "after_flush", lambda session, _contexto: _descartar_cache(session))
event.listen(Session, "after_rollback", _descartar_cache)


def _informado_mudou(alvo, _valor, _antigo, _iniciador) -> None:
    sessao = object_session(alvo)
    if sessao is not None:
        sessao.info.pop(CHAVE_CACHE, None)


event.listen(Produto.qtd_estimada_producao_mes, "set", _informado_mudou)
event.listen(Produto.tabela_de_precos, "set", _informado_mudou)
