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

from sqlalchemy import delete, event, or_
from sqlalchemy.orm import Session

from custos_precificacao import arvore, historico
from custos_precificacao.explosao import volume_de_producao
from custos_precificacao.models import (
    AliquotaImposto,
    Ambiente,
    AmbienteGasto,
    CenarioDestinoIcms,
    CenarioPrecificacao,
    ClassificacaoCredito,
    Despesa,
    Funcionario,
    FuncionarioGasto,
    IcmsCompra,
    IcmsDesenvolve,
    IcmsDestino,
    Insumo,
    Maquina,
    MaquinaGasto,
    PlanoDeContas,
    Produto,
    RoteiroInsumo,
)

GASTO_MODELS = (AmbienteGasto, MaquinaGasto, FuncionarioGasto)


CLASSES = ("icms", "cbs", "ir_csll")
ROTULO_CLASSE = {"icms": "ICMS", "cbs": "CBS", "ir_csll": "IR/CSLL"}
CHAVE_PADRAO = {"icms": "icms_credito", "cbs": "cbs", "ir_csll": "ir_csll"}  # classe -> `AliquotaImposto.classe` da alíquota padrão

# origem (ClassificacaoCredito.origem) -> modelo do item classificado
MODELO_DA_ORIGEM = {
    "insumo": Insumo,
    "ambiente_gasto": AmbienteGasto,
    "maquina_gasto": MaquinaGasto,
    "funcionario_gasto": FuncionarioGasto,
    "despesa": Despesa,
    "produto_revenda": Produto,
}
_ORIGEM_DO_MODELO = {modelo: origem for origem, modelo in MODELO_DA_ORIGEM.items()}


@event.listens_for(Session, "before_flush")
def _limpar_classificacoes_do_que_sai(session: Session, contexto, instancias) -> None:
    """Excluir um item apaga as classes de crédito dele (a referência é por origem + id, sem chave estrangeira)."""
    for objeto in list(session.deleted):
        origem = _ORIGEM_DO_MODELO.get(type(objeto))
        if origem is not None and getattr(objeto, "id", None) is not None:
            session.execute(delete(ClassificacaoCredito).where(ClassificacaoCredito.origem == origem, ClassificacaoCredito.origem_id == objeto.id))


def competencia_de_referencia(session: Session) -> date:
    """Mês das alíquotas padrão: a competência do custeio do Planejamento; sem custeio, o mês atual."""
    from custos_precificacao.custeio import execucao_do_planejamento

    execucao = execucao_do_planejamento(session)
    return execucao.competencia if execucao is not None else historico.mes_atual()


def aliquota_padrao(session: Session, classe: str, competencia: date) -> Decimal:
    """Alíquota padrão de uma classe de crédito na competência. CBS e IR/CSLL: a vigência mais recente que não passa dela (0 sem nenhuma).
    ICMS: a alíquota de crédito de uma compra dentro da Bahia (item sem origem informada)."""
    if classe == "icms":
        return aliquota_compra(session, "BA")
    linha = (
        session.query(AliquotaImposto)
        .filter(AliquotaImposto.classe == CHAVE_PADRAO[classe], AliquotaImposto.vigencia <= competencia)
        .order_by(AliquotaImposto.vigencia.desc())
        .first()
    )
    return linha.aliquota_pct if linha is not None else Decimal(0)


# --- classificação dos itens ----------------------------------------------------------------------------------------------------------

@dataclass
class ClasseMarcada:
    aliquota: Decimal | None = None  # alíquota própria do item (None = a padrão da classe)
    faixa: str | None = None  # ICMS: origem da compra (BA | SUL_SUDESTE | NORTE_NE_CO_ES); None = dentro da Bahia


def classes_do_item(session: Session, origem: str, origem_id: int) -> dict[str, ClasseMarcada]:
    """{classe: ClasseMarcada} do item."""
    return {
        c.classe: ClasseMarcada(c.aliquota_pct, c.faixa)
        for c in session.query(ClassificacaoCredito).filter_by(origem=origem, origem_id=origem_id).order_by(ClassificacaoCredito.classe)
    }


