"""Motor de precificação: o preço sugerido cobre custo, impostos, despesas variáveis, despesas fixas rateadas, quantidade produzida e lucro.

Por Produto (cabeça da Tabela de Preços) × Centro de Resultado:

    base  = custo de produção unitário (o "Total" da tela Custo dos Produtos: insumos + elementos + ociosidade)
            + despesa fixa rateada por unidade produzida
            − créditos de ICMS e CBS por unidade (só no modo contábil)
    preço = base ÷ (1 − %impostos − %despesas variáveis − %lucro ÷ (1 − alíquota de IR/CSLL))

Todos os % incidem sobre o preço de venda ("por dentro", como o débito da DRE). O lucro pretendido é **líquido**: o preço também cobre o IR/CSLL
sobre o lucro (a alíquota da vigência; sem alíquota cadastrada, 0). Os impostos têm duas formas, escolhidas por Planejamento (`configuracao_preco`):

- **contábil**: os créditos (ICMS e CBS) abatem o custo das aquisições — insumos, elementos de custo e despesas — e os débitos entram com valor
  cheio: %impostos = ICMS do Centro (média pelo destino da venda; sem destino = 100% Bahia) + CBS;
- **financeira**: o motor estima o % da receita que os impostos a pagar representam: débito (ICMS do Centro + CBS) menos os créditos do mês
  (todos os itens classificados, inclusive as despesas variáveis) ÷ a receita do Planejamento. Como a receita depende do preço, itera até convergir.

O benefício do Desenvolve-BA **não** entra: é reserva de lucro, não reduz o preço (o % usa o ICMS a pagar sem o benefício).
"""

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

from sqlalchemy.orm import Session

from custos_precificacao import impostos, rateio
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.explosao import volume_de_producao
from custos_precificacao.models import (
    AmbienteGasto,
    CenarioPrecificacao,
    ConfiguracaoPreco,
    CusteioProduto,
    Despesa,
    FuncionarioGasto,
    Insumo,
    Maquina,
    MaquinaGasto,
    Produto,
)

MODOS = ("contabil", "financeira")
CLASSES_DO_PRECO = ("icms", "cbs")  # entram como débito e crédito no preço; o IR/CSLL entra pelo lucro líquido
MAX_ITERACOES = 60
TOLERANCIA = Decimal("0.0000001")


# --- configuração -------------------------------------------------------------------------------------------------------------------

def obter_modo(session: Session) -> str:
    """Modo dos impostos do Planejamento (contábil se nunca foi escolhido)."""
    linha = session.query(ConfiguracaoPreco).first()
    return linha.modo_impostos if linha is not None else "contabil"


def definir_modo(session: Session, modo: str) -> str:
    if modo not in MODOS:
        raise ValueError(f"Modo {modo!r} inválido. Opções: {', '.join(MODOS)}")
    rateio.exigir_aberta(session)  # o modo dos impostos mexe no preço: só com a primária aberta
    linha = session.query(ConfiguracaoPreco).first()
    if linha is None:
        linha = ConfiguracaoPreco()
        session.add(linha)
    linha.modo_impostos = modo
    session.commit()
    return modo


# --- composição do preço ------------------------------------------------------------------------------------------------------------

