from dataclasses import asdict
from decimal import Decimal, InvalidOperation

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session

from custos_precificacao import alocacao, arvore, historico, vinculos
from custos_precificacao.api.deps import get_session
from custos_precificacao.api.schemas import (
    CopiaAlocacaoIn,
    CopiaAlocacaoOut,
    DirecionadorOpcaoOut,
    LinhaAlocacaoIn,
    LinhaAlocacaoPatch,
    NoAlocacaoOut,
    OpcaoOut,
    OpcoesAlocacaoOut,
    ProdutoAlocacaoOut,
)
from custos_precificacao.models import Ambiente, Atividade, Direcionador, GrupoMaoDeObra, Maquina, Produto

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


def _erro(exc: Exception) -> HTTPException:
    return HTTPException(status_code=404 if "não encontrado" in str(exc) else 422, detail=str(exc))


def _horas(texto: str) -> str:
    try:
        valor = Decimal((texto or "").strip().replace(",", "."))
    except InvalidOperation as exc:
        raise HTTPException(status_code=422, detail="As horas por bacia precisam ser um número") from exc
    if not valor.is_finite() or valor < 0:
        raise HTTPException(status_code=422, detail="As horas por bacia não podem ser negativas")
    return str(valor)


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


def _cadeia(session: Session, codigo: str) -> NoAlocacaoOut:
    return NoAlocacaoOut.model_validate(asdict(alocacao.montar_cadeia(session, codigo)))


@router.get("", response_model=list[ProdutoAlocacaoOut])
def listar(busca: str = "", tipo: str = "", so_sem_alocacao: bool = False, session: Session = Depends(get_session)):
    return alocacao.listar_produtos(session, busca=busca, tipo=tipo, so_sem_alocacao=so_sem_alocacao)


@router.get("/opcoes", response_model=OpcoesAlocacaoOut)
def opcoes(session: Session = Depends(get_session)) -> OpcoesAlocacaoOut:
    def lista(modelo, chave="codigo", categoria: str | None = None) -> list[OpcaoOut]:
        itens = session.query(modelo).all()
        if categoria is not None:
            itens = [o for o in itens if o.categoria == categoria]
        if hasattr(modelo, "aceita_lancamento"):  # cadastros em árvore: só nó analítico e ativo recebe lançamento
            itens = [o for o in itens if o.aceita_lancamento and o.ativo]
            itens.sort(key=lambda o: arvore.chave_de_ordem(o.codigo))
        else:
            itens.sort(key=lambda o: getattr(o, chave))
        return [OpcaoOut(id=o.id, codigo=getattr(o, chave), nome=o.nome) for o in itens]

    atividades = {a.id: a for a in session.query(Atividade)}
    direcionadores = [
        DirecionadorOpcaoOut(
            codigo=d.codigo, nome=d.nome, atividade_codigo=atividades[d.atividade_id].codigo, atividade_nome=atividades[d.atividade_id].nome
        )
        for d in session.query(Direcionador).filter_by(base="tempo").order_by(Direcionador.codigo)
    ]
    return OpcoesAlocacaoOut(
        maquinas=lista(Maquina, categoria="maquina"), outros=lista(Maquina, categoria="outro"), ambientes=lista(Ambiente),
        grupos=lista(GrupoMaoDeObra), direcionadores=direcionadores,
    )


@router.post("/copiar", response_model=CopiaAlocacaoOut)
def copiar(corpo: CopiaAlocacaoIn, session: Session = Depends(get_session)) -> CopiaAlocacaoOut:
    """Copia os elementos de custo de um produto para um ou vários (tudo-ou-nada), valendo a partir de `atualizado_em`."""
    try:
        resultado = alocacao.copiar_elementos(
            session, corpo.origem, corpo.destinos, substituir=corpo.substituir, atualizado_em=corpo.atualizado_em
        )
    except (ValueError, historico.ErroDeHistorico) as exc:
        raise _erro(exc) from exc
    return CopiaAlocacaoOut(criadas=resultado.criadas, puladas=resultado.puladas, removidas=resultado.removidas)


@router.get("/{codigo}", response_model=NoAlocacaoOut)
def cadeia(codigo: str, session: Session = Depends(get_session)) -> NoAlocacaoOut:
    """O produto e os intermediários que ele consome, cada um com os seus elementos de custo e o custo deles."""
    _produto(session, codigo)
    return _cadeia(session, codigo)


TIPOS_ACEITOS = (*vinculos.ROTEIRO_RECURSOS, *vinculos.TIPO_ROTEIRO_DE)  # maquina, ambiente, mao-de-obra e outros


def _linha(session: Session, codigo: str, tipo: str, linha_id: int):
    produto = _produto(session, codigo)
    if tipo not in TIPOS_ACEITOS:
        raise HTTPException(status_code=404, detail=f"Tipo {tipo!r} inválido. Opções: {', '.join(TIPOS_ACEITOS)}")
    linha = session.get(vinculos.ROTEIRO_RECURSOS[vinculos.TIPO_ROTEIRO_DE.get(tipo, tipo)].roteiro_model, linha_id)
    if linha is None or linha.produto_id != produto.id:
        raise HTTPException(status_code=404, detail=f"A linha {linha_id} não pertence à alocação de {codigo!r}")
    return linha


@router.post("/{codigo}/linhas", response_model=NoAlocacaoOut, status_code=201)
def adicionar(codigo: str, corpo: LinhaAlocacaoIn, session: Session = Depends(get_session)) -> NoAlocacaoOut:
    _produto(session, codigo)
    if corpo.tipo not in TIPOS_ACEITOS:
        raise HTTPException(status_code=422, detail=f"tipo deve ser um de: {', '.join(TIPOS_ACEITOS)}")
    try:
        vinculos.adicionar_roteiro_recurso(
            session, corpo.tipo, codigo, None, corpo.recurso_codigo, _horas(corpo.horas_bacia),
            atualizado_em=corpo.atualizado_em, direcionador_codigo=corpo.direcionador_codigo,
        )
    except (ValueError, historico.ErroDeHistorico) as exc:
        session.rollback()
        raise _erro(exc) from exc
    return _cadeia(session, codigo)


@router.patch("/{codigo}/linhas/{tipo}/{linha_id}", response_model=NoAlocacaoOut)
def alterar(codigo: str, tipo: str, linha_id: int, corpo: LinhaAlocacaoPatch, session: Session = Depends(get_session)) -> NoAlocacaoOut:
    _linha(session, codigo, tipo, linha_id)
    try:
        vinculos.alterar_linha_roteiro(
            session, tipo, linha_id, _horas(corpo.horas_bacia), atualizado_em=corpo.atualizado_em, direcionador_codigo=corpo.direcionador_codigo
        )
    except (ValueError, historico.ErroDeHistorico) as exc:
        session.rollback()
        raise _erro(exc) from exc
    return _cadeia(session, codigo)


@router.delete("/{codigo}/linhas/{tipo}/{linha_id}", response_model=NoAlocacaoOut)
def encerrar(codigo: str, tipo: str, linha_id: int, em: str | None = None, session: Session = Depends(get_session)) -> NoAlocacaoOut:
    """Encerra a linha a partir do mês `em` (padrão o atual); ela segue valendo nos meses anteriores."""
    _linha(session, codigo, tipo, linha_id)
    try:
        vinculos.remover_linha_roteiro(session, tipo, linha_id, em=em)
    except historico.ErroDeHistorico as exc:
        session.rollback()
        raise _erro(exc) from exc
    return _cadeia(session, codigo)
