"""Alocação dos elementos de custo (máquina, ambiente e mão de obra) por produto, em sequência na cadeia de produção.

Cada alocação é uma linha de roteiro de recurso (`RoteiroMaquina/Ambiente/MaoDeObra`): produto × elemento × **direcionador** (de
base tempo; a etapa é a atividade dele) × horas por bacia. O produto absorve custo/hora × horas que consome; as horas úteis
não usadas continuam sendo ociosidade → ineficiência (custeio e capacidade não mudam). A cadeia é a estrutura do TOTVS: o PI
tem as MPs mais os elementos que o transformam; o PA, os PIs, embalagens e MPs mais os seus elementos.
"""

from dataclasses import dataclass, field
from decimal import Decimal

from sqlalchemy import func
from sqlalchemy.orm import Session

from custos_precificacao import historico, unidades, vinculos
from custos_precificacao.entradas import Entradas
from custos_precificacao.models import Atividade, Direcionador, Produto, RoteiroInsumo

PROFUNDIDADE_MAXIMA = 12
TIPO_RECURSO_DE_ENTRADAS = {"maquina": "maquina", "ambiente": "ambiente", "mao-de-obra": "grupo"}
ROTULO_TIPO = {"mao-de-obra": "Mão de obra", "ambiente": "Instalação", "maquina": "Máquina", "outros": "Outros"}


@dataclass
class LinhaAlocacao:
    id: int
    tipo: str
    recurso_codigo: str
    recurso_nome: str
    direcionador_codigo: str | None
    direcionador_nome: str | None
    atividade_codigo: str
    atividade_nome: str
    horas_bacia: Decimal
    custo_hora: Decimal
    valor_bacia: Decimal
    valor_unitario: Decimal | None
    atualizado_em: str


@dataclass
class SubtotalDirecionador:
    direcionador_codigo: str | None
    direcionador_nome: str | None
    horas_bacia: Decimal
    valor_bacia: Decimal


@dataclass
class NoAlocacao:
    codigo: str
    nome: str
    tipo: str
    unidade: str
    origem: str
    aceita_elementos: bool
    base_quantidade: Decimal | None
    rendimento: Decimal | None
    nivel: int
    quantidade_consumida: Decimal | None = None  # quanto o pai consome deste intermediário (na unidade da linha)
    unidade_consumida: str | None = None
    n_insumos: int = 0  # insumos comprados (MP/EM) da formulação
    n_intermediarios: int = 0
    ciclo: bool = False
    linhas: list[LinhaAlocacao] = field(default_factory=list)
    subtotais: list[SubtotalDirecionador] = field(default_factory=list)
    total_bacia: Decimal = Decimal(0)
    total_unitario: Decimal | None = None
    filhos: list["NoAlocacao"] = field(default_factory=list)


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


def _linhas_do_produto(session: Session, produto: Produto, entradas: Entradas) -> tuple[list[LinhaAlocacao], Decimal | None]:
    rendimento_un, rendimento_kg = entradas.rendimentos(produto)
    rendimento = rendimento_un or rendimento_kg
    direcionadores = {d.id: d for d in session.query(Direcionador)}
    atividades = {a.id: a for a in session.query(Atividade)}
    resultado: list[LinhaAlocacao] = []
    for tipo, config in vinculos.ROTEIRO_RECURSOS.items():
        for linha in entradas.roteiro(produto, tipo):
            recurso = session.get(config.recurso_model, getattr(linha, config.campo_fk))
            custo_hora = entradas.custo_hora(TIPO_RECURSO_DE_ENTRADAS[tipo], recurso)
            valor = linha.tempo_horas * custo_hora
            tipo_exibido = "outros" if tipo == "maquina" and recurso.categoria == "outro" else tipo
            direcionador = direcionadores.get(getattr(linha, "direcionador_id", None))
            atividade = atividades.get(linha.atividade_id)
            resultado.append(LinhaAlocacao(
                id=linha.id, tipo=tipo_exibido, recurso_codigo=getattr(recurso, config.chave_recurso), recurso_nome=recurso.nome,
                direcionador_codigo=direcionador.codigo if direcionador else None,
                direcionador_nome=direcionador.nome if direcionador else None,
                atividade_codigo=atividade.codigo if atividade else "", atividade_nome=atividade.nome if atividade else "",
                horas_bacia=linha.tempo_horas, custo_hora=custo_hora, valor_bacia=valor,
                valor_unitario=(valor / rendimento) if rendimento else None, atualizado_em=_mes(linha.atualizado_em),
            ))
    resultado.sort(key=lambda l: (l.atividade_codigo, l.tipo, l.recurso_codigo, l.id))
    return resultado, rendimento


