<?php
/**
 * notificacoes.php — Substituição de tags dinâmicas e disparo multicanal
 * (WhatsApp via fila, SMS via gateway HTTP, E-mail via PHPMailer).
 *
 * OBS: PHPMailer não está vendorizado aqui — se você usar Composer, rode
 * `composer require phpmailer/phpmailer` na raiz e ajuste o require abaixo.
 */

require_once __DIR__ . '/db_helper.php';
require_once __DIR__ . '/auth.php';

/**
 * Sobe a cadeia de "quem criou quem" (o próprio usuário, depois quem o
 * criou, e assim por diante) procurando o primeiro que tenha um valor
 * "configurado de verdade" para a chave pedida, segundo $estaConfigurado.
 * Se ninguém na cadeia tiver, retorna null (quem chamou decide o fallback
 * pro padrão do sistema).
 */
function resolver_config_hierarquica(?string $idUsuario, string $chave, callable $estaConfigurado)
{
    $usuarios = db_read('usuarios');
    $porId = [];
    $idAdmRaiz = null;
    foreach ($usuarios as $u) {
        $porId[$u['id']] = $u;
        if (($u['nivel'] ?? '') === 'ADM' && empty($u['criado_por'])) {
            $idAdmRaiz = $u['id'];
        }
    }

    $idAtual = $idUsuario;
    $tentativas = 0;
    $chegouNoAdm = false;
    while ($idAtual && isset($porId[$idAtual]) && $tentativas < 10) {
        $usuario = $porId[$idAtual];
        $valor = $usuario[$chave] ?? null;
        if ($valor !== null && $estaConfigurado($valor)) {
            return $valor;
        }
        if ($idAtual === $idAdmRaiz) {
            $chegouNoAdm = true;
        }
        $idAtual = $usuario['criado_por'] ?? null;
        $tentativas++;
    }

    // Se a cadeia de "quem criou quem" não chegou até o ADM raiz (ex.:
    // operador vazio, "SISTEMA", ou usuário removido), ainda assim usa a
    // config do ADM como último fallback — ele é quem define o "padrão do
    // sistema" (configurando em Minha Conta), sem precisar de uma tela de
    // config global separada e duplicada.
    if (!$chegouNoAdm && $idAdmRaiz && isset($porId[$idAdmRaiz])) {
        $valor = $porId[$idAdmRaiz][$chave] ?? null;
        if ($valor !== null && $estaConfigurado($valor)) {
            return $valor;
        }
    }

    return null;
}

/**
 * Resolve qual chave PIX usar para um envio, respeitando a hierarquia:
 * primeiro tenta o PIX do próprio usuário (ADM, Revenda ou Sub-Revenda);
 * se não tiver configurado, sobe para quem o criou; se ninguém na cadeia
 * tiver, cai no PIX padrão do sistema (db/config.json).
 */
function resolver_pix_para_usuario(?string $idOperador): array
{
    $config = db_read('config');
    $pixPadraoSistema = $config['pix_manual'] ?? ['chave' => '', 'nome_beneficiario' => '', 'instrucoes' => ''];

    $pixUsuario = resolver_config_hierarquica($idOperador, 'pix_manual', function ($pix) {
        return !empty(trim($pix['chave'] ?? ''));
    });

    return $pixUsuario ?? $pixPadraoSistema;
}

/**
 * Retorna o texto do PIX manual pronto para entrar no lugar da tag {PIX}
 * nas mensagens, respeitando a hierarquia por usuário (cada Revenda/Sub-
 * Revenda pode ter sua própria chave PIX; se não configurar, usa a de
 * quem a criou, ou a padrão do sistema).
 */
function obter_pix_texto(?string $idOperador = null): string
{
    $pix = resolver_pix_para_usuario($idOperador);
    $chave = trim($pix['chave'] ?? '');

    if (!$chave) {
        return 'Chave PIX não configurada. Peça para o administrador cadastrar em Configurações > PIX.';
    }

    $partes = ["Chave PIX: {$chave}"];
    if (!empty($pix['nome_beneficiario'])) {
        $partes[] = "Beneficiário: {$pix['nome_beneficiario']}";
    }
    if (!empty($pix['instrucoes'])) {
        $partes[] = trim($pix['instrucoes']);
    }
    return implode("\n", $partes);
}

