from datetime import datetime

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

from custos_precificacao import auth, planejamento_schema, versoes as dominio
from custos_precificacao.api.deps import get_session, rotear_para_planejamento
from custos_precificacao.api import resultados
from custos_precificacao.api.routers.premissas import _out as _premissa_out
from custos_precificacao.api.schemas import (
    ComparacaoOut,
    ComparacaoRefOut,
    ExecucaoVersaoOut,
    VersaoDadosOut,
    VersaoIn,
    VersaoOut,
    VersaoPatch,
    VersaoResumoOut,
)
from custos_precificacao.custeio import execucao_do_planejamento
from custos_precificacao.models import Planejamento, PlanejamentoVersao, Premissa

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


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


def _obter_versao(session: Session, plano: Planejamento, numero: int) -> PlanejamentoVersao:
    versao = session.query(PlanejamentoVersao).filter_by(planejamento_id=plano.id, numero=numero).one_or_none()
    if versao is None:
        raise HTTPException(status_code=404, detail=f"Versão {numero} do Planejamento {plano.codigo!r} não encontrada")
    return versao


def _mes(data) -> str:
    return f"{data.year:04d}-{data.month:02d}"


def construir_dados(session: Session, plano: Planejamento) -> VersaoDadosOut:
    """Os resultados atuais do Planejamento, no formato guardado em `PlanejamentoVersao.dados` (formato 2): Meta de Lucro, DRE Consolidada, cada Centro
    de Resultados e as linhas por produto.

    ValueError (→ 400) se não der para calcular (falta Meta de Lucro ou custeio)."""
    calculado, dre = resultados.calcular(session)
    premissas = session.query(Premissa).filter_by(planejamento_id=plano.id).order_by(Premissa.codigo).all()
    nome = auth.NomesDeAutores(session)
    execucao = execucao_do_planejamento(session)
    return VersaoDadosOut(
        modo_impostos=calculado.primaria.modo, meta_lucro=calculado.primaria.meta.percentual_margem,
        consolidada=resultados.consolidada_out(calculado, dre), centros=resultados.centros_out(calculado, dre),
        produtos=resultados.produtos_out(calculado), premissas=[_premissa_out(p) for p in premissas],
        execucao=ExecucaoVersaoOut(
            execucao_id=execucao.id, competencia=_mes(execucao.competencia), executado_em=execucao.executado_em,
            executado_por=nome(execucao.executado_por),
        ) if execucao is not None else None,
    )


def _resumo(versao: PlanejamentoVersao, nome: auth.NomesDeAutores) -> VersaoResumoOut:
    total = versao.dados.get("consolidada", {}).get("total", {})
    return VersaoResumoOut(
        numero=versao.numero, rotulo=versao.rotulo, observacao=versao.observacao, oficial=versao.oficial,
        criado_em=versao.criado_em, criado_por=nome(versao.criado_por),
        receita_total=total.get("preco"), resultado_operacional_total=total.get("resultado_operacional"),
        n_centros=len(versao.dados.get("centros", [])),
    )


def _completa(versao: PlanejamentoVersao, nome: auth.NomesDeAutores) -> VersaoOut:
    return VersaoOut(**_resumo(versao, nome).model_dump(), dados=VersaoDadosOut.model_validate(versao.dados))


def _erro_de_dominio(exc: ValueError) -> HTTPException:
    return HTTPException(status_code=422, detail=str(exc))


@router.get("/comparar", response_model=ComparacaoOut)
def comparar_planejamentos(a: str, b: str, session: Session = Depends(get_session)) -> ComparacaoOut:
    """Diferença B − A entre os resultados ao vivo de dois Planejamentos (cada um com os seus dados)."""
    if not planejamento_schema.e_postgres(session.get_bind()):
        raise HTTPException(status_code=422, detail="Comparar Planejamentos exige Postgres (um schema por Planejamento)")

    def referencia(codigo: str):
        with Session(session.get_bind()) as sessao:
            rotear_para_planejamento(sessao, codigo)
            plano = _obter_planejamento(sessao, codigo)
            try:
                dados = construir_dados(sessao, plano).model_dump(mode="json")
            except ValueError as exc:
                raise HTTPException(status_code=400, detail=f"Não foi possível calcular {codigo}: {exc}") from exc
            return ComparacaoRefOut(numero=plano.numero_versao, rotulo=plano.nome, criado_em=plano.criado_em), dados

    ref_a, dados_a = referencia(a)
    ref_b, dados_b = referencia(b)
    return ComparacaoOut(a=ref_a, b=ref_b, **dominio.comparar(dados_a, dados_b))


