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

from sqlalchemy.orm import Session

from custos_precificacao import arvore, historico, unidades
from custos_precificacao.entradas import Entradas
from custos_precificacao.explosao import volume_de_producao
from custos_precificacao.models import (
    Ambiente,
    CenarioAmbienteDesligado,
    CenarioGrupoDesligado,
    CenarioMaquinaDesligado,
    CenarioPrecificacao,
    CusteioExecucao,
    CusteioProduto,
    GrupoMaoDeObra,
    Insumo,
    Maquina,
    Produto,
    RoteiroInsumo,
)


@dataclass
class RateioCenario:
    ambientes_desligados: set[int]
    maquinas_desligadas: set[int]
    grupos_desligados: set[int]  # grupos de mão de obra (Depto-Cargo)


def carregar_rateio_cenario(session: Session, cenario: CenarioPrecificacao) -> RateioCenario:
    """Carrega, para um Cenário, quais Instalação/Máquina/grupo de Mão de obra estão desligados do rateio.

    Ausência nas tabelas de exceção significa "ligado" (o padrão).
    """
    ambientes = {
        row.ambiente_id
        for row in session.query(CenarioAmbienteDesligado).filter_by(cenario_id=cenario.id)
    }
    maquinas = {
        row.maquina_id
        for row in session.query(CenarioMaquinaDesligado).filter_by(cenario_id=cenario.id)
    }
    grupos = {
        row.grupo_id
        for row in session.query(CenarioGrupoDesligado).filter_by(cenario_id=cenario.id)
    }
    return RateioCenario(ambientes, maquinas, grupos)


@dataclass
class CustoProduto:
    custo_maquina: Decimal
    custo_ambiente: Decimal
    custo_mao_de_obra: Decimal
    custo_insumos: Decimal
    custo_variavel_abc: Decimal  # parcela variável (ligada) de máquina+ambiente+mão de obra+outros
    custo_insumos_variavel: Decimal  # igual a custo_insumos, exceto quando há semiacabado
    custo_outros: Decimal = Decimal(0)  # elemento "Outros" (linhas de roteiro de máquinas com categoria 'outro')
    # Divisão do custo de insumos (absorvido): matéria-prima, embalagem e intermediários (semiacabados/produtos usados como insumo).
    custo_materia_prima: Decimal = Decimal(0)
    custo_embalagem: Decimal = Decimal(0)
    custo_intermediarios: Decimal = Decimal(0)
    # Rendimento (un ou kg) como era na competência do custeio. Sem isso, usa o do Produto (valor atual).
    rendimento: Decimal | None = None
    rendimento_resolvido: bool = False
    # Intermediários consumidos: (produto, quantidade por lote na unidade base — com a perda da linha —, rendimento por unidade base).
    # Servem para repassar a ociosidade do intermediário a quem o consome (ver `aplicar_ociosidade`).
    semiacabados: list[tuple[int, Decimal, Decimal]] = field(default_factory=list)

    @property
    def custo_producao_lote(self) -> Decimal:
        return self.custo_maquina + self.custo_ambiente + self.custo_mao_de_obra + self.custo_outros + self.custo_insumos

    @property
    def custo_variavel_lote(self) -> Decimal:
        return self.custo_variavel_abc + self.custo_insumos_variavel

    def _rendimento(self, produto: Produto) -> Decimal | None:
        if self.rendimento_resolvido:
            return self.rendimento
        return produto.rendimento_un or produto.rendimento_kg

    def custo_producao_unitario(self, produto: Produto) -> Decimal:
        rendimento = self._rendimento(produto)
        if not rendimento:
            return self.custo_producao_lote
        return self.custo_producao_lote / rendimento

    def custo_variavel_unitario(self, produto: Produto) -> Decimal:
        rendimento = self._rendimento(produto)
        if not rendimento:
            return self.custo_variavel_lote
        return self.custo_variavel_lote / rendimento