def _marcada(classe: str, valor) -> ClasseMarcada:
    """Normaliza o que vem de fora: None, uma alíquota (texto/Decimal), um ClasseMarcada ou {aliquota_pct, faixa}."""
    if isinstance(valor, ClasseMarcada):
        aliquota, faixa = valor.aliquota, valor.faixa
    elif isinstance(valor, dict):
        aliquota, faixa = valor.get("aliquota_pct"), valor.get("faixa")
    else:
        aliquota, faixa = valor, None
    if aliquota is not None and str(aliquota).strip() != "":
        numero = Decimal(str(aliquota).strip().replace(",", "."))
        if not numero.is_finite() or numero < 0 or numero >= 1:
            raise ValueError("A alíquota própria fica entre 0 e 1 (ex.: 0,12 = 12%)")
        aliquota = numero
    else:
        aliquota = None
    faixa = (faixa or None) if isinstance(faixa, str) else None
    if faixa is not None:
        if classe != "icms":
            raise ValueError("A origem da compra só vale para o ICMS")
        if faixa not in FAIXAS:
            raise ValueError(f"Origem {faixa!r} inválida. Opções: {', '.join(FAIXAS)}")
        if faixa == "BA":
            faixa = None  # dentro da Bahia é o padrão
    return ClasseMarcada(aliquota, faixa)


def definir_classes_do_item(session: Session, origem: str, origem_id: int, classes: dict) -> None:
    """Substitui as classes de crédito do item (vazio = o item deixa de gerar crédito). O valor de cada classe é None, uma alíquota própria,
    um ClasseMarcada ou {"aliquota_pct", "faixa"}. Não faz commit."""
    if origem not in MODELO_DA_ORIGEM:
        raise ValueError(f"Origem {origem!r} inválida. Opções: {', '.join(MODELO_DA_ORIGEM)}")
    if session.get(MODELO_DA_ORIGEM[origem], origem_id) is None:
        raise ValueError(f"Item {origem_id} não encontrado em {origem!r}")
    novas: dict[str, ClasseMarcada] = {}
    for classe, valor in classes.items():
        if classe not in CLASSES:
            raise ValueError(f"Classe {classe!r} inválida. Opções: {', '.join(CLASSES)}")
        novas[classe] = _marcada(classe, valor)
    existentes = {c.classe: c for c in session.query(ClassificacaoCredito).filter_by(origem=origem, origem_id=origem_id)}
    for classe, linha in existentes.items():
        if classe not in novas:
            session.delete(linha)
    for classe, marcada in novas.items():
        if classe in existentes:
            existentes[classe].aliquota_pct = marcada.aliquota
            existentes[classe].faixa = marcada.faixa
        else:
            session.add(ClassificacaoCredito(origem=origem, origem_id=origem_id, classe=classe, aliquota_pct=marcada.aliquota, faixa=marcada.faixa))
    session.flush()


def marcar_lote(session: Session, itens: list[tuple[str, int]], classe: str, marcar: bool) -> int:
    """Liga (ou desliga) uma classe de crédito em vários itens de uma vez, sem mexer nas outras classes nem nas alíquotas/origens próprias. Devolve quantos itens mudaram."""
    if classe not in CLASSES:
        raise ValueError(f"Classe {classe!r} inválida. Opções: {', '.join(CLASSES)}")
    mudou = 0
    for origem, origem_id in itens:
        atuais = classes_do_item(session, origem, origem_id)
        if marcar and classe not in atuais:
            atuais[classe] = ClasseMarcada()
        elif not marcar and classe in atuais:
            del atuais[classe]
        else:
            continue
        definir_classes_do_item(session, origem, origem_id, atuais)
        mudou += 1
    return mudou


@dataclass
class ItemClassificavel:
    secao: str  # insumos | mao_de_obra | instalacoes | maquinas | despesas | revenda
    origem: str
    origem_id: int
    elemento_codigo: str  # o insumo/colaborador/instalação/máquina/centro de custo/produto a que o item pertence
    elemento_nome: str
    descricao: str  # o que é a linha (conta do gasto, tipo do insumo, mês da despesa…)
    base: Decimal | None  # valor de referência (desembolso do gasto, custo de compra, valor da despesa)
    classes: dict[str, ClasseMarcada] = field(default_factory=dict)  # classes marcadas (com a alíquota própria e/ou a origem da compra)


SECOES = (
    ("insumos", "Insumos e embalagens"),
    ("mao_de_obra", "Mão de obra"),
    ("instalacoes", "Instalações"),
    ("maquinas", "Máquinas e outros"),
    ("despesas", "Despesas"),
    ("revenda", "Revenda"),
)