/**
 * Resolve quais canais (WhatsApp/E-mail/SMS) usar para cada tipo de envio
 * (cobrança, renovação, teste grátis), respeitando a hierarquia: o próprio
 * usuário → quem o criou → padrão do sistema. Cada usuário pode escolher
 * canais diferentes dos outros (ex.: uma Revenda usa só e-mail, outra usa
 * WhatsApp + SMS).
 */
function resolver_canais_para_usuario(?string $idOperador): array
{
    $config = db_read('config');
    $padraoSistema = $config['canais_notificacao'] ?? [
        'cobranca' => ['whatsapp' => true, 'email' => true, 'sms' => false],
        'renovacao' => ['whatsapp' => true, 'email' => true, 'sms' => false],
        'teste' => ['whatsapp' => true, 'email' => true, 'sms' => true],
    ];

    $canaisUsuario = resolver_config_hierarquica($idOperador, 'canais_notificacao', function ($c) {
        return is_array($c) && !empty($c);
    });

    return $canaisUsuario ?? $padraoSistema;
}

/**
 * Resolve qual gateway de SMS (celular Android) usar para um envio,
 * respeitando a mesma hierarquia do SMTP/PIX: próprio usuário → quem o
 * criou → padrão do sistema.
 */
function resolver_sms_gateway_para_usuario(?string $idOperador): array
{
    $config = db_read('config');
    $gwPadraoSistema = $config['sms_gateway'] ?? ['url_api' => '', 'usuario' => '', 'senha' => ''];

    $gwUsuario = resolver_config_hierarquica($idOperador, 'sms_gateway', function ($gw) {
        return !empty(trim($gw['url_api'] ?? '')) && !empty(trim($gw['usuario'] ?? '')) && !empty(trim($gw['senha'] ?? ''));
    });

    return $gwUsuario ?? $gwPadraoSistema;
}

/**
 * Resolve quais templates de mensagem usar para um envio, respeitando a
 * hierarquia: o usuário pode sobrescrever qualquer template individual
 * (ex.: só o "boas_vindas"); os que ele não sobrescrever continuam vindo
 * de quem o criou, ou do padrão global do sistema (db/mensagens.json).
 */
function resolver_mensagens_para_usuario(?string $idOperador): array
{
    $mensagensPadrao = db_read('mensagens');

    if (!$idOperador) {
        return $mensagensPadrao;
    }

    $usuarios = db_read('usuarios');
    $porId = [];
    foreach ($usuarios as $u) {
        $porId[$u['id']] = $u;
    }

    $chavesTemplate = ['boas_vindas', 'aviso_3_dias', 'aviso_vencimento_dia', 'confirmacao_renovacao', 'aviso_pos_vencimento', 'aviso_expirado', 'teste_expirando', 'teste_aviso_previo'];
    $resolvidas = $mensagensPadrao;

    // Monta a cadeia do mais distante (raiz) pro mais próximo (o próprio
    // usuário), aplicando as sobrescritas em ordem — assim quem está mais
    // perto do envio (o próprio usuário) tem prioridade sobre quem o criou.
    $cadeia = [];
    $idAtual = $idOperador;
    $tentativas = 0;
    while ($idAtual && isset($porId[$idAtual]) && $tentativas < 10) {
        $cadeia[] = $porId[$idAtual];
        $idAtual = $porId[$idAtual]['criado_por'] ?? null;
        $tentativas++;
    }
    $cadeia = array_reverse($cadeia);

    foreach ($cadeia as $usuario) {
        $overrides = $usuario['mensagens'] ?? [];
        foreach ($chavesTemplate as $chave) {
            // Funciona tanto pros templates de texto único (string) quanto
            // pros rotativos (array de variações) — em ambos os casos,
            // "vazio" (string vazia, ou array vazio/só com strings vazias)
            // significa "esse usuário não personalizou, mantém o herdado".
            $valorOverride = $overrides[$chave] ?? '';
            if (!empty(normalizar_lista_variacoes($valorOverride))) {
                $resolvidas[$chave] = $valorOverride;
            }
        }
    }

    return $resolvidas;
}

