"""Precificação por Centro de Resultados: parte da primária e soma o que é do Centro.

Cada Centro de Resultados tem as suas quantidades (`VolumeProjetado`), o seu ICMS (o mix de destinos das vendas), o desconto comercial e as **despesas
adicionais** (`Despesa.cenario_id`): as fixas são rateadas só entre as quantidades do Centro, as variáveis entram no divisor como % do preço. A Meta de Lucro
é a única da primária. O motor resolve todos os Centros num só passe (`motor_preco._calcular`); sem acréscimos o Centro dá o preço e o resultado da primária.

O Centro do sistema "Produção a alocar" recebe o que ainda não foi alocado aos outros (`quantidades`): a soma das quantidades fecha sempre com a produção.
O preço de tabela é o líquido ÷ (1 − desconto); o Preço Definido (`PrecoCentro`) é de tabela e, sem ele, segue o sugerido do Centro mais a decisão manual da
primária (definido − sugerido), de modo que sem acréscimos o Centro repete a primária.
"""

from dataclasses import dataclass, field
from decimal import Decimal

from sqlalchemy.orm import Session

from custos_precificacao import cenarios as dominio_centros
from custos_precificacao import impostos, motor_preco, precificacao_primaria as pp, trava_primaria
from custos_precificacao.despesas import calcular_alocacao_despesa
from custos_precificacao.explosao import volume_de_producao
from custos_precificacao.models import CenarioPrecificacao, Despesa, Planejamento, PrecoCentro, Produto, VolumeProjetado
from custos_precificacao.motor_preco import CLASSES_DO_PRECO, Perfil


# --- quantidades -----------------------------------------------------------------------------------------------------------------

def quantidades(session: Session) -> tuple[dict[int, dict[int, Decimal]], dict[int, Decimal]]:
    """({cenario_id: {produto_id: quantidade}}, {produto_id: alocado acima da produção}). As quantidades dos Centros comuns vêm de `VolumeProjetado`; a do
    "Produção a alocar" é calculada: produção − o que os outros Centros alocaram (nunca negativa; o excesso é devolvido à parte). Só cabeças da Tabela de Preços."""
    centros = session.query(CenarioPrecificacao).order_by(CenarioPrecificacao.id).all()
    a_alocar = next((c for c in centros if c.a_alocar), None)
    cabecas = {p.id: p for p in session.query(Produto).filter(Produto.tabela_de_precos.is_(True))}
    por_centro: dict[int, dict[int, Decimal]] = {c.id: {} for c in centros}
    alocado: dict[int, Decimal] = {}
    for v in session.query(VolumeProjetado):
        if v.produto_id not in cabecas or v.cenario_id not in por_centro or (a_alocar and v.cenario_id == a_alocar.id):
            continue
        if v.quantidade:
            por_centro[v.cenario_id][v.produto_id] = Decimal(v.quantidade)
            alocado[v.produto_id] = alocado.get(v.produto_id, Decimal(0)) + Decimal(v.quantidade)
    excesso: dict[int, Decimal] = {}
    if a_alocar is not None:
        for pid, produto in cabecas.items():
            producao = volume_de_producao(session, produto) or Decimal(0)
            saldo = producao - alocado.get(pid, Decimal(0))
            if saldo > 0:
                por_centro[a_alocar.id][pid] = saldo
            elif saldo < 0:
                excesso[pid] = -saldo
    return por_centro, excesso


def definir_quantidade(session: Session, cenario_codigo: str, produto_codigo: str, quantidade: Decimal) -> None:
    """Grava a quantidade de um produto num Centro (o "Produção a alocar" é calculado e não recebe quantidade)."""
    centro = session.query(CenarioPrecificacao).filter_by(codigo=cenario_codigo).one_or_none()
    if centro is None:
        raise ValueError(f"Centro de Resultados {cenario_codigo!r} não encontrado")
    if centro.a_alocar:
        raise ValueError("A quantidade do 'Produção a alocar' é calculada (produção − o que foi alocado aos outros Centros)")
    plano = session.query(Planejamento).order_by(Planejamento.id).first()
    if plano is None:
        raise ValueError("Nenhum Planejamento cadastrado")
    from custos_precificacao import planejamento

    planejamento.definir_volume_projetado(session, plano.codigo, cenario_codigo, produto_codigo, str(quantidade))


# --- preço definido do Centro ------------------------------------------------------------------------------------------------------