def listar_itens_classificaveis(session: Session) -> list[ItemClassificavel]:
    """Todos os itens que podem gerar crédito, com as classes marcadas: insumos e embalagens, os gastos de mão de obra, instalações e
    máquinas/outros, os lançamentos de despesa e os produtos de revenda."""
    marcadas: dict[tuple[str, int], dict[str, ClasseMarcada]] = {}
    for c in session.query(ClassificacaoCredito).order_by(ClassificacaoCredito.classe):
        marcadas.setdefault((c.origem, c.origem_id), {})[c.classe] = ClasseMarcada(c.aliquota_pct, c.faixa)
    planos = {pl.id: pl.nome for pl in session.query(PlanoDeContas)}

    def item(secao, origem, origem_id, cod, nome, descricao, base) -> ItemClassificavel:
        return ItemClassificavel(secao, origem, origem_id, cod, nome, descricao, base, dict(marcadas.get((origem, origem_id), {})))

    saida: list[ItemClassificavel] = []
    for i in session.query(Insumo).order_by(Insumo.codigo):
        saida.append(item("insumos", "insumo", i.id, i.codigo, i.nome, "Embalagem" if i.tipo == "embalagem" else "Insumo", i.valor_compra_unitario))
    gastos = (
        ("mao_de_obra", "funcionario_gasto", FuncionarioGasto, Funcionario, "funcionario_id", "codigo_fopag", "Mão de obra"),
        ("instalacoes", "ambiente_gasto", AmbienteGasto, Ambiente, "ambiente_id", "codigo", "Instalação"),
        ("maquinas", "maquina_gasto", MaquinaGasto, Maquina, "maquina_id", "codigo", "Máquina"),
    )
    for secao, origem, modelo, recurso_modelo, fk, chave, rotulo in gastos:
        recursos = {r.id: r for r in session.query(recurso_modelo)}
        for g in session.query(modelo).order_by(modelo.id):
            r = recursos.get(getattr(g, fk))
            if r is None:
                continue
            rot = "Outros" if origem == "maquina_gasto" and getattr(r, "categoria", "maquina") == "outro" else rotulo
            saida.append(item(secao, origem, g.id, getattr(r, chave), r.nome, f"{rot} · {planos.get(g.plano_de_contas_id, '')}", g.desembolso))
    for d in session.query(Despesa).order_by(Despesa.codigo):
        rotulo = f"{planos.get(d.plano_de_contas_id, '')} · " + (f"variável {d.percentual_receita * 100:.2f}% da receita" if d.tipo == "variavel" else "fixa")
        saida.append(item("despesas", "despesa", d.id, d.codigo, d.titulo, rotulo, None if d.tipo == "variavel" else d.valor))
    for p in session.query(Produto).filter(Produto.tipo == "revenda").order_by(Produto.codigo):
        saida.append(item("revenda", "produto_revenda", p.id, p.codigo, p.nome, "Produto de revenda", p.custo_compra))
    return saida


# --- crédito -------------------------------------------------------------------------------------------------------------------------

@dataclass
class LinhaCredito:
    """Uma linha de crédito de Imposto: de onde vem (`origem`), sobre qual base e quanto gerou."""

    origem: str  # 'funcionario' | 'maquina' | 'ambiente' | 'insumo' | 'despesa' | 'revenda'
    origem_codigo: str
    origem_nome: str
    detalhe: str  # Plano de Contas (Gasto) ou o texto de como a base foi montada
    base: Decimal
    credito: Decimal
    classe: str  # icms | cbs | ir_csll
    aliquota: Decimal


_ORIGEM_GASTO = (
    (AmbienteGasto, "ambiente", "ambiente_gasto", Ambiente, "ambiente_id", "codigo"),
    (MaquinaGasto, "maquina", "maquina_gasto", Maquina, "maquina_id", "codigo"),
    (FuncionarioGasto, "funcionario", "funcionario_gasto", Funcionario, "funcionario_id", "codigo_fopag"),
)


class TaxasDeCredito:
    """Alíquota de cada classe de crédito de um item: a própria dele; no ICMS, a da origem da compra (Bahia, Sul/Sudeste ou demais); senão a padrão
    da classe na vigência do custeio (CBS e IR/CSLL). Lê a classificação uma vez só (serve a todos os itens)."""

    def __init__(self, session: Session):
        self.session = session
        self.competencia = competencia_de_referencia(session)
        self._padroes: dict[str, Decimal] = {}
        self.marcadas: dict[tuple[str, int], list[ClassificacaoCredito]] = {}
        for c in session.query(ClassificacaoCredito).order_by(ClassificacaoCredito.id):
            self.marcadas.setdefault((c.origem, c.origem_id), []).append(c)

    def padrao(self, classe: str) -> Decimal:
        if classe not in self._padroes:
            self._padroes[classe] = aliquota_padrao(self.session, classe, self.competencia)
        return self._padroes[classe]

    def taxa(self, c: ClassificacaoCredito) -> Decimal:
        if c.aliquota_pct is not None:
            return c.aliquota_pct
        if c.classe == "icms" and c.faixa:
            chave = f"icms:{c.faixa}"
            if chave not in self._padroes:
                self._padroes[chave] = aliquota_compra(self.session, c.faixa)
            return self._padroes[chave]
        return self.padrao(c.classe)

    def do_item(self, origem: str, item_id: int) -> list[tuple[str, Decimal]]:
        """[(classe, alíquota)] das classes marcadas no item (vazio = não gera crédito)."""
        return [(c.classe, self.taxa(c)) for c in self.marcadas.get((origem, item_id), [])]

    def ids_marcados(self, origem: str) -> list[int]:
        return [i for (o, i) in self.marcadas if o == origem]