@dataclass
class ComposicaoPreco:
    """Como o preço sugerido de um produto num Centro de Resultado é formado (valores unitários; % como fração do preço de venda)."""

    produto_id: int
    cenario_id: int
    modo: str
    custo_producao: Decimal  # custo primário unitário (Custo dos Produtos)
    despesa_fixa: Decimal  # despesas fixas rateadas, por unidade produzida
    creditos_icms: Decimal  # créditos que abatem o custo (modo contábil; 0 na financeira)
    creditos_cbs: Decimal
    pct_icms: Decimal  # ICMS do Centro (média pelo destino da venda)
    pct_cbs: Decimal
    pct_creditos_icms: Decimal  # financeira: % da receita que os créditos de ICMS do mês representam (abate o débito)
    pct_creditos_cbs: Decimal
    pct_despesas_variaveis: Decimal  # as variáveis (no modo contábil, líquidas do crédito delas)
    pct_lucro: Decimal  # lucro líquido pretendido (Regra de Margem do Centro ou Meta de Lucro)
    aliquota_ir_csll: Decimal
    preco: Decimal = Decimal(0)
    pct_dv_bruto: Decimal = Decimal(0)  # despesas variáveis sem descontar o crédito delas
    pct_dv_credito_icms: Decimal = Decimal(0)  # contábil: parte das variáveis que volta como crédito, por classe (% do preço)
    pct_dv_credito_cbs: Decimal = Decimal(0)

    @property
    def creditos(self) -> Decimal:
        return self.creditos_icms + self.creditos_cbs

    @property
    def pct_creditos(self) -> Decimal:
        """Financeira: % da receita que os créditos (ICMS + CBS) do mês representam."""
        return self.pct_creditos_icms + self.pct_creditos_cbs

    @property
    def base(self) -> Decimal:
        return max(self.custo_producao + self.despesa_fixa - self.creditos, Decimal(0))

    @property
    def pct_impostos(self) -> Decimal:
        """% da receita que vai em ICMS + CBS: o débito cheio (contábil) ou o a pagar estimado, já sem os créditos (financeira)."""
        bruto = self.pct_icms + self.pct_cbs
        return max(bruto - self.pct_creditos, Decimal(0)) if self.modo == "financeira" else bruto

    @property
    def pct_lucro_antes_do_ir(self) -> Decimal:
        """Lucro antes do IR/CSLL, em % do preço: o líquido pretendido ÷ (1 − alíquota)."""
        return self.pct_lucro / (1 - self.aliquota_ir_csll)

    @property
    def divisor(self) -> Decimal:
        return 1 - self.pct_impostos - self.pct_despesas_variaveis - self.pct_lucro_antes_do_ir

    def margem_liquida(self, preco: Decimal) -> Decimal | None:
        """Margem líquida de um preço qualquer, no mesmo sentido do lucro pretendido: (1 − impostos − variáveis − base ÷ preço) × (1 − IR/CSLL).
        Igual ao lucro pretendido quando o preço é o sugerido. None sem preço."""
        if not preco:
            return None
        antes_do_ir = 1 - self.pct_impostos - self.pct_despesas_variaveis - self.base / preco
        return antes_do_ir * (1 - self.aliquota_ir_csll)


@dataclass
class ResultadoMotor:
    modo: str
    composicoes: dict[tuple[int, int], ComposicaoPreco] = field(default_factory=dict)  # (produto_id, cenario_id)
    avisos: list[str] = field(default_factory=list)
    erros: dict[int, str] = field(default_factory=dict)  # cenario_id -> por que o Centro não tem preço (ex.: percentuais somam 100% ou mais)
    avisos_centro: dict[int, str] = field(default_factory=dict)  # cenario_id -> por que os acréscimos do Centro ficaram de fora (ex.: despesa fixa sem quantidade)
    iteracoes: int | None = None  # financeira: passos até convergir


# --- créditos por unidade ----------------------------------------------------------------------------------------------------------

# (chave do elemento no custeio, modelo do gasto, origem da classificação, categoria da máquina)
_ELEMENTOS_GASTO = (
    ("mao_de_obra", FuncionarioGasto, "funcionario_gasto", None),
    ("ambiente", AmbienteGasto, "ambiente_gasto", None),
    ("maquina", MaquinaGasto, "maquina_gasto", "maquina"),
    ("outros", MaquinaGasto, "maquina_gasto", "outro"),
)


def _taxas_dos_elementos(session: Session, taxas: impostos.TaxasDeCredito) -> dict[str, dict[str, Decimal]]:
    """Por elemento de custo, a fração do custo mensal (desembolso + retenção) que volta como crédito de cada classe."""
    saida: dict[str, dict[str, Decimal]] = {}
    for chave, modelo, origem, categoria in _ELEMENTOS_GASTO:
        consulta = session.query(modelo)
        if categoria is not None:
            consulta = consulta.join(Maquina, modelo.maquina_id == Maquina.id).filter(Maquina.categoria == categoria)
        total = Decimal(0)
        credito = {classe: Decimal(0) for classe in CLASSES_DO_PRECO}
        for gasto in consulta:
            total += gasto.total
            for classe, aliquota in taxas.do_item(origem, gasto.id):
                if classe in credito:
                    credito[classe] += gasto.desembolso * aliquota
        saida[chave] = {classe: (valor / total if total else Decimal(0)) for classe, valor in credito.items()}
    return saida