@dataclass
class LinhaCusto:
    """Quanto uma linha de roteiro custa no lote (para a ficha técnica, §9)."""

    tipo: str  # maquina | outros | ambiente | mao-de-obra | insumo
    linha_id: int
    descricao: str
    quantidade: Decimal  # horas por lote, ou quantidade de insumo já convertida para a unidade base e com a perda
    unidade: str  # h | unidade base do insumo/produto
    base: Decimal  # custo/hora ou custo por unidade base usado
    valor_producao: Decimal
    valor_variavel: Decimal
    atualizado_em: date
    atividade_id: int
    # só nas linhas de insumo: o que foi digitado na formulação, a perda da linha e o produto de origem (semiacabado)
    quantidade_informada: Decimal | None = None
    unidade_informada: str | None = None
    perda_pct: Decimal = Decimal(0)
    produto_origem_id: int | None = None
    # só nas linhas de elemento (máquina, outros, ambiente, mão de obra): o direcionador pelo qual foram alocadas
    direcionador_id: int | None = None


def _custo_insumo_produto(
    session: Session, produto: Produto, rateio: RateioCenario, _visitados: set[int], entradas: Entradas
) -> tuple[str, Decimal, Decimal, Decimal]:
    """(unidade base, custo de produção, custo variável, rendimento) por unidade base de um produto/semiacabado usado como insumo de outro.
    A unidade base é `Produto.unidade` (kg por padrão); o rendimento vem da bacia com perda quando há `base_quantidade`."""
    if produto.id in _visitados:
        raise RuntimeError(f"Ciclo detectado no roteiro do produto {produto.codigo!r}")
    base = unidades.normalizar(produto.unidade)
    rendimento_un, rendimento_kg = entradas.rendimentos(produto)
    rendimento = rendimento_un if base == "un" else rendimento_kg
    if not rendimento:
        campo = "rendimento_un" if base == "un" else "rendimento_kg"
        raise ValueError(f"Produto {produto.codigo!r} não tem {campo} definido, não pode ser usado como insumo por {base}")

    custo = calcular_custo_produto(session, produto, rateio, _visitados | {produto.id}, entradas)
    return base, custo.custo_producao_lote / rendimento, custo.custo_variavel_lote / rendimento, rendimento