def detalhar_credito(
    session: Session, receita_bruta: Decimal | None = None, receitas_por_centro: dict[int, Decimal] | None = None
) -> list[LinhaCredito]:
    """Linhas de crédito da empresa inteira, sem recorte por Cenário (o mesmo recurso serve todos os canais; o débito é que é por Cenário).
    O crédito de cada item é a base × a alíquota de cada classe marcada: a própria do item; no ICMS, a da origem da compra (Bahia, Sul/Sudeste ou
    demais); senão a padrão da classe na vigência (CBS e IR/CSLL):

    - Gastos (Ambiente/Máquina/Funcionário): base = desembolso.
    - Insumo, por linha de RoteiroInsumo: base = valor_compra_unitario × quantidade_kg × volume de produção do produto que consome
      (Insumo não tem "quantidade comprada no mês"; produtos sem volume ficam de fora, sem erro).
    - Despesa: fixa = o valor mensal; variável = o % × a receita bruta do Planejamento (`receita_bruta`; sem ela a base é 0). Sem `receitas_por_centro`
      só entram as despesas do Planejamento; com ele ({cenario_id: receita}) entram também as adicionais dos Centros, a variável sobre a receita do Centro.
    - Revenda: base = custo de compra × volume de produção. Item sem classe marcada não gera crédito."""
    taxas = TaxasDeCredito(session)
    classes_de, ids_marcados = taxas.do_item, taxas.ids_marcados

    linhas: list[LinhaCredito] = []
    planos = {p.id: p for p in session.query(PlanoDeContas).all()}

    for model, origem, origem_cls, recurso_model, fk, chave in _ORIGEM_GASTO:
        for gasto in session.query(model).filter(model.id.in_(ids_marcados(origem_cls))).order_by(model.id):
            recurso = session.get(recurso_model, getattr(gasto, fk))
            plano = planos.get(gasto.plano_de_contas_id)
            for classe, aliquota in classes_de(origem_cls, gasto.id):
                linhas.append(LinhaCredito(
                    origem=origem, origem_codigo=getattr(recurso, chave), origem_nome=recurso.nome, detalhe=plano.nome if plano else "",
                    base=gasto.desembolso, credito=gasto.desembolso * aliquota, classe=classe, aliquota=aliquota,
                ))

    consumos = (
        session.query(RoteiroInsumo)
        .join(Insumo, RoteiroInsumo.insumo_id == Insumo.id)
        .filter(Insumo.id.in_(ids_marcados("insumo")))
        .order_by(RoteiroInsumo.id)
    )
    for linha in consumos:
        produto = linha.produto
        volume = volume_de_producao(session, produto)
        if not volume:
            continue
        insumo = linha.insumo
        base = insumo.valor_compra_unitario * linha.quantidade_kg * volume
        for classe, aliquota in classes_de("insumo", insumo.id):
            linhas.append(LinhaCredito(
                origem="insumo", origem_codigo=insumo.codigo, origem_nome=insumo.nome, detalhe=f"Consumo em {produto.nome}",
                base=base, credito=base * aliquota, classe=classe, aliquota=aliquota,
            ))

    ids_despesa = ids_marcados("despesa")
    if ids_despesa:
        consulta = session.query(Despesa).filter(Despesa.id.in_(ids_despesa))
        if receitas_por_centro is None:
            consulta = consulta.filter(Despesa.cenario_id.is_(None))
        for despesa in consulta.order_by(Despesa.id):
            plano = planos.get(despesa.plano_de_contas_id)
            if despesa.tipo == "variavel":
                receita_da_despesa = (receitas_por_centro or {}).get(despesa.cenario_id, Decimal(0)) if despesa.cenario_id else (receita_bruta or Decimal(0))
                base = (despesa.percentual_receita or Decimal(0)) * receita_da_despesa
                detalhe = f"{plano.nome if plano else ''} · {(despesa.percentual_receita or Decimal(0)) * 100:.2f}% da receita bruta"
            else:
                base = despesa.valor
                detalhe = f"{plano.nome if plano else ''} · fixa"
            for classe, aliquota in classes_de("despesa", despesa.id):
                linhas.append(LinhaCredito(
                    origem="despesa", origem_codigo=despesa.codigo, origem_nome=despesa.titulo, detalhe=detalhe,
                    base=base, credito=base * aliquota, classe=classe, aliquota=aliquota,
                ))

    for produto in session.query(Produto).filter(Produto.id.in_(ids_marcados("produto_revenda")), Produto.tipo == "revenda").order_by(Produto.id):
        volume = volume_de_producao(session, produto)
        if not volume or not produto.custo_compra:
            continue
        base = produto.custo_compra * volume
        for classe, aliquota in classes_de("produto_revenda", produto.id):
            linhas.append(LinhaCredito(
                origem="revenda", origem_codigo=produto.codigo, origem_nome=produto.nome, detalhe="Custo de compra × volume",
                base=base, credito=base * aliquota, classe=classe, aliquota=aliquota,
            ))
    return linhas


