"""
Repositórios — funções de acesso ao banco de dados.

Interface compatível com as funções JSON anteriores do equipe_rh.py:
  carregar_colaboradores()  →  lista de dicts
  colaboradores_ativos()    →  lista de dicts apenas ativos
  carregar_ponto(ano, mes)  →  {id_colab: {dia_str: status}}
  salvar_ponto(ano, mes, d) →  persiste o dict acima
  ... etc.

Assim a UI de equipe_rh.py não precisa mudar.
"""

from __future__ import annotations

import uuid
from datetime import date, datetime, timedelta
from typing import Any

from sqlalchemy import extract, func as _func

from db.database import get_session
from db.models import (
    CategoriaFinanceira, CentroCusto, Colaborador, ConfigEmpresa,
    ContaFinanceira, Contato, EscalaItem, Ferias, FormaPagamento,
    FechamentoCaixa, LancamentoBaixa, LancamentoMaquininha, MaquininhaCaixa,
    RegistroPonto, Titulo, Turno,
)


# ══════════════════════════════════════════════════════════════════════════════
# COLABORADORES
# ══════════════════════════════════════════════════════════════════════════════

def carregar_colaboradores() -> list[dict]:
    with get_session() as s:
        rows = s.query(Colaborador).order_by(Colaborador.nome).all()
        return [c.to_dict() for c in rows]


def colaboradores_ativos() -> list[dict]:
    with get_session() as s:
        rows = (
            s.query(Colaborador)
            .filter(Colaborador.status == "ativo")
            .order_by(Colaborador.nome)
            .all()
        )
        return [c.to_dict() for c in rows]


def salvar_colaborador(dados: dict) -> dict:
    """Insere ou atualiza um colaborador. Retorna o dict atualizado."""
    with get_session() as s:
        colab = s.get(Colaborador, dados.get("id")) if dados.get("id") else None

        if colab is None:
            colab = Colaborador(id=dados.get("id") or str(uuid.uuid4()))
            s.add(colab)

        colab.nome      = dados.get("nome", "").strip()
        colab.apelido   = dados.get("apelido", "").strip() or None
        colab.cpf       = dados.get("cpf", "").strip() or None
        colab.cargo     = dados.get("cargo", "").strip() or None
        colab.telefone  = dados.get("telefone", "").strip() or None
        colab.email     = dados.get("email", "").strip() or None
        colab.obs       = dados.get("obs", "").strip() or None
        colab.chave_pix = dados.get("chave_pix", "").strip() or None
        colab.uniformes = dados.get("uniformes", "").strip() or None
        colab.status    = dados.get("status", "ativo")
        colab.atualizado_em = datetime.now()

        # Demissão: aceita "DD/MM/AAAA" ou date object
        dem = dados.get("demissao", "")
        if isinstance(dem, date):
            colab.demissao = dem
        elif dem:
            try:
                d, m, a = str(dem).strip().split("/")
                colab.demissao = date(int(a), int(m), int(d))
            except Exception:
                colab.demissao = None
        else:
            colab.demissao = None

        # Salário e adiantamento
        try:
            colab.salario = float(str(dados.get("salario", 0)).replace(",", ".") or 0)
        except (ValueError, TypeError):
            colab.salario = 0.0
        try:
            colab.adiantamento = float(str(dados.get("adiantamento", 0)).replace(",", ".") or 0)
        except (ValueError, TypeError):
            colab.adiantamento = 0.0

        # Admissão: aceita "DD/MM/AAAA" ou date object
        adm = dados.get("admissao", "")
        if isinstance(adm, date):
            colab.admissao = adm
        elif adm:
            try:
                d, m, a = str(adm).strip().split("/")
                colab.admissao = date(int(a), int(m), int(d))
            except Exception:
                colab.admissao = None
        else:
            colab.admissao = None

        s.commit()
        s.refresh(colab)
        return colab.to_dict()


def deletar_colaborador(id_colab: str):
    with get_session() as s:
        c = s.get(Colaborador, id_colab)
        if c:
            s.delete(c)
            s.commit()


# Compatibilidade com código legado que chamava salvar_colaboradores(lista)
def salvar_colaboradores(lista: list[dict]):
    """Salva a lista completa (upsert individual). Mantém compatibilidade JSON."""
    ids_novos = {d.get("id") for d in lista if d.get("id")}
    for dados in lista:
        salvar_colaborador(dados)
    # Desativa colaboradores que foram removidos da lista (não deleta)
    with get_session() as s:
        todos = s.query(Colaborador).all()
        for c in todos:
            if c.id not in ids_novos:
                c.status = "inativo"
        s.commit()


# ══════════════════════════════════════════════════════════════════════════════
# PONTO
# ══════════════════════════════════════════════════════════════════════════════

def carregar_ponto(ano: int, mes: int) -> dict[str, dict[str, str]]:
    """Retorna {id_colab: {dia_str: status}} — mesmo formato do JSON antigo."""
    with get_session() as s:
        rows = (
            s.query(RegistroPonto)
            .filter(
                extract("year",  RegistroPonto.data) == ano,
                extract("month", RegistroPonto.data) == mes,
            )
            .all()
        )
        resultado: dict[str, dict[str, str]] = {}
        for r in rows:
            resultado.setdefault(r.colaborador_id, {})[str(r.data.day)] = r.status
        return resultado


def salvar_ponto(ano: int, mes: int, dados: dict[str, dict[str, str]]):
    """Persiste {id_colab: {dia_str: status}} no banco."""
    with get_session() as s:
        for id_colab, dias in dados.items():
            for dia_str, status in dias.items():
                data_reg = date(ano, mes, int(dia_str))
                reg = (
                    s.query(RegistroPonto)
                    .filter_by(colaborador_id=id_colab, data=data_reg)
                    .first()
                )
                if status:
                    if reg is None:
                        reg = RegistroPonto(colaborador_id=id_colab, data=data_reg)
                        s.add(reg)
                    reg.status = status
                elif reg is not None:
                    s.delete(reg)
        s.commit()


def ponto_colaborador_mes(id_colab: str, ano: int, mes: int) -> dict[str, str]:
    """Retorna {dia_str: status} para um colaborador específico."""
    with get_session() as s:
        rows = (
            s.query(RegistroPonto)
            .filter(
                RegistroPonto.colaborador_id == id_colab,
                extract("year",  RegistroPonto.data) == ano,
                extract("month", RegistroPonto.data) == mes,
            )
            .all()
        )
        return {str(r.data.day): r.status for r in rows}


# ══════════════════════════════════════════════════════════════════════════════
# ESCALA
# ══════════════════════════════════════════════════════════════════════════════

def carregar_escala(segunda: date) -> dict[str, dict[str, str]]:
    """Retorna {id_colab: {dow_str: turno}} — mesmo formato do JSON antigo."""
    with get_session() as s:
        rows = (
            s.query(EscalaItem)
            .filter(EscalaItem.semana_inicio == segunda)
            .all()
        )
        resultado: dict[str, dict[str, str]] = {}
        for r in rows:
            resultado.setdefault(r.colaborador_id, {})[str(r.dia_semana)] = r.turno
        return resultado


def salvar_escala(segunda: date, dados: dict[str, dict[str, str]]):
    """Persiste {id_colab: {dow_str: turno}}."""
    with get_session() as s:
        for id_colab, dias in dados.items():
            for dow_str, turno in dias.items():
                item = (
                    s.query(EscalaItem)
                    .filter_by(
                        colaborador_id=id_colab,
                        semana_inicio=segunda,
                        dia_semana=int(dow_str),
                    )
                    .first()
                )
                if item is None:
                    item = EscalaItem(
                        colaborador_id=id_colab,
                        semana_inicio=segunda,
                        dia_semana=int(dow_str),
                    )
                    s.add(item)
                item.turno = turno
        s.commit()