/**
 * Calcula o valor efetivo a cobrar de um cliente: o valor do plano menos o
 * desconto cadastrado, SE o desconto estiver marcado como ativo. Nunca
 * fica negativo. Usado nas mensagens ({VALOR}), no lançamento financeiro
 * automático (Pago = Sim) e nas renovações (o desconto, se ativo, se
 * aplica todo mês automaticamente).
 */
function valor_efetivo_cliente(array $cliente): float
{
    $valor = (float) ($cliente['valor'] ?? 0);
    if (!empty($cliente['desconto_ativo'])) {
        $valor -= (float) ($cliente['desconto_valor'] ?? 0);
    }
    return max(0, round($valor, 2));
}

/**
 * Substitui as tags {NOME}, {WHATSAPP}, {USUARIO}, {SENHA}, {VENCIMENTO},
 * {VALOR}, {PIX}, {SERVIDOR} pelos dados reais do cliente.
 */
/**
 * Normaliza uma "lista de variações" de template — aceita tanto o formato
 * novo (array de textos) quanto o formato antigo (uma única string), para
 * templates cadastrados antes desse recurso existir continuarem
 * funcionando sem precisar converter nada manualmente.
 */
function normalizar_lista_variacoes($valor): array
{
    if (is_array($valor)) {
        return array_values(array_filter(array_map('strval', $valor), fn($v) => trim($v) !== ''));
    }
    $valor = trim((string) $valor);
    return $valor !== '' ? [$valor] : [];
}

/**
 * Escolhe a variação "da vez" para ESTE cliente, em rodízio sequencial
 * (nunca aleatório) — cada cliente guarda sua própria posição em
 * $cliente['rotacao'][$chave], independente dos outros clientes e dos
 * outros tipos de rodízio. NÃO avança nada sozinho (uso seguro em
 * pré-visualização/cópia); o avanço só acontece via avancar_rotacao_cliente().
 */
function escolher_variacao_cliente(array $cliente, $variacoesBrutas, string $chave): string
{
    $variacoes = normalizar_lista_variacoes($variacoesBrutas);
    if (empty($variacoes)) {
        return '';
    }
    $posicao = (int) ($cliente['rotacao'][$chave] ?? 0);
    return $variacoes[$posicao % count($variacoes)];
}

/**
 * Avança o rodízio de UM cliente para uma ou mais chaves de uma vez só.
 * Só deve ser chamado depois que uma mensagem foi REALMENTE enviada
 * (nunca em pré-visualização/cópia), senão o rodízio "pula" posições sem
 * o cliente nunca ter recebido de fato aquela variação.
 */
function avancar_rotacao_cliente(string $idCliente, array $chaves): void
{
    if (empty($idCliente)) {
        return;
    }
    db_update('clientes', function (&$clientes) use ($idCliente, $chaves) {
        foreach ($clientes as &$c) {
            if ($c['id'] === $idCliente) {
                foreach ($chaves as $chave) {
                    $c['rotacao'][$chave] = ((int) ($c['rotacao'][$chave] ?? 0)) + 1;
                }
                break;
            }
        }
    });
}