def _credito_do_no(session: Session, no: NoEstrutura, taxas: impostos.TaxasDeCredito, elementos: dict[str, dict[str, Decimal]]) -> dict[str, Decimal]:
    """Crédito (ICMS e CBS) por unidade do item de um nó da estrutura: o dos elementos de custo dele (custo absorvido + ociosidade × taxa do
    elemento), o das matérias-primas e embalagens (consumo × preço de compra × alíquota) e, recursivamente, o dos intermediários que consome."""
    total = {classe: Decimal(0) for classe in CLASSES_DO_PRECO}
    if no.tipo == "revenda" and no.produto_id is not None:
        produto = session.get(Produto, no.produto_id)
        for classe, aliquota in taxas.do_item("produto_revenda", produto.id):
            if classe in total:
                total[classe] += (produto.custo_compra or Decimal(0)) * aliquota
        return total
    if no.custo is not None:
        for e in no.custo.elementos:
            for classe in total:
                total[classe] += (e.unitario + e.ociosidade) * elementos.get(e.chave, {}).get(classe, Decimal(0))
    for filho in no.filhos:
        if filho.ciclo:
            continue
        if filho.tipo in ("materia_prima", "embalagem"):
            insumo = session.get(Insumo, filho.insumo_id)
            base = filho.quantidade * insumo.valor_compra_unitario
            for classe, aliquota in taxas.do_item("insumo", insumo.id):
                if classe in total:
                    # o "ICMS (%)" do cadastro do insumo já abate o custo dele: só o que passa dele é crédito novo
                    ja_abatido = insumo.icms_pct if classe == "icms" else Decimal(0)
                    total[classe] += base * max(aliquota - ja_abatido, Decimal(0))
        elif filho.custo is not None:
            sub = _credito_do_no(session, filho, taxas, elementos)
            for classe in total:
                total[classe] += filho.quantidade * sub[classe]
    return total


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

def _despesas_fixas(session: Session, taxas: impostos.TaxasDeCredito) -> tuple[dict[int, Decimal], dict[int, dict[str, Decimal]]]:
    """Despesas fixas rateadas por unidade produzida de cada produto e o crédito (ICMS e CBS) que elas geram por unidade."""
    unitaria: dict[int, Decimal] = {}
    credito: dict[int, dict[str, Decimal]] = {}
    for despesa in session.query(Despesa).filter_by(tipo="fixa", cenario_id=None).order_by(Despesa.id):
        classes = [(c, a) for c, a in taxas.do_item("despesa", despesa.id) if c in CLASSES_DO_PRECO]
        for produto_id, valor in calcular_alocacao_despesa(session, despesa).items():
            volume = volume_de_producao(session, session.get(Produto, produto_id))
            if not volume:
                continue
            por_un = valor / volume
            unitaria[produto_id] = unitaria.get(produto_id, Decimal(0)) + por_un
            acumulado = credito.setdefault(produto_id, {c: Decimal(0) for c in CLASSES_DO_PRECO})
            for classe, aliquota in classes:
                acumulado[classe] += por_un * aliquota
    return unitaria, credito


def _despesas_variaveis(session: Session, taxas: impostos.TaxasDeCredito) -> tuple[Decimal, Decimal, Decimal, Decimal]:
    """% sobre a receita das despesas variáveis: (bruto, líquido do crédito, crédito de ICMS, crédito de CBS)."""
    bruto = Decimal(0)
    credito = {classe: Decimal(0) for classe in CLASSES_DO_PRECO}
    for despesa in session.query(Despesa).filter_by(tipo="variavel", cenario_id=None):
        pct = despesa.percentual_receita or Decimal(0)
        bruto += pct
        for classe, aliquota in taxas.do_item("despesa", despesa.id):
            if classe in credito:
                credito[classe] += pct * aliquota
    liquido = max(bruto - sum(credito.values(), Decimal(0)), Decimal(0))
    return bruto, liquido, credito["icms"], credito["cbs"]