def calcular_custo_produto(
    session: Session,
    produto: Produto,
    rateio: RateioCenario,
    _visitados: set[int] | None = None,
    entradas: Entradas | None = None,
    _linhas: list[LinhaCusto] | None = None,
) -> CustoProduto:
    """Calcula o custo do lote/receita de um produto a partir do seu roteiro, para um Cenário.

    Itens desligados do rateio nesse Cenário não entram na soma por produto. Resolve
    semiacabados usados como insumo (roteiro_insumo.insumo_produto) recursivamente.

    `entradas` resolve os valores de entrada (custo/hora, preço de insumo, rendimento, roteiro):
    omitido = valores atuais; `Entradas(session, competencia)` = como eram naquele mês (§9). Se `_linhas`
    for uma lista, recebe uma `LinhaCusto` por linha de roteiro (usada pela ficha técnica).
    """
    entradas = entradas or Entradas(session)
    visitados = (_visitados or set()) | {produto.id}

    if produto.tipo == "revenda":  # comprado pronto: o custo de compra de 1 un é o custo todo (sem formulação nem rateio)
        compra = entradas.custo_compra(produto)
        rendimento_un, rendimento_kg = entradas.rendimentos(produto)
        return CustoProduto(
            custo_maquina=Decimal(0), custo_ambiente=Decimal(0), custo_mao_de_obra=Decimal(0), custo_insumos=compra,
            custo_variavel_abc=Decimal(0), custo_insumos_variavel=compra,
            rendimento=rendimento_un or rendimento_kg or Decimal(1), rendimento_resolvido=True,
        )

    custo_maquina = Decimal(0)
    custo_outros = Decimal(0)
    custo_variavel_abc = Decimal(0)
    for linha in entradas.roteiro(produto, "maquina"):
        if linha.maquina_id in rateio.maquinas_desligadas:
            continue
        maquina = entradas.recurso("maquina", linha.maquina_id)
        base = entradas.custo_hora("maquina", maquina)
        valor = linha.tempo_horas * base
        outro = maquina.categoria == "outro"
        if outro:
            custo_outros += valor
        else:
            custo_maquina += valor
        if maquina.classificacao == "variavel":
            custo_variavel_abc += valor
        if _linhas is not None:
            _linhas.append(LinhaCusto(
                "outros" if outro else "maquina", linha.id, f"{maquina.codigo} - {maquina.nome}", linha.tempo_horas, "h", base, valor,
                valor if maquina.classificacao == "variavel" else Decimal(0), linha.atualizado_em, linha.atividade_id,
                direcionador_id=getattr(linha, "direcionador_id", None),
            ))

    custo_ambiente = Decimal(0)
    for linha in entradas.roteiro(produto, "ambiente"):
        if linha.ambiente_id in rateio.ambientes_desligados:
            continue
        ambiente = entradas.recurso("ambiente", linha.ambiente_id)
        base = entradas.custo_hora("ambiente", ambiente)
        valor = linha.tempo_horas * base
        custo_ambiente += valor
        if ambiente.classificacao == "variavel":
            custo_variavel_abc += valor
        if _linhas is not None:
            _linhas.append(LinhaCusto(
                "ambiente", linha.id, f"{ambiente.codigo} - {ambiente.nome}", linha.tempo_horas, "h", base, valor,
                valor if ambiente.classificacao == "variavel" else Decimal(0), linha.atualizado_em, linha.atividade_id,
                direcionador_id=getattr(linha, "direcionador_id", None),
            ))

    custo_mao_de_obra = Decimal(0)
    for linha in entradas.roteiro(produto, "mao-de-obra"):
        if linha.grupo_id in rateio.grupos_desligados:
            continue
        grupo = entradas.recurso("grupo", linha.grupo_id)
        base = entradas.custo_hora("grupo", grupo)
        valor = linha.tempo_horas * base
        custo_mao_de_obra += valor
        if grupo.classificacao == "variavel":
            custo_variavel_abc += valor
        if _linhas is not None:
            _linhas.append(LinhaCusto(
                "mao-de-obra", linha.id, f"{grupo.codigo} - {grupo.nome}", linha.tempo_horas, "h",
                base, valor, valor if grupo.classificacao == "variavel" else Decimal(0),
                linha.atualizado_em, linha.atividade_id, direcionador_id=getattr(linha, "direcionador_id", None),
            ))

    custo_insumos = Decimal(0)
    custo_insumos_variavel = Decimal(0)
    custo_materia_prima = custo_embalagem = custo_intermediarios = Decimal(0)
    semiacabados: list[tuple[int, Decimal, Decimal]] = []
    for linha in entradas.roteiro(produto, "insumo"):
        unidade_linha = unidades.normalizar(getattr(linha, "unidade", None))
        fator_perda = unidades.fator_de_perda(getattr(linha, "perda_pct", None))
        if linha.insumo_id is not None:
            insumo = session.get(Insumo, linha.insumo_id)
            preco_producao = preco_variavel = entradas.custo_final(insumo)
            unidade_base = unidades.normalizar(insumo.unidade)
            quantidade = unidades.converter(session, insumo, linha.quantidade_kg, unidade_linha, unidade_base) * fator_perda
            descricao = f"{insumo.codigo} - {insumo.nome}"
        else:
            semi = session.get(Produto, linha.insumo_produto_id)
            unidade_base, preco_producao, preco_variavel, rendimento_base = _custo_insumo_produto(session, semi, rateio, visitados, entradas)
            quantidade = unidades.converter(session, semi, linha.quantidade_kg, unidade_linha, unidade_base) * fator_perda
            semiacabados.append((semi.id, quantidade, rendimento_base))
            descricao = f"{semi.codigo} - {semi.nome}"
        custo_insumos += quantidade * preco_producao
        custo_insumos_variavel += quantidade * preco_variavel
        if linha.insumo_id is None:
            custo_intermediarios += quantidade * preco_producao
        elif insumo.tipo == "embalagem":
            custo_embalagem += quantidade * preco_producao
        else:
            custo_materia_prima += quantidade * preco_producao
        if _linhas is not None:
            _linhas.append(LinhaCusto(
                "insumo", linha.id, descricao, quantidade, unidade_base, preco_producao,
                quantidade * preco_producao, quantidade * preco_variavel,
                linha.atualizado_em, linha.atividade_id,
                quantidade_informada=linha.quantidade_kg, unidade_informada=unidade_linha,
                perda_pct=Decimal(getattr(linha, "perda_pct", None) or 0),
                produto_origem_id=linha.insumo_produto_id,
            ))

    rendimento_un, rendimento_kg = entradas.rendimentos(produto)
    return CustoProduto(
        custo_maquina=custo_maquina,
        custo_ambiente=custo_ambiente,
        custo_mao_de_obra=custo_mao_de_obra,
        custo_insumos=custo_insumos,
        custo_variavel_abc=custo_variavel_abc,
        custo_insumos_variavel=custo_insumos_variavel,
        custo_outros=custo_outros,
        custo_materia_prima=custo_materia_prima,
        custo_embalagem=custo_embalagem,
        custo_intermediarios=custo_intermediarios,
        rendimento=rendimento_un or rendimento_kg,
        rendimento_resolvido=True,
        semiacabados=semiacabados,
    )