function montar_mensagem(string $template, array $cliente, ?int $diasRestantes = null): string
{
    // Resolve o PIX pela hierarquia do dono do cliente (próprio usuário →
    // quem o criou → padrão do sistema) e expõe cada campo como uma tag
    // separada, com o valor CRU cadastrado — sem nenhum rótulo/formatação
    // adicionado pelo sistema. Quem decide como rotular no texto final é
    // o próprio template (ex.: "Chave PIX: {PIX}", "Nome: {BENEFICIARIO}").
    $pix = resolver_pix_para_usuario($cliente['criado_por'] ?? null);

    // {SAUDACAO} e {DESPEDIDA} são tags rotativas: a lista de opções é
    // global (Mensagens > Variações), e cada cliente guarda sua própria
    // posição no rodízio — assim o mesmo cliente não recebe sempre a
    // mesma palavra, mas a escolha aqui é só de LEITURA (não avança o
    // rodízio; isso só acontece quando a mensagem é realmente enviada,
    // via avancar_rotacao_cliente()).
    $config = db_read('config');
    $saudacoes = $config['saudacoes'] ?? ['Olá'];
    $despedidas = $config['despedidas'] ?? ['Atenciosamente'];

    $mapa = [
        '{NOME}' => $cliente['nome'] ?? '',
        '{WHATSAPP}' => $cliente['whatsapp'] ?? '',
        '{USUARIO}' => $cliente['usuario_painel'] ?? '',
        '{SENHA}' => $cliente['senha_painel'] ?? '',
        '{VENCIMENTO}' => isset($cliente['vencimento']) ? date('d/m/Y', strtotime($cliente['vencimento'])) : '',
        '{VENCIMENTO_HORA}' => isset($cliente['vencimento']) ? date('d/m/Y H:i', strtotime($cliente['vencimento'])) : '',
        '{VALOR}' => isset($cliente['valor']) ? 'R$ ' . number_format(valor_efetivo_cliente($cliente), 2, ',', '.') : '',
        '{PIX}' => $pix['chave'] ?? '',
        '{BENEFICIARIO}' => $pix['nome_beneficiario'] ?? '',
        '{PIX_INSTRUCOES}' => $pix['instrucoes'] ?? '',
        '{SERVIDOR}' => $cliente['servidor'] ?? '',
        '{DIAS}' => $diasRestantes !== null ? (string) $diasRestantes : '',
        '{SAUDACAO}' => escolher_variacao_cliente($cliente, $saudacoes, 'saudacao'),
        '{DESPEDIDA}' => escolher_variacao_cliente($cliente, $despedidas, 'despedida'),
    ];
    return strtr($template, $mapa);
}

/**
 * Coloca uma mensagem de WhatsApp na fila (fila_mensagens.json) para ser
 * consumida de forma cadenciada pelo worker processar_fila.php.
 */
function enfileirar_whatsapp(string $whatsapp, string $mensagem, string $idOperador, ?string $idCliente = null): void
{
    db_update('fila_mensagens', function (&$fila) use ($whatsapp, $mensagem, $idCliente, $idOperador) {
        $fila[] = [
            'id' => gerar_id('MSG-'),
            'whatsapp' => preg_replace('/\D/', '', $whatsapp),
            'mensagem' => $mensagem,
            'cliente_id' => $idCliente,
            'id_operador' => $idOperador,
            'status' => 'pendente', // pendente | enviado | erro
            'tentativas' => 0,
            'criado_em' => date('Y-m-d H:i:s'),
            'enviado_em' => null,
        ];
    });
}

/**
 * Coloca um e-mail na fila de envio agendado (fila_emails.json), para ser
 * disparado pelo worker cron/processar_fila_emails.php respeitando o
 * horário de início e o intervalo configurados em "Mensagens > Agendamento".
 * Usado pelos lembretes automáticos de vencimento (3/2/1 dias, no dia).
 */
function enfileirar_email(string $destinatario, string $assunto, string $corpoHtml, ?string $idCliente, ?string $idOperador): string
{
    $idGerado = gerar_id('MAIL-');
    db_update('fila_emails', function (&$fila) use ($destinatario, $assunto, $corpoHtml, $idCliente, $idOperador, $idGerado) {
        $fila[] = [
            'id' => $idGerado,
            'destinatario' => $destinatario,
            'assunto' => $assunto,
            'corpo_html' => $corpoHtml,
            'cliente_id' => $idCliente,
            'id_operador' => $idOperador,
            'status' => 'pendente', // pendente | enviado | erro
            'tentativas' => 0,
            'criado_em' => date('Y-m-d H:i:s'),
            'enviado_em' => null,
        ];
    });
    return $idGerado;
}