@dataclass
class Perfil:
    """Um "cenário de preço": quem tem ICMS, meta de lucro e volumes próprios. Cada Centro de Resultado é um; a precificação primária é um só."""

    chave: int  # cenario_id (ou PRIMARIO)
    nome: str
    pct_icms: Decimal
    pct_lucro: Decimal
    volumes: dict[int, Decimal]  # produto_id -> quantidade vendida no mês (receita da modalidade financeira)
    desconto: Decimal = Decimal(0)
    # acréscimos do Centro de Resultados (despesas adicionais), somados à primária:
    despesa_fixa: dict[int, Decimal] = field(default_factory=dict)  # produto_id -> despesa fixa adicional por unidade (rateada entre as quantidades do Centro)
    creditos_despesa: dict[int, dict[str, Decimal]] = field(default_factory=dict)  # produto_id -> crédito (ICMS/CBS) por unidade dessas despesas
    dv_bruto: Decimal = Decimal(0)  # despesas variáveis adicionais, % do preço (bruto, líquido do crédito e o crédito por classe)
    dv_liquido: Decimal = Decimal(0)
    dv_cred_icms: Decimal = Decimal(0)
    dv_cred_cbs: Decimal = Decimal(0)
    aviso: str | None = None  # por que os acréscimos ficaram de fora


PRIMARIO = 0  # chave do perfil da precificação primária (nenhum Centro tem id 0)