def _no(session: Session, produto: Produto, entradas: Entradas, nivel: int, caminho: tuple[int, ...], consumo: tuple[Decimal, str] | None) -> NoAlocacao:
    linhas, rendimento = _linhas_do_produto(session, produto, entradas)
    de_insumo = session.query(RoteiroInsumo).filter_by(produto_id=produto.id).order_by(RoteiroInsumo.ordem, RoteiroInsumo.id).all()
    no = NoAlocacao(
        codigo=produto.codigo, nome=produto.nome, tipo=produto.tipo, unidade=unidades.normalizar(produto.unidade), origem=produto.origem or "manual",
        aceita_elementos=produto.tipo != "revenda", base_quantidade=produto.base_quantidade, rendimento=rendimento, nivel=nivel,
        quantidade_consumida=consumo[0] if consumo else None, unidade_consumida=consumo[1] if consumo else None,
        n_insumos=sum(1 for l in de_insumo if l.insumo_id is not None), n_intermediarios=sum(1 for l in de_insumo if l.insumo_produto_id is not None),
        linhas=linhas,
    )
    por_direcionador: dict[str | None, SubtotalDirecionador] = {}
    for l in linhas:
        sub = por_direcionador.setdefault(l.direcionador_codigo, SubtotalDirecionador(l.direcionador_codigo, l.direcionador_nome, Decimal(0), Decimal(0)))
        sub.horas_bacia += l.horas_bacia
        sub.valor_bacia += l.valor_bacia
    no.subtotais = sorted(por_direcionador.values(), key=lambda s: (s.direcionador_codigo is None, s.direcionador_codigo or ""))
    no.total_bacia = sum((l.valor_bacia for l in linhas), Decimal(0))
    no.total_unitario = (no.total_bacia / rendimento) if rendimento else None

    if nivel < PROFUNDIDADE_MAXIMA:
        vistos: set[int] = set()
        for linha in de_insumo:
            if linha.insumo_produto_id is None or linha.insumo_produto_id in vistos:
                continue
            vistos.add(linha.insumo_produto_id)
            filho = session.get(Produto, linha.insumo_produto_id)
            consumo_filho = (Decimal(linha.quantidade_kg), unidades.normalizar(linha.unidade))
            if filho.id in caminho or filho.id == produto.id:
                no.filhos.append(NoAlocacao(
                    codigo=filho.codigo, nome=filho.nome, tipo=filho.tipo, unidade=unidades.normalizar(filho.unidade), origem=filho.origem or "manual",
                    aceita_elementos=False, base_quantidade=filho.base_quantidade, rendimento=None, nivel=nivel + 1, ciclo=True,
                    quantidade_consumida=consumo_filho[0], unidade_consumida=consumo_filho[1],
                ))
                continue
            no.filhos.append(_no(session, filho, entradas, nivel + 1, caminho + (produto.id,), consumo_filho))
    return no


def montar_cadeia(session: Session, codigo: str) -> NoAlocacao:
    """O produto e, recursivamente, os intermediários que ele consome, cada um com os seus elementos de custo e o custo deles."""
    produto = session.query(Produto).filter_by(codigo=codigo).one_or_none()
    if produto is None:
        raise ValueError(f"Produto {codigo!r} não encontrado")
    return _no(session, produto, Entradas(session), 0, (), None)


# --------------------------------------------------------------------------- lista de produtos


