"""Precificação primária: o resultado que a empresa almeja, único e sem os acréscimos dos Centros de Resultado (que partirão dela).

- **Meta de Lucro** (`MetaLucro`): % do preço de venda repartido entre sócios, reserva, expansão e outros — líquido do IR/CSLL.
- **Precificação**: o motor (`motor_preco.calcular_primario`) forma o Preço Sugerido de cada cabeça da Tabela de Preços com o ICMS interno da Bahia;
  o **Preço Definido** (`PrecoPrimario`) é o sugerido até alguém alterá-lo, e vale para os cálculos seguintes.
- **Análise de Preço x Lucro**: da receita ao Resultado Final, unitário e total (unitário × quantidade prevista) — `analisar`.
- **Tabela de Preços**: o preço de cada produto por faixa de ICMS de destino, mantendo o mesmo resultado — `tabela_de_precos`.
- **DRE**: a mesma análise aberta pela classificação do Plano de Contas — `dre`.
"""

from collections import defaultdict
from dataclasses import dataclass, field
from decimal import Decimal

from sqlalchemy.orm import Session

from custos_precificacao import impostos, motor_preco, plano_padrao
from custos_precificacao.custeio import execucao_do_planejamento
from custos_precificacao.custo_produtos import NoEstrutura, montar_estrutura
from custos_precificacao.despesas import calcular_alocacao_despesa
from custos_precificacao.rateio import exigir_aberta
from custos_precificacao.explosao import volume_de_producao
from custos_precificacao.models import (
    CusteioProduto,
    Despesa,
    MetaLucro,
    PlanoDeContas,
    PrecoPrimario,
    Produto,
)
from custos_precificacao.motor_preco import PRIMARIO, ComposicaoPreco

ELEMENTOS_DA_META = ("socios", "reserva", "expansao", "outros")
ROTULO_ELEMENTO = {"socios": "Distribuição Sócios", "reserva": "Reserva", "expansao": "Expansão", "outros": "Outros"}
ROTULO_FAIXA = {"BA": "Bahia", "SUL_SUDESTE": "Sul/Sudeste (exc. ES)", "NORTE_NE_CO_ES": "N/NE/CO/ES"}


# --- Meta de Lucro ------------------------------------------------------------------------------------------------------------------

def obter_meta(session: Session) -> MetaLucro | None:
    return session.query(MetaLucro).first()


def definir_meta(session: Session, socios, reserva, expansao, outros) -> MetaLucro:
    """Grava a Meta de Lucro única (frações do preço de venda). ValueError se algum for negativo/maior que 100% ou se a soma não ficar abaixo de 100%."""
    exigir_aberta(session)
    valores = {}
    for nome, bruto in (("sócios", socios), ("reserva", reserva), ("expansão", expansao), ("outros", outros)):
        try:
            valor = Decimal(str(bruto if bruto not in (None, "") else 0).replace(",", "."))
        except Exception as exc:  # noqa: BLE001 - qualquer texto inválido vira erro de validação
            raise ValueError(f"{nome} precisa ser um número") from exc
        if not valor.is_finite() or valor < 0 or valor > 1:
            raise ValueError(f"{nome} deve ficar entre 0 e 100%")
        valores[nome] = valor
    soma = sum(valores.values(), Decimal(0))
    if soma >= 1:
        raise ValueError("O Lucro Pretendido (soma dos elementos) precisa ficar abaixo de 100%: o preço é o custo ÷ (1 − lucro)")
    meta = obter_meta(session) or MetaLucro()
    meta.percentual_socios, meta.percentual_reserva = valores["sócios"], valores["reserva"]
    meta.percentual_expansao, meta.percentual_outros = valores["expansão"], valores["outros"]
    meta.percentual_margem = soma
    session.add(meta)
    session.commit()
    return meta


def _destinos_da_meta(meta: MetaLucro) -> dict[str, Decimal]:
    return {
        "socios": meta.percentual_socios, "reserva": meta.percentual_reserva,
        "expansao": meta.percentual_expansao, "outros": meta.percentual_outros,
    }


# --- Preço Definido -----------------------------------------------------------------------------------------------------------------