def definir_preco_definido(session: Session, cenario_codigo: str, produto_codigo: str, valor: Decimal | None) -> None:
    """Grava o Preço Definido (de tabela) do produto no Centro; `None` volta a seguir o sugerido."""
    centro = session.query(CenarioPrecificacao).filter_by(codigo=cenario_codigo).one_or_none()
    if centro is None:
        raise ValueError(f"Centro de Resultados {cenario_codigo!r} não encontrado")
    if centro.a_alocar:
        raise ValueError("O 'Produção a alocar' segue o preço da precificação primária e não tem preço próprio")
    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")
    if valor is not None and (not valor.is_finite() or valor < 0):
        raise ValueError("O preço não pode ser negativo")
    linha = session.query(PrecoCentro).filter_by(cenario_id=centro.id, produto_id=produto.id).one_or_none()
    if linha is None:
        linha = PrecoCentro(cenario_id=centro.id, produto_id=produto.id)
        session.add(linha)
    linha.preco_definido = valor if valor else None  # zero = sem preço definido
    session.commit()


# --- cálculo -----------------------------------------------------------------------------------------------------------------------

@dataclass
class Acrescimos:
    """O que as despesas adicionais de um Centro somam à primária."""

    fixa_unitaria: dict[int, Decimal] = field(default_factory=dict)
    creditos: dict[int, dict[str, Decimal]] = field(default_factory=dict)
    dv_bruto: Decimal = Decimal(0)
    dv_liquido: Decimal = Decimal(0)
    dv_cred_icms: Decimal = Decimal(0)
    dv_cred_cbs: Decimal = Decimal(0)
    fixas_por_conta: dict[int, Decimal] = field(default_factory=dict)  # plano_de_contas_id -> valor rateado (DRE)
    variaveis_por_conta: dict[int, Decimal] = field(default_factory=dict)  # plano_de_contas_id -> % da receita do Centro (DRE)
    avisos: list[str] = field(default_factory=list)


def acrescimos_do_centro(session: Session, centro: CenarioPrecificacao, volumes: dict[int, Decimal], taxas: impostos.TaxasDeCredito) -> Acrescimos:
    """Despesas adicionais do Centro: as fixas rateadas entre as quantidades dele (por unidade) e as variáveis em % do preço. Uma despesa fixa que não
    pode ser rateada (ex.: o Centro ainda não tem quantidade nos produtos da Atividade) fica de fora e vira aviso."""
    saida = Acrescimos()
    for despesa in session.query(Despesa).filter_by(cenario_id=centro.id).order_by(Despesa.id):
        classes = [(c, a) for c, a in taxas.do_item("despesa", despesa.id) if c in CLASSES_DO_PRECO]
        if despesa.tipo == "variavel":
            pct = despesa.percentual_receita or Decimal(0)
            saida.dv_bruto += pct
            credito = sum((a for _, a in classes), Decimal(0))
            saida.dv_liquido += pct * max(1 - credito, Decimal(0))
            for classe, aliquota in classes:
                if classe == "icms":
                    saida.dv_cred_icms += pct * aliquota
                else:
                    saida.dv_cred_cbs += pct * aliquota
            saida.variaveis_por_conta[despesa.plano_de_contas_id] = saida.variaveis_por_conta.get(despesa.plano_de_contas_id, Decimal(0)) + pct
            continue
        try:
            alocacao = calcular_alocacao_despesa(session, despesa, volumes)
        except ValueError as exc:
            saida.avisos.append(f"A despesa adicional {despesa.codigo!r} ficou de fora do preço: {exc}")
            continue
        for produto_id, valor in alocacao.items():
            por_unidade = valor / volumes[produto_id]
            saida.fixa_unitaria[produto_id] = saida.fixa_unitaria.get(produto_id, Decimal(0)) + por_unidade
            acumulado = saida.creditos.setdefault(produto_id, {c: Decimal(0) for c in CLASSES_DO_PRECO})
            for classe, aliquota in classes:
                acumulado[classe] += por_unidade * aliquota
            saida.fixas_por_conta[despesa.plano_de_contas_id] = saida.fixas_por_conta.get(despesa.plano_de_contas_id, Decimal(0)) + valor
    return saida


@dataclass
class ResultadoCentros:
    primaria: pp.Primaria
    centros: dict[str, pp.Primaria]  # código do Centro -> a precificação dele
    acrescimos: dict[str, Acrescimos]
    excesso: dict[int, Decimal]  # produto_id -> quantidade alocada acima da produção
    cenarios: dict[str, CenarioPrecificacao]


