"""Grupos de mão de obra Depto-Cargo: os colaboradores se agrupam por Centro de Custo (departamento), cargo e classificação.

O grupo é o recurso de mão de obra que se **aloca** aos produtos e cuja capacidade operacional e ociosidade se medem (ver
`models.GrupoMaoDeObra`). Ele é formado automaticamente: sempre que um colaborador é criado ou muda de Centro de Custo, de cargo
ou de classificação, o sistema acha — ou cria — o grupo e o catálogo de cargos (`before_flush`), seja pela API, pela importação,
pela linha de comando ou por um teste que grave `Funcionario` direto.
"""

from sqlalchemy import event
from sqlalchemy.orm import Session

from custos_precificacao.models import Cargo, CentroDeCusto, Funcionario, GrupoMaoDeObra

SEM_CARGO = "(sem cargo)"
CLASSIFICACOES = {"fixo": "F", "variavel": "V"}


def _nome_do_cargo(texto: str | None) -> str:
    return " ".join((texto or "").split()) or SEM_CARGO


def _pendentes(session: Session, tipo: type) -> list:
    return [o for o in session.new if isinstance(o, tipo)]


def obter_ou_criar_cargo(session: Session, nome: str | None) -> Cargo:
    """Cargo do catálogo pelo nome (sem diferenciar maiúsculas); cria com o próximo código `CGnn` se não existir."""
    nome = _nome_do_cargo(nome)
    chave = nome.casefold()
    for cargo in _pendentes(session, Cargo):
        if cargo.nome.casefold() == chave:
            return cargo
    with session.no_autoflush:
        existentes = session.query(Cargo).all()
    for cargo in existentes:
        if cargo.nome.casefold() == chave:
            return cargo
    numeros = [int(c.codigo[2:]) for c in [*existentes, *_pendentes(session, Cargo)] if c.codigo.startswith("CG") and c.codigo[2:].isdigit()]
    cargo = Cargo(codigo=f"CG{(max(numeros) + 1 if numeros else 1):02d}", nome=nome)
    session.add(cargo)
    return cargo


def obter_ou_criar_grupo(session: Session, centro_de_custo_id: int, cargo: Cargo, classificacao: str) -> GrupoMaoDeObra:
    """Grupo Depto-Cargo-classificação; cria com o código `{depto}-{cargo}-{F|V}` se ainda não existe."""
    for grupo in _pendentes(session, GrupoMaoDeObra):
        if grupo.centro_de_custo_id == centro_de_custo_id and grupo.cargo is cargo and grupo.classificacao == classificacao:
            return grupo
    if cargo.id is not None:
        with session.no_autoflush:
            existente = session.query(GrupoMaoDeObra).filter_by(centro_de_custo_id=centro_de_custo_id, cargo_id=cargo.id, classificacao=classificacao).one_or_none()
        if existente is not None:
            return existente
    with session.no_autoflush:
        centro = session.get(CentroDeCusto, centro_de_custo_id)
    codigo = f"{centro.codigo if centro else centro_de_custo_id}-{cargo.codigo}-{CLASSIFICACOES[classificacao]}"
    grupo = GrupoMaoDeObra(codigo=codigo, centro_de_custo_id=centro_de_custo_id, cargo=cargo, classificacao=classificacao)
    session.add(grupo)
    return grupo


def atribuir_grupo(session: Session, funcionario: Funcionario) -> None:
    """Põe o colaborador no grupo do seu Centro de Custo, cargo e classificação (e mantém o texto do cargo igual ao do catálogo)."""
    if funcionario.centro_de_custo_id is None or funcionario.classificacao not in CLASSIFICACOES:
        return
    cargo = obter_ou_criar_cargo(session, funcionario.cargo)
    grupo = obter_ou_criar_grupo(session, funcionario.centro_de_custo_id, cargo, funcionario.classificacao)
    if funcionario.grupo is not grupo:
        funcionario.grupo = grupo
    if funcionario.cargo != cargo.nome:
        funcionario.cargo = cargo.nome
    if funcionario.cargo_catalogo is not cargo:
        funcionario.cargo_catalogo = cargo


def _confere(funcionario: Funcionario) -> bool:
    grupo = funcionario.grupo
    return (
        grupo is not None
        and grupo.centro_de_custo_id == funcionario.centro_de_custo_id
        and grupo.classificacao == funcionario.classificacao
        and grupo.cargo is not None
        and grupo.cargo.nome == _nome_do_cargo(funcionario.cargo)
    )


@event.listens_for(Session, "before_flush")
def _formar_grupos(session: Session, _contexto, _instancias) -> None:
    for obj in [*session.new, *session.dirty]:
        if isinstance(obj, Funcionario) and not _confere(obj):
            atribuir_grupo(session, obj)


def grupo_do_codigo(session: Session, codigo: str) -> GrupoMaoDeObra | None:
    """Grupo pelo código; aceita também o código FOPAG de um colaborador (devolve o grupo dele) — o jeito antigo de alocar."""
    grupo = session.query(GrupoMaoDeObra).filter_by(codigo=codigo).one_or_none()
    if grupo is not None:
        return grupo
    pessoa = session.query(Funcionario).filter_by(codigo_fopag=codigo).one_or_none()
    return pessoa.grupo if pessoa is not None else None