/**
 * Coloca um SMS na fila de envio agendado (fila_sms.json), para ser
 * disparado pelo worker cron/processar_fila_sms.php respeitando o
 * horário e intervalo configurados em "Mensagens > Agendamento".
 */
function enfileirar_sms(string $numero, string $mensagem, ?string $idCliente, ?string $idOperador): string
{
    $idGerado = gerar_id('SMS-');
    db_update('fila_sms', function (&$fila) use ($numero, $mensagem, $idCliente, $idOperador, $idGerado) {
        $fila[] = [
            'id' => $idGerado,
            'numero' => preg_replace('/\D/', '', $numero),
            'mensagem' => $mensagem,
            'cliente_id' => $idCliente,
            'id_operador' => $idOperador,
            'status' => 'pendente', // pendente | enviado | erro
            'tentativas' => 0,
            'criado_em' => date('Y-m-d H:i:s'),
            'enviado_em' => null,
        ];
    });
    return $idGerado;
}


function enviar_sms(string $numero, string $mensagem, string $idOperador): bool
{
    // Integração com o app "SMS Gateway for Android" (sms-gate.app), que
    // transforma um celular Android num gateway de SMS acessível pela
    // internet (modo "Cloud Server"). Documentação: https://docs.sms-gate.app
    // Resolve o gateway a usar respeitando a hierarquia (o próprio usuário,
    // depois quem o criou, até o padrão do sistema) — mesmo padrão do SMTP.
    $gw = resolver_sms_gateway_para_usuario($idOperador);
    $url = $gw['url_api'] ?? '';
    $usuario = trim($gw['usuario'] ?? '');
    $senha = trim($gw['senha'] ?? '');

    if (!$url || !$usuario || !$senha) {
        error_log('SMS não enviado: gateway de SMS (celular) ainda não configurado em Mensagens > SMS via Celular (ou em Minha Conta, para o usuário/operador).');
        return false;
    }

    $numeroLimpo = preg_replace('/\D/', '', $numero);
    // A API exige formato E.164 (+DDI+DDD+numero); como skipPhoneValidation
    // está ligado, aceitamos o número como veio, só garantindo o "+" na frente.
    $numeroE164 = '+' . $numeroLimpo;

    try {
        $ch = curl_init($url . '?skipPhoneValidation=true');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_TIMEOUT => 8,
            CURLOPT_CONNECTTIMEOUT => 4,
            CURLOPT_USERPWD => "{$usuario}:{$senha}", // Basic Auth
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
            CURLOPT_POSTFIELDS => json_encode([
                'textMessage' => ['text' => $mensagem],
                'phoneNumbers' => [$numeroE164],
                'ttl' => 3600, // expira se não conseguir enviar em 1h
            ]),
        ]);
        $resp = curl_exec($ch);
        $erroCurl = curl_error($ch);
        $codigo = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($erroCurl) {
            error_log("Erro cURL ao enviar SMS via celular: {$erroCurl}");
            auditoria_log($idOperador, 'SMS_FALHOU', "Para {$numeroE164} | erro de conexão: {$erroCurl}");
            return false;
        }

        $sucesso = $codigo >= 200 && $codigo < 300;
        auditoria_log($idOperador, $sucesso ? 'SMS_ENVIADO' : 'SMS_FALHOU', "Para {$numeroE164} | HTTP {$codigo}" . ($sucesso ? '' : ' | ' . substr((string) $resp, 0, 200)));
        return $sucesso;
    } catch (\Throwable $e) {
        error_log('Exceção ao enviar SMS via celular: ' . $e->getMessage());
        return false;
    }
}