def escala_semana_completa(segunda: date) -> dict[str, dict[str, str]]:
    """Como carregar_escala — alias mais descritivo."""
    return carregar_escala(segunda)


def carregar_escala_mes(ano: int, mes: int) -> dict[str, dict[str, str]]:
    """Retorna {colab_id: {date_iso: turno}} para todos os dias do mês."""
    import calendar as _cal
    first = date(ano, mes, 1)
    last  = date(ano, mes, _cal.monthrange(ano, mes)[1])
    from_segunda = first - timedelta(days=6)

    resultado: dict[str, dict[str, str]] = {}
    with get_session() as s:
        items = (
            s.query(EscalaItem)
            .filter(
                EscalaItem.semana_inicio >= from_segunda,
                EscalaItem.semana_inicio <= last,
            )
            .all()
        )
        for item in items:
            data_dia = item.semana_inicio + timedelta(days=item.dia_semana)
            if first <= data_dia <= last:
                resultado.setdefault(
                    item.colaborador_id, {}
                )[data_dia.isoformat()] = item.turno
    return resultado


# ══════════════════════════════════════════════════════════════════════════════
# FÉRIAS
# ══════════════════════════════════════════════════════════════════════════════

def carregar_ferias() -> list[dict]:
    with get_session() as s:
        rows = (
            s.query(Ferias)
            .order_by(Ferias.inicio.desc())
            .all()
        )
        return [f.to_dict() for f in rows]


def salvar_ferias_registro(dados: dict) -> dict:
    """Insere ou atualiza um período de férias."""
    with get_session() as s:
        reg = s.get(Ferias, dados.get("id")) if dados.get("id") else None
        if reg is None:
            reg = Ferias(id=dados.get("id") or str(uuid.uuid4()))
            s.add(reg)

        reg.colaborador_id = dados["id_colab"]
        reg.obs = dados.get("obs", "") or None

        def _parse_date(val):
            if isinstance(val, date):
                return val
            try:
                return date.fromisoformat(str(val))
            except Exception:
                d, m, a = str(val).split("/")
                return date(int(a), int(m), int(d))

        reg.inicio = _parse_date(dados["inicio"])
        reg.fim    = _parse_date(dados["fim"])
        s.commit()
        s.refresh(reg)
        return reg.to_dict()


def deletar_ferias(id_ferias: str):
    with get_session() as s:
        r = s.get(Ferias, id_ferias)
        if r:
            s.delete(r)
            s.commit()


# Compatibilidade: salvar_ferias(lista) — usado pelo código legado
def salvar_ferias(lista: list[dict]):
    ids = {d.get("id") for d in lista if d.get("id")}
    # Remover os que sumiram da lista
    with get_session() as s:
        existentes = s.query(Ferias).all()
        for f in existentes:
            if f.id not in ids:
                s.delete(f)
        s.commit()
    for d in lista:
        salvar_ferias_registro(d)


# ══════════════════════════════════════════════════════════════════════════════
# TURNOS
# ══════════════════════════════════════════════════════════════════════════════

_TURNOS_PADRAO = [
    {"nome": "Manhã",    "abreviacao": "M",  "hora_inicio": "08:00", "hora_fim": "17:00", "cor": "#1A472A"},
    {"nome": "Tarde",    "abreviacao": "T",  "hora_inicio": "12:00", "hora_fim": "21:00", "cor": "#0D2137"},
    {"nome": "Noite",    "abreviacao": "N",  "hora_inicio": "21:00", "hora_fim": "06:00", "cor": "#2D1B4E"},
    {"nome": "Integral", "abreviacao": "I",  "hora_inicio": "08:00", "hora_fim": "18:00", "cor": "#4D3800"},
]


def seed_turnos_padrao():
    """Insere turnos padrão se a tabela estiver vazia."""
    with get_session() as s:
        if s.query(Turno).count() == 0:
            for d in _TURNOS_PADRAO:
                s.add(Turno(**d))
            s.commit()


def listar_turnos(apenas_ativos: bool = True) -> list[dict]:
    with get_session() as s:
        q = s.query(Turno)
        if apenas_ativos:
            q = q.filter(Turno.ativo == True)  # noqa: E712
        return [t.to_dict() for t in q.order_by(Turno.nome).all()]


def salvar_turno(dados: dict) -> dict:
    with get_session() as s:
        turno = s.get(Turno, dados.get("id")) if dados.get("id") else None
        if turno is None:
            turno = Turno()
            s.add(turno)
        turno.nome        = dados["nome"].strip()
        turno.abreviacao  = dados["abreviacao"].strip()[:4]
        turno.hora_inicio = dados.get("hora_inicio", "08:00")
        turno.hora_fim    = dados.get("hora_fim",    "17:00")
        turno.cor         = dados.get("cor",         "#1A472A")
        turno.ativo       = bool(dados.get("ativo", True))
        s.commit()
        s.refresh(turno)
        return turno.to_dict()


def deletar_turno(id_turno: int):
    with get_session() as s:
        t = s.get(Turno, id_turno)
        if t:
            s.delete(t)
            s.commit()


# ══════════════════════════════════════════════════════════════════════════════
# GERAÇÃO AUTOMÁTICA DE ESCALA
# ══════════════════════════════════════════════════════════════════════════════

# Padrões embutidos: lista de (work=True/False) por posição no ciclo
_PADROES: dict[str, list[bool]] = {
    "5x2":  [True,  True,  True,  True,  True,  False, False],
    "6x1":  [True,  True,  True,  True,  True,  True,  False],
    "3x1":  [True,  True,  True,  False],
    "4x2":  [True,  True,  True,  True,  False, False],
    "2x1":  [True,  True,  False],
}


def gerar_escala_automatica(
    colabs_config: list[dict],
    data_inicio: date,
    data_fim:    date,
    padrao:      str  = "5x2",
    ciclo_custom: list[bool] | None = None,
    sobrescrever: bool = True,
) -> int:
    """
    Gera EscalaItens para o período informado.

    colabs_config: lista de dicts com:
        id_colab   — str UUID do colaborador
        turno_abrev — abreviação do turno (ex: "M", "T") ou "" para usar folga
        offset     — posição inicial no ciclo (0 = começa no dia 1 do padrão)

    Retorna o total de itens escritos.
    """
    if padrao == "personalizado" and ciclo_custom:
        ciclo = ciclo_custom
    elif padrao == "5x2":
        ciclo = None   # calculado por dia da semana
    else:
        ciclo = _PADROES.get(padrao, _PADROES["5x2"])

    total = 0

    with get_session() as s:
        # Iterar dia a dia no período
        current = data_inicio
        while current <= data_fim:
            segunda = current - timedelta(days=current.weekday())
            dow     = current.weekday()   # 0=Seg … 6=Dom

            for cfg in colabs_config:
                id_colab    = cfg["id_colab"]
                turno_abrev = cfg.get("turno_abrev", "")
                offset      = int(cfg.get("offset", 0))

                # Determinar se é trabalho ou folga neste dia
                if padrao == "5x2":
                    trabalha = (dow < 5)
                else:
                    day_idx  = (current - data_inicio).days + offset
                    trabalha = ciclo[day_idx % len(ciclo)]

                turno_valor = turno_abrev if trabalha else "FO"

                # Upsert EscalaItem
                item = (
                    s.query(EscalaItem)
                    .filter_by(
                        colaborador_id=id_colab,
                        semana_inicio=segunda,
                        dia_semana=dow,
                    )
                    .first()
                )
                if item is None:
                    item = EscalaItem(
                        colaborador_id=id_colab,
                        semana_inicio=segunda,
                        dia_semana=dow,
                    )
                    s.add(item)
                elif not sobrescrever:
                    continue

                item.turno = turno_valor
                total += 1

            current += timedelta(days=1)

        s.commit()

    return total