def calcular_credito_total(session: Session) -> Decimal:
    """Crédito de Imposto da empresa inteira — soma de `detalhar_credito` (ver as regras lá)."""
    return sum((linha.credito for linha in detalhar_credito(session)), Decimal(0))


# --- alíquotas por vigência ----------------------------------------------------------------------------------------------------------

def _fracao(valor, rotulo: str, *, maximo: Decimal = Decimal(1)) -> Decimal:
    try:
        numero = Decimal(str(valor).strip().replace(",", "."))
    except Exception as exc:
        raise ValueError(f"{rotulo} precisa ser um número") from exc
    if not numero.is_finite() or numero < 0 or numero > maximo:
        raise ValueError(f"{rotulo} deve ficar entre 0 e {maximo} (ex.: 0,12 = 12%)")
    return numero


def listar_aliquotas(session: Session) -> dict[str, list[AliquotaImposto]]:
    saida = {"cbs": [], "ir_csll": [], "icms_credito": []}
    for a in session.query(AliquotaImposto).order_by(AliquotaImposto.vigencia):
        saida[a.classe].append(a)
    return saida


def definir_aliquota(session: Session, classe: str, vigencia: date | str, aliquota, nota: str | None = None) -> AliquotaImposto:
    """Cria ou atualiza (upsert) a alíquota de uma classe (cbs | ir_csll | icms_credito) a partir do mês de vigência. Não faz commit."""
    if classe not in ("cbs", "ir_csll", "icms_credito"):
        raise ValueError("Classe deve ser cbs, ir_csll ou icms_credito")
    mes = historico.normalizar_mes(vigencia)
    valor = _fracao(aliquota, "A alíquota")
    linha = session.query(AliquotaImposto).filter_by(classe=classe, vigencia=mes).one_or_none()
    if linha is None:
        linha = AliquotaImposto(classe=classe, vigencia=mes)
        session.add(linha)
    linha.aliquota_pct = valor
    linha.nota = (nota or "").strip()[:300] or None
    session.flush()
    return linha


def remover_aliquota(session: Session, classe: str, vigencia: date | str) -> None:
    mes = historico.normalizar_mes(vigencia)
    linha = session.query(AliquotaImposto).filter_by(classe=classe, vigencia=mes).one_or_none()
    if linha is None:
        raise ValueError(f"Não há alíquota de {classe} a partir de {mes:%m/%Y}")
    session.delete(linha)
    session.flush()


# --- ICMS: destinos (UF), mix de destinos por Centro de Resultado ------------------------------------------------------------------------

