"""Ingestão do cadastro de produtos do TOTVS (aba `Totvs_Cad_Prod` da NDC): Código, Descrição, Unidade, Armazém e Grupo.

Só **atualiza** o que já existe no Planejamento e veio do TOTVS (a estrutura é que cria os itens): o nome (Descrição) dos
insumos e produtos de origem TOTVS. A unidade **não** é trocada (as linhas de formulação dependem dela): se o cadastro e a
estrutura divergem, vira aviso. Itens manuais não são alterados. Os itens do cadastro que parecem fazer parte da produção
(PA, PI, MP, EMB, RV) e ainda não existem aqui — porque nenhuma estrutura os usa — são listados como "sem estrutura".
"""

import re
from dataclasses import dataclass, field
from datetime import date
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, _norm, _unidade_normal

ABA_CADASTRO = "totvs_cad_prod"
PADRAO_PRODUCAO = re.compile(r"^(PA|PI|MP|EMB|RV)\d", re.IGNORECASE)


@dataclass
class ItemCadastro:
    codigo: str
    descricao: str
    unidade: str
    grupo: str


def ler_cadastro(dados: bytes) -> dict[str, ItemCadastro]:
    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_Cad_Prod.") from exc
    folha = next((f for f in livro.worksheets if _norm(f.title).replace(" ", "_") == ABA_CADASTRO), None)
    if folha is None:
        raise ErroDeRelatorio(f"Aba Totvs_Cad_Prod não encontrada (abas: {', '.join(f.title for f in livro.worksheets)})")

    colunas: dict[str, int] | None = None
    itens: dict[str, ItemCadastro] = {}
    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 "descricao" in nomes:
                colunas = {"codigo": nomes["codigo"], "descricao": nomes["descricao"], "unidade": nomes.get("unidade", -1), "grupo": nomes.get("grupo", -1)}
            elif numero >= 15:
                break
            continue

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

        codigo = str(cel("codigo") or "").strip()
        if codigo:
            itens[codigo] = ItemCadastro(codigo, str(cel("descricao") or "").strip(), str(cel("unidade") or "").strip().upper(), str(cel("grupo") or "").strip())
    if colunas is None:
        raise ErroDeRelatorio("Cabeçalho não encontrado na aba Totvs_Cad_Prod (precisa de Codigo e Descricao)")
    return itens


@dataclass
class PlanoCadastro:
    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)  # itens de produção do cadastro que ainda não existem aqui (sem estrutura)
    sem_custo: list[str] = field(default_factory=list)
    pais: int = 0
    linhas_no_relatorio: int = 0
    ops: list[tuple[str, int, str]] = field(default_factory=list)  # (insumo|produto, id, novo nome)
    total_sem_estrutura: int = 0

    @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_cadastro(session: Session, itens: dict[str, ItemCadastro], vigencia: date | str | None = None) -> PlanoCadastro:
    plano = PlanoCadastro(vigencia=historico.normalizar_mes(vigencia), linhas_no_relatorio=len(itens))
    existentes: dict[str, tuple[str, Insumo | Produto]] = {i.codigo: ("insumo", i) for i in session.query(Insumo)}
    existentes.update({p.codigo: ("produto", p) for p in session.query(Produto)})
    if not existentes:
        plano.avisos.append((None, "Não há insumos nem produtos neste Planejamento: importe antes a estrutura do TOTVS"))

    divergentes = []
    for codigo, (objeto, item) in sorted(existentes.items()):
        rotulo = "insumos" if objeto == "insumo" else "produtos"
        cad = itens.get(codigo)
        if cad is None:
            plano.contar(rotulo, "fora_do_cadastro")
            continue
        if item.origem != "totvs":
            plano.contar(rotulo, "manual_nao_alterado")
            continue
        limite = 200 if objeto == "insumo" else 300
        novo = cad.descricao[:limite]
        if cad.unidade and _unidade_normal(cad.unidade) != unidades.normalizar(item.unidade):
            divergentes.append(f"{codigo}: unidade {cad.unidade} no cadastro e {item.unidade.upper()} na estrutura (mantida a da estrutura)")
        if novo and novo != item.nome:
            plano.ops.append((objeto, item.id, novo))
            plano.mudancas.append(Mudanca(objeto, "alterar", codigo, f"nome: {item.nome[:40]} → {novo[:40]}"))
            plano.contar(rotulo, "alterar")
        else:
            plano.contar(rotulo, "igual")
    for texto in divergentes[:LIMITE_LISTA]:
        plano.avisos.append((texto.split(":")[0], texto))
    if len(divergentes) > LIMITE_LISTA:
        plano.avisos.append((None, f"… e mais {len(divergentes) - LIMITE_LISTA} unidades divergentes"))

    sem_estrutura = [c for c in itens.values() if c.codigo not in existentes and PADRAO_PRODUCAO.match(c.codigo)]
    plano.total_sem_estrutura = len(sem_estrutura)
    plano.nao_vieram = [f"{c.codigo} — {c.descricao}" for c in sorted(sem_estrutura, key=lambda c: c.codigo)][:LIMITE_LISTA]
    if sem_estrutura:
        plano.avisos.append((None, f"{len(sem_estrutura)} item(ns) de produção do cadastro (PA/PI/MP/EMB/RV) ainda não têm estrutura neste Planejamento"))
    return plano


def aplicar_cadastro(session: Session, plano: PlanoCadastro, *, 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, nome in plano.ops:
            session.get(Insumo if objeto == "insumo" else Produto, item_id).nome = nome
        carga = TotvsCarga(
            fonte="produtos", arquivo=arquivo, sha256=sha256, vigencia=plano.vigencia, carregado_por=usuario,
            resumo={"contagem": plano.contagem, "itens_no_cadastro": plano.linhas_no_relatorio, "sem_estrutura": plano.total_sem_estrutura},
        )
        session.add(carga)
        session.commit()
        return carga
    except Exception:
        session.rollback()
        raise
