<?php

/**
 * Admin controller: menus, pages, bulk actions, assets.
 *
 * @package Leadite
 */
namespace Leadite\Admin;

use Leadite\Config;
use Leadite\Pro;
use Leadite\Capture\AdapterRegistry;
use Leadite\Storage\LeadsRepository;
use Leadite\Support\FieldMapper;
defined( 'ABSPATH' ) || exit;
class Admin {
    /** @var AdapterRegistry */
    protected $registry;

    /** @var string */
    protected $hook_suffix = '';

    public function __construct( AdapterRegistry $registry ) {
        $this->registry = $registry;
    }

    public function boot() {
        add_action( 'admin_menu', array($this, 'register_menu') );
        add_action( 'admin_init', array($this, 'handle_actions') );
        add_action( 'admin_init', array($this, 'handle_save_settings') );
        add_action( 'admin_enqueue_scripts', array($this, 'enqueue') );
        add_action( 'admin_post_leadite_export', array($this, 'handle_export') );
        add_filter( 'admin_footer_text', array($this, 'admin_footer_brand'), 11 );
        add_action( 'admin_footer', array($this, 'render_docs_popup') );
        add_action( 'admin_enqueue_scripts', array($this, 'enqueue_freemius_skin') );
        // f-pro-end
        add_action( 'wp_ajax_leadite_update_lead_status', array($this, 'ajax_update_lead_status') );
        add_action( 'wp_ajax_leadite_get_lead_details', array($this, 'ajax_get_lead_details') );
        add_action( 'wp_ajax_leadite_download_file', array($this, 'ajax_download_file') );
        // f-pro-end
        add_action( 'admin_post_leadite_save_field_map', array($this, 'handle_save_field_map') );
        add_action( 'wp_ajax_leadite_batch_anonymize_ip', array($this, 'ajax_batch_anonymize_ip') );
        // f-pro-end
        // Register Screen Options for the leads list (per-user column visibility).
        add_action( 'load-leadite_page_leadite-leads', array($this, 'register_screen_options') );
        // f-pro-end
        add_action( 'admin_notices', array($this, 'render_cron_notice') );
    }

    /**
     * Top-level menu + subpages (white-label brand name + counts badge).
     */
    public function register_menu() {
        $manage_cap = Config::capability();
        $view_cap = Config::view_capability();
        // Use the lower capability so view-only users also see the menu.
        // Individual render methods enforce the correct level per page.
        $menu_cap = ( current_user_can( $manage_cap ) ? $manage_cap : $view_cap );
        $counts = LeadsRepository::status_counts_cached();
        $new = ( isset( $counts['new'] ) ? (int) $counts['new'] : 0 );
        $bubble = ( $new ? ' <span class="awaiting-mod">' . number_format_i18n( $new ) . '</span>' : '' );
        $this->hook_suffix = add_menu_page(
            'leadite',
            'Leadite' . $bubble,
            $menu_cap,
            'leadite',
            array($this, 'render_dashboard'),
            Config::menu_icon(),
            26
        );
        add_submenu_page(
            'leadite',
            esc_html__( 'Dashboard', 'leadite' ),
            esc_html__( 'Dashboard', 'leadite' ),
            $menu_cap,
            'leadite',
            array($this, 'render_dashboard')
        );
        add_submenu_page(
            'leadite',
            esc_html__( 'All Leads', 'leadite' ),
            esc_html__( 'All Leads', 'leadite' ),
            $menu_cap,
            'leadite-leads',
            array($this, 'render_router')
        );
        add_submenu_page(
            'leadite',
            esc_html__( 'Follow-Up Guard', 'leadite' ),
            esc_html__( 'Follow-Up Guard', 'leadite' ),
            $menu_cap,
            'leadite-smart-views',
            array($this, 'render_smart_views')
        );
        // Settings: only for users who can manage (not view-only).
        add_submenu_page(
            'leadite',
            esc_html__( 'Settings', 'leadite' ),
            esc_html__( 'Settings', 'leadite' ),
            $manage_cap,
            'leadite-settings',
            array($this, 'render_settings')
        );
    }

    /**
     * Register Screen Options (column visibility) for the leads list page.
     * Hooked on load-{page_hook} so WP shows the gear icon with a Columns panel.
     */
    public function register_screen_options() {
        add_screen_option( 'per_page', array(
            'label'   => esc_html__( 'Leads per page', 'leadite' ),
            'default' => (int) Config::get( 'per_page', 25 ),
            'option'  => 'leadite_leads_per_page',
        ) );
    }

    /**
     * Enqueue admin CSS/JS only on our screens.
     *
     * @param string $hook Current admin hook suffix.
     */
    public function enqueue( $hook ) {
        if ( false === strpos( (string) $hook, 'leadite' ) ) {
            return;
        }
        // Google Fonts removed: system-font stack in admin.css is sufficient.
        wp_enqueue_style(
            'Leadite-admin',
            leadite_URL . 'assets/css/admin.css',
            array(),
            leadite_VERSION
        );
        // Native WP modal engine for the quick-guide popup.
        add_thickbox();
        // RTL (he/ar) is handled inline via CSS logical properties — no separate file needed.
        wp_register_script(
            'leadite-admin',
            leadite_URL . 'assets/js/admin.js',
            array('jquery'),
            leadite_VERSION,
            true
        );
        wp_enqueue_script(
            'leadite-sla-dismiss',
            leadite_URL . 'assets/js/leadite-sla-dismiss.js',
            array('leadite-admin'),
            leadite_VERSION,
            true
        );
        wp_enqueue_script( 'leadite-admin' );
        wp_localize_script( 'leadite-admin', 'leaditeAdmin', array(
            'ajaxUrl'         => admin_url( 'admin-ajax.php' ),
            'kanbanNonce'     => wp_create_nonce( 'leadite_kanban_nonce' ),
            'noteNonce'       => wp_create_nonce( 'leadite_note_nonce' ),
            'detailsNonce'    => wp_create_nonce( 'leadite_details_nonce' ),
            'starredNonce'    => wp_create_nonce( 'leadite_starred_nonce' ),
            'assignNonce'     => wp_create_nonce( 'leadite_assign_nonce' ),
            'tagsNonce'       => wp_create_nonce( 'leadite_tags_nonce' ),
            'slaNoticeNonce'  => wp_create_nonce( 'leadite_sla_dismiss' ),
            'testDigestNonce' => wp_create_nonce( 'leadite_test_digest_nonce' ),
            'strings'         => array(
                'importingText'    => esc_html__( 'Importing...', 'leadite' ),
                'copiedText'       => esc_html__( 'Copied!', 'leadite' ),
                'copiedErrorText'  => esc_html__( 'Error!', 'leadite' ),
                'confirmDelete'    => esc_html__( 'Delete the selected leads permanently? This cannot be undone.', 'leadite' ),
                'confirmDeleteTag' => esc_html__( 'Delete this tag? It will be removed from all leads.', 'leadite' ),
            ),
        ) );
    }

    /**
     * Render the premium admin dashboard with KPI cards and interactive SVG charts.
     */
    public function render_dashboard() {
        if ( !current_user_can( Config::capability() ) && !current_user_can( Config::view_capability() ) ) {
            wp_die( esc_html__( 'You do not have permission to view leads.', 'leadite' ) );
        }
        $kpis = LeadsRepository::get_kpis_cached();
        $leads_by_day = LeadsRepository::get_leads_by_day( 30 );
        $status_counts = LeadsRepository::status_counts_cached();
        $top_forms = LeadsRepository::get_leads_by_form( 5 );
        $arrow_next = ( is_rtl() ? '&larr;' : '&rarr;' );
        // Fetch 5 most recent submissions and batch-load their meta.
        $recent_query = LeadsRepository::query( array(
            'per_page' => 5,
            'page'     => 1,
        ) );
        $recent_leads = $recent_query['items'];
        $recent_meta_map = LeadsRepository::find_many( $recent_leads );
        echo '<div class="wrap leadite leadite-dashboard-page">';
        // Header
        echo '<div class="leadite-dashboard-header">';
        echo '<h1>' . esc_html( Config::brand() ) . ' — ' . esc_html__( 'Dashboard', 'leadite' ) . '</h1>';
        echo '<p class="leadite-dashboard-subtitle">' . esc_html__( 'Overview of your lead generation metrics and recent activity.', 'leadite' ) . '</p>';
        echo '</div>';
        // Calculate weekly growth performance
        $week_count = (int) $kpis['week'];
        $prev_week = (int) $kpis['prev_week'];
        $perf_val = '0%';
        $perf_label = esc_html__( 'same volume as last week', 'leadite' );
        $perf_class = 'growth-flat';
        $perf_icon = '<svg class="leadite-kpi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
        if ( $prev_week === 0 ) {
            if ( $week_count > 0 ) {
                $perf_val = '+100%';
                /* translators: %d: number of leads compared to previous week */
                $perf_label = sprintf( _n(
                    '+%d lead vs previous week',
                    '+%d leads vs previous week',
                    $week_count,
                    'leadite'
                ), $week_count );
                $perf_class = 'growth-up';
                $perf_icon = '<svg class="leadite-kpi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline><polyline points="17 6 23 6 23 12"></polyline></svg>';
            } else {
                $perf_label = esc_html__( 'no leads in last 14 days', 'leadite' );
            }
        } else {
            $diff = $week_count - $prev_week;
            $pct = round( $diff / $prev_week * 100 );
            if ( $diff > 0 ) {
                $perf_val = '+' . $pct . '%';
                /* translators: %d: number of leads compared to previous week */
                $perf_label = sprintf( _n(
                    '+%d lead vs previous week',
                    '+%d leads vs previous week',
                    $diff,
                    'leadite'
                ), $diff );
                $perf_class = 'growth-up';
                $perf_icon = '<svg class="leadite-kpi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline><polyline points="17 6 23 6 23 12"></polyline></svg>';
            } elseif ( $diff < 0 ) {
                $abs_diff = abs( $diff );
                $perf_val = $pct . '%';
                /* translators: %d: number of leads compared to previous week */
                $perf_label = sprintf( _n(
                    '-%d lead vs previous week',
                    '-%d leads vs previous week',
                    $abs_diff,
                    'leadite'
                ), $abs_diff );
                $perf_class = 'growth-down';
                $perf_icon = '<svg class="leadite-kpi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 18 13.5 8.5 8.5 13.5 1 6"></polyline><polyline points="17 18 23 18 23 12"></polyline></svg>';
            }
        }
        // KPI Cards
        echo '<div class="leadite-kpi-grid">';
        // Card 1: Total Leads
        $this->render_kpi_card(
            esc_html__( 'Total Leads', 'leadite' ),
            $kpis['total'],
            esc_html__( 'All time submissions', 'leadite' ),
            '<svg class="leadite-kpi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>',
            'total'
        );
        // Card 2: Today's Leads
        $this->render_kpi_card(
            esc_html__( 'Today', 'leadite' ),
            $kpis['today'],
            esc_html__( 'Last 24 hours', 'leadite' ),
            '<svg class="leadite-kpi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>',
            'today'
        );
        // Card 3: This Week
        $this->render_kpi_card(
            esc_html__( 'This Week', 'leadite' ),
            $kpis['week'],
            esc_html__( 'Last 7 days', 'leadite' ),
            '<svg class="leadite-kpi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg>',
            'week'
        );
        // Card 4: Unread / New
        $this->render_kpi_card(
            esc_html__( 'Unread Leads', 'leadite' ),
            $kpis['new'],
            esc_html__( 'Requires attention', 'leadite' ),
            '<svg class="leadite-kpi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>',
            'new'
        );
        // Card 5: Weekly Growth Performance
        $this->render_kpi_card(
            esc_html__( 'Weekly Growth', 'leadite' ),
            $perf_val,
            $perf_label,
            $perf_icon,
            $perf_class
        );
        echo '</div>';
        // leadite-kpi-grid
        // Main Grid: Charts & Top Forms
        echo '<div class="leadite-dashboard-grid">';
        // Left: Leads Over Time
        echo '<div class="leadite-card leadite-chart-card">';
        echo '<div class="leadite-card-header">';
        echo '<h2>' . esc_html__( 'Leads over Time', 'leadite' ) . '</h2>';
        echo '<p class="leadite-card-subtitle">' . esc_html__( 'Submission counts for the last 30 days', 'leadite' ) . '</p>';
        echo '</div>';
        echo '<div class="leadite-chart-wrapper">';
        $this->render_leads_time_chart( $leads_by_day );
        echo '</div>';
        echo '</div>';
        // leadite-chart-card
        // Right Sidebar
        echo '<div class="leadite-dashboard-sidebar">';
        // Top Right: Status Donut
        echo '<div class="leadite-card leadite-donut-card">';
        echo '<div class="leadite-card-header">';
        echo '<h2>' . esc_html__( 'Leads by Status', 'leadite' ) . '</h2>';
        echo '</div>';
        echo '<div class="leadite-donut-wrapper">';
        $this->render_status_donut_chart( $status_counts );
        echo '</div>';
        echo '</div>';
        // leadite-donut-card
        // Bottom Right: Top Forms List
        echo '<div class="leadite-card leadite-forms-card">';
        echo '<div class="leadite-card-header">';
        echo '<h2>' . esc_html__( 'Top Active Forms', 'leadite' ) . '</h2>';
        echo '</div>';
        echo '<div class="leadite-forms-list">';
        $this->render_top_forms_list( $top_forms );
        echo '</div>';
        echo '</div>';
        // leadite-forms-card
        // Follow-Up Guard widget
        $this->render_sla_dashboard_widget();
        echo '</div>';
        // leadite-dashboard-sidebar
        echo '</div>';
        // leadite-dashboard-grid
        // Bottom: Recent Submissions Table
        echo '<div class="leadite-card leadite-recent-card">';
        echo '<div class="leadite-card-header leadite-recent-header">';
        echo '<div>';
        echo '<h2>' . esc_html__( 'Recent Submissions', 'leadite' ) . '</h2>';
        echo '<p class="leadite-card-subtitle">' . esc_html__( 'Latest forms received by your site', 'leadite' ) . '</p>';
        echo '</div>';
        echo '<a href="' . esc_url( add_query_arg( array(
            'page' => 'leadite-leads',
        ), admin_url( 'admin.php' ) ) ) . '" class="button button-secondary">' . esc_html__( 'View All Leads', 'leadite' ) . ' ' . esc_html( $arrow_next ) . '</a>';
        echo '</div>';
        $this->render_recent_submissions_table( $recent_leads, $recent_meta_map );
        echo '</div>';
        // leadite-recent-card
        $this->branding_footer();
        echo '</div>';
        // wrap
    }

    /**
     * Whether the current admin screen belongs to Leadite.
     *
     * @return bool
     */
    protected function is_leadite_screen() {
        $screen = ( function_exists( 'get_current_screen' ) ? get_current_screen() : null );
        return $screen && false !== strpos( (string) $screen->id, 'leadite' );
    }