def calcular_custo_fixo_nao_rateado(session: Session, rateio: RateioCenario, entradas: Entradas | None = None) -> Decimal:
    """Soma o custo mensal próprio de cada recurso desligado do rateio nesse Cenário."""
    entradas = entradas or Entradas(session)
    total = Decimal(0)

    if rateio.ambientes_desligados:
        for ambiente in session.query(Ambiente).filter(Ambiente.id.in_(rateio.ambientes_desligados)):
            total += entradas.custo_mensal("ambiente", ambiente)

    if rateio.maquinas_desligadas:
        for maquina in session.query(Maquina).filter(Maquina.id.in_(rateio.maquinas_desligadas)):
            total += entradas.custo_mensal("maquina", maquina)

    if rateio.grupos_desligados:
        for grupo in session.query(GrupoMaoDeObra).filter(GrupoMaoDeObra.id.in_(rateio.grupos_desligados)):
            total += entradas.custo_mensal("grupo", grupo)

    return total


TIPOS_ELEMENTO = ("mao_de_obra", "maquina", "ambiente", "outros")


@dataclass
class Ociosidade:
    """Ociosidade dos elementos rateada aos produtos, por tipo (ver `calcular_ociosidade`)."""

    # produto_id -> tipo -> (ociosidade própria por lote, parte dela vinda de elementos Variáveis)
    por_produto: dict[int, dict[str, tuple[Decimal, Decimal]]]
    nao_alocada: Decimal  # de todos os elementos: nenhum produto com volume usa o tipo
    variavel_nao_alocado: Decimal  # parte da `nao_alocada` vinda de elementos Variáveis


def _custo_do_tipo(custo: CustoProduto, tipo: str) -> Decimal:
    return {"mao_de_obra": custo.custo_mao_de_obra, "maquina": custo.custo_maquina, "ambiente": custo.custo_ambiente, "outros": custo.custo_outros}[tipo]


