"""Precificação primária (Meta de Lucro, Precificação, Análise de Preço x Lucro, Tabela de Preços e DRE) — ver `precificacao_primaria`."""

from io import BytesIO
from decimal import Decimal, InvalidOperation

from fastapi import APIRouter, Depends, HTTPException, Response
from openpyxl import Workbook
from openpyxl.styles import Alignment, Font, PatternFill
from pydantic import BaseModel
from sqlalchemy.orm import Session

from custos_precificacao import motor_preco, precificacao_primaria as pp
from custos_precificacao.api.deps import get_session
from custos_precificacao.api.routers.precificacao import _composicao_out
from custos_precificacao.api.schemas import ComposicaoPrecoOut
from custos_precificacao.rateio import PrimariaTravada

router = APIRouter(tags=["precificacao-primaria"])

XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"


class MetaIn(BaseModel):
    percentual_socios: str | None = None
    percentual_reserva: str | None = None
    percentual_expansao: str | None = None
    percentual_outros: str | None = None


class MetaOut(BaseModel):
    definida: bool
    percentual_socios: Decimal
    percentual_reserva: Decimal
    percentual_expansao: Decimal
    percentual_outros: Decimal
    percentual_margem: Decimal  # o Lucro Pretendido: líquido do IR/CSLL


class LinhaPrimariaOut(BaseModel):
    produto_codigo: str
    produto_nome: str
    quantidade: Decimal
    # unitários ao Preço Sugerido (a lista de Precificação)
    impostos: Decimal
    custo: Decimal
    despesas_variaveis: Decimal
    despesa_fixa: Decimal
    pct_lucro: Decimal
    preco_sugerido: Decimal
    preco_definido: Decimal
    definido_manual: bool
    spread: Decimal  # margem líquida efetiva do definido − meta (pontos)
    composicao: ComposicaoPrecoOut


class PrimariaOut(BaseModel):
    modo_impostos: str
    pct_lucro: Decimal | None
    aviso: str | None = None
    avisos: list[str] = []
    linhas: list[LinhaPrimariaOut] = []
    desconto: Decimal = Decimal(0)  # desconto comercial do Centro (0 na primária): o preço de tabela é o líquido ÷ (1 − desconto)
    a_alocar: bool = False  # "Produção a alocar": quantidade calculada, preço da primária


class PrecoDefinidoIn(BaseModel):
    preco_definido: str | None = None  # vazio/None volta a seguir o sugerido


class ResultadoOut(BaseModel):
    preco: Decimal
    impostos: Decimal
    custo: Decimal
    despesas_variaveis: Decimal
    margem_contribuicao: Decimal
    despesas_fixas: Decimal
    resultado_operacional: Decimal
    destinacao: Decimal
    resultado_final: Decimal
    destinos: dict[str, Decimal]


class ProdutoAnaliseOut(BaseModel):
    produto_codigo: str
    produto_nome: str
    quantidade: Decimal
    unitario: ResultadoOut
    total: ResultadoOut


class AnaliseOut(BaseModel):
    aviso: str | None = None
    avisos: list[str] = []
    rotulos_destinos: dict[str, str] = {}
    total_geral: ResultadoOut | None = None
    produtos: list[ProdutoAnaliseOut] = []


class LinhaTabelaOut(BaseModel):
    produto_codigo: str
    produto_nome: str
    precos: dict[str, Decimal]  # faixa de ICMS -> preço


class TabelaOut(BaseModel):
    aviso: str | None = None
    faixas: list[str] = []
    rotulos: dict[str, str] = {}
    aliquotas: dict[str, Decimal] = {}
    linhas: list[LinhaTabelaOut] = []


class LinhaDreOut(BaseModel):
    rotulo: str
    valor: Decimal
    nivel: int
    estilo: str  # conta | grupo | total
    codigo: str | None = None
    desembolso: Decimal | None = None  # parte em caixa; Desembolso + Retenções = Valor
    retencao: Decimal | None = None  # parte em provisão/encargos (só nos custos dos elementos de custo)


def linha_dre_out(l) -> "LinhaDreOut":
    return LinhaDreOut(rotulo=l.rotulo, valor=l.valor, nivel=l.nivel, estilo=l.estilo, codigo=l.codigo, desembolso=l.desembolso, retencao=l.retencao)


class DreOut(BaseModel):
    aviso: str | None = None
    linhas: list[LinhaDreOut] = []


