from decimal import Decimal

from sqlalchemy.orm import Session

from custos_precificacao.custeio import execucao_do_planejamento
from custos_precificacao.explosao import volume_de_producao
from custos_precificacao.models import (
    CusteioProduto,
    Despesa,
    Produto,
    RoteiroAmbiente,
    RoteiroInsumo,
    RoteiroMaoDeObra,
    RoteiroMaquina,
)


def validar_despesa(d: Despesa) -> None:
    """Fixa pede o valor mensal e o Direcionador (critério de rateio); variável pede o % sobre a receita bruta. Ajusta o que o tipo não usa
    (fixa sem %, variável sem valor nem Direcionador). ValueError com a mensagem para o usuário."""
    if d.tipo not in ("fixa", "variavel"):
        raise ValueError("tipo deve ser 'fixa' ou 'variavel'")
    if not (d.titulo or "").strip():
        raise ValueError("O título é obrigatório")
    if d.tipo == "fixa":
        if d.direcionador_id is None:
            raise ValueError("Despesa fixa precisa de um Direcionador (o critério de rateio)")
        d.percentual_receita = None
    else:
        if not d.percentual_receita:
            raise ValueError("Despesa variável precisa do % sobre a receita bruta")
        d.valor = Decimal(0)
        d.direcionador_id = None


def _produtos_da_atividade(session: Session, atividade_id: int) -> set[int]:
    """Produtos com qualquer linha de roteiro (dos 4 tipos) referenciando essa Atividade."""
    ids: set[int] = set()
    for model in (RoteiroMaquina, RoteiroAmbiente, RoteiroMaoDeObra, RoteiroInsumo):
        ids.update(
            produto_id
            for (produto_id,) in session.query(model.produto_id).filter(model.atividade_id == atividade_id)
        )
    return ids


def _quantidade_produzida(session: Session, produto_id: int, volumes: dict[int, Decimal] | None = None) -> Decimal:
    """Volume de produção do mês (informado + o puxado pelos produtos que o consomem); 0 = o produto não é produzido e fica fora do rateio.
    Com `volumes` (as quantidades de um Centro de Resultados), vale a quantidade dele no lugar da produção."""
    if volumes is not None:
        return volumes.get(produto_id) or Decimal(0)
    return volume_de_producao(session, session.get(Produto, produto_id)) or Decimal(0)


def _cabecas_da_atividade(session: Session, atividade_id: int) -> set[int]:
    """Produtos da Tabela de Preços (cabeças) que usam a Atividade — no roteiro deles ou no de um intermediário da estrutura deles. É entre eles
    que a despesa se ratea por quantidade ou custo primário: o que ela custa chega ao preço de quem é vendido."""
    diretos = _produtos_da_atividade(session, atividade_id)
    if not diretos:
        return set()
    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)
    cabecas: set[int] = set()
    for (produto_id,) in session.query(Produto.id).filter(Produto.tabela_de_precos.is_(True)):
        visto, pilha = {produto_id}, [produto_id]
        while pilha:
            atual = pilha.pop()
            if atual in diretos:
                cabecas.add(produto_id)
                break
            for filho_id in consumo.get(atual, []):
                if filho_id not in visto:
                    visto.add(filho_id)
                    pilha.append(filho_id)
    return cabecas


def _peso_tempo_por_produto(session: Session, atividade_id: int, volumes: dict[int, Decimal] | None = None) -> dict[int, Decimal]:
    """Peso = soma de tempo_horas (maquina+ambiente+mao_de_obra) na Atividade × quantidade produzida."""
    tempo_por_produto: dict[int, Decimal] = {}
    for model in (RoteiroMaquina, RoteiroAmbiente, RoteiroMaoDeObra):
        for produto_id, tempo_horas in session.query(model.produto_id, model.tempo_horas).filter(
            model.atividade_id == atividade_id
        ):
            tempo_por_produto[produto_id] = tempo_por_produto.get(produto_id, Decimal(0)) + tempo_horas

    pesos = {produto_id: tempo * _quantidade_produzida(session, produto_id, volumes) for produto_id, tempo in tempo_por_produto.items() if tempo}
    return {produto_id: peso for produto_id, peso in pesos.items() if peso}


def _peso_quantidade_por_produto(session: Session, atividade_id: int, volumes: dict[int, Decimal] | None = None) -> dict[int, Decimal]:
    pesos = {produto_id: _quantidade_produzida(session, produto_id, volumes) for produto_id in _cabecas_da_atividade(session, atividade_id)}
    return {produto_id: peso for produto_id, peso in pesos.items() if peso}


