"""Trava da Precificação Primária: fechada, ela congela o rateio das despesas fixas, a Meta de Lucro, os Preços Definidos e o modo dos impostos, e libera
os Centros de Resultados (que partem da primária).

Ao travar o sistema grava os % efetivos de todos os pools de despesa fixa (a sugestão também vira rateio gravado) e a **assinatura** do que a primária usa:
valores dos pools, rateio, Meta, preços definidos, modo dos impostos, o custeio (custo unitário de cada cabeça) e a produção prevista. Se algo disso mudar
depois (custo, quantidade, despesa), a trava fica **desatualizada** e os Centros voltam a ficar bloqueados até reabrir, conferir e travar de novo."""

import hashlib
import json
from dataclasses import dataclass, field
from datetime import datetime
from decimal import Decimal

from sqlalchemy.orm import Session

from custos_precificacao import motor_preco, precificacao_primaria as pp, rateio
from custos_precificacao.custeio import execucao_do_planejamento
from custos_precificacao.explosao import volume_de_producao
from custos_precificacao.models import CusteioProduto, PrecoPrimario, Produto, TravaPrimaria

ABERTA, TRAVADA, DESATUALIZADA = "aberta", "travada", "desatualizada"
MAX_PENDENCIAS = 8


@dataclass
class Estado:
    situacao: str
    travada_em: datetime | None = None
    travada_por: str | None = None
    pendencias: list[str] = field(default_factory=list)  # o que impede de travar (só na primária aberta)


def _numero(valor) -> str:
    return "" if valor is None else f"{Decimal(valor):.6f}"


def _assinatura(session: Session) -> str:
    """Resumo (sha256) de tudo que a primária usa. Muda quando custo, quantidade, despesa, rateio, Meta, preço definido ou modo mudam."""
    execucao = execucao_do_planejamento(session)
    custos = {}
    if execucao is not None:
        custos = {r.produto_id: _numero(r.custo_producao_unitario) for r in session.query(CusteioProduto).filter_by(execucao_id=execucao.id)}
    cabecas = {}
    for p in session.query(Produto).filter(Produto.tabela_de_precos.is_(True)).order_by(Produto.codigo):
        cabecas[p.codigo] = {"custo": custos.get(p.id, ""), "volume": _numero(volume_de_producao(session, p))}
    grupos = {}
    for pool in rateio.pools(session):
        codigo = pool.direcionador.codigo
        criterio, w = rateio.criterio_do_grupo(session, pool.direcionador)
        try:
            pesos = rateio.pesos_efetivos(session, pool.direcionador)
            total = sum(pesos.values(), Decimal(0))
            produtos = {s.id: s.codigo for s in session.query(Produto).filter(Produto.id.in_(pesos))}
            partes = {produtos[k]: _numero(v / total) for k, v in pesos.items()}
        except ValueError as exc:
            partes = {"erro": str(exc)}
        grupos[codigo] = {"valor": _numero(pool.valor), "criterio": criterio, "w": _numero(w), "rateio": partes}
    meta = pp.obter_meta(session)
    produtos_por_id = {p.id: p.codigo for p in session.query(Produto)}
    conteudo = {
        "execucao": execucao.id if execucao is not None else None,
        "cabecas": cabecas,
        "grupos": grupos,
        "meta": [_numero(getattr(meta, c)) for c in ("percentual_socios", "percentual_reserva", "percentual_expansao", "percentual_outros")] if meta else None,
        "definidos": {produtos_por_id[p.produto_id]: _numero(p.preco_definido) for p in session.query(PrecoPrimario) if p.preco_definido},
        "modo": motor_preco.obter_modo(session),
    }
    return hashlib.sha256(json.dumps(conteudo, sort_keys=True).encode()).hexdigest()