def gerar_escala_ciclo_dias(
    colabs_config: list[dict],
    data_inicio: date,
    data_fim: date,
    sobrescrever: bool = True,
) -> int:
    """
    Gera escala com ciclo de N dias a partir da segunda-feira da semana de data_inicio.

    colabs_config: [{id_colab, ciclo: [turno_dia0, ..., turno_diaN-1]}]
    ciclo[0] = segunda-feira da semana de data_inicio
    ciclo[6] = domingo da semana 1, ciclo[7] = segunda-feira da semana 2, etc.
    """
    # referência: segunda-feira da semana de data_inicio
    referencia = data_inicio - timedelta(days=data_inicio.weekday())
    total = 0
    with get_session() as s:
        current = data_inicio
        while current <= data_fim:
            dow     = current.weekday()
            segunda = current - timedelta(days=dow)
            offset  = (current - referencia).days
            for cfg in colabs_config:
                ciclo = cfg["ciclo"]
                turno = ciclo[offset % len(ciclo)]
                item  = (
                    s.query(EscalaItem)
                    .filter_by(
                        colaborador_id=cfg["id_colab"],
                        semana_inicio=segunda,
                        dia_semana=dow,
                    )
                    .first()
                )
                if item is None:
                    item = EscalaItem(
                        colaborador_id=cfg["id_colab"],
                        semana_inicio=segunda,
                        dia_semana=dow,
                    )
                    s.add(item)
                elif not sobrescrever:
                    continue
                item.turno = turno
                total += 1
            current += timedelta(days=1)
        s.commit()
    return total


def gerar_escala_semana_visual(
    colabs_config: list[dict],
    data_inicio: date,
    data_fim: date,
    sobrescrever: bool = True,
) -> int:
    """
    Gera escala com padrão por dia da semana (0=Seg … 6=Dom).

    colabs_config: [{id_colab, padrao_dow: {0: "M", 1: "T", ..., 6: "FO"}}]
    """
    total = 0
    with get_session() as s:
        current = data_inicio
        while current <= data_fim:
            dow     = current.weekday()
            segunda = current - timedelta(days=dow)
            for cfg in colabs_config:
                turno = cfg["padrao_dow"].get(dow, "FO")
                item = (
                    s.query(EscalaItem)
                    .filter_by(
                        colaborador_id=cfg["id_colab"],
                        semana_inicio=segunda,
                        dia_semana=dow,
                    )
                    .first()
                )
                if item is None:
                    item = EscalaItem(
                        colaborador_id=cfg["id_colab"],
                        semana_inicio=segunda,
                        dia_semana=dow,
                    )
                    s.add(item)
                elif not sobrescrever:
                    continue
                item.turno = turno
                total += 1
            current += timedelta(days=1)
        s.commit()
    return total


# ══════════════════════════════════════════════════════════════════════════════
# RELATÓRIOS / CÁLCULOS
# ══════════════════════════════════════════════════════════════════════════════

# ══════════════════════════════════════════════════════════════════════════════
# CONFIGURAÇÕES DA EMPRESA
# ══════════════════════════════════════════════════════════════════════════════

def carregar_config() -> dict:
    """Retorna os dados da empresa (registro único id=1). Nunca levanta exceção."""
    with get_session() as s:
        cfg = s.get(ConfigEmpresa, 1)
        if cfg is None:
            return ConfigEmpresa().to_dict()
        return cfg.to_dict()


def salvar_config(dados: dict) -> dict:
    """Upsert dos dados da empresa. Retorna o dict salvo."""
    with get_session() as s:
        cfg = s.get(ConfigEmpresa, 1)
        if cfg is None:
            cfg = ConfigEmpresa(id=1)
            s.add(cfg)

        cfg.nome              = dados.get("nome", "").strip() or None
        cfg.nome_fantasia     = dados.get("nome_fantasia", "").strip() or None
        cfg.cnpj              = dados.get("cnpj", "").strip() or None
        cfg.ie                = dados.get("ie", "").strip() or None
        cfg.im                = dados.get("im", "").strip() or None
        cfg.logradouro        = dados.get("logradouro", "").strip() or None
        cfg.numero            = dados.get("numero", "").strip() or None
        cfg.complemento       = dados.get("complemento", "").strip() or None
        cfg.bairro            = dados.get("bairro", "").strip() or None
        cfg.cidade            = dados.get("cidade", "").strip() or None
        cfg.estado            = (dados.get("estado", "").strip() or "")[:2] or None
        cfg.cep               = dados.get("cep", "").strip() or None
        cfg.telefone          = dados.get("telefone", "").strip() or None
        cfg.email             = dados.get("email", "").strip() or None
        cfg.site              = dados.get("site", "").strip() or None
        cfg.responsavel       = dados.get("responsavel", "").strip() or None
        cfg.cargo_responsavel = dados.get("cargo_responsavel", "").strip() or None
        cfg.logo_path         = dados.get("logo_path", "").strip() or None
        cfg.fc_conta_dinheiro = dados.get("fc_conta_dinheiro", "").strip() or None
        cfg.fc_conta_moedas   = dados.get("fc_conta_moedas",   "").strip() or None

        s.commit()
        s.refresh(cfg)
        return cfg.to_dict()


# ══════════════════════════════════════════════════════════════════════════════
# MÓDULO: FINANCEIRO — CONTAS A PAGAR / RECEBER
# ══════════════════════════════════════════════════════════════════════════════

def contatos_recentes(limite: int = 60) -> list[str]:
    """Retorna nomes de contato únicos já usados em títulos (para autocomplete)."""
    with get_session() as s:
        rows = (
            s.query(Titulo.nome_contato)
            .filter(Titulo.nome_contato.isnot(None), Titulo.nome_contato != "")
            .distinct()
            .order_by(Titulo.nome_contato)
            .limit(limite)
            .all()
        )
        return [r[0] for r in rows]


def _atualizar_vencidos():
    """Marca como 'vencido' qualquer título aberto cujo vencimento já passou."""
    hoje = date.today()
    with get_session() as s:
        vencidos = (
            s.query(Titulo)
            .filter(Titulo.status == "aberto", Titulo.vencimento < hoje)
            .all()
        )
        for t in vencidos:
            t.status = "vencido"
        s.commit()


def listar_titulos(
    tipo: str | None = None,
    status: str | None = None,
    mes: int | None = None,
    ano: int | None = None,
) -> list[dict]:
    _atualizar_vencidos()
    with get_session() as s:
        q = s.query(Titulo)
        if tipo:
            q = q.filter(Titulo.tipo == tipo)
        if status:
            q = q.filter(Titulo.status == status)
        if mes and ano:
            q = q.filter(
                extract("month", Titulo.vencimento) == mes,
                extract("year",  Titulo.vencimento) == ano,
            )
        elif ano:
            q = q.filter(extract("year", Titulo.vencimento) == ano)
        return [t.to_dict() for t in q.order_by(Titulo.vencimento).all()]


