from dataclasses import dataclass
from decimal import Decimal

from sqlalchemy.orm import Session

from custos_precificacao.custeio import execucao_do_planejamento, execucao_mais_recente
from custos_precificacao.models import (
    CenarioPrecificacao,
    CusteioProduto,
    Planejamento,
    Premissa,
    Produto,
    VolumeProjetado,
)


def _obter_por_codigo(session: Session, model: type, chave_coluna: str, valor: str):
    instancia = session.query(model).filter_by(**{chave_coluna: valor}).one_or_none()
    if instancia is None:
        raise ValueError(f"{model.__name__} com {chave_coluna}={valor!r} não encontrado")
    return instancia


def definir_volume_projetado(
    session: Session, planejamento_codigo: str, cenario_codigo: str, produto_codigo: str, quantidade: str
) -> VolumeProjetado:
    """Cria ou atualiza (upsert) o Volume Projetado de um Produto num Cenário, dentro de um Planejamento."""
    planejamento = _obter_por_codigo(session, Planejamento, "codigo", planejamento_codigo)
    cenario = _obter_por_codigo(session, CenarioPrecificacao, "codigo", cenario_codigo)
    produto = _obter_por_codigo(session, Produto, "codigo", produto_codigo)

    linha = (
        session.query(VolumeProjetado)
        .filter_by(planejamento_id=planejamento.id, cenario_id=cenario.id, produto_id=produto.id)
        .one_or_none()
    )
    if linha is None:
        linha = VolumeProjetado(planejamento_id=planejamento.id, cenario_id=cenario.id, produto_id=produto.id)
        session.add(linha)
    linha.quantidade = Decimal(quantidade)
    session.commit()
    return linha


def listar_volume_projetado(session: Session, planejamento_codigo: str) -> list[VolumeProjetado]:
    planejamento = _obter_por_codigo(session, Planejamento, "codigo", planejamento_codigo)
    return session.query(VolumeProjetado).filter_by(planejamento_id=planejamento.id).all()


def listar_premissas(
    session: Session, planejamento_codigo: str, tipo: str | None = None
) -> list[Premissa]:
    """Premissas de um Planejamento, opcionalmente filtradas por tipo (comercial/operacional/
    despesas/financeiro — ver relatório `planejamento resumo`, §9.2)."""
    planejamento = _obter_por_codigo(session, Planejamento, "codigo", planejamento_codigo)
    query = session.query(Premissa).filter_by(planejamento_id=planejamento.id)
    if tipo is not None:
        query = query.filter_by(tipo=tipo)
    return query.all()


def remover_volume_projetado(session: Session, planejamento_codigo: str, cenario_codigo: str, produto_codigo: str) -> None:
    """Remove o Volume Projetado de um Produto num Cenário (ValueError se a linha não existe)."""
    planejamento = _obter_por_codigo(session, Planejamento, "codigo", planejamento_codigo)
    cenario = _obter_por_codigo(session, CenarioPrecificacao, "codigo", cenario_codigo)
    produto = _obter_por_codigo(session, Produto, "codigo", produto_codigo)
    linha = (
        session.query(VolumeProjetado)
        .filter_by(planejamento_id=planejamento.id, cenario_id=cenario.id, produto_id=produto.id)
        .one_or_none()
    )
    if linha is None:
        raise ValueError(
            f"Não há Volume Projetado de {produto_codigo!r} no Cenário {cenario_codigo!r} do Planejamento {planejamento_codigo!r}"
        )
    session.delete(linha)
    session.commit()


@dataclass
class LinhaProducao:
    cenario_id: int
    produto_id: int
    quantidade: Decimal
    custo_producao_unitario: Decimal | None
    custo_variavel_unitario: Decimal | None
    """Custos None = o Cenário não tem execução de custeio, ou o Produto não foi custeado nela."""

    @property
    def custo_producao_total(self) -> Decimal | None:
        if self.custo_producao_unitario is None:
            return None
        return self.custo_producao_unitario * self.quantidade

    @property
    def custo_variavel_total(self) -> Decimal | None:
        if self.custo_variavel_unitario is None:
            return None
        return self.custo_variavel_unitario * self.quantidade


def detalhar_producao(session: Session, planejamento_codigo: str) -> list[LinhaProducao]:
    """Volume e custo de produção/variável por Produto+Cenário — a seção Operacional/Produção
    (§9.2), com os mesmos números (execução de custeio mais recente do Cenário × volume) de
    `consolidar_planejamento`. Diferente dele, não levanta erro quando falta custeio: devolve custos
    None, pra que o volume continue editável mesmo sem `custeio-run`."""
    planejamento = _obter_por_codigo(session, Planejamento, "codigo", planejamento_codigo)
    volumes = session.query(VolumeProjetado).filter_by(planejamento_id=planejamento.id).all()

    resultados_por_cenario: dict[int, dict[int, CusteioProduto]] = {}
    for cenario_id in {v.cenario_id for v in volumes}:
        execucao = execucao_mais_recente(session, cenario_id)
        resultados_por_cenario[cenario_id] = (
            {r.produto_id: r for r in session.query(CusteioProduto).filter_by(execucao_id=execucao.id)}
            if execucao
            else {}
        )

    linhas = []
    for volume in volumes:
        resultado = resultados_por_cenario[volume.cenario_id].get(volume.produto_id)
        linhas.append(LinhaProducao(
            cenario_id=volume.cenario_id,
            produto_id=volume.produto_id,
            quantidade=volume.quantidade,
            custo_producao_unitario=resultado.custo_producao_unitario if resultado else None,
            custo_variavel_unitario=resultado.custo_variavel_unitario if resultado else None,
        ))
    # o Centro "Produção a alocar" não guarda volume: a quantidade dele é calculada (produção − o que os outros Centros alocaram)
    from custos_precificacao import precificacao_centros

    quantidades, _ = precificacao_centros.quantidades(session)
    execucao_geral = execucao_do_planejamento(session)
    custos = {r.produto_id: r for r in session.query(CusteioProduto).filter_by(execucao_id=execucao_geral.id)} if execucao_geral else {}
    for centro in session.query(CenarioPrecificacao).filter_by(a_alocar=True):
        for produto_id, quantidade in quantidades.get(centro.id, {}).items():
            resultado = custos.get(produto_id)
            linhas.append(LinhaProducao(
                cenario_id=centro.id, produto_id=produto_id, quantidade=quantidade,
                custo_producao_unitario=resultado.custo_producao_unitario if resultado else None,
                custo_variavel_unitario=resultado.custo_variavel_unitario if resultado else None,
            ))
    return linhas
