"""Gastos fixos do recurso: os campos do formulário "Alterar gastos" (Elementos de Custos e Capacidades).

Vale para Funcionário (a folha: 8 campos), Máquina (5) e Ambiente (7). Cada campo é uma linha de `FuncionarioGasto` /
`MaquinaGasto` / `AmbienteGasto` amarrada a uma conta fixa do Plano de Contas — o custo mensal, o custo/hora, o histórico
"Atualizado em", a autoria e a importação por planilha seguem funcionando como para qualquer gasto; o formulário é só uma
visão estruturada dessas linhas. Gastos de outras contas aparecem como "outros" (somente leitura) e continuam somando no custo.
"""

from dataclasses import dataclass
from datetime import date
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation

from sqlalchemy.orm import Session

from custos_precificacao import arvore, historico, plano_padrao
from custos_precificacao.models import (
    Ambiente,
    AmbienteGasto,
    Funcionario,
    FuncionarioGasto,
    Maquina,
    MaquinaGasto,
    PlanoDeContas,
)

_CASAS = Decimal("0.000001")
_MAXIMO = Decimal("100000000")
CONTA_PAI = "PC-CUSTOS-PESSOAS"


@dataclass(frozen=True)
class CampoFolha:
    chave: str
    rotulo: str
    conta_codigo: str  # CHAVE estável da conta no Plano de Contas (p.ex. PC-PESSOAS-SALARIOS), não o código da árvore
    conta_nome: str
    coluna: str = "desembolso"  # 'desembolso' | 'retencao'


@dataclass(frozen=True)
class GrupoGastos:
    tipo: str  # 'funcionarios' | 'maquinas' | 'ambientes'
    conta_pai: str
    campos: tuple[CampoFolha, ...]
    modelo: type  # linha de gasto
    fk: str  # coluna da linha que aponta para o recurso


CAMPOS_FOLHA: tuple[CampoFolha, ...] = (
    CampoFolha("salario_bruto", "Salário Bruto", "PC-PESSOAS-SALARIOS", "Salários e Ordenados"),
    CampoFolha("outras_remuneracoes", "Outras Remunerações", "PC-PESSOAS-OUTREM", "Outras Remunerações"),
    CampoFolha("alimentacao", "Alimentação", "PC-PESSOAS-ALIM", "Alimentação"),
    CampoFolha("transporte", "Transporte", "PC-PESSOAS-TRANSP", "Transporte"),
    CampoFolha("assistencia_medica", "Assistência Médica", "PC-PESSOAS-ASSMED", "Assistência Médica"),
    CampoFolha("fgts", "FGTS Mensal", "PC-PESSOAS-FGTS", "FGTS"),
    CampoFolha("inss", "INSS Mensal", "PC-PESSOAS-INSS", "INSS"),
    CampoFolha("provisoes", "Provisões Sobre Folha (Retenções)", "PC-PESSOAS-PROVIS", "Provisões sobre Folha", "retencao"),
)

CAMPOS_MAQUINA: tuple[CampoFolha, ...] = (
    CampoFolha("depreciacao", "Depreciação (Retenção)", "PC-MAQEQ-DEPREC", "Depreciação", "retencao"),
    CampoFolha("manutencao", "Manutenção", "PC-MAQEQ-MANUT", "Manutenção"),
    CampoFolha("energia", "Energia", "PC-MAQEQ-ENERGIA", "Energia"),
    CampoFolha("seguro", "Seguro", "PC-MAQEQ-SEGURO", "Seguro"),
    CampoFolha("outros", "Outros", "PC-MAQEQ-OUTROS", "Outros Custos de Máquina"),
)

CAMPOS_AMBIENTE: tuple[CampoFolha, ...] = (
    CampoFolha("aluguel", "Aluguel", "PC-INSTAL-ALUGUEL", "Aluguel"),
    CampoFolha("energia", "Energia", "PC-INSTAL-ENERGIA", "Energia"),
    CampoFolha("agua", "Água", "PC-INSTAL-AGUA", "Água"),
    CampoFolha("manutencao_predial", "Manutenção Predial", "PC-INSTAL-MANUT", "Manutenção Predial"),
    CampoFolha("iptu_condominio", "IPTU / Condomínio", "PC-INSTAL-IPTU", "IPTU e Condomínio"),
    CampoFolha("limpeza_seguranca", "Limpeza e Segurança", "PC-INSTAL-LIMPSEG", "Limpeza e Segurança"),
    CampoFolha("depreciacao", "Depreciação (Retenção)", "PC-INSTAL-DEPREC", "Depreciação", "retencao"),
)

GRUPOS: dict[str, GrupoGastos] = {
    "funcionarios": GrupoGastos("funcionarios", CONTA_PAI, CAMPOS_FOLHA, FuncionarioGasto, "funcionario_id"),
    "maquinas": GrupoGastos("maquinas", "PC-CUSTOS-MAQEQUIP", CAMPOS_MAQUINA, MaquinaGasto, "maquina_id"),
    "ambientes": GrupoGastos("ambientes", "PC-CUSTOS-INSTAL", CAMPOS_AMBIENTE, AmbienteGasto, "ambiente_id"),
}
CAMPOS_POR_CHAVE = {c.chave: c for c in CAMPOS_FOLHA}

Recurso = Funcionario | Maquina | Ambiente


def grupo_de(recurso: Recurso) -> GrupoGastos:
    if isinstance(recurso, Funcionario):
        return GRUPOS["funcionarios"]
    if isinstance(recurso, Maquina):
        return GRUPOS["maquinas"]
    if isinstance(recurso, Ambiente):
        return GRUPOS["ambientes"]
    raise TypeError(f"Recurso sem gastos fixos: {type(recurso).__name__}")