def definir_preco_definido(session: Session, produto_codigo: str, valor: Decimal | None) -> None:
    """Grava o Preço Definido do produto; `None` volta a seguir o sugerido. Recusado com a primária travada."""
    exigir_aberta(session)
    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(PrecoPrimario).filter_by(produto_id=produto.id).one_or_none()
    if linha is None:
        linha = PrecoPrimario(produto_id=produto.id)
        session.add(linha)
    linha.preco_definido = valor if valor else None  # zero = sem preço definido
    session.commit()


def _definidos(session: Session) -> dict[int, Decimal]:
    return {p.produto_id: p.preco_definido for p in session.query(PrecoPrimario) if p.preco_definido}


# --- análise unitária ---------------------------------------------------------------------------------------------------------------

@dataclass
class Resultado:
    """Da receita ao resultado final, por unidade (ou total, multiplicado pela quantidade). IR/CSLL está dentro de `impostos`."""

    preco: Decimal
    impostos: Decimal  # ICMS + CBS a pagar (líquidos de créditos) + IR/CSLL
    custo: Decimal
    despesas_variaveis: Decimal
    margem_contribuicao: Decimal
    despesas_fixas: Decimal
    resultado_operacional: Decimal
    destinacao: Decimal
    resultado_final: Decimal
    icms: Decimal = Decimal(0)  # líquidos de créditos
    cbs: Decimal = Decimal(0)
    ir_csll: Decimal = Decimal(0)
    destinos: dict[str, Decimal] = field(default_factory=dict)

    def vezes(self, quantidade: Decimal) -> "Resultado":
        return Resultado(
            preco=self.preco * quantidade, impostos=self.impostos * quantidade, custo=self.custo * quantidade,
            despesas_variaveis=self.despesas_variaveis * quantidade, margem_contribuicao=self.margem_contribuicao * quantidade,
            despesas_fixas=self.despesas_fixas * quantidade, resultado_operacional=self.resultado_operacional * quantidade,
            destinacao=self.destinacao * quantidade, resultado_final=self.resultado_final * quantidade, icms=self.icms * quantidade,
            cbs=self.cbs * quantidade, ir_csll=self.ir_csll * quantidade, destinos={k: v * quantidade for k, v in self.destinos.items()},
        )

    def somar(self, outro: "Resultado") -> "Resultado":
        campos = ("preco", "impostos", "custo", "despesas_variaveis", "margem_contribuicao", "despesas_fixas", "resultado_operacional",
                  "destinacao", "resultado_final", "icms", "cbs", "ir_csll")
        soma = {c: getattr(self, c) + getattr(outro, c) for c in campos}
        destinos = {k: self.destinos.get(k, Decimal(0)) + outro.destinos.get(k, Decimal(0)) for k in {*self.destinos, *outro.destinos}}
        return Resultado(**soma, destinos=destinos)


def _zero() -> Resultado:
    return Resultado(*([Decimal(0)] * 9))


def analisar(c: ComposicaoPreco, preco: Decimal, meta: MetaLucro) -> Resultado:
    """A cascata unitária ao `preço`: (−) Impostos, (−) Custo do Produto, (−) Despesas Variáveis = Margem de Contribuição; (−) Despesas Fixas =
    Resultado Operacional; (−) Destinação (a meta) = Resultado Final. Os impostos são o ICMS e a CBS líquidos de créditos mais o IR/CSLL (t × o lucro
    antes do IR, 0 se houver prejuízo). Ao preço sugerido o Resultado Operacional é a meta e o Resultado Final, zero."""
    if c.modo == "contabil":
        icms = preco * c.pct_icms - c.creditos_icms - preco * c.pct_dv_credito_icms
        cbs = preco * c.pct_cbs - c.creditos_cbs - preco * c.pct_dv_credito_cbs
    else:
        icms = preco * (c.pct_icms - c.pct_creditos_icms)
        cbs = preco * (c.pct_cbs - c.pct_creditos_cbs)
        liquido = icms + cbs
        alvo = preco * c.pct_impostos
        escala = alvo / liquido if liquido > 0 else Decimal(0)
        icms, cbs = icms * escala, cbs * escala
    despesas_variaveis = preco * c.pct_dv_bruto
    despesas_fixas = c.despesa_fixa
    lucro_antes_do_ir = preco - icms - cbs - c.custo_producao - despesas_variaveis - despesas_fixas
    ir = c.aliquota_ir_csll * max(lucro_antes_do_ir, Decimal(0))
    impostos_total = icms + cbs + ir
    margem_contribuicao = preco - impostos_total - c.custo_producao - despesas_variaveis
    resultado_operacional = margem_contribuicao - despesas_fixas
    destinos = {k: preco * v for k, v in _destinos_da_meta(meta).items()}
    destinacao = sum(destinos.values(), Decimal(0))
    return Resultado(
        preco=preco, impostos=impostos_total, custo=c.custo_producao, despesas_variaveis=despesas_variaveis, margem_contribuicao=margem_contribuicao,
        despesas_fixas=despesas_fixas, resultado_operacional=resultado_operacional, destinacao=destinacao,
        resultado_final=resultado_operacional - destinacao, icms=icms, cbs=cbs, ir_csll=ir, destinos=destinos,
    )