def _peso_custo_primario_por_produto(session: Session, atividade_id: int, volumes: dict[int, Decimal] | None = None) -> dict[int, Decimal]:
    """Peso = custo primário unitário (o "Total" da tela Custo dos Produtos: insumos + elementos + ociosidade) × quantidade produzida.
    Produto que a Atividade usa mas que o custeio não calculou fica de fora."""
    execucao = execucao_do_planejamento(session)
    if execucao is None:
        raise ValueError("Não há custeio calculado — calcule o Custo dos Produtos antes de ratear despesa por custo primário")
    custos = {
        produto_id: custo
        for produto_id, custo in session.query(CusteioProduto.produto_id, CusteioProduto.custo_producao_unitario).filter_by(execucao_id=execucao.id)
    }
    pesos: dict[int, Decimal] = {}
    for produto_id in _cabecas_da_atividade(session, atividade_id):
        custo = custos.get(produto_id)
        peso = custo * _quantidade_produzida(session, produto_id, volumes) if custo else Decimal(0)
        if peso:
            pesos[produto_id] = peso
    return pesos


def calcular_alocacao_despesa(session: Session, despesa: Despesa, volumes: dict[int, Decimal] | None = None) -> dict[int, Decimal]:
    """Rateia o valor da Despesa fixa entre os produtos que consomem a Atividade do seu Direcionador.

    A despesa do Planejamento (sem `volumes`) segue o rateio da primária (`rateio.alocar`: a sugestão pelo critério do Direcionador ou o % gravado pelo
    gestor). Na despesa adicional de um Centro (`volumes`) o peso de cada produto depende da base do Direcionador: 'tempo' (horas do roteiro × volume), 'quantidade_produzida' ou 'custo_primario'
    (custo unitário do produto × volume). Só entram produtos com volume de produção; os que não são produzidos ficam de fora.
    Retorna {produto_id: valor_alocado}, com a soma dos valores igual a despesa.valor.
    Despesa variável não se ratea (é um % da receita): devolve {}.
    `volumes` = {produto_id: quantidade} de um Centro de Resultados: a despesa adicional dele é rateada só entre as quantidades dele.
    """
    if despesa.tipo != "fixa":
        return {}
    if despesa.direcionador is None:
        raise ValueError(f"Despesa fixa {despesa.codigo!r} não tem Direcionador — não há como ratear")
    if volumes is None and despesa.cenario_id is None:  # despesa do Planejamento: vale o rateio da primária (a sugestão ou o que o gestor gravou)
        from custos_precificacao import rateio

        return rateio.alocar(session, despesa)
    atividade_id = despesa.direcionador.atividade_id
    base = despesa.direcionador.base

    if base == "tempo":
        pesos = _peso_tempo_por_produto(session, atividade_id, volumes)
    elif base == "custo_primario":
        pesos = _peso_custo_primario_por_produto(session, atividade_id, volumes)
    else:
        pesos = _peso_quantidade_por_produto(session, atividade_id, volumes)

    total_peso = sum(pesos.values(), Decimal(0))
    if not total_peso:
        raise ValueError(
            f"Nenhum produto com peso > 0 para a Atividade {atividade_id} "
            f"(Direcionador {despesa.direcionador.codigo!r}) — não há como ratear a despesa"
        )

    return {produto_id: despesa.valor * peso / total_peso for produto_id, peso in pesos.items()}


def despesas_variaveis_pct(session: Session) -> Decimal:
    """Soma dos % sobre a receita bruta das Despesas variáveis (fração: 0,05 = 5% do preço de venda)."""
    return sum((d.percentual_receita or Decimal(0) for d in session.query(Despesa).filter_by(tipo="variavel", cenario_id=None)), Decimal(0))


def _por_unidade(session: Session, totais: dict[int, Decimal]) -> dict[int, Decimal]:
    """Divide o total alocado de cada produto pelo volume de produção dele (produto sem volume fica de fora)."""
    unitarias: dict[int, Decimal] = {}
    for produto_id, total in totais.items():
        volume = volume_de_producao(session, session.get(Produto, produto_id))
        if volume:
            unitarias[produto_id] = total / volume
    return unitarias


def calcular_despesas_alocadas_unitarias(session: Session) -> dict[int, Decimal]:
    """Soma, por Produto, a alocação das Despesas fixas, por unidade produzida.

    Despesa fixa é do Planejamento (sem competência e sem recorte por Cenário: rateio por Atividade/Direcionador, vale pra qualquer Cenário).
    Despesa variável (% da receita) não entra aqui: é um percentual do preço, tratado à parte (`despesas_variaveis_pct`).

    Divide o total alocado pela quantidade produzida do próprio Produto.
    """
    totais: dict[int, Decimal] = {}
    for despesa in session.query(Despesa).filter_by(tipo="fixa", cenario_id=None):
        for produto_id, valor in calcular_alocacao_despesa(session, despesa).items():
            totais[produto_id] = totais.get(produto_id, Decimal(0)) + valor
    return _por_unidade(session, totais)