def calcular_centros(session: Session, modo: str | None = None, exigir_trava: bool = False) -> ResultadoCentros:
    """A precificação de todos os Centros de Resultados (o "Produção a alocar" inclusive), partindo da primária. ValueError sem Meta de Lucro.
    Com `exigir_trava` (as telas dos Centros e a DRE Consolidada) a primária precisa estar travada e atualizada: sem isso, ValueError com o motivo."""
    if exigir_trava:
        bloqueio = trava_primaria.motivo_do_bloqueio(session)
        if bloqueio:
            raise ValueError(bloqueio)
    dominio_centros.garantir_centro_a_alocar(session)
    primaria = pp.calcular(session, modo)
    meta = primaria.meta
    modo = primaria.modo
    centros = session.query(CenarioPrecificacao).order_by(CenarioPrecificacao.a_alocar, CenarioPrecificacao.codigo).all()
    quant, excesso = quantidades(session)
    resultado = ResultadoCentros(primaria=primaria, centros={}, acrescimos={}, excesso=excesso, cenarios={c.codigo: c for c in centros})
    if primaria.aviso and not primaria.linhas:  # sem custeio, percentuais ≥ 100%…: nenhum Centro tem preço
        for c in centros:
            resultado.centros[c.codigo] = pp.Primaria(modo=modo, meta=meta, aviso=primaria.aviso)
        return resultado

    taxas = impostos.TaxasDeCredito(session)
    perfis: list[Perfil] = []
    for c in centros:
        extra = Acrescimos() if c.a_alocar else acrescimos_do_centro(session, c, quant[c.id], taxas)
        resultado.acrescimos[c.codigo] = extra
        perfis.append(Perfil(
            chave=c.id, nome=c.nome, volumes=quant[c.id], pct_lucro=meta.percentual_margem,
            pct_icms=impostos.aliquota_icms_primaria(session) if c.a_alocar else impostos.aliquota_icms_do_centro(session, c.id),
            desconto=Decimal(0) if c.a_alocar else (c.percentual_desconto or Decimal(0)),
            despesa_fixa=extra.fixa_unitaria, creditos_despesa=extra.creditos, dv_bruto=extra.dv_bruto, dv_liquido=extra.dv_liquido,
            dv_cred_icms=extra.dv_cred_icms, dv_cred_cbs=extra.dv_cred_cbs, aviso=" ".join(extra.avisos) or None,
        ))

    sugerido_p = {l.produto.id: l.preco_sugerido for l in primaria.linhas}
    definido_p = {l.produto.id: l.preco_definido for l in primaria.linhas}
    descontos = {p.chave: p.desconto for p in perfis}
    manuais = {(p.cenario_id, p.produto_id): p.preco_definido for p in session.query(PrecoCentro) if p.preco_definido}

    def lista_do_centro(produto_id: int, centro_id: int, sugerido_liquido: Decimal) -> Decimal:
        """Preço de tabela que vale no Centro: o digitado ou o sugerido do Centro mais a decisão manual da primária."""
        manual = manuais.get((centro_id, produto_id))
        if manual:
            return manual
        sugerido_lista = sugerido_liquido / (1 - descontos[centro_id])
        return max(sugerido_lista + (definido_p.get(produto_id, Decimal(0)) - sugerido_p.get(produto_id, Decimal(0))), Decimal(0)) if produto_id in sugerido_p else sugerido_lista

    motor = motor_preco._calcular(
        session, perfis, modo, lambda produto_id, chave, liquido: lista_do_centro(produto_id, chave, liquido) * (1 - descontos[chave])
    )
    produtos = {p.id: p for p in session.query(Produto)}
    for c in centros:
        desconto = descontos[c.id]
        centro_pp = pp.Primaria(modo=modo, meta=meta, avisos=list(motor.avisos), resultado=motor)
        aviso = motor.erros.get(c.id) or motor.avisos_centro.get(c.id)
        centro_pp.aviso = aviso
        for (produto_id, chave), comp in sorted(motor.composicoes.items(), key=lambda kv: produtos[kv[0][0]].codigo):
            if chave != c.id:
                continue
            lista = lista_do_centro(produto_id, c.id, comp.preco)
            sugerido_lista = comp.preco / (1 - desconto)
            centro_pp.linhas.append(pp.LinhaPrimaria(
                produto=produtos[produto_id], quantidade=quant[c.id].get(produto_id, Decimal(0)), composicao=comp,
                preco_sugerido=sugerido_lista, preco_definido=lista, definido_manual=(c.id, produto_id) in manuais,
                ao_sugerido=pp.analisar(comp, comp.preco, meta), ao_definido=pp.analisar(comp, lista * (1 - desconto), meta), desconto=desconto,
            ))
        resultado.centros[c.codigo] = centro_pp
    return resultado


def total_do_centro(centro: pp.Primaria) -> pp.Resultado:
    """Soma dos totais (unitário × quantidade) das linhas do Centro."""
    return centro.total_geral()


# --- DRE Consolidada -----------------------------------------------------------------------------------------------------------------