# --- visão completa ----------------------------------------------------------------------------------------------------------------

@dataclass
class LinhaPrimaria:
    produto: Produto
    quantidade: Decimal  # produção prevista no mês
    composicao: ComposicaoPreco
    preco_sugerido: Decimal  # de tabela (num Centro com desconto: o líquido ÷ (1 − desconto))
    preco_definido: Decimal  # o efetivo, de tabela: o digitado ou, sem ele, o sugerido
    definido_manual: bool
    ao_sugerido: Resultado  # unitário ao preço sugerido (o que a lista de Precificação mostra; o faturamento é o líquido)
    ao_definido: Resultado  # unitário ao preço definido (o que a Análise e a DRE usam)
    desconto: Decimal = Decimal(0)  # desconto comercial do Centro (0 na primária)

    @property
    def spread(self) -> Decimal:
        """Margem líquida efetiva do Preço Definido menos a Meta, em pontos (0,02 = 2 p.p.); negativo = abaixo da meta."""
        r = self.ao_definido
        return (r.resultado_operacional / r.preco if r.preco else Decimal(0)) - self.composicao.pct_lucro


@dataclass
class Primaria:
    modo: str
    meta: MetaLucro
    linhas: list[LinhaPrimaria] = field(default_factory=list)
    avisos: list[str] = field(default_factory=list)
    aviso: str | None = None  # o que impede de calcular (sem custeio, sem meta, percentuais ≥ 100%…)
    resultado: motor_preco.ResultadoMotor | None = None

    def total_geral(self) -> Resultado:
        total = _zero()
        for l in self.linhas:
            total = total.somar(l.ao_definido.vezes(l.quantidade))
        return total


def calcular(session: Session, modo: str | None = None) -> Primaria:
    """Precificação primária completa. A Meta de Lucro precisa existir (ValueError); o resto vira `aviso` e as linhas ficam vazias."""
    meta = obter_meta(session)
    if meta is None or not meta.percentual_margem:
        raise ValueError("Defina a Meta de Lucro (aba Meta de Lucro) para calcular a precificação primária")
    modo = modo or motor_preco.obter_modo(session)
    primaria = Primaria(modo=modo, meta=meta)
    definidos = _definidos(session)
    try:
        motor = motor_preco.calcular_primario(session, meta.percentual_margem, modo, definidos)
    except ValueError as exc:
        primaria.aviso = str(exc)
        return primaria
    primaria.resultado, primaria.avisos = motor, list(motor.avisos)
    if PRIMARIO in motor.erros:
        primaria.aviso = motor.erros[PRIMARIO]
    produtos = {p.id: p for p in session.query(Produto)}
    for (produto_id, _), c in sorted(motor.composicoes.items(), key=lambda kv: produtos[kv[0][0]].codigo):
        produto = produtos[produto_id]
        definido = definidos.get(produto_id)
        preco = definido or c.preco
        primaria.linhas.append(LinhaPrimaria(
            produto=produto, quantidade=volume_de_producao(session, produto) or Decimal(0), composicao=c, preco_sugerido=c.preco,
            preco_definido=preco, definido_manual=bool(definido), ao_sugerido=analisar(c, c.preco, meta), ao_definido=analisar(c, preco, meta),
        ))
    return primaria