def listar_produtos(session: Session, *, busca: str = "", tipo: str = "", so_sem_alocacao: bool = False) -> list[dict]:
    contagem: dict[int, int] = {}
    for config in vinculos.ROTEIRO_RECURSOS.values():
        modelo = config.roteiro_model
        for produto_id, n in session.query(modelo.produto_id, func.count(modelo.id)).group_by(modelo.produto_id):
            contagem[produto_id] = contagem.get(produto_id, 0) + n
    termo = busca.strip().lower()
    itens = []
    for p in session.query(Produto).order_by(Produto.codigo):
        if termo and termo not in p.codigo.lower() and termo not in p.nome.lower():
            continue
        if tipo and p.tipo != tipo:
            continue
        n = contagem.get(p.id, 0)
        sem = p.tipo != "revenda" and n == 0
        if so_sem_alocacao and not sem:
            continue
        itens.append({
            "codigo": p.codigo, "nome": p.nome, "tipo": p.tipo, "origem": p.origem or "manual", "unidade": unidades.normalizar(p.unidade),
            "linhas_elemento": n, "sem_alocacao": sem, "aceita_elementos": p.tipo != "revenda",
        })
    return itens


# --------------------------------------------------------------------------- copiar / aplicar a vários


@dataclass
class ResultadoCopia:
    criadas: int = 0
    puladas: int = 0
    removidas: int = 0


def _linhas_de_elemento(session: Session, produto: Produto) -> list[tuple[str, object]]:
    return [
        (tipo, linha)
        for tipo, config in vinculos.ROTEIRO_RECURSOS.items()
        for linha in session.query(config.roteiro_model).filter_by(produto_id=produto.id).order_by(config.roteiro_model.id)
    ]


def _copiar_para(session: Session, origem: Produto, destino: Produto, substituir: bool, mes, resultado: ResultadoCopia) -> None:
    if destino.tipo == "revenda":
        raise ValueError(f"O produto de revenda {destino.codigo!r} não recebe elementos de custo")
    if destino.id == origem.id:
        raise ValueError("Origem e destino são o mesmo produto")
    if substituir:
        for _, linha in _linhas_de_elemento(session, destino):
            historico.remover_com_historico(session, linha, mes, commit=False)
            resultado.removidas += 1
        session.flush()
    existentes = {(tipo, getattr(l, vinculos.ROTEIRO_RECURSOS[tipo].campo_fk), l.direcionador_id) for tipo, l in _linhas_de_elemento(session, destino)}
    for tipo, linha in _linhas_de_elemento(session, origem):
        config = vinculos.ROTEIRO_RECURSOS[tipo]
        chave = (tipo, getattr(linha, config.campo_fk), linha.direcionador_id)
        if chave in existentes:
            resultado.puladas += 1
            continue
        existentes.add(chave)
        session.add(config.roteiro_model(
            produto_id=destino.id, atividade_id=linha.atividade_id, direcionador_id=linha.direcionador_id, ordem=linha.ordem,
            tempo_horas=linha.tempo_horas, atualizado_em=mes, **{config.campo_fk: getattr(linha, config.campo_fk)},
        ))
        resultado.criadas += 1


def copiar_elementos(
    session: Session, origem_codigo: str, destinos: list[str], *, substituir: bool = False, atualizado_em: str | None = None
) -> ResultadoCopia:
    """Copia os elementos de custo de `origem` para um ou vários produtos, valendo de `atualizado_em`. Tudo-ou-nada.
    Sem `substituir`, o que o destino já tem (mesmo elemento e direcionador) é mantido; com ele, as linhas atuais do destino são
    encerradas (histórico) antes."""
    mes = historico.normalizar_mes(atualizado_em)
    origem = session.query(Produto).filter_by(codigo=origem_codigo).one_or_none()
    if origem is None:
        raise ValueError(f"Produto de origem {origem_codigo!r} não encontrado")
    if not destinos:
        raise ValueError("Informe ao menos um produto de destino")
    resultado = ResultadoCopia()
    try:
        for codigo in dict.fromkeys(destinos):
            destino = session.query(Produto).filter_by(codigo=codigo).one_or_none()
            if destino is None:
                raise ValueError(f"Produto de destino {codigo!r} não encontrado")
            _copiar_para(session, origem, destino, substituir, mes, resultado)
        session.commit()
    except Exception:
        session.rollback()
        raise
    return resultado
