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 cenarios as dominio
from custos_precificacao.api.deps import get_session
from custos_precificacao.api.schemas import CenarioIn, CenarioOut, CenarioPainelOut
from custos_precificacao.models import CenarioPrecificacao, Despesa, VolumeProjetado

router = APIRouter(prefix="/cenarios", tags=["cenarios"])


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


def _desconto(texto: str | None) -> Decimal | None:
    """Desconto comercial (fração 0..1; aceita vírgula). None = não informado."""
    if texto is None or str(texto).strip() == "":
        return None
    try:
        valor = Decimal(str(texto).strip().replace(",", "."))
    except InvalidOperation as exc:
        raise HTTPException(status_code=422, detail="desconto precisa ser um número") from exc
    if not valor.is_finite() or valor < 0 or valor >= 1:
        raise HTTPException(status_code=422, detail="desconto deve ficar entre 0 e 1 (ex.: 0,05 = 5%)")
    return valor.quantize(Decimal("0.0001"))


def _cenario_out(c: CenarioPrecificacao) -> CenarioOut:
    return CenarioOut(codigo=c.codigo, nome=c.nome, descricao=c.descricao, percentual_desconto=c.percentual_desconto, a_alocar=c.a_alocar)


def _ordenados(session: Session) -> list[CenarioPrecificacao]:
    return session.query(CenarioPrecificacao).order_by(CenarioPrecificacao.a_alocar, CenarioPrecificacao.codigo).all()


@router.get("", response_model=list[CenarioOut])
def listar_cenarios(session: Session = Depends(get_session)) -> list[CenarioOut]:
    dominio.garantir_centro_a_alocar(session)
    session.commit()
    return [_cenario_out(c) for c in _ordenados(session)]


@router.post("", response_model=CenarioOut, status_code=201)
def criar_cenario(corpo: CenarioIn, session: Session = Depends(get_session)) -> CenarioOut:
    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")
    descricao = (corpo.descricao or "").strip() or None
    if descricao and len(descricao) > 500:
        raise HTTPException(status_code=422, detail="a descrição aceita até 500 caracteres")
    cenario = CenarioPrecificacao(codigo=codigo, nome=nome, descricao=descricao, percentual_desconto=_desconto(corpo.percentual_desconto) or Decimal(0))
    session.add(cenario)
    try:
        session.commit()
    except IntegrityError as exc:
        session.rollback()
        raise HTTPException(status_code=409, detail=f"Já existe um Centro de Resultados com código {codigo!r}") from exc
    return _cenario_out(cenario)


@router.patch("/{codigo}", response_model=CenarioOut)
def alterar_cenario(codigo: str, corpo: CenarioIn, session: Session = Depends(get_session)) -> CenarioOut:
    cenario = _obter(session, codigo)
    enviados = corpo.model_fields_set
    if cenario.a_alocar:
        raise HTTPException(status_code=409, detail="O Centro 'Produção a alocar' é do sistema e não pode ser alterado")
    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")
    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)")
        cenario.nome = nome
    if "descricao" in enviados:
        descricao = (corpo.descricao or "").strip() or None
        if descricao and len(descricao) > 500:
            raise HTTPException(status_code=422, detail="a descrição aceita até 500 caracteres")
        cenario.descricao = descricao
    desconto = _desconto(corpo.percentual_desconto)
    if desconto is not None:
        cenario.percentual_desconto = desconto
    session.commit()
    return _cenario_out(cenario)


@router.delete("/{codigo}", status_code=204)
def excluir_cenario(codigo: str, session: Session = Depends(get_session)) -> None:
    _obter(session, codigo)
    try:
        dominio.excluir_cenario(session, codigo)
    except ValueError as exc:  # do sistema, ou ainda com quantidades alocadas
        session.rollback()
        raise HTTPException(status_code=409, detail=str(exc)) from exc


@router.get("/painel", response_model=list[CenarioPainelOut])
def obter_painel(session: Session = Depends(get_session)) -> list[CenarioPainelOut]:
    dominio.garantir_centro_a_alocar(session)
    session.commit()
    despesas = dict(session.query(Despesa.cenario_id, func.count(Despesa.id)).filter(Despesa.cenario_id.isnot(None)).group_by(Despesa.cenario_id).all())
    produtos = dict(
        session.query(VolumeProjetado.cenario_id, func.count(VolumeProjetado.id)).filter(VolumeProjetado.quantidade > 0).group_by(VolumeProjetado.cenario_id).all()
    )
    return [
        CenarioPainelOut(
            codigo=c.codigo, nome=c.nome, descricao=c.descricao, desconto=c.percentual_desconto, a_alocar=c.a_alocar,
            despesas_qtd=despesas.get(c.id, 0), produtos_qtd=produtos.get(c.id, 0),
        )
        for c in _ordenados(session)
    ]
