/**
 * Consolety Network JavaScript
 *
 * @package Consolety
 * @since 1.0.0
 */

/* global jQuery, consoletyNetwork, consoletyData */

(function($) {
    'use strict';

    // Use dedicated localized data, fallback to legacy for backward compatibility
    const VPN = typeof consoletyNetwork !== 'undefined' ? consoletyNetwork : {
        ajaxUrl: consoletyData.ajaxUrl,
        nonce: consoletyData.nonce,
        i18n: {}
    };

    let currentTab = 'discover';
    let partnersData = [];
    let connectionsData = { sent: [], received: [], connected: [] };

    /**
     * Handle AJAX errors with smart diagnostics
     * Detects nonce failures (403 + "-1" response) and auto-reloads (once only)
     */
    function handleAjaxError(xhr, status, error, context) {
        const responseText = (xhr.responseText || '').trim();
        const httpStatus = xhr.status;

        console.error('[Consolety] AJAX Error:', {
            context: context,
            action: 'consolety_*',
            httpStatus: httpStatus,
            status: status,
            error: error,
            responseText: responseText.substring(0, 200)
        });

        // Strict nonce failure detection: WP returns exactly "-1" or "0" with 403
        // Must be BOTH 403 AND the specific WP nonce failure response
        const isNonceFailure = httpStatus === 403 && (responseText === '-1' || responseText === '0');

        if (isNonceFailure) {
            // Prevent reload loops: only reload once per page session
            const reloadKey = 'vp_nonce_reload_' + window.location.href;
            if (sessionStorage.getItem(reloadKey)) {
                console.error('[Consolety] Nonce failure persists after reload. Manual refresh needed.');
                showNotice(VPN.i18n.sessionExpiredManual || 'Session expired. Please refresh the page manually.', 'error');
                return true;
            }

            console.warn('[Consolety] Nonce/session failure detected. Reloading page (once)...');
            sessionStorage.setItem(reloadKey, '1');
            showNotice(VPN.i18n.sessionExpired || 'Session expired. Reloading page...', 'warning');
            setTimeout(function() {
                window.location.reload();
            }, 1500);
            return true; // Handled
        }

        // Clear reload guard on successful page (non-nonce errors don't count)
        sessionStorage.removeItem('vp_nonce_reload_' + window.location.href);

        // Generic network error (not nonce-related)
        showNotice((VPN.i18n.networkError || 'Network error') + ': ' + escapeHtml(error), 'error');
        return false;
    }

    /**
     * Show a temporary notice to the user
     */
    function showNotice(message, type) {
        let noticeClass = 'notice-warning';
        if (type === 'error') noticeClass = 'notice-error';
        if (type === 'success') noticeClass = 'notice-success';
        const $notice = $('<div class="notice ' + noticeClass + ' is-dismissible vp-ajax-notice"><p>' + escapeHtml(message) + '</p></div>');

        // Remove any existing VP notices
        $('.vp-ajax-notice').remove();

        // Insert at top of content area
        $('.vp-dashboard .vp-header').after($notice);

        // Auto-dismiss after 5 seconds (unless it's a reload notice)
        if (type !== 'warning') {
            setTimeout(function() {
                $notice.fadeOut(function() { $(this).remove(); });
            }, 5000);
        }
    }

    /**
     * Show a custom confirm modal (replaces native confirm() which can be blocked)
     * @param {Object} options - Modal options
     * @param {string} options.title - Modal title
     * @param {string} options.message - Confirmation message
     * @param {string} options.confirmText - Confirm button text (default: 'Confirm')
     * @param {string} options.cancelText - Cancel button text (default: 'Cancel')
     * @param {string} options.confirmClass - Confirm button class (default: 'vp-btn-primary')
     * @param {Function} options.onConfirm - Callback when confirmed
     * @param {Function} options.onCancel - Callback when cancelled (optional)
     */
    function showConfirmModal(options) {
        // Remove any existing confirm modal
        $('#vp-confirm-modal').remove();

        const title = options.title || VPN.i18n.confirm || 'Confirm';
        const message = options.message || VPN.i18n.areYouSure || 'Are you sure?';
        const confirmText = options.confirmText || VPN.i18n.confirm || 'Confirm';
        const cancelText = options.cancelText || VPN.i18n.cancel || 'Cancel';
        const confirmClass = options.confirmClass || 'vp-btn-primary';

        const modalHtml = '<div id="vp-confirm-modal" class="vp-modal-overlay" style="display:flex;">' +
            '<div class="vp-modal vp-confirm-modal" style="max-width:400px;">' +
                '<div class="vp-modal-header">' +
                    '<h2><span class="dashicons dashicons-warning" style="color:#f59e0b;margin-right:8px;"></span>' + escapeHtml(title) + '</h2>' +
                    '<button type="button" class="vp-modal-close" id="vp-confirm-close">' +
                        '<span class="dashicons dashicons-no-alt"></span>' +
                    '</button>' +
                '</div>' +
                '<div class="vp-modal-body" style="padding:20px;">' +
                    '<p style="margin:0;font-size:14px;color:#374151;">' + escapeHtml(message) + '</p>' +
                '</div>' +
                '<div class="vp-modal-footer" style="display:flex;gap:12px;justify-content:flex-end;padding:16px 20px;border-top:1px solid #e5e7eb;">' +
                    '<button type="button" class="vp-btn vp-btn-ghost" id="vp-confirm-cancel">' + escapeHtml(cancelText) + '</button>' +
                    '<button type="button" class="vp-btn ' + confirmClass + '" id="vp-confirm-ok">' + escapeHtml(confirmText) + '</button>' +
                '</div>' +
            '</div>' +
        '</div>';

        $('body').append(modalHtml);

        const $modal = $('#vp-confirm-modal');

        // Handle confirm
        $('#vp-confirm-ok').on('click', function() {
            $modal.remove();
            if (typeof options.onConfirm === 'function') {
                options.onConfirm();
            }
        });

        // Handle cancel (close button, cancel button, or overlay click)
        function closeModal() {
            $modal.remove();
            if (typeof options.onCancel === 'function') {
                options.onCancel();
            }
        }

        $('#vp-confirm-cancel, #vp-confirm-close').on('click', closeModal);
        $modal.on('click', function(e) {
            if (e.target === this) {
                closeModal();
            }
        });

        // Handle Escape key
        $(document).on('keydown.vpConfirm', function(e) {
            if (e.key === 'Escape') {
                closeModal();
                $(document).off('keydown.vpConfirm');
            }
        });

        // Focus the confirm button
        $('#vp-confirm-ok').focus();
    }

    // Initialize
    $(document).ready(function() {
        loadCategories();
        loadPartners();
        loadConnections();

        // Tab switching
        $('.vp-tab[data-tab]').on('click', function() {
            const tab = $(this).data('tab');
            if (!tab) return;

            $('.vp-tab').removeClass('active');
            $(this).addClass('active');
            $('.vp-tab-content').hide();
            $('#tab-' + tab).show();
            currentTab = tab;
        });

        // Search with debounce
        let searchTimeout;
        $('#network-search').on('keyup', function() {
            clearTimeout(searchTimeout);
            searchTimeout = setTimeout(loadPartners, 300);
        });

        // Language filter
        $('#filter-language').on('change', loadPartners);

        // Category filter -> populate subcategory dependent dropdown
        $('#filter-category').on('change', function() {
            const parentId = $(this).val();
            populateSubcategories(parentId);
            loadPartners();
        });

        // Subcategory filter
        $('#filter-subcategory').on('change', loadPartners);

        // Hide sensitive toggle (v1.0.29: styled button with active state)
        $('#filter-hide-sensitive').on('change', function() {
            const $toggle = $('#vp-sensitive-toggle');
            if ($(this).is(':checked')) {
                $toggle.addClass('active');
            } else {
                $toggle.removeClass('active');
            }
            loadPartners();
        });

        // Clear all filters
        $(document).on('click', '#vp-clear-all-filters', function() {
            $('#network-search').val('');
            $('#filter-category').val('');
            $('#filter-subcategory').val('').prop('disabled', true);
            $('#filter-language').val('');
            $('#filter-hide-sensitive').prop('checked', true);
            loadPartners();
        });

        // Remove individual filter chip
        $(document).on('click', '.vp-chip-remove', function() {
            const filterTarget = $(this).data('filter');
            if (filterTarget === 'category') {
                $('#filter-category').val('');
                $('#filter-subcategory').val('').prop('disabled', true);
            } else if (filterTarget === 'subcategory') {
                $('#filter-subcategory').val('');
            } else if (filterTarget === 'language') {
                $('#filter-language').val('');
            } else if (filterTarget === 'search') {
                $('#network-search').val('');
            } else if (filterTarget === 'sensitive') {
                $('#filter-hide-sensitive').prop('checked', true);
            }
            loadPartners();
        });

        // Connect button click
        $(document).on('click', '.show-interest-btn', function() {
            const siteId = $(this).data('site-id');
            const domain = $(this).data('domain');
            showInterest(siteId, domain, $(this));
        });

        // Accept request
        $(document).on('click', '.accept-btn', function() {
            const connId = $(this).data('id');
            respondToRequest(connId, 'accept', $(this));
        });

        // Reject request
        $(document).on('click', '.reject-btn', function() {
            const connId = $(this).data('id');
            const $btn = $(this);
            showConfirmModal({
                title: VPN.i18n.rejectRequest || 'Reject Request',
                message: VPN.i18n.confirmReject || 'Are you sure you want to reject this connection request?',
                confirmText: VPN.i18n.reject || 'Reject',
                confirmClass: 'vp-btn-danger',
                onConfirm: function() {
                    respondToRequest(connId, 'reject', $btn);
                }
            });
        });

        // Cancel sent request
        $(document).on('click', '.cancel-btn', function(e) {
            e.preventDefault();
            e.stopPropagation();
            const connId = $(this).data('id');
            const $btn = $(this);
            if (!connId) {
                console.error('[Consolety] Cancel failed: No connection ID on button');
                showNotice(VPN.i18n.errorNoConnectionId || 'Error: Connection ID not found. Please refresh the page.', 'error');
                return;
            }
            showConfirmModal({
                title: VPN.i18n.cancelRequest || 'Cancel Request',
                message: VPN.i18n.confirmCancel || 'Cancel this connection request? Your points will be refunded.',
                confirmText: VPN.i18n.confirmCancelRequest || 'Yes, Cancel Request',
                cancelText: VPN.i18n.keepRequest || 'No, Keep It',
                confirmClass: 'vp-btn-warning',
                onConfirm: function() {
                    cancelRequest(connId, $btn);
                }
            });
        });

        // Disconnect
        $(document).on('click', '.disconnect-btn', function() {
            const connId = $(this).data('id');
            const domain = $(this).data('domain');
            const $btn = $(this);
            showConfirmModal({
                title: VPN.i18n.disconnect || 'Disconnect',
                message: (VPN.i18n.confirmDisconnect || 'Disconnect from') + ' ' + domain + '? ' + (VPN.i18n.disconnectWarning || 'You will no longer be able to exchange guest posts.'),
                confirmText: VPN.i18n.disconnect || 'Disconnect',
                confirmClass: 'vp-btn-danger',
                onConfirm: function() {
                    disconnectPartner(connId, $btn);
                }
            });
        });

        // Guest Post button - navigate to guest posts page with URL parameters
        $(document).on('click', '.guest-post-btn', function() {
            const partnerId = $(this).data('partner-id');
            const partnerDomain = $(this).data('partner-domain');
            const partnerClicks = $(this).data('partner-clicks') || 0;

            // Navigate to guest posts page with partner info in URL
            const url = new URL(VPN.guestPostsUrl || window.location.origin + '/wp-admin/admin.php?page=consolety-guest-posts');
            url.searchParams.set('partner_id', partnerId);
            url.searchParams.set('partner_domain', partnerDomain);
            url.searchParams.set('partner_clicks', partnerClicks);
            window.location.href = url.toString();
        });
    });

    // Category data cache (with children for dependent dropdown)
    let categoriesCache = [];

    function loadCategories() {
        $.post(VPN.ajaxUrl, {
            action: 'consolety_get_categories',
            nonce: VPN.nonce
        }, function(response) {
            const select = $('#filter-category');
            select.find('option:not(:first)').remove();

            if (response.success && response.data?.categories) {
                categoriesCache = response.data.categories;
                response.data.categories.forEach(cat => {
                    // Only show top-level (parent) categories in main dropdown
                    if (!cat.parent_id) {
                        const isSensitive = cat.is_sensitive ? ' (18+)' : '';
                        select.append('<option value="' + cat.id + '">' + escapeHtml(cat.name) + isSensitive + '</option>');
                    }
                });
            }
        }).fail(function(xhr, status, error) {
            handleAjaxError(xhr, status, error, 'loadCategories');
        });
    }

    // Populate subcategory dropdown based on selected main category
    function populateSubcategories(parentId) {
        const subSelect = $('#filter-subcategory');
        subSelect.empty().append('<option value="">' + (VPN.i18n.allSubcategories || 'All Subcategories') + '</option>');

        if (!parentId) {
            subSelect.find('option:first').text(VPN.i18n.selectMainFirst || 'Select a main category first');
            subSelect.prop('disabled', true).val('');
            $('#subcategory-helper').text(VPN.i18n.selectMainFirst || 'Select a main category first').show();
            return;
        }

        // Find children of selected parent (from cache or from children array)
        const parent = categoriesCache.find(c => c.id == parentId);
        let children = [];

        if (parent && parent.children && parent.children.length) {
            children = parent.children;
        } else {
            // Fallback: find categories with matching parent_id
            children = categoriesCache.filter(c => c.parent_id == parentId);
        }

        if (children.length > 0) {
            subSelect.prop('disabled', false);
            $('#subcategory-helper').hide();
            children.forEach(sub => {
                const isSensitive = sub.is_sensitive ? ' (18+)' : '';
                subSelect.append('<option value="' + sub.id + '">' + escapeHtml(sub.name) + isSensitive + '</option>');
            });
        } else {
            subSelect.find('option:first').text(VPN.i18n.noSubcategories || 'No subcategories available');
            subSelect.prop('disabled', true);
            $('#subcategory-helper').text(VPN.i18n.noSubcategories || 'No subcategories available').show();
        }
    }

    function loadPartners() {
        const grid = $('#network-grid');
        grid.html('<div class="vp-loading-state"><span class="spinner is-active"></span><span>' + (VPN.i18n.loading || 'Loading partners...') + '</span></div>');

        const categoryId = $('#filter-category').val();
        const subcategoryId = $('#filter-subcategory').val();

        $.post(VPN.ajaxUrl, {
            action: 'consolety_fetch_network_partners',
            nonce: VPN.nonce,
            search: $('#network-search').val(),
            language: $('#filter-language').val(),
            category_id: subcategoryId || categoryId,
            hide_sensitive: $('#filter-hide-sensitive').is(':checked')
        }, function(response) {
            if (response.success) {
                partnersData = response.data.partners || [];
                populateLanguageFilter(response.data.languages || []);
                renderPartners();
                updateFilterChips();
            } else {
                console.error('[Consolety] Failed to load partners:', response);
                grid.html('<div class="vp-empty-state"><div class="vp-empty-icon"><span class="dashicons dashicons-dismiss" style="font-size:48px;width:48px;height:48px;color:var(--vp-danger);"></span></div><p>' + escapeHtml(response.data?.message || VPN.i18n.failedToLoadPartners || 'Failed to load partners') + '</p></div>');
            }
        }).fail(function(xhr, status, error) {
            if (!handleAjaxError(xhr, status, error, 'loadPartners')) {
                grid.html('<div class="vp-empty-state"><div class="vp-empty-icon"><span class="dashicons dashicons-dismiss" style="font-size:48px;width:48px;height:48px;color:var(--vp-danger);"></span></div><p>Network error: ' + escapeHtml(error) + '</p></div>');
            }
        });
    }

    // Native language name map - sourced from WordPress translations via PHP (200+ languages)
    const langNativeNames = (VPN && VPN.langNames) ? VPN.langNames : {};

    function getLanguageLabel(code) {
        const c = (code || '').toLowerCase();
        const native = langNativeNames[c] || c.toUpperCase();
        return native + ' (' + c.toUpperCase() + ')';
    }

    function populateLanguageFilter(languages) {
        const select = $('#filter-language');
        const current = select.val();

        select.find('option:not(:first)').remove();

        languages.forEach(lang => {
            const label = getLanguageLabel(lang);
            const selected = lang === current ? ' selected' : '';
            select.append('<option value="' + lang + '"' + selected + '>' + escapeHtml(label) + '</option>');
        });
    }

    // Update active filter chips based on current filter state
    function updateFilterChips() {
        const chips = [];
        const search = $('#network-search').val();
        const category = $('#filter-category').val();
        const subcategory = $('#filter-subcategory').val();
        const language = $('#filter-language').val();
        const hideSensitive = $('#filter-hide-sensitive').is(':checked');

        if (search) {
            chips.push({ label: '"' + escapeHtml(search) + '"', filter: 'search' });
        }
        if (category) {
            const catText = $('#filter-category option:selected').text();
            chips.push({ label: escapeHtml(catText), filter: 'category' });
        }
        if (subcategory) {
            const subText = $('#filter-subcategory option:selected').text();
            chips.push({ label: escapeHtml(subText), filter: 'subcategory' });
        }
        if (language) {
            chips.push({ label: escapeHtml(getLanguageLabel(language)), filter: 'language' });
        }
        if (!hideSensitive) {
            chips.push({ label: VPN.i18n.includingSensitive || 'Including sensitive', filter: 'sensitive' });
        }

        const $container = $('#vp-active-filters');
        const $chipsEl = $('#vp-filter-chips');

        if (chips.length === 0) {
            $container.hide();
            return;
        }

        $chipsEl.html(chips.map(c =>
            '<span class="vp-filter-chip">' + c.label +
            ' <span class="vp-chip-remove" data-filter="' + c.filter + '">&times;</span></span>'
        ).join(''));
        $container.show();
    }

    function renderPartners() {
        const grid = $('#network-grid');
        const countText = $('#vp-partner-count-text');

        if (!partnersData.length) {
            countText.text('');
            grid.html(`
                <div class="vp-empty-state">
                    <div class="vp-empty-icon"><span class="dashicons dashicons-search" style="font-size:48px;width:48px;height:48px;color:var(--vp-text-muted);"></span></div>
                    <h4>${VPN.i18n.noPartnersFound || 'No partners found'}</h4>
                    <p>${VPN.i18n.tryAdjusting || 'Try adjusting your filters or check back later.'}</p>
                </div>
            `);
            return;
        }

        countText.text((VPN.i18n.showing || 'Showing') + ' ' + partnersData.length + ' ' + (VPN.i18n.partners || 'partners'));
        grid.html(partnersData.map(partner => renderPartnerCard(partner)).join(''));
    }

    function renderPartnerCard(partner, showDisconnect = false) {
        // Language badge: native name + ISO code, no flags (accessibility best practice)
        const rawLang = (partner.language || '').toLowerCase().replace(/[^a-z]/g, '').substring(0, 2);
        const langBadge = rawLang ? `<span class="vp-badge vp-badge-lang">${escapeHtml(getLanguageLabel(rawLang))}</span>` : '';
        const categoryText = partner.category ? `${partner.category.icon || ''} ${escapeHtml(partner.category.name)}` : '';
        const bioPreview = partner.bio ? partner.bio.substring(0, 80) + (partner.bio.length > 80 ? '...' : '') : '';

        // "Why Matched" badges: show why this partner is relevant
        let matchBadges = '';
        const mySite = VPN.mySite || {};
        if (mySite.language && rawLang && mySite.language.toLowerCase() === rawLang) {
            matchBadges += '<span class="vp-badge-match vp-badge-green"><span class="dashicons dashicons-translation"></span> Language match</span>';
        }
        if (mySite.subcategory_id && partner.subcategory && partner.subcategory.id == mySite.subcategory_id) {
            matchBadges += '<span class="vp-badge-match vp-badge-blue"><span class="dashicons dashicons-tag"></span> Exact subniche</span>';
        } else if (mySite.category_id && partner.category && partner.category.id == mySite.category_id) {
            matchBadges += '<span class="vp-badge-match vp-badge-green"><span class="dashicons dashicons-category"></span> Niche match</span>';
        }
        if (partner.is_sensitive_niche) {
            matchBadges += '<span class="vp-badge-match vp-badge-orange"><span class="dashicons dashicons-warning"></span> Sensitive</span>';
        }

        // Check if connected
        const isConnected = partner.connection_status === 'connected' || partner.connection_status === 'accepted';
        const isDisconnected = partner.connection_status === 'disconnected' && partner.is_in_grace_period;

        // Domain display - always show full domain for transparency
        const domainDisplay = `<span class="vp-partner-domain">${escapeHtml(partner.domain)}</span>`;

        // Traffic stats - VERIFIED GSC data (what makes Consolety unique!)
        const clicks = partner.monthly_clicks || 0;
        const impressions = partner.monthly_impressions || 0;

        // Requirement tags (pills only, note extracted separately)
        const requirementPillsHtml = renderRequirementPills(partner.requirements);
        const noteHtml = renderPartnerNote(partner.requirements);

        // Visit button - always show so users can evaluate the site before connecting
        // SECURITY: Use escapeAttr for href and other attributes
        const visitBtn = `<a href="https://${escapeAttr(partner.domain)}" target="_blank" rel="noopener" class="vp-btn vp-btn-ghost vp-btn-visit" title="${VPN.i18n.visitSite || 'Visit Site'}">
                <span class="dashicons dashicons-external"></span>
            </a>`;

        // Status badge (shown at top of actions column)
        let statusBadge = '';
        let actionButtons = '';

        if (showDisconnect) {
            const isInactive = partner.is_active === false;
            statusBadge = '<div class="vp-partner-status"><span class="vp-status-connected"><span class="dashicons dashicons-yes-alt"></span> ' + (VPN.i18n.connected || 'Connected') + '</span></div>';
            actionButtons = `
                <button type="button" class="vp-btn vp-btn-primary guest-post-btn"
                        data-partner-id="${partner.id}"
                        data-partner-domain="${escapeAttr(partner.domain)}"
                        data-partner-clicks="${partner.monthly_clicks || 0}"
                        ${isInactive ? 'disabled title="' + escapeAttr(VPN.i18n.partnerInactive || 'This partner is no longer active') + '"' : ''}>
                    ${VPN.i18n.guestPost || 'Guest Post'}
                </button>
                <button type="button" class="vp-btn vp-btn-ghost vp-btn-disconnect-small disconnect-btn" data-id="${partner.connection_id}" data-domain="${escapeAttr(partner.domain)}">
                    ${VPN.i18n.disconnect || 'Disconnect'}
                </button>
            `;
        } else if (isConnected) {
            statusBadge = '<div class="vp-partner-status"><span class="vp-status-connected"><span class="dashicons dashicons-yes-alt"></span> ' + (VPN.i18n.connected || 'Connected') + '</span></div>';
        } else if (partner.connection_status === 'pending') {
            statusBadge = '<div class="vp-partner-status"><span class="vp-status-pending"><span class="dashicons dashicons-clock"></span> ' + (VPN.i18n.pending || 'Pending') + '</span></div>';
        } else if (isDisconnected) {
            // Grace period: show Reconnect button instead of Connect
            actionButtons = `<button type="button" class="vp-btn vp-btn-secondary show-interest-btn"
                                 data-site-id="${partner.id}"
                                 data-domain="${escapeAttr(partner.domain)}"
                                 data-reconnect="true">
                <span class="dashicons dashicons-update"></span>
                ${VPN.i18n.reconnect || 'Reconnect'}
            </button>`;
        } else {
            // Always disclose what a connection costs BEFORE the click:
            // Free tier pays a refundable deposit, Pro connects free.
            const costNote = VPN.isPro
                ? `<span class="vp-connect-cost">${VPN.i18n.connectFreePro || 'Free on your Pro plan'}</span>`
                : `<span class="vp-connect-cost">${VPN.i18n.connectCost || '5 points - refunded when answered'}</span>`;
            actionButtons = `<button type="button" class="vp-btn vp-btn-primary show-interest-btn" data-site-id="${partner.id}" data-domain="${escapeAttr(partner.domain)}">
                ${VPN.i18n.connect || 'Connect'}
            </button>${costNote}`;
        }

        // New Partner badge (sites < 30 days old)
        const newPartnerBadge = partner.is_new_partner
            ? `<span class="vp-badge-new">${VPN.i18n.newPartner || 'New Partner'}</span>`
            : '';

        // Disconnected info badge (in grace period - reconnect available!)
        let disconnectedBadge = '';
        if (isDisconnected && partner.grace_period_ends_at) {
            const endsAt = new Date(partner.grace_period_ends_at);
            const now = new Date();
            const daysLeft = Math.max(0, Math.ceil((endsAt - now) / (1000 * 60 * 60 * 24)));
            disconnectedBadge = `<span class="vp-badge vp-badge-info">
                <span class="dashicons dashicons-backup"></span>
                ${VPN.i18n.reconnectAvailable || 'Reconnect available'} (${daysLeft}d)
            </span>`;
        }

        // Status badge: Inactive (left network) vs Offline (no recent heartbeat)
        let offlineBadge = '';
        if (partner.is_active === false) {
            offlineBadge = `<span class="vp-badge vp-badge-offline">
                <span class="dashicons dashicons-dismiss"></span>
                ${VPN.i18n.inactive || 'Inactive'}
            </span>`;
        } else if (partner.is_online === false && partner.is_active !== undefined) {
            offlineBadge = `<span class="vp-badge vp-badge-offline">
                <span class="dashicons dashicons-format-status"></span>
                ${VPN.i18n.offline || 'Offline'}
            </span>`;
        }

        // 3-column card layout: Identity + Tags | Stats | Status + Actions
        return `
            <div class="vp-partner-card${partner.is_active === false ? ' vp-partner-offline' : ''}" data-site-id="${partner.id}">
                <!-- Column 1: Identity + requirement tags + note -->
                <div class="vp-partner-main">
                    <div class="vp-partner-domain-wrap">
                        ${domainDisplay}
                        ${categoryText ? `<div class="vp-partner-category-inline">${categoryText}</div>` : ''}
                        ${bioPreview ? `<div class="vp-partner-bio">${escapeHtml(bioPreview)}</div>` : ''}
                    </div>
                    <div class="vp-partner-badges">${langBadge}${offlineBadge}</div>
                    ${matchBadges ? `<div class="vp-partner-match-badges">${matchBadges}</div>` : ''}
                    ${requirementPillsHtml ? `<div class="vp-partner-req-tags">${requirementPillsHtml}</div>` : ''}
                    ${noteHtml}
                    ${newPartnerBadge ? `<div class="vp-partner-inline-badge">${newPartnerBadge}</div>` : ''}
                    ${disconnectedBadge ? `<div class="vp-partner-inline-badge">${disconnectedBadge}</div>` : ''}
                </div>

                <!-- Column 2: Stats -->
                <div class="vp-partner-stats-inline">
                    <div class="vp-stat-inline">
                        <span class="vp-stat-value">${formatNumber(clicks)}</span>
                        <span class="vp-stat-label">${VPN.i18n.clicks || 'clicks'}</span>
                    </div>
                    <div class="vp-stat-inline">
                        <span class="vp-stat-value">${formatNumber(impressions)}</span>
                        <span class="vp-stat-label">${VPN.i18n.impressions || 'impr.'}</span>
                    </div>
                </div>

                <!-- Column 3: Status + Actions -->
                <div class="vp-partner-actions-inline">
                    ${statusBadge}
                    ${actionButtons}
                    ${visitBtn}
                </div>
            </div>
        `;
    }

    function formatNumber(num) {
        if (!num || num === 0) return '0';
        if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
        if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
        return num.toString();
    }

    // Render requirement pills for partner cards (v1.0.29: tags only, note is separate)
    function renderRequirementPills(requirements) {
        if (!requirements) return '';

        const tags = [];

        // Min words - ALWAYS show so users know upfront
        const minWords = requirements.min_words || 500;
        tags.push(`<span class="vp-req-tag vp-req-words"><span class="dashicons dashicons-editor-paste-word"></span> ${minWords}+ words</span>`);

        // AI policy - ALWAYS show with clear icon
        const aiLabels = {
            'ai_allowed': { label: 'AI OK', class: 'vp-req-ai-ok' },
            'ai_with_edits': { label: 'AI + Edits', class: 'vp-req-ai-edits' },
            'human_only': { label: 'Human Only', class: 'vp-req-ai-no' }
        };
        const aiPolicy = requirements.ai_policy || 'ai_allowed';
        const aiInfo = aiLabels[aiPolicy] || aiLabels['ai_allowed'];
        const aiIcon = aiPolicy === 'human_only' ? 'dashicons-admin-users' : 'dashicons-admin-generic';
        tags.push(`<span class="vp-req-tag ${aiInfo.class}"><span class="dashicons ${aiIcon}"></span> ${aiInfo.label}</span>`);

        // Max links - ALWAYS show
        const maxLinks = requirements.max_dofollow_links || 3;
        tags.push(`<span class="vp-req-tag vp-req-links"><span class="dashicons dashicons-admin-links"></span> Max ${maxLinks} links</span>`);

        // Forbidden niches - show as compact list if any
        // SECURITY: Whitelist known niche slugs, escape any unknown values
        if (requirements.forbidden_niches && requirements.forbidden_niches.length > 0) {
            const nicheLabels = {
                'casino': 'Gambling', 'gambling': 'Gambling',
                'adult': 'Adult/18+', 'cbd': 'CBD',
                'crypto': 'Crypto', 'payday': 'Payday'
            };
            const forbiddenList = requirements.forbidden_niches
                .map(n => nicheLabels[n] || escapeHtml(n))
                .join(', ');
            tags.push(`<span class="vp-req-tag vp-req-restricted"><span class="dashicons dashicons-no"></span> No: ${forbiddenList}</span>`);
        }

        return tags.join('');
    }

    // Render publisher note with quote-style (v1.0.29: extracted from requirement tags)
    function renderPartnerNote(requirements) {
        if (!requirements || !requirements.custom_note || !requirements.custom_note.trim()) return '';

        // SECURITY: Use escapeAttr for title attribute, escapeHtml for text content
        return `<div class="vp-partner-note" title="${escapeAttr(requirements.custom_note)}">
            <span class="dashicons dashicons-format-quote"></span>
            <span class="vp-note-text">"${escapeHtml(requirements.custom_note)}"</span>
        </div>`;
    }

    function loadConnections() {
        $.post(VPN.ajaxUrl, {
            action: 'consolety_fetch_connections',
            nonce: VPN.nonce
        }, function(response) {
            if (response.success && response.data?.data) {
                const data = response.data.data;
                connectionsData.sent = (data.sent || []).filter(c => c.status === 'pending');
                connectionsData.received = (data.received || []).filter(c => c.status === 'pending');
                connectionsData.connected = [
                    ...(data.sent || []).filter(c => c.status === 'accepted'),
                    ...(data.received || []).filter(c => c.status === 'accepted')
                ];

                updateCounts();
                renderPending();
                renderConnected();
            }
        }).fail(function(xhr, status, error) {
            handleAjaxError(xhr, status, error, 'loadConnections');
        });
    }

    function updateCounts() {
        const pendingCount = connectionsData.sent.length + connectionsData.received.length;
        $('#pending-count').text(pendingCount);
        $('#connected-count').text(connectionsData.connected.length);
    }

    function renderPending() {
        // Received requests
        if (connectionsData.received.length) {
            $('#pending-received').html(connectionsData.received.map(conn => `
                <div class="vp-connection-item" data-id="${conn.id}">
                    <div class="vp-connection-info">
                        <span class="vp-connection-domain">${escapeHtml(conn.partner_site.domain)}</span>
                        <span class="vp-connection-meta">${VPN.i18n.wantsToConnect || 'wants to connect'}</span>
                    </div>
                    <div class="vp-connection-actions">
                        <button type="button" class="vp-btn vp-btn-success accept-btn" data-id="${conn.id}">
                            ${VPN.i18n.accept || 'Accept'}
                        </button>
                        <button type="button" class="vp-btn vp-btn-ghost reject-btn" data-id="${conn.id}">
                            ${VPN.i18n.reject || 'Reject'}
                        </button>
                    </div>
                </div>
            `).join(''));
        } else {
            $('#pending-received').html('<div class="vp-empty-list">' + (VPN.i18n.noReceivedRequests || 'No received requests') + '</div>');
        }

        // Sent requests
        if (connectionsData.sent.length) {
            $('#pending-sent').html(connectionsData.sent.map(conn => `
                <div class="vp-connection-item" data-id="${conn.id}">
                    <div class="vp-connection-info">
                        <span class="vp-connection-domain">${escapeHtml(conn.partner_site.domain)}</span>
                        <span class="vp-connection-meta">${VPN.i18n.waitingForResponse || 'waiting for response'}</span>
                    </div>
                    <div class="vp-connection-actions">
                        <button type="button" class="vp-btn vp-btn-ghost cancel-btn" data-id="${conn.id}">
                            ${VPN.i18n.cancel || 'Cancel'}
                        </button>
                    </div>
                </div>
            `).join(''));
        } else {
            $('#pending-sent').html('<div class="vp-empty-list">' + (VPN.i18n.noSentRequests || 'No sent requests') + '</div>');
        }
    }

    function renderConnected() {
        if (connectionsData.connected.length) {
            $('#connected-list').html(connectionsData.connected.map(conn => {
                return renderPartnerCard({
                    id: conn.partner_site.id,
                    domain: conn.partner_site.domain,
                    bio: conn.partner_site.bio || '',
                    language: conn.partner_site.language || null,
                    category: conn.partner_site.category || null,
                    monthly_clicks: conn.partner_site.monthly_clicks || 0,
                    monthly_impressions: conn.partner_site.monthly_impressions || 0,
                    requirements: conn.partner_site.requirements || null,
                    is_active: conn.partner_site.is_active,
                    is_online: conn.partner_site.is_online,
                    connection_id: conn.id,
                    connection_status: 'connected'
                }, true);
            }).join(''));
        } else {
            $('#connected-list').html(`
                <div class="vp-empty-state">
                    <div class="vp-empty-icon"><span class="dashicons dashicons-groups" style="font-size:48px;width:48px;height:48px;color:var(--vp-text-muted);"></span></div>
                    <h4>${VPN.i18n.noConnectionsYet || 'No connections yet'}</h4>
                    <p>${VPN.i18n.startByShowing || 'Start by showing interest in partners from the Discover tab.'}</p>
                </div>
            `);
        }
    }

    // Error code to user-friendly message mapping
    const CONNECTION_ERRORS = {
        'SELF_CONNECTION': VPN.i18n?.selfConnection || 'You cannot connect with yourself.',
        'COOLDOWN_ACTIVE': VPN.i18n?.cooldownActive || 'You must wait before requesting again. This partner rejected your previous request.',
        'DAILY_LIMIT_REACHED': VPN.i18n?.dailyLimitReached || 'Daily request limit reached. Try again tomorrow.',
        'CONNECTION_LIMIT_REACHED': VPN.i18n?.connectionLimitReached || 'Connection limit reached (10 active connections).',
        'TIER_MISMATCH': VPN.i18n?.tierMismatch || 'Traffic tier mismatch. Build more traffic to connect with higher-tier partners.',
        'ALREADY_PENDING': VPN.i18n?.alreadyPending || 'A request is already pending with this site.',
        'ALREADY_CONNECTED': VPN.i18n?.alreadyConnected || 'You are already connected with this site.',
        'INSUFFICIENT_CREDITS': VPN.i18n?.insufficientCredits || 'Not enough credits. Host guest posts or wait for daily rewards.',
        'REPUTATION_TOO_LOW': VPN.i18n?.reputationTooLow || 'Your reputation is too low. Build trust by providing quality content.',
        'LANGUAGE_MISMATCH': VPN.i18n?.languageMismatch || 'Language mismatch. Connect with sites in your language.',
        'NICHE_MISMATCH': VPN.i18n?.nicheMismatch || 'Niche not compatible. Connect with related categories.'
    };

    function getErrorMessage(response) {
        const errorCode = response?.error?.code || response?.data?.error?.code || '';
        const errorData = response?.error || response?.data?.error || {};
        if (errorCode === 'COOLDOWN_ACTIVE' && errorData.days_remaining) {
            return (VPN.i18n.youNeed || 'You must wait') + ' ' + errorData.days_remaining + ' ' + (VPN.i18n.moreDays || 'more days before requesting again.');
        }
        if (errorCode === 'INSUFFICIENT_CREDITS' && errorData.required) {
            return (VPN.i18n.youNeed || 'You need') + ' ' + errorData.required + ' ' + (VPN.i18n.creditsButHave || 'credits but only have') + ' ' + errorData.available + '.';
        }
        return CONNECTION_ERRORS[errorCode] || errorData.message || (VPN.i18n.failedToSend || 'Failed to send request. Please try again.');
    }

    function showInterest(siteId, domain, $btn) {
        if ($btn.data('sending')) return;
        $btn.data('sending', true);
        const idempotencyKey = 'conn_' + siteId + '_' + Date.now();
        $btn.prop('disabled', true).text(VPN.i18n.sending || 'Sending...');

        $.post(VPN.ajaxUrl, {
            action: 'consolety_send_connection',
            nonce: VPN.nonce,
            target_site_id: siteId,
            connection_type: 'guest_post',
            message: '',
            idempotency_key: idempotencyKey
        }, function(response) {
            if (response.success) {
                // Check if this was a reconnect (immediate accepted) or new request (pending)
                if (response.data?.reconnected || response.data?.status === 'accepted') {
                    // Reconnect during grace period = instant connection
                    $btn.replaceWith('<span class="vp-status-connected"><span class="dashicons dashicons-yes-alt"></span> ' +
                                    (VPN.i18n.connected || 'Connected') + '</span>');
                    showNotice(VPN.i18n.reconnected || 'Reconnected successfully!', 'success');
                } else {
                    // Normal new request = pending
                    let pendingLabel = VPN.i18n.pending || 'Pending';
                    if (response.data?.expires_in_days) {
                        pendingLabel += ' (' + response.data.expires_in_days + 'd)';
                    }
                    $btn.replaceWith('<span class="vp-status-pending"><span class="dashicons dashicons-clock"></span> ' + pendingLabel + '</span>');
                }
                loadConnections();
                updatePointsDisplay(); // Refresh points after deduction
            } else {
                const errorCode = response.data?.error_code || response.data?.code || '';
                const errorMsg = getErrorMessage(response);

                // Out of points is a decision moment, not a dead end: offer the
                // two real ways forward (host to earn, or go Pro).
                if (errorCode === 'INSUFFICIENT_CREDITS' && !VPN.isPro && VPN.upgradeUrl) {
                    const ask = (VPN.i18n.insufficientUpgradePrompt ||
                        'Not enough points. Host a guest post to earn more - or upgrade to Pro (EUR 29.99/mo) for free connections and double daily points. Open your plan options now?');
                    if (window.confirm(ask)) {
                        window.location.href = VPN.upgradeUrl;
                        return;
                    }
                } else {
                    showNotice(errorMsg, 'error');
                }
                $btn.prop('disabled', false).html(VPN.i18n.connect || 'Connect');
                $btn.data('sending', false);
            }
        }).fail(function(xhr, status, error) {
            if (!handleAjaxError(xhr, status, error, 'showInterest')) {
                $btn.prop('disabled', false).html(VPN.i18n.connect || 'Connect');
                $btn.data('sending', false);
            }
        });
    }

    function respondToRequest(connId, action, $btn) {
        $btn.prop('disabled', true);

        $.post(VPN.ajaxUrl, {
            action: 'consolety_respond_connection',
            nonce: VPN.nonce,
            connection_id: connId,
            response_action: action
        }, function(response) {
            if (response.success) {
                loadConnections();
                loadPartners();
                updatePointsDisplay(); // Refresh points after accept/reject
            } else {
                alert(response.data?.message || VPN.i18n.failedToRespond || 'Failed to respond');
                $btn.prop('disabled', false);
            }
        }).fail(function(xhr, status, error) {
            if (!handleAjaxError(xhr, status, error, 'respondToRequest')) {
                $btn.prop('disabled', false);
            }
        });
    }

    function cancelRequest(connId, $btn) {
        $btn.prop('disabled', true).text(VPN.i18n.cancelling || 'Cancelling...');

        $.post(VPN.ajaxUrl, {
            action: 'consolety_cancel_connection',
            nonce: VPN.nonce,
            connection_id: connId
        }, function(response) {
            if (response.success) {
                loadConnections();
                loadPartners();
                updatePointsDisplay(); // Refresh points after refund
            } else {
                alert(response.data?.message || VPN.i18n.failedToCancel || 'Failed to cancel');
                $btn.prop('disabled', false).text(VPN.i18n.cancel || 'Cancel');
            }
        }).fail(function(xhr, status, error) {
            if (!handleAjaxError(xhr, status, error, 'cancelRequest')) {
                $btn.prop('disabled', false).text(VPN.i18n.cancel || 'Cancel');
            }
        });
    }

    function disconnectPartner(connId, $btn) {
        $btn.prop('disabled', true).text(VPN.i18n.disconnecting || 'Disconnecting...');

        $.post(VPN.ajaxUrl, {
            action: 'consolety_cancel_connection',
            nonce: VPN.nonce,
            connection_id: connId
        }, function(response) {
            if (response.success) {
                loadConnections();
                loadPartners();
                updatePointsDisplay(); // Refresh points after disconnect
            } else {
                alert(response.data?.message || VPN.i18n.failedToDisconnect || 'Failed to disconnect');
                $btn.prop('disabled', false).text(VPN.i18n.disconnect || 'Disconnect');
            }
        }).fail(function(xhr, status, error) {
            if (!handleAjaxError(xhr, status, error, 'disconnectPartner')) {
                $btn.prop('disabled', false).text(VPN.i18n.disconnect || 'Disconnect');
            }
        });
    }

    /**
     * Update points display after any points-mutating action
     */
    function updatePointsDisplay() {
        $.post(VPN.ajaxUrl, {
            action: 'consolety_fetch_points_balance',
            nonce: VPN.nonce
        }, function(response) {
            if (response.success && response.data?.balance !== undefined) {
                $('.vp-credits-value').text(response.data.balance);
            }
        }).fail(function(xhr, status, error) {
            handleAjaxError(xhr, status, error, 'updatePointsDisplay');
        });
    }

    /**
     * Escape HTML entities for safe text content
     */
    function escapeHtml(text) {
        if (!text) return '';
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }

    /**
     * Escape for use in HTML attributes (includes quotes)
     * SECURITY: Must use this for title="", data-*="", href="", etc.
     */
    function escapeAttr(text) {
        if (!text) return '';
        return String(text)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;');
    }

})(jQuery);