UFS_DESTINO = [
    ("BA", "Bahia", "BA"),
    ("SP", "São Paulo", "SUL_SUDESTE"), ("RJ", "Rio de Janeiro", "SUL_SUDESTE"), ("MG", "Minas Gerais", "SUL_SUDESTE"),
    ("PR", "Paraná", "SUL_SUDESTE"), ("SC", "Santa Catarina", "SUL_SUDESTE"), ("RS", "Rio Grande do Sul", "SUL_SUDESTE"),
    ("ES", "Espírito Santo", "NORTE_NE_CO_ES"),
    ("AL", "Alagoas", "NORTE_NE_CO_ES"), ("CE", "Ceará", "NORTE_NE_CO_ES"), ("MA", "Maranhão", "NORTE_NE_CO_ES"),
    ("PB", "Paraíba", "NORTE_NE_CO_ES"), ("PE", "Pernambuco", "NORTE_NE_CO_ES"), ("PI", "Piauí", "NORTE_NE_CO_ES"),
    ("RN", "Rio Grande do Norte", "NORTE_NE_CO_ES"), ("SE", "Sergipe", "NORTE_NE_CO_ES"),
    ("AC", "Acre", "NORTE_NE_CO_ES"), ("AP", "Amapá", "NORTE_NE_CO_ES"), ("AM", "Amazonas", "NORTE_NE_CO_ES"),
    ("PA", "Pará", "NORTE_NE_CO_ES"), ("RO", "Rondônia", "NORTE_NE_CO_ES"), ("RR", "Roraima", "NORTE_NE_CO_ES"), ("TO", "Tocantins", "NORTE_NE_CO_ES"),
    ("DF", "Distrito Federal", "NORTE_NE_CO_ES"), ("GO", "Goiás", "NORTE_NE_CO_ES"), ("MT", "Mato Grosso", "NORTE_NE_CO_ES"),
    ("MS", "Mato Grosso do Sul", "NORTE_NE_CO_ES"),
]
FAIXAS = {"BA": "Dentro da Bahia (alíquota interna)", "SUL_SUDESTE": "Sul e Sudeste, exceto ES (interestadual 7%)", "NORTE_NE_CO_ES": "Norte, Nordeste, Centro-Oeste e ES (interestadual 12%)"}
ALIQUOTA_INICIAL_DA_FAIXA = {"BA": Decimal("0.205"), "SUL_SUDESTE": Decimal("0.07"), "NORTE_NE_CO_ES": Decimal("0.12")}


def garantir_destinos(session: Session) -> None:
    """Semeia as 27 UFs (a migration já faz; isto cobre bancos criados sem ela, como os dos testes). Não faz commit."""
    if session.query(IcmsDestino.id).first() is not None:
        return
    for uf, nome, faixa in UFS_DESTINO:
        session.add(IcmsDestino(uf=uf, nome=nome, faixa=faixa, aliquota_pct=ALIQUOTA_INICIAL_DA_FAIXA[faixa]))
    session.flush()


def listar_destinos(session: Session) -> list[IcmsDestino]:
    garantir_destinos(session)
    ordem = {uf: i for i, (uf, _, _) in enumerate(UFS_DESTINO)}
    return sorted(session.query(IcmsDestino).all(), key=lambda d: ordem.get(d.uf, 99))


def atualizar_destino(session: Session, uf: str, aliquota=None, reducao_base=None, frete_adicional=None) -> IcmsDestino:
    """Ajusta a alíquota, a redução de base e (reservado) o frete adicional de uma UF. Campo None = não mexe. Não faz commit."""
    garantir_destinos(session)
    destino = session.query(IcmsDestino).filter_by(uf=uf.upper()).one_or_none()
    if destino is None:
        raise ValueError(f"UF {uf!r} não encontrada")
    if aliquota is not None:
        destino.aliquota_pct = _fracao(aliquota, "A alíquota")
    if reducao_base is not None:
        destino.reducao_base_pct = _fracao(reducao_base, "A redução de base")
    if frete_adicional is not None:
        destino.frete_adicional_pct = _fracao(frete_adicional, "O frete adicional")
    session.flush()
    return destino


def atualizar_faixa(session: Session, faixa: str, aliquota) -> int:
    """Muda a alíquota de todas as UFs de uma faixa (BA | SUL_SUDESTE | NORTE_NE_CO_ES). Devolve quantas UFs mudaram. Não faz commit."""
    if faixa not in FAIXAS:
        raise ValueError(f"Faixa {faixa!r} inválida. Opções: {', '.join(FAIXAS)}")
    garantir_destinos(session)
    valor = _fracao(aliquota, "A alíquota")
    destinos = session.query(IcmsDestino).filter_by(faixa=faixa).all()
    for d in destinos:
        d.aliquota_pct = valor
    session.flush()
    return len(destinos)


def garantir_compras(session: Session) -> None:
    """Semeia as 3 origens de compra (a migration já faz; isto cobre bancos criados sem ela). Não faz commit."""
    if session.query(IcmsCompra.id).first() is not None:
        return
    for faixa, aliquota in ALIQUOTA_INICIAL_DA_FAIXA.items():
        session.add(IcmsCompra(faixa=faixa, aliquota_pct=aliquota))
    session.flush()


def listar_compras(session: Session) -> list[IcmsCompra]:
    garantir_compras(session)
    ordem = {faixa: i for i, faixa in enumerate(FAIXAS)}
    return sorted(session.query(IcmsCompra).all(), key=lambda c: ordem.get(c.faixa, 99))


def aliquota_compra(session: Session, faixa: str) -> Decimal:
    """Alíquota do crédito de ICMS de uma compra feita de fornecedor da faixa (BA = dentro da Bahia, interna)."""
    garantir_compras(session)
    linha = session.query(IcmsCompra).filter_by(faixa=faixa).one_or_none()
    return linha.aliquota_pct if linha is not None else Decimal(0)