def calcular_ociosidade(
    session: Session, rateio: RateioCenario, entradas: Entradas, produtos: list[tuple[Produto, CustoProduto]]
) -> Ociosidade:
    """Ociosidade dos elementos (fixos e variáveis) rateada aos produtos, por tipo: pessoal, máquinas, ambiente e outros.

    Ociosidade de um elemento ligado ao rateio do Centro = (horas úteis − horas usadas pelo roteiro no volume de produção) × custo/hora;
    sobrecarga não gera ociosidade negativa. As ociosidades de um tipo (separadas em Fixo e Variável) formam um bloco dividido entre
    os produtos que usam o tipo, proporcional ao **custo absorvido do tipo × volume**: taxa = bloco ÷ Σ(lotes × custo do tipo no lote);
    a ociosidade do produto no tipo (por lote) = taxa × custo do tipo no lote. Produto sem volume ou sem rendimento fica de fora. Se
    ninguém usa o tipo, o bloco fica "não alocado". O volume é `explosao.volume_de_producao` (PI calculado pela estrutura)."""
    horas_usadas: dict[tuple[str, int], Decimal] = {}
    pesos: dict[int, dict[str, Decimal]] = {}  # produto -> tipo -> custo do tipo no lote
    peso_total = {tipo: Decimal(0) for tipo in TIPOS_ELEMENTO}
    for produto, custo in produtos:
        un, kg = entradas.rendimentos(produto)
        rendimento = un or kg
        volume = volume_de_producao(session, produto)
        if not rendimento or not volume:
            continue
        lotes = volume / rendimento
        for tipo_recurso, tipo_roteiro, coluna in (
            ("maquina", "maquina", "maquina_id"), ("ambiente", "ambiente", "ambiente_id"), ("grupo", "mao-de-obra", "grupo_id"),
        ):
            for linha in entradas.roteiro(produto, tipo_roteiro):
                chave = (tipo_recurso, getattr(linha, coluna))
                horas_usadas[chave] = horas_usadas.get(chave, Decimal(0)) + linha.tempo_horas * lotes
        pesos[produto.id] = {tipo: _custo_do_tipo(custo, tipo) for tipo in TIPOS_ELEMENTO}
        for tipo in TIPOS_ELEMENTO:
            peso_total[tipo] += lotes * pesos[produto.id][tipo]
    produtivos = arvore.centros_produtivos(session)
    # blocos de ociosidade: (tipo, classe) -> valor
    blocos: dict[tuple[str, str], Decimal] = {}
    for tipo_recurso, modelo, desligados in (
        ("maquina", Maquina, rateio.maquinas_desligadas),
        ("ambiente", Ambiente, rateio.ambientes_desligados),
        ("grupo", GrupoMaoDeObra, rateio.grupos_desligados),
    ):
        for recurso in session.query(modelo):
            if recurso.id in desligados or getattr(recurso, "aceita_lancamento", True) is False:
                continue
            if recurso.centro_de_custo_id is not None and recurso.centro_de_custo_id not in produtivos:
                continue  # centro não produtivo (despesa): a ociosidade dele não entra no custo dos produtos
            ociosas = entradas.horas_uteis(tipo_recurso, recurso) - horas_usadas.get((tipo_recurso, recurso.id), Decimal(0))
            if ociosas <= 0:
                continue
            tipo = "outros" if tipo_recurso == "maquina" and recurso.categoria == "outro" else {"maquina": "maquina", "ambiente": "ambiente", "grupo": "mao_de_obra"}[tipo_recurso]
            chave = (tipo, recurso.classificacao)
            blocos[chave] = blocos.get(chave, Decimal(0)) + ociosas * entradas.custo_hora(tipo_recurso, recurso)

    nao_alocada = variavel_nao_alocado = Decimal(0)
    taxas: dict[tuple[str, str], Decimal] = {}
    for (tipo, classe), valor in blocos.items():
        if peso_total[tipo] > 0:
            taxas[(tipo, classe)] = valor / peso_total[tipo]
        else:
            nao_alocada += valor
            if classe == "variavel":
                variavel_nao_alocado += valor

    por_produto: dict[int, dict[str, tuple[Decimal, Decimal]]] = {}
    for produto_id, custos_do_tipo in pesos.items():
        por_produto[produto_id] = {
            tipo: (
                (taxas.get((tipo, "fixo"), Decimal(0)) + taxas.get((tipo, "variavel"), Decimal(0))) * custo_tipo,
                taxas.get((tipo, "variavel"), Decimal(0)) * custo_tipo,
            )
            for tipo, custo_tipo in custos_do_tipo.items()
        }
    return Ociosidade(por_produto, nao_alocada, variavel_nao_alocado)


@dataclass
class CustoComOciosidade:
    """Custo do produto com a ociosidade: a própria (por tipo) e a repassada pelos intermediários que ele consome."""

    ociosidade_propria: dict[str, Decimal]  # tipo -> ociosidade própria por lote
    ociosidade_propria_variavel: Decimal  # parte própria vinda de elementos Variáveis (por lote)
    repassada: Decimal  # ociosidade dos intermediários consumida (por lote), já dentro do "insumos"
    repassada_variavel: Decimal
    producao_lote: Decimal  # absorvido + ociosidade própria + repassada
    variavel_lote: Decimal