    /**
     * Branded footer: build-aware license/upgrade link, quick-guide popup, support.
     *
     * @param string $text Default footer text.
     * @return string
     */
    public function admin_footer_brand( $text ) {
        if ( !$this->is_leadite_screen() ) {
            return $text;
        }
        $links = array();
        if ( \function_exists( 'leadite_fs' ) ) {
            $fs = \leadite_fs();
            if ( $fs->is_premium() && $fs->is_registered() ) {
                $links[] = '<a href="' . esc_url( $fs->get_account_url() ) . '">' . esc_html__( 'License & account', 'leadite' ) . '</a>';
            } else {
                $links[] = '<a href="' . esc_url( $fs->pricing_url() ) . '">' . esc_html__( 'Upgrade to Pro', 'leadite' ) . '</a>';
            }
        }
        $links[] = '<a href="#TB_inline?width=720&height=540&inlineId=leadite-docs-popup" class="thickbox" title="' . esc_attr__( 'Leadite — quick guide', 'leadite' ) . '">' . esc_html__( 'Quick guide', 'leadite' ) . '</a>';
        $links[] = '<a href="https://akava.site/leadite/" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Details & support', 'leadite' ) . '</a>';
        return '<span id="footer-thankyou">' . esc_html( Config::brand() ) . ' · ' . implode( ' · ', $links ) . '</span>';
    }

    /**
     * Hidden quick-guide popup content (opened via Thickbox from the footer).
     *
     * @return void
     */
    public function render_docs_popup() {
        if ( !$this->is_leadite_screen() ) {
            return;
        }
        ?>
		<div id="leadite-docs-popup" style="display:none;">
			<div class="leadite-docs">
				<h2><?php 
        esc_html_e( 'Getting started', 'leadite' );
        ?></h2>
				<p><?php 
        esc_html_e( 'Leadite automatically captures submissions from Contact Form 7, Elementor Forms, WPForms, Ninja Forms, Gravity Forms and Fluent Forms — no setup needed. Every submission lands in the unified inbox under Leadite → All Leads.', 'leadite' );
        ?></p>

				<h2><?php 
        esc_html_e( 'Working with leads', 'leadite' );
        ?></h2>
				<p><?php 
        esc_html_e( 'Statuses: New → Read → In Progress → Closed (or Spam). Use bulk actions to change status, export or delete. Attachments are stored in a protected folder and downloaded through gated links.', 'leadite' );
        ?></p>

				<h2><?php 
        esc_html_e( 'Exports', 'leadite' );
        ?></h2>
				<p><?php 
        esc_html_e( 'Export filtered leads to CSV, XLSX, JSON or XML from the All Leads screen. Date and status filters apply to the export.', 'leadite' );
        ?></p>

				<h2><?php 
        esc_html_e( 'Privacy & data retention', 'leadite' );
        ?></h2>
				<p><?php 
        esc_html_e( 'Leadite integrates with the WordPress privacy tools (Tools → Export / Erase Personal Data) and can auto-delete old leads on a schedule: Settings → General → Data Retention.', 'leadite' );
        ?></p>

				<h2><?php 
        esc_html_e( 'Pro & license', 'leadite' );
        ?></h2>
				<p><?php 
        esc_html_e( 'Pro adds tags, assignments, notes & timeline, Smart Views, Follow-Up Guard, Telegram / webhook / Google Sheets integrations and WP-CLI commands.', 'leadite' );
        ?></p>
				<p><?php 
        esc_html_e( 'Already purchased? Install the Pro version from your purchase e-mail, then activate the key via Plugins → Leadite (Pro) → Activate License. If the e-mails differ, confirm the activation from the message Freemius sends you.', 'leadite' );
        ?></p>

				<p class="leadite-docs-more"><a href="https://akava.site/leadite/" target="_blank" rel="noopener noreferrer"><?php 
        esc_html_e( 'Full documentation & support →', 'leadite' );
        ?></a></p>
			</div>
		</div>
		<?php 
    }

    /**
     * Light brand skin for the Freemius license-activation modal on plugins.php.
     *
     * @param string $hook Current admin page hook.
     * @return void
     */
    public function enqueue_freemius_skin( $hook ) {
        if ( 'plugins.php' !== $hook ) {
            return;
        }
        wp_register_style(
            'leadite-fs-skin',
            false,
            array(),
            leadite_VERSION
        );
        wp_enqueue_style( 'leadite-fs-skin' );
        wp_add_inline_style( 'leadite-fs-skin', '.fs-modal-license-activation .fs-modal-dialog{border-radius:12px;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}' . '.fs-modal-license-activation .fs-modal-header{background:#4f46e5}' . '.fs-modal-license-activation .fs-modal-header h4{color:#fff}' . '.fs-modal-license-activation .fs-modal-footer .button-primary{background:#4f46e5;border-color:#4f46e5;border-radius:8px}' . '.fs-modal-license-activation .fs-modal-footer .button-primary:hover{filter:brightness(1.08)}' );
    }

    /**
     * Allowlist for inline SVG icons (internal, hardcoded markup).
     *
     * @return array
     */
    protected static function allowed_svg() {
        $dims = array(
            'class'           => array(),
            'width'           => array(),
            'height'          => array(),
            'viewbox'         => array(),
            'fill'            => array(),
            'stroke'          => array(),
            'stroke-width'    => array(),
            'stroke-linecap'  => array(),
            'stroke-linejoin' => array(),
            'aria-hidden'     => array(),
            'xmlns'           => array(),
            'style'           => array(),
        );
        $shape = array(
            'd'      => array(),
            'points' => array(),
            'cx'     => array(),
            'cy'     => array(),
            'r'      => array(),
            'x'      => array(),
            'y'      => array(),
            'x1'     => array(),
            'y1'     => array(),
            'x2'     => array(),
            'y2'     => array(),
            'width'  => array(),
            'height' => array(),
            'rx'     => array(),
            'ry'     => array(),
            'fill'   => array(),
            'stroke' => array(),
        );
        return array(
            'svg'      => $dims,
            'path'     => $shape,
            'circle'   => $shape,
            'rect'     => $shape,
            'line'     => $shape,
            'polyline' => $shape,
            'polygon'  => $shape,
            'g'        => $dims,
            'span'     => array(
                'class' => array(),
            ),
        );
    }

    protected function render_kpi_card(
        $title,
        $value,
        $label,
        $icon,
        $type
    ) {
        $display_val = ( is_int( $value ) || is_float( $value ) ? number_format_i18n( $value ) : $value );
        printf(
            '<div class="leadite-kpi-card leadite-kpi-card--%s">
				<div class="leadite-kpi-header">
					<span class="leadite-kpi-title">%s</span>
					<div class="leadite-kpi-icon-wrapper">%s</div>
				</div>
				<div class="leadite-kpi-value">%s</div>
				<span class="leadite-kpi-label">%s</span>
			</div>',
            esc_attr( $type ),
            esc_html( $title ),
            wp_kses( $icon, self::allowed_svg() ),
            esc_html( $display_val ),
            esc_html( $label )
        );
    }

    protected function render_leads_time_chart( $leads_by_day ) {
        $counts = array_values( $leads_by_day );
        $dates = array_keys( $leads_by_day );
        $total_points = count( $counts );
        if ( $total_points === 0 ) {
            echo '<div class="leadite-chart-empty">' . esc_html__( 'No data available.', 'leadite' ) . '</div>';
            return;
        }
        $max_val = max( $counts );
        if ( $max_val <= 0 ) {
            $max_val = 10;
            // Default height scaling if zero leads
        }
        // Round up max_val to a nice round number if possible
        if ( $max_val > 10 ) {
            $max_val = ceil( $max_val / 5 ) * 5;
        }
        // Viewport dimensions
        $w = 800;
        $h = 250;
        $padding_left = 50;
        $padding_right = 20;
        $padding_top = 30;
        $padding_bottom = 40;
        $chart_w = $w - $padding_left - $padding_right;
        $chart_h = $h - $padding_top - $padding_bottom;
        $x_coords = array();
        $y_coords = array();
        for ($i = 0; $i < $total_points; $i++) {
            $x_coords[$i] = $padding_left + $i * ($chart_w / ($total_points - 1));
            $y_coords[$i] = $h - $padding_bottom - $counts[$i] / $max_val * $chart_h;
        }
        // Build Area Path
        $area_path = "M {$x_coords[0]} " . ($h - $padding_bottom);
        for ($i = 0; $i < $total_points; $i++) {
            $area_path .= " L {$x_coords[$i]} {$y_coords[$i]}";
        }
        $area_path .= " L {$x_coords[$total_points - 1]} " . ($h - $padding_bottom) . " Z";
        // Build Line Path
        $line_path = "M {$x_coords[0]} {$y_coords[0]}";
        for ($i = 1; $i < $total_points; $i++) {
            $line_path .= " L {$x_coords[$i]} {$y_coords[$i]}";
        }
        echo '<svg class="leadite-svg-chart" viewBox="0 0 ' . (int) $w . ' ' . (int) $h . '" width="100%" xmlns="http://www.w3.org/2000/svg">';
        // Gradients & Filters
        echo '<defs>';
        echo '  <linearGradient id="leadite-area-gradient" x1="0" y1="0" x2="0" y2="1">';
        echo '    <stop offset="0%" stop-color="#4f46e5" stop-opacity="0.25"/>';
        echo '    <stop offset="100%" stop-color="#4f46e5" stop-opacity="0.00"/>';
        echo '  </linearGradient>';
        echo '</defs>';
        // Grid Lines & Y-axis Labels
        // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- all variables are numeric (int/float) computed from DB counts, never user input
        $grid_levels = 4;
        for ($i = 0; $i < $grid_levels; $i++) {
            $y_val = $max_val * ($i / ($grid_levels - 1));
            $y_pos = $h - $padding_bottom - $y_val / $max_val * $chart_h;
            // Grid Line
            printf(
                '<line x1="%d" y1="%d" x2="%d" y2="%d" class="leadite-chart-grid-line" stroke="#ebdcb9" stroke-width="1" stroke-dasharray="4 4" opacity="0.5" />',
                $padding_left,
                $y_pos,
                $w - $padding_right,
                $y_pos
            );
            // Y Label
            printf(
                '<text x="%d" y="%d" text-anchor="end" class="leadite-chart-axis-text" alignment-baseline="middle" fill="#5e6b7e" font-size="11">%s</text>',
                $padding_left - 10,
                $y_pos,
                esc_html( round( $y_val ) )
            );
        }
        // Fill Area
        printf( '<path d="%s" fill="url(#leadite-area-gradient)"></path>', esc_attr( $area_path ) );
        // Stroke Line
        printf( '<path d="%s" fill="none" stroke="#4f46e5" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="leadite-chart-stroke-line"></path>', esc_attr( $line_path ) );
        // X Axis Labels (every 6th item to avoid crowding)
        for ($i = 0; $i < $total_points; $i += 6) {
            if ( $i >= $total_points ) {
                break;
            }
            $ts = strtotime( $dates[$i] );
            $label = wp_date( 'j M', $ts );
            printf(
                '<text x="%d" y="%d" text-anchor="middle" class="leadite-chart-axis-text" fill="#5e6b7e" font-size="11">%s</text>',
                $x_coords[$i],
                $h - $padding_bottom + 20,
                esc_html( $label )
            );
        }
        // Also show the last label if it wasn't shown
        if ( ($total_points - 1) % 6 !== 0 ) {
            $ts = strtotime( $dates[$total_points - 1] );
            $label = wp_date( 'j M', $ts );
            printf(
                '<text x="%d" y="%d" text-anchor="middle" class="leadite-chart-axis-text" fill="#5e6b7e" font-size="11">%s</text>',
                $x_coords[$total_points - 1],
                $h - $padding_bottom + 20,
                esc_html( $label )
            );
        }
        // Dots & Tooltip Hotspots
        $dx = ( $total_points > 1 ? $chart_w / ($total_points - 1) : $chart_w );
        for ($i = 0; $i < $total_points; $i++) {
            $ts = strtotime( $dates[$i] );
            $label_long = wp_date( 'j F Y', $ts );
            // Invisible rectangle for vertical hover slice (covers full height of the chart)
            printf(
                '<rect x="%s" y="%d" width="%s" height="%d" fill="transparent" style="cursor:pointer;" class="leadite-chart-trigger" data-index="%d" data-date="%s" data-count="%d" data-x="%d" data-y="%d"></rect>',
                $x_coords[$i] - $dx / 2,
                $padding_top,
                $dx,
                $chart_h,
                $i,
                esc_attr( $label_long ),
                (int) $counts[$i],
                $x_coords[$i],
                $y_coords[$i]
            );
            // Highlight Dot (printed second so it is selected by trigger:hover + .leadite-chart-dot or .leadite-chart-dot.is-active)
            printf(
                '<circle cx="%d" cy="%d" r="4" fill="#ffffff" stroke="#4f46e5" stroke-width="2" class="leadite-chart-dot" id="leadite-dot-%d"></circle>',
                $x_coords[$i],
                $y_coords[$i],
                $i
            );
        }
        echo '</svg>';
        // The Tooltip DOM element itself (will be positioned absolute via JS)
        echo '<div class="leadite-chart-tooltip" style="opacity: 0; pointer-events: none; position: absolute;"></div>';
    }

    protected function render_status_donut_chart( $status_counts ) {
        $total = array_sum( $status_counts );
        $palette = array(
            'new'         => '#db2777',
            'read'        => '#475569',
            'in_progress' => '#d97706',
            'closed'      => '#64748b',
            'spam'        => '#2563eb',
        );
        $labels = array(
            'new'         => esc_html__( 'New', 'leadite' ),
            'read'        => esc_html__( 'Read', 'leadite' ),
            'in_progress' => esc_html__( 'In progress', 'leadite' ),
            'closed'      => esc_html__( 'Closed', 'leadite' ),
            'spam'        => esc_html__( 'Spam', 'leadite' ),
        );
        echo '<div class="leadite-donut-flex">';
        if ( $total === 0 ) {
            // Empty state Donut
            echo '  <div class="leadite-donut-svg-container">';
            echo '    <svg viewBox="0 0 180 180" class="leadite-donut-svg">';
            echo '      <circle cx="90" cy="90" r="60" fill="transparent" stroke="#e5e7eb" stroke-width="18"></circle>';
            echo '      <text x="90" y="94" text-anchor="middle" class="leadite-donut-center-value">0</text>';
            echo '      <text x="90" y="112" text-anchor="middle" class="leadite-donut-center-label">' . esc_html__( 'Leads', 'leadite' ) . '</text>';
            echo '    </svg>';
            echo '  </div>';
            echo '  <div class="leadite-donut-legend">';
            echo '    <p class="leadite-donut-empty-text">' . esc_html__( 'No status data.', 'leadite' ) . '</p>';
            echo '  </div>';
            echo '</div>';
            return;
        }
        $r = 60;
        $circumference = 377;
        echo '  <div class="leadite-donut-svg-container">';
        echo '    <svg viewBox="0 0 180 180" class="leadite-donut-svg">';
        $cumulative = 0;
        $i = 0;
        foreach ( $palette as $status => $color ) {
            $count = ( isset( $status_counts[$status] ) ? $status_counts[$status] : 0 );
            if ( $count <= 0 ) {
                continue;
            }
            $len = $count / $total * $circumference;
            $offset = -$cumulative;
            $cumulative += $len;
            $percentage = round( $count / $total * 100 );
            printf(
                '<circle cx="90" cy="90" r="60" fill="transparent" stroke="%s" stroke-width="18" 
					stroke-dasharray="%f %d" stroke-dashoffset="%f" 
					transform="rotate(-90 90 90)" class="leadite-donut-segment" 
					data-source="%s" data-count="%d" data-percent="%d" data-index="%d"></circle>',
                esc_attr( $color ),
                $len,
                $circumference,
                $offset,
                esc_attr( $status ),
                (int) $count,
                (int) $percentage,
                $i
            );
            $i++;
        }
        printf( '<text x="90" y="94" text-anchor="middle" class="leadite-donut-center-value">%s</text>', esc_html( $total ) );
        printf( '<text x="90" y="112" text-anchor="middle" class="leadite-donut-center-label">%s</text>', esc_html__( 'Leads', 'leadite' ) );
        echo '    </svg>';
        echo '  </div>';
        // Legend sidebar
        echo '  <div class="leadite-donut-legend">';
        $i = 0;
        foreach ( $palette as $status => $color ) {
            $count = ( isset( $status_counts[$status] ) ? $status_counts[$status] : 0 );
            if ( $count <= 0 ) {
                continue;
            }
            $pct = round( $count / $total * 100 );
            printf(
                '<div class="leadite-legend-item" data-index="%d">
					<span class="leadite-legend-bullet" style="background-color: %s;"></span>
					<span class="leadite-legend-name">%s</span>
					<span class="leadite-legend-stats"><strong>%d%%</strong> (%s)</span>
				</div>',
                $i,
                esc_attr( $color ),
                esc_html( $labels[$status] ),
                (int) $pct,
                esc_html( $count )
            );
            $i++;
        }
        echo '  </div>';
        // leadite-donut-legend
        echo '</div>';
        // leadite-donut-flex
    }

    protected function render_top_forms_list( $top_forms ) {
        if ( empty( $top_forms ) ) {
            echo '<p class="leadite-empty-text">' . esc_html__( 'No form submissions recorded yet.', 'leadite' ) . '</p>';
            return;
        }
        $max_c = 0;
        foreach ( $top_forms as $f ) {
            if ( $f->c > $max_c ) {
                $max_c = $f->c;
            }
        }
        foreach ( $top_forms as $f ) {
            $pct = ( $max_c > 0 ? $f->c / $max_c * 100 : 0 );
            printf(
                '<div class="leadite-form-row">
					<div class="leadite-form-row-header">
						<span class="leadite-form-row-name">%s</span>
						<span class="leadite-badge leadite-badge--source leadite-form-row-badge">%s</span>
					</div>
					<div class="leadite-form-row-bar-wrapper">
						<div class="leadite-form-row-bar-fill" style="width: %f%%;"></div>
						<span class="leadite-form-row-count">%d</span>
					</div>
				</div>',
                esc_html( $f->form_name ),
                esc_html( $f->source ),
                $pct,
                (int) $f->c
            );
        }
    }

    // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
    /**
     * Recent submissions table on the dashboard.
     *
     * @param object[] $recent_leads  Row objects from query().
     * @param array    $meta_map      Pre-fetched meta map from find_many(). If empty,
     *                                falls back to per-row find() for safety.
     */
    protected function render_recent_submissions_table( $recent_leads, array $meta_map = array() ) {
        if ( empty( $recent_leads ) ) {
            echo '<p class="leadite-empty-text">' . esc_html__( 'No submissions found.', 'leadite' ) . '</p>';
            return;
        }
        echo '<div class="leadite-table-responsive">';
        echo '<table class="wp-list-table widefat fixed striped table-view-list leadite-recent-table">';
        echo '  <thead>';
        echo '    <tr>';
        echo '      <th>' . esc_html__( 'Lead', 'leadite' ) . '</th>';
        echo '      <th>' . esc_html__( 'Source', 'leadite' ) . '</th>';
        echo '      <th>' . esc_html__( 'Form', 'leadite' ) . '</th>';
        echo '      <th>' . esc_html__( 'Status', 'leadite' ) . '</th>';
        echo '      <th>' . esc_html__( 'Date', 'leadite' ) . '</th>';
        echo '    </tr>';
        echo '  </thead>';
        echo '  <tbody>';
        $statuses = array(
            'new'         => esc_html__( 'New', 'leadite' ),
            'read'        => esc_html__( 'Read', 'leadite' ),
            'in_progress' => esc_html__( 'In progress', 'leadite' ),
            'closed'      => esc_html__( 'Closed', 'leadite' ),
            'spam'        => esc_html__( 'Spam', 'leadite' ),
        );
        foreach ( $recent_leads as $item ) {
            // Use pre-fetched meta map; fall back to live find() when not available.
            if ( isset( $meta_map[(int) $item->id] ) ) {
                $fields = $meta_map[(int) $item->id]['fields'];
            } else {
                $data = LeadsRepository::find( $item->id );
                $fields = $data['fields'];
            }
            $name = FieldMapper::canonical_value( $fields, 'full_name' );
            $email = FieldMapper::canonical_value( $fields, 'email' );
            $phone = FieldMapper::canonical_value( $fields, 'phone' );
            $title = ( $name ? $name : (( $email ? $email : esc_html__( '(no name)', 'leadite' ) )) );
            $view_url = add_query_arg( array(
                'page' => 'leadite-leads',
                'view' => (int) $item->id,
            ), admin_url( 'admin.php' ) );
            $lead_lines = array();
            $lead_lines[] = '<strong><a class="row-title" href="' . esc_url( $view_url ) . '">' . esc_html( $title ) . '</a></strong>';
            if ( $email ) {
                $lead_lines[] = '<span class="leadite-list__sub">' . esc_html( $email ) . '</span>';
            }
            if ( $phone ) {
                $lead_lines[] = '<span class="leadite-list__sub">' . esc_html( $phone ) . '</span>';
            }
            if ( !empty( $item->page_url ) ) {
                $page_label = LeadsListTable::get_page_label( $item->page_url );
                $lead_lines[] = '<span class="leadite-list__sub">' . esc_html__( 'Page:', 'leadite' ) . ' <a href="' . esc_url( $item->page_url ) . '" target="_blank">' . esc_html( $page_label ) . '</a></span>';
            }
            $lead_cell = implode( '<br>', $lead_lines );
            $status_label = ( isset( $statuses[$item->status] ) ? $statuses[$item->status] : $item->status );
            $status_badge = '<span class="leadite-badge leadite-badge--status leadite-badge--' . esc_attr( $item->status ) . '">' . esc_html( $status_label ) . '</span>';
            // current_time('mysql') stores local site time; strtotime() parses it
            // as UTC which causes a double timezone shift with wp_date().
            // get_gmt_from_date() converts local→GMT so strtotime gives correct Unix ts.
            $ts = strtotime( get_gmt_from_date( $item->created_at ) );
            $date_formatted = wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $ts );
            printf(
                '<tr>
					<td class="column-primary">
						%s
						<div class="row-actions">
							<span class="view"><a href="%s">%s</a></span>
						</div>
					</td>
					<td><span class="leadite-badge leadite-badge--source">%s</span></td>
					<td>%s</td>
					<td>%s</td>
					<td>%s</td>
				</tr>',
                wp_kses( $lead_cell, array(
                    'a'      => array(
                        'href'   => array(),
                        'class'  => array(),
                        'target' => array(),
                    ),
                    'strong' => array(),
                    'span'   => array(
                        'class' => array(),
                    ),
                    'br'     => array(),
                ) ),
                esc_url( $view_url ),
                esc_html__( 'View Details', 'leadite' ),
                esc_html( $item->source ),
                esc_html( ( $item->form_name ? $item->form_name : '—' ) ),
                wp_kses( $status_badge, array(
                    'span' => array(
                        'class' => array(),
                    ),
                ) ),
                esc_html( $date_formatted )
            );
        }
        echo '  </tbody>';
        echo '</table>';
        echo '</div>';
        // leadite-table-responsive
    }

    /**
     * Route between the list view and the single-lead view.
     */
    public function render_router() {
        if ( !current_user_can( Config::capability() ) && !current_user_can( Config::view_capability() ) ) {
            wp_die( esc_html__( 'You do not have permission to view leads.', 'leadite' ) );
        }
        $view = ( isset( $_GET['view'] ) ? (int) $_GET['view'] : 0 );
        // phpcs:ignore WordPress.Security.NonceVerification
        if ( $view ) {
            $this->render_single( $view );
            return;
        }
        $this->render_list();
    }

    /**
     * Leads list page.
     */
    protected function render_list() {
        $table = new LeadsListTable();
        $table->prepare_items();
        $user_id = get_current_user_id();
        // view_mode preference: nonce + allowlist to prevent CSRF.
        if ( isset( $_GET['view_mode'], $_GET['_view_nonce'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_GET['_view_nonce'] ) ), 'leadite_view_mode' ) ) {
            $requested_mode = sanitize_key( wp_unslash( $_GET['view_mode'] ) );
            if ( in_array( $requested_mode, array('list', 'kanban'), true ) ) {
                update_user_meta( $user_id, 'leadite_view_mode', $requested_mode );
            }
        }
        $view_mode = get_user_meta( $user_id, 'leadite_view_mode', true );
        if ( !in_array( $view_mode, array('list', 'kanban'), true ) ) {
            $view_mode = 'list';
        }
        echo '<div class="wrap lh">';
        $view_nonce = wp_create_nonce( 'leadite_view_mode' );
        $list_url = add_query_arg( array(
            'view_mode'   => 'list',
            '_view_nonce' => $view_nonce,
        ) );
        $kanban_url = add_query_arg( array(
            'view_mode'   => 'kanban',
            '_view_nonce' => $view_nonce,
        ) );
        echo '<div class="leadite-title-row">';
        echo '<h1>' . esc_html( Config::brand() ) . ' — ' . esc_html__( 'Leads', 'leadite' ) . '</h1>';
        echo '<div class="leadite-view-toggle">';
        printf(
            '<a href="%s" class="button %s" id="leadite-toggle-list">%s</a>',
            esc_url( $list_url ),
            ( 'list' === $view_mode ? 'button-primary' : '' ),
            esc_html__( 'List View', 'leadite' )
        );
        printf(
            '<a href="%s" class="button %s" id="leadite-toggle-kanban">%s</a>',
            esc_url( $kanban_url ),
            ( 'kanban' === $view_mode ? 'button-primary' : '' ),
            esc_html__( 'Kanban View', 'leadite' )
        );
        echo '</div>';
        echo '</div>';
        // Status summary chips.
        $counts = LeadsRepository::status_counts_cached();
        $total = array_sum( $counts );
        // Export buttons with active query filters
        $export_args = array(
            'action'   => 'leadite_export',
            '_wpnonce' => wp_create_nonce( 'leadite_export_nonce' ),
            'source'   => ( isset( $_GET['source'] ) ? sanitize_text_field( wp_unslash( $_GET['source'] ) ) : '' ),
            'form'     => ( isset( $_GET['form'] ) ? sanitize_text_field( wp_unslash( $_GET['form'] ) ) : '' ),
            'status'   => ( isset( $_GET['status'] ) ? sanitize_text_field( wp_unslash( $_GET['status'] ) ) : '' ),
            'from'     => ( isset( $_GET['from'] ) ? sanitize_text_field( wp_unslash( $_GET['from'] ) ) : '' ),
            'to'       => ( isset( $_GET['to'] ) ? sanitize_text_field( wp_unslash( $_GET['to'] ) ) : '' ),
            's'        => ( isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '' ),
        );
        $csv_url = add_query_arg( array_merge( $export_args, array(
            'format' => 'csv',
        ) ), admin_url( 'admin-post.php' ) );
        $xlsx_url = add_query_arg( array_merge( $export_args, array(
            'format' => 'xlsx',
        ) ), admin_url( 'admin-post.php' ) );
        $json_url = add_query_arg( array_merge( $export_args, array(
            'format' => 'json',
        ) ), admin_url( 'admin-post.php' ) );
        $xml_url = add_query_arg( array_merge( $export_args, array(
            'format' => 'xml',
        ) ), admin_url( 'admin-post.php' ) );
        echo '<div class="leadite-summary">';
        printf( '<span class="leadite-chip">%s: <strong>%s</strong></span>', esc_html__( 'Total', 'leadite' ), esc_html( number_format_i18n( $total ) ) );
        printf( '<span class="leadite-chip leadite-chip--new">%s: <strong>%s</strong></span>', esc_html__( 'New', 'leadite' ), esc_html( number_format_i18n( ( isset( $counts['new'] ) ? $counts['new'] : 0 ) ) ) );
        printf(
            '<span class="leadite-chip leadite-chip--export">%s: <span class="leadite-export-links"><a href="%s" class="leadite-export-link">%s</a> <a href="%s" class="leadite-export-link">%s</a> <a href="%s" class="leadite-export-link">%s</a> <a href="%s" class="leadite-export-link">%s</a></span></span>',
            esc_html__( 'Export', 'leadite' ),
            esc_url( $csv_url ),
            'CSV',
            esc_url( $xlsx_url ),
            'XLSX',
            esc_url( $json_url ),
            'JSON',
            esc_url( $xml_url ),
            'XML'
        );
        echo '</div>';
        echo '<form method="get" id="leadite-filter-form">';
        echo '<input type="hidden" name="page" value="leadite-leads" />';
        echo '<input type="hidden" name="view_mode" value="' . esc_attr( $view_mode ) . '" />';
        if ( 'list' === $view_mode ) {
            $table->search_box( esc_html__( 'Search leads', 'leadite' ), 'Leadite-search' );
            $table->display();
        } else {
            $table->search_box( esc_html__( 'Search leads', 'leadite' ), 'Leadite-search' );
            $this->render_kanban_filters();
            $this->render_kanban_board();
        }
        echo '</form>';
        // Render AJAX details modal container
        echo '<div id="leadite-details-modal" class="leadite-modal" style="display:none;">';
        echo '  <div class="leadite-modal-overlay"></div>';
        echo '  <div class="leadite-modal-container">';
        echo '    <div class="leadite-modal-content">';
        echo '      <div class="leadite-modal-spinner"><div class="leadite-spinner"></div></div>';
        echo '    </div>';
        echo '  </div>';
        echo '</div>';
        $this->branding_footer();
        echo '</div>';
    }

    /**
     * Single lead detail page.
     *
     * @param int $id Submission ID.
     */
    protected function render_single( $id ) {
        $data = LeadsRepository::find( $id );
        $row = $data['row'];
        if ( !$row ) {
            echo '<div class="wrap lh"><h1>' . esc_html__( 'Lead not found', 'leadite' ) . '</h1></div>';
            return;
        }
        // Mark as read on first open (only if we did not just update the status).
        if ( 'new' === $row->status && !isset( $_GET['updated'] ) ) {
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only status check, no data modified
            LeadsRepository::set_status( $id, 'read' );
            $row->status = 'read';
        }
        $back = add_query_arg( array(
            'page' => 'leadite-leads',
        ), admin_url( 'admin.php' ) );
        $name = FieldMapper::canonical_value( $data['fields'], 'full_name' );
        echo '<div class="wrap leadite leadite-single">';
        $arrow_back = ( is_rtl() ? '&rarr;' : '&larr;' );
        echo '<a class="leadite-back" href="' . esc_url( $back ) . '">' . esc_html( $arrow_back ) . ' ' . esc_html__( 'Back to leads', 'leadite' ) . '</a>';
        echo '<h1>' . esc_html( ( $name ? $name : esc_html__( 'Lead', 'leadite' ) ) ) . ' <span class="leadite-id">#' . (int) $row->id . '</span></h1>';
        $this->render_single_content( $id );
        $this->branding_footer();
        echo '</div>';
    }

    /**
     * Output the inner content grid of a single lead.
     *
     * @param int $id Lead submission ID.
     */
    public function render_single_content( $id ) {
        $data = LeadsRepository::find( $id );
        $row = $data['row'];
        if ( !$row ) {
            return;
        }
        $email = FieldMapper::canonical_value( $data['fields'], 'email' );
        $phone = FieldMapper::canonical_value( $data['fields'], 'phone' );
        // Split real form fields from system meta (UTM/gclid).
        $form_fields = array();
        $system_meta = array();
        foreach ( $data['fields'] as $f ) {
            if ( 'system' === $f->field_type ) {
                $system_meta[] = $f;
            } else {
                $form_fields[] = $f;
            }
        }
        // Quick actions.
        echo '<div class="leadite-quick">';
        if ( $email ) {
            echo '<a class="button" href="mailto:' . esc_attr( $email ) . '">' . esc_html__( 'Reply by email', 'leadite' ) . '</a> ';
        }
        if ( $phone ) {
            $wa = preg_replace( '/[^0-9]/', '', $phone );
            echo '<a class="button" target="_blank" rel="noopener" href="https://wa.me/' . esc_attr( $wa ) . '">' . esc_html__( 'WhatsApp', 'leadite' ) . '</a> ';
        }
        echo '</div>';
        echo '<div class="leadite-grid">';
        // Left Column: Submitted Data + Attachments
        echo '<div class="leadite-grid-col-main">';
        // Fields card.
        echo '<div class="leadite-card"><h2>' . esc_html__( 'Submitted data', 'leadite' ) . '</h2><table class="leadite-kv">';
        foreach ( $form_fields as $f ) {
            // Skip internal/private fields (keys starting with _).
            if ( isset( $f->field_key ) && 0 === strpos( (string) $f->field_key, '_' ) ) {
                continue;
            }
            echo '<tr><th>' . esc_html( $f->field_label ) . '</th><td>' . nl2br( esc_html( $f->field_value ) ) . '</td></tr>';
        }
        echo '</table></div>';
        // Files card (if files exist).
        $files = LeadsRepository::find_files( $id );
        if ( !empty( $files ) ) {
            echo '<div class="leadite-card leadite-card--files"><h2>' . esc_html__( 'Attachments', 'leadite' ) . '</h2><table class="leadite-kv">';
            foreach ( $files as $file ) {
                $size = size_format( $file->size_bytes );
                $mime_state = (string) $file->mime;
                // may be 'pending', 'failed', or real mime
                // Show a status badge for rows not yet fully copied from the remote source.
                if ( 'pending' === $mime_state ) {
                    printf(
                        '<tr><th>%s</th><td>' . '<span class="leadite-file-status leadite-file-status--pending" title="%s"><svg class="leadite-icon leadite-icon--pending" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> %s</span> ' . '<span class="leadite-list__sub">%s</span></td></tr>',
                        esc_html( $file->field_key ),
                        esc_attr__( 'The file is being copied in the background. Refresh the page in a moment.', 'leadite' ),
                        esc_html( $file->file_name ),
                        esc_html__( 'Processing—…', 'leadite' )
                    );
                    continue;
                }
                if ( 'failed' === $mime_state ) {
                    printf(
                        '<tr><th>%s</th><td>' . '<span class="leadite-file-status leadite-file-status--failed" title="%s"><svg class="leadite-icon leadite-icon--warn" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> %s</span></td></tr>',
                        esc_html( $file->field_key ),
                        esc_attr__( 'The file could not be retrieved from the remote URL. The form plugin may have deleted it before Leadite could copy it.', 'leadite' ),
                        esc_html( $file->file_name )
                    );
                    continue;
                }
                if ( 'skipped' === $mime_state ) {
                    $reason = ( isset( $file->skip_reason ) ? (string) $file->skip_reason : '' );
                    printf(
                        '<tr><th>%s</th><td>' . '<span class="leadite-file-status leadite-file-status--skipped" title="%s"><svg class="leadite-icon leadite-icon--skipped" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg> %s</span>' . '<p class="description" style="font-size:11px;margin:4px 0 0;">%s</p>' . '</td></tr>',
                        esc_html( $file->field_key ),
                        esc_attr( $reason ),
                        esc_html( $file->file_name ),
                        ( $reason ? esc_html( $reason ) : esc_html__( 'Skipped by Leadite attachment policy.', 'leadite' ) )
                    );
                    continue;
                }
                // Build a gated, nonced download URL — never expose file_path directly.
                $url = wp_nonce_url( admin_url( 'admin-ajax.php?action=leadite_download_file&file_id=' . (int) $file->id ), 'leadite_download_' . (int) $file->id );
                $ext = strtolower( pathinfo( $file->file_name, PATHINFO_EXTENSION ) );
                $is_image = in_array( $ext, array(
                    'jpg',
                    'jpeg',
                    'png',
                    'gif',
                    'webp'
                ), true );
                printf( '<tr><th>%s</th><td>', esc_html( $file->field_key ) );
                if ( $is_image ) {
                    $preview_url = add_query_arg( 'disposition', 'inline', $url );
                    printf(
                        '<div class="leadite-file-preview"><a href="%s"><img src="%s" alt="%s" class="leadite-file-thumb" /></a></div>',
                        esc_url( $url ),
                        esc_url( $preview_url ),
                        esc_attr( $file->file_name )
                    );
                }
                printf(
                    '<a href="%s">%s</a> <span class="leadite-list__sub">(%s)</span></td></tr>',
                    esc_url( $url ),
                    esc_html( $file->file_name ),
                    esc_html( $size )
                );
            }
            echo '</table></div>';
        }
        // f-pro-end
        // f-pro-free-start
        if ( !Pro::is_active() ) {
            // Free: locked mockup of the timeline card.
            echo '<div class="leadite-card leadite-timeline-card">';
            echo '<h2>' . esc_html__( 'Timeline & Notes', 'leadite' ) . ' <span class="leadite-badge leadite-badge--pro">Pro</span></h2>';
            // Pro notice banner.
            echo '<div class="leadite-pro-tab-notice" style="margin-top:16px; margin-bottom:16px;">';
            echo '<span class="leadite-pro-tab-notice__icon"><svg class="leadite-pro-notice-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></span>';
            echo '<div class="leadite-pro-tab-notice__body">';
            echo '<strong>' . esc_html__( 'Leadite Pro required', 'leadite' ) . '</strong> — ';
            echo esc_html__( 'All settings below are available in Pro. You can explore the fields now so you know what to prepare. Saving without a Pro license has no effect.', 'leadite' );
            echo ' <a href="' . esc_url( \leadite_get_upgrade_url() ) . '" target="_blank" rel="noopener noreferrer"><strong>' . esc_html__( 'Get Pro →', 'leadite' ) . '</strong></a>';
            echo '</div>';
            echo '</div>';
            // Add Note form — locked.
            echo '<div class="leadite-note-form-wrapper leadite-field-locked" style="pointer-events:none; opacity:0.6;">';
            echo '<textarea class="leadite-note-textarea leadite-field-locked" placeholder="' . esc_attr__( 'Type an internal note/comment...', 'leadite' ) . '" disabled="disabled"></textarea>';
            echo '<button type="button" class="button button-primary leadite-field-locked" disabled="disabled">' . esc_html__( 'Add Note', 'leadite' ) . '</button>';
            echo '</div>';
            // Timeline feed — placeholder.
            echo '<div class="leadite-timeline" style="opacity:0.5; pointer-events:none;">';
            echo '<p class="leadite-timeline-empty">' . esc_html__( 'Timeline activity history will be displayed here in Pro.', 'leadite' ) . '</p>';
            echo '</div>';
            // leadite-timeline.
            echo '</div>';
            // leadite-timeline-card.
        }
        // f-pro-free-end
        echo '</div>';
        // leadite-grid-col-main
        // Right Column: Metadata
        echo '<div class="leadite-grid-col-sidebar">';
        // Single small Pro notice in Free mode — replaces all individual teaser cards.
        if ( !Pro::is_active() ) {
            $pro_url = admin_url( 'admin.php?page=leadite-settings&tab=pro' );
            printf(
                '<div class="leadite-pro-sidebar-notice">%s <a href="%s">%s</a></div>',
                '<svg class="leadite-icon leadite-icon--pro-star" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none" aria-hidden="true"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg> ' . esc_html__( 'Pro: starred leads, assignments, notes & timeline, tags.', 'leadite' ),
                esc_url( $pro_url ),
                esc_html__( 'Learn more →', 'leadite' )
            );
        }
        // Meta card.
        echo '<div class="leadite-card leadite-card--meta"><h2>' . esc_html__( 'Metadata', 'leadite' ) . '</h2><table class="leadite-kv">';
        $details = array(
            __( 'Source', 'leadite' )   => $row->source,
            __( 'Form', 'leadite' )     => $row->form_name,
            __( 'Status', 'leadite' )   => $row->status,
            __( 'Date', 'leadite' )     => $row->created_at,
            __( 'Read at', 'leadite' )  => ( $row->read_at ? $row->read_at : '' ),
            __( 'Language', 'leadite' ) => $row->lang,
            __( 'IP', 'leadite' )       => $row->ip,
            __( 'Page', 'leadite' )     => $row->page_url,
            __( 'Referer', 'leadite' )  => $row->referer,
        );
        foreach ( $details as $label => $value ) {
            if ( '' === (string) $value ) {
                continue;
            }
            echo '<tr><th>' . esc_html( $label ) . '</th><td>' . esc_html( $value ) . '</td></tr>';
        }
        // Attribution (UTM / gclid), captured as system meta.
        foreach ( $system_meta as $sm ) {
            echo '<tr><th>' . esc_html( $sm->field_label ) . '</th><td>' . esc_html( $sm->field_value ) . '</td></tr>';
        }
        echo '</table>';
        // Status changer.
        $nonce = wp_create_nonce( 'leadite_status_' . $row->id );
        $action = admin_url( 'admin-post.php' );
        echo '<form class="leadite-status-form" method="post" action="' . esc_url( $action ) . '">';
        echo '<input type="hidden" name="action" value="leadite_set_status" />';
        echo '<input type="hidden" name="id" value="' . (int) $row->id . '" />';
        echo '<input type="hidden" name="_wpnonce" value="' . esc_attr( $nonce ) . '" />';
        echo '<label>' . esc_html__( 'Set status', 'leadite' ) . ' ';
        echo '<select name="status" class="leadite-status-select-ajax">';
        foreach ( array(
            'new',
            'read',
            'in_progress',
            'closed',
            'spam'
        ) as $st ) {
            printf(
                '<option value="%s" %s>%s</option>',
                esc_attr( $st ),
                selected( $row->status, $st, false ),
                esc_html( $st )
            );
        }
        echo '</select></label> ';
        submit_button(
            esc_html__( 'Save', 'leadite' ),
            'primary',
            'submit',
            false
        );
        echo '</form>';
        echo '</div>';
        // meta card
        // Follow-Up Guard block (Pro only).
        $this->render_followup_guard_block( $row, $data['fields'] );
        // f-pro-end
        echo '</div>';
        // leadite-grid-col-sidebar
        echo '</div>';
        // grid
    }

    /**
     * AJAX: stream a lead attachment file to an authorised admin.
     *
     * Security checklist (in order of execution):
     *  1. Nonce verification.
     *  2. Capability check.
     *  3. DB row lookup.
     *  4. Resolve absolute path from relative (portable across server moves).
     *  5. realpath() path-traversal guard — must stay inside uploads/Leadite.
     *  6. is_file() + is_readable() — 404 if missing.
     *  7. MIME detection via wp_check_filetype().
     *  8. sanitize_file_name() on Content-Disposition header value.
     *  9. File body emitted via WP_Filesystem (size capped at upload time).
     * 10. Audit log entry (GDPR: who downloaded what and when).
     * 11. Mandatory exit — prevents WP from appending HTML to the binary stream.
     */
    public function ajax_download_file() {
        $file_id = ( isset( $_GET['file_id'] ) ? (int) $_GET['file_id'] : 0 );
        // 1. Nonce.
        check_ajax_referer( 'leadite_download_' . $file_id );
        // 2. Capability.
        if ( !current_user_can( Config::capability() ) ) {
            wp_die( '', '', array(
                'response' => 403,
            ) );
        }
        // 3. DB row.
        $file = LeadsRepository::find_file( $file_id );
        if ( !$file ) {
            wp_die( '', '', array(
                'response' => 404,
            ) );
        }
        // 3b. Guard against files that are still being fetched in the background
        //     or that permanently failed.  Return a descriptive status rather than
        //     letting the realpath() guard silently 403.
        $mime_state = (string) $file->mime;
        if ( 'pending' === $mime_state ) {
            wp_send_json( array(
                'error'   => 'pending',
                'message' => esc_html__( 'This file is still being processed. Please try again in a moment.', 'leadite' ),
            ), 202 );
        }
        if ( 'failed' === $mime_state ) {
            wp_send_json( array(
                'error'   => 'failed',
                'message' => esc_html__( 'This file could not be retrieved from the remote source.', 'leadite' ),
            ), 410 );
        }
        // 4. Resolve absolute path from the stored relative path.
        $upload_dir = wp_upload_dir();
        $abs_path = $upload_dir['basedir'] . '/' . ltrim( $file->file_path, '/' );
        // 5. realpath() path-traversal guard.
        $real_file = realpath( $abs_path );
        $real_root = realpath( $upload_dir['basedir'] . '/Leadite' );
        if ( false === $real_file || false === $real_root || 0 !== strpos( $real_file, $real_root . DIRECTORY_SEPARATOR ) ) {
            wp_die( '', '', array(
                'response' => 403,
            ) );
        }
        // 6. Existence + readability.
        if ( !is_file( $real_file ) || !is_readable( $real_file ) ) {
            wp_die( '', '', array(
                'response' => 404,
            ) );
        }
        // 7. MIME.
        $file_type = wp_check_filetype( $real_file );
        $content_type = ( $file_type['type'] && '' !== $file_type['type'] ? $file_type['type'] : 'application/octet-stream' );
        // 8. Safe filename for Content-Disposition.
        $safe_name = sanitize_file_name( $file->file_name );
        // 9. Stream headers.
        $file_size = (int) filesize( $real_file );
        $disposition_param = ( isset( $_GET['disposition'] ) ? sanitize_key( $_GET['disposition'] ) : 'attachment' );
        // phpcs:ignore WordPress.Security.NonceVerification
        $is_image_mime = 0 === strpos( $content_type, 'image/' );
        $use_inline = 'inline' === $disposition_param && $is_image_mime;
        if ( ob_get_level() ) {
            ob_end_clean();
        }
        header( 'Content-Type: ' . $content_type );
        header( 'Content-Disposition: ' . (( $use_inline ? 'inline' : 'attachment' )) . '; filename="' . $safe_name . '"' );
        header( 'Content-Length: ' . $file_size );
        header( 'X-Content-Type-Options: nosniff' );
        header( 'Cache-Control: private, no-store' );
        header( 'Pragma: no-cache' );
        // Stream the validated file straight to the output buffer.
        // copy() to php://output avoids echoing raw binary data; the
        // attachment size is capped at upload time (att_max_size_kb).
        copy( $real_file, 'php://output' );
        // phpcs:ignore WordPress.PerformanceOptimizations -- direct stream copy of a validated file
        exit;
    }

    /**
     * AJAX endpoint to retrieve lead detail content.
     */
    public function ajax_get_lead_details() {
        check_ajax_referer( 'leadite_details_nonce', 'security' );
        if ( !current_user_can( Config::capability() ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Forbidden', 'leadite' ),
            ) );
        }
        $id = ( isset( $_POST['lead_id'] ) ? (int) $_POST['lead_id'] : 0 );
        if ( !$id ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Invalid lead ID', 'leadite' ),
            ) );
        }
        $data = LeadsRepository::find( $id );
        $row = $data['row'];
        if ( !$row ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Lead not found', 'leadite' ),
            ) );
        }
        // Mark as read on opening via AJAX.
        if ( 'new' === $row->status ) {
            LeadsRepository::set_status( $id, 'read' );
            $row->status = 'read';
        }
        $name = FieldMapper::canonical_value( $data['fields'], 'full_name' );
        $title = ( $name ? $name : esc_html__( 'Lead', 'leadite' ) );
        ob_start();
        ?>
		<div class="leadite-modal-header">
			<h2><?php 
        echo esc_html( $title );
        ?> <span class="leadite-id">#<?php 
        echo (int) $row->id;
        ?></span></h2>
			<button type="button" class="leadite-modal-close" aria-label="<?php 
        esc_attr_e( 'Close', 'leadite' );
        ?>">&times;</button>
		</div>
		<div class="leadite-modal-body">
			<?php 
        $this->render_single_content( $id );
        ?>
		</div>
		<?php 
        $html = ob_get_clean();
        wp_send_json_success( array(
            'html' => $html,
        ) );
    }

    /**
     * Settings page (white-label + sources). Minimal for now.
     */
    public function render_settings() {
        if ( !current_user_can( Config::capability() ) ) {
            wp_die( esc_html__( 'No permission.', 'leadite' ) );
        }
        $active_tab = ( isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general' );
        // phpcs:ignore WordPress.Security.NonceVerification
        if ( isset( $_GET['migrated_count'] ) ) {
            // phpcs:ignore WordPress.Security.NonceVerification
            $migrated = (int) $_GET['migrated_count'];
            // phpcs:ignore WordPress.Security.NonceVerification
            printf( 
                '<div class="notice notice-success is-dismissible"><p>%s</p></div>',
                /* translators: %d: number of migrated submissions */
                esc_html( sprintf( _n(
                    'Successfully migrated %d submission.',
                    'Successfully migrated %d submissions.',
                    $migrated,
                    'leadite'
                ), $migrated ) )
             );
        }
        if ( isset( $_GET['cleared'] ) ) {
            // phpcs:ignore WordPress.Security.NonceVerification
            printf( '<div class="notice notice-success is-dismissible"><p>%s</p></div>', esc_html__( 'All leads and attachments metadata have been successfully cleared.', 'leadite' ) );
        }
        // PRG: POST is handled in handle_save_settings() on admin_init; we only
        // display the result notice here based on the redirect query arg.
        if ( isset( $_GET['settings_saved'] ) ) {
            // phpcs:ignore WordPress.Security.NonceVerification -- read-only display of save result notice
            echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Settings saved.', 'leadite' ) . '</p></div>';
        }
        // URL-validation error notice (carried via transient to survive the redirect).
        $url_error = get_transient( 'leadite_settings_url_error_' . get_current_user_id() );
        if ( $url_error ) {
            delete_transient( 'leadite_settings_url_error_' . get_current_user_id() );
            printf( '<div class="notice notice-error is-dismissible"><p>%s</p></div>', wp_kses_post( $url_error ) );
        }
        $cfg = Config::all();
        echo '<div class="wrap lh">';
        echo '<h1>' . esc_html__( 'Leadite Settings', 'leadite' ) . '</h1>';
        if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
            printf( '<div class="notice notice-warning inline" style="margin: 0 0 20px;"><p><strong>%s:</strong> %s</p></div>', esc_html__( 'WP-Cron is disabled', 'leadite' ), esc_html__( 'Background deliveries (Telegram/Webhook) and remote attachment downloads depend on WP-Cron. Since DISABLE_WP_CRON is enabled, please ensure that a system cron job triggers wp-cron.php regularly to prevent delivery delays.', 'leadite' ) );
        }
        // Tabs navigation.
        echo '<h2 class="nav-tab-wrapper leadite-settings-tabs">';
        printf(
            '<a href="%s" class="nav-tab %s">%s</a>',
            esc_url( admin_url( 'admin.php?page=leadite-settings&tab=general' ) ),
            ( 'general' === $active_tab ? 'nav-tab-active' : '' ),
            esc_html__( 'General', 'leadite' )
        );
        printf(
            '<a href="%s" class="nav-tab %s">%s</a>',
            esc_url( admin_url( 'admin.php?page=leadite-settings&tab=attachments' ) ),
            ( 'attachments' === $active_tab ? 'nav-tab-active' : '' ),
            esc_html__( 'Attachments', 'leadite' )
        );
        printf(
            '<a href="%s" class="nav-tab %s">%s <span class="leadite-tab-pro-badge">Pro</span></a>',
            esc_url( admin_url( 'admin.php?page=leadite-settings&tab=followup' ) ),
            ( 'followup' === $active_tab ? 'nav-tab-active' : '' ),
            esc_html__( 'Follow-Up Guard', 'leadite' )
        );
        printf(
            '<a href="%s" class="nav-tab %s">%s <span class="leadite-tab-pro-badge">Pro</span></a>',
            esc_url( admin_url( 'admin.php?page=leadite-settings&tab=field_mapper' ) ),
            ( 'field_mapper' === $active_tab ? 'nav-tab-active' : '' ),
            esc_html__( 'Field Mapper', 'leadite' )
        );
        printf(
            '<a href="%s" class="nav-tab %s">%s <span class="leadite-tab-pro-badge">Pro</span></a>',
            esc_url( admin_url( 'admin.php?page=leadite-settings&tab=pro' ) ),
            ( 'pro' === $active_tab ? 'nav-tab-active' : '' ),
            esc_html__( 'Pro Integrations', 'leadite' )
        );
        printf(
            '<a href="%s" class="nav-tab %s">%s</a>',
            esc_url( admin_url( 'admin.php?page=leadite-settings&tab=google_sheets' ) ),
            ( 'google_sheets' === $active_tab ? 'nav-tab-active' : '' ),
            esc_html__( 'Google Sheets', 'leadite' )
        );
        echo '</h2>';
        // The field_mapper tab renders its own standalone <form> (posts to admin-post.php).
        // The attachments tab uses the standard settings form.
        // Do NOT open the outer settings form for field_mapper — nested forms are invalid HTML.
        $uses_settings_form = 'field_mapper' !== $active_tab;
        if ( $uses_settings_form ) {
            echo '<form method="post">';
            wp_nonce_field( 'leadite_save_settings', 'leadite_settings_nonce' );
            // Pass the active tab to the PRG handler so the redirect returns to the same section.
            echo '<input type="hidden" name="tab" value="' . esc_attr( $active_tab ) . '" />';
        }
        if ( 'general' === $active_tab ) {
            $this->render_general_tab( $cfg );
        } elseif ( 'google_sheets' === $active_tab ) {
            $this->render_google_sheets_tab( $cfg );
        } elseif ( 'pro' === $active_tab ) {
            $this->render_pro_integrations_tab( $cfg );
        } elseif ( 'field_mapper' === $active_tab ) {
            $this->render_field_mapper_tab();
        } elseif ( 'attachments' === $active_tab ) {
            $this->render_attachments_tab( $cfg );
        } elseif ( 'followup' === $active_tab ) {
            $this->render_followup_settings_section( $cfg );
        }
        if ( $uses_settings_form ) {
            if ( 'pro' === $active_tab && Pro::is_active() ) {
                submit_button( esc_html__( 'Save settings', 'leadite' ) );
            } elseif ( in_array( $active_tab, array('general', 'attachments'), true ) ) {
                submit_button( esc_html__( 'Save settings', 'leadite' ) );
            } elseif ( 'google_sheets' === $active_tab ) {
            } elseif ( 'followup' === $active_tab ) {
            }
            echo '</form>';
        }
        // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
        // Data Migration section
        $detected_sources = \Leadite\Support\SubmissionImporter::detect_sources();
        if ( !empty( $detected_sources ) ) {
            echo '<div class="leadite-migration-section" style="margin-top:40px; padding-top:20px; border-top:1px solid var(--leadite-border-light);">';
            echo '<h2>' . esc_html__( 'Data Migration', 'leadite' ) . '</h2>';
            echo '<p class="description">' . esc_html__( 'We detected historical lead submissions from other storage plugins on your site. You can import them directly into Leadite in safe batches of 500 leads per click.', 'leadite' ) . '</p>';
            echo '<div class="leadite-migration-grid">';
            foreach ( $detected_sources as $source_key => $info ) {
                $mig_nonce = wp_create_nonce( 'leadite_migrate_' . $source_key );
                echo '<div class="leadite-migration-card">';
                echo '  <div class="leadite-migration-card-info">';
                echo '    <h3>' . esc_html( $info['label'] ) . '</h3>';
                if ( $info['count'] === 0 ) {
                    echo '    <p>' . esc_html__( 'All submissions have been successfully imported.', 'leadite' ) . '</p>';
                } else {
                    /* translators: %s: number of pending submissions (wrapped in <strong>) */
                    $mig_text = sprintf( _n(
                        'Found %s pending submission.',
                        'Found %s pending submissions.',
                        $info['count'],
                        'leadite'
                    ), '<strong>' . number_format_i18n( $info['count'] ) . '</strong>' );
                    printf( '    <p>%s</p>', wp_kses( $mig_text, array(
                        'strong' => array(),
                    ) ) );
                }
                echo '  </div>';
                echo '  <form method="post" style="margin:0;">';
                echo '    <input type="hidden" name="leadite_migrate_nonce" value="' . esc_attr( $mig_nonce ) . '" />';
                echo '    <input type="hidden" name="leadite_action" value="migrate_source" />';
                echo '    <input type="hidden" name="migrate_source" value="' . esc_attr( $source_key ) . '" />';
                $disabled = ( $info['count'] === 0 ? array(
                    'disabled' => 'disabled',
                ) : array() );
                submit_button(
                    esc_html__( 'Import Leads', 'leadite' ),
                    'secondary',
                    'submit_migration',
                    false,
                    $disabled
                );
                echo '  </form>';
                echo '</div>';
                // leadite-migration-card
            }
            echo '</div>';
            // leadite-migration-grid
            echo '</div>';
            // leadite-migration-section
        }
        // Danger Zone: Clear Leads Section
        echo '<div class="leadite-danger-zone">';
        echo '<h2 class="leadite-danger-zone__title">' . esc_html__( 'Danger Zone', 'leadite' ) . '</h2>';
        echo '<p>' . esc_html__( 'Permanently delete all leads, metadata, and logs from the database. This action is irreversible.', 'leadite' ) . '</p>';
        $clear_nonce = wp_create_nonce( 'leadite_clear_all_leads' );
        // The confirm() message is passed via wp_localize_script so it is translatable.
        echo '<form method="post" class="leadite-danger-zone__form">';
        echo '<input type="hidden" name="leadite_clear_nonce" value="' . esc_attr( $clear_nonce ) . '" />';
        echo '<input type="hidden" name="leadite_action" value="clear_all_leads" />';
        submit_button(
            esc_html__( 'Delete All Leads', 'leadite' ),
            'delete',
            'submit_clear',
            false
        );
        echo '</form>';
        echo '</div>';
        $this->branding_footer();
        echo '</div>';
    }

    /**
     * Build a slug => [label, available] map of known adapters for the settings UI.
     *
     * @return array
     */
    protected function registry_choices() {
        $out = array();
        $adapters = $this->registry->adapters();
        // Also include known-but-disabled adapters so they can be toggled on.
        $known = array(
            'cf7'          => new \Leadite\Capture\Cf7Adapter(),
            'elementor'    => new \Leadite\Capture\ElementorAdapter(),
            'wpforms'      => new \Leadite\Capture\WpformsAdapter(),
            'ninjaforms'   => new \Leadite\Capture\NinjaFormsAdapter(),
            'gravityforms' => new \Leadite\Capture\GravityFormsAdapter(),
            'fluentforms'  => new \Leadite\Capture\FluentFormsAdapter(),
        );
        foreach ( $known as $slug => $adapter ) {
            $out[$slug] = array($adapter->label(), $adapter->is_available());
        }
        return $out;
    }

    /**
     * Handle admin-post actions (status change, bulk) before any output.
     */
    public function handle_actions() {
        // Unified migration action.
        if ( isset( $_POST['leadite_action'] ) && 'migrate_source' === $_POST['leadite_action'] ) {
            $this->handle_source_migration();
        }
        // Clear all leads.
        if ( isset( $_POST['leadite_action'] ) && 'clear_all_leads' === $_POST['leadite_action'] ) {
            $this->handle_clear_all_leads();
        }
        // Single status change.
        if ( isset( $_POST['action'] ) && 'leadite_set_status' === $_POST['action'] ) {
            $this->handle_status_change();
        }
        // Bulk actions from the list table.
        $this->handle_bulk();
    }

    /**
     * PRG handler for the Settings form.
     *
     * Runs on admin_init (before any output) so a redirect + exit is safe.
     * After saving, redirects back to the same settings tab with ?settings_saved=1
     * so render_settings() can display the success notice without a double-submit
     * on F5 / browser back.
     */
    public function handle_save_settings() {
        if ( !isset( $_POST['leadite_settings_nonce'] ) ) {
            return;
        }
        if ( !wp_verify_nonce( sanitize_key( wp_unslash( $_POST['leadite_settings_nonce'] ) ), 'leadite_save_settings' ) ) {
            return;
        }
        if ( !current_user_can( Config::capability() ) ) {
            return;
        }
        $active_tab = ( isset( $_POST['tab'] ) ? sanitize_key( $_POST['tab'] ) : 'general' );
        $current = Config::all();
        $url_error = '';
        if ( 'general' === $active_tab ) {
            $current['per_page'] = ( isset( $_POST['per_page'] ) ? max( 1, (int) $_POST['per_page'] ) : 25 );
            $current['enabled_sources'] = ( isset( $_POST['enabled_sources'] ) ? array_map( 'sanitize_text_field', (array) wp_unslash( $_POST['enabled_sources'] ) ) : array() );
            $current['delete_on_uninstall'] = !empty( $_POST['delete_on_uninstall'] );
            $current['anonymize_ip'] = !empty( $_POST['anonymize_ip'] );
            $current['autopurge_enabled'] = !empty( $_POST['autopurge_enabled'] );
            $current['autopurge_days'] = ( isset( $_POST['autopurge_days'] ) ? max( 7, min( 3650, (int) $_POST['autopurge_days'] ) ) : 365 );
            $current['autopurge_only_closed'] = !empty( $_POST['autopurge_only_closed'] );
            // Role Access: sanitize and apply.
            $raw_role_access = ( isset( $_POST['role_access'] ) && is_array( $_POST['role_access'] ) ? map_deep( wp_unslash( $_POST['role_access'] ), 'sanitize_text_field' ) : array() );
            $clean_role_access = array();
            $allowed_levels = array('manage', 'view', '');
            foreach ( $raw_role_access as $role_slug => $level ) {
                $role_slug = sanitize_key( $role_slug );
                $level = ( in_array( $level, $allowed_levels, true ) ? $level : '' );
                if ( $role_slug && 'administrator' !== $role_slug ) {
                    $clean_role_access[$role_slug] = $level;
                }
            }
            $current['role_access'] = $clean_role_access;
            // Apply capabilities immediately so the change takes effect without reactivation.
            \Leadite\Activator::apply_role_access( $clean_role_access );
        } elseif ( 'google_sheets' === $active_tab ) {
        } elseif ( 'pro' === $active_tab ) {
        } elseif ( 'attachments' === $active_tab ) {
            $current['att_filtering_enabled'] = !empty( $_POST['att_filtering_enabled'] );
            $raw_max_kb = ( isset( $_POST['att_max_size_kb'] ) ? (int) $_POST['att_max_size_kb'] : 10240 );
            $current['att_max_size_kb'] = max( 1, $raw_max_kb );
            // Sanitize extension list: only lowercase alphanumeric and commas.
            $raw_exts = ( isset( $_POST['att_allowed_extensions'] ) ? sanitize_text_field( wp_unslash( $_POST['att_allowed_extensions'] ) ) : '' );
            $exts = array_filter( array_map( 'trim', explode( ',', strtolower( $raw_exts ) ) ) );
            $exts = array_filter( $exts, static function ( $e ) {
                return preg_match( '/^[a-z0-9]{1,10}$/', $e );
            } );
            $current['att_allowed_extensions'] = implode( ',', array_values( $exts ) );
        } elseif ( 'followup' === $active_tab ) {
        }
        update_option( Config::OPTION, $current );
        // Apply the (possibly changed) auto-purge schedule immediately.
        \Leadite\Support\AutoPurge::sync_schedule();
        if ( $url_error ) {
            set_transient( 'leadite_settings_url_error_' . get_current_user_id(), $url_error, 60 );
        }
        // PRG: redirect so F5 does not re-submit the form.
        wp_safe_redirect( add_query_arg( array(
            'page'           => 'leadite-settings',
            'tab'            => $active_tab,
            'settings_saved' => 1,
        ), admin_url( 'admin.php' ) ) );
        exit;
    }

    protected function handle_source_migration() {
        $source = ( isset( $_POST['migrate_source'] ) ? sanitize_key( $_POST['migrate_source'] ) : '' );
        if ( !$source ) {
            return;
        }
        if ( !isset( $_POST['leadite_migrate_nonce'] ) || !wp_verify_nonce( sanitize_key( wp_unslash( $_POST['leadite_migrate_nonce'] ) ), 'leadite_migrate_' . $source ) ) {
            return;
        }
        if ( !current_user_can( Config::capability() ) ) {
            wp_die( esc_html__( 'Cheatin&#8217; uh?', 'leadite' ) );
        }
        $migrated = \Leadite\Support\SubmissionImporter::migrate( $source, 500 );
        LeadsRepository::log( 0, 'migrate_source', sprintf( 'Migrated %d leads from %s', $migrated, $source ) );
        wp_safe_redirect( add_query_arg( array(
            'page'           => 'leadite-settings',
            'migrated_count' => $migrated,
        ), admin_url( 'admin.php' ) ) );
        exit;
    }

    protected function handle_status_change() {
        $id = ( isset( $_POST['id'] ) ? (int) $_POST['id'] : 0 );
        if ( !$id || !isset( $_POST['_wpnonce'] ) || !wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ), 'leadite_status_' . $id ) ) {
            return;
        }
        if ( !current_user_can( Config::capability() ) ) {
            wp_die( esc_html__( 'Cheatin&#8217; uh?', 'leadite' ) );
        }
        $status = ( isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : '' );
        LeadsRepository::set_status( $id, $status );
        LeadsRepository::log( $id, 'status', $status );
        wp_safe_redirect( add_query_arg( array(
            'page'    => 'leadite-leads',
            'view'    => $id,
            'updated' => 1,
        ), admin_url( 'admin.php' ) ) );
        exit;
    }

    /**
     * Render a "Pro feature teaser" block inside a card.
     *
     * Shown in Free mode so users can see exactly what they'll unlock and what
     * fields/inputs they will need to fill in after upgrading.
     *
     * @param string   $description Short summary sentence.
     * @param string[] $bullets     Feature bullet points.
     * @return void
     */
    protected function render_pro_feature_teaser( string $description, array $bullets ) {
        echo '<div class="leadite-pro-teaser-body">';
        echo '<p class="leadite-pro-teaser-desc">' . esc_html( $description ) . '</p>';
        if ( !empty( $bullets ) ) {
            echo '<ul class="leadite-pro-teaser-list">';
            foreach ( $bullets as $bullet ) {
                echo '<li>' . esc_html( $bullet ) . '</li>';
            }
            echo '</ul>';
        }
        echo '<p class="leadite-pro-teaser-cta">';
        printf( '<a href="%s" target="_blank" rel="noopener noreferrer" class="button button-primary leadite-pro-upgrade-btn">%s</a>', esc_url( \leadite_get_upgrade_url() ), esc_html__( '🔥 Upgrade to Pro', 'leadite' ) );
        echo '</p>';
        echo '</div>';
    }

    /**
     * Render the Field Mapper settings tab.
     *
     * In Pro: full interactive mapper UI (TODO: implement).
     * In Free: teaser with a realistic locked mockup of the mapper table.
     *
     * @return void
     */
    protected function render_field_mapper_tab() {
        // Free build: fully functional read-only inventory of detected fields.
        // (Manual per-form overrides are a Pro feature and ship in the Pro build only.)
        $rows = \Leadite\Storage\LeadsRepository::get_distinct_field_keys();
        echo '<div class="leadite-field-mapper-tab">';
        echo '<h2 style="margin:0 0 4px;font-size:20px;color:var(--leadite-text);">' . esc_html__( 'Field Mapper', 'leadite' ) . '</h2>';
        echo '<p style="color:var(--leadite-text-soft);font-size:13px;max-width:680px;margin:0 0 20px;">' . esc_html__( 'This is what Leadite detected in your forms: every field key it has seen, and the canonical role it was recognized as (e-mail, name, phone, company, subject, message). Recognition is automatic and works in any language.', 'leadite' ) . '</p>';
        if ( empty( $rows ) ) {
            echo '<div class="notice notice-info inline"><p>' . esc_html__( 'No fields yet — they will appear here after the first form submission is captured.', 'leadite' ) . '</p></div>';
        } else {
            $groups = array();
            foreach ( $rows as $r ) {
                $gk = $r->source . '||' . $r->form_id;
                if ( !isset( $groups[$gk] ) ) {
                    $groups[$gk] = array(
                        'source'    => $r->source,
                        'form_id'   => $r->form_id,
                        'form_name' => $r->form_name,
                        'rows'      => array(),
                    );
                }
                $groups[$gk]['rows'][] = $r;
            }
            $max_groups = 12;
            $max_fields = 30;
            $shown_groups = 0;
            foreach ( $groups as $g ) {
                if ( ++$shown_groups > $max_groups ) {
                    /* translators: %d: number of additional forms not shown */
                    echo '<p style="color:var(--leadite-text-soft);font-size:13px;">' . esc_html( sprintf( __( '…and %d more forms. The full list is available on the All Leads screen filters.', 'leadite' ), count( $groups ) - $max_groups ) ) . '</p>';
                    break;
                }
                echo '<div class="leadite-fm-group">';
                echo '<div class="leadite-fm-group-header">';
                echo '<span class="leadite-badge leadite-badge--source">' . esc_html( $g['source'] ) . '</span> ';
                echo '<strong>' . esc_html( ( $g['form_name'] ? $g['form_name'] : $g['form_id'] ) ) . '</strong> ';
                echo '<span class="leadite-fm-group-id">(ID: ' . esc_html( $g['form_id'] ) . ')</span>';
                echo '</div>';
                echo '<table class="leadite-fm-table widefat striped"><thead><tr>';
                echo '<th>' . esc_html__( 'Field key', 'leadite' ) . '</th>';
                echo '<th>' . esc_html__( 'Label', 'leadite' ) . '</th>';
                echo '<th>' . esc_html__( 'Detected as', 'leadite' ) . '</th>';
                echo '</tr></thead><tbody>';
                $shown_fields = 0;
                foreach ( $g['rows'] as $r ) {
                    if ( ++$shown_fields > $max_fields ) {
                        /* translators: %d: number of additional fields not shown */
                        echo '<tr><td colspan="3" style="color:var(--leadite-text-soft);">' . esc_html( sprintf( __( '…and %d more fields.', 'leadite' ), count( $g['rows'] ) - $max_fields ) ) . '</td></tr>';
                        break;
                    }
                    echo '<tr>';
                    echo '<td><code class="leadite-fm-key">' . esc_html( $r->field_key ) . '</code></td>';
                    echo '<td>' . esc_html( $r->field_label ) . '</td>';
                    echo '<td>';
                    if ( '' !== (string) $r->guessed_canonical ) {
                        echo '<span class="leadite-fm-auto-badge">' . esc_html( $r->guessed_canonical ) . '</span>';
                    } else {
                        echo '<span style="color:var(--leadite-text-soft);">&mdash;</span>';
                    }
                    echo '</td>';
                    echo '</tr>';
                }
                echo '</tbody></table>';
                echo '</div>';
            }
        }
        echo '<p style="margin-top:18px;color:var(--leadite-text-soft);font-size:13px;">' . esc_html__( 'Need to correct a detection? Leadite Pro adds manual overrides — change the role of any field per form, right on this screen.', 'leadite' ) . ' <a class="button button-primary" style="margin-inline-start:8px;" href="' . esc_url( \leadite_get_upgrade_url() ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Get Pro →', 'leadite' ) . '</a></p>';
        echo '</div>';
    }

    // =========================================================================
    // === DEBUG ONLY — all three methods below must be removed when real
    // licensing is implemented.  They are isolated here to make the removal
    // a single, contained operation.
    // =========================================================================
    /**
     * Render the "Pro locked" banner shown on the Pro tab when Pro is inactive.
     *
     * Replace this with a license-key input field when real licensing arrives.
     *
     * @return void
     */
    protected function render_pro_locked_banner() {
        ?>
		<div class="leadite-pro-locked-banner" style="
			background: linear-gradient(135deg, #f8f9ff 0%, #eef0fb 100%);
			border: 2px solid #c7d2fe;
			border-radius: 12px;
			padding: 32px 28px;
			margin: 8px 0 24px;
			max-width: 680px;
		">
			<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
				<span style="display:flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:10px;background:rgba(99,102,241,0.12);color:#4f46e5;flex-shrink:0;"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></span>
				<div>
					<h3 style="margin:0 0 4px;font-size:18px;color:#1e1b4b;">
						<?php 
        esc_html_e( 'Leadite Pro', 'leadite' );
        ?>
					</h3>
					<p style="margin:0;color:#6366f1;font-size:13px;font-weight:600;">
						<?php 
        esc_html_e( 'Upgrade to unlock all Pro features', 'leadite' );
        ?>
					</p>
				</div>
			</div>
			<ul style="margin:0 0 20px;padding-left:20px;color:#374151;line-height:1.8;font-size:14px;">
				<li><?php 
        esc_html_e( 'Star & favourite leads for quick access', 'leadite' );
        ?></li>
				<li><?php 
        esc_html_e( 'Assign leads to team members', 'leadite' );
        ?></li>
				<li><?php 
        esc_html_e( 'Internal notes & full activity timeline', 'leadite' );
        ?></li>
				<li><?php 
        esc_html_e( 'Telegram lead alerts', 'leadite' );
        ?></li>
				<li><?php 
        esc_html_e( 'Outbound webhooks (Make, Zapier, n8n—…)', 'leadite' );
        ?></li>
				<li><?php 
        esc_html_e( 'White-label: custom brand name & hidden footer', 'leadite' );
        ?></li>
			</ul>
			<a href="<?php 
        echo esc_url( \leadite_get_upgrade_url() );
        ?>" target="_blank" rel="noopener noreferrer"
			   class="button button-primary"
			   style="background:#4f46e5;border-color:#4f46e5;padding:8px 20px;height:auto;font-size:14px;">
				<?php 
        esc_html_e( 'Learn more &amp; get Pro →', 'leadite' );
        ?>
			</a>
		</div>
		<?php 
    }

    protected function handle_bulk() {
        if ( empty( $_REQUEST['page'] ) || 'leadite-leads' !== $_REQUEST['page'] ) {
            return;
        }
        $action = '';
        if ( isset( $_REQUEST['action'] ) && '-1' !== $_REQUEST['action'] ) {
            $action = sanitize_text_field( wp_unslash( $_REQUEST['action'] ) );
        } elseif ( isset( $_REQUEST['action2'] ) && '-1' !== $_REQUEST['action2'] ) {
            $action = sanitize_text_field( wp_unslash( $_REQUEST['action2'] ) );
        }
        if ( !$action || empty( $_REQUEST['ids'] ) ) {
            return;
        }
        check_admin_referer( 'bulk-leads' );
        if ( !current_user_can( Config::capability() ) ) {
            return;
        }
        $ids = array_map( 'intval', (array) $_REQUEST['ids'] );
        // ———— Bulk export of selected IDs ————————————————————————————————————————————————————————————————————————
        $export_formats = array(
            'export_csv'  => 'csv',
            'export_xlsx' => 'xlsx',
            'export_json' => 'json',
        );
        if ( isset( $export_formats[$action] ) ) {
            $format = $export_formats[$action];
            switch ( $format ) {
                case 'xlsx':
                    $exporter = new \Leadite\Export\XlsxExporter();
                    break;
                case 'json':
                    $exporter = new \Leadite\Export\JsonExporter();
                    break;
                case 'csv':
                default:
                    $exporter = new \Leadite\Export\CsvExporter();
                    break;
            }
            $filename = 'Leadite-export-' . wp_date( 'Y-m-d-His' ) . '.' . $exporter->format();
            header( 'Content-Type: ' . $exporter->mime_type() );
            header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
            header( 'Cache-Control: max-age=0' );
            $result = LeadsRepository::query( array(
                'ids'      => $ids,
                'per_page' => -1,
            ) );
            $leads = $result['items'];
            LeadsRepository::log( 0, 'export', sprintf( 'Exported %d selected leads as %s', count( $leads ), strtoupper( $format ) ) );
            $exporter->export( $leads );
            exit;
        }
        // ———— Standard bulk status / delete actions ————————————————————————————————————————————————————
        global $wpdb;
        $table = LeadsRepository::table();
        $mtable = LeadsRepository::meta_table();
        foreach ( $ids as $id ) {
            switch ( $action ) {
                case 'mark_read':
                    LeadsRepository::set_status( $id, 'read' );
                    break;
                case 'mark_closed':
                    LeadsRepository::set_status( $id, 'closed' );
                    break;
                case 'mark_spam':
                    LeadsRepository::set_status( $id, 'spam' );
                    break;
                case 'delete':
                    // Single canonical delete: cleans all 6 tables + physical files.
                    LeadsRepository::delete( $id );
                    break;
            }
        }
        wp_safe_redirect( add_query_arg( array(
            'page'      => 'leadite-leads',
            'bulk_done' => 1,
        ), admin_url( 'admin.php' ) ) );
        exit;
    }

    /**
     * Handle admin-post action for exporting leads.
     *
     * CSV exports are streamed in 500-row chunks so memory usage is bounded.
     * JSON, XML, and XLSX are fully buffered (format constraints) but still
     * benefit from the find_many() batch-meta optimisation in each exporter.
     */
    public function handle_export() {
        if ( !current_user_can( Config::capability() ) ) {
            wp_die( esc_html__( 'No permission.', 'leadite' ) );
        }
        if ( !isset( $_GET['_wpnonce'] ) || !wp_verify_nonce( sanitize_key( wp_unslash( $_GET['_wpnonce'] ) ), 'leadite_export_nonce' ) ) {
            wp_die( esc_html__( 'Security check failed.', 'leadite' ) );
        }
        $format = ( isset( $_GET['format'] ) ? sanitize_key( $_GET['format'] ) : 'csv' );
        // Shared filter args (no per_page yet — set per path below).
        $filter_args = array(
            'source'    => ( isset( $_GET['source'] ) ? sanitize_text_field( wp_unslash( $_GET['source'] ) ) : '' ),
            'form'      => ( isset( $_GET['form'] ) ? sanitize_text_field( wp_unslash( $_GET['form'] ) ) : '' ),
            'status'    => ( isset( $_GET['status'] ) ? sanitize_text_field( wp_unslash( $_GET['status'] ) ) : '' ),
            'date_from' => ( isset( $_GET['from'] ) ? sanitize_text_field( wp_unslash( $_GET['from'] ) ) : '' ),
            'date_to'   => ( isset( $_GET['to'] ) ? sanitize_text_field( wp_unslash( $_GET['to'] ) ) : '' ),
            'search'    => ( isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '' ),
            'orderby'   => 'created_at',
            'order'     => 'DESC',
        );
        switch ( $format ) {
            case 'xlsx':
                $exporter = new \Leadite\Export\XlsxExporter();
                break;
            case 'json':
                $exporter = new \Leadite\Export\JsonExporter();
                break;
            case 'xml':
                $exporter = new \Leadite\Export\XmlExporter();
                break;
            case 'csv':
            default:
                $exporter = new \Leadite\Export\CsvExporter();
                break;
        }
        $filename = 'Leadite-export-' . wp_date( 'Y-m-d-His' ) . '.' . $exporter->format();
        header( 'Content-Type: ' . $exporter->mime_type() );
        header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
        header( 'Cache-Control: max-age=0' );
        // Disable output buffering so chunks are sent to the browser as they arrive.
        header( 'X-Accel-Buffering: no' );
        if ( $exporter->supports_streaming() ) {
            // ---------------------------------------------------------------
            // Streaming path (CSV): iterate in 500-row pages, flush each chunk.
            // ---------------------------------------------------------------
            $chunk_size = 500;
            $page = 1;
            $total_rows = 0;
            $headers_map = array();
            $output = fopen( 'php://output', 'w' );
            $is_first = true;
            // Suppress any previous output buffering to enable true streaming.
            while ( ob_get_level() > 0 ) {
                ob_end_flush();
            }
            do {
                $chunk_args = array_merge( $filter_args, array(
                    'per_page' => $chunk_size,
                    'page'     => $page,
                ) );
                $chunk_result = LeadsRepository::query( $chunk_args );
                $chunk_leads = $chunk_result['items'];
                if ( empty( $chunk_leads ) ) {
                    break;
                }
                $meta_map = LeadsRepository::find_many( $chunk_leads );
                $total_rows += count( $chunk_leads );
                /** @var \Leadite\Export\CsvExporter $exporter */
                $exporter->export_chunk(
                    $output,
                    $chunk_leads,
                    $meta_map,
                    $headers_map,
                    $is_first
                );
                $is_first = false;
                // Flush the current chunk to the HTTP response.
                if ( function_exists( 'ob_flush' ) ) {
                    ob_flush();
                }
                flush();
                // Stop when we've fetched fewer rows than the chunk size.
                if ( count( $chunk_leads ) < $chunk_size ) {
                    break;
                }
                $page++;
            } while ( true );
            // Note: php://output needs no fclose; the request ends with exit below.
            // Audit log (uses $total_rows counted during iteration).
            LeadsRepository::log( 0, 'export', sprintf( 'Exported %d leads as %s (streaming)', $total_rows, strtoupper( $format ) ) );
        } else {
            // ---------------------------------------------------------------
            // Buffered path (JSON, XML, XLSX): load all rows, then export.
            // find_many() inside each exporter handles batch meta loading.
            // ---------------------------------------------------------------
            $all_args = array_merge( $filter_args, array(
                'per_page' => -1,
            ) );
            $query_result = LeadsRepository::query( $all_args );
            $leads = $query_result['items'];
            // Audit log.
            LeadsRepository::log( 0, 'export', sprintf( 'Exported %d leads as %s', count( $leads ), strtoupper( $format ) ) );
            $exporter->export( $leads );
        }
        exit;
    }

    protected function handle_clear_all_leads() {
        if ( !current_user_can( Config::capability() ) ) {
            return;
        }
        if ( !isset( $_POST['leadite_clear_nonce'] ) || !wp_verify_nonce( sanitize_key( wp_unslash( $_POST['leadite_clear_nonce'] ) ), 'leadite_clear_all_leads' ) ) {
            return;
        }
        global $wpdb;
        // Erase leads in batches of 100 to avoid loading all IDs into memory at
        // once on large installs.  LeadsRepository::delete() handles all 6 tables
        // + physical file removal for each row (satisfies GDPR right-to-erasure).
        $batch_size = 100;
        $table = LeadsRepository::table();
        do {
            $ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM %i LIMIT %d", $table, $batch_size ) );
            foreach ( (array) $ids as $sid ) {
                LeadsRepository::delete( (int) $sid );
            }
        } while ( !empty( $ids ) );
        // Truncate any remaining rows in tables not covered per-submission
        // (e.g. log row with submission_id=0 written by clear_all itself).
        $extra_tables = array($wpdb->prefix . 'leadite_log', $wpdb->prefix . 'leadite_submission_tags');
        foreach ( $extra_tables as $table ) {
            $wpdb->query( $wpdb->prepare( "DELETE FROM %i", $table ) );
        }
        LeadsRepository::log( 0, 'clear_all', 'User cleared all leads database tables.' );
        wp_safe_redirect( add_query_arg( array(
            'page'    => 'leadite-settings',
            'cleared' => 1,
        ), admin_url( 'admin.php' ) ) );
        exit;
    }

    // =========================================================================
    // Follow-Up Guard — Admin methods
    // =========================================================================
    /**
     * Admin notice if WP-Cron is disabled.
     * Shown ONLY on Leadite pages.
     */
    public function render_cron_notice() : void {
        if ( !defined( 'DISABLE_WP_CRON' ) || !DISABLE_WP_CRON ) {
            return;
        }
        // Only show on Leadite admin pages.
        $screen = get_current_screen();
        if ( !$screen || false === strpos( (string) $screen->id, 'leadite' ) ) {
            return;
        }
        if ( !current_user_can( Config::capability() ) ) {
            return;
        }
        echo '<div class="notice notice-warning is-dismissible">';
        echo '<p><strong>' . esc_html( Config::brand() ) . ':</strong> ' . esc_html__( 'WP-Cron is disabled. Background notifications (Telegram/Webhooks) and attachment downloads may be delayed.', 'leadite' ) . '</p>';
        echo '</div>';
    }

    /**
     * Admin notice: overdue and needs_attention counts.
     * Shown ONLY on Leadite pages, ONLY to Pro users, ONLY if there are results,
     * and NOT if the user has already dismissed it today.
     */
    public function render_sla_notice() : void {
        if ( !Pro::is_active() ) {
            return;
        }
    }

    /**
     * AJAX: persist per-user, per-day dismiss for the SLA notice.
     */
    public function ajax_dismiss_sla_notice() : void {
        check_ajax_referer( 'leadite_sla_dismiss', 'security' );
        if ( !current_user_can( Config::capability() ) ) {
            wp_send_json_error( 'forbidden', 403 );
        }
        if ( !Pro::is_active() ) {
            wp_send_json_error( 'pro_required', 403 );
        }
    }

    /**
     * Render the Follow-Up Guard / Smart Views page.
     *
     * Free users see a blurred preview and upgrade CTA.
     * Pro users get the full list with preset smart_view filter.
     */
    public function render_smart_views() : void {
        if ( !current_user_can( Config::capability() ) && !current_user_can( Config::view_capability() ) ) {
            wp_die( esc_html__( 'You do not have permission to view leads.', 'leadite' ) );
        }
        $allowed_svg = array(
            'svg'      => array(
                'class'           => true,
                'width'           => true,
                'height'          => true,
                'viewbox'         => true,
                'fill'            => true,
                'stroke'          => true,
                'stroke-width'    => true,
                'stroke-linecap'  => true,
                'stroke-linejoin' => true,
                'aria-hidden'     => true,
            ),
            'path'     => array(
                'd' => true,
            ),
            'line'     => array(
                'x1' => true,
                'y1' => true,
                'x2' => true,
                'y2' => true,
            ),
            'polyline' => array(
                'points' => true,
            ),
            'circle'   => array(
                'cx' => true,
                'cy' => true,
                'r'  => true,
            ),
        );
        $views = array(
            'hot'             => array(
                'icon'  => wp_kses( '<svg class="leadite-icon leadite-icon--hot" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg>', $allowed_svg ),
                'label' => esc_html__( 'Hot Leads', 'leadite' ),
                'desc'  => esc_html__( 'High-priority leads most likely to convert.', 'leadite' ),
            ),
            'needs_attention' => array(
                'icon'  => wp_kses( '<svg class="leadite-icon leadite-icon--warn" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>', $allowed_svg ),
                'label' => esc_html__( 'Needs Follow-up', 'leadite' ),
                'desc'  => esc_html__( 'Leads not opened within the configured time window.', 'leadite' ),
            ),
            'overdue'         => array(
                'icon'  => wp_kses( '<svg class="leadite-icon leadite-icon--overdue" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>', $allowed_svg ),
                'label' => esc_html__( 'Overdue', 'leadite' ),
                'desc'  => esc_html__( 'Leads not closed within the deadline.', 'leadite' ),
            ),
            'unassigned'      => array(
                'icon'  => wp_kses( '<svg class="leadite-icon leadite-icon--unassigned" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>', $allowed_svg ),
                'label' => esc_html__( 'Unassigned', 'leadite' ),
                'desc'  => esc_html__( 'Active leads not yet assigned to a team member.', 'leadite' ),
            ),
            'returning'       => array(
                'icon'  => wp_kses( '<svg class="leadite-icon leadite-icon--returning" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-3.45"/></svg>', $allowed_svg ),
                'label' => esc_html__( 'Returning Leads', 'leadite' ),
                'desc'  => esc_html__( 'Visitors who have submitted a lead before.', 'leadite' ),
            ),
        );
        $current_view = ( isset( $_GET['smart_view'] ) ? sanitize_key( wp_unslash( $_GET['smart_view'] ) ) : 'hot' );
        // phpcs:ignore WordPress.Security.NonceVerification
        if ( !array_key_exists( $current_view, $views ) ) {
            $current_view = 'hot';
        }
        echo '<div class="wrap leadite leadite-smart-views-page">';
        echo '<h1>' . esc_html( Config::brand() ) . ' — ' . esc_html__( 'Follow-Up Guard', 'leadite' ) . '</h1>';
        echo '<p class="leadite-dashboard-subtitle">' . esc_html__( 'Prioritise your leads and make sure nothing falls through the cracks.', 'leadite' ) . '</p>';
        // View tabs
        echo '<nav class="leadite-smart-view-tabs" aria-label="' . esc_attr__( 'Smart views', 'leadite' ) . '">';
        foreach ( $views as $slug => $view ) {
            $url = add_query_arg( array(
                'page'       => 'leadite-smart-views',
                'smart_view' => $slug,
            ), admin_url( 'admin.php' ) );
            $active = ( $slug === $current_view ? ' leadite-tab--active' : '' );
            echo '<a href="' . esc_url( $url ) . '" class="leadite-tab' . esc_attr( $active ) . '">' . wp_kses( $view['icon'], self::allowed_svg() ) . ' ' . esc_html( $view['label'] ) . '</a>';
        }
        echo '</nav>';
        if ( !Pro::is_active() ) {
            // Pro notice — same pattern as every other Pro-gated section.
            echo '<div class="leadite-pro-tab-notice" style="margin-bottom:20px;">';
            echo '<span class="leadite-pro-tab-notice__icon"><svg class="leadite-pro-notice-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg></span>';
            echo '<div class="leadite-pro-tab-notice__body">';
            echo '<strong>' . esc_html__( 'Leadite Pro required', 'leadite' ) . '</strong> — ';
            echo esc_html__( 'Follow-Up Guard shows your hottest leads, overdue alerts, and Smart Views — upgrade to unlock.', 'leadite' );
            echo ' <a href="' . esc_url( \leadite_get_upgrade_url() ) . '" target="_blank" rel="noopener noreferrer"><strong>' . esc_html__( 'Get Pro →', 'leadite' ) . '</strong></a>';
            echo '</div>';
            echo '</div>';
            // Tabs are already rendered above — just add a disabled-looking content area.
            echo '<div class="leadite-field-locked" style="margin-top:8px;">';
            echo '<table class="wp-list-table widefat fixed striped" style="opacity:0.5;">';
            echo '<thead><tr>';
            echo '<th>' . esc_html__( 'Lead', 'leadite' ) . '</th>';
            echo '<th>' . esc_html__( 'Source', 'leadite' ) . '</th>';
            echo '<th>' . esc_html__( 'Status', 'leadite' ) . '</th>';
            echo '<th>' . esc_html__( 'Priority', 'leadite' ) . '</th>';
            echo '<th>' . esc_html__( 'Date', 'leadite' ) . '</th>';
            echo '</tr></thead>';
            echo '<tbody>';
            for ($i = 0; $i < 4; $i++) {
                echo '<tr><td colspan="5">&nbsp;</td></tr>';
            }
            echo '</tbody></table>';
            echo '</div>';
        }
        $this->branding_footer();
        echo '</div>';
        // wrap
    }

    /**
     * Render the Follow-Up Guard block inside the lead detail view (Pro only).
     * Shows score with breakdown tooltip, returning lead info, and SLA status.
     *
     * @param object $row    Submission row from LeadsRepository::find().
     * @param array  $fields Meta fields array.
     */
    protected function render_followup_guard_block( object $row, array $fields ) : void {
        if ( !Pro::is_active() ) {
            return;
        }
    }

    /**
     * Render the Follow-Up Guard settings section within the Settings page.
     * Called from render_settings() when the current tab is 'followup'.
     *
     * @param array $cfg Current settings array.
     */
    protected function render_followup_settings_section( array $cfg ) : void {
        $enabled = !empty( $cfg['followup_enabled'] );
        $not_open_min = (int) ($cfg['followup_not_opened_min'] ?? 15);
        $not_close_h = (int) ($cfg['followup_not_closed_hours'] ?? 24);
        $threshold = (int) ($cfg['followup_hot_threshold'] ?? 50);
        $digest = !empty( $cfg['followup_digest_enabled'] );
        $digest_email = sanitize_email( (string) ($cfg['followup_digest_email'] ?? '') );
        if ( !Pro::is_active() ) {
            // Pro notice banner (same pattern as timeline/other Pro sections).
            echo '<div class="leadite-pro-tab-notice" style="margin-bottom:24px;">';
            echo '<span class="leadite-pro-tab-notice__icon"><svg class="leadite-pro-notice-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg></span>';
            echo '<div class="leadite-pro-tab-notice__body">';
            echo '<strong>' . esc_html__( 'Leadite Pro required', 'leadite' ) . '</strong> — ';
            echo esc_html__( 'Follow-Up Guard monitors response times and scores your leads automatically. You can see the settings below. Saving without a Pro license has no effect.', 'leadite' );
            echo ' <a href="' . esc_url( \leadite_get_upgrade_url() ) . '" target="_blank" rel="noopener noreferrer"><strong>' . esc_html__( 'Get Pro →', 'leadite' ) . '</strong></a>';
            echo '</div>';
            echo '</div>';
            // Blurred preview — real settings table with leadite-field-locked (same as other Pro sections).
            echo '<div class="leadite-settings-section leadite-field-locked" style="margin-top:4px;">';
            echo '<table class="form-table leadite-settings-table" role="presentation">';
            echo '<tr><th>' . esc_html__( 'Follow-Up Guard', 'leadite' ) . '</th><td>';
            echo '<label><input type="checkbox" disabled> ' . esc_html__( 'Enable Follow-Up Guard', 'leadite' ) . '</label>';
            echo '<p class="description">' . esc_html__( 'Enables hot scoring, SLA monitoring, and Smart Views.', 'leadite' ) . '</p>';
            echo '</td></tr>';
            echo '<tr><th>' . esc_html__( 'Sidebar menu badge', 'leadite' ) . '</th><td>';
            echo '<label><input type="checkbox" checked disabled> ' . esc_html__( 'Show overdue leads badge in the admin sidebar menu', 'leadite' ) . '</label>';
            echo '<p class="description">' . esc_html__( 'If checked, shows a red badge with the count of overdue leads next to the Leadite menu item.', 'leadite' ) . '</p>';
            echo '</td></tr>';
            echo '<tr><th>' . esc_html__( 'Needs-attention threshold', 'leadite' ) . '</th><td>';
            echo '<label>' . esc_html__( 'Not opened within', 'leadite' ) . ' <input type="number" value="15" class="small-text" disabled> ' . esc_html__( 'minutes', 'leadite' ) . '</label>';
            echo '</td></tr>';
            echo '<tr><th>' . esc_html__( 'Overdue threshold', 'leadite' ) . '</th><td>';
            echo '<label>' . esc_html__( 'Not closed within', 'leadite' ) . ' <input type="number" value="24" class="small-text" disabled> ' . esc_html__( 'hours', 'leadite' ) . '</label>';
            echo '</td></tr>';
            echo '<tr><th>' . esc_html__( 'Hot lead threshold', 'leadite' ) . '</th><td>';
            echo '<label>' . esc_html__( 'Score at least', 'leadite' ) . ' <input type="number" value="50" class="small-text" disabled> ' . esc_html__( 'points', 'leadite' ) . '</label>';
            echo '</td></tr>';
            echo '<tr><th>' . esc_html__( 'Daily digest', 'leadite' ) . '</th><td>';
            echo '<label><input type="checkbox" disabled> ' . esc_html__( 'Send daily Follow-Up Guard digest email', 'leadite' ) . '</label>';
            echo '</td></tr>';
            echo '<tr><th>' . esc_html__( 'Digest sending hour', 'leadite' ) . '</th><td>';
            echo '<select disabled><option value="8">08:00</option></select>';
            echo '</td></tr>';
            echo '</table>';
            echo '</div>';
            return;
        }
    }

    /**
     * Render the Follow-Up Guard summary widget for the Dashboard.
     * Pro: shows counts. Free: shows a blurred upgrade CTA card.
     */
    protected function render_sla_dashboard_widget() : void {
        if ( !Pro::is_active() ) {
            // Blur preview for Free
            echo '<div class="leadite-card leadite-fug-dashboard-card leadite-pro-preview">';
            echo '  <div class="leadite-card-header"><h2><svg class="leadite-icon leadite-icon--hot" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg> ' . esc_html__( 'Follow-Up Guard', 'leadite' ) . ' <span class="leadite-pro-badge">Pro</span></h2></div>';
            echo '  <div style="filter:blur(4px);pointer-events:none;padding:16px;">';
            echo '    <div style="background:#fee2e2;border-radius:8px;padding:12px;margin-bottom:8px;"><svg width="12" height="12" viewBox="0 0 24 24" fill="#dc2626" stroke="none" aria-hidden="true"><circle cx="12" cy="12" r="10"/></svg> 3 ' . esc_html__( 'leads overdue', 'leadite' ) . '</div>';
            echo '    <div style="background:#fef3c7;border-radius:8px;padding:12px;margin-bottom:8px;"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#92400e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> 7 ' . esc_html__( 'need follow-up', 'leadite' ) . '</div>';
            echo '    <div style="background:#dcfce7;border-radius:8px;padding:12px;"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#166534" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg> 2 ' . esc_html__( 'hot leads', 'leadite' ) . '</div>';
            echo '  </div>';
            echo '  <p style="text-align:center;padding:8px 0 0;">' . esc_html__( 'Upgrade to Pro to monitor your lead response times.', 'leadite' ) . '</p>';
            echo '</div>';
            return;
        }
    }

    /**
     * Optional "powered by" footer (respects white-label toggle).
     */
    protected function branding_footer() {
        if ( Config::branding_hidden() ) {
            return;
        }
        echo '<p class="leadite-powered">' . esc_html( sprintf( 
            /* translators: %s brand */
            esc_html__( 'Powered by %s', 'leadite' ),
            Config::brand()
         ) ) . '</p>';
    }

    /**
     * AJAX handler for adding a note to a lead.
     */
    public function ajax_add_note() {
        check_ajax_referer( 'leadite_note_nonce', 'security' );
        if ( !current_user_can( Config::capability() ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Forbidden', 'leadite' ),
            ) );
        }
        // Notes are a Pro feature.
        if ( !Pro::is_active() ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Notes require Leadite Pro.', 'leadite' ),
            ) );
        }
    }

    /**
     * AJAX handler for updating a lead status via Kanban drag-and-drop.
     */
    public function ajax_update_lead_status() {
        check_ajax_referer( 'leadite_kanban_nonce', 'security' );
        if ( !current_user_can( Config::capability() ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Forbidden', 'leadite' ),
            ) );
        }
        $lead_id = ( isset( $_POST['lead_id'] ) ? (int) $_POST['lead_id'] : 0 );
        $status = ( isset( $_POST['status'] ) ? sanitize_key( $_POST['status'] ) : '' );
        $allowed = array(
            'new',
            'read',
            'in_progress',
            'closed',
            'spam'
        );
        if ( !$lead_id || !in_array( $status, $allowed, true ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Invalid status', 'leadite' ),
            ) );
        }
        $ok = LeadsRepository::set_status( $lead_id, $status );
        if ( $ok ) {
            LeadsRepository::log( $lead_id, 'status', $status );
            wp_send_json_success( array(
                'message' => esc_html__( 'Status updated successfully', 'leadite' ),
            ) );
        } else {
            wp_send_json_error( array(
                'message' => esc_html__( 'Failed to update status', 'leadite' ),
            ) );
        }
    }

    /**
     * AJAX handler to toggle the starred flag on a lead.
     */
    public function ajax_toggle_starred() {
        $lead_id = ( isset( $_POST['lead_id'] ) ? (int) $_POST['lead_id'] : 0 );
        if ( !$lead_id ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Invalid lead ID', 'leadite' ),
            ) );
        }
        check_ajax_referer( 'leadite_starred_' . $lead_id, 'security' );
        if ( !current_user_can( Config::capability() ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Forbidden', 'leadite' ),
            ) );
        }
        // Starred leads are a Pro feature.
        if ( !Pro::is_active() ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Starred leads require Leadite Pro.', 'leadite' ),
            ) );
        }
    }

    /**
     * AJAX handler to assign a lead to a WordPress user.
     */
    public function ajax_assign_lead() {
        $lead_id = ( isset( $_POST['lead_id'] ) ? (int) $_POST['lead_id'] : 0 );
        if ( !$lead_id ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Invalid lead ID', 'leadite' ),
            ) );
        }
        check_ajax_referer( 'leadite_assign_' . $lead_id, 'security' );
        if ( !current_user_can( Config::capability() ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Forbidden', 'leadite' ),
            ) );
        }
        // Lead assignment is a Pro feature.
        if ( !Pro::is_active() ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Lead assignment requires Leadite Pro.', 'leadite' ),
            ) );
        }
    }

    // ———— Tags AJAX ————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
    /**
     * AJAX: add a tag to a lead.
     */
    public function ajax_add_lead_tag() {
        check_ajax_referer( 'leadite_tags_nonce', 'security' );
        if ( !current_user_can( Config::capability() ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Forbidden', 'leadite' ),
            ) );
        }
        if ( !Pro::is_active() ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Tags require Leadite Pro.', 'leadite' ),
            ) );
        }
    }

    /**
     * AJAX: remove a tag from a lead.
     */
    public function ajax_remove_lead_tag() {
        check_ajax_referer( 'leadite_tags_nonce', 'security' );
        if ( !current_user_can( Config::capability() ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Forbidden', 'leadite' ),
            ) );
        }
        if ( !Pro::is_active() ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Tags require Leadite Pro.', 'leadite' ),
            ) );
        }
    }

    /**
     * AJAX: create a new global tag, and optionally attach it to a lead.
     */
    public function ajax_create_tag() {
        check_ajax_referer( 'leadite_tags_nonce', 'security' );
        if ( !current_user_can( Config::capability() ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Forbidden', 'leadite' ),
            ) );
        }
        if ( !Pro::is_active() ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Tags require Leadite Pro.', 'leadite' ),
            ) );
        }
    }

    /**
     * AJAX: delete a global tag (removes relations, does NOT delete leads).
     */
    public function ajax_delete_tag() {
        check_ajax_referer( 'leadite_tags_nonce', 'security' );
        if ( !current_user_can( Config::capability() ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Forbidden', 'leadite' ),
            ) );
        }
        if ( !Pro::is_active() ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Tags require Leadite Pro.', 'leadite' ),
            ) );
        }
    }

    // ———— Field Mapper POST handler ——————————————————————————————————————————————————————————————————————————————————————————
    /**
     * Handle POST: save field-map overrides from the Field Mapper settings tab.
     *
     * Nonce: leadite_save_field_map
     * Capability: Config::capability()
     * Pro gate: Pro::is_active()
     */
    public function handle_save_field_map() {
        if ( !isset( $_POST['leadite_field_map_nonce'] ) || !wp_verify_nonce( sanitize_key( wp_unslash( $_POST['leadite_field_map_nonce'] ) ), 'leadite_save_field_map' ) ) {
            wp_die( esc_html__( 'Invalid nonce', 'leadite' ) );
        }
        if ( !current_user_can( Config::capability() ) ) {
            wp_die( esc_html__( 'Forbidden', 'leadite' ) );
        }
        if ( !Pro::is_active() ) {
            wp_die( esc_html__( 'Field Mapper requires Leadite Pro.', 'leadite' ) );
        }
    }

    /**
     * Render Kanban view filters.
     */
    // phpcs:disable WordPress.Security.NonceVerification.Recommended -- display-only GET params for filter controls, sanitized via sanitize_text_field
    protected function render_kanban_filters() {
        $source = ( isset( $_GET['source'] ) ? sanitize_text_field( wp_unslash( $_GET['source'] ) ) : '' );
        $form = ( isset( $_GET['form'] ) ? sanitize_text_field( wp_unslash( $_GET['form'] ) ) : '' );
        $from = ( isset( $_GET['from'] ) ? sanitize_text_field( wp_unslash( $_GET['from'] ) ) : '' );
        $to = ( isset( $_GET['to'] ) ? sanitize_text_field( wp_unslash( $_GET['to'] ) ) : '' );
        echo '<div class="tablenav top">';
        echo '<div class="alignleft actions leadite-filters">';
        // Source.
        echo '<select name="source"><option value="">' . esc_html__( 'All sources', 'leadite' ) . '</option>';
        foreach ( LeadsRepository::sources() as $s ) {
            printf(
                '<option value="%s" %s>%s</option>',
                esc_attr( $s ),
                selected( $source, $s, false ),
                esc_html( $s )
            );
        }
        echo '</select> ';
        // Form.
        echo '<select name="form"><option value="">' . esc_html__( 'All forms', 'leadite' ) . '</option>';
        foreach ( LeadsRepository::form_names() as $f ) {
            printf(
                '<option value="%s" %s>%s</option>',
                esc_attr( $f ),
                selected( $form, $f, false ),
                esc_html( $f )
            );
        }
        echo '</select> ';
        echo '<input type="date" name="from" value="' . esc_attr( $from ) . '"> ';
        echo '<input type="date" name="to" value="' . esc_attr( $to ) . '"> ';
        submit_button(
            esc_html__( 'Filter', 'leadite' ),
            'secondary',
            'filter_action',
            false
        );
        $has_filter = !empty( $source ) || !empty( $form ) || !empty( $from ) || !empty( $to ) || !empty( $_GET['s'] ) || Pro::is_active() && isset( $_GET['tag_id'] ) && (int) $_GET['tag_id'] > 0;
        if ( $has_filter ) {
            echo ' <a href="' . esc_url( admin_url( 'admin.php?page=leadite-leads&view_mode=kanban' ) ) . '" class="button">' . esc_html__( 'Clear Filters', 'leadite' ) . '</a>';
        }
        echo '</div>';
        echo '</div>';
    }

    /**
     * Render Kanban board column grid.
     */
    protected function render_kanban_board() {
        $statuses = array(
            'new'         => esc_html__( 'New', 'leadite' ),
            'read'        => esc_html__( 'Read', 'leadite' ),
            'in_progress' => esc_html__( 'In progress', 'leadite' ),
            'closed'      => esc_html__( 'Closed', 'leadite' ),
            'spam'        => esc_html__( 'Spam', 'leadite' ),
        );
        $filter_args = array(
            'source'    => ( isset( $_GET['source'] ) ? sanitize_text_field( wp_unslash( $_GET['source'] ) ) : '' ),
            'form'      => ( isset( $_GET['form'] ) ? sanitize_text_field( wp_unslash( $_GET['form'] ) ) : '' ),
            'date_from' => ( isset( $_GET['from'] ) ? sanitize_text_field( wp_unslash( $_GET['from'] ) ) : '' ),
            'date_to'   => ( isset( $_GET['to'] ) ? sanitize_text_field( wp_unslash( $_GET['to'] ) ) : '' ),
            'search'    => ( isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '' ),
            'per_page'  => 30,
        );
        // Info Banner for Kanban limits.
        echo '<div class="leadite-kanban-info-banner">';
        echo '<svg class="leadite-kanban-info-banner__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>';
        echo '<span>' . esc_html__( 'Showing the latest 30 leads per column. Use the filters above to find older submissions.', 'leadite' ) . '</span>';
        echo '</div>';
        echo '<div class="leadite-kanban-board">';
        foreach ( $statuses as $status_slug => $status_label ) {
            $query_args = array_merge( $filter_args, array(
                'status' => $status_slug,
            ) );
            $results = LeadsRepository::query( $query_args );
            $leads = $results['items'];
            $total = $results['total'];
            // Batch-fetch all meta for this column in one query.
            $col_meta_map = LeadsRepository::find_many( $leads );
            $col_tags_map = array();
            printf( '<div class="leadite-kanban-column" data-status="%s">', esc_attr( $status_slug ) );
            // Column Header.
            echo '<div class="leadite-kanban-column-header">';
            printf( '<h3>%s</h3>', esc_html( $status_label ) );
            printf( '<span class="leadite-kanban-column-count">%d</span>', (int) $total );
            echo '</div>';
            // Column Cards List.
            printf( '<div class="leadite-kanban-column-cards" id="leadite-kanban-cards-%s">', esc_attr( $status_slug ) );
            foreach ( $leads as $lead ) {
                // Use the pre-fetched column meta map.
                if ( isset( $col_meta_map[(int) $lead->id] ) ) {
                    $fields = $col_meta_map[(int) $lead->id]['fields'];
                } else {
                    $lead_data = LeadsRepository::find( $lead->id );
                    $fields = $lead_data['fields'];
                }
                $name = FieldMapper::canonical_value( $fields, 'full_name' );
                $email = FieldMapper::canonical_value( $fields, 'email' );
                $phone = FieldMapper::canonical_value( $fields, 'phone' );
                $title = ( $name ? $name : (( $email ? $email : esc_html__( '(no name)', 'leadite' ) )) );
                $view_url = add_query_arg( array(
                    'page' => 'leadite-leads',
                    'view' => (int) $lead->id,
                ), admin_url( 'admin.php' ) );
                // Star and assignee data for kanban card.
                $card_starred = !empty( $lead->starred );
                $card_starred_nonce = wp_create_nonce( 'leadite_starred_' . (int) $lead->id );
                $card_assigned_id = (int) $lead->assigned_to;
                printf( '<div class="leadite-kanban-card" draggable="true" data-lead-id="%d">', (int) $lead->id );
                // Card top row: title + star button.
                echo '<div class="leadite-kanban-card-header">';
                printf( '<div class="leadite-kanban-card-title"><a href="%s"><strong>%s</strong></a></div>', esc_url( $view_url ), esc_html( $title ) );
                printf(
                    '<button type="button" class="leadite-star-btn leadite-star-btn--list %s" data-lead-id="%d" data-nonce="%s" aria-label="%s" title="%s">%s</button>',
                    ( $card_starred ? 'is-starred' : '' ),
                    (int) $lead->id,
                    esc_attr( $card_starred_nonce ),
                    ( $card_starred ? esc_attr__( 'Remove from favourites', 'leadite' ) : esc_attr__( 'Add to favourites', 'leadite' ) ),
                    ( $card_starred ? esc_attr__( 'Starred', 'leadite' ) : esc_attr__( 'Star this lead', 'leadite' ) ),
                    ( $card_starred ? '<svg class="leadite-icon leadite-star-icon" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none" aria-hidden="true"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>' : '<svg class="leadite-icon leadite-star-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>' )
                );
                echo '</div>';
                // leadite-kanban-card-header
                if ( $email ) {
                    printf( '<div class="leadite-kanban-card-meta">%s</div>', esc_html( $email ) );
                }
                if ( $phone ) {
                    printf( '<div class="leadite-kanban-card-meta">%s</div>', esc_html( $phone ) );
                }
                $is_imported = false;
                foreach ( $fields as $f ) {
                    $f_key = ( is_array( $f ) ? $f['key'] ?? '' : $f->field_key ?? '' );
                    $f_val = ( is_array( $f ) ? $f['value'] ?? '' : $f->field_value ?? '' );
                    if ( '_imported' === $f_key && '1' === $f_val ) {
                        $is_imported = true;
                        break;
                    }
                }
                echo '<div class="leadite-kanban-card-footer">';
                printf( '<span class="leadite-badge leadite-badge--source">%s</span>', esc_html( $lead->source ) );
                if ( $is_imported ) {
                    printf( '<span class="leadite-badge leadite-badge--imported">%s</span>', esc_html__( 'Imported', 'leadite' ) );
                }
                // Assignee initials badge.
                if ( $card_assigned_id > 0 ) {
                    $assignee_data = get_userdata( $card_assigned_id );
                    if ( $assignee_data ) {
                        $initials = mb_strtoupper( mb_substr( $assignee_data->display_name, 0, 1 ) );
                        printf( '<span class="leadite-kanban-assignee" title="%s">%s</span>', esc_attr( $assignee_data->display_name ), esc_html( $initials ) );
                    }
                }
                printf( '<span class="leadite-kanban-card-date">%s</span>', esc_html( wp_date( 'j M H:i', strtotime( get_gmt_from_date( $lead->created_at ) ) ) ) );
                echo '</div>';
                echo '</div>';
                // card
            }
            if ( empty( $leads ) ) {
                printf( '<div class="leadite-kanban-empty">%s</div>', esc_html__( 'No leads', 'leadite' ) );
            }
            echo '</div>';
            // column-cards
            echo '</div>';
            // column
        }
        echo '</div>';
        // board
        // Kanban Tip Instruction
        echo '<div class="leadite-kanban-instruction">';
        echo '<svg class="leadite-kanban-info-banner__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>';
        echo '<span>' . esc_html__( 'Tip: Drag and drop cards to change status, or click on a card to open its details and manager comments.', 'leadite' ) . '</span>';
        echo '</div>';
    }

    // phpcs:enable WordPress.Security.NonceVerification.Recommended
    /**
     * Render the Attachments settings tab.
     *
     * Shows:
     *  1. File storage protection status (best-effort self-test result — always on).
     *  2. Optional attachment filtering settings (OFF by default).
     *
     * @param array $cfg Current Config::all() values.
     */
    protected function render_attachments_tab( array $cfg ) {
        // ———— 1. File Storage Protection Status ————————————————————————————————————————————————————————————
        $protection_status = get_option( 'leadite_upload_protection_status', 'unknown' );
        echo '<div class="leadite-settings-section">';
        echo '<h3 class="leadite-settings-section-title">' . esc_html__( 'File Storage Protection', 'leadite' ) . '</h3>';
        echo '<p class="description" style="margin-bottom:12px;">' . esc_html__( 'Leadite stores all uploaded attachments in a private directory with .htaccess (Apache) and web.config (IIS) deny rules. The result below is from a self-test performed at activation/update time.', 'leadite' ) . '</p>';
        if ( 'ok' === $protection_status ) {
            printf( '<div class="notice notice-success inline" style="margin:0 0 16px;"><p><svg style="vertical-align:middle;margin-right:4px" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg><strong>%s</strong> — %s</p></div>', esc_html__( 'Direct access is blocked', 'leadite' ), esc_html__( 'The server correctly returned 403/404 for a test file in uploads/Leadite/. Your lead attachments are protected.', 'leadite' ) );
        } elseif ( 'fail' === $protection_status ) {
            printf(
                '<div class="notice notice-warning inline" style="margin:0 0 16px;"><p><svg style="vertical-align:middle;margin-right:4px" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg><strong>%s</strong> — %s</p><p>%s</p></div>',
                esc_html__( 'Direct access NOT blocked', 'leadite' ),
                esc_html__( 'The server returned HTTP 200 for a test file in uploads/Leadite/. Files may be publicly accessible via direct URL.', 'leadite' ),
                esc_html__( 'This is a server configuration issue, not a Leadite bug. Leadite has already created .htaccess and web.config deny rules — if they are not respected, your web server (e.g. Nginx) may need explicit configuration. Contact your hosting provider or add deny rules to your Nginx config. Leadite will continue to function normally; all file downloads still require an authenticated user and a one-time nonce.', 'leadite' )
            );
        } else {
            printf( '<div class="notice notice-info inline" style="margin:0 0 16px;"><p><svg style="vertical-align:middle;margin-right:4px" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg><strong>%s</strong> — %s</p></div>', esc_html__( 'Protection status: unknown', 'leadite' ), esc_html__( 'The self-test could not be completed (firewall, Basic Auth, local dev environment, or loopback HTTP blocked). Leadite has created .htaccess and web.config rules — this is not an error. Re-run the test by deactivating and reactivating the plugin.', 'leadite' ) );
        }
        printf( '<p><strong>%s</strong> <code>%s</code></p>', esc_html__( 'Storage path:', 'leadite' ), esc_html( wp_upload_dir()['basedir'] . '/Leadite/' ) );
        echo '</div>';
        // leadite-settings-section
        // ———— 2. Attachment Filtering ——————————————————————————————————————————————————————————————————————————————————
        echo '<div class="leadite-settings-section">';
        echo '<h3 class="leadite-settings-section-title">' . esc_html__( 'Attachment Filtering', 'leadite' ) . '</h3>';
        echo '<p class="description" style="margin-bottom:12px;">' . esc_html__( 'By default Leadite follows the form plugin upload rules and does not add extra restrictions. Enable this only if you want Leadite to apply an additional safety layer before storing duplicated attachments.', 'leadite' ) . '</p>';
        $filtering_enabled = !empty( $cfg['att_filtering_enabled'] );
        $max_kb = (int) (( isset( $cfg['att_max_size_kb'] ) ? $cfg['att_max_size_kb'] : 10240 ));
        $allowed_exts = ( isset( $cfg['att_allowed_extensions'] ) ? $cfg['att_allowed_extensions'] : 'jpg,jpeg,png,gif,webp,pdf,doc,docx,xls,xlsx,ppt,pptx,txt,csv,zip,odt,ods' );
        echo '<table class="form-table">';
        // Enable toggle.
        echo '<tr>';
        echo '<th><label for="att_filtering_enabled">' . esc_html__( 'Enable Leadite attachment filtering', 'leadite' ) . '</label></th>';
        echo '<td>';
        printf( '<label><input type="checkbox" id="att_filtering_enabled" name="att_filtering_enabled" value="1" %s> %s</label>', checked( $filtering_enabled, true, false ), esc_html__( 'Apply size and extension checks before storing attachments in Leadite', 'leadite' ) );
        echo '<p class="description">' . esc_html__( 'When disabled (default), files already accepted by the form plugin are copied as-is. When enabled, files that fail the checks below are NOT copied and a "skipped" record is saved so you can see what was blocked and why.', 'leadite' ) . '</p>';
        echo '</td>';
        echo '</tr>';
        // Max size.
        echo '<tr>';
        echo '<th><label for="att_max_size_kb">' . esc_html__( 'Maximum file size (KB)', 'leadite' ) . '</label></th>';
        echo '<td>';
        printf( '<input type="number" id="att_max_size_kb" name="att_max_size_kb" value="%d" min="1" class="small-text">', esc_attr( $max_kb ) );
        printf( 
            '<p class="description">%s</p>',
            /* translators: %s: human-readable size */
            sprintf( esc_html__( 'Default: 10240 KB (10 MB). Current value: %s.', 'leadite' ), esc_html( size_format( $max_kb * 1024 ) ) )
         );
        echo '</td>';
        echo '</tr>';
        // Allowed extensions.
        echo '<tr>';
        echo '<th><label for="att_allowed_extensions">' . esc_html__( 'Allowed extensions', 'leadite' ) . '</label></th>';
        echo '<td>';
        printf( '<input type="text" id="att_allowed_extensions" name="att_allowed_extensions" value="%s" class="large-text" placeholder="jpg,jpeg,png,pdf,doc,...">', esc_attr( $allowed_exts ) );
        echo '<p class="description">' . esc_html__( 'Comma-separated list of allowed file extensions (lowercase, no dots). Files with extensions not on this list will be skipped. Leadite also uses wp_check_filetype_and_ext() to validate the real MIME type against the extension.', 'leadite' ) . '</p>';
        echo '</td>';
        echo '</tr>';
        echo '</table>';
        echo '</div>';
        // leadite-settings-section
    }

    // f-pro-start
    /**
     * Render timeline item (notes or logs).
     *
     * @param object $item Timeline event item.
     */
    protected function render_timeline_item( $item ) {
    }

    // f-pro-end
    /**
     * AJAX: Batch-anonymize IP addresses in existing leadite_submissions rows.
     *
     * Processes 200 rows per call so that each AJAX request completes well within
     * the PHP timeout. The JS layer repeatedly calls this until remaining === 0.
     *
     * Action:     wp_ajax_leadite_batch_anonymize_ip
     * Nonce:      leadite_batch_anonymize
     * Capability: Config::capability()
     */
    public function ajax_batch_anonymize_ip() {
        check_ajax_referer( 'leadite_batch_anonymize', 'security' );
        if ( !current_user_can( Config::capability() ) ) {
            wp_send_json_error( array(
                'message' => esc_html__( 'Forbidden', 'leadite' ),
            ) );
        }
        global $wpdb;
        $table = $wpdb->prefix . 'leadite_submissions';
        $batch = 200;
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB -- $table is hardcoded from $wpdb->prefix
        // Fetch a batch of rows where IP is not empty and not already zeroed.
        // We detect "not yet zeroed" by checking if the IP does NOT end in .0 (IPv4)
        // or is not already all-zero (simple heuristic; re-running is always safe).
        $rows = $wpdb->get_results( 
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery
            $wpdb->prepare( "SELECT id, ip FROM {$table} WHERE ip != '' AND ip IS NOT NULL ORDER BY id ASC LIMIT %d", $batch )
         );
        $processed = 0;
        foreach ( (array) $rows as $row ) {
            $anon = \Leadite\Support\FieldMapper::anonymize_ip( (string) $row->ip );
            if ( $anon !== $row->ip ) {
                $wpdb->update(
                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery
                    $table,
                    array(
                        'ip' => $anon,
                    ),
                    array(
                        'id' => (int) $row->id,
                    ),
                    array('%s'),
                    array('%d')
                );
            } else {
                // Already anonymized — overwrite with empty string to remove it from future batches.
                $wpdb->update(
                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery
                    $table,
                    array(
                        'ip' => $anon,
                    ),
                    array(
                        'id' => (int) $row->id,
                    ),
                    array('%s'),
                    array('%d')
                );
            }
            ++$processed;
        }
        // Count remaining non-anonymized rows.
        $remaining = (int) $wpdb->get_var( 
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery
            "SELECT COUNT(*) FROM {$table} WHERE ip != '' AND ip IS NOT NULL"
         );
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB
        wp_send_json_success( array(
            'processed' => $processed,
            'remaining' => $remaining,
        ) );
    }

    // f-pro-start
    /**
     * Send a test digest email.
     */
    public function ajax_send_test_digest() {
    }

    // f-pro-end
    /**
     * Render the 'General' settings tab.
     *
     * @param array $cfg Plugin configuration.
     */
    protected function render_general_tab( $cfg ) {
        echo '<table class="form-table">';
        echo '<tr><th><label for="per_page">' . esc_html__( 'Leads per page', 'leadite' ) . '</label></th>';
        echo '<td><input type="number" min="1" id="per_page" name="per_page" value="' . esc_attr( $cfg['per_page'] ) . '"></td></tr>';
        echo '<tr><th>' . esc_html__( 'Enabled sources', 'leadite' ) . '</th><td>';
        foreach ( $this->registry_choices() as $slug => $label_available ) {
            list( $label, $available ) = $label_available;
            $checked = in_array( $slug, (array) $cfg['enabled_sources'], true );
            echo '<label style="display:block;margin-bottom:4px;">';
            echo '<input type="checkbox" name="enabled_sources[]" value="' . esc_attr( $slug ) . '" ' . checked( $checked, true, false ) . '> ';
            echo esc_html( $label );
            echo ( $available ? '' : ' <em>(' . esc_html__( 'plugin not active', 'leadite' ) . ')</em>' );
            echo '</label>';
        }
        echo '</td></tr>';
        echo '<tr><th>' . esc_html__( 'Uninstall Settings', 'leadite' ) . '</th><td>';
        echo '<label><input type="checkbox" name="delete_on_uninstall" value="1" ' . checked( !empty( $cfg['delete_on_uninstall'] ), true, false ) . '> ';
        echo esc_html__( 'Delete all Leadite data (leads, notes, logs, settings) when the plugin is deleted', 'leadite' );
        echo '</label><p class="description">' . esc_html__( 'WARNING: If checked, all Leadite tables and options will be permanently dropped when you delete the plugin. Kept unchecked by default to protect your leads.', 'leadite' ) . '</p>';
        echo '</td></tr>';
        // ———— Data Privacy ——————————————————————————————————————————————————————————————————————————————————————————
        echo '<tr><th>' . esc_html__( 'Data Privacy', 'leadite' ) . '</th><td>';
        printf( '<label><input type="checkbox" id="anonymize_ip" name="anonymize_ip" value="1" %s> %s</label>', checked( !empty( $cfg['anonymize_ip'] ), true, false ), esc_html__( 'Anonymize IP addresses before storage (recommended for GDPR compliance)', 'leadite' ) );
        echo '<p class="description">';
        esc_html_e( 'When enabled, the last octet of IPv4 (e.g. 203.0.113.42 → 203.0.113.0) and the last 80 bits of IPv6 addresses are zeroed before the lead is stored. The full IP is used only within the same request for deduplication.', 'leadite' );
        echo '<br>';
        esc_html_e( 'Developers can override this setting with define( \'leadite_ANONYMIZE_IP\', false ) in wp-config.php.', 'leadite' );
        echo '</p>';
        echo '</td></tr>';
        // ———— Data Retention (auto-purge) ————————————————————————————————————————————————————————————
        $ap_last = get_option( 'leadite_autopurge_last', array() );
        echo '<tr><th>' . esc_html__( 'Data Retention', 'leadite' ) . '</th><td>';
        printf( '<label><input type="checkbox" id="autopurge_enabled" name="autopurge_enabled" value="1" %s> %s</label>', checked( !empty( $cfg['autopurge_enabled'] ), true, false ), esc_html__( 'Automatically delete leads older than', 'leadite' ) );
        printf( ' <input type="number" id="autopurge_days" name="autopurge_days" min="7" max="3650" step="1" value="%s" class="small-text"> %s', esc_attr( (int) ($cfg['autopurge_days'] ?? 365) ), esc_html__( 'days', 'leadite' ) );
        echo '<br><label style="margin-top:6px;display:inline-block;"><input type="checkbox" name="autopurge_only_closed" value="1" ' . checked( !empty( $cfg['autopurge_only_closed'] ), true, false ) . '> ';
        echo esc_html__( 'Only purge leads with status Closed or Spam (recommended)', 'leadite' ) . '</label>';
        echo '<p class="description">';
        esc_html_e( 'Runs once a day via WP-Cron. Deleted leads are removed with the full cascade: submitted fields, notes, activity log, tags and attachment files. This implements a site-wide data-retention policy and complements the per-person GDPR erasure in Tools → Erase Personal Data.', 'leadite' );
        if ( !empty( $ap_last['time'] ) ) {
            echo '<br>';
            printf( 
                /* translators: 1: date/time of the last purge run, 2: number of deleted leads */
                esc_html__( 'Last run: %1$s — %2$d lead(s) deleted.', 'leadite' ),
                esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), (int) $ap_last['time'] ) ),
                (int) ($ap_last['deleted'] ?? 0)
             );
        }
        echo '</p>';
        echo '</td></tr>';
        echo '</table>';
        // ———— Batch IP Anonymization ————————————————————————————————————————————————————————————————————————
        echo '<div style="margin-top:24px; padding:16px 20px; background:#fff8e1; border:1px solid #ffe082; border-radius:8px; max-width:600px;">';
        echo '<h3 style="margin-top:0; font-size:14px;">' . esc_html__( 'Anonymize stored IP addresses', 'leadite' ) . '</h3>';
        echo '<p class="description" style="margin-bottom:12px;">' . esc_html__( 'Existing lead records still contain the raw IPs that were stored before this setting was enabled. Use this button to retroactively anonymize all stored IP addresses. This action is permanent and cannot be undone.', 'leadite' ) . '</p>';
        printf( '<button type="button" id="leadite-batch-anonymize-btn" class="button" data-nonce="%s">%s</button>', esc_attr( wp_create_nonce( 'leadite_batch_anonymize' ) ), esc_html__( 'Anonymize existing stored IPs', 'leadite' ) );
        echo '<span id="leadite-batch-anonymize-status" style="margin-left:12px; font-style:italic;"></span>';
        echo '</div>';
        // ———— Role Access ——————————————————————————————————————————————————————————————————————————————
        $this->render_role_access_section( $cfg );
    }

    /**
     * Render the Role Access management section in Settings > General.
     *
     * Shows all registered WP roles (except administrator, subscriber, customer)
     * and lets the admin set 'No access', 'View only', or 'Full manage' per role.
     *
     * @param array $cfg Plugin configuration.
     */
    protected function render_role_access_section( array $cfg ) {
        global $wp_roles;
        if ( !isset( $wp_roles ) ) {
            $wp_roles = new \WP_Roles();
        }
        // Roles to hide from the UI (always no-access by design).
        $hidden_roles = array('administrator', 'subscriber', 'customer');
        // Build list of manageable roles.
        $roles_to_show = array();
        foreach ( $wp_roles->role_names as $slug => $name ) {
            if ( in_array( $slug, $hidden_roles, true ) ) {
                continue;
            }
            $roles_to_show[$slug] = translate_user_role( $name );
        }
        $saved = ( isset( $cfg['role_access'] ) && is_array( $cfg['role_access'] ) ? $cfg['role_access'] : array() );
        ?>
		<div class="leadite-role-access-wrap" style="margin-top:32px;">
			<h2 style="font-size:16px; font-weight:600; margin:0 0 4px; color:var(--leadite-text, #1a1a2e);">
				<?php 
        esc_html_e( 'Role Access', 'leadite' );
        ?>
			</h2>
			<p class="description" style="margin:0 0 16px; color:var(--leadite-text-soft, #6b7280); font-size:13px;">
				<?php 
        esc_html_e( 'Choose which WordPress roles can access Leadite. Changes apply immediately on save.', 'leadite' );
        ?>
			</p>

			<?php 
        if ( empty( $roles_to_show ) ) {
            ?>
				<p class="description"><?php 
            esc_html_e( 'No configurable roles found on this site.', 'leadite' );
            ?></p>
			<?php 
        } else {
            ?>

			<div class="leadite-role-access-card" style="
				background: var(--leadite-bg-card, #fff);
				border: 1px solid var(--leadite-border, #e5e7eb);
				border-radius: 10px;
				overflow: hidden;
				max-width: 680px;
			">
				<?php 
            // Header row.
            echo '<div style="display:grid; grid-template-columns:1fr repeat(3,120px); gap:0; background:var(--leadite-bg-warm,#f8fafc); border-bottom:1px solid var(--leadite-border,#e5e7eb); padding:10px 20px; font-size:12px; font-weight:600; color:var(--leadite-text-soft,#6b7280); text-transform:uppercase; letter-spacing:.04em;">';
            echo '<span>' . esc_html__( 'Role', 'leadite' ) . '</span>';
            echo '<span style="text-align:center">' . esc_html__( 'No access', 'leadite' ) . '</span>';
            echo '<span style="text-align:center">' . esc_html__( 'View only', 'leadite' ) . '</span>';
            echo '<span style="text-align:center">' . esc_html__( 'Full manage', 'leadite' ) . '</span>';
            echo '</div>';
            $row_idx = 0;
            foreach ( $roles_to_show as $slug => $display_name ) {
                $current_level = ( isset( $saved[$slug] ) ? $saved[$slug] : '' );
                $row_bg = ( $row_idx % 2 === 0 ? '' : 'background:var(--leadite-bg-warm,#f8fafc);' );
                $row_idx++;
                echo '<div style="display:grid; grid-template-columns:1fr repeat(3,120px); gap:0; padding:12px 20px; border-bottom:1px solid var(--leadite-border-light,#f0f0f0); align-items:center; ' . esc_attr( $row_bg ) . '">';
                // Role name.
                echo '<span style="font-size:14px; font-weight:500; color:var(--leadite-text,#1a1a2e);">';
                echo '<code style="background:var(--leadite-bg-warm,#f1f5f9); padding:2px 6px; border-radius:4px; font-size:11px; color:var(--leadite-text-soft,#6b7280); margin-right:6px;">' . esc_html( $slug ) . '</code>';
                echo esc_html( $display_name );
                echo '</span>';
                // Radio: No access.
                printf(
                    '<span style="text-align:center"><input type="radio" name="role_access[%s]" value="" id="ra_%s_none" %s style="accent-color:var(--leadite-primary,#6366f1);"><label for="ra_%s_none" class="screen-reader-text">%s</label></span>',
                    esc_attr( $slug ),
                    esc_attr( $slug ),
                    checked( $current_level, '', false ),
                    esc_attr( $slug ),
                    esc_html__( 'No access', 'leadite' )
                );
                // Radio: View only.
                printf(
                    '<span style="text-align:center"><input type="radio" name="role_access[%s]" value="view" id="ra_%s_view" %s style="accent-color:var(--leadite-primary,#6366f1);"><label for="ra_%s_view" class="screen-reader-text">%s</label></span>',
                    esc_attr( $slug ),
                    esc_attr( $slug ),
                    checked( $current_level, 'view', false ),
                    esc_attr( $slug ),
                    esc_html__( 'View only', 'leadite' )
                );
                // Radio: Full manage.
                printf(
                    '<span style="text-align:center"><input type="radio" name="role_access[%s]" value="manage" id="ra_%s_manage" %s style="accent-color:var(--leadite-primary,#6366f1);"><label for="ra_%s_manage" class="screen-reader-text">%s</label></span>',
                    esc_attr( $slug ),
                    esc_attr( $slug ),
                    checked( $current_level, 'manage', false ),
                    esc_attr( $slug ),
                    esc_html__( 'Full manage', 'leadite' )
                );
                echo '</div>';
            }
            // Administrator — always full (read-only notice row).
            echo '<div style="display:grid; grid-template-columns:1fr auto; gap:0; padding:12px 20px; background:var(--leadite-bg-warm,#f0f7ff); border-top:1px solid var(--leadite-border,#e5e7eb); align-items:center;">';
            echo '<span style="font-size:13px; color:var(--leadite-text-soft,#6b7280);">';
            echo '<code style="background:rgba(99,102,241,.08); padding:2px 6px; border-radius:4px; font-size:11px; margin-right:6px;">administrator</code>';
            echo esc_html__( 'Administrator', 'leadite' );
            echo '</span>';
            echo '<span style="font-size:12px; font-weight:600; color:#16a34a; padding:3px 10px; background:#dcfce7; border-radius:20px;">' . esc_html__( 'Always: Full manage', 'leadite' ) . '</span>';
            echo '</div>';
            ?>
			</div>

			<p class="description" style="margin-top:10px; font-size:12px;">
				<strong><?php 
            esc_html_e( 'View only:', 'leadite' );
            ?></strong>
				<?php 
            esc_html_e( 'Can browse leads and open lead details, but cannot change status, assign, add notes, export or delete.', 'leadite' );
            ?>
				<br>
				<strong><?php 
            esc_html_e( 'Full manage:', 'leadite' );
            ?></strong>
				<?php 
            esc_html_e( 'Full access identical to administrator: can change status, assign leads, add notes, export and delete.', 'leadite' );
            ?>
			</p>

			<?php 
        }
        ?>
		</div>
		<?php 
    }

    /**
     * Render the 'Google Sheets' settings tab.
     *
     * @param array $cfg Plugin configuration.
     */
    protected function render_google_sheets_tab( $cfg ) {
        // Free build: teaser only — the integration ships in Pro.
        echo '<div class="leadite-settings-section leadite-field-locked">';
        echo '<h3 class="leadite-settings-section-title">' . esc_html__( 'Google Sheets Sync', 'leadite' ) . ' <span class="leadite-badge leadite-badge--pro">Pro</span></h3>';
        echo '<p class="leadite-pro-section-hint">' . esc_html__( 'Push every captured lead to a Google Sheet in real time via a simple Apps Script web app — a shared, always-up-to-date list for your team. Available in Leadite Pro.', 'leadite' ) . '</p>';
        echo '<p><a class="button button-primary" href="' . esc_url( \leadite_get_upgrade_url() ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Get Pro →', 'leadite' ) . '</a></p>';
        echo '</div>';
    }

    /**
     * Render the 'Pro Integrations' settings tab.
     *
     * @param array $cfg Plugin configuration.
     */
    protected function render_pro_integrations_tab( $cfg ) {
        $pro_active = Pro::is_active();
        $dis = ( $pro_active ? '' : 'disabled="disabled"' );
        // phpcs:ignore -- safe HTML boolean attribute
        $dis_cls = ( $pro_active ? '' : ' leadite-field-locked' );
        // SVG icon set for the Pro settings tab.
        $lh_icons = array(
            'lock'     => '<svg class="leadite-pro-notice-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>',
            'star'     => '<svg class="leadite-feat-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
            'user'     => '<svg class="leadite-feat-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
            'note'     => '<svg class="leadite-feat-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>',
            'clock'    => '<svg class="leadite-feat-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
            'tag'      => '<svg class="leadite-feat-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>',
            'telegram' => '<svg class="leadite-feat-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg>',
            'webhook'  => '<svg class="leadite-feat-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>',
            'palette'  => '<svg class="leadite-feat-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="13.5" cy="6.5" r=".5"/><circle cx="17.5" cy="10.5" r=".5"/><circle cx="8.5" cy="7.5" r=".5"/><circle cx="6.5" cy="12.5" r=".5"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/></svg>',
        );
        echo '<div class="leadite-pro-settings-container">';
        // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- $dis is hardcoded 'disabled="disabled"' or empty, not user input
        // Pro-not-active notice banner at top.
        if ( !$pro_active ) {
            echo '<div class="leadite-pro-tab-notice">';
            echo '<span class="leadite-pro-tab-notice__icon">' . $lh_icons['lock'] . '</span>';
            // phpcs:ignore WordPress.Security.EscapeOutput
            echo '<div class="leadite-pro-tab-notice__body">';
            echo '<strong>' . esc_html__( 'Leadite Pro required', 'leadite' ) . '</strong> — ';
            echo esc_html__( 'All settings below are available in Pro. You can explore the fields now so you know what to prepare. Saving without a Pro license has no effect.', 'leadite' );
            echo ' <a href="' . esc_url( \leadite_get_upgrade_url() ) . '" target="_blank" rel="noopener noreferrer"><strong>' . esc_html__( 'Get Pro →', 'leadite' ) . '</strong></a>';
            echo '</div>';
            echo '</div>';
        }
        // f-pro-end
        // f-pro-free-start
        if ( !$pro_active ) {
            echo '<div class="leadite-settings-section leadite-field-locked">';
            echo '<h3 class="leadite-settings-section-title">' . esc_html__( 'Telegram Alerts', 'leadite' ) . ' <span class="leadite-badge leadite-badge--pro">Pro</span></h3>';
            echo '<p class="leadite-pro-section-hint">' . esc_html__( 'You will need: a Telegram bot token (from @BotFather) and a chat ID (from @userinfobot).', 'leadite' ) . '</p>';
            echo '<table class="form-table leadite-settings-table">';
            echo '<tr><th>' . esc_html__( 'Enable Telegram', 'leadite' ) . '</th>';
            echo '<td><label><input type="checkbox" disabled> ' . esc_html__( 'Send instant lead alerts to Telegram', 'leadite' ) . '</label></td></tr>';
            echo '<tr><th><label>' . esc_html__( 'Bot Token', 'leadite' ) . '</label></th>';
            echo '<td><span class="leadite-secret-field-wrap"><input type="password" class="regular-text leadite-field-locked" disabled value="********"><button type="button" class="leadite-secret-reveal button" disabled>&#128065;</button></span>';
            echo '</td></tr>';
            echo '</table>';
            echo '</div>';
        }
        // f-pro-end
        // f-pro-free-start
        if ( !$pro_active ) {
            echo '<div class="leadite-settings-section leadite-field-locked">';
            echo '<h3 class="leadite-settings-section-title">' . esc_html__( 'Outbound Webhooks', 'leadite' ) . ' <span class="leadite-badge leadite-badge--pro">Pro</span></h3>';
            echo '<p class="leadite-pro-section-hint">' . esc_html__( 'You will need: a public HTTPS endpoint URL (e.g. from Make, Zapier, or n8n).', 'leadite' ) . '</p>';
            echo '<table class="form-table leadite-settings-table">';
            echo '<tr><th>' . esc_html__( 'Enable Webhook', 'leadite' ) . '</th>';
            echo '<td><label><input type="checkbox" disabled> ' . esc_html__( 'POST JSON payload on form submission', 'leadite' ) . '</label></td></tr>';
            echo '</table>';
            echo '</div>';
        }
        // f-pro-end
        // f-pro-free-start
        if ( !$pro_active ) {
            echo '<div class="leadite-settings-section leadite-settings-section--last leadite-field-locked">';
            echo '<h3 class="leadite-settings-section-title">' . esc_html__( 'Agency White-Labeling', 'leadite' ) . ' <span class="leadite-badge leadite-badge--pro">Pro</span></h3>';
            echo '<p class="leadite-pro-section-hint">' . esc_html__( 'You will need: your agency or product brand name to replace "Leadite".', 'leadite' ) . '</p>';
            echo '<table class="form-table leadite-settings-table">';
            echo '<tr><th><label>' . esc_html__( 'Brand name override', 'leadite' ) . '</label></th>';
            echo '<td><input type="text" class="regular-text leadite-field-locked" disabled value="' . esc_attr( $cfg['brand_name'] ) . '"></td></tr>';
            echo '</table>';
            echo '</div>';
        }
        // f-pro-free-end
        // ———— Tags management ——————————————————————————————————————————————————————————————————————————————————————
        echo '<div class="leadite-settings-section' . esc_attr( $dis_cls ) . '">';
        echo '<h3 class="leadite-settings-section-title">' . esc_html__( 'Tags & Segments', 'leadite' ) . ' <span class="leadite-badge leadite-badge--pro">Pro</span></h3>';
        if ( !$pro_active ) {
            echo '<p class="leadite-pro-section-hint">' . esc_html__( 'Create colour-coded tags and assign them to individual leads from the detail page. Requires Pro.', 'leadite' ) . '</p>';
        }
        echo '<p class="description" style="margin-bottom:16px;">' . esc_html__( 'Create and manage global tags. Assign them to individual leads from the lead detail view.', 'leadite' ) . '</p>';
        // f-pro-end
        // f-pro-free-start
        if ( !$pro_active ) {
            // Locked teaser — shows what the section looks like, inputs disabled.
            echo '<div class="leadite-tags-create-settings">';
            printf( '<input type="text" class="regular-text leadite-new-tag-name-settings" placeholder="%s" maxlength="100" disabled>', esc_attr__( 'New tag name—…', 'leadite' ) );
            echo '<input type="color" class="leadite-new-tag-color-settings" value="#6366f1" disabled>';
            printf( '<button type="button" class="button" disabled>%s</button>', esc_html__( 'Create tag', 'leadite' ) );
            echo '</div>';
        }
        // f-pro-free-end
        echo '</div>';
        // leadite-settings-section
        // ———— Pro feature highlights (always visible when Pro active) ——————
        echo '<div class="leadite-settings-section leadite-settings-section--last">';
        echo '<h3 class="leadite-settings-section-title">' . esc_html__( 'Other Pro features', 'leadite' ) . ' <span class="leadite-badge leadite-badge--pro">Pro</span></h3>';
        echo '<p class="description" style="margin-bottom:12px;">' . esc_html__( 'These features are active on every lead detail page when Pro is enabled:', 'leadite' ) . '</p>';
        echo '<ul class="leadite-pro-features-list">';
        printf(
            '<li><span class="leadite-feat-icon-wrap">%s</span><span><strong>%s</strong> — %s</span></li>',
            $lh_icons['star'],
            esc_html__( 'Starred leads', 'leadite' ),
            esc_html__( 'Mark important leads with a star for quick access from the list view.', 'leadite' )
        );
        // phpcs:ignore WordPress.Security.EscapeOutput
        printf(
            '<li><span class="leadite-feat-icon-wrap">%s</span><span><strong>%s</strong> — %s</span></li>',
            $lh_icons['user'],
            esc_html__( 'Manager assignment', 'leadite' ),
            esc_html__( 'Assign any lead to a team member. They see their queue in the list view filter.', 'leadite' )
        );
        // phpcs:ignore WordPress.Security.EscapeOutput
        printf(
            '<li><span class="leadite-feat-icon-wrap">%s</span><span><strong>%s</strong> — %s</span></li>',
            $lh_icons['note'],
            esc_html__( 'Internal notes', 'leadite' ),
            esc_html__( 'Leave private comments on leads, visible only to your team.', 'leadite' )
        );
        // phpcs:ignore WordPress.Security.EscapeOutput
        printf(
            '<li><span class="leadite-feat-icon-wrap">%s</span><span><strong>%s</strong> — %s</span></li>',
            $lh_icons['clock'],
            esc_html__( 'Activity timeline', 'leadite' ),
            esc_html__( 'Full audit log of status changes, assignments, downloads and notes per lead.', 'leadite' )
        );
        // phpcs:ignore WordPress.Security.EscapeOutput
        echo '</ul>';
        echo '</div>';
        // leadite-settings-section
        // ———— WP-CLI (Pro) ———————————————————————————————————————————————————————————————————————————
        $cli_cmds = array(
            'wp leadite list --status=new --limit=50'           => __( 'List leads in the terminal (filters: status, source, search; formats: table, json, csv, ids).', 'leadite' ),
            'wp leadite export --format=csv --output=leads.csv' => __( 'Export leads to CSV or JSON — perfect for backups and cron pipelines.', 'leadite' ),
            'wp leadite stats'                                  => __( 'Status counts and KPIs at a glance.', 'leadite' ),
            'wp leadite purge --days=180 --dry-run'             => __( 'Run the data-retention purge manually, with a safe dry-run mode.', 'leadite' ),
            'wp leadite delete 15 16 17 --yes'                  => __( 'Delete specific leads with the full cascade (fields, notes, log, tags, files).', 'leadite' ),
        );
        // f-pro-end
        // f-pro-free-start
        if ( !$pro_active ) {
            echo '<div class="leadite-settings-section leadite-field-locked">';
            echo '<h3 class="leadite-settings-section-title">' . esc_html__( 'WP-CLI Commands', 'leadite' ) . ' <span class="leadite-badge leadite-badge--pro">Pro</span></h3>';
            echo '<p class="leadite-pro-section-hint">' . esc_html__( 'Manage leads from the terminal — ideal for agencies, automation, backups and cron pipelines. Pro unlocks the "wp leadite" command family:', 'leadite' ) . '</p>';
            echo '<table class="form-table leadite-settings-table">';
            foreach ( $cli_cmds as $cmd => $desc ) {
                echo '<tr><th style="width:44%"><code>' . esc_html( $cmd ) . '</code></th><td>' . esc_html( $desc ) . '</td></tr>';
            }
            echo '</table>';
            echo '</div>';
        }
        // f-pro-free-end
        echo '</div>';
        // pro container
    }

}