# --- Tabela de Preços: variação só do ICMS ------------------------------------------------------------------------------------------

@dataclass
class LinhaTabela:
    produto: Produto
    precos: dict[str, Decimal]  # faixa de ICMS -> preço


def tabela_de_precos(session: Session, primaria: Primaria) -> tuple[dict[str, Decimal], list[LinhaTabela]]:
    """Preço de cada produto por faixa de ICMS de destino, mantendo o mesmo resultado do preço definido (referência: dentro da Bahia).
    P_faixa = P × (1 − impostos − variáveis) ÷ (1 − impostos com o ICMS da faixa − variáveis). Devolve ({faixa: alíquota}, linhas)."""
    aliquotas = {faixa: impostos.aliquota_venda_da_faixa(session, faixa) for faixa in impostos.FAIXAS}
    linhas = []
    for l in primaria.linhas:
        c = l.composicao
        referencia = 1 - c.pct_impostos - c.pct_despesas_variaveis
        liquido = l.ao_definido.preco  # o faturamento do produto: o preço definido já sem o desconto do Centro
        precos = {}
        for faixa, aliquota in aliquotas.items():
            divisor = 1 - (c.pct_impostos - c.pct_icms + aliquota) - c.pct_despesas_variaveis
            precos[faixa] = liquido * referencia / divisor / (1 - l.desconto) if divisor > 0 else Decimal(0)
        linhas.append(LinhaTabela(produto=l.produto, precos=precos))
    return aliquotas, linhas


# --- DRE pela classificação do Plano de Contas ------------------------------------------------------------------------------------

@dataclass
class LinhaDre:
    rotulo: str
    valor: Decimal
    nivel: int = 0
    estilo: str = "conta"  # conta | grupo | total
    codigo: str | None = None
    # O valor (Total) se divide em Desembolso (caixa) e Retenções (provisão/encargos dos gastos dos elementos de custo); Desembolso + Retenções = Total.
    # Onde não há retenção (receita, impostos, insumos, despesas) tudo é Desembolso.
    retencao: Decimal | None = None
    desembolso: Decimal | None = None

    def __post_init__(self) -> None:
        if self.retencao is None:
            self.retencao = Decimal(0)
        if self.desembolso is None:
            self.desembolso = self.valor - self.retencao


def _decompor(no: NoEstrutura) -> dict[str, Decimal]:
    """O custo unitário do item por natureza: insumos (matéria-prima e embalagem) e cada elemento de custo, descendo pela estrutura."""
    total: dict[str, Decimal] = defaultdict(Decimal)
    if no.tipo == "revenda":
        total["insumos"] += no.custo.total if no.custo is not None else Decimal(0)
        return total
    if no.custo is not None:
        for e in no.custo.elementos:
            total[e.chave] += e.unitario + e.ociosidade
    for filho in no.filhos:
        if filho.ciclo:
            continue
        if filho.tipo in ("materia_prima", "embalagem"):
            total["insumos"] += filho.participacao or Decimal(0)
        elif filho.custo is not None:
            for chave, valor in _decompor(filho).items():
                total[chave] += filho.quantidade * valor
    return total


def _contas_dos_elementos(session: Session) -> dict[str, dict[int, tuple[Decimal, Decimal]]]:
    """Por elemento de custo, a fração do gasto mensal em cada conta do Plano de Contas, separada em (desembolso, retenção); as frações de todas as
    contas somam 1."""
    saida: dict[str, dict[int, tuple[Decimal, Decimal]]] = {}
    for chave, modelo, _origem, categoria in motor_preco._ELEMENTOS_GASTO:
        consulta = session.query(modelo)
        if categoria is not None:
            consulta = consulta.join(motor_preco.Maquina, modelo.maquina_id == motor_preco.Maquina.id).filter(motor_preco.Maquina.categoria == categoria)
        desembolso: dict[int, Decimal] = defaultdict(Decimal)
        retencao: dict[int, Decimal] = defaultdict(Decimal)
        for gasto in consulta:
            desembolso[gasto.plano_de_contas_id] += gasto.desembolso
            retencao[gasto.plano_de_contas_id] += gasto.retencao
        total = sum(desembolso.values(), Decimal(0)) + sum(retencao.values(), Decimal(0))
        saida[chave] = {pid: (desembolso[pid] / total, retencao[pid] / total) for pid in set(desembolso) | set(retencao)} if total else {}
    return saida


