from decimal import Decimal, InvalidOperation

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import func
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from custos_precificacao import auth, cadastro, historico
from custos_precificacao.api.deps import get_session
from custos_precificacao.api.schemas import HistoricoInsumoOut, InsumoIn, InsumoOut, VersaoInsumoOut
from custos_precificacao.models import Insumo, RoteiroInsumo

router = APIRouter(tags=["insumos"])

TIPOS = ("ingrediente", "embalagem")
CAMPOS_VALOR = ("valor_compra_unitario", "frete")
CAMPOS_PCT = ("ipi_pct", "icms_pct")


def _mes(data) -> str:
    return f"{data.year:04d}-{data.month:02d}"


def _numero(texto: str | None, rotulo: str, *, maximo: Decimal | None = None) -> Decimal:
    try:
        valor = Decimal((texto or "").strip().replace(",", "."))
    except InvalidOperation as exc:
        raise HTTPException(status_code=422, detail=f"{rotulo} precisa ser um número") from exc
    if not valor.is_finite() or valor < 0:
        raise HTTPException(status_code=422, detail=f"{rotulo} não pode ser negativo")
    if maximo is not None and valor > maximo:
        raise HTTPException(status_code=422, detail=f"{rotulo} deve ficar entre 0 e {maximo} (ex.: 0,18 = 18%)")
    return valor


def _obter(session: Session, codigo: str) -> Insumo:
    insumo = session.query(Insumo).filter_by(codigo=codigo).one_or_none()
    if insumo is None:
        raise HTTPException(status_code=404, detail=f"Insumo {codigo!r} não encontrado")
    return insumo


def _unidade(texto: str | None) -> str | None:
    unidade = (texto or "").strip().lower()
    if len(unidade) > 10:
        raise HTTPException(status_code=422, detail="unidade aceita até 10 caracteres")
    return unidade or None


def _saida(session: Session, i: Insumo) -> InsumoOut:
    usos = session.query(func.count(RoteiroInsumo.id)).filter_by(insumo_id=i.id).scalar()
    return InsumoOut(
        codigo=i.codigo, nome=i.nome, tipo=i.tipo, unidade=(i.unidade or "kg").strip().lower(),
        valor_compra_unitario=i.valor_compra_unitario, frete=i.frete, ipi_pct=i.ipi_pct, icms_pct=i.icms_pct,
        custo_final=i.custo_final,
        atualizado_em=_mes(i.atualizado_em), linhas_de_roteiro=usos, origem=i.origem or "manual",
        digitado_por=auth.NomesDeAutores(session)(i.digitado_por), digitado_em=i.digitado_em,
    )


def _valores_versionados(corpo: InsumoIn, *, criando: bool) -> dict[str, Decimal]:
    """Só os campos de preço enviados (na criação, os que faltam valem 0)."""
    valores: dict[str, Decimal] = {}
    for campo in (*CAMPOS_VALOR, *CAMPOS_PCT):
        bruto = getattr(corpo, campo)
        if bruto is None or bruto.strip() == "":
            if criando:
                valores[campo] = Decimal(0)
            continue
        valores[campo] = _numero(bruto, campo, maximo=Decimal(1) if campo in CAMPOS_PCT else None)
    return valores


@router.get("/insumos", response_model=list[InsumoOut])
def listar_insumos(session: Session = Depends(get_session)) -> list[InsumoOut]:
    return [_saida(session, i) for i in session.query(Insumo).order_by(Insumo.codigo)]


@router.post("/insumos", response_model=InsumoOut, status_code=201)
def criar_insumo(corpo: InsumoIn, session: Session = Depends(get_session)) -> InsumoOut:
    codigo = (corpo.codigo or "").strip()
    nome = (corpo.nome or "").strip()
    if not codigo or not nome:
        raise HTTPException(status_code=422, detail="codigo e nome são obrigatórios")
    if len(codigo) > 20 or len(nome) > 200:
        raise HTTPException(status_code=422, detail="codigo aceita até 20 e nome até 200 caracteres")
    if corpo.tipo not in TIPOS:
        raise HTTPException(status_code=422, detail=f"tipo deve ser um de: {', '.join(TIPOS)}")
    valores = _valores_versionados(corpo, criando=True)
    try:
        mes = historico.normalizar_mes(corpo.atualizado_em)
    except historico.ErroDeHistorico as exc:
        raise HTTPException(status_code=422, detail=str(exc)) from exc
    unidade = _unidade(corpo.unidade) or "kg"
    insumo = Insumo(codigo=codigo, nome=nome, tipo=corpo.tipo, unidade=unidade, atualizado_em=mes, **valores)
    session.add(insumo)
    try:
        session.commit()
    except IntegrityError as exc:
        session.rollback()
        raise HTTPException(status_code=409, detail=f"Já existe um Insumo com código {codigo!r}") from exc
    return _saida(session, insumo)