def aplicar_ociosidade(custos: dict[int, CustoProduto], ociosidade: Ociosidade) -> dict[int, CustoComOciosidade]:
    """Soma a ociosidade ao custo de cada produto e repassa a dos intermediários a quem os consome:
    total_lote(p) = absorvido(p) + ociosidade própria(p) + Σ q × (total_lote(semi) − absorvido(semi)) ÷ rendimento base do semi."""
    resultado: dict[int, CustoComOciosidade] = {}

    def calcular(produto_id: int, pilha: frozenset[int]) -> CustoComOciosidade:
        if produto_id in resultado:
            return resultado[produto_id]
        custo = custos[produto_id]
        por_tipo = ociosidade.por_produto.get(produto_id, {})
        propria = {tipo: por_tipo.get(tipo, (Decimal(0), Decimal(0)))[0] for tipo in TIPOS_ELEMENTO}
        propria_variavel = sum((por_tipo.get(tipo, (Decimal(0), Decimal(0)))[1] for tipo in TIPOS_ELEMENTO), Decimal(0))
        repassada = repassada_variavel = Decimal(0)
        for semi_id, quantidade, rendimento_base in custo.semiacabados:
            if semi_id in pilha or semi_id == produto_id or semi_id not in custos:
                continue
            semi = calcular(semi_id, pilha | {produto_id})
            semi_custo = custos[semi_id]
            repassada += quantidade * (semi.producao_lote - semi_custo.custo_producao_lote) / rendimento_base
            repassada_variavel += quantidade * (semi.variavel_lote - semi_custo.custo_variavel_lote) / rendimento_base
        resultado[produto_id] = CustoComOciosidade(
            ociosidade_propria=propria, ociosidade_propria_variavel=propria_variavel, repassada=repassada, repassada_variavel=repassada_variavel,
            producao_lote=custo.custo_producao_lote + sum(propria.values(), Decimal(0)) + repassada,
            variavel_lote=custo.custo_variavel_lote + propria_variavel + repassada_variavel,
        )
        return resultado[produto_id]

    for produto_id in custos:
        calcular(produto_id, frozenset())
    return resultado


def execucao_mais_recente(session: Session, cenario_id: int | None = None) -> CusteioExecucao | None:
    """A execução de custeio que vale "agora": a de **competência** mais recente (um custeio retroativo, rodado depois, não pode passar
    na frente) e, na mesma competência, a rodada por último. O custeio é do Planejamento, não de um Centro de Resultado: `cenario_id`
    só existe por compatibilidade e não filtra (ver `execucao_do_planejamento`). None se nunca houve custeio."""
    return execucao_do_planejamento(session)


def execucao_do_planejamento(session: Session) -> CusteioExecucao | None:
    """A execução de custeio que vale "agora" para o Planejamento (preço, DRE, Custo dos Produtos): a de competência mais recente entre as
    do Planejamento (sem Centro de Resultado); sem nenhuma, a mais recente de qualquer Centro (dados de antes do custeio único)."""
    ordem = (CusteioExecucao.competencia.desc(), CusteioExecucao.executado_em.desc(), CusteioExecucao.id.desc())
    return (
        session.query(CusteioExecucao).filter(CusteioExecucao.cenario_id.is_(None)).order_by(*ordem).first()
        or session.query(CusteioExecucao).order_by(*ordem).first()
    )


def produtos_do_custeio(session: Session) -> list[Produto]:
    """Produtos que o custeio calcula: as cabeças (marcadas na Tabela de Preços) e, descendo pela estrutura
    (`RoteiroInsumo.insumo_produto_id`), os intermediários que elas usam."""
    todos = session.query(Produto).order_by(Produto.id).all()
    consumo: dict[int, list[int]] = {}
    for pai_id, filho_id in session.query(RoteiroInsumo.produto_id, RoteiroInsumo.insumo_produto_id).filter(RoteiroInsumo.insumo_produto_id.isnot(None)):
        consumo.setdefault(pai_id, []).append(filho_id)
    alcance = {p.id for p in todos if p.tabela_de_precos}
    pilha = list(alcance)
    while pilha:
        for filho_id in consumo.get(pilha.pop(), []):
            if filho_id not in alcance:
                alcance.add(filho_id)
                pilha.append(filho_id)
    return [p for p in todos if p.id in alcance]


