<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Core\Auth;
use App\Core\Controller;
use App\Core\Database;
use App\Core\Logger;
use App\Core\Request;
use App\Core\Response;
use App\Core\Session;
use App\Models\GlobalSetting;
use App\Models\CompanySetting;
use App\Models\WhatsAppMetaTemplate;
use App\Services\AiHelpService;
use App\Services\AuditService;
use App\Services\FactusService;
use App\Services\NotificationChannelService;
use App\Services\OpenAiGovernanceService;
use App\Services\RenewalEmailService;
use App\Services\SaasCheckoutRecoveryService;
use App\Services\SecretCipherService;
use App\Services\SubscriptionAutomationService;
use App\Services\WhatsAppTemplateService;
use App\Services\WompiService;
use PDO;

class SuperadminController extends Controller
{
    public function index(Request $request): void
    {
        $pdo = Database::connection();
        $this->syncSupportSlaStates($pdo);
        [$supportFrom, $supportTo] = $this->resolveSupportReportRange($request);

        $stats = [
            'empresas' => (int) $pdo->query('SELECT COUNT(*) FROM empresas')->fetchColumn(),
            'empresas_activas' => (int) $pdo->query('SELECT COUNT(*) FROM empresas WHERE estado = "activa"')->fetchColumn(),
            'empresas_suspendidas' => (int) $pdo->query('SELECT COUNT(*) FROM empresas WHERE estado = "suspendida"')->fetchColumn(),
            'empresas_bloqueadas' => (int) $pdo->query('SELECT COUNT(*) FROM empresas WHERE estado = "bloqueada"')->fetchColumn(),
            'suscripciones_activas' => (int) $pdo->query('SELECT COUNT(*) FROM suscripciones WHERE estado = "activa" AND fecha_vencimiento >= CURDATE()')->fetchColumn(),
            'suscripciones_alerta' => (int) $pdo->query('SELECT COUNT(*) FROM suscripciones WHERE estado IN ("vencida","morosa") OR fecha_vencimiento < CURDATE()')->fetchColumn(),
            'ingresos_mes' => (float) $pdo->query('SELECT COALESCE(SUM(monto),0) FROM pagos_suscripcion WHERE estado = "pagado" AND DATE_FORMAT(fecha_pago, "%Y-%m") = DATE_FORMAT(CURDATE(), "%Y-%m")')->fetchColumn(),
            'pagos_pendientes' => (int) $pdo->query('SELECT COUNT(*) FROM pagos_suscripcion WHERE estado = "pendiente"')->fetchColumn(),
            'notices_pending' => (int) $pdo->query('SELECT COUNT(*) FROM avisos_cobro_suscripcion WHERE estado = "pendiente"')->fetchColumn(),
            'notices_error' => (int) $pdo->query('SELECT COUNT(*) FROM avisos_cobro_suscripcion WHERE estado = "error"')->fetchColumn(),
            'support_open' => (int) $pdo->query('SELECT COUNT(*) FROM soporte_tickets WHERE estado IN ("abierto","en_proceso","pendiente_cliente")')->fetchColumn(),
        ];

        $plans = $pdo->query(
            'SELECT p.*,
                    (SELECT COUNT(*) FROM suscripciones s WHERE s.plan_id = p.id) AS empresas_en_plan
             FROM planes p
             ORDER BY p.precio_mensual ASC, p.id ASC'
        )->fetchAll() ?: [];

        $companies = $pdo->query(
            'SELECT
                e.id,
                e.nombre,
                e.email,
                e.nit,
                e.telefono,
                e.direccion,
                e.estado,
                e.created_at,
                ls.id AS suscripcion_id,
                ls.estado AS suscripcion_estado,
                ls.fecha_inicio AS suscripcion_inicio,
                ls.fecha_vencimiento AS suscripcion_vencimiento,
                ls.plan_id AS suscripcion_plan_id,
                p.nombre AS plan_nombre,
                p.permite_facturacion_electronica AS plan_permite_facturacion_electronica
             FROM empresas e
             LEFT JOIN (
                SELECT s1.*
                FROM suscripciones s1
                INNER JOIN (
                    SELECT empresa_id, MAX(id) AS max_id
                    FROM suscripciones
                    GROUP BY empresa_id
                ) ux ON ux.max_id = s1.id
             ) ls ON ls.empresa_id = e.id
             LEFT JOIN planes p ON p.id = ls.plan_id
             ORDER BY e.id DESC
             LIMIT 120'
        )->fetchAll() ?: [];
        $this->hydrateCompaniesFactusOverview($companies);

        $subscriptions = $pdo->query(
            'SELECT
                s.id,
                s.empresa_id,
                e.nombre AS empresa,
                s.plan_id,
                p.nombre AS plan,
                s.fecha_inicio,
                s.fecha_vencimiento,
                s.estado,
                s.trial,
                s.auto_renovacion,
                s.updated_at
             FROM suscripciones s
             INNER JOIN empresas e ON e.id = s.empresa_id
             LEFT JOIN planes p ON p.id = s.plan_id
             ORDER BY s.id DESC
             LIMIT 160'
        )->fetchAll() ?: [];

        $payments = $pdo->query(
            'SELECT
                ps.id,
                ps.empresa_id,
                e.nombre AS empresa,
                ps.suscripcion_id,
                p.nombre AS plan,
                ps.pasarela,
                ps.referencia,
                ps.monto,
                ps.moneda,
                ps.estado,
                ps.fecha_pago
             FROM pagos_suscripcion ps
             INNER JOIN empresas e ON e.id = ps.empresa_id
             LEFT JOIN suscripciones s ON s.id = ps.suscripcion_id
             LEFT JOIN planes p ON p.id = s.plan_id
             ORDER BY ps.id DESC
             LIMIT 160'
        )->fetchAll() ?: [];

        $checkoutIntents = $pdo->query(
            'SELECT
                sci.id,
                sci.reference,
                sci.plan_id,
                p.nombre AS plan_nombre,
                sci.empresa_nombre,
                sci.empresa_slug,
                sci.admin_email,
                sci.currency,
                sci.amount_in_cents,
                sci.estado,
                sci.wompi_status,
                sci.wompi_transaction_id,
                sci.failure_reason,
                sci.created_at,
                sci.updated_at
             FROM saas_checkout_intents sci
             LEFT JOIN planes p ON p.id = sci.plan_id
             WHERE sci.estado <> "provisioned"
             ORDER BY sci.id DESC
             LIMIT 200'
        )->fetchAll() ?: [];

        $supportAgents = $pdo->query(
            'SELECT u.id, u.nombre, u.email
             FROM usuarios u
             INNER JOIN roles r ON r.id = u.rol_id
             WHERE r.slug = "superadmin"
               AND u.activo = 1
             ORDER BY u.nombre ASC'
        )->fetchAll() ?: [];

        $supportTickets = $pdo->query(
            'SELECT
                t.id,
                t.empresa_id,
                e.nombre AS empresa,
                t.creado_por_usuario_id,
                uc.nombre AS creado_por,
                t.asignado_a_usuario_id,
                ua.nombre AS asignado_a,
                t.asunto,
                t.descripcion,
                t.prioridad,
                t.estado,
                t.canal,
                t.created_at,
                t.updated_at,
                t.primera_respuesta_at,
                t.primera_respuesta_limite_at,
                t.resolucion_limite_at,
                t.objetivo_primera_respuesta_min,
                t.objetivo_resolucion_horas,
                t.sla_primera_respuesta_estado,
                t.sla_resolucion_estado,
                t.cerrado_at,
                t.resuelto_at
             FROM soporte_tickets t
             INNER JOIN empresas e ON e.id = t.empresa_id
             LEFT JOIN usuarios uc ON uc.id = t.creado_por_usuario_id
             LEFT JOIN usuarios ua ON ua.id = t.asignado_a_usuario_id
             ORDER BY t.updated_at DESC, t.id DESC
             LIMIT 200'
        )->fetchAll() ?: [];

        $supportEventsByTicket = [];
        $ticketIds = array_map('intval', array_column($supportTickets, 'id'));
        if ($ticketIds !== []) {
            $placeholders = implode(', ', array_fill(0, count($ticketIds), '?'));
            $eventsStmt = $pdo->prepare(
                'SELECT
                    te.id,
                    te.ticket_id,
                    te.usuario_id,
                    u.nombre AS usuario,
                    te.tipo,
                    te.mensaje,
                    te.estado_anterior,
                    te.estado_nuevo,
                    te.created_at
                 FROM soporte_ticket_eventos te
                 LEFT JOIN usuarios u ON u.id = te.usuario_id
                 WHERE te.ticket_id IN (' . $placeholders . ')
                 ORDER BY te.id DESC'
            );
            $eventsStmt->execute($ticketIds);
            $events = $eventsStmt->fetchAll() ?: [];
            foreach ($events as $event) {
                $supportEventsByTicket[(int) ($event['ticket_id'] ?? 0)][] = $event;
            }
        }

        $supportSlaSummaryStmt = $pdo->prepare(
            'SELECT
                COUNT(*) AS total_tickets,
                SUM(CASE WHEN t.estado IN ("abierto","en_proceso","pendiente_cliente") THEN 1 ELSE 0 END) AS abiertos,
                SUM(CASE WHEN t.sla_primera_respuesta_estado = "incumplido" THEN 1 ELSE 0 END) AS primera_respuesta_incumplida,
                SUM(CASE WHEN t.sla_resolucion_estado = "incumplido" THEN 1 ELSE 0 END) AS resolucion_incumplida,
                ROUND(AVG(
                    CASE
                        WHEN t.primera_respuesta_at IS NOT NULL THEN TIMESTAMPDIFF(MINUTE, t.created_at, t.primera_respuesta_at)
                        ELSE NULL
                    END
                ), 1) AS promedio_primera_respuesta_min,
                ROUND(AVG(
                    CASE
                        WHEN t.resuelto_at IS NOT NULL THEN TIMESTAMPDIFF(MINUTE, t.created_at, t.resuelto_at)
                        ELSE NULL
                    END
                ), 1) AS promedio_resolucion_min
             FROM soporte_tickets t
             WHERE DATE(t.created_at) BETWEEN :desde AND :hasta'
        );
        $supportSlaSummaryStmt->execute([
            ':desde' => $supportFrom,
            ':hasta' => $supportTo,
        ]);
        $supportSlaSummary = $supportSlaSummaryStmt->fetch() ?: [];

        $supportSlaByCompanyStmt = $pdo->prepare(
            'SELECT
                e.id,
                e.nombre AS empresa,
                COUNT(*) AS total_tickets,
                SUM(CASE WHEN t.estado IN ("abierto","en_proceso","pendiente_cliente") THEN 1 ELSE 0 END) AS abiertos,
                SUM(CASE WHEN t.sla_primera_respuesta_estado = "incumplido" THEN 1 ELSE 0 END) AS primera_respuesta_incumplida,
                SUM(CASE WHEN t.sla_resolucion_estado = "incumplido" THEN 1 ELSE 0 END) AS resolucion_incumplida,
                ROUND(AVG(
                    CASE
                        WHEN t.resuelto_at IS NOT NULL THEN TIMESTAMPDIFF(HOUR, t.created_at, t.resuelto_at)
                        ELSE NULL
                    END
                ), 1) AS promedio_resolucion_horas
             FROM soporte_tickets t
             INNER JOIN empresas e ON e.id = t.empresa_id
             WHERE DATE(t.created_at) BETWEEN :desde AND :hasta
             GROUP BY e.id, e.nombre
             ORDER BY total_tickets DESC, e.nombre ASC
             LIMIT 80'
        );
        $supportSlaByCompanyStmt->execute([
            ':desde' => $supportFrom,
            ':hasta' => $supportTo,
        ]);
        $supportSlaByCompany = $supportSlaByCompanyStmt->fetchAll() ?: [];

        $supportSlaByAgentStmt = $pdo->prepare(
            'SELECT
                COALESCE(u.id, 0) AS agente_id,
                COALESCE(u.nombre, "Sin asignar") AS agente,
                COUNT(*) AS total_asignados,
                SUM(CASE WHEN t.estado IN ("resuelto","cerrado") THEN 1 ELSE 0 END) AS resueltos,
                SUM(CASE WHEN t.sla_resolucion_estado = "incumplido" THEN 1 ELSE 0 END) AS resolucion_incumplida,
                ROUND(AVG(
                    CASE
                        WHEN t.primera_respuesta_at IS NOT NULL THEN TIMESTAMPDIFF(MINUTE, t.created_at, t.primera_respuesta_at)
                        ELSE NULL
                    END
                ), 1) AS promedio_primera_respuesta_min,
                ROUND(AVG(
                    CASE
                        WHEN t.resuelto_at IS NOT NULL THEN TIMESTAMPDIFF(HOUR, t.created_at, t.resuelto_at)
                        ELSE NULL
                    END
                ), 1) AS promedio_resolucion_horas
             FROM soporte_tickets t
             LEFT JOIN usuarios u ON u.id = t.asignado_a_usuario_id
             WHERE DATE(t.created_at) BETWEEN :desde AND :hasta
             GROUP BY agente_id, agente
             ORDER BY total_asignados DESC, agente ASC
             LIMIT 80'
        );
        $supportSlaByAgentStmt->execute([
            ':desde' => $supportFrom,
            ':hasta' => $supportTo,
        ]);
        $supportSlaByAgent = $supportSlaByAgentStmt->fetchAll() ?: [];

        $collectionNoticeQueue = $pdo->query(
            'SELECT
                a.id,
                a.empresa_id,
                e.nombre AS empresa,
                a.suscripcion_id,
                a.tipo,
                a.fecha_referencia,
                a.canal,
                a.destinatario,
                a.estado,
                a.intentos,
                a.max_intentos,
                a.proximo_intento_at,
                a.error_detalle,
                a.updated_at
             FROM avisos_cobro_suscripcion a
             INNER JOIN empresas e ON e.id = a.empresa_id
             ORDER BY a.id DESC
             LIMIT 160'
        )->fetchAll() ?: [];

        $wompiKeys = [
            'wompi_public_key',
            'wompi_integrity_secret',
            'wompi_events_secret',
            'wompi_redirect_url',
        ];
        $wompiRawSettings = [];
        try {
            $wompiRawSettings = (new GlobalSetting())->getMany($wompiKeys);
        } catch (\Throwable) {
            $wompiRawSettings = [];
        }

        $publicKey = trim((string) ($wompiRawSettings['wompi_public_key'] ?? ''));
        $integritySecret = trim((string) ($wompiRawSettings['wompi_integrity_secret'] ?? ''));
        $eventsSecret = trim((string) ($wompiRawSettings['wompi_events_secret'] ?? ''));
        $redirectUrl = trim((string) ($wompiRawSettings['wompi_redirect_url'] ?? ''));
        $hasCustomRedirectUrl = $redirectUrl !== '' && filter_var($redirectUrl, FILTER_VALIDATE_URL) !== false;
        if ($hasCustomRedirectUrl && preg_match('/^https?:\/\//i', $redirectUrl) !== 1) {
            $hasCustomRedirectUrl = false;
        }
        $defaultRedirectUrl = rtrim(url('login'), '/');
        $wompiConfig = [
            'public_key_masked' => $this->maskSecret($publicKey),
            'integrity_secret_masked' => $this->maskSecret($integritySecret),
            'events_secret_masked' => $this->maskSecret($eventsSecret),
            'has_public_key' => $publicKey !== '',
            'has_integrity_secret' => $integritySecret !== '',
            'has_events_secret' => $eventsSecret !== '',
            'checkout_configured' => $publicKey !== '' && $integritySecret !== '',
            'webhook_configured' => $eventsSecret !== '',
            'webhook_url' => url('webhooks/wompi'),
            'redirect_url' => $hasCustomRedirectUrl ? $redirectUrl : '',
            'redirect_url_effective' => $hasCustomRedirectUrl ? $redirectUrl : $defaultRedirectUrl,
            'has_custom_redirect_url' => $hasCustomRedirectUrl,
        ];

        $smtpKeys = array_values($this->smtpSettingMap());
        $smtpRawSettings = [];
        try {
            $smtpRawSettings = (new GlobalSetting())->getMany($smtpKeys);
        } catch (\Throwable) {
            $smtpRawSettings = [];
        }

        $smtpHost = trim((string) ($smtpRawSettings['notify_smtp_host'] ?? (string) config('notifications.smtp.host', '')));
        $smtpPort = (int) ($smtpRawSettings['notify_smtp_port'] ?? (string) config('notifications.smtp.port', 587));
        if ($smtpPort <= 0 || $smtpPort > 65535) {
            $smtpPort = 587;
        }
        $smtpSecure = strtolower(trim((string) ($smtpRawSettings['notify_smtp_secure'] ?? (string) config('notifications.smtp.secure', 'tls'))));
        if (!in_array($smtpSecure, ['tls', 'ssl', 'none'], true)) {
            $smtpSecure = 'tls';
        }
        $smtpUsername = trim((string) ($smtpRawSettings['notify_smtp_username'] ?? (string) config('notifications.smtp.username', '')));
        $smtpPassword = trim((string) ($smtpRawSettings['notify_smtp_password'] ?? (string) config('notifications.smtp.password', '')));
        $smtpFromEmail = trim((string) ($smtpRawSettings['notify_smtp_from_email'] ?? (string) config('notifications.smtp.from_email', '')));
        $smtpFromName = trim((string) ($smtpRawSettings['notify_smtp_from_name'] ?? (string) config('notifications.smtp.from_name', 'Veltrix POS')));
        $authUser = Auth::user();
        $smtpTestEmailDefault = strtolower(trim((string) ($authUser['email'] ?? $smtpFromEmail)));
        $notificationTestPhoneDefault = trim((string) ($authUser['telefono'] ?? ''));
        if ($notificationTestPhoneDefault === '') {
            $notificationTestPhoneDefault = trim((string) (($companies[0]['telefono'] ?? '') ?: ''));
        }

        $smtpConfig = [
            'host' => $smtpHost,
            'port' => $smtpPort,
            'secure' => $smtpSecure,
            'username' => $smtpUsername,
            'password_masked' => $this->maskSecret($smtpPassword),
            'from_email' => $smtpFromEmail,
            'from_name' => $smtpFromName !== '' ? $smtpFromName : 'Veltrix POS',
            'test_email_default' => $smtpTestEmailDefault,
            'is_ready' => $smtpHost !== ''
                && $smtpPort > 0
                && $smtpUsername !== ''
                && $smtpPassword !== ''
                && $smtpFromEmail !== ''
                && filter_var($smtpFromEmail, FILTER_VALIDATE_EMAIL) !== false,
        ];

        $notificationKeys = array_values($this->notificationSettingMap());
        $notificationRawSettings = [];
        try {
            $notificationRawSettings = (new GlobalSetting())->getMany($notificationKeys);
        } catch (\Throwable) {
            $notificationRawSettings = [];
        }

        $emailFromName = trim((string) ($notificationRawSettings['notify_email_from_name'] ?? (string) config('notifications.email.from_name', 'Veltrix POS')));
        $emailFromAddress = trim((string) ($notificationRawSettings['notify_email_from_address'] ?? (string) config('notifications.email.from_address', '')));
        $emailReplyTo = trim((string) ($notificationRawSettings['notify_email_reply_to'] ?? (string) config('notifications.email.reply_to', '')));
        $whatsappProvider = strtolower(trim((string) ($notificationRawSettings['notify_whatsapp_provider'] ?? (string) config('notifications.whatsapp.provider', 'meta'))));
        if (!in_array($whatsappProvider, ['meta', 'manychat'], true)) {
            $whatsappProvider = 'meta';
        }
        $whatsappApiUrl = trim((string) ($notificationRawSettings['notify_whatsapp_api_url'] ?? (string) config('notifications.whatsapp.api_url', '')));
        $whatsappPhoneNumberId = trim((string) ($notificationRawSettings['notify_whatsapp_phone_number_id'] ?? (string) config('notifications.whatsapp.phone_number_id', '')));
        $whatsappBusinessAccountId = trim((string) ($notificationRawSettings['notify_whatsapp_business_account_id'] ?? ''));
        $whatsappAccessToken = trim((string) ($notificationRawSettings['notify_whatsapp_access_token'] ?? (string) config('notifications.whatsapp.access_token', '')));
        $whatsappDefaultCountryCode = trim((string) ($notificationRawSettings['notify_whatsapp_default_country_code'] ?? (string) config('notifications.whatsapp.default_country_code', '57')));
        $manyChatApiUrl = trim((string) ($notificationRawSettings['notify_manychat_api_url'] ?? (string) config('notifications.whatsapp.manychat_api_url', 'https://api.manychat.com')));
        $manyChatAccessToken = trim((string) ($notificationRawSettings['notify_manychat_access_token'] ?? (string) config('notifications.whatsapp.manychat_access_token', '')));
        $manyChatMessageTag = strtoupper(trim((string) ($notificationRawSettings['notify_manychat_message_tag'] ?? (string) config('notifications.whatsapp.manychat_message_tag', 'ACCOUNT_UPDATE'))));
        $manyChatContactFieldId = trim((string) ($notificationRawSettings['notify_manychat_contact_field_id'] ?? (string) config('notifications.whatsapp.manychat_contact_field_id', '')));
        $whatsAppTemplateEnabled = $this->toBooleanSetting((string) ($notificationRawSettings['notify_whatsapp_template_enabled'] ?? ((bool) config('notifications.whatsapp.template_enabled', false) ? '1' : '0')));
        $whatsAppTemplateLanguage = trim((string) ($notificationRawSettings['notify_whatsapp_template_language'] ?? (string) config('notifications.whatsapp.template_language', 'es_CO')));
        $whatsAppTemplateName10d = trim((string) ($notificationRawSettings['notify_whatsapp_template_name_10d'] ?? (string) config('notifications.whatsapp.template_name_10d', '')));
        $whatsAppTemplateName3d = trim((string) ($notificationRawSettings['notify_whatsapp_template_name_3d'] ?? (string) config('notifications.whatsapp.template_name_3d', '')));

        $retryMaxAttempts = (int) ($notificationRawSettings['notify_retry_max_attempts'] ?? (string) config('notifications.retry.max_attempts', 3));
        $retryMaxAttempts = max(1, min(15, $retryMaxAttempts));
        $retryBackoffMinutes = trim((string) ($notificationRawSettings['notify_retry_backoff_minutes'] ?? (string) config('notifications.retry.backoff_minutes', '5,30,180')));
        if ($retryBackoffMinutes === '') {
            $retryBackoffMinutes = '5,30,180';
        }
        if ($manyChatMessageTag === '') {
            $manyChatMessageTag = 'ACCOUNT_UPDATE';
        }

        $channelEmail = $this->toBooleanSetting((string) ($notificationRawSettings['notify_channel_email'] ?? ((bool) config('notifications.channels.email', true) ? '1' : '0')));
        $channelWhatsApp = $this->toBooleanSetting((string) ($notificationRawSettings['notify_channel_whatsapp'] ?? ((bool) config('notifications.channels.whatsapp', true) ? '1' : '0')));
        $hasWhatsAppConfig = $whatsappProvider === 'manychat'
            ? $manyChatApiUrl !== '' && $manyChatAccessToken !== ''
            : $whatsappApiUrl !== '' && $whatsappPhoneNumberId !== '' && $whatsappAccessToken !== '';

        $notificationConfig = [
            'email_from_name' => $emailFromName !== '' ? $emailFromName : 'Veltrix POS',
            'email_from_address' => $emailFromAddress,
            'email_reply_to' => $emailReplyTo,
            'whatsapp_provider' => $whatsappProvider,
            'whatsapp_api_url' => $whatsappApiUrl,
            'whatsapp_phone_number_id' => $whatsappPhoneNumberId,
            'whatsapp_phone_number_id_masked' => $this->maskSecret($whatsappPhoneNumberId),
            'whatsapp_business_account_id' => $whatsappBusinessAccountId,
            'whatsapp_access_token_masked' => $this->maskSecret($whatsappAccessToken),
            'whatsapp_default_country_code' => $whatsappDefaultCountryCode !== '' ? $whatsappDefaultCountryCode : '57',
            'manychat_api_url' => $manyChatApiUrl,
            'manychat_access_token_masked' => $this->maskSecret($manyChatAccessToken),
            'manychat_message_tag' => $manyChatMessageTag,
            'manychat_contact_field_id' => $manyChatContactFieldId,
            'whatsapp_template_enabled' => $whatsAppTemplateEnabled,
            'whatsapp_template_language' => $whatsAppTemplateLanguage !== '' ? $whatsAppTemplateLanguage : 'es_CO',
            'whatsapp_template_name_10d' => $whatsAppTemplateName10d,
            'whatsapp_template_name_3d' => $whatsAppTemplateName3d,
            'channel_email' => $channelEmail,
            'channel_whatsapp' => $channelWhatsApp,
            'retry_max_attempts' => $retryMaxAttempts,
            'retry_backoff_minutes' => $retryBackoffMinutes,
            'has_email_sender' => $emailFromAddress !== '' && filter_var($emailFromAddress, FILTER_VALIDATE_EMAIL) !== false,
            'has_whatsapp_config' => $hasWhatsAppConfig,
        ];

        $whatsAppTemplateService = new WhatsAppTemplateService();
        $whatsAppMetaTemplates = $whatsAppTemplateService->recentTemplates(36);
        $whatsAppTemplateStatusStats = $whatsAppTemplateService->statusSummary();
        $whatsAppDispatchTimeSettings = $this->resolveWhatsAppDispatchTimeSettings($whatsAppMetaTemplates);

        $aiConfig = (new AiHelpService())->configSnapshot();
        $openAiGovernance = new OpenAiGovernanceService();
        $openAiReportSettings = $openAiGovernance->settingsSnapshot();
        $openAiReportUsage = $openAiGovernance->usageSummary();
        $openAiReport = [
            'settings' => $openAiReportSettings,
            'usage' => $openAiReportUsage,
            'top_companies_today' => $openAiGovernance->topCompaniesToday(12),
            'daily_limit_reached' => (float) ($openAiReportSettings['daily_budget_usd'] ?? 0) > 0
                && (float) ($openAiReportUsage['day_cost_usd'] ?? 0) >= (float) ($openAiReportSettings['daily_budget_usd'] ?? 0),
            'monthly_limit_reached' => (float) ($openAiReportSettings['monthly_budget_usd'] ?? 0) > 0
                && (float) ($openAiReportUsage['month_cost_usd'] ?? 0) >= (float) ($openAiReportSettings['monthly_budget_usd'] ?? 0),
        ];

        $this->render('dashboard/superadmin', [
            'title' => 'Panel Superadmin',
            'stats' => $stats,
            'plans' => $plans,
            'companies' => $companies,
            'subscriptions' => $subscriptions,
            'payments' => $payments,
            'checkout_intents' => $checkoutIntents,
            'support_agents' => $supportAgents,
            'support_tickets' => $supportTickets,
            'support_events_by_ticket' => $supportEventsByTicket,
            'support_sla_summary' => $supportSlaSummary,
            'support_sla_by_company' => $supportSlaByCompany,
            'support_sla_by_agent' => $supportSlaByAgent,
            'support_report_filter' => [
                'desde' => $supportFrom,
                'hasta' => $supportTo,
            ],
            'collection_notice_queue' => $collectionNoticeQueue,
            'wompi' => $wompiConfig,
            'smtp_config' => $smtpConfig,
            'notification_config' => $notificationConfig,
            'notification_test_phone_default' => $notificationTestPhoneDefault,
            'whatsapp_meta_templates' => $whatsAppMetaTemplates,
            'whatsapp_template_status_stats' => $whatsAppTemplateStatusStats,
            'whatsapp_dispatch_time_settings' => $whatsAppDispatchTimeSettings,
            'ai_config' => $aiConfig,
            'openai_report' => $openAiReport,
        ]);
    }