def atualizar_compra(session: Session, faixa: str, aliquota) -> IcmsCompra:
    """Muda a alíquota do crédito de ICMS das compras de uma origem. Não faz commit."""
    if faixa not in FAIXAS:
        raise ValueError(f"Origem {faixa!r} inválida. Opções: {', '.join(FAIXAS)}")
    garantir_compras(session)
    linha = session.query(IcmsCompra).filter_by(faixa=faixa).one()
    linha.aliquota_pct = _fracao(aliquota, "A alíquota")
    session.flush()
    return linha


def obter_mix(session: Session, cenario_id: int) -> dict[str, Decimal]:
    """{UF: fração da receita} do Centro de Resultado (vazio = destino ainda não definido)."""
    return {
        m.uf: m.percentual
        for m in session.query(CenarioDestinoIcms).filter_by(cenario_id=cenario_id).order_by(CenarioDestinoIcms.uf)
        if m.percentual
    }


def definir_mix(session: Session, cenario_id: int, mix: dict[str, Decimal | str]) -> None:
    """Define o mix de destinos do Centro (fração da receita por UF). Vazio limpa; senão a soma tem de fechar 100%. Não faz commit."""
    if session.get(CenarioPrecificacao, cenario_id) is None:
        raise ValueError("Centro de Resultado não encontrado")
    garantir_destinos(session)
    ufs = {d.uf for d in session.query(IcmsDestino)}
    novas: dict[str, Decimal] = {}
    for uf, percentual in mix.items():
        sigla = uf.upper()
        if sigla not in ufs:
            raise ValueError(f"UF {uf!r} não encontrada")
        valor = _fracao(percentual, f"O percentual de {sigla}")
        if valor:
            novas[sigla] = valor
    if novas and abs(sum(novas.values()) - 1) > Decimal("0.0001"):
        soma = (sum(novas.values()) * 100).quantize(Decimal("0.01"))
        raise ValueError(f"A soma dos destinos deve fechar 100% (está em {soma}%)")
    existentes = {m.uf: m for m in session.query(CenarioDestinoIcms).filter_by(cenario_id=cenario_id)}
    for uf, linha in existentes.items():
        if uf not in novas:
            session.delete(linha)
    for uf, valor in novas.items():
        if uf in existentes:
            existentes[uf].percentual = valor
        else:
            session.add(CenarioDestinoIcms(cenario_id=cenario_id, uf=uf, percentual=valor))
    session.flush()


def aliquota_icms_do_centro(session: Session, cenario_id: int) -> Decimal:
    """Alíquota efetiva de ICMS da venda do Centro: Σ (fração da receita da UF × alíquota da UF × (1 − redução de base)). Sem mix, 100% Bahia."""
    mix = obter_mix(session, cenario_id) or {"BA": Decimal(1)}  # sem destino definido: tudo dentro da Bahia
    destinos = {d.uf: d for d in listar_destinos(session)}
    return sum((frac * destinos[uf].aliquota_pct * (1 - destinos[uf].reducao_base_pct) for uf, frac in mix.items()), Decimal(0))


def aliquota_venda_da_faixa(session: Session, faixa: str) -> Decimal:
    """Alíquota efetiva de ICMS da venda para uma faixa de destino (a da primeira UF da faixa × (1 − redução de base))."""
    for destino in listar_destinos(session):
        if destino.faixa == faixa:
            return destino.aliquota_pct * (1 - destino.reducao_base_pct)
    raise ValueError(f"Faixa {faixa!r} inválida. Opções: {', '.join(FAIXAS)}")


def aliquota_icms_primaria(session: Session) -> Decimal:
    """ICMS de referência da precificação primária: a venda dentro da Bahia (alíquota interna)."""
    return aliquota_venda_da_faixa(session, "BA")


# --- Desenvolve-BA -----------------------------------------------------------------------------------------------------------------

@dataclass
class ResultadoDesenvolve:
    """Efeito do Desenvolve-BA sobre o saldo devedor mensal de ICMS."""

    saldo_devedor: Decimal  # débito − crédito de ICMS no mês (0 quando há saldo credor)
    fundo_pobreza: Decimal  # parte do saldo do adicional do Fundo de Pobreza: fora do benefício, paga integralmente
    parcela_dilatada: Decimal  # parcela incentivada: (saldo − Fundo de Pobreza) × % ICMS dilatado
    parcela_normal: Decimal  # o resto do saldo, paga no prazo normal
    pago_a_vista: Decimal  # parte da parcela dilatada efetivamente desembolsada à vista
    beneficio: Decimal  # parcela dilatada − pago à vista: o que o programa poupa por mês (reserva de lucro)
    icms_a_pagar: Decimal  # Fundo de Pobreza + parcela normal + pago à vista
    debito_icms: Decimal = Decimal(0)
    credito_icms: Decimal = Decimal(0)


