"""Plano de produção: o que se estima produzir de fato (`Produto.qtd_estimada_producao_mes`), a conferência com a soma dos
volumes projetados por Centro de Resultado (`VolumeProjetado`) e o ajuste da produção a esses volumes.

A soma dos volumes por Centro de Resultado fecha com a produção de cada produto: o que ainda não foi alocado é o Centro "Produção a alocar"
(quantidade calculada). Só o que se aloca **acima** da produção diverge: é avisado ao usuário (no cabeçalho do app e na tela) e pode ser corrigido
ajustando a produção aos volumes dos Centros. O volume dos intermediários
(PI) não entra na conferência: ele é calculado pela estrutura (`explosao.py`).
"""

from dataclasses import dataclass
from decimal import Decimal

from sqlalchemy import func
from sqlalchemy.orm import Session

from custos_precificacao import unidades
from custos_precificacao.explosao import Volumes, volumes_de_producao
from custos_precificacao.fichas import produtos_na_tabela_de_precos
from custos_precificacao.models import Planejamento, Produto, VolumeProjetado

TOLERANCIA = Decimal("0.0001")


@dataclass
class ItemPlano:
    produto: Produto
    unidade: str
    na_tabela_de_precos: bool
    informado: Decimal
    calculado: Decimal
    previsto_centros: Decimal
    tem_previsao_centros: bool  # há ao menos uma linha de volume em algum Centro de Resultado

    @property
    def total(self) -> Decimal:
        return self.informado + self.calculado

    @property
    def divergente(self) -> bool:
        """Alocado aos Centros acima da produção. O que falta alocar não diverge: vai para o Centro "Produção a alocar"."""
        return self.previsto_centros - self.informado > TOLERANCIA


@dataclass
class PlanoProducao:
    itens: list[ItemPlano]  # produtos da tabela de preços (ou todos), com a produção informada
    intermediarios: list[ItemPlano]  # só volume calculado pela estrutura
    divergentes: list[ItemPlano]  # de todos os produtos, não só dos listados
    avisos: list[str]


def _obter_planejamento(session: Session, codigo: str) -> Planejamento:
    plano = session.query(Planejamento).filter_by(codigo=codigo).one_or_none()
    if plano is None:
        raise ValueError(f"Planejamento com codigo={codigo!r} não encontrado")
    return plano


def _previsto_nos_centros(session: Session, planejamento: Planejamento) -> tuple[dict[int, Decimal], set[int]]:
    soma = {
        produto_id: total
        for produto_id, total in session.query(VolumeProjetado.produto_id, func.sum(VolumeProjetado.quantidade))
        .filter(VolumeProjetado.planejamento_id == planejamento.id)
        .group_by(VolumeProjetado.produto_id)
    }
    return {pid: Decimal(v) for pid, v in soma.items()}, set(soma)


def _itens(session: Session, planejamento: Planejamento, volumes: Volumes) -> list[ItemPlano]:
    na_tabela = produtos_na_tabela_de_precos(session)
    soma, com_previsao = _previsto_nos_centros(session, planejamento)
    itens = []
    for produto in session.query(Produto).order_by(Produto.codigo):
        volume = volumes.por_produto.get(produto.id)
        itens.append(ItemPlano(
            produto=produto, unidade=unidades.normalizar(produto.unidade), na_tabela_de_precos=produto.id in na_tabela,
            informado=volume.informado if volume else Decimal(0), calculado=volume.calculado if volume else Decimal(0),
            previsto_centros=soma.get(produto.id, Decimal(0)), tem_previsao_centros=produto.id in com_previsao,
        ))
    return itens


def montar_plano(session: Session, planejamento_codigo: str, *, todos: bool = False) -> PlanoProducao:
    """Itens da tabela de preços atual (todos os produtos com `todos`; sem nenhuma tabela, também todos) com a produção informada,
    os intermediários com volume calculado pela estrutura e as divergências com os Centros de Resultado."""
    planejamento = _obter_planejamento(session, planejamento_codigo)
    volumes = volumes_de_producao(session)
    todos_itens = _itens(session, planejamento, volumes)
    ha_tabela = any(i.na_tabela_de_precos for i in todos_itens)
    listados = [i for i in todos_itens if todos or not ha_tabela or i.na_tabela_de_precos]
    ids_listados = {i.produto.id for i in listados}
    return PlanoProducao(
        itens=listados,
        intermediarios=[i for i in todos_itens if i.produto.id not in ids_listados and i.calculado > 0],
        divergentes=[i for i in todos_itens if i.divergente and i.na_tabela_de_precos],
        avisos=volumes.avisos,
    )


def divergencias(session: Session, planejamento_codigo: str) -> list[ItemPlano]:
    return montar_plano(session, planejamento_codigo).divergentes


def definir_producao(session: Session, produto_codigo: str, quantidade: Decimal) -> Produto:
    produto = session.query(Produto).filter_by(codigo=produto_codigo).one_or_none()
    if produto is None:
        raise ValueError(f"Produto com codigo={produto_codigo!r} não encontrado")
    produto.qtd_estimada_producao_mes = quantidade
    session.commit()
    return produto


def ajustar_aos_centros(session: Session, planejamento_codigo: str) -> int:
    """Produção := soma dos volumes projetados nos Centros de Resultado, para os produtos divergentes que têm previsão em ao
    menos um Centro (quem não tem previsão alguma fica como está: não se zera produção por falta de previsão). Devolve quantos."""
    ajustados = 0
    for item in divergencias(session, planejamento_codigo):
        if not item.tem_previsao_centros:
            continue
        item.produto.qtd_estimada_producao_mes = item.previsto_centros
        ajustados += 1
    session.commit()
    return ajustados