def _arvore(session: Session, valores: dict[int | None, Decimal], retencoes: dict[int | None, Decimal] | None = None) -> list[LinhaDre]:
    """As contas com valor (somando para cima nos sintéticos), em ordem de código e recuadas pela profundidade. A chave None = sem conta.
    `retencoes` = a parte de cada valor que é retenção (o resto é desembolso)."""
    contas = {c.id: c for c in session.query(PlanoDeContas)}
    acumulado: dict[int, Decimal] = defaultdict(Decimal)
    acumulado_retencao: dict[int, Decimal] = defaultdict(Decimal)
    for conta_id, valor in valores.items():
        if conta_id is None or not valor:
            continue
        retido = (retencoes or {}).get(conta_id, Decimal(0))
        atual = contas.get(conta_id)
        while atual is not None:
            acumulado[atual.id] += valor
            acumulado_retencao[atual.id] += retido
            atual = contas.get(atual.parent_id) if atual.parent_id else None

    def profundidade(conta: PlanoDeContas) -> int:
        n = 0
        while conta.parent_id:
            conta, n = contas[conta.parent_id], n + 1
        return n

    def ordem(conta: PlanoDeContas):
        return tuple((0, int(p)) if p.isdigit() else (1, p) for p in conta.codigo.split("."))

    saida = [
        LinhaDre(
            rotulo=contas[i].nome, valor=v, nivel=profundidade(contas[i]), estilo="conta" if contas[i].aceita_lancamento else "grupo", codigo=contas[i].codigo,
            retencao=acumulado_retencao[i],
        )
        for i, v in sorted(acumulado.items(), key=lambda kv: ordem(contas[kv[0]])) if v
    ]
    sem_conta = valores.get(None)
    if sem_conta:
        saida.append(LinhaDre(rotulo="Sem conta no Plano de Contas", valor=sem_conta, nivel=0, estilo="conta"))
    return saida


def _conta(session: Session, chave: str) -> int | None:
    conta = plano_padrao.conta_por_chave(session, chave)
    return conta.id if conta is not None else None


def dre(session: Session, primaria: Primaria) -> list[LinhaDre]:
    """A DRE primária pela classificação do Plano de Contas: Receita − Impostos − Custos − Despesas = Resultado Operacional − Destinação =
    Resultado Final, fechando com a Análise. Valores de todo o Planejamento (unitário × quantidade prevista)."""
    return dre_de_linhas(session, primaria.linhas, primaria.meta)