def calcular_desenvolve(saldo_devedor: Decimal, parametros) -> ResultadoDesenvolve:
    """Aplica o Desenvolve-BA a um saldo devedor mensal de ICMS (débito − crédito):

    - Fundo de Pobreza = saldo × % de Fundo de Pobreza: fica de fora do benefício e é pago integralmente;
    - parcela dilatada (incentivada) = (saldo − Fundo de Pobreza) × % ICMS dilatado; o resto é a parcela normal;
    - pago à vista = parcela dilatada × % da dilatação paga à vista (a dilação é quitada na hora);
    - benefício = parcela dilatada − pago à vista. É **reserva de lucro** (incentivo fiscal): não se distribui aos sócios e não é desconto
      para reduzir preço;
    - ICMS a pagar = Fundo de Pobreza + parcela normal + pago à vista."""
    saldo = max(Decimal(0), saldo_devedor)
    fundo = saldo * parametros.pct_fundo_pobreza
    base = saldo - fundo
    dilatada = base * parametros.pct_dilatado
    normal = base - dilatada
    a_vista = dilatada * parametros.pct_pago_a_vista
    return ResultadoDesenvolve(
        saldo_devedor=saldo, fundo_pobreza=fundo, parcela_dilatada=dilatada, parcela_normal=normal, pago_a_vista=a_vista,
        beneficio=dilatada - a_vista, icms_a_pagar=fundo + normal + a_vista,
    )


def obter_desenvolve(session: Session) -> IcmsDesenvolve:
    """Os parâmetros do Desenvolve (um registro, sem vigência: se mudar, muda-se no próximo Planejamento). Se ainda não existe, nasce desligado,
    com valores de EXEMPLO a trocar pelos da resolução de enquadramento da empresa. Não faz commit."""
    linha = session.query(IcmsDesenvolve).first()
    if linha is None:
        linha = IcmsDesenvolve(
            ativo=False, pct_dilatado=Decimal("0.90"), pct_pago_a_vista=Decimal("0.20"), pct_fundo_pobreza=Decimal(0),
            observacao="Valores de EXEMPLO: troque pelos da resolução de enquadramento da empresa no Desenvolve-BA.",
        )
        session.add(linha)
        session.flush()
    return linha


def definir_desenvolve(session: Session, **campos) -> IcmsDesenvolve:
    """Atualiza os parâmetros do Desenvolve (só os campos informados). Não faz commit."""
    linha = obter_desenvolve(session)
    for chave, rotulo in (("pct_dilatado", "O % de ICMS dilatado"), ("pct_pago_a_vista", "O % pago à vista"), ("pct_fundo_pobreza", "O % de Fundo de Pobreza")):
        if campos.get(chave) is not None:
            setattr(linha, chave, _fracao(campos[chave], rotulo))
    if campos.get("ativo") is not None:
        linha.ativo = bool(campos["ativo"])
    if "plano_de_contas_codigo" in campos:
        codigo = (campos["plano_de_contas_codigo"] or "").strip()
        if not codigo:
            linha.plano_de_contas_id = None
        else:
            conta = session.query(PlanoDeContas).filter_by(codigo=codigo).one_or_none()
            if conta is None:
                raise ValueError(f"Conta {codigo!r} não encontrada no Plano de Contas")
            arvore.exigir_lancavel(conta)
            linha.plano_de_contas_id = conta.id
    if "observacao" in campos:
        linha.observacao = (campos["observacao"] or "").strip()[:500] or None
    session.flush()
    return linha


def conta_da_reserva(session: Session) -> str | None:
    """"CÓDIGO - Nome" da conta do Plano de Contas que recebe a diferença do Desenvolve (None se não foi escolhida)."""
    linha = session.query(IcmsDesenvolve).first()
    conta = linha.plano_de_contas if linha is not None else None
    return f"{conta.codigo} - {conta.nome}" if conta is not None else None


def apurar_desenvolve(session: Session, debito_icms: Decimal, credito_icms: Decimal) -> ResultadoDesenvolve | None:
    """O benefício do mês: None se o Desenvolve está desligado."""
    parametros = session.query(IcmsDesenvolve).first()
    if parametros is None or not parametros.ativo:
        return None
    resultado = calcular_desenvolve(debito_icms - credito_icms, parametros)
    resultado.debito_icms, resultado.credito_icms = debito_icms, credito_icms
    return resultado