def executar_custeio(
    session: Session,
    cenario: CenarioPrecificacao | None = None,
    observacao: str | None = None,
    competencia: date | str | None = None,
) -> CusteioExecucao:
    """Calcula o custo das cabeças da Tabela de Preços e da estrutura delas e grava uma nova rodada de custeio.

    Sem `cenario` é o **custeio do Planejamento** (o que o app roda): todos os elementos ligados, sem Centro de Resultado. Com `cenario`,
    vale o liga/desliga de elementos daquele Centro (caminho antigo, mantido para o CLI e os testes).

    `competencia` (mês, padrão o atual) escolhe as versões das entradas que valem: custos atualizados
    depois dela não entram, e atualizações já lançadas para meses futuros também não. A execução guarda
    a competência e um snapshot das entradas usadas (`CusteioExecucao.entradas`).
    """
    mes = historico.normalizar_mes(competencia)
    entradas = Entradas(session, mes, registrar=True)
    rateio = carregar_rateio_cenario(session, cenario) if cenario is not None else RateioCenario(set(), set(), set())

    execucao = CusteioExecucao(
        cenario_id=cenario.id if cenario is not None else None,
        observacao=observacao,
        competencia=mes,
        executado_por=session.info.get("usuario"),
        custo_fixo_nao_rateado=calcular_custo_fixo_nao_rateado(session, rateio, entradas),
    )
    session.add(execucao)
    session.flush()

    produtos = [(produto, calcular_custo_produto(session, produto, rateio, entradas=entradas)) for produto in produtos_do_custeio(session)]
    ociosidade = calcular_ociosidade(session, rateio, entradas, produtos)
    com_ociosidade = aplicar_ociosidade({produto.id: custo for produto, custo in produtos}, ociosidade)
    execucao.ociosidade_nao_alocada = ociosidade.nao_alocada
    execucao.variavel_nao_alocado = ociosidade.variavel_nao_alocado
    for produto, custo in produtos:
        total = com_ociosidade[produto.id]
        rendimento = custo.rendimento or None
        por_lote = lambda valor: valor / rendimento if rendimento else valor  # noqa: E731  (sem rendimento, o unitário é o do lote)
        session.add(
            CusteioProduto(
                execucao_id=execucao.id,
                produto_id=produto.id,
                custo_maquina=custo.custo_maquina,
                custo_ambiente=custo.custo_ambiente,
                custo_mao_de_obra=custo.custo_mao_de_obra,
                custo_outros=custo.custo_outros,
                custo_insumos=custo.custo_insumos + total.repassada,  # inclui a ociosidade dos intermediários consumidos
                custo_variavel_lote=total.variavel_lote,
                custo_producao_unitario=por_lote(total.producao_lote),  # total geral por unidade: absorvido + ociosidade
                custo_variavel_unitario=por_lote(total.variavel_lote),
                custo_ineficiencia_unitario=por_lote(total.variavel_lote - custo.custo_variavel_lote),
                ociosidade_maquina=total.ociosidade_propria["maquina"],
                ociosidade_ambiente=total.ociosidade_propria["ambiente"],
                ociosidade_mao_de_obra=total.ociosidade_propria["mao_de_obra"],
                ociosidade_outros=total.ociosidade_propria["outros"],
                custo_materia_prima=custo.custo_materia_prima,
                custo_embalagem=custo.custo_embalagem,
                custo_intermediarios=custo.custo_intermediarios,
                volume_producao=volume_de_producao(session, produto),
                rendimento=rendimento,
            )
        )

    execucao.entradas = entradas.snapshot(rateio)
    session.commit()
    return execucao
