"""Unidades de medida, conversões e perda das formulações.

Cada Insumo e cada Produto tem uma **unidade base** (`unidade`: kg, l, un, cx…) — a unidade do seu custo unitário (insumo) ou em que o
produto é consumido como insumo de outro (produto). Uma linha de formulação pode informar a quantidade em outra unidade:
g/kg e ml/l convertem sozinhos; as demais (cx, mc, pc, densidade…) vêm de `ConversaoUnidade` (cadastro por item:
"1 `unidade` = `fator` unidades-base").

Perda: um produto tem `base_quantidade` (a bacia), `perda_pct` e rendimento real = base × (1 − perda); dois deles
determinam o terceiro (`reconciliar_rendimento`). Uma linha de insumo com `perda_pct` consome quantidade ÷ (1 − perda).
"""

from decimal import Decimal

from sqlalchemy.orm import Session

from custos_precificacao.models import ConversaoUnidade, Insumo, Produto

UM = Decimal(1)

# conversões que valem para qualquer item (massa e volume)
_UNIVERSAIS: dict[tuple[str, str], Decimal] = {
    ("g", "kg"): Decimal("0.001"), ("kg", "g"): Decimal(1000),
    ("mg", "kg"): Decimal("0.000001"), ("kg", "mg"): Decimal(1000000),
    ("ml", "l"): Decimal("0.001"), ("l", "ml"): Decimal(1000),
}


class ErroDeUnidade(ValueError):
    """Conversão impossível (falta cadastrar o fator) ou valor inválido."""


def normalizar(unidade: str | None, padrao: str = "kg") -> str:
    texto = (unidade or "").strip().lower()
    return texto or padrao


def fator_de_perda(perda_pct: Decimal | None) -> Decimal:
    """Multiplicador de consumo: 1 ÷ (1 − perda). Perda de 0 = 1; perda ≥ 100% é inválida."""
    perda = Decimal(perda_pct or 0)
    if perda < 0 or perda >= 1:
        raise ErroDeUnidade(f"A perda deve ficar entre 0 e 100% (recebi {perda * 100:.2f}%)")
    return UM / (UM - perda)


def _conversoes(session: Session, item: Insumo | Produto) -> dict[str, Decimal]:
    coluna = ConversaoUnidade.insumo_id if isinstance(item, Insumo) else ConversaoUnidade.produto_id
    return {normalizar(c.unidade): Decimal(c.fator) for c in session.query(ConversaoUnidade).filter(coluna == item.id)}


def _para_base(session: Session, item: Insumo | Produto, unidade: str) -> Decimal:
    """Quantas unidades-base do item cabem em 1 `unidade`."""
    base = normalizar(item.unidade)
    if unidade == base:
        return UM
    if (unidade, base) in _UNIVERSAIS:
        return _UNIVERSAIS[(unidade, base)]
    cadastradas = _conversoes(session, item)
    if unidade in cadastradas:
        return cadastradas[unidade]
    raise ErroDeUnidade(
        f"Não sei converter {unidade!r} para {base!r} em {item.codigo}: cadastre a conversão (1 {unidade} = ? {base}) em Unidades e conversões"
    )


def converter(session: Session, item: Insumo | Produto, quantidade: Decimal, de: str | None, para: str | None) -> Decimal:
    """`quantidade` em `de` → `para`, para o item informado (por padrão as duas em kg)."""
    de_n, para_n = normalizar(de), normalizar(para)
    if de_n == para_n:
        return Decimal(quantidade)
    return Decimal(quantidade) * _para_base(session, item, de_n) / _para_base(session, item, para_n)


def reconciliar_rendimento(
    base: Decimal | None, perda_pct: Decimal | None, rendimento: Decimal | None
) -> tuple[Decimal | None, Decimal | None, Decimal | None]:
    """Dados dois entre (base, perda, rendimento real), calcula o terceiro. Prioridade se vierem os três: base e perda."""
    if base is not None and perda_pct is not None:
        if not (0 <= perda_pct < 1):
            raise ErroDeUnidade("A perda deve ficar entre 0 e 100%")
        return base, perda_pct, base * (UM - perda_pct)
    if base is not None and rendimento is not None:
        if base <= 0 or rendimento < 0 or rendimento > base:
            raise ErroDeUnidade("O rendimento real não pode ser maior que a base (bacia) nem negativo")
        return base, UM - rendimento / base, rendimento
    if perda_pct is not None and rendimento is not None:
        if not (0 <= perda_pct < 1):
            raise ErroDeUnidade("A perda deve ficar entre 0 e 100%")
        return rendimento / (UM - perda_pct), perda_pct, rendimento
    return base, perda_pct, rendimento