def garantir_contas(session: Session, tipo: str = "funcionarios") -> int:
    """Cria (com `flush`, sem `commit`) as contas dos campos do grupo (e ancestrais) que ainda não existem. Idempotente.
    Devolve quantas criou."""
    return sum(plano_padrao.garantir_conta(session, campo.conta_codigo)[1] for campo in GRUPOS[tipo].campos)


def garantir_todas_as_contas(session: Session) -> int:
    """`garantir_contas` dos três grupos (Funcionário, Máquina, Ambiente)."""
    return sum(garantir_contas(session, tipo) for tipo in GRUPOS)


@dataclass
class ValorFolha:
    campo: CampoFolha
    valor: Decimal
    linha: object | None  # linha de gasto; None = ainda sem linha (valor 0)


@dataclass
class Folha:
    campos: list[ValorFolha]
    outros: list  # gastos fora dos campos (ou repetidos na mesma conta, ou com valor na outra coluna)

    @property
    def total(self) -> Decimal:
        return sum((c.valor for c in self.campos), Decimal(0)) + sum((g.total for g in self.outros), Decimal(0))


def _outra_coluna(campo: CampoFolha) -> str:
    return "desembolso" if campo.coluna == "retencao" else "retencao"


def ler_folha(session: Session, recurso: Recurso) -> Folha:
    """Valor vigente de cada campo. A linha mais antiga da conta é o campo, desde que só use a coluna do campo;
    repetidas, de outras contas ou com valor na outra coluna viram 'outros' (somente leitura, somam no custo)."""
    grupo = grupo_de(recurso)
    contas = {c.id: c.chave for c in session.query(PlanoDeContas)}  # id -> chave estável
    por_conta = {campo.conta_codigo: campo for campo in grupo.campos}
    linhas: dict[str, object] = {}
    outros: list = []
    for gasto in sorted(recurso.gastos, key=lambda g: g.id):
        codigo = contas.get(gasto.plano_de_contas_id)
        campo = por_conta.get(codigo)
        if campo is not None and codigo not in linhas and getattr(gasto, _outra_coluna(campo)) == 0:
            linhas[codigo] = gasto
        else:
            outros.append(gasto)
    campos = []
    for campo in grupo.campos:
        linha = linhas.get(campo.conta_codigo)
        campos.append(ValorFolha(campo, getattr(linha, campo.coluna) if linha else Decimal(0), linha))
    return Folha(campos, outros)


def _valor(rotulo: str, bruto) -> Decimal | None:
    """None/vazio = não mexe; senão número >= 0 (aceita vírgula)."""
    if bruto is None or (isinstance(bruto, str) and not bruto.strip()):
        return None
    texto = str(bruto).strip().replace("R$", "").replace(" ", "")
    if "," in texto:
        texto = texto.replace(".", "").replace(",", ".")
    try:
        valor = Decimal(texto)
    except InvalidOperation as exc:
        raise ValueError(f"{rotulo}: {bruto!r} não é um número") from exc
    if not valor.is_finite() or valor < 0:
        raise ValueError(f"{rotulo}: o valor não pode ser negativo")
    if valor > _MAXIMO:
        raise ValueError(f"{rotulo}: valor grande demais — confira a digitação")
    return valor.quantize(_CASAS, ROUND_HALF_UP)


def gravar_folha(session: Session, recurso: Recurso, valores: dict[str, object], mes: date | str | None = None) -> int:
    """Grava os campos informados (um único commit). Devolve quantas linhas foram criadas ou alteradas.

    Linha existente: atualizada pelo histórico (mês posterior guarda a versão antiga; mesmo mês é correção; mês
    anterior ao vigente é recusado — `historico.ErroDeHistorico`). Linha inexistente: criada (só se o valor > 0) valendo
    a partir de `mes`. Campo não informado ou vazio não mexe. Não altera o imposto de crédito das linhas.
    """
    grupo = grupo_de(recurso)
    por_chave = {c.chave: c for c in grupo.campos}
    desconhecidos = set(valores) - set(por_chave)
    if desconhecidos:
        raise ValueError(f"Campos desconhecidos: {', '.join(sorted(desconhecidos))}")
    novos = {chave: _valor(por_chave[chave].rotulo, bruto) for chave, bruto in valores.items()}
    mes_normalizado = historico.normalizar_mes(mes)  # ErroDeHistorico (ValueError) se o mês for inválido

    garantir_contas(session, grupo.tipo)
    atual = ler_folha(session, recurso)
    contas = {c.chave: c for c in session.query(PlanoDeContas).filter(
        PlanoDeContas.chave.in_([c.conta_codigo for c in grupo.campos]))}
    mudou = 0
    try:
        for item in atual.campos:
            valor = novos.get(item.campo.chave)
            if valor is None:
                continue
            if item.linha is not None:
                if getattr(item.linha, item.campo.coluna) != valor:
                    historico.atualizar_com_historico(session, item.linha, {item.campo.coluna: valor}, mes_normalizado, commit=False)
                    mudou += 1
            elif valor > 0:
                arvore.exigir_lancavel(contas[item.campo.conta_codigo], "Conta")
                session.add(grupo.modelo(**{
                    grupo.fk: recurso.id,
                    "plano_de_contas_id": contas[item.campo.conta_codigo].id,
                    "desembolso": valor if item.campo.coluna == "desembolso" else Decimal(0),
                    "retencao": valor if item.campo.coluna == "retencao" else Decimal(0),
                    "atualizado_em": mes_normalizado,
                }))
                mudou += 1
        session.commit()
    except Exception:
        session.rollback()
        raise
    return mudou