    public function storePlan(Request $request): never
    {
        $name = trim((string) $request->input('nombre', ''));
        $slugInput = trim((string) $request->input('slug', ''));
        $slug = $this->sanitizeSlug($slugInput !== '' ? $slugInput : $name);
        $price = max(0, (float) $request->input('precio_mensual', 0));
        $maxUsers = max(1, (int) $request->input('max_usuarios', 1));
        $maxBranches = max(0, (int) $request->input('max_sucursales', 1));
        $maxProducts = max(1, (int) $request->input('max_productos', 1));
        $electronicInvoice = (int) $request->input('permite_facturacion_electronica', 0) === 1 ? 1 : 0;
        $advancedReports = (int) $request->input('permite_reportes_avanzados', 0) === 1 ? 1 : 0;
        $active = (int) $request->input('activo', 1) === 1 ? 1 : 0;

        if ($name === '' || $slug === '') {
            flash('error', 'Nombre y slug del plan son obligatorios.');
            redirect('superadmin');
        }

        $pdo = Database::connection();
        $existsStmt = $pdo->prepare('SELECT id FROM planes WHERE slug = :slug LIMIT 1');
        $existsStmt->execute([':slug' => $slug]);
        if ((bool) $existsStmt->fetchColumn()) {
            flash('error', 'Ya existe un plan con ese slug.');
            redirect('superadmin');
        }

        $stmt = $pdo->prepare(
            'INSERT INTO planes (
                nombre, slug, precio_mensual, max_usuarios, max_sucursales, max_productos,
                permite_facturacion_electronica, permite_reportes_avanzados, activo, created_at, updated_at
            ) VALUES (
                :nombre, :slug, :precio_mensual, :max_usuarios, :max_sucursales, :max_productos,
                :permite_facturacion_electronica, :permite_reportes_avanzados, :activo, NOW(), NOW()
            )'
        );
        $stmt->execute([
            ':nombre' => $name,
            ':slug' => $slug,
            ':precio_mensual' => $price,
            ':max_usuarios' => $maxUsers,
            ':max_sucursales' => $maxBranches,
            ':max_productos' => $maxProducts,
            ':permite_facturacion_electronica' => $electronicInvoice,
            ':permite_reportes_avanzados' => $advancedReports,
            ':activo' => $active,
        ]);