def _parse_date_titulo(val) -> date | None:
    if val is None or val == "":
        return None
    if isinstance(val, date):
        return val
    try:
        d, m, a = str(val).strip().split("/")
        return date(int(a), int(m), int(d))
    except Exception:
        return None


def salvar_titulo(dados: dict) -> dict:
    with get_session() as s:
        titulo = s.get(Titulo, dados.get("id")) if dados.get("id") else None
        if titulo is None:
            titulo = Titulo()
            s.add(titulo)

        titulo.tipo            = dados.get("tipo", "pagar")
        titulo.descricao       = dados.get("descricao", "").strip()
        titulo.categoria       = dados.get("categoria", "").strip() or None
        titulo.nome_contato    = dados.get("nome_contato", "").strip() or None
        titulo.obs             = dados.get("obs", "").strip() or None
        titulo.status          = dados.get("status", "aberto")
        titulo.centro_custo    = dados.get("centro_custo", "").strip() or None
        titulo.forma_pagamento = dados.get("forma_pagamento", "").strip() or None
        titulo.conta_nome      = dados.get("conta_nome", "").strip() or None
        titulo.vencimento      = _parse_date_titulo(dados.get("vencimento")) or date.today()
        titulo.data_emissao    = _parse_date_titulo(dados.get("data_emissao"))
        titulo.data_pagamento  = _parse_date_titulo(dados.get("data_pagamento"))

        vp = dados.get("valor_pago")
        if vp is not None:
            try:
                titulo.valor_pago = float(
                    str(vp).replace("R$","").replace(".","").replace(",",".").strip()
                ) if not isinstance(vp, (int, float)) else float(vp)
            except Exception:
                titulo.valor_pago = None
        else:
            titulo.valor_pago = None

        v = dados.get("valor", 0)
        if isinstance(v, (int, float)):
            titulo.valor = float(v)
        else:
            try:
                titulo.valor = float(
                    str(v).replace("R$", "").replace(".", "").replace(",", ".").strip()
                )
            except (ValueError, TypeError):
                titulo.valor = 0.0

        titulo.atualizado_em = datetime.now()
        s.commit()
        s.refresh(titulo)
        return titulo.to_dict()


# ── Lançamentos de Baixa ─────────────────────────────────────────────────────

def _recalcular_titulo(t: "Titulo", s) -> None:
    """Recalcula valor_pago, status e data_pagamento baseado nos lançamentos."""
    total = round(sum(l.valor for l in t.lancamentos), 2)
    t.valor_pago = total
    hoje = date.today()
    if total >= t.valor - 0.005:
        t.status = "pago"
        if t.lancamentos:
            t.data_pagamento = max(l.data for l in t.lancamentos)
    elif total > 0:
        t.status = "parcial"
    else:
        t.status = "vencido" if t.vencimento < hoje else "aberto"
        t.data_pagamento = None


def listar_lancamentos(titulo_id: int) -> list[dict]:
    with get_session() as s:
        rows = (
            s.query(LancamentoBaixa)
            .filter(LancamentoBaixa.titulo_id == titulo_id)
            .order_by(LancamentoBaixa.data, LancamentoBaixa.hora)
            .all()
        )
        return [l.to_dict() for l in rows]


def adicionar_lancamento(titulo_id: int, dados: dict) -> dict:
    """Adiciona um lançamento de baixa e recalcula o status do título."""
    with get_session() as s:
        t = s.get(Titulo, titulo_id)
        if not t:
            raise ValueError(f"Título {titulo_id} não encontrado.")

        lanc = LancamentoBaixa()
        lanc.titulo_id  = titulo_id
        lanc.data       = _parse_date_titulo(dados.get("data")) or date.today()
        lanc.hora       = dados.get("hora", datetime.now().strftime("%H:%M"))[:5]
        lanc.historico  = (dados.get("historico") or "PAGAMENTO").strip()
        lanc.conta_nome = dados.get("conta_nome", "").strip() or None
        lanc.cheque     = dados.get("cheque", "").strip() or None

        v = dados.get("valor", 0)
        try:
            lanc.valor = float(v) if isinstance(v, (int, float)) else float(
                str(v).replace("R$", "").replace(".", "").replace(",", ".").strip()
            )
        except Exception:
            lanc.valor = 0.0

        s.add(lanc)
        s.flush()
        _recalcular_titulo(t, s)
        s.commit()
        s.refresh(lanc)
        return lanc.to_dict()


def listar_extrato(
    conta_nome: str | None = None,
    mes: int | None = None,
    ano: int | None = None,
    tipo: str | None = None,
    busca: str | None = None,
) -> list[dict]:
    """Retorna lançamentos de baixa com dados do título, para extrato por conta."""
    from sqlalchemy import and_
    with get_session() as s:
        q = (
            s.query(LancamentoBaixa, Titulo)
            .join(Titulo, LancamentoBaixa.titulo_id == Titulo.id)
        )
        if conta_nome:
            q = q.filter(LancamentoBaixa.conta_nome == conta_nome)
        if mes and ano:
            q = q.filter(
                extract("month", LancamentoBaixa.data) == mes,
                extract("year",  LancamentoBaixa.data) == ano,
            )
        elif ano:
            q = q.filter(extract("year", LancamentoBaixa.data) == ano)
        if tipo and tipo != "todos":
            q = q.filter(Titulo.tipo == tipo)
        rows = q.order_by(LancamentoBaixa.data.desc(), LancamentoBaixa.hora.desc()).all()

        result = []
        for lanc, tit in rows:
            result.append({
                "id":           lanc.id,
                "data":         lanc.data.strftime("%d/%m/%Y") if lanc.data else "",
                "hora":         lanc.hora or "",
                "conta_nome":   lanc.conta_nome or "",
                "historico":    lanc.historico or "PAGAMENTO",
                "valor":        lanc.valor or 0.0,
                "titulo_id":    tit.id,
                "descricao":    tit.descricao,
                "categoria":    tit.categoria or "",
                "nome_contato": tit.nome_contato or "",
                "tipo":         tit.tipo,
            })

    if busca:
        b = busca.lower()
        result = [r for r in result if b in (
            r["descricao"] + r["historico"] + r["nome_contato"] + r["categoria"]
        ).lower()]
    return result


def deletar_lancamento(id_lancamento: int) -> None:
    """Remove um lançamento e recalcula o status do título."""
    with get_session() as s:
        lanc = s.get(LancamentoBaixa, id_lancamento)
        if not lanc:
            return
        titulo_id = lanc.titulo_id
        s.delete(lanc)
        s.flush()
        t = s.get(Titulo, titulo_id)
        if t:
            _recalcular_titulo(t, s)
        s.commit()


def baixar_titulo(
    id_titulo: int,
    data_pgto: str,
    valor_pago: float,
    conta_nome: str | None = None,
    obs: str | None = None,
) -> dict:
    """Marca um título como pago, opcionalmente atualizando conta e observação."""
    with get_session() as s:
        t = s.get(Titulo, id_titulo)
        if not t:
            raise ValueError(f"Título {id_titulo} não encontrado.")
        t.status         = "pago"
        t.valor_pago     = valor_pago
        t.data_pagamento = _parse_date_titulo(data_pgto) or date.today()
        if conta_nome is not None:
            t.conta_nome = conta_nome.strip() or None
        if obs is not None:
            t.obs = obs.strip() or None
        t.atualizado_em  = datetime.now()
        s.commit()
        s.refresh(t)
        return t.to_dict()


def deletar_titulo(id_titulo: int):
    with get_session() as s:
        t = s.get(Titulo, id_titulo)
        if t:
            s.delete(t)
            s.commit()