def _meta_out(meta) -> MetaOut:
    if meta is None:
        zero = Decimal(0)
        return MetaOut(definida=False, percentual_socios=zero, percentual_reserva=zero, percentual_expansao=zero, percentual_outros=zero, percentual_margem=zero)
    return MetaOut(
        definida=bool(meta.percentual_margem), percentual_socios=meta.percentual_socios, percentual_reserva=meta.percentual_reserva,
        percentual_expansao=meta.percentual_expansao, percentual_outros=meta.percentual_outros, percentual_margem=meta.percentual_margem,
    )


def _resultado_out(r: pp.Resultado) -> ResultadoOut:
    return ResultadoOut(
        preco=r.preco, impostos=r.impostos, custo=r.custo, despesas_variaveis=r.despesas_variaveis, margem_contribuicao=r.margem_contribuicao,
        despesas_fixas=r.despesas_fixas, resultado_operacional=r.resultado_operacional, destinacao=r.destinacao, resultado_final=r.resultado_final,
        destinos=r.destinos,
    )


def primaria_out(primaria: pp.Primaria | None, aviso: str | None, modo: str, pct_lucro: Decimal | None) -> PrimariaOut:
    """A lista de Precificação (da primária ou de um Centro de Resultados) a partir da precificação calculada."""
    saida = PrimariaOut(modo_impostos=modo, pct_lucro=pct_lucro, aviso=aviso)
    if primaria is None:
        return saida
    saida.avisos = primaria.avisos
    saida.linhas = [
        LinhaPrimariaOut(
            produto_codigo=l.produto.codigo, produto_nome=l.produto.nome, quantidade=l.quantidade, impostos=l.ao_sugerido.impostos,
            custo=l.ao_sugerido.custo, despesas_variaveis=l.ao_sugerido.despesas_variaveis, despesa_fixa=l.ao_sugerido.despesas_fixas,
            pct_lucro=l.composicao.pct_lucro, preco_sugerido=l.preco_sugerido, preco_definido=l.preco_definido, definido_manual=l.definido_manual,
            spread=l.spread, composicao=_composicao_out(l.composicao),
        )
        for l in primaria.linhas
    ]
    saida.desconto = primaria.linhas[0].desconto if primaria.linhas else Decimal(0)
    return saida


def analise_out(primaria: pp.Primaria | None, aviso: str | None) -> AnaliseOut:
    saida = AnaliseOut(aviso=aviso, rotulos_destinos=pp.ROTULO_ELEMENTO)
    if primaria is None:
        return saida
    saida.avisos = primaria.avisos
    saida.total_geral = _resultado_out(primaria.total_geral())
    saida.produtos = [
        ProdutoAnaliseOut(
            produto_codigo=l.produto.codigo, produto_nome=l.produto.nome, quantidade=l.quantidade,
            unitario=_resultado_out(l.ao_definido), total=_resultado_out(l.ao_definido.vezes(l.quantidade)),
        )
        for l in primaria.linhas
    ]
    return saida


def tabela_out(session: Session, primaria: pp.Primaria | None, aviso: str | None) -> TabelaOut:
    if primaria is None:
        return TabelaOut(aviso=aviso)
    aliquotas, linhas = pp.tabela_de_precos(session, primaria)
    return TabelaOut(
        aviso=aviso, faixas=list(aliquotas), rotulos=pp.ROTULO_FAIXA, aliquotas=aliquotas,
        linhas=[LinhaTabelaOut(produto_codigo=l.produto.codigo, produto_nome=l.produto.nome, precos=l.precos) for l in linhas],
    )


def _calcular(session: Session) -> tuple[pp.Primaria | None, str | None]:
    """A precificação primária, ou o motivo de não haver (sem Meta de Lucro etc.): a tela continua utilizável."""
    try:
        primaria = pp.calcular(session)
    except ValueError as exc:
        return None, str(exc)
    return primaria, primaria.aviso


# --- Meta de Lucro ------------------------------------------------------------------------------------------------------------------

@router.get("/precificacao/meta", response_model=MetaOut)
def obter_meta(session: Session = Depends(get_session)) -> MetaOut:
    return _meta_out(pp.obter_meta(session))


@router.put("/precificacao/meta", response_model=MetaOut)
def definir_meta(corpo: MetaIn, session: Session = Depends(get_session)) -> MetaOut:
    try:
        return _meta_out(pp.definir_meta(
            session, corpo.percentual_socios, corpo.percentual_reserva, corpo.percentual_expansao, corpo.percentual_outros,
        ))
    except PrimariaTravada as exc:
        raise HTTPException(status_code=409, detail=str(exc)) from exc
    except ValueError as exc:
        session.rollback()
        raise HTTPException(status_code=422, detail=str(exc)) from exc