def dre_de_linhas(
    session: Session, linhas: list[LinhaPrimaria], meta: MetaLucro, adicionais: dict[int | None, Decimal] | None = None
) -> list[LinhaDre]:
    """A DRE por Plano de Contas de um conjunto de linhas (produto × quantidade ao Preço Definido; o mesmo produto pode aparecer em vários Centros de
    Resultados). `adicionais` = {plano_de_contas_id: valor} das despesas adicionais dos Centros (fixas rateadas e variáveis sobre a receita deles), que
    se somam às despesas do Planejamento. Fecha com a Análise de Preço x Lucro das mesmas linhas."""
    receita = sum((l.ao_definido.preco * l.quantidade for l in linhas), Decimal(0))
    total = _zero()
    for l in linhas:
        total = total.somar(l.ao_definido.vezes(l.quantidade))

    # impostos por conta: ICMS e CBS líquidos de créditos e IR/CSLL
    impostos_por_conta: dict[int | None, Decimal] = defaultdict(Decimal)
    for chave, valor in (("PC-IMP-ICMS", total.icms), ("PC-IMP-CBS", total.cbs), ("PC-IMP-IRCSLL", total.ir_csll)):
        impostos_por_conta[_conta(session, chave)] += valor

    # custos: insumos e elementos de custo, pelas contas dos gastos de cada elemento
    execucao = execucao_do_planejamento(session)
    contas_elementos = _contas_dos_elementos(session)
    natureza_do_produto: dict[int, dict[str, Decimal]] = {}
    custos: dict[int | None, Decimal] = defaultdict(Decimal)
    custos_retencao: dict[int | None, Decimal] = defaultdict(Decimal)  # a parte dos custos que é retenção (provisão dos gastos dos elementos)
    for l in linhas:
        if not l.quantidade:
            continue
        if l.produto.id not in natureza_do_produto:
            natureza = _decompor(montar_estrutura(session, l.produto, execucao))
            natureza["insumos"] += l.composicao.custo_producao - sum(natureza.values(), Decimal(0))  # o que a estrutura não explica fica em insumos
            natureza_do_produto[l.produto.id] = natureza
        for chave, valor in natureza_do_produto[l.produto.id].items():
            montante = valor * l.quantidade
            if chave == "insumos":
                custos[_conta(session, "PC-CUSTOS-INSUMOS")] += montante
            elif contas_elementos.get(chave):
                for conta_id, (fracao_desembolso, fracao_retencao) in contas_elementos[chave].items():
                    custos[conta_id] += montante * (fracao_desembolso + fracao_retencao)
                    custos_retencao[conta_id] += montante * fracao_retencao
            else:
                custos[None] += montante

    # despesas do Planejamento: fixas pelo que foi rateado às cabeças (por unidade × a quantidade de cada linha); variáveis = % da receita
    despesas: dict[int | None, Decimal] = defaultdict(Decimal)
    for d in session.query(Despesa).filter_by(tipo="fixa", cenario_id=None):
        por_unidade = {
            produto_id: valor / volume
            for produto_id, valor in calcular_alocacao_despesa(session, d).items()
            if (volume := volume_de_producao(session, session.get(Produto, produto_id)))
        }
        for l in linhas:
            despesas[d.plano_de_contas_id] += por_unidade.get(l.produto.id, Decimal(0)) * l.quantidade
    for d in session.query(Despesa).filter_by(tipo="variavel", cenario_id=None):
        despesas[d.plano_de_contas_id] += (d.percentual_receita or Decimal(0)) * receita
    for conta_id, valor in (adicionais or {}).items():
        despesas[conta_id] += valor

    saida = [LinhaDre("Receita Bruta", receita, estilo="total")]
    saida += _arvore(session, {_conta(session, "PC-RECEITA-VENDAS"): receita})
    for titulo, valores in (("(−) Impostos", impostos_por_conta), ("(−) Custos", custos), ("(−) Despesas", despesas)):
        retidas = custos_retencao if valores is custos else None
        soma = sum(valores.values(), Decimal(0))
        saida.append(LinhaDre(titulo, soma, estilo="total", retencao=sum(retidas.values(), Decimal(0)) if retidas else None))
        saida += _arvore(session, dict(valores), retidas)
    # As retenções são provisões dentro dos custos: o resultado "em desembolso" é o de antes delas e a coluna Retenções carrega o efeito (negativo).
    efeito_retencao = -sum(custos_retencao.values(), Decimal(0))
    saida.append(LinhaDre("(=) Resultado Operacional", total.resultado_operacional, estilo="total", retencao=efeito_retencao))
    saida.append(LinhaDre("(−) Destinação de Resultados", total.destinacao, estilo="total"))
    saida += [LinhaDre(ROTULO_ELEMENTO[k], total.destinos.get(k, Decimal(0)), nivel=1) for k in ELEMENTOS_DA_META if _destinos_da_meta(meta)[k]]
    saida.append(LinhaDre("(=) Resultado Final", total.resultado_final, estilo="total", retencao=efeito_retencao))
    # informativo: o que o custeio não conseguiu alocar a nenhum produto (não entra no preço)
    if execucao is not None and execucao.ociosidade_nao_alocada:
        saida.append(LinhaDre("Ociosidade não alocada (informativo, fora do preço)", execucao.ociosidade_nao_alocada, estilo="conta"))
    return saida