# ══════════════════════════════════════════════════════════════════════════════
# CONFIGURAÇÕES FINANCEIRAS
# ══════════════════════════════════════════════════════════════════════════════

# ── Centro de Custo ──────────────────────────────────────────────────────────

def listar_centros_custo(apenas_ativos: bool = False) -> list[dict]:
    with get_session() as s:
        q = s.query(CentroCusto)
        if apenas_ativos:
            q = q.filter(CentroCusto.ativo == True)  # noqa
        return [c.to_dict() for c in q.order_by(CentroCusto.nome).all()]


def salvar_centro_custo(dados: dict) -> dict:
    with get_session() as s:
        obj = s.get(CentroCusto, dados.get("id")) if dados.get("id") else None
        if obj is None:
            obj = CentroCusto()
            s.add(obj)
        obj.nome   = dados.get("nome", "").strip()
        obj.codigo = dados.get("codigo", "").strip() or None
        obj.tipo   = dados.get("tipo", "ambos")
        obj.ativo  = bool(dados.get("ativo", True))
        s.commit()
        s.refresh(obj)
        return obj.to_dict()


def deletar_centro_custo(id_: int):
    with get_session() as s:
        obj = s.get(CentroCusto, id_)
        if obj:
            s.delete(obj)
            s.commit()


# ── Categorias Financeiras ───────────────────────────────────────────────────

_CATS_SEED = [
    ("Aluguel",                  "pagar"),
    ("Energia Elétrica",         "pagar"),
    ("Água/Saneamento",          "pagar"),
    ("Internet/Telefone",        "pagar"),
    ("Fornecedor",               "pagar"),
    ("Salários",                 "pagar"),
    ("Impostos/Taxas",           "pagar"),
    ("Manutenção",               "pagar"),
    ("Material de Escritório",   "pagar"),
    ("Empréstimo/Financiamento", "pagar"),
    ("Vendas à Vista",           "receber"),
    ("Vendas a Prazo",           "receber"),
    ("Serviços",                 "receber"),
    ("Comissões",                "receber"),
    ("Aluguéis Recebidos",       "receber"),
    ("Outros",                   "ambos"),
]


def seed_categorias_financeiras():
    with get_session() as s:
        if s.query(CategoriaFinanceira).count() == 0:
            for nome, tipo in _CATS_SEED:
                s.add(CategoriaFinanceira(nome=nome, tipo=tipo))
            s.commit()


def listar_categorias_fin(tipo: str | None = None, apenas_ativas: bool = True) -> list[dict]:
    with get_session() as s:
        q = s.query(CategoriaFinanceira)
        if apenas_ativas:
            q = q.filter(CategoriaFinanceira.ativo == True)  # noqa
        if tipo:
            q = q.filter(CategoriaFinanceira.tipo.in_([tipo, "ambos"]))
        return [c.to_dict() for c in q.order_by(CategoriaFinanceira.nome).all()]


def nomes_categorias(tipo: str | None = None) -> list[str]:
    """Lista de nomes para popular Combobox."""
    seed_categorias_financeiras()
    return [""] + [c["nome"] for c in listar_categorias_fin(tipo=tipo)]


def salvar_categoria_fin(dados: dict) -> dict:
    with get_session() as s:
        obj = s.get(CategoriaFinanceira, dados.get("id")) if dados.get("id") else None
        if obj is None:
            obj = CategoriaFinanceira()
            s.add(obj)
        obj.nome  = dados.get("nome", "").strip()
        obj.tipo  = dados.get("tipo", "ambos")
        obj.ativo = bool(dados.get("ativo", True))
        s.commit()
        s.refresh(obj)
        return obj.to_dict()


def deletar_categoria_fin(id_: int):
    with get_session() as s:
        obj = s.get(CategoriaFinanceira, id_)
        if obj:
            s.delete(obj)
            s.commit()


# ── Formas de Pagamento ──────────────────────────────────────────────────────

_FORMAS_SEED = [
    "PIX", "Dinheiro", "Cartão Débito", "Cartão Crédito",
    "Boleto", "Transferência", "Cheque",
]


def seed_formas_pagamento():
    with get_session() as s:
        if s.query(FormaPagamento).count() == 0:
            for nome in _FORMAS_SEED:
                s.add(FormaPagamento(nome=nome))
            s.commit()


def listar_formas_pgto(apenas_ativas: bool = True) -> list[dict]:
    with get_session() as s:
        q = s.query(FormaPagamento)
        if apenas_ativas:
            q = q.filter(FormaPagamento.ativo == True)  # noqa
        return [f.to_dict() for f in q.order_by(FormaPagamento.nome).all()]


def nomes_formas_pgto() -> list[str]:
    seed_formas_pagamento()
    return [""] + [f["nome"] for f in listar_formas_pgto()]


def salvar_forma_pgto(dados: dict) -> dict:
    with get_session() as s:
        obj = s.get(FormaPagamento, dados.get("id")) if dados.get("id") else None
        if obj is None:
            obj = FormaPagamento()
            s.add(obj)
        obj.nome  = dados.get("nome", "").strip()
        obj.ativo = bool(dados.get("ativo", True))
        s.commit()
        s.refresh(obj)
        return obj.to_dict()


def deletar_forma_pgto(id_: int):
    with get_session() as s:
        obj = s.get(FormaPagamento, id_)
        if obj:
            s.delete(obj)
            s.commit()


# ── Contas Financeiras ────────────────────────────────────────────────────────

def listar_contas_fin(apenas_ativas: bool = True) -> list[dict]:
    with get_session() as s:
        q = s.query(ContaFinanceira)
        if apenas_ativas:
            q = q.filter(ContaFinanceira.ativo == True)  # noqa
        return [c.to_dict() for c in q.order_by(ContaFinanceira.nome).all()]


def nomes_contas_fin() -> list[str]:
    return [""] + [c["nome"] for c in listar_contas_fin()]


def salvar_conta_fin(dados: dict) -> dict:
    with get_session() as s:
        obj = s.get(ContaFinanceira, dados.get("id")) if dados.get("id") else None
        if obj is None:
            obj = ContaFinanceira()
            s.add(obj)
        obj.nome         = dados.get("nome", "").strip()
        obj.tipo         = dados.get("tipo", "corrente")
        obj.banco        = dados.get("banco", "").strip() or None
        obj.agencia      = dados.get("agencia", "").strip() or None
        obj.conta_numero = dados.get("conta_numero", "").strip() or None
        obj.ativo        = bool(dados.get("ativo", True))
        v = dados.get("saldo_inicial", 0)
        try:
            obj.saldo_inicial = float(
                str(v).replace("R$", "").replace(".", "").replace(",", ".").strip()
            ) if not isinstance(v, (int, float)) else float(v)
        except Exception:
            obj.saldo_inicial = 0.0
        s.commit()
        s.refresh(obj)
        return obj.to_dict()


def deletar_conta_fin(id_: int):
    with get_session() as s:
        obj = s.get(ContaFinanceira, id_)
        if obj:
            s.delete(obj)
            s.commit()


def saldo_ferias(id_colab: str) -> float:
    """Retorna dias de saldo de férias disponível."""
    import calendar as _cal
    with get_session() as s:
        colab = s.get(Colaborador, id_colab)
        if not colab or not colab.admissao:
            return 0.0

        hoje = date.today()
        meses = max(0,
            (hoje.year - colab.admissao.year) * 12 +
            hoje.month - colab.admissao.month
        )
        direito = (meses / 12) * 30

        gozados = sum(
            (f.fim - f.inicio).days + 1
            for f in colab.ferias
        )
        return max(0.0, direito - gozados)