@router.get("/{codigo}/versoes", response_model=list[VersaoResumoOut])
def listar_versoes(codigo: str, session: Session = Depends(get_session)) -> list[VersaoResumoOut]:
    plano = _obter_planejamento(session, codigo)
    nome = auth.NomesDeAutores(session)
    versoes = session.query(PlanejamentoVersao).filter_by(planejamento_id=plano.id).order_by(PlanejamentoVersao.numero.desc())
    return [_resumo(v, nome) for v in versoes]


@router.post("/{codigo}/versoes", response_model=VersaoOut, status_code=201)
def congelar_versao(codigo: str, corpo: VersaoIn, session: Session = Depends(get_session)) -> VersaoOut:
    """Congela os resultados de hoje do Planejamento (Meta de Lucro, DRE Consolidada, Centros de Resultados, Premissas e linhas por produto)."""
    plano = _obter_planejamento(session, codigo)
    try:
        dados = construir_dados(session, plano)
    except ValueError as exc:  # falta Meta de Lucro/custeio: não congela uma versão sem DRE
        raise HTTPException(status_code=400, detail=str(exc)) from exc
    try:
        versao = dominio.congelar(session, plano, corpo.rotulo, corpo.observacao, dados.model_dump(mode="json"))
    except ValueError as exc:
        session.rollback()
        raise _erro_de_dominio(exc) from exc
    return _completa(versao, auth.NomesDeAutores(session))


def _referencia(session: Session, plano: Planejamento, ref: str, nome: auth.NomesDeAutores):
    """('atual' | número) → (ComparacaoRefOut, dados em dict)."""
    if ref.strip().lower() == "atual":
        try:
            dados = construir_dados(session, plano).model_dump(mode="json")
        except ValueError as exc:
            raise HTTPException(status_code=400, detail=f"Não foi possível calcular o Atual: {exc}") from exc
        return ComparacaoRefOut(numero=None, rotulo="Atual (ao vivo)", criado_em=datetime.utcnow()), dados
    try:
        numero = int(ref)
    except ValueError as exc:
        raise HTTPException(status_code=422, detail=f"Referência inválida {ref!r}: use um número de versão ou 'atual'") from exc
    versao = _obter_versao(session, plano, numero)
    return ComparacaoRefOut(numero=versao.numero, rotulo=versao.rotulo, criado_em=versao.criado_em), versao.dados


@router.get("/{codigo}/versoes/comparar", response_model=ComparacaoOut)
def comparar_versoes(codigo: str, a: str, b: str, session: Session = Depends(get_session)) -> ComparacaoOut:
    """Diferença B − A entre duas versões (ou uma versão e o 'atual', calculado na hora)."""
    plano = _obter_planejamento(session, codigo)
    nome = auth.NomesDeAutores(session)
    ref_a, dados_a = _referencia(session, plano, a, nome)
    ref_b, dados_b = _referencia(session, plano, b, nome)
    return ComparacaoOut(a=ref_a, b=ref_b, **dominio.comparar(dados_a, dados_b))


@router.get("/{codigo}/versoes/{numero}", response_model=VersaoOut)
def obter_versao(codigo: str, numero: int, session: Session = Depends(get_session)) -> VersaoOut:
    plano = _obter_planejamento(session, codigo)
    return _completa(_obter_versao(session, plano, numero), auth.NomesDeAutores(session))


@router.patch("/{codigo}/versoes/{numero}", response_model=VersaoResumoOut)
def alterar_versao(
    codigo: str, numero: int, corpo: VersaoPatch, session: Session = Depends(get_session)
) -> VersaoResumoOut:
    """Só rótulo, observação e o marcador de oficial mudam; os números da versão são imutáveis."""
    versao = _obter_versao(session, _obter_planejamento(session, codigo), numero)
    try:
        dominio.editar(
            session, versao, rotulo=corpo.rotulo, observacao=corpo.observacao,
            mudar_observacao="observacao" in corpo.model_fields_set, oficial=corpo.oficial,
        )
    except ValueError as exc:
        session.rollback()
        raise _erro_de_dominio(exc) from exc
    return _resumo(versao, auth.NomesDeAutores(session))


@router.delete("/{codigo}/versoes/{numero}", status_code=204, dependencies=[Depends(auth.exigir_admin)])
def excluir_versao(codigo: str, numero: int, session: Session = Depends(get_session)) -> None:
    versao = _obter_versao(session, _obter_planejamento(session, codigo), numero)
    session.delete(versao)
    session.commit()