/**
 * Envia e-mail em HTML. Requer PHPMailer instalado via Composer em /vendor.
 * Se PHPMailer não estiver disponível, faz fallback para mail() nativo.
 */
/**
 * Verifica se um bloco de configuração SMTP está de fato preenchido
 * (não é vazio nem um dos valores de exemplo/placeholder do sistema).
 */
function smtp_esta_configurado(?array $smtp): bool
{
    if (!$smtp) {
        return false;
    }
    $host = trim($smtp['host'] ?? '');
    $senha = trim($smtp['senha'] ?? '');
    $placeholdersHost = ['', 'smtp.seudominio.com'];
    $placeholdersSenha = ['', 'ALTERAR_SENHA_SMTP'];
    return !in_array($host, $placeholdersHost, true) && !in_array($senha, $placeholdersSenha, true);
}

/**
 * Resolve qual configuração SMTP usar para um envio, respeitando a
 * hierarquia: primeiro tenta o SMTP do próprio usuário (ADM, Revenda ou
 * Sub-Revenda); se não tiver configurado, sobe para quem o criou; se
 * ninguém na cadeia tiver, cai no SMTP padrão do sistema (db/config.json).
 */
function resolver_smtp_para_usuario(?string $idUsuario): array
{
    $config = db_read('config');
    $smtpPadraoSistema = $config['smtp'] ?? [];

    if (!$idUsuario) {
        return $smtpPadraoSistema;
    }

    $usuarios = db_read('usuarios');
    $porId = [];
    foreach ($usuarios as $u) {
        $porId[$u['id']] = $u;
    }

    $idAtual = $idUsuario;
    $tentativas = 0;
    while ($idAtual && isset($porId[$idAtual]) && $tentativas < 10) {
        $usuario = $porId[$idAtual];
        if (smtp_esta_configurado($usuario['smtp'] ?? null)) {
            return $usuario['smtp'];
        }
        $idAtual = $usuario['criado_por'] ?? null;
        $tentativas++;
    }

    return $smtpPadraoSistema;
}

function enviar_email(string $destinatario, string $assunto, string $corpoHtml, ?string $idOperador = null): bool
{
    // Resolve o SMTP a usar respeitando a hierarquia (o próprio usuário,
    // depois quem o criou, até o padrão do sistema).
    $smtp = resolver_smtp_para_usuario($idOperador);

    // Se nenhum SMTP na cadeia estiver configurado de verdade, nem tenta
    // conectar — evita travar a requisição por vários segundos tentando
    // um host que não existe/não está pronto.
    if (!smtp_esta_configurado($smtp)) {
        error_log('E-mail nao enviado: nenhum SMTP configurado na hierarquia do usuario ' . ($idOperador ?? '(nenhum)') . '. Configure em Minha Conta > E-mail (SMTP).');
        return false;
    }

    $autoload = __DIR__ . '/vendor/autoload.php';
    if (file_exists($autoload) && class_exists('\PHPMailer\PHPMailer\PHPMailer', false) === false) {
        require_once $autoload;
    }

    if (class_exists('\PHPMailer\PHPMailer\PHPMailer')) {
        $mail = new \PHPMailer\PHPMailer\PHPMailer(true);
        try {
            $mail->isSMTP();
            $mail->Host = $smtp['host'] ?? '';
            $mail->SMTPAuth = true;
            $mail->Username = $smtp['usuario'] ?? '';
            $mail->Password = $smtp['senha'] ?? '';
            $mail->SMTPSecure = $smtp['seguranca'] ?? 'tls';
            $mail->Port = (int) ($smtp['port'] ?? 587);
            $mail->CharSet = 'UTF-8';

            $mail->setFrom($smtp['usuario'] ?? 'no-reply@dominio.com', $smtp['remetente_nome'] ?? 'Notificações');
            $mail->addAddress($destinatario);
            $mail->isHTML(true);
            $mail->Subject = $assunto;
            $mail->Body = $corpoHtml;

            $mail->send();
            return true;
        } catch (\Exception $e) {
            error_log('Erro PHPMailer: ' . $mail->ErrorInfo);
            return false;
        }
    }

    // Sem PHPMailer instalado: usa um cliente SMTP simples via socket, que
    // funciona só com os dados de db/config.json (host, porta, usuário,
    // senha) — não depende de Composer nem de mail()/sendmail configurado.
    if (!empty($smtp['host'])) {
        $ok = smtp_enviar_email($smtp, $destinatario, $assunto, $corpoHtml);
        if ($ok) {
            return true;
        }
        // Se o SMTP falhar, cai para o fallback abaixo como última tentativa.
    }

    // Último fallback (requer sendmail configurado no servidor de hospedagem)
    $headers = "MIME-Version: 1.0\r\nContent-type:text/html;charset=UTF-8\r\n";
    $headers .= 'From: ' . ($smtp['remetente_nome'] ?? 'Notificações') . " <{$smtp['usuario']}>\r\n";
    return @mail($destinatario, $assunto, $corpoHtml, $headers);
}