# ══════════════════════════════════════════════════════════════════════════════
# MÓDULO: CLIENTES / FORNECEDORES
# ══════════════════════════════════════════════════════════════════════════════

def listar_contatos(
    tipo: str | None = None,
    busca: str | None = None,
    apenas_ativos: bool = False,
) -> list[dict]:
    with get_session() as s:
        q = s.query(Contato)
        if tipo and tipo != "todos":
            from sqlalchemy import or_
            q = q.filter(or_(Contato.tipo == tipo, Contato.tipo == "ambos"))
        if apenas_ativos:
            q = q.filter(Contato.ativo == True)
        rows = q.order_by(Contato.nome).all()
        result = [c.to_dict() for c in rows]
    if busca:
        b = busca.lower()
        result = [c for c in result if b in (
            c["nome"] + c["nome_fantasia"] + c["cnpj"] + c["cpf"] + c["cidade"]
        ).lower()]
    return result


def salvar_contato(dados: dict) -> dict:
    with get_session() as s:
        contato = s.get(Contato, dados.get("id")) if dados.get("id") else None
        if contato is None:
            contato = Contato()
            s.add(contato)
        contato.tipo          = dados.get("tipo", "fornecedor")
        contato.nome          = dados.get("nome", "").strip()
        contato.nome_fantasia = dados.get("nome_fantasia", "").strip() or None
        contato.cnpj          = dados.get("cnpj", "").strip() or None
        contato.cpf           = dados.get("cpf", "").strip() or None
        contato.ie            = dados.get("ie", "").strip() or None
        contato.email         = dados.get("email", "").strip() or None
        contato.telefone      = dados.get("telefone", "").strip() or None
        contato.celular       = dados.get("celular", "").strip() or None
        contato.logradouro    = dados.get("logradouro", "").strip() or None
        contato.numero        = dados.get("numero", "").strip() or None
        contato.complemento   = dados.get("complemento", "").strip() or None
        contato.bairro        = dados.get("bairro", "").strip() or None
        contato.cidade        = dados.get("cidade", "").strip() or None
        contato.estado        = (dados.get("estado", "").strip() or "")[:2].upper() or None
        contato.cep           = dados.get("cep", "").strip() or None
        contato.obs           = dados.get("obs", "").strip() or None
        contato.ativo         = dados.get("ativo", True)
        contato.situacao_rf   = dados.get("situacao_rf", "").strip() or None
        contato.atualizado_em = datetime.now()
        s.commit()
        s.refresh(contato)
        return contato.to_dict()


def deletar_contato(id_: int) -> None:
    with get_session() as s:
        contato = s.get(Contato, id_)
        if contato:
            s.delete(contato)
            s.commit()


# ══════════════════════════════════════════════════════════════════════════════
# MÓDULO: FECHAMENTO DE CAIXA
# ══════════════════════════════════════════════════════════════════════════════

# ── Maquininhas ───────────────────────────────────────────────────────────────

def listar_maquininhas(apenas_ativas: bool = True) -> list[dict]:
    with get_session() as s:
        q = s.query(MaquininhaCaixa)
        if apenas_ativas:
            q = q.filter(MaquininhaCaixa.ativo == True)  # noqa: E712
        return [m.to_dict() for m in q.order_by(MaquininhaCaixa.ordem, MaquininhaCaixa.nome).all()]


def salvar_maquininha(dados: dict) -> dict:
    with get_session() as s:
        obj = s.get(MaquininhaCaixa, dados.get("id")) if dados.get("id") else None
        if obj is None:
            obj = MaquininhaCaixa()
            s.add(obj)
        obj.nome          = dados.get("nome", "").strip()
        obj.conta_destino = dados.get("conta_destino", "").strip() or None
        obj.ordem         = int(dados.get("ordem", 0) or 0)
        obj.ativo         = bool(dados.get("ativo", True))
        s.commit()
        s.refresh(obj)
        return obj.to_dict()


def deletar_maquininha(id_: int) -> None:
    with get_session() as s:
        obj = s.get(MaquininhaCaixa, id_)
        if obj:
            s.delete(obj)
            s.commit()


# ── Fechamentos ───────────────────────────────────────────────────────────────

def listar_fechamentos(
    data_ini: "date | None" = None,
    data_fim: "date | None" = None,
) -> list[dict]:
    with get_session() as s:
        q = s.query(FechamentoCaixa)
        if data_ini:
            q = q.filter(FechamentoCaixa.data >= data_ini)
        if data_fim:
            q = q.filter(FechamentoCaixa.data <= data_fim)
        rows = q.order_by(FechamentoCaixa.data.desc(), FechamentoCaixa.id.desc()).all()
        return [fc.to_dict() for fc in rows]