def pendencias(session: Session, calcular_preco: bool = True) -> list[str]:
    """O que impede de travar a primária (lista vazia = pode travar). `calcular_preco` confere também se o preço sugerido calcula (mais pesado)."""
    itens: list[str] = []
    if execucao_do_planejamento(session) is None:
        itens.append("Calcule o Custo dos Produtos antes de travar")
    meta = pp.obter_meta(session)
    if meta is None or not meta.percentual_margem:
        itens.append("Defina a Meta de Lucro")
    for pool in rateio.pools(session):
        codigo = pool.direcionador.codigo
        try:
            rateio.pesos_efetivos(session, pool.direcionador)
        except ValueError as exc:
            itens.append(f"Direcionador {codigo}: {exc}")
            continue
        novos = rateio.sem_percentual(session, pool.direcionador)
        if novos:
            itens.append(f"Direcionador {codigo}: produto(s) {', '.join(novos)} entraram no rateio e estão sem % — refaça o rateio ou volte à sugestão")
    if not itens and calcular_preco:
        try:
            primaria = pp.calcular(session)
        except ValueError as exc:
            itens.append(str(exc))
        else:
            if primaria.aviso:
                itens.append(primaria.aviso)
    return itens[:MAX_PENDENCIAS]


def situacao(session: Session) -> str:
    """aberta | travada | desatualizada (travada, mas o que a primária usa mudou desde a trava)."""
    trava = session.query(TravaPrimaria).first()
    if trava is None or not trava.travada:
        return ABERTA
    return TRAVADA if trava.assinatura == _assinatura(session) else DESATUALIZADA


def estado(session: Session, calcular_preco: bool = True) -> Estado:
    trava = session.query(TravaPrimaria).first()
    atual = situacao(session)
    saida = Estado(situacao=atual)
    if atual != ABERTA and trava is not None:
        saida.travada_em, saida.travada_por = trava.travada_em, trava.travada_por
    if atual == ABERTA:
        saida.pendencias = pendencias(session, calcular_preco)
    return saida


def motivo_do_bloqueio(session: Session) -> str | None:
    """Por que os Centros de Resultados não podem ser calculados (None = primária travada e atualizada)."""
    atual = situacao(session)
    if atual == TRAVADA:
        return None
    if atual == DESATUALIZADA:
        return "A Precificação Primária está desatualizada (custo, quantidade ou despesa mudaram depois da trava): reabra, confira e trave de novo para liberar os Centros"
    return "Conclua e trave a Precificação Primária (Rateio das Despesas Fixas) para liberar os Centros de Resultados"


def travar(session: Session, usuario: str | None = None) -> Estado:
    """Trava a primária inteira. ValueError com as pendências se algo impede. Grava os % efetivos de todos os pools e a assinatura."""
    if rateio.esta_travada(session):
        raise rateio.PrimariaTravada("A Precificação Primária já está travada — reabra para travar de novo")
    itens = pendencias(session)
    if itens:
        raise ValueError("Não dá para travar a Precificação Primária: " + "; ".join(itens))
    for pool in rateio.pools(session):
        pesos = rateio.pesos_efetivos(session, pool.direcionador)
        total = sum(pesos.values(), Decimal(0))
        rateio.gravar_fracoes(session, pool.direcionador, {k: v / total for k, v in pesos.items()})
    session.commit()  # a assinatura é calculada depois, sobre o que ficou gravado (arredondado)
    trava = session.query(TravaPrimaria).first() or TravaPrimaria()
    trava.travada, trava.travada_em, trava.travada_por = True, datetime.utcnow(), usuario
    trava.assinatura = _assinatura(session)
    session.add(trava)
    session.commit()
    return estado(session)


def reabrir(session: Session) -> Estado:
    """Reabre a primária: rateio, Meta e Preços Definidos voltam a poder mudar e os Centros ficam bloqueados até travar de novo."""
    trava = session.query(TravaPrimaria).first()
    if trava is not None and trava.travada:
        trava.travada, trava.assinatura = False, None
        session.commit()
    return estado(session)