/**
 * Cliente SMTP mínimo via socket (sem dependências externas). Suporta
 * autenticação com AUTH LOGIN e STARTTLS (porta 587) ou SSL direto (porta 465).
 */
function smtp_enviar_email(array $smtp, string $destinatario, string $assunto, string $corpoHtml): bool
{
    $host = $smtp['host'] ?? '';
    $porta = (int) ($smtp['port'] ?? 587);
    $usuario = $smtp['usuario'] ?? '';
    $senha = $smtp['senha'] ?? '';
    $seguranca = strtolower($smtp['seguranca'] ?? 'tls'); // 'tls' (STARTTLS) ou 'ssl' (direto)
    $remetenteNome = $smtp['remetente_nome'] ?? 'Notificações';

    $prefixo = ($seguranca === 'ssl') ? 'ssl://' : '';
    $fp = @stream_socket_client("{$prefixo}{$host}:{$porta}", $errno, $errstr, 6);
    if (!$fp) {
        error_log("Erro SMTP: não foi possível conectar em {$host}:{$porta} — {$errstr}");
        return false;
    }

    $ler = function () use ($fp) {
        $resposta = '';
        while ($linha = fgets($fp, 515)) {
            $resposta .= $linha;
            if (isset($linha[3]) && $linha[3] === ' ') break;
        }
        return $resposta;
    };
    $enviar = function (string $comando) use ($fp) {
        fwrite($fp, $comando . "\r\n");
    };
    $codigoOk = function (string $resposta, array $codigos) {
        $codigo = (int) substr($resposta, 0, 3);
        return in_array($codigo, $codigos, true);
    };

    $ler(); // saudação do servidor

    $enviar('EHLO ' . ($_SERVER['SERVER_NAME'] ?? 'cobranex.local'));
    $ler();

    if ($seguranca === 'tls') {
        $enviar('STARTTLS');
        $resp = $ler();
        if (!$codigoOk($resp, [220])) { fclose($fp); return false; }
        if (!stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
            fclose($fp);
            return false;
        }
        $enviar('EHLO ' . ($_SERVER['SERVER_NAME'] ?? 'cobranex.local'));
        $ler();
    }

    if ($usuario) {
        $enviar('AUTH LOGIN');
        $resp = $ler();
        if (!$codigoOk($resp, [334])) { fclose($fp); return false; }
        $enviar(base64_encode($usuario));
        $resp = $ler();
        if (!$codigoOk($resp, [334])) { fclose($fp); return false; }
        $enviar(base64_encode($senha));
        $resp = $ler();
        if (!$codigoOk($resp, [235])) {
            error_log('Erro SMTP: falha de autenticação (usuário/senha incorretos?)');
            fclose($fp);
            return false;
        }
    }

    $enviar("MAIL FROM:<{$usuario}>");
    $resp = $ler();
    if (!$codigoOk($resp, [250])) { fclose($fp); return false; }

    $enviar("RCPT TO:<{$destinatario}>");
    $resp = $ler();
    if (!$codigoOk($resp, [250, 251])) { fclose($fp); return false; }

    $enviar('DATA');
    $resp = $ler();
    if (!$codigoOk($resp, [354])) { fclose($fp); return false; }

    $assuntoCodificado = '=?UTF-8?B?' . base64_encode($assunto) . '?=';
    $corpoEscapado = str_replace("\n.", "\n..", $corpoHtml); // "dot-stuffing" do protocolo SMTP

    $mensagem = "From: {$remetenteNome} <{$usuario}>\r\n";
    $mensagem .= "To: <{$destinatario}>\r\n";
    $mensagem .= "Subject: {$assuntoCodificado}\r\n";
    $mensagem .= "MIME-Version: 1.0\r\n";
    $mensagem .= "Content-Type: text/html; charset=UTF-8\r\n";
    $mensagem .= "\r\n{$corpoEscapado}\r\n.";

    $enviar($mensagem);
    $resp = $ler();
    $enviar('QUIT');
    fclose($fp);

    return $codigoOk($resp, [250]);
}