def gravar_fechamento(dados: dict, lancamentos_maq: list[dict]) -> int:
    """
    Grava FechamentoCaixa + LancamentoMaquininha e faz auto-postagem nas contas.
    Retorna o ID do fechamento gravado.

    Auto-postagem:
    - Dinheiro físico → LancamentoBaixa na conta_dinheiro
    - Cada maquininha (cred+deb+pix) → LancamentoBaixa na conta_destino da máquina
    - Sangria > 0 → Titulo "pagar" em aberto (para direcionar depois)
    """
    data_fc = _parse_date_titulo(dados.get("data")) or date.today()

    # ─── Totais ───────────────────────────────────────────────────────────────
    total_ced = (
        int(dados.get("cedulas_200", 0) or 0) * 200 +
        int(dados.get("cedulas_100", 0) or 0) * 100 +
        int(dados.get("cedulas_50",  0) or 0) * 50  +
        int(dados.get("cedulas_20",  0) or 0) * 20  +
        int(dados.get("cedulas_10",  0) or 0) * 10  +
        int(dados.get("cedulas_5",   0) or 0) * 5   +
        int(dados.get("cedulas_2",   0) or 0) * 2
    )
    total_moe = round(
        int(dados.get("moedas_100", 0) or 0) * 1.0  +
        int(dados.get("moedas_050", 0) or 0) * 0.5  +
        int(dados.get("moedas_025", 0) or 0) * 0.25 +
        int(dados.get("moedas_010", 0) or 0) * 0.1  +
        int(dados.get("moedas_005", 0) or 0) * 0.05,
        2,
    )
    total_din  = round(total_ced + total_moe, 2)

    total_cred = round(sum(float(l.get("credito", 0) or 0) for l in lancamentos_maq), 2)
    total_deb  = round(sum(float(l.get("debito",  0) or 0) for l in lancamentos_maq), 2)
    total_pix  = round(sum(float(l.get("pix",     0) or 0) for l in lancamentos_maq), 2)

    erp_din  = float(dados.get("erp_dinheiro", 0) or 0)
    erp_cred = float(dados.get("erp_credito",  0) or 0)
    erp_deb  = float(dados.get("erp_debito",   0) or 0)
    erp_pix  = float(dados.get("erp_pix",      0) or 0)
    sangria  = float(dados.get("sangria",       0) or 0)

    div_din  = round(total_din  - erp_din,  2)
    div_cred = round(total_cred - erp_cred, 2)
    div_deb  = round(total_deb  - erp_deb,  2)
    div_pix  = round(total_pix  - erp_pix,  2)

    conta_din = (dados.get("conta_dinheiro") or "").strip() or None
    conta_moe = (dados.get("conta_moedas")  or "").strip() or None
    hora_now  = datetime.now().strftime("%H:%M")

    with get_session() as s:
        # ─── Gravar FechamentoCaixa ───────────────────────────────────────────
        fc = FechamentoCaixa()
        fc.data           = data_fc
        fc.turno_id       = dados.get("turno_id") or None
        fc.operador_id    = dados.get("operador_id") or None
        fc.apoio_id       = dados.get("apoio_id") or None
        fc.valor_envelope = float(dados.get("valor_envelope", 0) or 0)
        fc.cedulas_200    = int(dados.get("cedulas_200", 0) or 0)
        fc.cedulas_100    = int(dados.get("cedulas_100", 0) or 0)
        fc.cedulas_50     = int(dados.get("cedulas_50",  0) or 0)
        fc.cedulas_20     = int(dados.get("cedulas_20",  0) or 0)
        fc.cedulas_10     = int(dados.get("cedulas_10",  0) or 0)
        fc.cedulas_5      = int(dados.get("cedulas_5",   0) or 0)
        fc.cedulas_2      = int(dados.get("cedulas_2",   0) or 0)
        fc.moedas_100     = int(dados.get("moedas_100", 0) or 0)
        fc.moedas_050     = int(dados.get("moedas_050", 0) or 0)
        fc.moedas_025     = int(dados.get("moedas_025", 0) or 0)
        fc.moedas_010     = int(dados.get("moedas_010", 0) or 0)
        fc.moedas_005     = int(dados.get("moedas_005", 0) or 0)
        fc.erp_dinheiro   = erp_din
        fc.erp_credito    = erp_cred
        fc.erp_debito     = erp_deb
        fc.erp_pix        = erp_pix
        fc.sangria        = sangria
        fc.conta_dinheiro = conta_din
        fc.conta_moedas   = conta_moe or conta_din
        fc.div_dinheiro   = div_din
        fc.div_credito    = div_cred
        fc.div_debito     = div_deb
        fc.div_pix        = div_pix
        fc.obs = (dados.get("obs") or "").strip() or None
        s.add(fc)
        s.flush()

        # ─── Gravar LancamentoMaquininha ──────────────────────────────────────
        for lm in lancamentos_maq:
            maq_id = lm.get("maquininha_id")
            if not maq_id:
                continue
            row = LancamentoMaquininha()
            row.fechamento_id = fc.id
            row.maquininha_id = maq_id
            row.credito = float(lm.get("credito", 0) or 0)
            row.debito  = float(lm.get("debito",  0) or 0)
            row.pix     = float(lm.get("pix",     0) or 0)
            s.add(row)

        # ─── Rótulo de turno para historicos ─────────────────────────────────
        data_str = data_fc.strftime("%d/%m/%Y")
        turno_tag = ""
        if fc.turno_id:
            t = s.get(Turno, fc.turno_id)
            turno_tag = f" [{t.abreviacao}]" if t else ""

        def _post(descricao, valor, conta):
            """Cria Titulo receber já pago + LancamentoBaixa."""
            tit = Titulo()
            tit.tipo           = "receber"
            tit.descricao      = descricao
            tit.valor          = round(valor, 2)
            tit.vencimento     = data_fc
            tit.data_emissao   = data_fc
            tit.conta_nome     = conta
            tit.status         = "pago"
            tit.valor_pago     = round(valor, 2)
            tit.data_pagamento = data_fc
            s.add(tit)
            s.flush()
            lanc = LancamentoBaixa()
            lanc.titulo_id  = tit.id
            lanc.data       = data_fc
            lanc.hora       = hora_now
            lanc.historico  = descricao.upper()
            lanc.conta_nome = conta
            lanc.valor      = round(valor, 2)
            s.add(lanc)

        # ─── Auto-postagem: cédulas → conta_dinheiro ──────────────────────────
        if total_ced > 0 and conta_din:
            _post(f"Fechamento Caixa - Cédulas{turno_tag} {data_str}", total_ced, conta_din)

        # ─── Auto-postagem: moedas → conta_moedas (ou conta_dinheiro) ─────────
        conta_moe_ef = conta_moe or conta_din
        if total_moe > 0 and conta_moe_ef:
            _post(f"Fechamento Caixa - Moedas{turno_tag} {data_str}", total_moe, conta_moe_ef)

        # ─── Auto-postagem: maquininhas ───────────────────────────────────────
        for lm in lancamentos_maq:
            maq_id = lm.get("maquininha_id")
            if not maq_id:
                continue
            maq = s.get(MaquininhaCaixa, maq_id)
            if not maq or not maq.conta_destino:
                continue
            total_maq = round(
                float(lm.get("credito", 0) or 0) +
                float(lm.get("debito",  0) or 0) +
                float(lm.get("pix",     0) or 0),
                2,
            )
            if total_maq > 0:
                _post(f"Fechamento Caixa - {maq.nome}{turno_tag} {data_str}",
                      total_maq, maq.conta_destino)

        # ─── Auto-postagem: sangria → título A Pagar em aberto ───────────────
        if sangria > 0:
            tit = Titulo()
            tit.tipo         = "pagar"
            tit.descricao    = f"Sangria - Caixa{turno_tag} {data_str}"
            tit.valor        = round(sangria, 2)
            tit.vencimento   = data_fc
            tit.data_emissao = data_fc
            tit.status       = "aberto"
            s.add(tit)

        s.commit()
        return fc.id


def buscar_fechamento(id_: int) -> dict | None:
    with get_session() as s:
        obj = s.get(FechamentoCaixa, id_)
        return obj.to_dict() if obj else None