@router.patch("/insumos/{codigo}", response_model=InsumoOut)
def atualizar_insumo(codigo: str, corpo: InsumoIn, session: Session = Depends(get_session)) -> InsumoOut:
    """Nome e tipo mudam direto; preço, frete, IPI e ICMS mudam com histórico (versão antiga guardada)."""
    insumo = _obter(session, codigo)
    enviados = corpo.model_fields_set
    if "codigo" in enviados and (corpo.codigo or "").strip() not in ("", codigo):
        raise HTTPException(status_code=422, detail="O código não pode ser alterado")
    novos: dict = {}
    if "nome" in enviados:
        nome = (corpo.nome or "").strip()
        if not nome or len(nome) > 200:
            raise HTTPException(status_code=422, detail="nome é obrigatório (até 200 caracteres)")
        novos["nome"] = nome
    if "tipo" in enviados and corpo.tipo is not None:
        if corpo.tipo not in TIPOS:
            raise HTTPException(status_code=422, detail=f"tipo deve ser um de: {', '.join(TIPOS)}")
        novos["tipo"] = corpo.tipo
    if "unidade" in enviados and _unidade(corpo.unidade):
        novos["unidade"] = _unidade(corpo.unidade)
    if insumo.origem == "totvs":  # cadastro do TOTVS: identificação travada (custo, frete e impostos seguem editáveis)
        travados = [c for c in ("nome", "tipo", "unidade") if c in novos and novos[c] != getattr(insumo, c)]
        if travados:
            raise HTTPException(
                status_code=409,
                detail=f"Insumo {codigo!r} veio do TOTVS: {', '.join(travados)} só mudam no TOTVS (reimporte a estrutura)",
            )
    novos.update(_valores_versionados(corpo, criando=False))
    try:
        historico.atualizar_com_historico(session, insumo, novos, corpo.atualizado_em)
    except historico.ErroDeHistorico as exc:
        session.rollback()
        raise HTTPException(status_code=422, detail=str(exc)) from exc
    return _saida(session, insumo)


@router.delete("/insumos/{codigo}", status_code=204)
def excluir_insumo(codigo: str, session: Session = Depends(get_session)) -> None:
    insumo = _obter(session, codigo)
    usos = session.query(func.count(RoteiroInsumo.id)).filter_by(insumo_id=insumo.id).scalar()
    if usos:
        raise HTTPException(
            status_code=409,
            detail=f"Insumo {codigo!r} é consumido em {usos} linha(s) de roteiro — remova-as antes de excluir",
        )
    try:
        cadastro.deletar(session, cadastro.resolver_entidade("insumo"), codigo)
    except IntegrityError as exc:
        session.rollback()
        raise HTTPException(status_code=409, detail=f"Insumo {codigo!r} está em uso e não pode ser excluído") from exc


@router.get("/insumos/{codigo}/historico", response_model=HistoricoInsumoOut)
def historico_do_insumo(codigo: str, session: Session = Depends(get_session)) -> HistoricoInsumoOut:
    insumo = _obter(session, codigo)
    versoes = []
    nome = auth.NomesDeAutores(session)
    for ver in historico.versoes_com_autoria(session, insumo):
        v = ver.valores
        preco = v["valor_compra_unitario"]
        versoes.append(VersaoInsumoOut(
            atualizado_em=_mes(ver.inicio), substituido_em=_mes(ver.fim) if ver.fim else None,
            autor=nome(ver.autor), digitado_em=ver.digitado_em, substituido_por=nome(ver.substituido_por),
            valor_compra_unitario=preco, frete=v["frete"], ipi_pct=v["ipi_pct"], icms_pct=v["icms_pct"],
            custo_final=preco + v["frete"] + preco * v["ipi_pct"] - preco * v["icms_pct"],
        ))
    return HistoricoInsumoOut(versoes=list(reversed(versoes)))
