"""Ingestão da NDC (custos dos itens do TOTVS): preço de compra dos Insumos e custo de compra dos produtos de Revenda.

A planilha NDC_PRIME traz, na aba `Totvs_Custo`, `Codigo`, `Descricao`, `Unidade`, `Custo Stand.` e `Ult. Preco`. Aqui só se
**atualiza** o que já existe no Planejamento (a NDC não cria itens; a estrutura do TOTVS é que cria): o custo é o `Ult. Preco`
e, se ele for zero, o `Custo Stand.`. O novo valor entra com histórico na vigência escolhida (mesma regra das demais telas);
frete, IPI, ICMS e imposto do insumo ficam como estão. Item sem custo na NDC (ou zerado) não muda e vai para a lista "sem custo".
"""

from dataclasses import dataclass, field
from datetime import date
from decimal import Decimal
from io import BytesIO
from typing import Any

from sqlalchemy.orm import Session

from custos_precificacao import historico, unidades
from custos_precificacao.models import Insumo, Produto, TotvsCarga
from custos_precificacao.totvs_estrutura import LIMITE_BYTES, LIMITE_LISTA, ErroDeRelatorio, Mudanca, _dec, _norm, _q6, _unidade_normal

ABA_CUSTO = "totvs_custo"


@dataclass
class CustoNdc:
    codigo: str
    descricao: str
    unidade: str
    custo_padrao: Decimal
    ultimo_preco: Decimal

    @property
    def valor(self) -> tuple[Decimal, str]:
        """(custo a usar, de onde veio): o último preço; sem ele, o custo padrão; sem nenhum, zero."""
        if self.ultimo_preco > 0:
            return self.ultimo_preco, "último preço"
        if self.custo_padrao > 0:
            return self.custo_padrao, "custo padrão"
        return Decimal(0), "sem custo"


@dataclass
class RelatorioNdc:
    itens: dict[str, CustoNdc]
    repetidos: int = 0


def ler_ndc(dados: bytes) -> RelatorioNdc:
    from openpyxl import load_workbook

    if len(dados) > LIMITE_BYTES:
        raise ErroDeRelatorio(f"Arquivo grande demais; o limite é {LIMITE_BYTES // 1024 // 1024} MB")
    try:
        livro = load_workbook(BytesIO(dados), read_only=True, data_only=True)
    except Exception as exc:
        raise ErroDeRelatorio("Não foi possível abrir a planilha. Use a NDC (.xlsx) com a aba Totvs_Custo.") from exc
    folha = next((f for f in livro.worksheets if _norm(f.title).replace(" ", "_") == ABA_CUSTO), None)
    if folha is None:
        raise ErroDeRelatorio(f"Aba Totvs_Custo não encontrada (abas: {', '.join(f.title for f in livro.worksheets)})")

    colunas: dict[str, int] | None = None
    itens: dict[str, CustoNdc] = {}
    repetidos = 0
    for numero, linha in enumerate(folha.iter_rows(values_only=True), start=1):
        if colunas is None:
            nomes = {_norm(c): i for i, c in enumerate(linha) if c is not None}
            if "codigo" in nomes and "ult. preco" in nomes:
                colunas = {
                    "codigo": nomes["codigo"], "descricao": nomes.get("descricao", -1), "unidade": nomes.get("unidade", -1),
                    "padrao": nomes.get("custo stand.", nomes.get("custo stand", -1)), "ultimo": nomes["ult. preco"],
                }
            elif numero >= 15:
                break
            continue
        codigo = str(linha[colunas["codigo"]]).strip() if linha[colunas["codigo"]] is not None else ""
        if not codigo:
            continue

        def cel(chave: str) -> Any:
            i = colunas[chave]
            return linha[i] if 0 <= i < len(linha) else None

        item = CustoNdc(
            codigo=codigo, descricao=str(cel("descricao") or "").strip(), unidade=str(cel("unidade") or "").strip().upper(),
            custo_padrao=_dec(cel("padrao"), "Custo Stand.", numero, obrigatorio=False) or Decimal(0),
            ultimo_preco=_dec(cel("ultimo"), "Ult. Preco", numero, obrigatorio=False) or Decimal(0),
        )
        if codigo in itens:
            repetidos += 1
        itens[codigo] = item
    if colunas is None:
        raise ErroDeRelatorio("Cabeçalho não encontrado na aba Totvs_Custo (precisa de Codigo e Ult. Preco)")
    return RelatorioNdc(itens=itens, repetidos=repetidos)