/**
 * Dispara os 3 canais de uma vez para um cliente, usando o template informado.
 * Todos os canais são liberados sem limite por disparo (sem sistema de créditos).
 */
/**
 * Usado pelas ações MANUAIS (Cobrar, Mensagem avulsa, Renovar): registra o
 * SMS na fila (pra aparecer em "Filas de Envio" com histórico) e já tenta
 * enviar na hora — diferente do fluxo automático do cron, que só enfileira
 * e deixa o worker respeitar o horário/intervalo configurado.
 */
function enviar_sms_com_registro(string $numero, string $mensagem, ?string $idCliente, string $idOperador): bool
{
    $idItemFila = enfileirar_sms($numero, $mensagem, $idCliente, $idOperador);
    $ok = enviar_sms($numero, $mensagem, $idOperador);

    db_update('fila_sms', function (&$fila) use ($idItemFila, $ok) {
        foreach ($fila as &$item) {
            if ($item['id'] === $idItemFila) {
                $item['tentativas'] = 1;
                $item['status'] = $ok ? 'enviado' : 'erro';
                if ($ok) {
                    $item['enviado_em'] = date('Y-m-d H:i:s');
                }
                break;
            }
        }
    });

    return $ok;
}

function disparar_notificacao_multicanal(array $cliente, string $chaveTemplate): void
{
    $idOperador = $cliente['criado_por'] ?? '';
    $mensagens = resolver_mensagens_para_usuario($idOperador);
    $textoEscolhido = escolher_variacao_cliente($cliente, $mensagens[$chaveTemplate] ?? [], $chaveTemplate);
    if (!$textoEscolhido) {
        return;
    }

    $texto = montar_mensagem($textoEscolhido, $cliente);
    $enviouAlgo = false;

    if (!empty($cliente['whatsapp'])) {
        enfileirar_whatsapp($cliente['whatsapp'], $texto, $idOperador, $cliente['id'] ?? null);
        $enviouAlgo = true;
    }
    if (!empty($cliente['email'])) {
        $htmlSeguro = nl2br(htmlspecialchars($texto));
        enfileirar_email($cliente['email'], 'Notificação: ' . ($cliente['nome'] ?? ''), $htmlSeguro, $cliente['id'] ?? null, $idOperador);
        $enviouAlgo = true;
    }
    if (!empty($cliente['whatsapp'])) {
        // SMS usa o mesmo número de WhatsApp por padrão; ajuste se houver campo próprio
        enfileirar_sms($cliente['whatsapp'], $texto, $cliente['id'] ?? null, $idOperador);
    }

    // Só avança o rodízio (do template e das tags de saudação/despedida)
    // depois que a mensagem realmente foi para algum canal — nunca em
    // pré-visualização.
    if ($enviouAlgo) {
        avancar_rotacao_cliente($cliente['id'] ?? '', [$chaveTemplate, 'saudacao', 'despedida']);
    }
}