        $planId = (int) $pdo->lastInsertId();
        AuditService::track('superadmin.planes.crear', 'Plan creado ID: ' . $planId);
        flash('success', 'Plan creado correctamente.');
        redirect('superadmin');
    }

    public function updatePlan(Request $request): never
    {
        $planId = (int) $request->param('id', 0);
        if ($planId <= 0) {
            flash('error', 'Plan inválido para actualizar.');
            redirect('superadmin');
        }

        $name = trim((string) $request->input('nombre', ''));
        $slugInput = trim((string) $request->input('slug', ''));
        $slug = $this->sanitizeSlug($slugInput !== '' ? $slugInput : $name);
        $price = max(0, (float) $request->input('precio_mensual', 0));
        $maxUsers = max(1, (int) $request->input('max_usuarios', 1));
        $maxBranches = max(0, (int) $request->input('max_sucursales', 1));
        $maxProducts = max(1, (int) $request->input('max_productos', 1));
        $electronicInvoice = (int) $request->input('permite_facturacion_electronica', 0) === 1 ? 1 : 0;
        $advancedReports = (int) $request->input('permite_reportes_avanzados', 0) === 1 ? 1 : 0;
        $active = (int) $request->input('activo', 1) === 1 ? 1 : 0;

        if ($name === '' || $slug === '') {
            flash('error', 'Nombre y slug del plan son obligatorios.');
            redirect('superadmin');
        }

        $pdo = Database::connection();
        $planStmt = $pdo->prepare('SELECT id FROM planes WHERE id = :id LIMIT 1');
        $planStmt->execute([':id' => $planId]);
        if (!(bool) $planStmt->fetchColumn()) {
            flash('error', 'El plan no existe.');
            redirect('superadmin');
        }

        $slugStmt = $pdo->prepare('SELECT id FROM planes WHERE slug = :slug AND id <> :id LIMIT 1');
        $slugStmt->execute([
            ':slug' => $slug,
            ':id' => $planId,
        ]);
        if ((bool) $slugStmt->fetchColumn()) {
            flash('error', 'Ya existe otro plan con ese slug.');
            redirect('superadmin');
        }

        $updateStmt = $pdo->prepare(
            'UPDATE planes
             SET nombre = :nombre,
                 slug = :slug,
                 precio_mensual = :precio_mensual,
                 max_usuarios = :max_usuarios,
                 max_sucursales = :max_sucursales,
                 max_productos = :max_productos,
                 permite_facturacion_electronica = :permite_facturacion_electronica,
                 permite_reportes_avanzados = :permite_reportes_avanzados,
                 activo = :activo,
                 updated_at = NOW()
             WHERE id = :id'
        );
        $updateStmt->execute([
            ':nombre' => $name,
            ':slug' => $slug,
            ':precio_mensual' => $price,
            ':max_usuarios' => $maxUsers,
            ':max_sucursales' => $maxBranches,
            ':max_productos' => $maxProducts,
            ':permite_facturacion_electronica' => $electronicInvoice,
            ':permite_reportes_avanzados' => $advancedReports,
            ':activo' => $active,
            ':id' => $planId,
        ]);

        AuditService::track('superadmin.planes.actualizar', 'Plan actualizado ID: ' . $planId);
        flash('success', 'Plan actualizado correctamente.');
        redirect('superadmin');
    }

    public function deletePlan(Request $request): never
    {
        $planId = (int) $request->param('id', 0);
        if ($planId <= 0) {
            flash('error', 'Plan inválido para eliminar.');
            redirect('superadmin');
        }

        $pdo = Database::connection();
        $usageStmt = $pdo->prepare('SELECT COUNT(*) FROM suscripciones WHERE plan_id = :plan_id');
        $usageStmt->execute([':plan_id' => $planId]);
        $usage = (int) $usageStmt->fetchColumn();
        if ($usage > 0) {
            flash('error', 'No se puede eliminar: el plan tiene suscripciones asociadas.');
            redirect('superadmin');
        }

        $deleteStmt = $pdo->prepare('DELETE FROM planes WHERE id = :id');
        $deleteStmt->execute([':id' => $planId]);

        AuditService::track('superadmin.planes.eliminar', 'Plan eliminado ID: ' . $planId);
        flash('success', 'Plan eliminado correctamente.');
        redirect('superadmin');
    }

    public function updateCompanyStatus(Request $request): never
    {
        $companyId = (int) $request->param('id', 0);
        $status = trim((string) $request->input('estado', ''));
        $allowedStatuses = ['activa', 'suspendida', 'bloqueada'];

        if ($companyId <= 0 || !in_array($status, $allowedStatuses, true)) {
            flash('error', 'Datos inválidos para actualizar estado de empresa.');
            redirect('superadmin');
        }

        $stmt = Database::connection()->prepare(
            'UPDATE empresas
             SET estado = :estado,
                 updated_at = NOW()
             WHERE id = :id'
        );
        $stmt->execute([
            ':estado' => $status,
            ':id' => $companyId,
        ]);

        AuditService::track('superadmin.empresas.estado', 'Empresa ID ' . $companyId . ' -> ' . $status);
        flash('success', 'Estado de empresa actualizado.');
        redirect('superadmin');
    }

    public function deleteCompany(Request $request): never
    {
        $companyId = (int) $request->param('id', 0);
        if ($companyId <= 0) {
            flash('error', 'Empresa inválida para eliminar.');
            redirect('superadmin?seccion=empresas');
        }

        $pdo = Database::connection();
        $companyStmt = $pdo->prepare('SELECT id, nombre FROM empresas WHERE id = :id LIMIT 1');
        $companyStmt->execute([':id' => $companyId]);
        $company = $companyStmt->fetch();

        if (!$company) {
            flash('error', 'La empresa no existe o ya fue eliminada.');
            redirect('superadmin?seccion=empresas');
        }

        $hasSuperadminStmt = $pdo->prepare(
            'SELECT COUNT(*)
             FROM usuarios u
             INNER JOIN roles r ON r.id = u.rol_id
             WHERE u.empresa_id = :empresa_id
               AND r.slug = "superadmin"'
        );
        $hasSuperadminStmt->execute([':empresa_id' => $companyId]);
        if ((int) $hasSuperadminStmt->fetchColumn() > 0) {
            flash('error', 'No se puede eliminar una empresa con usuarios superadmin asociados.');
            redirect('superadmin?seccion=empresas');
        }

        $companyName = (string) ($company['nombre'] ?? '');

        try {
            Database::transaction(function (PDO $pdo) use ($companyId): void {
                $this->purgeCompanyData($pdo, $companyId);

                $deleteCompanyStmt = $pdo->prepare('DELETE FROM empresas WHERE id = :id');
                $deleteCompanyStmt->execute([':id' => $companyId]);
            });
        } catch (\Throwable $e) {
            flash('error', 'No fue posible eliminar la empresa: ' . $e->getMessage());
            redirect('superadmin?seccion=empresas');
        }

        AuditService::track(
            'superadmin.empresas.eliminar',
            'Empresa eliminada ID: ' . $companyId . ' (' . $companyName . ')'
        );
        flash('success', 'Empresa eliminada con toda su información relacionada.');
        redirect('superadmin?seccion=empresas');
    }

    public function updateCompanyFactus(Request $request): never
    {
        $companyId = (int) $request->param('id', 0);
        if ($companyId <= 0) {
            flash('error', 'Empresa inválida para actualizar Factus.');
            redirect('superadmin?seccion=empresas');
        }

        $pdo = Database::connection();
        $company = $this->loadCompanyForFactus($pdo, $companyId);
        if ($company === null) {
            flash('error', 'La empresa no existe.');
            redirect('superadmin?seccion=empresas');
        }

        $feEnabled = (int) $request->input('facturacion_electronica', 1) === 1;
        if (!$feEnabled) {
            try {
                (new CompanySetting())->upsertMany($companyId, [
                    'facturacion_electronica' => '0',
                ]);
            } catch (\Throwable $e) {
                flash('error', 'No fue posible desactivar facturación electrónica: ' . $e->getMessage());
                redirect('superadmin?seccion=empresas&factus_empresa=' . $companyId);
            }

            AuditService::track(
                'superadmin.empresas.factus.modulo',
                'Módulo FE desactivado para empresa ID: ' . $companyId
            );
            flash('success', 'Facturación electrónica desactivada para la empresa.');
            redirect('superadmin?seccion=empresas');
        }

        if (!$this->companyAllowsElectronicInvoicing($pdo, $companyId)) {
            flash('error', 'El plan actual de la empresa no permite facturación electrónica.');
            redirect('superadmin?seccion=empresas&factus_empresa=' . $companyId);
        }

        $factusResult = $this->resolveAndValidateFactusSettingsForCompany($request, $companyId, true);
        if (!($factusResult['ok'] ?? false)) {
            flash('error', (string) ($factusResult['message'] ?? 'Error de validación en Factus.'));
            redirect('superadmin?seccion=empresas&factus_empresa=' . $companyId);
        }

        $factusData = is_array($factusResult['data'] ?? null) ? $factusResult['data'] : [];
        $settings = [
            'facturacion_electronica' => '1',
            'factus_api_url' => (string) ($factusData['api_url'] ?? ''),
            'factus_client_id' => SecretCipherService::encrypt((string) ($factusData['client_id'] ?? '')),
            'factus_client_secret' => SecretCipherService::encrypt((string) ($factusData['client_secret'] ?? '')),
            'factus_username' => SecretCipherService::encrypt((string) ($factusData['username'] ?? '')),
            'factus_password' => SecretCipherService::encrypt((string) ($factusData['password'] ?? '')),
            'factus_software_id' => SecretCipherService::encrypt((string) ($factusData['software_id'] ?? '')),
            'factus_software_pin' => SecretCipherService::encrypt((string) ($factusData['software_pin'] ?? '')),
            'factus_numbering_range_id' => (string) ($factusData['numbering_range_id'] ?? ''),
            'factus_municipality_id' => (string) ($factusData['municipality_id'] ?? ''),
        ];

        try {
            (new CompanySetting())->upsertMany($companyId, $settings);
        } catch (\Throwable $e) {
            flash('error', 'No fue posible actualizar la configuración Factus: ' . $e->getMessage());
            redirect('superadmin?seccion=empresas&factus_empresa=' . $companyId);
        }

        AuditService::track(
            'superadmin.empresas.factus.actualizar',
            'Factus actualizado para empresa ID: ' . $companyId
        );
        flash('success', 'Configuración Factus guardada correctamente para la empresa.');
        redirect('superadmin?seccion=empresas');
    }

    public function testCompanyFactusConnection(Request $request): never
    {
        $companyId = (int) $request->param('id', 0);
        if ($companyId <= 0) {
            flash('error', 'Empresa inválida para probar Factus.');
            redirect('superadmin?seccion=empresas');
        }

        $pdo = Database::connection();
        $company = $this->loadCompanyForFactus($pdo, $companyId);
        if ($company === null) {
            flash('error', 'La empresa no existe.');
            redirect('superadmin?seccion=empresas');
        }

        if (!$this->companyAllowsElectronicInvoicing($pdo, $companyId)) {
            flash('error', 'El plan actual de la empresa no permite facturación electrónica.');
            redirect('superadmin?seccion=empresas&factus_empresa=' . $companyId);
        }

        $factusResult = $this->resolveAndValidateFactusSettingsForCompany($request, $companyId, false);
        if (!($factusResult['ok'] ?? false)) {
            flash('error', (string) ($factusResult['message'] ?? 'No fue posible validar la configuración Factus.'));
            redirect('superadmin?seccion=empresas&factus_empresa=' . $companyId);
        }

        $data = is_array($factusResult['data'] ?? null) ? $factusResult['data'] : [];
        $probe = (new FactusService())->probeConnection($data);
        $ok = (bool) ($probe['ok'] ?? false);
        $message = trim((string) ($probe['message'] ?? 'Sin respuesta de prueba.'));

        AuditService::track(
            'superadmin.empresas.factus.probar',
            'Empresa ID ' . $companyId . ' -> ' . ($ok ? 'OK' : 'ERROR') . '. ' . $message
        );
        flash($ok ? 'success' : 'error', $message);
        redirect('superadmin?seccion=empresas&factus_empresa=' . $companyId);
    }

    public function testCompanyFactusInvoice(Request $request): never
    {
        $companyId = (int) $request->param('id', 0);
        if ($companyId <= 0) {
            flash('error', 'Empresa inválida para emitir prueba FE.');
            redirect('superadmin?seccion=empresas');
        }

        $pdo = Database::connection();
        $company = $this->loadCompanyForFactus($pdo, $companyId);
        if ($company === null) {
            flash('error', 'La empresa no existe.');
            redirect('superadmin?seccion=empresas');
        }

        if (!$this->companyAllowsElectronicInvoicing($pdo, $companyId)) {
            flash('error', 'El plan actual de la empresa no permite facturación electrónica.');
            redirect('superadmin?seccion=empresas&factus_empresa=' . $companyId);
        }

        $factusResult = $this->resolveAndValidateFactusSettingsForCompany($request, $companyId, true);
        if (!($factusResult['ok'] ?? false)) {
            flash('error', (string) ($factusResult['message'] ?? 'No fue posible validar la configuración Factus.'));
            redirect('superadmin?seccion=empresas&factus_empresa=' . $companyId);
        }

        $context = [
            'company_name' => (string) ($company['nombre'] ?? 'Veltrix POS'),
            'company_email' => (string) ($company['email'] ?? ''),
            'company_phone' => (string) ($company['telefono'] ?? ''),
            'company_address' => (string) ($company['direccion'] ?? ''),
            'customer_identification' => (string) ($company['nit'] ?? ''),
        ];

        $data = is_array($factusResult['data'] ?? null) ? $factusResult['data'] : [];
        $probe = (new FactusService())->issueTestInvoice($data, $context);
        $ok = (bool) ($probe['ok'] ?? false);
        $message = trim((string) ($probe['message'] ?? 'Sin respuesta de prueba.'));

        AuditService::track(
            'superadmin.empresas.factus.probar_factura',
            'Empresa ID ' . $companyId . ' -> ' . ($ok ? 'OK' : 'ERROR') . '. ' . $message
        );
        flash($ok ? 'success' : 'error', $message);
        redirect('superadmin?seccion=empresas&factus_empresa=' . $companyId);
    }

    public function loadCompanyFactusRanges(Request $request): never
    {
        $companyId = (int) $request->param('id', 0);
        if ($companyId <= 0) {
            Response::json([
                'ok' => false,
                'message' => 'Empresa inválida para consultar rangos Factus.',
            ], 422);
        }

        $pdo = Database::connection();
        if ($this->loadCompanyForFactus($pdo, $companyId) === null) {
            Response::json([
                'ok' => false,
                'message' => 'La empresa no existe.',
            ], 404);
        }

        if (!$this->companyAllowsElectronicInvoicing($pdo, $companyId)) {
            Response::json([
                'ok' => false,
                'message' => 'El plan actual de la empresa no permite facturación electrónica.',
            ], 403);
        }

        $factusResult = $this->resolveAndValidateFactusSettingsForCompany($request, $companyId, false);
        if (!($factusResult['ok'] ?? false)) {
            Response::json([
                'ok' => false,
                'message' => (string) ($factusResult['message'] ?? 'No fue posible validar la configuración Factus.'),
            ], 422);
        }

        $document = trim((string) $request->input('document', '21'));
        if ($document === '') {
            $document = '21';
        }

        $onlyActive = (int) $request->input('solo_activas', 1) === 1;
        $data = is_array($factusResult['data'] ?? null) ? $factusResult['data'] : [];

        try {
            $result = (new FactusService())->fetchNumberingRanges($data, $document, $onlyActive);
        } catch (\Throwable $e) {
            Response::json([
                'ok' => false,
                'message' => 'No fue posible consultar rangos en Factus: ' . $e->getMessage(),
            ], 500);
        }

        if (!(bool) ($result['ok'] ?? false)) {
            Response::json([
                'ok' => false,
                'message' => (string) ($result['message'] ?? 'No fue posible consultar rangos en Factus.'),
            ], 422);
        }

        $rows = is_array($result['data'] ?? null) ? $result['data'] : [];
        Response::json([
            'ok' => true,
            'message' => (string) ($result['message'] ?? 'Rangos cargados correctamente.'),
            'data' => $rows,
            'count' => count($rows),
        ]);
    }

    public function loadCompanyFactusMunicipalities(Request $request): never
    {
        $companyId = (int) $request->param('id', 0);
        if ($companyId <= 0) {
            Response::json([
                'ok' => false,
                'message' => 'Empresa inválida para consultar municipios Factus.',
            ], 422);
        }

        $pdo = Database::connection();
        if ($this->loadCompanyForFactus($pdo, $companyId) === null) {
            Response::json([
                'ok' => false,
                'message' => 'La empresa no existe.',
            ], 404);
        }

        if (!$this->companyAllowsElectronicInvoicing($pdo, $companyId)) {
            Response::json([
                'ok' => false,
                'message' => 'El plan actual de la empresa no permite facturación electrónica.',
            ], 403);
        }

        $factusResult = $this->resolveAndValidateFactusSettingsForCompany($request, $companyId, false);
        if (!($factusResult['ok'] ?? false)) {
            Response::json([
                'ok' => false,
                'message' => (string) ($factusResult['message'] ?? 'No fue posible validar la configuración Factus.'),
            ], 422);
        }

        $query = trim((string) $request->input('query', ''));
        $limit = (int) $request->input('limit', 60);
        if ($limit <= 0) {
            $limit = 60;
        }
        $limit = max(1, min(200, $limit));

        $data = is_array($factusResult['data'] ?? null) ? $factusResult['data'] : [];
        try {
            $result = (new FactusService())->fetchMunicipalities($data, $query, $limit);
        } catch (\Throwable $e) {
            Response::json([
                'ok' => false,
                'message' => 'No fue posible consultar municipios en Factus: ' . $e->getMessage(),
            ], 500);
        }

        if (!(bool) ($result['ok'] ?? false)) {
            Response::json([
                'ok' => false,
                'message' => (string) ($result['message'] ?? 'No fue posible consultar municipios en Factus.'),
            ], 422);
        }

        $rows = is_array($result['data'] ?? null) ? $result['data'] : [];
        Response::json([
            'ok' => true,
            'message' => (string) ($result['message'] ?? 'Municipios cargados correctamente.'),
            'data' => $rows,
            'count' => count($rows),
        ]);
    }

    public function updateSubscription(Request $request): never
    {
        $subscriptionId = (int) $request->param('id', 0);
        $planId = (int) $request->input('plan_id', 0);
        $startDate = trim((string) $request->input('fecha_inicio', ''));
        $endDate = trim((string) $request->input('fecha_vencimiento', ''));
        $status = trim((string) $request->input('estado', ''));
        $autoRenew = (int) $request->input('auto_renovacion', 0) === 1 ? 1 : 0;
        $trial = (int) $request->input('trial', 0) === 1 ? 1 : 0;

        $allowedStatuses = ['activa', 'vencida', 'cancelada', 'morosa'];
        if (
            $subscriptionId <= 0
            || $planId <= 0
            || !$this->isDate($startDate)
            || !$this->isDate($endDate)
            || $startDate > $endDate
            || !in_array($status, $allowedStatuses, true)
        ) {
            flash('error', 'Datos inválidos para actualizar la suscripción.');
            redirect('superadmin');
        }

        $pdo = Database::connection();
        $planStmt = $pdo->prepare('SELECT id FROM planes WHERE id = :id LIMIT 1');
        $planStmt->execute([':id' => $planId]);
        if (!(bool) $planStmt->fetchColumn()) {
            flash('error', 'El plan seleccionado no existe.');
            redirect('superadmin');
        }

        $stmt = $pdo->prepare(
            'UPDATE suscripciones
             SET plan_id = :plan_id,
                 fecha_inicio = :fecha_inicio,
                 fecha_vencimiento = :fecha_vencimiento,
                 estado = :estado,
                 trial = :trial,
                 auto_renovacion = :auto_renovacion,
                 updated_at = NOW()
             WHERE id = :id'
        );
        $stmt->execute([
            ':plan_id' => $planId,
            ':fecha_inicio' => $startDate,
            ':fecha_vencimiento' => $endDate,
            ':estado' => $status,
            ':trial' => $trial,
            ':auto_renovacion' => $autoRenew,
            ':id' => $subscriptionId,
        ]);

        AuditService::track('superadmin.suscripciones.actualizar', 'Suscripción actualizada ID: ' . $subscriptionId);
        flash('success', 'Suscripción actualizada correctamente.');
        redirect('superadmin');
    }

    public function storePayment(Request $request): never
    {
        $subscriptionId = (int) $request->input('suscripcion_id', 0);
        $gateway = trim((string) $request->input('pasarela', 'manual'));
        $reference = trim((string) $request->input('referencia', ''));
        $amount = (float) $request->input('monto', 0);
        $currency = strtoupper(trim((string) $request->input('moneda', 'COP')));
        $status = trim((string) $request->input('estado', 'pagado'));
        $paymentAtInput = trim((string) $request->input('fecha_pago', ''));
        $renewDays = max(0, (int) $request->input('renovar_dias', 0));
        $allowedStatuses = ['pendiente', 'pagado', 'fallido', 'reembolsado'];

        if ($subscriptionId <= 0 || $amount <= 0 || !in_array($status, $allowedStatuses, true)) {
            flash('error', 'Datos inválidos para registrar el pago.');
            redirect('superadmin');
        }

        $paymentAt = date('Y-m-d H:i:s');
        if ($paymentAtInput !== '') {
            $parsed = strtotime($paymentAtInput);
            if ($parsed !== false) {
                $paymentAt = date('Y-m-d H:i:s', $parsed);
            }
        }

        $renewalEmailPayload = null;

        Database::transaction(function () use (
            $subscriptionId,
            $gateway,
            $reference,
            $amount,
            $currency,
            $status,
            $paymentAt,
            $renewDays,
            &$renewalEmailPayload
        ): void {
            $pdo = Database::connection();

            $subscriptionStmt = $pdo->prepare(
                'SELECT id, empresa_id, fecha_vencimiento, estado
                 FROM suscripciones
                 WHERE id = :id
                 LIMIT 1
                 FOR UPDATE'
            );
            $subscriptionStmt->execute([':id' => $subscriptionId]);
            $subscription = $subscriptionStmt->fetch();
            if (!$subscription) {
                throw new \RuntimeException('La suscripción seleccionada no existe.');
            }

            $paidCountBefore = 0;
            if ($status === 'pagado') {
                $paidCountStmt = $pdo->prepare(
                    'SELECT COUNT(*)
                     FROM pagos_suscripcion
                     WHERE suscripcion_id = :suscripcion_id
                       AND estado = "pagado"'
                );
                $paidCountStmt->execute([':suscripcion_id' => $subscriptionId]);
                $paidCountBefore = (int) $paidCountStmt->fetchColumn();
            }

            $paymentStmt = $pdo->prepare(
                'INSERT INTO pagos_suscripcion (
                    empresa_id, suscripcion_id, pasarela, referencia, monto, moneda, estado, fecha_pago, created_at
                ) VALUES (
                    :empresa_id, :suscripcion_id, :pasarela, :referencia, :monto, :moneda, :estado, :fecha_pago, NOW()
                )'
            );
            $paymentStmt->execute([
                ':empresa_id' => (int) $subscription['empresa_id'],
                ':suscripcion_id' => $subscriptionId,
                ':pasarela' => $gateway !== '' ? $gateway : 'manual',
                ':referencia' => $reference !== '' ? $reference : null,
                ':monto' => $amount,
                ':moneda' => $currency !== '' ? $currency : 'COP',
                ':estado' => $status,
                ':fecha_pago' => $paymentAt,
            ]);

            if ($status === 'pagado') {
                $today = date('Y-m-d');
                $currentDue = (string) ($subscription['fecha_vencimiento'] ?? $today);
                $baseDate = $currentDue >= $today ? $currentDue : $today;
                $newDue = $baseDate;

                if ($renewDays > 0) {
                    $newDue = date('Y-m-d', strtotime($baseDate . ' +' . $renewDays . ' days'));
                }

                $updateSubscriptionStmt = $pdo->prepare(
                    'UPDATE suscripciones
                     SET estado = "activa",
                         fecha_vencimiento = :fecha_vencimiento,
                         updated_at = NOW()
                     WHERE id = :id'
                );
                $updateSubscriptionStmt->execute([
                    ':fecha_vencimiento' => $newDue,
                    ':id' => $subscriptionId,
                ]);

                $reactivateCompanyStmt = $pdo->prepare(
                    'UPDATE empresas
                     SET estado = "activa",
                         updated_at = NOW()
                     WHERE id = :empresa_id
                       AND estado <> "activa"'
                );
                $reactivateCompanyStmt->execute([
                    ':empresa_id' => (int) $subscription['empresa_id'],
                ]);

                if ($paidCountBefore >= 1) {
                    $detailsStmt = $pdo->prepare(
                        'SELECT
                            s.id AS suscripcion_id,
                            s.empresa_id,
                            s.fecha_vencimiento,
                            e.nombre AS empresa_nombre,
                            e.email AS empresa_email,
                            p.nombre AS plan_nombre,
                            (
                                SELECT u1.nombre
                                FROM usuarios u1
                                INNER JOIN usuario_empresa ue1 ON ue1.usuario_id = u1.id
                                WHERE ue1.empresa_id = s.empresa_id
                                  AND ue1.es_principal = 1
                                ORDER BY ue1.id ASC
                                LIMIT 1
                            ) AS admin_nombre,
                            (
                                SELECT u2.email
                                FROM usuarios u2
                                INNER JOIN usuario_empresa ue2 ON ue2.usuario_id = u2.id
                                WHERE ue2.empresa_id = s.empresa_id
                                  AND ue2.es_principal = 1
                                ORDER BY ue2.id ASC
                                LIMIT 1
                            ) AS admin_email
                         FROM suscripciones s
                         INNER JOIN empresas e ON e.id = s.empresa_id
                         LEFT JOIN planes p ON p.id = s.plan_id
                         WHERE s.id = :id
                         LIMIT 1'
                    );
                    $detailsStmt->execute([':id' => $subscriptionId]);
                    $details = $detailsStmt->fetch() ?: [];

                    $adminEmail = strtolower(trim((string) ($details['admin_email'] ?? '')));
                    if ($adminEmail === '') {
                        $adminEmail = strtolower(trim((string) ($details['empresa_email'] ?? '')));
                    }

                    if ($adminEmail !== '') {
                        $renewalEmailPayload = [
                            'subscription_id' => (int) ($details['suscripcion_id'] ?? $subscriptionId),
                            'company_id' => (int) ($details['empresa_id'] ?? (int) $subscription['empresa_id']),
                            'company_name' => trim((string) ($details['empresa_nombre'] ?? 'Tu empresa')),
                            'admin_name' => trim((string) ($details['admin_nombre'] ?? 'Administrador')),
                            'admin_email' => $adminEmail,
                            'plan_name' => trim((string) ($details['plan_nombre'] ?? 'Plan activo')),
                            'payment_at' => $paymentAt,
                            'gateway' => $gateway !== '' ? $gateway : 'manual',
                            'reference' => $reference,
                            'amount' => $amount,
                            'currency' => $currency !== '' ? $currency : 'COP',
                            'renewed_until' => $newDue,
                        ];
                    }
                }
            }
        });

        if (is_array($renewalEmailPayload)) {
            try {
                $renewalResult = (new RenewalEmailService())->sendSubscriptionRenewal($renewalEmailPayload);
                if ((bool) ($renewalResult['ok'] ?? false)) {
                    Logger::info('Correo de renovación mensual enviado', [
                        'subscription_id' => (int) ($renewalEmailPayload['subscription_id'] ?? 0),
                        'admin_email' => (string) ($renewalEmailPayload['admin_email'] ?? ''),
                    ]);
                } else {
                    Logger::info('Correo de renovación mensual no enviado', [
                        'subscription_id' => (int) ($renewalEmailPayload['subscription_id'] ?? 0),
                        'admin_email' => (string) ($renewalEmailPayload['admin_email'] ?? ''),
                        'error' => (string) ($renewalResult['error'] ?? 'Error desconocido'),
                    ]);
                }
            } catch (\Throwable $e) {
                Logger::error('Error enviando correo de renovación mensual', [
                    'subscription_id' => (int) ($renewalEmailPayload['subscription_id'] ?? 0),
                    'error' => $e->getMessage(),
                ]);
            }
        }

        AuditService::track('superadmin.pagos.crear', 'Pago de suscripción registrado para suscripción ID: ' . $subscriptionId);
        flash('success', 'Pago registrado correctamente.');
        redirect('superadmin');
    }

    public function runSubscriptionAutomation(Request $request): never
    {
        $summary = (new SubscriptionAutomationService())->runDaily(true);

        $resume = sprintf(
            'Automatización ejecutada. Vencidas: %d, Morosas: %d, Empresas bloqueadas: %d, Avisos creados: %d, Enviados: %d, Reprogramados: %d.',
            (int) ($summary['subscriptions_marked_vencida'] ?? 0),
            (int) ($summary['subscriptions_marked_morosa'] ?? 0),
            (int) ($summary['companies_blocked'] ?? 0),
            (int) ($summary['collection_notices_created'] ?? 0),
            (int) ($summary['collection_notices_sent'] ?? 0),
            (int) ($summary['collection_notices_retry_scheduled'] ?? 0)
        );

        AuditService::track('superadmin.suscripciones.automatizacion', $resume);

        $errors = is_array($summary['errors'] ?? null) ? $summary['errors'] : [];
        if ($errors !== []) {
            flash('error', 'Automatización ejecutada con alertas: ' . implode(' | ', $errors));
            flash('success', $resume);
            redirect('superadmin');
        }

        flash('success', $resume);
        redirect('superadmin');
    }

    public function retryCollectionNotices(Request $request): never
    {
        $limit = max(1, min(600, (int) $request->input('limit', 200)));
        $summary = (new SubscriptionAutomationService())->retryPendingNotices($limit);

        $resume = sprintf(
            'Reintentos ejecutados. Procesados: %d, Enviados: %d, Fallidos: %d, Reprogramados: %d, Agotados: %d.',
            (int) ($summary['processed'] ?? 0),
            (int) ($summary['sent'] ?? 0),
            (int) ($summary['failed'] ?? 0),
            (int) ($summary['retry_scheduled'] ?? 0),
            (int) ($summary['exhausted'] ?? 0)
        );

        AuditService::track('superadmin.suscripciones.avisos.reintento', $resume);

        $errors = is_array($summary['errors'] ?? null) ? $summary['errors'] : [];
        if ($errors !== []) {
            flash('error', 'Reintentos ejecutados con alertas: ' . implode(' | ', array_slice($errors, 0, 4)));
            flash('success', $resume);
            redirect('superadmin');
        }

        flash('success', $resume);
        redirect('superadmin');
    }

    public function reprocessCheckoutIntent(Request $request): never
    {
        $intentId = (int) $request->param('id', 0);
        if ($intentId <= 0) {
            flash('error', 'Intento de checkout inválido.');
            redirect('superadmin');
        }

        $result = (new SaasCheckoutRecoveryService())->reprocessIntentById($intentId);
        $ok = (bool) ($result['ok'] ?? false);
        $message = trim((string) ($result['message'] ?? 'Sin respuesta de reproceso.'));

        AuditService::track(
            'superadmin.checkout.reprocesar',
            'Intento ID ' . $intentId . ' => ' . ($ok ? 'OK' : 'ERROR') . '. ' . $message
        );

        if ($ok) {
            flash('success', $message);
            redirect('superadmin');
        }

        flash('error', $message);
        redirect('superadmin');
    }

    public function storeSupportTicket(Request $request): never
    {
        $companyId = (int) $request->input('empresa_id', 0);
        $subject = trim((string) $request->input('asunto', ''));
        $description = trim((string) $request->input('descripcion', ''));
        $priority = trim((string) $request->input('prioridad', 'media'));
        $allowedPriorities = $this->ticketPriorities();

        if ($companyId <= 0 || $subject === '' || $description === '' || !in_array($priority, $allowedPriorities, true)) {
            flash('error', 'Datos inválidos para crear ticket de soporte.');
            redirect('superadmin');
        }

        $pdo = Database::connection();
        $companyStmt = $pdo->prepare('SELECT id FROM empresas WHERE id = :id LIMIT 1');
        $companyStmt->execute([':id' => $companyId]);
        if (!(bool) $companyStmt->fetchColumn()) {
            flash('error', 'La empresa seleccionada no existe.');
            redirect('superadmin');
        }

        $userId = (int) (Auth::id() ?? 0);
        $slaTargets = $this->ticketSlaTargets($priority);
        Database::transaction(function (PDO $pdo) use ($companyId, $userId, $subject, $description, $priority, $slaTargets): void {
            $createdAt = date('Y-m-d H:i:s');
            $firstResponseLimitAt = date(
                'Y-m-d H:i:s',
                strtotime($createdAt . ' +' . (int) ($slaTargets['first_response_minutes'] ?? 120) . ' minutes')
            );
            $resolutionLimitAt = date(
                'Y-m-d H:i:s',
                strtotime($createdAt . ' +' . (int) ($slaTargets['resolution_hours'] ?? 24) . ' hours')
            );

            $insertTicketStmt = $pdo->prepare(
                'INSERT INTO soporte_tickets (
                    empresa_id,
                    creado_por_usuario_id,
                    asunto,
                    descripcion,
                    prioridad,
                    estado,
                    canal,
                    created_at,
                    updated_at,
                    primera_respuesta_at,
                    primera_respuesta_limite_at,
                    resolucion_limite_at,
                    objetivo_primera_respuesta_min,
                    objetivo_resolucion_horas,
                    sla_primera_respuesta_estado,
                    sla_resolucion_estado,
                    cerrado_at,
                    resuelto_at
                ) VALUES (
                    :empresa_id,
                    :creado_por_usuario_id,
                    :asunto,
                    :descripcion,
                    :prioridad,
                    "abierto",
                    "panel",
                    :created_at,
                    :updated_at,
                    NULL,
                    :primera_respuesta_limite_at,
                    :resolucion_limite_at,
                    :objetivo_primera_respuesta_min,
                    :objetivo_resolucion_horas,
                    "pendiente",
                    "pendiente",
                    NULL,
                    NULL
                )'
            );
            $insertTicketStmt->execute([
                ':empresa_id' => $companyId,
                ':creado_por_usuario_id' => $userId > 0 ? $userId : null,
                ':asunto' => $subject,
                ':descripcion' => $description,
                ':prioridad' => $priority,
                ':created_at' => $createdAt,
                ':updated_at' => $createdAt,
                ':primera_respuesta_limite_at' => $firstResponseLimitAt,
                ':resolucion_limite_at' => $resolutionLimitAt,
                ':objetivo_primera_respuesta_min' => (int) ($slaTargets['first_response_minutes'] ?? 120),
                ':objetivo_resolucion_horas' => (int) ($slaTargets['resolution_hours'] ?? 24),
            ]);

            $ticketId = (int) $pdo->lastInsertId();
            $insertEventStmt = $pdo->prepare(
                'INSERT INTO soporte_ticket_eventos (
                    ticket_id, usuario_id, tipo, mensaje, estado_anterior, estado_nuevo, created_at
                ) VALUES (
                    :ticket_id, :usuario_id, "creacion", :mensaje, NULL, "abierto", NOW()
                )'
            );
            $insertEventStmt->execute([
                ':ticket_id' => $ticketId,
                ':usuario_id' => $userId > 0 ? $userId : null,
                ':mensaje' => $description,
            ]);
        });

        AuditService::track('superadmin.soporte.ticket.crear', 'Ticket de soporte creado para empresa ID: ' . $companyId);
        flash('success', 'Ticket de soporte creado correctamente.');
        redirect('superadmin');
    }

    public function updateSupportTicket(Request $request): never
    {
        $ticketId = (int) $request->param('id', 0);
        if ($ticketId <= 0) {
            flash('error', 'Ticket inválido para actualizar.');
            redirect('superadmin');
        }

        $newStatus = trim((string) $request->input('estado', ''));
        $newPriority = trim((string) $request->input('prioridad', ''));
        $assigneeIdRaw = (int) $request->input('asignado_a_usuario_id', 0);
        $assigneeId = $assigneeIdRaw > 0 ? $assigneeIdRaw : null;

        if (
            !in_array($newStatus, $this->ticketStatuses(), true)
            || !in_array($newPriority, $this->ticketPriorities(), true)
        ) {
            flash('error', 'Estado o prioridad inválida para ticket.');
            redirect('superadmin');
        }

        $pdo = Database::connection();
        $ticketStmt = $pdo->prepare(
            'SELECT id, estado, prioridad, asignado_a_usuario_id
             FROM soporte_tickets
             WHERE id = :id
             LIMIT 1'
        );
        $ticketStmt->execute([':id' => $ticketId]);
        $ticket = $ticketStmt->fetch();
        if (!$ticket) {
            flash('error', 'Ticket no encontrado.');
            redirect('superadmin');
        }

        if ($assigneeId !== null) {
            $assigneeStmt = $pdo->prepare(
                'SELECT u.id
                 FROM usuarios u
                 INNER JOIN roles r ON r.id = u.rol_id
                 WHERE u.id = :id
                   AND u.activo = 1
                   AND r.slug = "superadmin"
                 LIMIT 1'
            );
            $assigneeStmt->execute([':id' => $assigneeId]);
            if (!(bool) $assigneeStmt->fetchColumn()) {
                flash('error', 'El usuario asignado debe ser superadmin activo.');
                redirect('superadmin');
            }
        }

        $currentStatus = (string) ($ticket['estado'] ?? 'abierto');
        $currentPriority = (string) ($ticket['prioridad'] ?? 'media');
        $currentAssignee = isset($ticket['asignado_a_usuario_id']) ? (int) $ticket['asignado_a_usuario_id'] : 0;
        $changedStatus = $newStatus !== $currentStatus;
        $changedPriority = $newPriority !== $currentPriority;
        $changedAssignee = (int) ($assigneeId ?? 0) !== $currentAssignee;

        if (!$changedStatus && !$changedPriority && !$changedAssignee) {
            flash('error', 'No hay cambios para guardar en el ticket.');
            redirect('superadmin');
        }

        $userId = (int) (Auth::id() ?? 0);
        Database::transaction(function (PDO $pdo) use (
            $ticketId,
            $newStatus,
            $newPriority,
            $assigneeId,
            $currentStatus,
            $currentPriority,
            $currentAssignee,
            $changedStatus,
            $changedPriority,
            $changedAssignee,
            $userId
        ): void {
            $updateStmt = $pdo->prepare(
                'UPDATE soporte_tickets
                 SET estado = :estado,
                     prioridad = :prioridad,
                     asignado_a_usuario_id = :asignado_a_usuario_id,
                     updated_at = NOW()
                 WHERE id = :id'
            );
            $updateStmt->execute([
                ':estado' => $newStatus,
                ':prioridad' => $newPriority,
                ':asignado_a_usuario_id' => $assigneeId,
                ':id' => $ticketId,
            ]);

            if ($newStatus !== 'abierto') {
                $this->registerFirstSupportResponseIfNeeded($pdo, $ticketId);
            }

            if (in_array($newStatus, ['resuelto', 'cerrado'], true)) {
                $this->markSupportTicketResolved($pdo, $ticketId, $newStatus);
            } else {
                $this->reopenSupportTicketIfNeeded($pdo, $ticketId, $newStatus);
            }

            $changes = [];
            if ($changedStatus) {
                $changes[] = 'Estado: ' . $currentStatus . ' -> ' . $newStatus;
            }
            if ($changedPriority) {
                $changes[] = 'Prioridad: ' . $currentPriority . ' -> ' . $newPriority;
            }
            if ($changedAssignee) {
                $changes[] = 'Asignación: ' . ($currentAssignee > 0 ? (string) $currentAssignee : 'sin asignar')
                    . ' -> '
                    . ($assigneeId !== null ? (string) $assigneeId : 'sin asignar');
            }

            $eventType = $changedStatus ? 'estado' : ($changedAssignee ? 'asignacion' : 'interno');
            $eventStmt = $pdo->prepare(
                'INSERT INTO soporte_ticket_eventos (
                    ticket_id, usuario_id, tipo, mensaje, estado_anterior, estado_nuevo, created_at
                ) VALUES (
                    :ticket_id, :usuario_id, :tipo, :mensaje, :estado_anterior, :estado_nuevo, NOW()
                )'
            );
            $eventStmt->execute([
                ':ticket_id' => $ticketId,
                ':usuario_id' => $userId > 0 ? $userId : null,
                ':tipo' => $eventType,
                ':mensaje' => implode(' | ', $changes),
                ':estado_anterior' => $currentStatus,
                ':estado_nuevo' => $newStatus,
            ]);
        });

        AuditService::track('superadmin.soporte.ticket.actualizar', 'Ticket actualizado ID: ' . $ticketId);
        flash('success', 'Ticket actualizado correctamente.');
        redirect('superadmin');
    }

    public function addSupportTicketComment(Request $request): never
    {
        $ticketId = (int) $request->param('id', 0);
        $message = trim((string) $request->input('mensaje', ''));
        $type = trim((string) $request->input('tipo', 'comentario'));
        $allowedTypes = ['comentario', 'interno'];

        if ($ticketId <= 0 || $message === '' || !in_array($type, $allowedTypes, true)) {
            flash('error', 'Comentario inválido para ticket.');
            redirect('superadmin');
        }

        $pdo = Database::connection();
        $ticketStmt = $pdo->prepare('SELECT id FROM soporte_tickets WHERE id = :id LIMIT 1');
        $ticketStmt->execute([':id' => $ticketId]);
        if (!(bool) $ticketStmt->fetchColumn()) {
            flash('error', 'Ticket no encontrado para comentar.');
            redirect('superadmin');
        }

        $userId = (int) (Auth::id() ?? 0);
        Database::transaction(function (PDO $pdo) use ($ticketId, $userId, $type, $message): void {
            $insertStmt = $pdo->prepare(
                'INSERT INTO soporte_ticket_eventos (
                    ticket_id, usuario_id, tipo, mensaje, estado_anterior, estado_nuevo, created_at
                ) VALUES (
                    :ticket_id, :usuario_id, :tipo, :mensaje, NULL, NULL, NOW()
                )'
            );
            $insertStmt->execute([
                ':ticket_id' => $ticketId,
                ':usuario_id' => $userId > 0 ? $userId : null,
                ':tipo' => $type,
                ':mensaje' => $message,
            ]);

            $touchStmt = $pdo->prepare(
                'UPDATE soporte_tickets
                 SET updated_at = NOW()
                 WHERE id = :id'
            );
            $touchStmt->execute([':id' => $ticketId]);

            $this->registerFirstSupportResponseIfNeeded($pdo, $ticketId);
        });

        AuditService::track('superadmin.soporte.ticket.comentario', 'Comentario agregado ticket ID: ' . $ticketId);
        flash('success', 'Comentario agregado correctamente.');
        redirect('superadmin');
    }

    public function updateWompiConfig(Request $request): never
    {
        $incoming = [
            'wompi_public_key' => trim((string) $request->input('wompi_public_key', '')),
            'wompi_integrity_secret' => trim((string) $request->input('wompi_integrity_secret', '')),
            'wompi_events_secret' => trim((string) $request->input('wompi_events_secret', '')),
            'wompi_redirect_url' => trim((string) $request->input('wompi_redirect_url', '')),
        ];

        $updates = [];
        foreach ($incoming as $key => $value) {
            if ($value === '') {
                continue;
            }
            if (strlen($value) > 500) {
                flash('error', 'El valor de ' . $key . ' es demasiado largo.');
                redirect('superadmin');
            }

            if ($key === 'wompi_redirect_url') {
                if (filter_var($value, FILTER_VALIDATE_URL) === false || preg_match('/^https?:\/\//i', $value) !== 1) {
                    flash('error', 'WOMPI_REDIRECT_URL debe ser una URL válida (http o https).');
                    redirect('superadmin');
                }
            }
            $updates[$key] = $value;
        }

        if ($updates === []) {
            flash('error', 'No se recibieron nuevas llaves. Si no deseas cambiar un campo, déjalo en blanco.');
            redirect('superadmin');
        }

        try {
            (new GlobalSetting())->upsertMany($updates);
        } catch (\Throwable $e) {
            flash('error', 'No fue posible guardar configuración Wompi: ' . $e->getMessage());
            redirect('superadmin');
        }

        AuditService::track(
            'superadmin.wompi.actualizar',
            'Configuración Wompi actualizada. Claves: ' . implode(', ', array_keys($updates))
        );
        flash('success', 'Configuración Wompi guardada correctamente.');
        redirect('superadmin');
    }

    public function clearWompiKey(Request $request): never
    {
        $inputKey = trim((string) $request->input('key', ''));
        $keysMap = $this->wompiSettingMap();
        $settingKey = $keysMap[$inputKey] ?? null;

        if ($settingKey === null) {
            flash('error', 'Llave inválida para limpiar.');
            redirect('superadmin');
        }

        try {
            (new GlobalSetting())->deleteByKey($settingKey);
        } catch (\Throwable $e) {
            flash('error', 'No fue posible limpiar la llave: ' . $e->getMessage());
            redirect('superadmin');
        }

        AuditService::track('superadmin.wompi.limpiar', 'Llave limpiada: ' . $settingKey);
        flash('success', 'Llave eliminada correctamente: ' . strtoupper($settingKey));
        redirect('superadmin');
    }

    public function testWompiConnection(Request $request): never
    {
        $result = (new WompiService())->probeConnection();
        $ok = (bool) ($result['ok'] ?? false);
        $message = trim((string) ($result['message'] ?? 'Sin respuesta de prueba.'));

        if ($ok) {
            AuditService::track('superadmin.wompi.probar', 'Prueba de conexión Wompi exitosa.');
            flash('success', $message);
            redirect('superadmin');
        }

        AuditService::track('superadmin.wompi.probar', 'Prueba de conexión Wompi fallida.');
        flash('error', $message);
        redirect('superadmin');
    }

    public function updateSmtpConfig(Request $request): never
    {
        $host = strtolower(trim((string) $request->input('notify_smtp_host', '')));
        $portRaw = trim((string) $request->input('notify_smtp_port', ''));
        $username = trim((string) $request->input('notify_smtp_username', ''));
        $password = trim((string) $request->input('notify_smtp_password', ''));
        $secure = strtolower(trim((string) $request->input('notify_smtp_secure', 'tls')));
        $fromEmail = strtolower(trim((string) $request->input('notify_smtp_from_email', '')));
        $fromName = trim((string) $request->input('notify_smtp_from_name', 'Veltrix POS'));

        if ($host === '' || strlen($host) > 190) {
            flash('error', 'SMTP_HOST es obligatorio y debe tener máximo 190 caracteres.');
            redirect('superadmin?seccion=correo');
        }

        if (preg_match('/^[a-z0-9.\-]+$/i', $host) !== 1) {
            flash('error', 'SMTP_HOST contiene caracteres inválidos.');
            redirect('superadmin?seccion=correo');
        }

        if ($portRaw === '' || preg_match('/^\d+$/', $portRaw) !== 1) {
            flash('error', 'SMTP_PORT debe ser numérico.');
            redirect('superadmin?seccion=correo');
        }

        $port = (int) $portRaw;
        if ($port < 1 || $port > 65535) {
            flash('error', 'SMTP_PORT debe estar entre 1 y 65535.');
            redirect('superadmin?seccion=correo');
        }

        if (!in_array($secure, ['tls', 'ssl', 'none'], true)) {
            flash('error', 'SMTP_SECURE debe ser tls, ssl o none.');
            redirect('superadmin?seccion=correo');
        }

        if ($username !== '' && strlen($username) > 190) {
            flash('error', 'SMTP_USERNAME es demasiado largo (máximo 190 caracteres).');
            redirect('superadmin?seccion=correo');
        }

        if ($password !== '' && strlen($password) > 255) {
            flash('error', 'SMTP_PASSWORD es demasiado largo (máximo 255 caracteres).');
            redirect('superadmin?seccion=correo');
        }

        if ($fromEmail === '' || filter_var($fromEmail, FILTER_VALIDATE_EMAIL) === false) {
            flash('error', 'SMTP_FROM_EMAIL es obligatorio y debe ser válido.');
            redirect('superadmin?seccion=correo');
        }

        if ($fromName === '') {
            $fromName = 'Veltrix POS';
        }
        if (strlen($fromName) > 120) {
            flash('error', 'SMTP_FROM_NAME es demasiado largo (máximo 120 caracteres).');
            redirect('superadmin?seccion=correo');
        }

        $updates = [
            'notify_smtp_host' => $host,
            'notify_smtp_port' => (string) $port,
            'notify_smtp_username' => $username,
            'notify_smtp_secure' => $secure,
            'notify_smtp_from_email' => $fromEmail,
            'notify_smtp_from_name' => $fromName,
            // Sincroniza remitente base para módulos que usan notificación email estándar.
            'notify_email_from_address' => $fromEmail,
            'notify_email_from_name' => $fromName,
        ];

        if ($password !== '') {
            $updates['notify_smtp_password'] = $password;
        }

        try {
            (new GlobalSetting())->upsertMany($updates);
        } catch (\Throwable $e) {
            flash('error', 'No fue posible guardar configuración SMTP: ' . $e->getMessage());
            redirect('superadmin?seccion=correo');
        }

        AuditService::track(
            'superadmin.smtp.actualizar',
            'Configuración SMTP actualizada. Claves: ' . implode(', ', array_keys($updates))
        );
        flash('success', 'Configuración SMTP guardada correctamente.');
        redirect('superadmin?seccion=correo');
    }

    public function testSmtpConnection(Request $request): never
    {
        $targetEmail = strtolower(trim((string) $request->input('smtp_test_email', '')));
        if ($targetEmail === '') {
            $targetEmail = strtolower(trim((string) (Auth::user()['email'] ?? '')));
        }

        if ($targetEmail === '' || filter_var($targetEmail, FILTER_VALIDATE_EMAIL) === false) {
            flash('error', 'Debes indicar un correo válido para la prueba SMTP.');
            redirect('superadmin?seccion=correo');
        }

        $subject = 'Prueba SMTP Veltrix POS';
        $sentAt = date('Y-m-d H:i:s');
        $safeSentAt = htmlspecialchars($sentAt, ENT_QUOTES, 'UTF-8');
        $safeTargetEmail = htmlspecialchars($targetEmail, ENT_QUOTES, 'UTF-8');
        $textMessage = "Prueba SMTP exitosa.\nFecha: {$sentAt}\nDestino: {$targetEmail}\nSistema: Veltrix POS";
        $htmlMessage = '<div style="font-family:Arial,sans-serif;padding:18px;border:1px solid #dbe4ff;border-radius:12px;">'
            . '<h2 style="margin:0 0 8px;color:#1f2f56;">Prueba SMTP Veltrix POS</h2>'
            . '<p style="margin:0 0 10px;color:#4a587c;">Este correo confirma que la configuración SMTP está operativa.</p>'
            . '<p style="margin:0;color:#1f2f56;"><strong>Fecha:</strong> ' . $safeSentAt . '</p>'
            . '<p style="margin:4px 0 0;color:#1f2f56;"><strong>Destino:</strong> ' . $safeTargetEmail . '</p>'
            . '</div>';

        $result = (new NotificationChannelService())->sendEmail($targetEmail, $subject, $textMessage, $htmlMessage);
        $ok = (bool) ($result['ok'] ?? false);
        $error = trim((string) ($result['error'] ?? ''));

        if ($ok) {
            AuditService::track('superadmin.smtp.probar', 'Prueba SMTP exitosa hacia: ' . $targetEmail);
            flash('success', 'Correo de prueba enviado correctamente a ' . $targetEmail . '.');
            redirect('superadmin?seccion=correo');
        }

        AuditService::track('superadmin.smtp.probar', 'Prueba SMTP fallida hacia: ' . $targetEmail);
        flash('error', 'La prueba SMTP falló: ' . ($error !== '' ? $error : 'sin detalle.'));
        redirect('superadmin?seccion=correo');
    }

    public function updateNotificationConfig(Request $request): never
    {
        $redirectUrl = 'superadmin?seccion=notificaciones';
        $updates = [];

        $emailFromName = trim((string) $request->input('notify_email_from_name', ''));
        if ($emailFromName !== '') {
            if (strlen($emailFromName) > 120) {
                flash('error', 'El nombre remitente de email es demasiado largo (máximo 120 caracteres).');
                redirect($redirectUrl);
            }
            $updates['notify_email_from_name'] = $emailFromName;
        }

        $emailFromAddress = strtolower(trim((string) $request->input('notify_email_from_address', '')));
        if ($emailFromAddress !== '') {
            if (filter_var($emailFromAddress, FILTER_VALIDATE_EMAIL) === false) {
                flash('error', 'El correo remitente no es válido.');
                redirect($redirectUrl);
            }
            $updates['notify_email_from_address'] = $emailFromAddress;
        }

        $emailReplyTo = strtolower(trim((string) $request->input('notify_email_reply_to', '')));
        if ($emailReplyTo !== '') {
            if (filter_var($emailReplyTo, FILTER_VALIDATE_EMAIL) === false) {
                flash('error', 'El correo reply-to no es válido.');
                redirect($redirectUrl);
            }
            $updates['notify_email_reply_to'] = $emailReplyTo;
        }

        $whatsappProvider = strtolower(trim((string) $request->input('notify_whatsapp_provider', 'meta')));
        if (!in_array($whatsappProvider, ['meta', 'manychat'], true)) {
            flash('error', 'Proveedor de WhatsApp inválido.');
            redirect($redirectUrl);
        }
        $updates['notify_whatsapp_provider'] = $whatsappProvider;

        $whatsappApiUrl = trim((string) $request->input('notify_whatsapp_api_url', ''));
        if ($whatsappApiUrl !== '') {
            $isValidUrl = filter_var($whatsappApiUrl, FILTER_VALIDATE_URL) !== false
                && preg_match('/^https?:\/\//i', $whatsappApiUrl) === 1;
            if (!$isValidUrl) {
                flash('error', 'La URL del API de WhatsApp no es válida.');
                redirect($redirectUrl);
            }
            if (strlen($whatsappApiUrl) > 255) {
                flash('error', 'La URL del API de WhatsApp es demasiado larga.');
                redirect($redirectUrl);
            }
            $updates['notify_whatsapp_api_url'] = rtrim($whatsappApiUrl, '/');
        }

        $whatsappPhoneNumberId = trim((string) $request->input('notify_whatsapp_phone_number_id', ''));
        if ($whatsappPhoneNumberId !== '') {
            if (strlen($whatsappPhoneNumberId) > 120) {
                flash('error', 'El Phone Number ID de WhatsApp es demasiado largo.');
                redirect($redirectUrl);
            }
            $updates['notify_whatsapp_phone_number_id'] = $whatsappPhoneNumberId;
        }

        $whatsappBusinessAccountId = trim((string) $request->input('notify_whatsapp_business_account_id', ''));
        if ($whatsappBusinessAccountId !== '') {
            if (preg_match('/^\d{6,40}$/', $whatsappBusinessAccountId) !== 1) {
                flash('error', 'El WhatsApp Business Account ID (WABA) debe ser numérico (6 a 40 dígitos).');
                redirect($redirectUrl);
            }
            $updates['notify_whatsapp_business_account_id'] = $whatsappBusinessAccountId;
        }

        $whatsappAccessToken = trim((string) $request->input('notify_whatsapp_access_token', ''));
        if ($whatsappAccessToken !== '') {
            if (strlen($whatsappAccessToken) > 4096) {
                flash('error', 'El access token de WhatsApp es demasiado largo (máximo 4096 caracteres).');
                redirect($redirectUrl);
            }
            $updates['notify_whatsapp_access_token'] = $whatsappAccessToken;
        }

        $countryCodeRaw = trim((string) $request->input('notify_whatsapp_default_country_code', ''));
        if ($countryCodeRaw !== '') {
            $countryCode = preg_replace('/[^0-9]/', '', $countryCodeRaw) ?: '';
            if ($countryCode === '' || strlen($countryCode) > 5) {
                flash('error', 'El código de país WhatsApp debe tener entre 1 y 5 dígitos.');
                redirect($redirectUrl);
            }
            $updates['notify_whatsapp_default_country_code'] = $countryCode;
        }

        $manyChatApiUrl = trim((string) $request->input('notify_manychat_api_url', ''));
        if ($manyChatApiUrl !== '') {
            $isValidManyChatUrl = filter_var($manyChatApiUrl, FILTER_VALIDATE_URL) !== false
                && preg_match('/^https?:\/\//i', $manyChatApiUrl) === 1;
            if (!$isValidManyChatUrl) {
                flash('error', 'La URL del API de ManyChat no es válida.');
                redirect($redirectUrl);
            }
            if (strlen($manyChatApiUrl) > 255) {
                flash('error', 'La URL del API de ManyChat es demasiado larga.');
                redirect($redirectUrl);
            }
            $updates['notify_manychat_api_url'] = rtrim($manyChatApiUrl, '/');
        }

        $manyChatAccessToken = trim((string) $request->input('notify_manychat_access_token', ''));
        if ($manyChatAccessToken !== '') {
            if (strlen($manyChatAccessToken) > 4096) {
                flash('error', 'El access token de ManyChat es demasiado largo (máximo 4096 caracteres).');
                redirect($redirectUrl);
            }
            $updates['notify_manychat_access_token'] = $manyChatAccessToken;
        }

        $manyChatMessageTag = strtoupper(trim((string) $request->input('notify_manychat_message_tag', '')));
        if ($manyChatMessageTag !== '') {
            if (preg_match('/^[A-Z0-9_]{3,120}$/', $manyChatMessageTag) !== 1) {
                flash('error', 'El Message Tag de ManyChat debe contener solo mayúsculas, números y guion bajo (3-120 caracteres).');
                redirect($redirectUrl);
            }
            $updates['notify_manychat_message_tag'] = $manyChatMessageTag;
        }

        $manyChatContactFieldId = trim((string) $request->input('notify_manychat_contact_field_id', ''));
        if ($manyChatContactFieldId !== '') {
            if (preg_match('/^\d{1,20}$/', $manyChatContactFieldId) !== 1) {
                flash('error', 'El Custom Field ID de ManyChat debe ser numérico (1-20 dígitos).');
                redirect($redirectUrl);
            }
            $updates['notify_manychat_contact_field_id'] = $manyChatContactFieldId;
        }

        $updates['notify_whatsapp_template_enabled'] = (int) $request->input('notify_whatsapp_template_enabled', 0) === 1 ? '1' : '0';

        $whatsAppTemplateLanguage = trim((string) $request->input('notify_whatsapp_template_language', ''));
        if ($whatsAppTemplateLanguage !== '') {
            if (preg_match('/^[a-z]{2}([_-][A-Za-z]{2})?$/', $whatsAppTemplateLanguage) !== 1) {
                flash('error', 'El idioma de plantilla de WhatsApp debe tener formato válido (ej: es_CO).');
                redirect($redirectUrl);
            }
            $updates['notify_whatsapp_template_language'] = str_replace('-', '_', $whatsAppTemplateLanguage);
        }

        $whatsAppTemplateName10d = trim((string) $request->input('notify_whatsapp_template_name_10d', ''));
        if ($whatsAppTemplateName10d !== '') {
            if (preg_match('/^[a-z0-9_]{3,120}$/', $whatsAppTemplateName10d) !== 1) {
                flash('error', 'La plantilla de 10 días debe tener solo minúsculas, números y guion bajo (3-120 caracteres).');
                redirect($redirectUrl);
            }
            $updates['notify_whatsapp_template_name_10d'] = $whatsAppTemplateName10d;
        }

        $whatsAppTemplateName3d = trim((string) $request->input('notify_whatsapp_template_name_3d', ''));
        if ($whatsAppTemplateName3d !== '') {
            if (preg_match('/^[a-z0-9_]{3,120}$/', $whatsAppTemplateName3d) !== 1) {
                flash('error', 'La plantilla de 3 días debe tener solo minúsculas, números y guion bajo (3-120 caracteres).');
                redirect($redirectUrl);
            }
            $updates['notify_whatsapp_template_name_3d'] = $whatsAppTemplateName3d;
        }

        $retryMaxAttemptsRaw = trim((string) $request->input('notify_retry_max_attempts', ''));
        if ($retryMaxAttemptsRaw !== '') {
            $retryMaxAttempts = (int) $retryMaxAttemptsRaw;
            if ($retryMaxAttempts < 1 || $retryMaxAttempts > 15) {
                flash('error', 'Los intentos máximos deben estar entre 1 y 15.');
                redirect($redirectUrl);
            }
            $updates['notify_retry_max_attempts'] = (string) $retryMaxAttempts;
        }

        $backoffRaw = trim((string) $request->input('notify_retry_backoff_minutes', ''));
        if ($backoffRaw !== '') {
            $segments = array_filter(array_map('trim', explode(',', $backoffRaw)), static fn (string $v): bool => $v !== '');
            if ($segments === []) {
                flash('error', 'El backoff debe ser una lista de minutos separados por coma, por ejemplo: 5,30,180.');
                redirect($redirectUrl);
            }

            $validBackoff = [];
            foreach ($segments as $segment) {
                if (preg_match('/^\d+$/', $segment) !== 1) {
                    flash('error', 'El backoff contiene valores inválidos. Usa solo minutos numéricos.');
                    redirect($redirectUrl);
                }
                $minutes = (int) $segment;
                if ($minutes < 1 || $minutes > 10080) {
                    flash('error', 'Cada valor de backoff debe estar entre 1 y 10080 minutos.');
                    redirect($redirectUrl);
                }
                $validBackoff[] = $minutes;
            }

            $updates['notify_retry_backoff_minutes'] = implode(',', array_values(array_unique($validBackoff)));
        }

        $updates['notify_channel_email'] = (int) $request->input('notify_channel_email', 0) === 1 ? '1' : '0';
        $updates['notify_channel_whatsapp'] = (int) $request->input('notify_channel_whatsapp', 0) === 1 ? '1' : '0';

        try {
            (new GlobalSetting())->upsertMany($updates);
        } catch (\Throwable $e) {
            flash('error', 'No fue posible guardar la configuración de notificaciones: ' . $e->getMessage());
            redirect($redirectUrl);
        }

        AuditService::track(
            'superadmin.notificaciones.actualizar',
            'Configuración de notificaciones actualizada. Claves: ' . implode(', ', array_keys($updates))
        );
        flash('success', 'Configuración de notificaciones guardada correctamente.');
        redirect($redirectUrl);
    }

    public function clearNotificationKey(Request $request): never
    {
        $redirectSection = strtolower(trim((string) $request->input('redirect_section', 'notificaciones')));
        $allowedSections = ['notificaciones', 'whatsapp_meta', 'whatsapp_meta_templates', 'whatsapp_manychat', 'correo'];
        if (!in_array($redirectSection, $allowedSections, true)) {
            $redirectSection = 'notificaciones';
        }
        $redirectUrl = 'superadmin?seccion=' . $redirectSection;
        $inputKey = trim((string) $request->input('key', ''));
        $keysMap = $this->notificationSettingMap();
        $settingKey = $keysMap[$inputKey] ?? null;

        if ($settingKey === null) {
            flash('error', 'Llave de notificación inválida para limpiar.');
            redirect($redirectUrl);
        }

        try {
            (new GlobalSetting())->deleteByKey($settingKey);
        } catch (\Throwable $e) {
            flash('error', 'No fue posible limpiar la llave: ' . $e->getMessage());
            redirect($redirectUrl);
        }

        AuditService::track('superadmin.notificaciones.limpiar', 'Llave limpiada: ' . $settingKey);
        flash('success', 'Llave de notificación eliminada: ' . strtoupper($settingKey));
        redirect($redirectUrl);
    }

    public function testNotificationWhatsApp(Request $request): never
    {
        $redirectUrl = 'superadmin?seccion=notificaciones';
        $phone = trim((string) $request->input('test_whatsapp_phone', ''));
        $message = trim((string) $request->input('test_whatsapp_message', ''));

        if ($phone === '') {
            flash('error', 'Ingresa un número de WhatsApp para la prueba.');
            redirect($redirectUrl);
        }

        if ($message === '') {
            $message = 'Prueba de notificaciones Veltrix - ' . date('Y-m-d H:i:s');
        }

        $provider = (string) config('notifications.whatsapp.provider', 'meta');
        try {
            $raw = (new GlobalSetting())->getMany(['notify_whatsapp_provider']);
            $provider = trim((string) ($raw['notify_whatsapp_provider'] ?? $provider));
        } catch (\Throwable) {
            $provider = (string) config('notifications.whatsapp.provider', 'meta');
        }
        $provider = strtolower($provider);
        if (!in_array($provider, ['meta', 'manychat'], true)) {
            $provider = 'meta';
        }

        $notifier = new NotificationChannelService();
        $normalizedPhone = $notifier->normalizePhone($phone);
        $result = $notifier->sendWhatsAppText($phone, $message);
        $ok = (bool) ($result['ok'] ?? false);
        $error = trim((string) ($result['error'] ?? ''));
        $response = $result['response'] ?? null;

        if ($ok) {
            $metaState = '';
            if (is_array($response) && is_array($response['body'] ?? null)) {
                $body = $response['body'];
                $messageId = trim((string) ((is_array($body['messages'] ?? null) && isset($body['messages'][0]['id']))
                    ? $body['messages'][0]['id']
                    : ''));
                $messageStatus = trim((string) ((is_array($body['messages'] ?? null) && isset($body['messages'][0]['message_status']))
                    ? $body['messages'][0]['message_status']
                    : ''));
                if ($messageId !== '') {
                    $metaState .= ' ID: ' . $messageId . '.';
                }
                if ($messageStatus !== '') {
                    $metaState .= ' Estado API: ' . $messageStatus . '.';
                }
            }

            AuditService::track(
                'superadmin.notificaciones.whatsapp.probar',
                'Prueba WhatsApp aceptada vía ' . $provider . ' hacia ' . ($normalizedPhone !== '' ? $normalizedPhone : $phone)
            );
            flash(
                'success',
                'Prueba WhatsApp aceptada por API vía ' . strtoupper($provider)
                    . ' hacia ' . ($normalizedPhone !== '' ? $normalizedPhone : $phone)
                    . '.'
                    . $metaState
                    . ' Si no llega, abre conversación enviando "Hola" al número del negocio y reintenta.'
            );
            redirect($redirectUrl);
        }

        AuditService::track(
            'superadmin.notificaciones.whatsapp.probar',
            'Prueba WhatsApp fallida vía ' . $provider . ' hacia ' . ($normalizedPhone !== '' ? $normalizedPhone : $phone)
        );
        flash('error', 'La prueba WhatsApp falló: ' . ($error !== '' ? $error : 'sin detalle.'));
        redirect($redirectUrl);
    }

    public function storeWhatsAppMetaTemplate(Request $request): never
    {
        $redirectUrl = 'superadmin?seccion=whatsapp_meta';
        $redirectModalUrl = $redirectUrl . '&wa_modal=template';
        Session::setOldInput($request->all());

        $templateUsage = strtolower(trim((string) $request->input('template_usage', 'libre')));
        if ($templateUsage !== 'libre' && preg_match('/^(previo_\d{1,3}_dias|previo_1_dia|vence_hoy|vencida|morosa)$/', $templateUsage) !== 1) {
            $templateUsage = 'libre';
        }

        $templateName = strtolower(trim((string) $request->input('template_name', '')));
        if ($templateName === '' || preg_match('/^[a-z0-9_]{3,120}$/', $templateName) !== 1) {
            flash('error', 'El nombre de plantilla debe usar solo minusculas, numeros y guion bajo (3-120 caracteres).');
            redirect($redirectModalUrl);
        }

        $templateLanguage = trim((string) $request->input('template_language', 'es_CO'));
        if (preg_match('/^[a-z]{2}([_-][A-Za-z]{2})?$/', $templateLanguage) !== 1) {
            flash('error', 'El idioma debe tener formato valido, por ejemplo: es_CO.');
            redirect($redirectModalUrl);
        }
        $templateLanguage = str_replace('-', '_', $templateLanguage);

        $templateCategory = strtoupper(trim((string) $request->input('template_category', 'UTILITY')));
        if (!in_array($templateCategory, ['UTILITY', 'MARKETING', 'AUTHENTICATION'], true)) {
            flash('error', 'La categoria de plantilla no es valida.');
            redirect($redirectModalUrl);
        }

        $templateHeader = trim((string) $request->input('template_header_text', ''));
        $templateBody = trim((string) $request->input('template_body_text', ''));
        $templateFooter = trim((string) $request->input('template_footer_text', ''));
        $templateButtonText = trim((string) $request->input('template_button_text', ''));
        $templateButtonUrl = trim((string) $request->input('template_button_url', ''));
        $templateExamples = trim((string) $request->input('template_variable_examples', ''));

        if ($templateBody === '' || strlen($templateBody) > 1024) {
            flash('error', 'El cuerpo de la plantilla es obligatorio y debe tener maximo 1024 caracteres.');
            redirect($redirectModalUrl);
        }
        if ($templateHeader !== '' && strlen($templateHeader) > 120) {
            flash('error', 'El encabezado de la plantilla puede tener maximo 120 caracteres.');
            redirect($redirectModalUrl);
        }
        if ($templateFooter !== '' && strlen($templateFooter) > 120) {
            flash('error', 'El pie de pagina puede tener maximo 120 caracteres.');
            redirect($redirectModalUrl);
        }
        if ($templateButtonText !== '' && strlen($templateButtonText) > 30) {
            flash('error', 'El texto del boton puede tener maximo 30 caracteres.');
            redirect($redirectModalUrl);
        }
        if ($templateButtonUrl !== '') {
            $validButtonUrl = filter_var($templateButtonUrl, FILTER_VALIDATE_URL) !== false
                && preg_match('/^https?:\/\//i', $templateButtonUrl) === 1;
            if (!$validButtonUrl || strlen($templateButtonUrl) > 255) {
                flash('error', 'La URL del boton debe iniciar por http(s) y tener maximo 255 caracteres.');
                redirect($redirectModalUrl);
            }
            if ($templateButtonText === '') {
                $templateButtonText = 'Pagar enlace';
            }
        } elseif ($templateButtonText !== '') {
            flash('error', 'Si defines texto de boton, debes definir tambien la URL de destino.');
            redirect($redirectModalUrl);
        }

        $service = new WhatsAppTemplateService();
        $authUser = Auth::user();
        $creatorId = (int) ($authUser['id'] ?? 0);

        $result = $service->createAndSubmit([
            'nombre' => $templateName,
            'idioma' => $templateLanguage,
            'categoria' => $templateCategory,
            'uso' => $templateUsage,
            'encabezado_texto' => $templateHeader,
            'cuerpo_texto' => $templateBody,
            'pie_texto' => $templateFooter,
            'boton_texto' => $templateButtonText,
            'boton_url' => $templateButtonUrl,
            'ejemplos_variables' => $templateExamples,
        ], $creatorId);

        if (!(bool) ($result['created'] ?? false)) {
            $message = trim((string) ($result['error'] ?? 'No fue posible crear la plantilla en Veltrix.'));
            flash('error', $message);
            redirect($redirectModalUrl);
        }

        $templateAutoAssign = (int) $request->input('template_auto_assign', 0) === 1;
        if ($templateAutoAssign && in_array($templateUsage, ['previo_10_dias', 'previo_3_dias'], true)) {
            $updates = [
                'notify_whatsapp_template_enabled' => '1',
                'notify_whatsapp_template_language' => $templateLanguage,
            ];
            if ($templateUsage === 'previo_10_dias') {
                $updates['notify_whatsapp_template_name_10d'] = $templateName;
            } elseif ($templateUsage === 'previo_3_dias') {
                $updates['notify_whatsapp_template_name_3d'] = $templateName;
            }

            try {
                (new GlobalSetting())->upsertMany($updates);
            } catch (\Throwable $e) {
                flash('error', 'Plantilla creada, pero no se pudo asociar a la automatizacion: ' . $e->getMessage());
                redirect($redirectUrl);
            }
        }

        $statusRaw = trim((string) ($result['status_raw'] ?? 'PENDING'));
        $statusLocal = trim((string) ($result['status_local'] ?? 'pendiente'));
        $statusLabel = $statusRaw !== '' ? $statusRaw : strtoupper($statusLocal);
        $actionLabel = (bool) ($result['updated_existing'] ?? false) ? 'actualizada' : 'creada';

        if ((bool) ($result['ok'] ?? false)) {
            AuditService::track(
                'superadmin.whatsapp.template.crear',
                'Plantilla WhatsApp ' . $actionLabel . ' y enviada a Meta: ' . $templateName . ' (' . $statusLabel . ')'
            );
            Session::clearOldInput();
            flash(
                'success',
                'Plantilla ' . $actionLabel . ' en Veltrix y enviada a Meta correctamente. Estado actual: ' . $statusLabel . '.'
            );
            redirect($redirectUrl);
        }

        $errorDetail = trim((string) ($result['error'] ?? 'Meta no acepto la plantilla en este intento.'));
        AuditService::track(
            'superadmin.whatsapp.template.crear',
            'Plantilla ' . $actionLabel . ' localmente con error Meta: ' . $templateName
        );
        flash(
            'error',
            'Plantilla ' . $actionLabel . ' en Veltrix, pero Meta devolvio error: ' . $errorDetail
                . '. Puedes ajustar y volver a enviar desde el modal.'
        );
        redirect($redirectModalUrl);
    }

    public function syncWhatsAppMetaTemplateStatus(Request $request): never
    {
        $returnModal = strtolower(trim((string) $request->input('return_modal', '')));
        $redirectUrl = 'superadmin?seccion=whatsapp_meta_templates';
        if (in_array($returnModal, ['template_link'], true)) {
            $redirectUrl = 'superadmin?seccion=whatsapp_meta&wa_modal=' . $returnModal;
        }
        $templateId = (int) $request->param('id', 0);
        if ($templateId <= 0) {
            flash('error', 'Plantilla invalida para sincronizar.');
            redirect($redirectUrl);
        }

        $service = new WhatsAppTemplateService();
        $result = $service->syncTemplateStatus($templateId);
        if (!(bool) ($result['ok'] ?? false)) {
            $message = trim((string) ($result['error'] ?? 'No fue posible sincronizar estado con Meta.'));
            flash('error', $message);
            redirect($redirectUrl);
        }

        $statusLocal = trim((string) ($result['status_local'] ?? 'pendiente'));
        $statusRaw = trim((string) ($result['status_raw'] ?? strtoupper($statusLocal)));
        $statusDetail = trim((string) ($result['message'] ?? ''));

        AuditService::track(
            'superadmin.whatsapp.template.sincronizar',
            'Plantilla sincronizada con Meta. ID local: ' . $templateId . ', estado: ' . $statusRaw
        );
        flash(
            'success',
            'Estado de plantilla sincronizado: ' . $statusRaw
                . ($statusDetail !== '' ? (' (' . $statusDetail . ')') : '')
        );
        redirect($redirectUrl);
    }

    public function syncWhatsAppMetaTemplateCatalog(Request $request): never
    {
        $returnModal = strtolower(trim((string) $request->input('return_modal', '')));
        $redirectUrl = 'superadmin?seccion=whatsapp_meta_templates';
        if (in_array($returnModal, ['template_link'], true)) {
            $redirectUrl = 'superadmin?seccion=whatsapp_meta&wa_modal=' . $returnModal;
        }
        $limitRaw = trim((string) $request->input('meta_sync_limit', '120'));
        if ($limitRaw !== '' && preg_match('/^\d{1,3}$/', $limitRaw) !== 1) {
            flash('error', 'El límite de sincronización debe ser numérico (1 a 250).');
            redirect($redirectUrl);
        }

        $limit = (int) ($limitRaw !== '' ? $limitRaw : '120');
        $limit = max(1, min(250, $limit));

        $service = new WhatsAppTemplateService();
        $authUser = Auth::user();
        $creatorId = (int) ($authUser['id'] ?? 0);
        $result = $service->syncCatalogFromMeta($limit, $creatorId);
        if (!(bool) ($result['ok'] ?? false)) {
            $message = trim((string) ($result['error'] ?? 'No fue posible sincronizar plantillas desde Meta.'));
            flash('error', $message);
            redirect($redirectUrl);
        }

        $created = (int) ($result['created'] ?? 0);
        $updated = (int) ($result['updated'] ?? 0);
        $failed = (int) ($result['failed'] ?? 0);
        $total = (int) ($result['total'] ?? ($created + $updated + $failed));
        $wabaId = trim((string) ($result['waba_id'] ?? ''));
        AuditService::track(
            'superadmin.whatsapp.template.catalogo.sincronizar',
            'Sincronización de catálogo Meta. Total: ' . $total . ', creadas: ' . $created . ', actualizadas: ' . $updated . ', fallidas: ' . $failed
        );
        if ($total <= 0) {
            flash(
                'error',
                'Meta no devolvió plantillas para sincronizar'
                    . ($wabaId !== '' ? (' (WABA: ' . $wabaId . ')') : '')
                    . '. Verifica que ese WABA tenga plantillas creadas y que el token tenga permisos vigentes.'
            );
            redirect($redirectUrl);
        }

        flash(
            'success',
            'Catálogo Meta sincronizado. Total: ' . $total . ' · nuevas: ' . $created . ' · actualizadas: ' . $updated
                . ($failed > 0 ? (' · omitidas: ' . $failed) : '') . '.'
        );
        redirect($redirectUrl);
    }

    public function bindWhatsAppMetaTemplate(Request $request): never
    {
        $returnModal = strtolower(trim((string) $request->input('return_modal', '')));
        $redirectUrl = 'superadmin?seccion=whatsapp_meta_templates';
        if (in_array($returnModal, ['template_link'], true)) {
            $redirectUrl = 'superadmin?seccion=whatsapp_meta&wa_modal=' . $returnModal;
        }
        $templateId = (int) $request->param('id', 0);
        if ($templateId <= 0) {
            flash('error', 'Plantilla inválida para enlazar.');
            redirect($redirectUrl);
        }

        $linkSlot = strtolower(trim((string) $request->input('link_slot', '')));
        $daysBeforeDueRaw = trim((string) $request->input('link_days_before_due', ''));
        $resolvedSlot = '';
        $slotLabel = '';

        if ($daysBeforeDueRaw !== '') {
            if (preg_match('/^\d{1,3}$/', $daysBeforeDueRaw) !== 1) {
                flash('error', 'Los días antes de vencer deben ser numéricos (0 a 365).');
                redirect($redirectUrl);
            }
            $daysBeforeDue = (int) $daysBeforeDueRaw;
            if ($daysBeforeDue < 0 || $daysBeforeDue > 365) {
                flash('error', 'Los días antes de vencer deben estar entre 0 y 365.');
                redirect($redirectUrl);
            }

            $resolvedSlot = $this->noticeTypeFromDaysBeforeDue($daysBeforeDue);
            $slotLabel = $daysBeforeDue === 0
                ? 'día de vencimiento'
                : ($daysBeforeDue . ' día' . ($daysBeforeDue === 1 ? '' : 's') . ' antes de vencer');
        } elseif (in_array($linkSlot, ['previo_10_dias', 'previo_3_dias', 'vencida', 'morosa', 'previo_1_dia', 'vence_hoy', 'libre'], true)) {
            $resolvedSlot = $linkSlot;
            $slotLabel = $this->templateSlotLabel($resolvedSlot);
        } else {
            flash('error', 'Debes elegir un destino válido para enlazar la plantilla.');
            redirect($redirectUrl);
        }

        $template = (new WhatsAppMetaTemplate())->findById($templateId);
        if (!is_array($template)) {
            flash('error', 'La plantilla no existe en el sistema.');
            redirect($redirectUrl);
        }

        $templateName = strtolower(trim((string) ($template['nombre'] ?? '')));
        $templateLanguage = trim((string) ($template['idioma'] ?? 'es_CO'));
        if ($templateName === '') {
            flash('error', 'La plantilla seleccionada no tiene nombre válido.');
            redirect($redirectUrl);
        }

        try {
            $pdo = Database::connection();
            if ($resolvedSlot !== 'libre') {
                $resetStmt = $pdo->prepare(
                    'UPDATE whatsapp_meta_templates
                     SET uso = :libre, updated_at = NOW()
                     WHERE uso = :slot AND id <> :id'
                );
                $resetStmt->execute([
                    ':libre' => 'libre',
                    ':slot' => $resolvedSlot,
                    ':id' => $templateId,
                ]);
            }

            $updateStmt = $pdo->prepare(
                'UPDATE whatsapp_meta_templates
                 SET uso = :slot, updated_at = NOW()
                 WHERE id = :id
                 LIMIT 1'
            );
            $updateStmt->execute([
                ':slot' => $resolvedSlot,
                ':id' => $templateId,
            ]);

            if ($resolvedSlot !== 'libre') {
                $settingsUpdates = [
                    'notify_whatsapp_template_enabled' => '1',
                    'notify_whatsapp_template_language' => $templateLanguage !== '' ? $templateLanguage : 'es_CO',
                ];
                if ($resolvedSlot === 'previo_10_dias') {
                    $settingsUpdates['notify_whatsapp_template_name_10d'] = $templateName;
                } elseif ($resolvedSlot === 'previo_3_dias') {
                    $settingsUpdates['notify_whatsapp_template_name_3d'] = $templateName;
                }
                (new GlobalSetting())->upsertMany($settingsUpdates);
            }
        } catch (\Throwable $e) {
            flash('error', 'No se pudo enlazar la plantilla: ' . $e->getMessage());
            redirect($redirectUrl);
        }

        AuditService::track(
            'superadmin.whatsapp.template.enlazar',
            'Plantilla enlazada a aviso ' . $slotLabel . ': ' . $templateName . ' (' . $resolvedSlot . ')'
        );
        flash('success', $resolvedSlot === 'libre'
            ? 'Plantilla desenlazada correctamente.'
            : ('Plantilla enlazada correctamente para ' . $slotLabel . '.')
        );
        redirect($redirectUrl);
    }

    public function updateWhatsAppTemplateDispatchTimes(Request $request): never
    {
        $redirectUrl = 'superadmin?seccion=whatsapp_meta_templates';
        $dispatchTimes = $request->input('dispatch_time', []);
        if (!is_array($dispatchTimes)) {
            flash('error', 'Los horarios recibidos no tienen un formato válido.');
            redirect($redirectUrl);
        }

        $updates = [];
        $deletions = [];
        foreach ($dispatchTimes as $noticeTypeRaw => $timeRaw) {
            $noticeType = $this->normalizeDispatchNoticeType((string) $noticeTypeRaw);
            if ($noticeType === null) {
                continue;
            }

            $time = $this->normalizeDispatchTime((string) $timeRaw);
            if ($time === null) {
                flash('error', 'Hora inválida para "' . $this->templateSlotLabel((string) $noticeTypeRaw) . '". Usa formato HH:MM (24 horas).');
                redirect($redirectUrl);
            }

            $settingKey = $this->dispatchTimeSettingKey($noticeType);
            if ($settingKey === '') {
                continue;
            }

            if ($time === '') {
                $deletions[] = $settingKey;
            } else {
                $updates[$settingKey] = $time;
            }
        }

        if ($updates === [] && $deletions === []) {
            flash('error', 'No se detectaron horarios para guardar.');
            redirect($redirectUrl);
        }

        try {
            $settings = new GlobalSetting();
            if ($updates !== []) {
                $settings->upsertMany($updates);
            }
            foreach (array_values(array_unique($deletions)) as $settingKey) {
                $settings->deleteByKey((string) $settingKey);
            }
        } catch (\Throwable $e) {
            flash('error', 'No se pudo guardar la programación de WhatsApp: ' . $e->getMessage());
            redirect($redirectUrl);
        }

        AuditService::track(
            'superadmin.whatsapp.template.programacion',
            'Horarios de salida actualizados: ' . implode(', ', array_keys($updates))
        );
        flash('success', 'Horario de salida actualizado correctamente.');
        redirect($redirectUrl);
    }

    public function sendWhatsAppMetaTemplateNow(Request $request): never
    {
        $redirectUrl = 'superadmin?seccion=whatsapp_meta_templates';
        $templateId = (int) $request->param('id', 0);
        if ($templateId <= 0) {
            flash('error', 'Plantilla inválida para envío inmediato.');
            redirect($redirectUrl);
        }

        $template = (new WhatsAppMetaTemplate())->findById($templateId);
        if (!is_array($template)) {
            flash('error', 'La plantilla seleccionada no existe.');
            redirect($redirectUrl);
        }

        $phone = trim((string) $request->input('send_now_phone', ''));
        $recipientName = trim((string) $request->input('send_now_name', 'Cliente'));
        if ($recipientName === '') {
            $recipientName = 'Cliente';
        }
        $planAmount = (float) $request->input('send_now_plan_amount', 0);
        $planAmount = max(0.0, $planAmount);

        if ($phone === '') {
            flash('error', 'Debes indicar un número de WhatsApp para enviar ahora.');
            redirect($redirectUrl);
        }

        $notifier = new NotificationChannelService();
        $normalizedPhone = $notifier->normalizePhone($phone);
        if ($normalizedPhone === '') {
            flash('error', 'El número destino no tiene un formato válido.');
            redirect($redirectUrl);
        }

        $noticeType = strtolower(trim((string) ($template['uso'] ?? '')));
        if ($noticeType === '' || $noticeType === 'libre') {
            $noticeType = 'previo_3_dias';
        }

        $daysBeforeDue = $this->daysBeforeDueFromNoticeType($noticeType);
        $dueDateTs = strtotime('today +' . $daysBeforeDue . ' day');
        if ($dueDateTs === false) {
            $dueDateTs = time();
        }
        $dueDate = date('Y-m-d', $dueDateTs);
        $timelyDate = date('Y-m-d', strtotime($dueDate . ' -1 day') ?: $dueDateTs);

        $monthLabel = $this->monthLabelSpanish($dueDate);
        $dueDateHuman = $this->dateHumanSpanish($dueDate);
        $timelyDateHuman = $this->dateHumanSpanish($timelyDate);
        $amountLabel = $this->formatMoneyCop($planAmount);
        $paymentLink = $this->buildSendNowPaymentLink($templateId, $planAmount);

        $templateName = strtolower(trim((string) ($template['nombre'] ?? '')));
        $templateLanguage = trim((string) ($template['idioma'] ?? 'es_CO'));
        $fallbackMessage = trim((string) ($template['cuerpo_texto'] ?? ''));
        if ($fallbackMessage === '') {
            $fallbackMessage = 'Aviso de suscripción Veltrix (' . $monthLabel . '). Fecha de pago: ' . $timelyDateHuman . '. Vence: ' . $dueDateHuman . '.';
        }
        if ($paymentLink !== '') {
            $fallbackMessage .= "\n" . $paymentLink;
        }

        $result = $notifier->sendWhatsAppNotice($normalizedPhone, $fallbackMessage, [
            'notice_type' => $noticeType,
            'template_name' => $templateName,
            'template_language' => $templateLanguage,
            'body_parameters' => [
                $recipientName,
                $monthLabel,
                $timelyDateHuman,
                $dueDateHuman,
                $amountLabel,
            ],
            'payment_link' => $paymentLink,
        ]);

        if (!(bool) ($result['ok'] ?? false)) {
            $error = trim((string) ($result['error'] ?? 'No fue posible enviar el mensaje.'));
            flash('error', 'No se pudo enviar ahora: ' . $error);
            redirect($redirectUrl);
        }

        $messageId = '';
        $response = $result['response'] ?? null;
        if (is_array($response) && is_array($response['body'] ?? null) && is_array($response['body']['messages'] ?? null)) {
            $messageId = trim((string) ($response['body']['messages'][0]['id'] ?? ''));
        }

        AuditService::track(
            'superadmin.whatsapp.template.enviar_ahora',
            'Plantilla enviada manualmente. ID local: ' . $templateId . ' destino: ' . $normalizedPhone
        );
        flash(
            'success',
            'Envío inmediato aceptado para ' . $normalizedPhone
                . ($messageId !== '' ? (' · ID: ' . $messageId) : '')
                . '.'
        );
        redirect($redirectUrl);
    }

    public function updateAiConfig(Request $request): never
    {
        $provider = strtolower(trim((string) $request->input('ai_provider', 'openai')));
        if ($provider === '') {
            $provider = 'openai';
        }
        if (!in_array($provider, ['openai'], true)) {
            flash('error', 'Proveedor IA no soportado.');
            redirect('superadmin?seccion=ai');
        }

        $enabled = (int) $request->input('ai_enabled', 0) === 1 ? '1' : '0';
        $model = trim((string) $request->input('ai_model', 'gpt-4o-mini'));
        $baseUrl = trim((string) $request->input('ai_openai_base_url', 'https://api.openai.com/v1'));
        $temperatureRaw = trim((string) $request->input('ai_temperature', '0.2'));
        $maxOutputTokensRaw = trim((string) $request->input('ai_max_output_tokens', '700'));
        $systemPrompt = trim((string) $request->input('ai_system_prompt', ''));
        $apiKey = trim((string) $request->input('ai_openai_api_key', ''));

        if ($model === '' || strlen($model) > 120) {
            flash('error', 'El modelo IA es obligatorio y debe tener máximo 120 caracteres.');
            redirect('superadmin?seccion=ai');
        }

        $isValidBaseUrl = filter_var($baseUrl, FILTER_VALIDATE_URL) !== false
            && preg_match('/^https?:\/\//i', $baseUrl) === 1;
        if (!$isValidBaseUrl || strlen($baseUrl) > 255) {
            flash('error', 'La URL base de OpenAI no es válida.');
            redirect('superadmin?seccion=ai');
        }

        if (!is_numeric($temperatureRaw)) {
            flash('error', 'La temperatura IA debe ser numérica.');
            redirect('superadmin?seccion=ai');
        }
        $temperature = (float) $temperatureRaw;
        if ($temperature < 0 || $temperature > 1.2) {
            flash('error', 'La temperatura IA debe estar entre 0 y 1.2.');
            redirect('superadmin?seccion=ai');
        }

        if ($maxOutputTokensRaw === '' || preg_match('/^\d+$/', $maxOutputTokensRaw) !== 1) {
            flash('error', 'Max output tokens debe ser un número entero.');
            redirect('superadmin?seccion=ai');
        }
        $maxOutputTokens = (int) $maxOutputTokensRaw;
        if ($maxOutputTokens < 120 || $maxOutputTokens > 1800) {
            flash('error', 'Max output tokens debe estar entre 120 y 1800.');
            redirect('superadmin?seccion=ai');
        }

        if ($systemPrompt !== '' && strlen($systemPrompt) > 6000) {
            flash('error', 'El system prompt es demasiado largo (máximo 6000 caracteres).');
            redirect('superadmin?seccion=ai');
        }

        if ($apiKey !== '' && strlen($apiKey) > 500) {
            flash('error', 'La API key es demasiado larga.');
            redirect('superadmin?seccion=ai');
        }

        $updates = [
            'ai_enabled' => $enabled,
            'ai_provider' => $provider,
            'ai_model' => $model,
            'ai_openai_base_url' => rtrim($baseUrl, '/'),
            'ai_temperature' => (string) $temperature,
            'ai_max_output_tokens' => (string) $maxOutputTokens,
        ];
        if ($systemPrompt !== '') {
            $updates['ai_system_prompt'] = $systemPrompt;
        }
        if ($apiKey !== '') {
            $updates['ai_openai_api_key'] = $apiKey;
        }

        try {
            (new GlobalSetting())->upsertMany($updates);
        } catch (\Throwable $e) {
            flash('error', 'No fue posible guardar configuración del agente IA: ' . $e->getMessage());
            redirect('superadmin?seccion=ai');
        }

        (new AiHelpService())->clearRuntimeCache();
        AuditService::track(
            'superadmin.ai.actualizar',
            'Configuración IA actualizada. Claves: ' . implode(', ', array_keys($updates))
        );
        flash('success', 'Configuración del agente IA guardada correctamente.');
        redirect('superadmin?seccion=ai');
    }

    public function clearAiKey(Request $request): never
    {
        $inputKey = trim((string) $request->input('key', ''));
        $keysMap = $this->aiSettingMap();
        $settingKey = $keysMap[$inputKey] ?? null;

        if ($settingKey === null) {
            flash('error', 'Llave IA inválida para limpiar.');
            redirect('superadmin?seccion=ai');
        }

        try {
            (new GlobalSetting())->deleteByKey($settingKey);
        } catch (\Throwable $e) {
            flash('error', 'No fue posible limpiar la llave IA: ' . $e->getMessage());
            redirect('superadmin?seccion=ai');
        }

        (new AiHelpService())->clearRuntimeCache();
        AuditService::track('superadmin.ai.limpiar', 'Llave IA limpiada: ' . $settingKey);
        flash('success', 'Llave IA eliminada: ' . strtoupper($settingKey));
        redirect('superadmin?seccion=ai');
    }

    public function testAiAssistant(Request $request): never
    {
        $question = trim((string) $request->input('ai_test_prompt', ''));
        if ($question === '') {
            $question = '¿Qué módulos principales tiene Veltrix POS y en qué orden recomiendas iniciar?';
        }

        $result = (new AiHelpService())->probe($question);
        if ((bool) ($result['ok'] ?? false)) {
            $answer = trim((string) ($result['answer'] ?? ''));
            $snippet = $answer !== '' ? substr($answer, 0, 220) : 'Prueba exitosa.';
            AuditService::track('superadmin.ai.probar', 'Prueba de asistente IA exitosa.');
            flash('success', 'Prueba IA OK. Respuesta: ' . $snippet);
            redirect('superadmin?seccion=ai');
        }

        $error = trim((string) ($result['error'] ?? 'Sin detalle.'));
        AuditService::track('superadmin.ai.probar', 'Prueba de asistente IA fallida.');
        flash('error', 'La prueba del agente IA falló: ' . $error);
        redirect('superadmin?seccion=ai');
    }

    public function updateOpenAiReportConfig(Request $request): never
    {
        $dailyBudgetRaw = trim((string) $request->input('openai_budget_daily_usd', '0'));
        $monthlyBudgetRaw = trim((string) $request->input('openai_budget_monthly_usd', '0'));
        $companyDailyQuestionLimitRaw = trim((string) $request->input('openai_company_daily_question_limit', '0'));
        $contextHistoryLimitRaw = trim((string) $request->input('openai_context_history_limit', '12'));
        $dailyBudgetNormalized = str_replace(',', '.', $dailyBudgetRaw);
        $monthlyBudgetNormalized = str_replace(',', '.', $monthlyBudgetRaw);

        if ($dailyBudgetRaw === '' || preg_match('/^\d+(\.\d{1,2})?$/', $dailyBudgetNormalized) !== 1) {
            flash('error', 'El tope diario debe estar en USD con máximo 2 decimales. Ejemplo: 0.46');
            redirect('superadmin?seccion=reporte_openia');
        }
        if ($monthlyBudgetRaw === '' || preg_match('/^\d+(\.\d{1,2})?$/', $monthlyBudgetNormalized) !== 1) {
            flash('error', 'El tope mensual debe estar en USD con máximo 2 decimales. Ejemplo: 14.00');
            redirect('superadmin?seccion=reporte_openia');
        }
        if ($companyDailyQuestionLimitRaw === '' || preg_match('/^\d+$/', $companyDailyQuestionLimitRaw) !== 1) {
            flash('error', 'El límite diario de preguntas por empresa debe ser un entero mayor o igual a 0.');
            redirect('superadmin?seccion=reporte_openia');
        }
        if ($contextHistoryLimitRaw === '' || preg_match('/^\d+$/', $contextHistoryLimitRaw) !== 1) {
            flash('error', 'El recorte de historial debe ser un entero entre 4 y 30.');
            redirect('superadmin?seccion=reporte_openia');
        }

        $dailyBudget = round(max(0, (float) $dailyBudgetNormalized), 2);
        $monthlyBudget = round(max(0, (float) $monthlyBudgetNormalized), 2);
        $companyDailyQuestionLimit = max(0, min(100000, (int) $companyDailyQuestionLimitRaw));
        $contextHistoryLimit = max(4, min(30, (int) $contextHistoryLimitRaw));

        if ($dailyBudget > 1000000 || $monthlyBudget > 1000000) {
            flash('error', 'Los topes en USD no pueden superar 1,000,000.00.');
            redirect('superadmin?seccion=reporte_openia');
        }

        if ($monthlyBudget > 0 && $dailyBudget > $monthlyBudget) {
            flash('error', 'El tope diario no puede ser mayor al tope mensual.');
            redirect('superadmin?seccion=reporte_openia');
        }

        $updates = [
            OpenAiGovernanceService::KEY_DAILY_BUDGET_USD => (string) $dailyBudget,
            OpenAiGovernanceService::KEY_MONTHLY_BUDGET_USD => (string) $monthlyBudget,
            OpenAiGovernanceService::KEY_COMPANY_DAILY_QUESTION_LIMIT => (string) $companyDailyQuestionLimit,
            OpenAiGovernanceService::KEY_CONTEXT_HISTORY_LIMIT => (string) $contextHistoryLimit,
        ];

        try {
            (new GlobalSetting())->upsertMany($updates);
        } catch (\Throwable $e) {
            flash('error', 'No fue posible guardar la configuración de Reporte OpenIA: ' . $e->getMessage());
            redirect('superadmin?seccion=reporte_openia');
        }

        (new OpenAiGovernanceService())->clearRuntimeCache();
        AuditService::track(
            'superadmin.openia.reporte.actualizar',
            'Configuración de Reporte OpenIA actualizada. Claves: ' . implode(', ', array_keys($updates))
        );
        flash('success', 'Configuración de Reporte OpenIA guardada correctamente.');
        redirect('superadmin?seccion=reporte_openia');
    }

    private function resolveSupportReportRange(Request $request): array
    {
        $from = trim((string) $request->query('soporte_desde', date('Y-m-01')));
        $to = trim((string) $request->query('soporte_hasta', date('Y-m-d')));

        if (!$this->isDate($from)) {
            $from = date('Y-m-01');
        }
        if (!$this->isDate($to)) {
            $to = date('Y-m-d');
        }
        if ($from > $to) {
            [$from, $to] = [$to, $from];
        }

        return [$from, $to];
    }

    private function syncSupportSlaStates(PDO $pdo): void
    {
        $firstResponseLateStmt = $pdo->prepare(
            'UPDATE soporte_tickets
             SET sla_primera_respuesta_estado = "incumplido",
                 updated_at = NOW()
             WHERE primera_respuesta_at IS NULL
               AND sla_primera_respuesta_estado = "pendiente"
               AND primera_respuesta_limite_at IS NOT NULL
               AND NOW() > primera_respuesta_limite_at'
        );
        $firstResponseLateStmt->execute();

        $resolutionLateStmt = $pdo->prepare(
            'UPDATE soporte_tickets
             SET sla_resolucion_estado = "incumplido",
                 updated_at = NOW()
             WHERE resuelto_at IS NULL
               AND estado IN ("abierto", "en_proceso", "pendiente_cliente")
               AND sla_resolucion_estado = "pendiente"
               AND resolucion_limite_at IS NOT NULL
               AND NOW() > resolucion_limite_at'
        );
        $resolutionLateStmt->execute();
    }

    private function ticketSlaTargets(string $priority): array
    {
        $defaults = [
            'first_response_minutes' => 120,
            'resolution_hours' => 24,
        ];
        $configByPriority = config('support.sla.' . $priority, []);

        return [
            'first_response_minutes' => max(
                1,
                (int) ($configByPriority['first_response_minutes'] ?? $defaults['first_response_minutes'])
            ),
            'resolution_hours' => max(
                1,
                (int) ($configByPriority['resolution_hours'] ?? $defaults['resolution_hours'])
            ),
        ];
    }

    private function registerFirstSupportResponseIfNeeded(PDO $pdo, int $ticketId): void
    {
        if ($ticketId <= 0) {
            return;
        }

        $stmt = $pdo->prepare(
            'UPDATE soporte_tickets
             SET primera_respuesta_at = NOW(),
                 sla_primera_respuesta_estado = CASE
                    WHEN primera_respuesta_limite_at IS NULL OR NOW() <= primera_respuesta_limite_at
                        THEN "cumplido"
                    ELSE "incumplido"
                 END,
                 updated_at = NOW()
             WHERE id = :id
               AND primera_respuesta_at IS NULL'
        );
        $stmt->execute([':id' => $ticketId]);
    }

    private function markSupportTicketResolved(PDO $pdo, int $ticketId, string $status): void
    {
        if ($ticketId <= 0) {
            return;
        }

        $closedAt = $status === 'cerrado' ? date('Y-m-d H:i:s') : null;

        $stmt = $pdo->prepare(
            'UPDATE soporte_tickets
             SET resuelto_at = COALESCE(resuelto_at, NOW()),
                 cerrado_at = COALESCE(cerrado_at, :cerrado_at),
                 sla_resolucion_estado = CASE
                    WHEN resolucion_limite_at IS NULL OR NOW() <= resolucion_limite_at
                        THEN "cumplido"
                    ELSE "incumplido"
                 END,
                 updated_at = NOW()
             WHERE id = :id'
        );
        $stmt->execute([
            ':cerrado_at' => $closedAt,
            ':id' => $ticketId,
        ]);
    }

    private function reopenSupportTicketIfNeeded(PDO $pdo, int $ticketId, string $status): void
    {
        if ($ticketId <= 0 || in_array($status, ['resuelto', 'cerrado'], true)) {
            return;
        }

        $stmt = $pdo->prepare(
            'UPDATE soporte_tickets
             SET cerrado_at = NULL,
                 resuelto_at = NULL,
                 sla_resolucion_estado = CASE
                    WHEN resolucion_limite_at IS NOT NULL AND NOW() > resolucion_limite_at
                        THEN "incumplido"
                    ELSE "pendiente"
                 END,
                 updated_at = NOW()
             WHERE id = :id'
        );
        $stmt->execute([':id' => $ticketId]);
    }

    private function purgeCompanyData(PDO $pdo, int $companyId): void
    {
        if ($companyId <= 0) {
            return;
        }

        // Dependencias sin empresa_id directo.
        $deleteSupportEventsStmt = $pdo->prepare(
            'DELETE ste
             FROM soporte_ticket_eventos ste
             INNER JOIN soporte_tickets st ON st.id = ste.ticket_id
             WHERE st.empresa_id = :empresa_id'
        );
        $deleteSupportEventsStmt->execute([':empresa_id' => $companyId]);

        $deleteInvoiceDetailsStmt = $pdo->prepare(
            'DELETE fd
             FROM factura_detalles fd
             INNER JOIN facturas f ON f.id = fd.factura_id
             WHERE f.empresa_id = :empresa_id'
        );
        $deleteInvoiceDetailsStmt->execute([':empresa_id' => $companyId]);

        $companyTables = [
            'sucursales',
            'usuario_empresa',
            'logs_actividad',
            'pagos_suscripcion',
            'avisos_cobro_suscripcion',
            'saas_checkout_intents',
            'suscripciones',
            'soporte_tickets',
            'notificacion_vistas',
            'ai_help_messages',
            'ai_help_sessions',
            'cierres_caja',
            'traspasos_caja',
            'movimientos_caja',
            'aperturas_caja',
            'cajas',
            'venta_pagos',
            'venta_detalles',
            'devoluciones',
            'ventas',
            'pagos_compra',
            'compra_detalles',
            'compras',
            'recepcion_compra_detalles',
            'recepciones_compra',
            'orden_compra_detalles',
            'ordenes_compra',
            'movimientos_inventario',
            'inventario',
            'ajustes_inventario',
            'producto_variantes',
            'productos',
            'clientes',
            'proveedores',
            'facturas',
            'numeraciones_documentos',
            'resoluciones_dian',
            'impuestos',
            'metodos_pago',
            'impresoras',
            'categorias',
            'marcas',
            'unidades_medida',
            'configuracion_empresa',
            'usuarios',
        ];

        $pdo->exec('SET FOREIGN_KEY_CHECKS = 0');
        try {
            foreach ($companyTables as $table) {
                if (!$this->tableExists($pdo, $table)) {
                    continue;
                }

                $tableName = str_replace('`', '``', $table);
                $stmt = $pdo->prepare('DELETE FROM `' . $tableName . '` WHERE empresa_id = :empresa_id');
                $stmt->execute([':empresa_id' => $companyId]);
            }
        } finally {
            $pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
        }
    }

    private function tableExists(PDO $pdo, string $tableName): bool
    {
        static $cache = [];
        $tableName = trim($tableName);
        if ($tableName === '') {
            return false;
        }

        if (array_key_exists($tableName, $cache)) {
            return (bool) $cache[$tableName];
        }

        try {
            $stmt = $pdo->prepare(
                'SELECT COUNT(*)
                 FROM information_schema.TABLES
                 WHERE TABLE_SCHEMA = DATABASE()
                   AND TABLE_NAME = :table_name'
            );
            $stmt->execute([
                ':table_name' => $tableName,
            ]);
            $cache[$tableName] = (int) $stmt->fetchColumn() > 0;
        } catch (\Throwable) {
            $cache[$tableName] = false;
        }

        return (bool) $cache[$tableName];
    }

    private function hydrateCompaniesFactusOverview(array &$companies): void
    {
        if ($companies === []) {
            return;
        }

        foreach ($companies as &$company) {
            $companyId = (int) ($company['id'] ?? 0);
            if ($companyId <= 0) {
                continue;
            }

            $settings = $this->loadFactusSettingsForCompany($companyId);

            $planAllowsElectronicInvoicing = (int) ($company['plan_permite_facturacion_electronica'] ?? 0) === 1;
            $moduleEnabled = (string) ($settings['facturacion_electronica'] ?? '0') === '1';

            $flags = [
                'api_url' => trim((string) ($settings['factus_api_url'] ?? '')) !== '',
                'client_id' => trim((string) ($settings['factus_client_id'] ?? '')) !== '',
                'client_secret' => trim((string) ($settings['factus_client_secret'] ?? '')) !== '',
                'username' => trim((string) ($settings['factus_username'] ?? '')) !== '',
                'password' => trim((string) ($settings['factus_password'] ?? '')) !== '',
                'software_id' => trim((string) ($settings['factus_software_id'] ?? '')) !== '',
                'software_pin' => trim((string) ($settings['factus_software_pin'] ?? '')) !== '',
                'numbering_range_id' => trim((string) ($settings['factus_numbering_range_id'] ?? '')) !== '',
                'municipality_id' => trim((string) ($settings['factus_municipality_id'] ?? '')) !== '',
            ];

            $fullyConfigured = $planAllowsElectronicInvoicing && $moduleEnabled;
            foreach ($flags as $flag) {
                if (!$flag) {
                    $fullyConfigured = false;
                    break;
                }
            }

            $company['fe_module_active'] = $moduleEnabled ? 1 : 0;
            $company['plan_allows_fe'] = $planAllowsElectronicInvoicing ? 1 : 0;
            $company['factus_ready'] = $fullyConfigured ? 1 : 0;
            $company['factus_configured_flags'] = $flags;
            $company['factus_api_url'] = (string) ($settings['factus_api_url'] ?? '');
            $company['factus_numbering_range_id'] = (string) ($settings['factus_numbering_range_id'] ?? '');
            $company['factus_municipality_id'] = (string) ($settings['factus_municipality_id'] ?? '');
        }
        unset($company);
    }

    private function loadCompanyForFactus(PDO $pdo, int $companyId): ?array
    {
        if ($companyId <= 0) {
            return null;
        }

        $stmt = $pdo->prepare(
            'SELECT id, nombre, email, nit, telefono, direccion
             FROM empresas
             WHERE id = :id
             LIMIT 1'
        );
        $stmt->execute([':id' => $companyId]);
        $company = $stmt->fetch();

        return is_array($company) ? $company : null;
    }

    private function companyAllowsElectronicInvoicing(PDO $pdo, int $companyId): bool
    {
        if ($companyId <= 0) {
            return false;
        }

        $stmt = $pdo->prepare(
            'SELECT p.permite_facturacion_electronica
             FROM suscripciones s
             INNER JOIN planes p ON p.id = s.plan_id
             WHERE s.empresa_id = :empresa_id
             ORDER BY s.id DESC
             LIMIT 1'
        );
        $stmt->execute([':empresa_id' => $companyId]);

        return (int) $stmt->fetchColumn() === 1;
    }

    private function resolveAndValidateFactusSettingsForCompany(Request $request, int $companyId, bool $requireIssuanceFields = true): array
    {
        $existing = $this->loadFactusSettingsForCompany($companyId);

        $fields = [
            'api_url' => ['input' => 'factus_api_url', 'label' => 'URL API Factus', 'max' => 255],
            'client_id' => ['input' => 'factus_client_id', 'label' => 'Client ID', 'max' => 120],
            'client_secret' => ['input' => 'factus_client_secret', 'label' => 'Client Secret', 'max' => 255],
            'username' => ['input' => 'factus_username', 'label' => 'Usuario Factus', 'max' => 120],
            'password' => ['input' => 'factus_password', 'label' => 'Contraseña Factus', 'max' => 255],
            'software_id' => ['input' => 'factus_software_id', 'label' => 'Software ID', 'max' => 120],
            'software_pin' => ['input' => 'factus_software_pin', 'label' => 'Software PIN', 'max' => 120],
        ];

        $data = [];
        foreach ($fields as $key => $meta) {
            $inputKey = (string) ($meta['input'] ?? '');
            $label = (string) ($meta['label'] ?? $inputKey);
            $maxLength = (int) ($meta['max'] ?? 255);

            $incoming = trim((string) $request->input($inputKey, ''));
            $value = $incoming !== '' ? $incoming : trim((string) ($existing[$inputKey] ?? ''));

            if ($value === '') {
                return [
                    'ok' => false,
                    'message' => $label . ' es obligatorio.',
                ];
            }

            if (strlen($value) > $maxLength) {
                return [
                    'ok' => false,
                    'message' => $label . ' excede el tamaño permitido (' . $maxLength . ').',
                ];
            }

            $data[$key] = $value;
        }

        $numberingRangeInput = trim((string) $request->input('factus_numbering_range_id', ''));
        $numberingRangeValue = $numberingRangeInput !== ''
            ? $numberingRangeInput
            : trim((string) ($existing['factus_numbering_range_id'] ?? ''));

        if ($requireIssuanceFields && $numberingRangeValue === '') {
            return [
                'ok' => false,
                'message' => 'El Numbering Range ID de Factus es obligatorio.',
            ];
        }
        if ($numberingRangeValue !== '' && preg_match('/^\d+$/', $numberingRangeValue) !== 1) {
            return [
                'ok' => false,
                'message' => 'El Numbering Range ID de Factus debe ser numérico.',
            ];
        }
        $data['numbering_range_id'] = $numberingRangeValue;

        $municipalityInput = trim((string) $request->input('factus_municipality_id', ''));
        $municipalityValue = $municipalityInput !== ''
            ? $municipalityInput
            : trim((string) ($existing['factus_municipality_id'] ?? ''));

        if ($requireIssuanceFields && $municipalityValue === '') {
            return [
                'ok' => false,
                'message' => 'El Municipality ID de Factus es obligatorio.',
            ];
        }
        if ($municipalityValue !== '' && preg_match('/^\d+$/', $municipalityValue) !== 1) {
            return [
                'ok' => false,
                'message' => 'El Municipality ID de Factus debe ser numérico.',
            ];
        }
        $data['municipality_id'] = $municipalityValue;

        $apiUrl = trim((string) ($data['api_url'] ?? ''));
        if (!filter_var($apiUrl, FILTER_VALIDATE_URL)) {
            return [
                'ok' => false,
                'message' => 'La URL API Factus no es válida.',
            ];
        }

        $scheme = strtolower((string) parse_url($apiUrl, PHP_URL_SCHEME));
        if (!in_array($scheme, ['http', 'https'], true)) {
            return [
                'ok' => false,
                'message' => 'La URL API Factus debe iniciar por http:// o https://.',
            ];
        }

        if (preg_match('/^[A-Za-z0-9._:-]+$/', (string) ($data['client_id'] ?? '')) !== 1) {
            return [
                'ok' => false,
                'message' => 'Client ID contiene caracteres inválidos.',
            ];
        }

        $username = (string) ($data['username'] ?? '');
        if (str_contains($username, '@') && !filter_var($username, FILTER_VALIDATE_EMAIL)) {
            return [
                'ok' => false,
                'message' => 'Usuario Factus no tiene formato válido.',
            ];
        }

        if (preg_match('/^[A-Za-z0-9._:-]+$/', (string) ($data['software_id'] ?? '')) !== 1) {
            return [
                'ok' => false,
                'message' => 'Software ID contiene caracteres inválidos.',
            ];
        }

        if (preg_match('/^[A-Za-z0-9._:-]+$/', (string) ($data['software_pin'] ?? '')) !== 1) {
            return [
                'ok' => false,
                'message' => 'Software PIN contiene caracteres inválidos.',
            ];
        }

        $data['api_url'] = rtrim($apiUrl, '/');

        return [
            'ok' => true,
            'data' => $data,
        ];
    }

    private function loadFactusSettingsForCompany(int $companyId): array
    {
        if ($companyId <= 0) {
            return [];
        }

        $keys = [
            'facturacion_electronica',
            'factus_api_url',
            'factus_client_id',
            'factus_client_secret',
            'factus_username',
            'factus_password',
            'factus_software_id',
            'factus_software_pin',
            'factus_numbering_range_id',
            'factus_municipality_id',
        ];
        $settings = (new CompanySetting())->getMany($companyId, $keys);
        $this->encryptFactusSettingsIfNeeded($companyId, $settings);

        foreach ($this->factusSensitiveKeys() as $sensitiveKey) {
            $settings[$sensitiveKey] = SecretCipherService::decrypt((string) ($settings[$sensitiveKey] ?? ''));
        }

        return $settings;
    }

    private function factusSensitiveKeys(): array
    {
        return [
            'factus_client_id',
            'factus_client_secret',
            'factus_username',
            'factus_password',
            'factus_software_id',
            'factus_software_pin',
        ];
    }

    private function encryptFactusSettingsIfNeeded(int $companyId, array &$settings): void
    {
        if ($companyId <= 0) {
            return;
        }

        $updates = [];
        foreach ($this->factusSensitiveKeys() as $sensitiveKey) {
            $currentValue = trim((string) ($settings[$sensitiveKey] ?? ''));
            if ($currentValue === '' || SecretCipherService::isEncrypted($currentValue)) {
                continue;
            }

            $encryptedValue = SecretCipherService::encrypt($currentValue);
            if ($encryptedValue === '' || $encryptedValue === $currentValue) {
                continue;
            }

            $settings[$sensitiveKey] = $encryptedValue;
            $updates[$sensitiveKey] = $encryptedValue;
        }

        if ($updates === []) {
            return;
        }

        try {
            (new CompanySetting())->upsertMany($companyId, $updates);
        } catch (\Throwable $e) {
        }
    }

    private function sanitizeSlug(string $value): string
    {
        $value = strtolower(trim($value));
        $value = preg_replace('/[^a-z0-9]+/i', '-', $value) ?: '';
        return trim($value, '-');
    }

    private function noticeTypeFromDaysBeforeDue(int $daysBeforeDue): string
    {
        $daysBeforeDue = max(0, $daysBeforeDue);
        if ($daysBeforeDue === 10) {
            return 'previo_10_dias';
        }
        if ($daysBeforeDue === 3) {
            return 'previo_3_dias';
        }
        if ($daysBeforeDue === 1) {
            return 'previo_1_dia';
        }
        if ($daysBeforeDue === 0) {
            return 'vence_hoy';
        }

        return 'previo_' . $daysBeforeDue . '_dias';
    }

    private function templateSlotLabel(string $slot): string
    {
        $slot = strtolower(trim($slot));
        if ($slot === 'previo_10_dias') {
            return '10 días antes de vencer';
        }
        if ($slot === 'previo_3_dias') {
            return '3 días antes de vencer';
        }
        if ($slot === 'previo_1_dia') {
            return '1 día antes de vencer';
        }
        if ($slot === 'vence_hoy') {
            return 'día de vencimiento';
        }
        if ($slot === 'vencida') {
            return 'suscripción vencida';
        }
        if ($slot === 'morosa') {
            return 'suscripción en mora';
        }
        if ($slot === 'libre') {
            return 'sin enlace';
        }
        if (preg_match('/^previo_(\d{1,3})_dias$/', $slot, $matches) === 1) {
            $days = (int) ($matches[1] ?? 0);
            return $days . ' días antes de vencer';
        }

        return $slot !== '' ? $slot : 'aviso';
    }

    private function resolveWhatsAppDispatchTimeSettings(array $templates): array
    {
        $noticeTypes = ['previo_10_dias', 'previo_3_dias', 'previo_1_dia', 'vence_hoy', 'vencida', 'morosa'];
        foreach ($templates as $template) {
            if (!is_array($template)) {
                continue;
            }
            $usage = strtolower(trim((string) ($template['uso'] ?? '')));
            $normalized = $this->normalizeDispatchNoticeType($usage);
            if ($normalized === null || $normalized === 'default') {
                continue;
            }
            $noticeTypes[] = $normalized;
        }
        $noticeTypes = array_values(array_unique($noticeTypes));

        $keys = ['notify_whatsapp_dispatch_time_default'];
        foreach ($noticeTypes as $noticeType) {
            $settingKey = $this->dispatchTimeSettingKey($noticeType);
            if ($settingKey !== '') {
                $keys[] = $settingKey;
            }
        }

        $raw = [];
        try {
            $raw = (new GlobalSetting())->getMany(array_values(array_unique($keys)));
        } catch (\Throwable) {
            $raw = [];
        }

        $default = $this->normalizeDispatchTime((string) ($raw['notify_whatsapp_dispatch_time_default'] ?? ''));
        if ($default === null || $default === '') {
            $default = '10:00';
        }

        $settings = ['default' => $default];
        foreach ($noticeTypes as $noticeType) {
            $settingKey = $this->dispatchTimeSettingKey($noticeType);
            if ($settingKey === '') {
                continue;
            }
            $time = $this->normalizeDispatchTime((string) ($raw[$settingKey] ?? ''));
            $settings[$noticeType] = ($time !== null && $time !== '') ? $time : $default;
        }

        return $settings;
    }

    private function normalizeDispatchNoticeType(string $noticeType): ?string
    {
        $noticeType = strtolower(trim($noticeType));
        if ($noticeType === '' || $noticeType === 'libre') {
            return null;
        }
        if ($noticeType === 'default') {
            return 'default';
        }
        if (in_array($noticeType, ['previo_1_dia', 'previo_3_dias', 'previo_10_dias', 'vence_hoy', 'vencida', 'morosa'], true)) {
            return $noticeType;
        }
        if (preg_match('/^previo_(\d{1,3})_dias$/', $noticeType, $matches) === 1) {
            $days = (int) ($matches[1] ?? 0);
            if ($days < 0 || $days > 365) {
                return null;
            }

            return 'previo_' . $days . '_dias';
        }

        return null;
    }

    private function dispatchTimeSettingKey(string $noticeType): string
    {
        $noticeType = strtolower(trim($noticeType));
        if ($noticeType === 'default') {
            return 'notify_whatsapp_dispatch_time_default';
        }
        if (preg_match('/^previo_(\d{1,3})_dias$/', $noticeType, $matches) === 1) {
            $days = (int) ($matches[1] ?? 0);
            return 'notify_whatsapp_dispatch_time_previo_' . $days . '_dias';
        }
        if (in_array($noticeType, ['vencida', 'morosa', 'vence_hoy', 'previo_1_dia'], true)) {
            return 'notify_whatsapp_dispatch_time_' . $noticeType;
        }

        return '';
    }

    private function normalizeDispatchTime(string $time): ?string
    {
        $time = trim($time);
        if ($time === '') {
            return '';
        }
        if (preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d$/', $time) !== 1) {
            return null;
        }

        return $time;
    }

    private function daysBeforeDueFromNoticeType(string $noticeType): int
    {
        $noticeType = strtolower(trim($noticeType));
        if ($noticeType === 'previo_10_dias') {
            return 10;
        }
        if ($noticeType === 'previo_3_dias') {
            return 3;
        }
        if ($noticeType === 'previo_1_dia') {
            return 1;
        }
        if ($noticeType === 'vence_hoy') {
            return 0;
        }
        if (preg_match('/^previo_(\d{1,3})_dias$/', $noticeType, $matches) === 1) {
            return max(0, (int) ($matches[1] ?? 0));
        }

        return 0;
    }

    private function monthLabelSpanish(string $date): string
    {
        if (!$this->isDate($date)) {
            return 'Este mes';
        }

        $month = (int) date('n', strtotime($date));
        $map = [
            1 => 'Enero',
            2 => 'Febrero',
            3 => 'Marzo',
            4 => 'Abril',
            5 => 'Mayo',
            6 => 'Junio',
            7 => 'Julio',
            8 => 'Agosto',
            9 => 'Septiembre',
            10 => 'Octubre',
            11 => 'Noviembre',
            12 => 'Diciembre',
        ];

        return $map[$month] ?? 'Este mes';
    }

    private function dateHumanSpanish(string $date): string
    {
        if (!$this->isDate($date)) {
            return $date;
        }

        $ts = strtotime($date);
        if ($ts === false) {
            return $date;
        }

        $day = (int) date('j', $ts);
        $month = strtolower($this->monthLabelSpanish($date));
        $year = (int) date('Y', $ts);

        return $day . ' de ' . $month . ' de ' . $year;
    }

    private function formatMoneyCop(float $amount): string
    {
        $amount = max(0.0, $amount);
        $formatted = number_format($amount, 2, ',', '.');
        if (str_ends_with($formatted, ',00')) {
            $formatted = substr($formatted, 0, -3);
        }

        return '$' . $formatted;
    }

    private function isDate(string $value): bool
    {
        return (bool) preg_match('/^\d{4}-\d{2}-\d{2}$/', $value);
    }

    private function ticketStatuses(): array
    {
        return ['abierto', 'en_proceso', 'pendiente_cliente', 'resuelto', 'cerrado'];
    }

    private function ticketPriorities(): array
    {
        return ['baja', 'media', 'alta', 'critica'];
    }

    private function smtpSettingMap(): array
    {
        return [
            'host' => 'notify_smtp_host',
            'port' => 'notify_smtp_port',
            'username' => 'notify_smtp_username',
            'password' => 'notify_smtp_password',
            'secure' => 'notify_smtp_secure',
            'from_email' => 'notify_smtp_from_email',
            'from_name' => 'notify_smtp_from_name',
        ];
    }

    private function notificationSettingMap(): array
    {
        return [
            'email_from_name' => 'notify_email_from_name',
            'email_from_address' => 'notify_email_from_address',
            'email_reply_to' => 'notify_email_reply_to',
            'whatsapp_provider' => 'notify_whatsapp_provider',
            'whatsapp_api_url' => 'notify_whatsapp_api_url',
            'whatsapp_phone_number_id' => 'notify_whatsapp_phone_number_id',
            'whatsapp_business_account_id' => 'notify_whatsapp_business_account_id',
            'whatsapp_access_token' => 'notify_whatsapp_access_token',
            'whatsapp_default_country_code' => 'notify_whatsapp_default_country_code',
            'manychat_api_url' => 'notify_manychat_api_url',
            'manychat_access_token' => 'notify_manychat_access_token',
            'manychat_message_tag' => 'notify_manychat_message_tag',
            'manychat_contact_field_id' => 'notify_manychat_contact_field_id',
            'whatsapp_template_enabled' => 'notify_whatsapp_template_enabled',
            'whatsapp_template_language' => 'notify_whatsapp_template_language',
            'whatsapp_template_name_10d' => 'notify_whatsapp_template_name_10d',
            'whatsapp_template_name_3d' => 'notify_whatsapp_template_name_3d',
            'channel_email' => 'notify_channel_email',
            'channel_whatsapp' => 'notify_channel_whatsapp',
            'retry_max_attempts' => 'notify_retry_max_attempts',
            'retry_backoff_minutes' => 'notify_retry_backoff_minutes',
        ];
    }

    private function wompiSettingMap(): array
    {
        return [
            'public' => 'wompi_public_key',
            'integrity' => 'wompi_integrity_secret',
            'events' => 'wompi_events_secret',
            'redirect' => 'wompi_redirect_url',
        ];
    }

    private function aiSettingMap(): array
    {
        return [
            'api_key' => 'ai_openai_api_key',
        ];
    }

    private function toBooleanSetting(string $value): bool
    {
        $normalized = strtolower(trim($value));
        if ($normalized === '') {
            return false;
        }

        return in_array($normalized, ['1', 'true', 'yes', 'si', 'on'], true);
    }

    private function maskSecret(string $value): string
    {
        $value = trim($value);
        if ($value === '') {
            return 'No configurada';
        }

        $length = strlen($value);
        if ($length <= 8) {
            return str_repeat('*', $length);
        }

        return substr($value, 0, 4)
            . str_repeat('*', max(4, $length - 8))
            . substr($value, -4);
    }

    private function buildSendNowPaymentLink(int $templateId, float $amount): string
    {
        $amountInCents = (int) round(max(0.0, $amount) * 100);
        if ($amountInCents <= 0) {
            return '';
        }

        $wompi = new WompiService();
        if (!$wompi->isConfiguredForCheckout()) {
            return '';
        }

        $referenceRaw = 'VXREN-' . max(1, $templateId) . '-' . date('YmdHis');
        $reference = substr(preg_replace('/[^A-Za-z0-9-]/', '', $referenceRaw) ?: '', 0, 32);
        if ($reference === '') {
            return '';
        }

        $fields = $wompi->buildCheckoutFields(
            $reference,
            $amountInCents,
            $wompi->redirectUrl(),
            null
        );
        $publicKey = trim((string) ($fields['public-key'] ?? ''));
        $signature = trim((string) ($fields['signature:integrity'] ?? ''));
        if ($publicKey === '' || $signature === '') {
            return '';
        }

        $checkoutUrl = trim($wompi->checkoutUrl());
        if ($checkoutUrl === '') {
            return '';
        }

        return rtrim($checkoutUrl, '?') . '?' . http_build_query($fields);
    }
}