@dataclass
class ReconciliacaoCentro:
    """De quanto o Centro se afasta da primária: o que ele fatura a mais (preço, desconto), as despesas adicionais e o efeito no Resultado Operacional."""

    codigo: str
    nome: str
    quantidade: Decimal  # unidades alocadas ao Centro (soma dos produtos)
    faturamento_extra: Decimal  # faturamento do Centro − o que as mesmas quantidades dariam ao preço primário
    despesas_adicionais: Decimal  # fixas rateadas + variáveis sobre a receita do Centro
    resultado: Decimal  # Resultado Operacional do Centro
    resultado_a_preco_primario: Decimal  # o das mesmas quantidades se vendidas pela primária
    efeito: Decimal  # resultado − resultado_a_preco_primario


@dataclass
class DreConsolidada:
    linhas: list[pp.LinhaDre] = field(default_factory=list)
    reconciliacao: list[ReconciliacaoCentro] = field(default_factory=list)
    resultado_primaria: Decimal = Decimal(0)
    resultado_consolidado: Decimal = Decimal(0)
    informativos: list[pp.LinhaDre] = field(default_factory=list)


def dre_consolidada(session: Session, resultado: ResultadoCentros, centro: str | None = None) -> DreConsolidada:
    """A DRE por Plano de Contas da soma dos Centros de Resultados (ou só de `centro`): receita − impostos − custos − despesas (as do Planejamento rateadas às
    quantidades de cada Centro mais as adicionais dele) = Resultado Operacional − Destinação = Resultado Final, com a reconciliação Primária → Consolidada por
    Centro e o benefício do Desenvolve-BA como informativo (reserva de lucro: não entra no resultado nem reduz o preço)."""
    escopo = {centro: resultado.centros[centro]} if centro else resultado.centros
    meta = resultado.primaria.meta
    primaria_ro = {l.produto.id: l.ao_definido.resultado_operacional for l in resultado.primaria.linhas}
    primaria_preco = {l.produto.id: l.ao_definido.preco for l in resultado.primaria.linhas}
    linhas: list[pp.LinhaPrimaria] = []
    adicionais: dict[int | None, Decimal] = {}
    saida = DreConsolidada(resultado_primaria=resultado.primaria.total_geral().resultado_operacional)
    for codigo, cp in escopo.items():
        extra = resultado.acrescimos.get(codigo, Acrescimos())
        receita_c = sum((l.ao_definido.preco * l.quantidade for l in cp.linhas), Decimal(0))
        despesas_c = sum(extra.fixas_por_conta.values(), Decimal(0)) + sum(extra.variaveis_por_conta.values(), Decimal(0)) * receita_c
        for conta_id, valor in extra.fixas_por_conta.items():
            adicionais[conta_id] = adicionais.get(conta_id, Decimal(0)) + valor
        for conta_id, pct in extra.variaveis_por_conta.items():
            adicionais[conta_id] = adicionais.get(conta_id, Decimal(0)) + pct * receita_c
        total = cp.total_geral()
        a_preco_primario = sum((primaria_ro.get(l.produto.id, Decimal(0)) * l.quantidade for l in cp.linhas), Decimal(0))
        saida.reconciliacao.append(ReconciliacaoCentro(
            codigo=codigo, nome=resultado.cenarios[codigo].nome, quantidade=sum((l.quantidade for l in cp.linhas), Decimal(0)),
            faturamento_extra=total.preco - sum((primaria_preco.get(l.produto.id, Decimal(0)) * l.quantidade for l in cp.linhas), Decimal(0)),
            despesas_adicionais=despesas_c, resultado=total.resultado_operacional, resultado_a_preco_primario=a_preco_primario,
            efeito=total.resultado_operacional - a_preco_primario,
        ))
        linhas += cp.linhas
    saida.linhas = pp.dre_de_linhas(session, linhas, meta, adicionais)
    total_geral = pp.Resultado(*([Decimal(0)] * 9))
    for l in linhas:
        total_geral = total_geral.somar(l.ao_definido.vezes(l.quantidade))
    saida.resultado_consolidado = total_geral.resultado_operacional
    desenvolve = impostos.apurar_desenvolve(session, total_geral.icms, Decimal(0))
    if desenvolve is not None:
        conta = impostos.conta_da_reserva(session)
        saida.informativos = [
            pp.LinhaDre("Saldo devedor de ICMS (sem o benefício)", total_geral.icms, estilo="conta"),
            pp.LinhaDre(
                "Benefício do Desenvolve-BA — reserva de lucro" + (f" ({conta})" if conta else ""), desenvolve.beneficio, estilo="conta",
            ),
        ]
    return saida