@dataclass
class PlanoCustos:
    vigencia: date
    emitido_em: date | None = None
    mudancas: list[Mudanca] = field(default_factory=list)
    avisos: list[tuple[str | None, str]] = field(default_factory=list)
    erros: list[tuple[str | None, str]] = field(default_factory=list)
    contagem: dict[str, dict[str, int]] = field(default_factory=dict)
    nao_vieram: list[str] = field(default_factory=list)
    sem_custo: list[str] = field(default_factory=list)  # "COD — nome": itens do sistema sem custo utilizável na NDC
    pais: int = 0
    linhas_no_relatorio: int = 0
    ops: list[tuple[str, int, Decimal]] = field(default_factory=list)  # (insumo|produto, id, novo custo)

    @property
    def ok(self) -> bool:
        return not self.erros

    def contar(self, objeto: str, acao: str) -> None:
        self.contagem.setdefault(objeto, {}).setdefault(acao, 0)
        self.contagem[objeto][acao] += 1


def planejar_custos(session: Session, relatorio: RelatorioNdc, vigencia: date | str | None = None) -> PlanoCustos:
    mes = historico.normalizar_mes(vigencia)
    plano = PlanoCustos(vigencia=mes, linhas_no_relatorio=len(relatorio.itens))
    if relatorio.repetidos:
        plano.avisos.append((None, f"{relatorio.repetidos} código(s) repetido(s) na NDC: vale a última ocorrência"))
    insumos = list(session.query(Insumo).order_by(Insumo.codigo))
    revendas = list(session.query(Produto).filter_by(tipo="revenda").order_by(Produto.codigo))
    if not insumos and not revendas:
        plano.avisos.append((None, "Não há insumos nem produtos de revenda neste Planejamento: importe antes a estrutura do TOTVS"))
        return plano

    fora: list[str] = []
    itens = [("insumo", i, i.valor_compra_unitario) for i in insumos] + [("produto", p, p.custo_compra) for p in revendas]
    for objeto, item, atual in itens:
        rotulo = "insumos" if objeto == "insumo" else "produtos"
        ndc = relatorio.itens.get(item.codigo)
        if ndc is None:
            fora.append(item.codigo)
            plano.sem_custo.append(f"{item.codigo} — {item.nome}")
            plano.contar(rotulo, "fora_da_ndc")
            continue
        custo, origem = ndc.valor
        if custo <= 0:
            plano.sem_custo.append(f"{item.codigo} — {item.nome}")
            plano.contar(rotulo, "sem_custo")
            continue
        if _unidade_normal(ndc.unidade) != unidades.normalizar(item.unidade):
            plano.avisos.append((item.codigo, f"{item.codigo}: unidade {ndc.unidade} na NDC e {item.unidade.upper()} no cadastro — custo não atualizado"))
            plano.contar(rotulo, "unidade_diferente")
            continue
        novo = _q6(custo)
        if atual is not None and _q6(atual) == novo:
            plano.contar(rotulo, "igual")
            continue
        if mes < item.atualizado_em:
            plano.erros.append((item.codigo, f"{item.codigo}: a vigência {mes:%m/%Y} é anterior à versão atual do cadastro ({item.atualizado_em:%m/%Y})"))
            continue
        plano.ops.append((objeto, item.id, novo))
        antes = "sem custo" if atual is None else f"R$ {_q6(atual):f}"
        plano.mudancas.append(Mudanca(objeto, "alterar", item.codigo, f"{antes} → R$ {novo:f} ({origem})"))
        plano.contar(rotulo, "alterar")
    if fora:
        plano.avisos.append((None, f"{len(fora)} item(ns) do sistema não estão na NDC (ficam sem custo): {', '.join(fora[:10])}{'…' if len(fora) > 10 else ''}"))
    plano.nao_vieram = fora[:LIMITE_LISTA]
    sem_custo_ndc = sum(n.get("sem_custo", 0) for n in plano.contagem.values())
    if sem_custo_ndc:
        plano.avisos.append((None, f"{sem_custo_ndc} item(ns) sem custo (ou zerado) na NDC: nada foi alterado neles"))
    return plano


def aplicar_custos(
    session: Session, plano: PlanoCustos, *, arquivo: str | None = None, sha256: str = "", usuario: str | None = None
) -> TotvsCarga:
    if plano.erros:
        raise ValueError("O plano tem erros; corrija antes de aplicar")
    try:
        for objeto, item_id, novo in plano.ops:
            if objeto == "insumo":
                historico.atualizar_com_historico(session, session.get(Insumo, item_id), {"valor_compra_unitario": novo}, plano.vigencia, commit=False)
            else:
                historico.atualizar_com_historico(session, session.get(Produto, item_id), {"custo_compra": novo}, plano.vigencia, commit=False)
        carga = TotvsCarga(
            fonte="ndc", arquivo=arquivo, sha256=sha256, emitido_em=None, vigencia=plano.vigencia, carregado_por=usuario,
            resumo={"contagem": plano.contagem, "itens_na_ndc": plano.linhas_no_relatorio, "sem_custo": len(plano.sem_custo)},
        )
        session.add(carga)
        session.commit()
        return carga
    except Exception:
        session.rollback()
        raise