# --- Precificação ------------------------------------------------------------------------------------------------------------------

@router.get("/precificacao/primaria", response_model=PrimariaOut)
def obter_primaria(session: Session = Depends(get_session)) -> PrimariaOut:
    meta = pp.obter_meta(session)
    primaria, aviso = _calcular(session)
    return primaria_out(primaria, aviso, motor_preco.obter_modo(session), meta.percentual_margem if meta else None)


@router.put("/precificacao/primaria/{produto}", status_code=204)
def definir_preco_definido(produto: str, corpo: PrecoDefinidoIn, session: Session = Depends(get_session)) -> None:
    valor = None
    if corpo.preco_definido is not None and corpo.preco_definido.strip():
        try:
            valor = Decimal(corpo.preco_definido.strip().replace(",", "."))
        except InvalidOperation as exc:
            raise HTTPException(status_code=422, detail="preço precisa ser um número") from exc
    try:
        pp.definir_preco_definido(session, produto, valor)
    except PrimariaTravada as exc:
        raise HTTPException(status_code=409, detail=str(exc)) from exc
    except ValueError as exc:
        session.rollback()
        raise HTTPException(status_code=404 if "não encontrado" in str(exc) else 422, detail=str(exc)) from exc


# --- Análise de Preço x Lucro ---------------------------------------------------------------------------------------------------------

@router.get("/precificacao/analise", response_model=AnaliseOut)
def obter_analise(session: Session = Depends(get_session)) -> AnaliseOut:
    primaria, aviso = _calcular(session)
    return analise_out(primaria, aviso)


# --- Tabela de Preços ---------------------------------------------------------------------------------------------------------------

def _tabela(session: Session) -> TabelaOut:
    primaria, aviso = _calcular(session)
    return tabela_out(session, primaria, aviso)


@router.get("/precificacao/tabela", response_model=TabelaOut)
def obter_tabela(session: Session = Depends(get_session)) -> TabelaOut:
    return _tabela(session)


@router.get("/precificacao/tabela.xlsx")
def baixar_tabela(session: Session = Depends(get_session)) -> Response:
    return xlsx_da_tabela(_tabela(session), "Tabela de Preços — precificação primária", "tabela-de-precos.xlsx")


def xlsx_da_tabela(tabela: TabelaOut, titulo: str, arquivo: str) -> Response:
    pasta = Workbook()
    folha = pasta.active
    folha.title = "Tabela de Preços"
    folha.append([titulo])
    folha["A1"].font = Font(bold=True, size=14)
    folha.append(["Preço de venda por unidade; só o ICMS de destino muda de uma coluna para outra."])
    folha.append([])
    cabecalho = ["Código", "Produto", *(f"{tabela.rotulos[f]} (ICMS {tabela.aliquotas[f] * 100:.2f}%)" for f in tabela.faixas)]
    folha.append(cabecalho)
    for celula in folha[4]:
        celula.font = Font(bold=True, color="FFFFFF")
        celula.fill = PatternFill("solid", fgColor="000000")
        celula.alignment = Alignment(horizontal="center", wrap_text=True)
    for linha in tabela.linhas:
        folha.append([linha.produto_codigo, linha.produto_nome, *(float(linha.precos[f]) for f in tabela.faixas)])
    for linha in folha.iter_rows(min_row=5, min_col=3):
        for celula in linha:
            celula.number_format = '"R$" #,##0.00'
    folha.column_dimensions["A"].width = 14
    folha.column_dimensions["B"].width = 44
    for coluna in "CDE":
        folha.column_dimensions[coluna].width = 26
    saida = BytesIO()
    pasta.save(saida)
    return Response(
        content=saida.getvalue(), media_type=XLSX, headers={"Content-Disposition": f'attachment; filename="{arquivo}"'},
    )


# --- DRE ---------------------------------------------------------------------------------------------------------------------------

@router.get("/precificacao/dre", response_model=DreOut)
def obter_dre(session: Session = Depends(get_session)) -> DreOut:
    primaria, aviso = _calcular(session)
    if primaria is None:
        return DreOut(aviso=aviso)
    return DreOut(
        aviso=aviso,
        linhas=[linha_dre_out(l) for l in pp.dre(session, primaria)],
    )