def atualizar_fechamento(id_: int, dados: dict, lancamentos_maq: list[dict]) -> None:
    """
    Edita um fechamento já gravado. Recalcula divergências e atualiza
    lancamentos_maquininha, mas NÃO altera os lançamentos financeiros já postados.
    Registra editado_em e acrescenta ao log_edicao (JSON array).
    """
    import json
    data_fc = _parse_date_titulo(dados.get("data")) or date.today()

    total_ced = (
        int(dados.get("cedulas_200", 0) or 0) * 200 +
        int(dados.get("cedulas_100", 0) or 0) * 100 +
        int(dados.get("cedulas_50",  0) or 0) * 50  +
        int(dados.get("cedulas_20",  0) or 0) * 20  +
        int(dados.get("cedulas_10",  0) or 0) * 10  +
        int(dados.get("cedulas_5",   0) or 0) * 5   +
        int(dados.get("cedulas_2",   0) or 0) * 2
    )
    total_moe = round(
        int(dados.get("moedas_100", 0) or 0) * 1.0  +
        int(dados.get("moedas_050", 0) or 0) * 0.5  +
        int(dados.get("moedas_025", 0) or 0) * 0.25 +
        int(dados.get("moedas_010", 0) or 0) * 0.1  +
        int(dados.get("moedas_005", 0) or 0) * 0.05,
        2,
    )
    total_din  = round(total_ced + total_moe, 2)
    total_cred = round(sum(float(l.get("credito", 0) or 0) for l in lancamentos_maq), 2)
    total_deb  = round(sum(float(l.get("debito",  0) or 0) for l in lancamentos_maq), 2)
    total_pix  = round(sum(float(l.get("pix",     0) or 0) for l in lancamentos_maq), 2)

    erp_din  = float(dados.get("erp_dinheiro", 0) or 0)
    erp_cred = float(dados.get("erp_credito",  0) or 0)
    erp_deb  = float(dados.get("erp_debito",   0) or 0)
    erp_pix  = float(dados.get("erp_pix",      0) or 0)
    sangria  = float(dados.get("sangria",       0) or 0)

    with get_session() as s:
        fc = s.get(FechamentoCaixa, id_)
        if not fc:
            raise ValueError(f"Fechamento #{id_} não encontrado.")

        fc.data           = data_fc
        fc.turno_id       = dados.get("turno_id") or None
        fc.operador_id    = dados.get("operador_id") or None
        fc.apoio_id       = dados.get("apoio_id") or None
        fc.valor_envelope = float(dados.get("valor_envelope", 0) or 0)
        fc.cedulas_200    = int(dados.get("cedulas_200", 0) or 0)
        fc.cedulas_100    = int(dados.get("cedulas_100", 0) or 0)
        fc.cedulas_50     = int(dados.get("cedulas_50",  0) or 0)
        fc.cedulas_20     = int(dados.get("cedulas_20",  0) or 0)
        fc.cedulas_10     = int(dados.get("cedulas_10",  0) or 0)
        fc.cedulas_5      = int(dados.get("cedulas_5",   0) or 0)
        fc.cedulas_2      = int(dados.get("cedulas_2",   0) or 0)
        fc.moedas_100     = int(dados.get("moedas_100", 0) or 0)
        fc.moedas_050     = int(dados.get("moedas_050", 0) or 0)
        fc.moedas_025     = int(dados.get("moedas_025", 0) or 0)
        fc.moedas_010     = int(dados.get("moedas_010", 0) or 0)
        fc.moedas_005     = int(dados.get("moedas_005", 0) or 0)
        fc.erp_dinheiro   = erp_din
        fc.erp_credito    = erp_cred
        fc.erp_debito     = erp_deb
        fc.erp_pix        = erp_pix
        fc.sangria        = sangria
        fc.div_dinheiro   = round(total_din  - erp_din,  2)
        fc.div_credito    = round(total_cred - erp_cred, 2)
        fc.div_debito     = round(total_deb  - erp_deb,  2)
        fc.div_pix        = round(total_pix  - erp_pix,  2)
        fc.obs            = (dados.get("obs") or "").strip() or None
        fc.editado_em     = datetime.now()

        # Append to log
        try:
            log = json.loads(fc.log_edicao or "[]")
        except Exception:
            log = []
        log.append({"em": fc.editado_em.isoformat()})
        fc.log_edicao = json.dumps(log, ensure_ascii=False)

        # Replace lancamentos_maquininha
        for lm in list(fc.lancamentos_maq):
            s.delete(lm)
        s.flush()
        for lm in lancamentos_maq:
            maq_id = lm.get("maquininha_id")
            if not maq_id:
                continue
            row = LancamentoMaquininha()
            row.fechamento_id = fc.id
            row.maquininha_id = maq_id
            row.credito = float(lm.get("credito", 0) or 0)
            row.debito  = float(lm.get("debito",  0) or 0)
            row.pix     = float(lm.get("pix",     0) or 0)
            s.add(row)

        s.commit()


def deletar_fechamento(id_: int) -> None:
    with get_session() as s:
        obj = s.get(FechamentoCaixa, id_)
        if obj:
            s.delete(obj)
            s.commit()


# ══════════════════════════════════════════════════════════════════════════════
# MÓDULO: DASHBOARD — KPIs e saldos em tempo real
# ══════════════════════════════════════════════════════════════════════════════

def saldo_por_conta() -> list[dict]:
    """Saldo atual de cada conta ativa: saldo_inicial + entradas − saídas."""
    with get_session() as s:
        contas = (
            s.query(ContaFinanceira)
            .filter(ContaFinanceira.ativo == True)  # noqa: E712
            .order_by(ContaFinanceira.nome)
            .all()
        )
        result = []
        for conta in contas:
            entradas = (
                s.query(_func.sum(LancamentoBaixa.valor))
                .join(Titulo, LancamentoBaixa.titulo_id == Titulo.id)
                .filter(
                    LancamentoBaixa.conta_nome == conta.nome,
                    Titulo.tipo == "receber",
                )
                .scalar()
            ) or 0.0

            saidas = (
                s.query(_func.sum(LancamentoBaixa.valor))
                .join(Titulo, LancamentoBaixa.titulo_id == Titulo.id)
                .filter(
                    LancamentoBaixa.conta_nome == conta.nome,
                    Titulo.tipo == "pagar",
                )
                .scalar()
            ) or 0.0

            saldo = round((conta.saldo_inicial or 0.0) + entradas - saidas, 2)
            result.append({
                "id":            conta.id,
                "nome":          conta.nome,
                "tipo":          conta.tipo,
                "saldo_inicial": conta.saldo_inicial or 0.0,
                "entradas":      round(entradas, 2),
                "saidas":        round(saidas, 2),
                "saldo_atual":   saldo,
            })
        return result


def titulos_a_vencer(dias: int = 7, tipo: str | None = None) -> list[dict]:
    """Títulos abertos/parciais com vencimento entre hoje e hoje+dias (inclusive)."""
    _atualizar_vencidos()
    hoje   = date.today()
    limite = hoje + timedelta(days=dias)
    with get_session() as s:
        q = s.query(Titulo).filter(
            Titulo.status.in_(["aberto", "parcial"]),
            Titulo.vencimento >= hoje,
            Titulo.vencimento <= limite,
        )
        if tipo:
            q = q.filter(Titulo.tipo == tipo)
        return [t.to_dict() for t in q.order_by(Titulo.vencimento).all()]


def titulos_vencidos(tipo: str | None = None) -> list[dict]:
    """Títulos com status 'vencido', opcionalmente filtrados por tipo."""
    _atualizar_vencidos()
    with get_session() as s:
        q = s.query(Titulo).filter(Titulo.status == "vencido")
        if tipo:
            q = q.filter(Titulo.tipo == tipo)
        return [t.to_dict() for t in q.order_by(Titulo.vencimento).all()]


def resumo_financeiro() -> dict:
    """KPIs do dashboard: saldo total, a receber/pagar em 30 dias, vencidos."""
    _atualizar_vencidos()
    hoje = date.today()
    d30  = hoje + timedelta(days=30)

    with get_session() as s:
        rows = [
            {
                "tipo":       t.tipo,
                "status":     t.status,
                "valor":      t.valor or 0.0,
                "valor_pago": t.valor_pago or 0.0,
                "vencimento": t.vencimento,
            }
            for t in s.query(Titulo).filter(
                Titulo.status.in_(["aberto", "parcial", "vencido"])
            ).all()
        ]

    a_rec = sum(
        d["valor"] - d["valor_pago"]
        for d in rows
        if d["tipo"] == "receber"
        and d["status"] in ("aberto", "parcial")
        and d["vencimento"] and hoje <= d["vencimento"] <= d30
    )
    a_pag = sum(
        d["valor"] - d["valor_pago"]
        for d in rows
        if d["tipo"] == "pagar"
        and d["status"] in ("aberto", "parcial")
        and d["vencimento"] and hoje <= d["vencimento"] <= d30
    )
    v_pag = sum(
        d["valor"] - d["valor_pago"]
        for d in rows
        if d["tipo"] == "pagar" and d["status"] == "vencido"
    )

    contas      = saldo_por_conta()
    saldo_total = sum(c["saldo_atual"] for c in contas)

    return {
        "saldo_total":    round(saldo_total, 2),
        "a_receber_30d":  round(max(a_rec, 0.0), 2),
        "a_pagar_30d":    round(max(a_pag, 0.0), 2),
        "vencidos_pagar": round(max(v_pag, 0.0), 2),
    }