def _calcular(session: Session, perfis: list[Perfil], modo: str, definidos=None) -> ResultadoMotor:
    """Compõe o preço sugerido de cada cabeça custeada em cada perfil. `definidos` — {(produto_id, chave): preço líquido} ou uma função
    `(produto_id, chave, preço sugerido líquido) -> preço líquido` — vale no lugar do sugerido quando se calcula a receita da modalidade
    financeira (o preço definido alimenta os cálculos seguintes)."""
    if modo not in MODOS:
        raise ValueError(f"Modo {modo!r} inválido. Opções: {', '.join(MODOS)}")
    execucao = execucao_do_planejamento(session)
    if execucao is None:
        raise ValueError("O Planejamento não tem nenhuma execução de custeio — calcule os custos (Custo dos Produtos) antes de sugerir preço")

    custos = {r.produto_id: r.custo_producao_unitario for r in session.query(CusteioProduto).filter_by(execucao_id=execucao.id)}
    cabecas = [p for p in session.query(Produto).filter(Produto.tabela_de_precos.is_(True)).order_by(Produto.id) if p.id in custos]

    taxas = impostos.TaxasDeCredito(session)
    competencia = taxas.competencia
    aliquota_ir = impostos.aliquota_padrao(session, "ir_csll", competencia)
    pct_cbs = impostos.aliquota_padrao(session, "cbs", competencia)
    if aliquota_ir >= 1:
        raise ValueError("A alíquota de IR/CSLL precisa ser menor que 100%")
    despesa_fixa, credito_despesa = _despesas_fixas(session, taxas)
    dv_bruto, dv_liquido, dv_cred_icms, dv_cred_cbs = _despesas_variaveis(session, taxas)
    pct_variaveis = dv_liquido if modo == "contabil" else dv_bruto  # na financeira o crédito delas vai no pool de créditos

    creditos_unitarios: dict[int, dict[str, Decimal]] = {}
    if modo == "contabil":
        elementos = _taxas_dos_elementos(session, taxas)
        for produto in cabecas:
            soma = _credito_do_no(session, montar_estrutura(session, produto, execucao), taxas, elementos)
            for classe, valor in credito_despesa.get(produto.id, {}).items():
                soma[classe] += valor
            creditos_unitarios[produto.id] = soma

    resultado = ResultadoMotor(modo=modo)
    composicoes = resultado.composicoes
    resultado.avisos_centro = {p.chave: p.aviso for p in perfis if p.aviso}

    def compor(pct_cred_icms: Decimal, pct_cred_cbs: Decimal) -> None:
        for perfil in perfis:
            for produto in cabecas:
                credito = creditos_unitarios.get(produto.id, {})
                credito_extra = perfil.creditos_despesa.get(produto.id, {}) if modo == "contabil" else {}
                c = ComposicaoPreco(
                    produto_id=produto.id, cenario_id=perfil.chave, modo=modo, custo_producao=custos[produto.id],
                    despesa_fixa=despesa_fixa.get(produto.id, Decimal(0)) + perfil.despesa_fixa.get(produto.id, Decimal(0)),
                    creditos_icms=credito.get("icms", Decimal(0)) + credito_extra.get("icms", Decimal(0)),
                    creditos_cbs=credito.get("cbs", Decimal(0)) + credito_extra.get("cbs", Decimal(0)),
                    pct_icms=perfil.pct_icms, pct_cbs=pct_cbs,
                    pct_creditos_icms=pct_cred_icms if modo == "financeira" else Decimal(0),
                    pct_creditos_cbs=pct_cred_cbs if modo == "financeira" else Decimal(0),
                    pct_despesas_variaveis=pct_variaveis + (perfil.dv_liquido if modo == "contabil" else perfil.dv_bruto),
                    pct_lucro=perfil.pct_lucro, aliquota_ir_csll=aliquota_ir,
                    pct_dv_bruto=dv_bruto + perfil.dv_bruto,
                    pct_dv_credito_icms=(dv_cred_icms + perfil.dv_cred_icms) if modo == "contabil" else Decimal(0),
                    pct_dv_credito_cbs=(dv_cred_cbs + perfil.dv_cred_cbs) if modo == "contabil" else Decimal(0),
                )
                if c.divisor <= 0:
                    resultado.erros[perfil.chave] = (
                        f"No Centro de Resultado {perfil.nome!r}, impostos ({c.pct_impostos:.2%}), despesas variáveis ({c.pct_despesas_variaveis:.2%}) "
                        f"e lucro pretendido ({c.pct_lucro_antes_do_ir:.2%}, antes do IR/CSLL) somam {1 - c.divisor:.2%} do preço — precisa ser menos de 100%"
                    )
                    composicoes.pop((produto.id, perfil.chave), None)
                    continue
                resultado.erros.pop(perfil.chave, None)
                c.preco = c.base / c.divisor
                composicoes[(produto.id, perfil.chave)] = c

    if modo == "contabil":
        compor(Decimal(0), Decimal(0))
        return resultado

    # financeira: o % dos créditos sobre a receita depende do preço, que depende do %; itera até convergir
    pct_icms_cred = pct_cbs_cred = Decimal(0)
    for passo in range(1, MAX_ITERACOES + 1):
        compor(pct_icms_cred, pct_cbs_cred)
        resultado.iteracoes = passo
        receita = Decimal(0)
        receitas: dict[int, Decimal] = {}
        for (produto_id, chave), c in composicoes.items():
            perfil = next(p for p in perfis if p.chave == chave)
            if callable(definidos):
                preco = definidos(produto_id, chave, c.preco) or c.preco
            else:
                preco = (definidos or {}).get((produto_id, chave)) or c.preco
            parcela = preco * perfil.volumes.get(produto_id, Decimal(0))  # o preço já é líquido do desconto do Centro
            receita += parcela
            receitas[chave] = receitas.get(chave, Decimal(0)) + parcela
        if receita <= 0:
            resultado.avisos.append(
                "Sem volume (ou sem preço) não há receita para estimar o % de impostos a pagar: os créditos foram considerados 0."
            )
            break
        pool = {classe: Decimal(0) for classe in CLASSES_DO_PRECO}
        com_centros = any(p.chave != PRIMARIO for p in perfis)  # com Centros, o pool inclui as despesas adicionais deles
        for linha in impostos.detalhar_credito(session, receita, receitas if com_centros else None):
            if linha.classe in pool:
                pool[linha.classe] += linha.credito
        novo_icms, novo_cbs = pool["icms"] / receita, pool["cbs"] / receita
        if abs(novo_icms - pct_icms_cred) + abs(novo_cbs - pct_cbs_cred) < TOLERANCIA:
            break
        pct_icms_cred, pct_cbs_cred = novo_icms, novo_cbs
    else:
        resultado.avisos.append(f"O % de impostos a pagar não convergiu em {MAX_ITERACOES} passos; usado o último valor.")
    return resultado


def calcular_primario(
    session: Session, pct_lucro: Decimal, modo: str | None = None, definidos: dict[int, Decimal] | None = None
) -> ResultadoMotor:
    """Precificação primária: um perfil só, sem Centros de Resultado — ICMS interno da Bahia, a Meta de Lucro única (`pct_lucro`),
    volume = a produção prevista e sem desconto. `definidos` = {produto_id: preço definido} (alimenta a receita da modalidade financeira).
    As composições vêm com a chave `(produto_id, PRIMARIO)`."""
    modo = modo or obter_modo(session)
    produtos = {p.id: p for p in session.query(Produto)}
    perfil = Perfil(
        chave=PRIMARIO, nome="Primária", pct_icms=impostos.aliquota_icms_primaria(session), pct_lucro=pct_lucro,
        volumes={pid: (volume_de_producao(session, p) or Decimal(0)) for pid, p in produtos.items() if p.tabela_de_precos},
    )
    return _calcular(session, [perfil], modo, {(pid, PRIMARIO): v for pid, v in (definidos or {}).items()})
