<div class="wrap" id="ai-smartcopy-admin-wrap">
<h1 style="font-size:23px!important; font-weight:400!important; line-height:1.3!important;">Inception AI SmartCopy</h1>

<!-- IMPORTANT: ADD "define('AI_SC_DEBUG', true);" to wp-config.php if you need DEBUG LOGS!  -->

<!-- NEW: Seed progress from the previous run, then apply once DOM is ready -->
<script>(function(){
  try {
    var raw = sessionStorage.getItem('aiPB_seed');
    if (!raw) return;
    var st = JSON.parse(raw) || {};
    window._aiPB = window._aiPB || {};
    window._aiPB.total = parseInt(st.total || 0, 10);
    window._aiPB.done  = parseInt(st.done  || 0, 10);
    window._aiPB.targetPct = Math.round((window._aiPB.total > 0 ? (window._aiPB.done / window._aiPB.total) : 0) * 100);
    window._aiPB.seeded = true;             // tells later scripts not to reset to 0
    sessionStorage.removeItem('aiPB_seed'); // one-time seed

    // If the bar exists later, paint the seeded numbers immediately on ready
    document.addEventListener('DOMContentLoaded', function(){
      try {
        var bar = document.getElementById('progress-bar-new');
        if (bar && window._aiPB && window._aiPB.total > 0) {
          var p = Math.round(window._aiPB.done / window._aiPB.total * 100);
          bar.style.width = p + '%';
          bar.textContent = window._aiPB.done + ' / ' + window._aiPB.total + ' (' + p + '%)';
        }
      } catch (e) {}
    });
  } catch (e) {}
})();</script>

<?php
// ==== AI SmartCopy Debug Logger (safe) =========================================
if ( ! function_exists('ai_sc_debug_log_path') ) {
    function ai_sc_debug_log_path() {
        if ( defined('ABSPATH') ) {
            $p = rtrim(ABSPATH, '/\\') . DIRECTORY_SEPARATOR . 'ai-smartcopy-debug.log';
        } else {
            $p = __DIR__ . DIRECTORY_SEPARATOR . 'ai-smartcopy-debug.log';
        }
        return $p;
    }
}

/** Enable debug logs only when AI_SC_DEBUG is explicitly true. */
if ( ! function_exists('ai_sc_debug_enabled') ) {
    function ai_sc_debug_enabled() {
        return defined('AI_SC_DEBUG') && AI_SC_DEBUG;
    }
}

// ==== AI SmartCopy HELPER TO REMOVE EVERYTHING BEFORE THE DOMAINS =========================================
if ( ! function_exists('ai_sc_norm_url_key') ) {
    function ai_sc_norm_url_key( $url ) {
        $url = trim( (string)$url );
        if ($url === '') return '';

        // Make sure parse_url works consistently
        if (strpos($url, '//') === 0) $url = 'https:' . $url;

        $parts = parse_url($url);
        if (empty($parts['host'])) {
            // Non-absolute (rare for Copyscape CSV), just normalize slashes
            return untrailingslashit($url);
        }

        $host = strtolower($parts['host']);
        $host = preg_replace('/^www\./i', '', $host);

        $path = isset($parts['path']) ? rawurldecode($parts['path']) : '';
        $path = preg_replace('#/+#', '/', $path);
        $path = rtrim($path, '/');

        return $host . $path; // NOTE: no scheme
    }
}

if ( ! function_exists('ai_sc_openai_chat_json') ) {
    function ai_sc_openai_chat_json( $api_key, $model, $prompt ) {
        $api_key = trim( (string) $api_key );
        if ( $api_key === '' ) {
            return new WP_Error('ai_sc_missing_api_key', 'OpenAI API key is not configured.');
        }

        $response = wp_remote_post('https://api.openai.com/v1/chat/completions', [
            'timeout' => 120,
            'headers' => [
                'Authorization' => 'Bearer ' . $api_key,
                'Content-Type'  => 'application/json',
            ],
            'body' => wp_json_encode([
                'model' => $model ?: 'gpt-4o-mini',
                'temperature' => 0.4,
                'max_tokens' => 6000,
                'messages' => [
                    [
                        'role' => 'system',
                        'content' => 'Return only valid JSON matching the keys requested by the user.',
                    ],
                    [
                        'role' => 'user',
                        'content' => (string) $prompt,
                    ],
                ],
                'response_format' => [
                    'type' => 'json_object',
                ],
            ]),
        ]);

        if ( is_wp_error($response) ) {
            return $response;
        }

        $status = wp_remote_retrieve_response_code($response);
        $body = json_decode(wp_remote_retrieve_body($response), true);
        if ( $status !== 200 ) {
            $message = $body['error']['message'] ?? ('OpenAI request failed with HTTP ' . $status . '.');
            $error_code = strtolower( (string) ($body['error']['code'] ?? '') );
            $error_type = strtolower( (string) ($body['error']['type'] ?? '') );
            $quota_error = $status === 429 && (
                strpos(strtolower($message), 'quota') !== false
                || strpos($error_code, 'quota') !== false
                || strpos($error_type, 'quota') !== false
                || $error_code === 'billing_hard_limit_reached'
            );
            if ( $quota_error ) {
                $message = 'OpenAI API quota exceeded. Add billing credits or increase the organization/project budget, then try again.';
            }
            return new WP_Error('ai_sc_openai_http', $message);
        }

        $text = trim( (string) ($body['choices'][0]['message']['content'] ?? '') );
        if ( $text === '' ) {
            return new WP_Error('ai_sc_openai_empty', 'OpenAI returned an empty rewrite response.');
        }

        return [
            'text' => $text,
            'raw'  => $body,
        ];
    }
}

if ( ! function_exists('ai_sc_ensure_city_state_title') ) {
    /**
     * Guarantee that rewrite titles contain "CITY STATE" with no comma.
     */
    function ai_sc_ensure_city_state_title($title, $city, $state) {
        $title = trim((string) $title);
        $city  = trim((string) $city);
        $state = trim((string) $state);

        if ($city === '' || $state === '') {
            return $title;
        }

        $location = $city . ' ' . $state;
        $city_pattern = preg_quote($city, '/');
        $state_pattern = preg_quote($state, '/');

        $title = preg_replace(
            '/' . $city_pattern . '\s*,\s*' . $state_pattern . '/iu',
            $location,
            $title
        );
        if (preg_match('/' . $city_pattern . '\s+' . $state_pattern . '/iu', $title)) {
            return preg_replace(
                '/' . $city_pattern . '\s+' . $state_pattern . '/iu',
                $location,
                $title,
                1
            );
        }

        if ($title === '') {
            return $location;
        }

        return rtrim($title, " \t\n\r\0\x0B-|,") . ' in ' . $location;
    }
}

if ( ! function_exists('ai_sc_rewrite_fidelity_issues') ) {
    function ai_sc_normalized_sentences($html) {
        $text = html_entity_decode(wp_strip_all_tags((string) $html), ENT_QUOTES | ENT_HTML5, 'UTF-8');
        $text = preg_replace('/\s+/u', ' ', trim($text));
        if ($text === '') {
            return [];
        }

        $sentences = preg_split('/(?<=[.!?])\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY);
        $normalized = [];
        foreach ($sentences as $sentence) {
            $sentence = trim($sentence);
            $words = preg_split('/\s+/u', $sentence, -1, PREG_SPLIT_NO_EMPTY);
            if (count($words) < 8) {
                continue;
            }
            $key = mb_strtolower($sentence, 'UTF-8');
            $key = preg_replace('/[^\p{L}\p{N}\s]/u', '', $key);
            $key = preg_replace('/\s+/u', ' ', trim($key));
            if ($key !== '') {
                $normalized[$key] = $sentence;
            }
        }
        return $normalized;
    }

    function ai_sc_unchanged_source_sentences($source_html, $rewrite_html) {
        $source_sentences = ai_sc_normalized_sentences($source_html);
        $rewrite_sentences = ai_sc_normalized_sentences($rewrite_html);
        $matches = [];

        foreach ($source_sentences as $key => $source_sentence) {
            if (isset($rewrite_sentences[$key])) {
                $matches[] = $source_sentence;
            }
        }

        return $matches;
    }

    /**
     * Detect placeholder output and obvious profession drift before publishing.
     */
    function ai_sc_rewrite_fidelity_issues($source_html, $rewrite_data) {
        $issues = [];
        $source_original = wp_strip_all_tags((string) $source_html);
        $output_original =
            (string) ($rewrite_data['seo_title'] ?? '') . ' '
            . (string) ($rewrite_data['meta_desc'] ?? '') . ' '
            . wp_strip_all_tags((string) ($rewrite_data['html'] ?? ''));
        $source = strtolower($source_original);
        $output = strtolower($output_original);

        if (preg_match('/\[[^\]]+\]|clinic\s+name\*?|city\*|state\*|\bdr\.\s*\[(?:first|last)\s+name\]/i', $output)) {
            $issues[] = 'placeholder text';
        }

        $source_is_dental = preg_match('/\b(dentist|dentistry|dental|dds|dmd)\b/i', $source);
        $source_is_chiro = preg_match('/\b(chiropractor|chiropractic|doctor of chiropractic)\b/i', $source);
        $output_is_dental = preg_match('/\b(dentist|dentistry|dental|dds|dmd)\b/i', $output);
        $output_is_chiro = preg_match('/\b(chiropractor|chiropractic|doctor of chiropractic)\b/i', $output);

        if ($source_is_dental && $output_is_chiro && !$source_is_chiro) {
            $issues[] = 'changed dental content to chiropractic content';
        }
        if ($source_is_chiro && $output_is_dental && !$source_is_dental) {
            $issues[] = 'changed chiropractic content to dental content';
        }

        if (preg_match_all('/\bDr\.?\s+(?!Dr\.?\b)[\p{Lu}][\p{L}\'.-]+(?:\s+(?!Dr\.?\b)[\p{Lu}][\p{L}\'.-]+){0,2}/u', $source_original, $doctor_matches)) {
            $doctor_names_by_surname = [];
            foreach (array_unique($doctor_matches[0]) as $doctor_name) {
                $name_parts = preg_split('/\s+/u', trim($doctor_name));
                $non_name_words = [
                    'medical', 'dental', 'chiropractic', 'therapy', 'clinic',
                    'center', 'practice', 'hospital', 'university', 'college',
                    'he', 'she', 'his', 'her', 'they', 'their', 'the',
                ];
                while (count($name_parts) > 2) {
                    $last_word = strtolower(rtrim((string) end($name_parts), '.,;:'));
                    if (!in_array($last_word, $non_name_words, true)) {
                        break;
                    }
                    array_pop($name_parts);
                }
                $doctor_name = implode(' ', $name_parts);
                $surname = strtolower((string) end($name_parts));
                if (!isset($doctor_names_by_surname[$surname]) || mb_strlen($doctor_name) > mb_strlen($doctor_names_by_surname[$surname])) {
                    $doctor_names_by_surname[$surname] = $doctor_name;
                }
            }
            foreach ($doctor_names_by_surname as $doctor_name) {
                if (stripos($output_original, $doctor_name) === false) {
                    $issues[] = 'omitted source name ' . $doctor_name;
                }
            }
        }

        $unchanged_sentences = ai_sc_unchanged_source_sentences(
            $source_html,
            (string) ($rewrite_data['html'] ?? '')
        );
        if ($unchanged_sentences) {
            $issues[] = count($unchanged_sentences) . ' source sentence(s) were copied verbatim';
        }

        return $issues;
    }
}

if ( ! function_exists('ai_sc_format_fidelity_warning') ) {
    function ai_sc_format_fidelity_warning($warning) {
        $warning = sanitize_text_field((string) $warning);

        if (preg_match('/^omitted source name\s+(.+)$/i', $warning, $match)) {
            return 'Please confirm that the person named "' . trim($match[1]) . '" appears correctly in the published content. The automated review could not verify the full name.';
        }
        if ($warning === 'placeholder text') {
            return 'Please check for placeholder text such as "[First Name]" or "CLINIC NAME" and replace it if present.';
        }
        if (strpos($warning, 'changed dental content to chiropractic content') !== false) {
            return 'Please verify the profession and services. The rewrite may describe chiropractic care instead of dental care.';
        }
        if (strpos($warning, 'changed chiropractic content to dental content') !== false) {
            return 'Please verify the profession and services. The rewrite may describe dental care instead of chiropractic care.';
        }
        if (preg_match('/^(\d+)\s+source sentence\(s\) were copied verbatim$/i', $warning, $match)) {
            return $match[1] . ' sentence(s) may still match the source exactly. Please reword them before considering the page final.';
        }
        if (stripos($warning, 'correction request failed') !== false) {
            return 'The page was published, but the automated quality-review retry could not be completed. Please review the content manually.';
        }
        if (stripos($warning, 'invalid correction response') !== false) {
            return 'The page was published, but the automated quality-review response could not be read. Please review the content manually.';
        }

        return 'Please review this item before considering it final: ' . $warning;
    }
}

if ( ! function_exists('ai_sc_rewrite_page_content') ) {
    /**
     * Rewrite existing HTML content using OpenAI per local SEO guidelines.
     */
    function ai_sc_rewrite_page_content($api_key, $model, $clinic, $city, $state, $source_html) {
        $source_text = trim(preg_replace('/\s+/u', ' ', wp_strip_all_tags((string) $source_html)));
        $prompt = "Act as a highly skilled SEO specialist and expert copywriter. Rewrite the provided webpage content into a unique version optimized for local SEO.\n\n"
                . "SOURCE FIDELITY RULES:\n"
                . "- Treat the supplied source as the only factual authority.\n"
                . "- Preserve every person's real name, title, credentials, institution, date, number, location, service, and profession found in the source.\n"
                . "- Never replace source facts with placeholders such as [First Name], [Last Name], [Year], [University Name], CLINIC NAME, CITY, or STATE.\n"
                . "- Never change dentistry into chiropractic, chiropractic into dentistry, or otherwise change the business type.\n"
                . "- Do not invent biographies, qualifications, hours, communities, or other details.\n"
                . "- If a detail is absent from the source, omit it instead of guessing or inserting a placeholder.\n\n"
                . "UNIQUENESS RULES:\n"
                . "- Rewrite every sentence in fresh language while preserving its factual meaning.\n"
                . "- Do not copy any complete source sentence of eight or more words verbatim.\n"
                . "- Names, credentials, dates, numbers, schools, addresses, and service names may remain unchanged because they are facts.\n\n"
                . "Follow Yoast SEO guidelines for readability, ideal length, keyword density, and key phrase distribution.\n"
                . "Provide an SEO title of 50-60 characters containing the exact phrase " . (string) $city . " " . (string) $state . ", with no comma between the city and state.\n"
                . "Provide a meta description no longer than 155 characters featuring " . (string) $clinic . ", the primary key phrase, " . (string) $city . ", and " . (string) $state . ".\n"
                . "Recommend a short, keyword-focused URL slug.\n"
                . "Use useful subheadings, active voice, short sentences, natural transitions, and a conversational human tone.\n\n"
                . "Clinic: " . (string) $clinic . "\n"
                . "City: " . (string) $city . "\n"
                . "State: " . (string) $state . "\n\n"
                . "Rewrite the following source content:\n<CONTENT>\n"
                . $source_text
                . "\n</CONTENT>\n\n"
                . "Return JSON with keys: seo_title, meta_desc, slug, html. The html value must contain the complete rewritten page body as valid HTML.";

        $response = ai_sc_openai_chat_json($api_key, $model, $prompt);
        if ( is_wp_error($response) ) {
            return $response;
        }

        $data = json_decode($response['text'] ?? '', true);
        if ( ! is_array($data) ) {
            return new WP_Error('ai_sc_invalid_rewrite_json', 'OpenAI returned invalid JSON for the page rewrite.');
        }

        $result = [
            'seo_title' => sanitize_text_field($data['seo_title'] ?? ''),
            'meta_desc' => sanitize_text_field($data['meta_desc'] ?? ''),
            'slug'      => sanitize_title($data['slug'] ?? ''),
            'html'      => wp_kses_post($data['html'] ?? ''),
        ];

        $issues = ai_sc_rewrite_fidelity_issues($source_html, $result);
        if ($issues) {
            $repair_prompt = "Correct this rewrite using the source as the only factual authority.\n"
                . "The prior rewrite failed because it contained: " . implode(', ', $issues) . ".\n"
                . "Preserve all real names, credentials, schools, dates, numbers, professions, and services from the source.\n"
                . "Rewrite every complete sentence in fresh language. Do not copy any source sentence of eight or more words verbatim.\n"
                . "Do not use placeholders and do not invent facts. Return complete valid JSON with keys seo_title, meta_desc, slug, html.\n\n"
                . "City: " . (string) $city . "\nState: " . (string) $state . "\n\n"
                . "<SOURCE>\n" . $source_text . "\n</SOURCE>\n\n"
                . "<FAILED_REWRITE>\n" . wp_json_encode($result) . "\n</FAILED_REWRITE>";
            $repair = ai_sc_openai_chat_json($api_key, $model, $repair_prompt);
            if (is_wp_error($repair)) {
                $result['fidelity_warnings'] = [
                    'The source-fidelity correction request failed: ' . $repair->get_error_message(),
                ];
                return $result;
            }
            $repair_data = json_decode($repair['text'] ?? '', true);
            if (!is_array($repair_data)) {
                $result['fidelity_warnings'] = [
                    'OpenAI returned an invalid correction response. The original rewrite was published for manual review.',
                ];
                return $result;
            }
            $result = [
                'seo_title' => sanitize_text_field($repair_data['seo_title'] ?? ''),
                'meta_desc' => sanitize_text_field($repair_data['meta_desc'] ?? ''),
                'slug'      => sanitize_title($repair_data['slug'] ?? ''),
                'html'      => wp_kses_post($repair_data['html'] ?? ''),
            ];
            $remaining_issues = ai_sc_rewrite_fidelity_issues($source_html, $result);
            $remaining_copies = ai_sc_unchanged_source_sentences(
                $source_html,
                (string) ($result['html'] ?? '')
            );
            if ($remaining_copies) {
                $copy_repair_prompt = "Rewrite only the copied sentences in this candidate while keeping the complete page coherent.\n"
                    . "Preserve every factual detail, including names, credentials, schools, dates, numbers, profession, and services.\n"
                    . "Do not use placeholders or invent information.\n"
                    . "None of the listed sentences may remain verbatim. Return complete valid JSON with keys seo_title, meta_desc, slug, html.\n\n"
                    . "<VERBATIM_SENTENCES>\n- " . implode("\n- ", $remaining_copies) . "\n</VERBATIM_SENTENCES>\n\n"
                    . "<SOURCE>\n" . $source_text . "\n</SOURCE>\n\n"
                    . "<CANDIDATE>\n" . wp_json_encode($result) . "\n</CANDIDATE>";
                $copy_repair = ai_sc_openai_chat_json($api_key, $model, $copy_repair_prompt);
                if (!is_wp_error($copy_repair)) {
                    $copy_repair_data = json_decode($copy_repair['text'] ?? '', true);
                    if (is_array($copy_repair_data)) {
                        $result = [
                            'seo_title' => sanitize_text_field($copy_repair_data['seo_title'] ?? $result['seo_title']),
                            'meta_desc' => sanitize_text_field($copy_repair_data['meta_desc'] ?? $result['meta_desc']),
                            'slug'      => sanitize_title($copy_repair_data['slug'] ?? $result['slug']),
                            'html'      => wp_kses_post($copy_repair_data['html'] ?? $result['html']),
                        ];
                    }
                }
                $remaining_issues = ai_sc_rewrite_fidelity_issues($source_html, $result);
            }
            if ($remaining_issues) {
                $result['fidelity_warnings'] = $remaining_issues;
            }
        }

        return $result;
    }
}

// ==== AI SmartCopy Debug Logger (safe) =========================================
if ( ! function_exists('ai_sc_debug') ) {
    function ai_sc_debug( $message, $context = array() ) {
        // EARLY EXIT when debug is off — prevents file creation entirely
        if ( ! ai_sc_debug_enabled() ) { return; }

        try {
            $ts  = microtime(true);
            $dt  = date('Y-m-d H:i:s') . sprintf('.%03d', ($ts - floor($ts)) * 1000);
            $mem = function_exists('memory_get_usage') ? memory_get_usage(true) : 0;
            $user = (function_exists('wp_get_current_user') && wp_get_current_user()) ? wp_get_current_user()->user_login : '';
            $req  = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
            $act  = isset($_POST['action']) ? $_POST['action'] : '';
            $pid  = function_exists('getmypid') ? getmypid() : 0;

            $ctx = '';
            if ( !empty($context) ) {
                $flags = defined('JSON_PARTIAL_OUTPUT_ON_ERROR') ? JSON_PARTIAL_OUTPUT_ON_ERROR : 0;
                $safe  = @json_encode($context, $flags);
                if ($safe === false) { $safe = @var_export($context, true); }
                if (is_string($safe) && strlen($safe) > 2000) { $safe = substr($safe, 0, 2000) . '…(truncated)'; }
                $ctx = ' | ctx=' . $safe;
            }

            // NOTE: include $ctx BEFORE PHP_EOL so context is visible in the log
            $line = sprintf(
                '[%s] pid=%s user=%s mem=%d uri=%s action=%s | %s%s%s',
                $dt, $pid, $user, $mem, $req, $act, $message, $ctx, PHP_EOL
            );

            @file_put_contents( ai_sc_debug_log_path(), $line, FILE_APPEND | LOCK_EX );
        } catch (Throwable $e) {
            // swallow; never break page render
        }
    }
}

if ( ! function_exists('ai_sc_register_error_handlers') ) {
    function ai_sc_register_error_handlers() {
        if ( defined('AI_SC_HANDLERS') ) { return; }
        define('AI_SC_HANDLERS', true);

        set_error_handler(function($errno, $errstr, $errfile, $errline){
            $level_names = array(
                E_ERROR => 'E_ERROR', E_WARNING => 'E_WARNING', E_PARSE => 'E_PARSE',
                E_NOTICE => 'E_NOTICE', E_CORE_ERROR => 'E_CORE_ERROR', E_CORE_WARNING => 'E_CORE_WARNING',
                E_COMPILE_ERROR => 'E_COMPILE_ERROR', E_COMPILE_WARNING => 'E_COMPILE_WARNING',
                E_USER_ERROR => 'E_USER_ERROR', E_USER_WARNING => 'E_USER_WARNING', E_USER_NOTICE => 'E_USER_NOTICE',
                E_STRICT => 'E_STRICT', E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR',
                E_DEPRECATED => 'E_DEPRECATED', E_USER_DEPRECATED => 'E_USER_DEPRECATED'
            );
            ai_sc_debug('PHP error', array(
                'level' => ($level_names[$errno] ?? $errno),
                'errno' => $errno,
                'errstr'=> $errstr,
                'file'  => $errfile,
                'line'  => $errline
            ));
            return false; // let PHP/WP continue normal handling
        });

        set_exception_handler(function($ex){
            ai_sc_debug('Uncaught exception', array(
                'type' => is_object($ex) ? get_class($ex) : 'unknown',
                'message' => is_object($ex) ? $ex->getMessage() : '',
                'file' => is_object($ex) ? $ex->getFile() : '',
                'line' => is_object($ex) ? $ex->getLine() : 0
            ));
        });

        register_shutdown_function(function(){
            $e = error_get_last();
            if ($e && in_array($e['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR))) {
                ai_sc_debug('Fatal shutdown', $e);
            }
        });
    }
}
if ( ai_sc_debug_enabled() ) {
    ai_sc_register_error_handlers();
    ai_sc_debug('Request start', array('method'=>$_SERVER['REQUEST_METHOD'] ?? 'CLI', 'post_keys'=>array_keys($_POST ?? array())));
}
// =============================================================================

// ==== Salesforce JWT helpers and Project file fetch ===================================
if ( ! function_exists('ai_sc_b64url') ) {
    function ai_sc_b64url($data) {
        ai_sc_debug(__FUNCTION__ . '() start', array('args'=>func_get_args()));
        // --- added: guard against null / non-string for PHP 8.1+ deprecations
        if ($data === null) {
            $data = '';
        } elseif (!is_string($data)) {
            $data = (string) $data;
        }
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }
}
if ( ! function_exists('ai_sc_sf_get_access_token_jwt') ) {
function ai_sc_sf_get_access_token_jwt() {
    ai_sc_debug(__FUNCTION__ . '() start', array('args'=>func_get_args()));

$sf_client_id  = '3MVG9cHH2bfKACZbqCVKn_vtAm6JdRsvpmkG_OcG5iLFAe9TZ5pET0fybtWwmQCKCooxCBsNmcMXb4mcql78w';
$sf_login_base = 'https://inceptiononlinem.my.salesforce.com';
$sf_jwt_user   = 'jforehand@inceptiononlinemarketing.com';
$sf_jwt_key    = <<<EOPRIVATEKEY
-----BEGIN PRIVATE KEY-----
MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDuv+fRBdUL372Q
LpoL4pFDxrHiY28L8FwEVjVzPQHLycGFc2KrLFcR+fkYPER8S2igoSqZKBCDJ7NO
4V3FqeEVHwqlzoCtkV7r+hRqYnCDRY78Ivz6tYvdC1WfCbgzXqROgoMasaPDpp5P
6zBIonldJ0wWzroguWeAxJZiFNxwJtir2kfVafu7M5IHlN+u07zyUHYiFv8tK/me
bL+4zyaW4aoIi/rmxrPwdJnanHdRaH4E5ILbQxxrRGaf8oiow/3P5OZZoM4PE+W9
D4Ve4um0kiLpQsGQg4ogN8ZRT3sBIqwzIW9yXNr7b2Mn/q1EEL5R4qDphHW5lQ/2
Epocu9+hAgMBAAECggEAAWiMMZkHew/OIzJ02KN6KcXLb8y4PMra1nCg1WHiOll2
+459DKn+Rr9vBoNYVanJzNzx2/15nktyzjvL5mt10I6vrMoYwLAICOvKDhVanm9K
bFQjbRzFbc0IK5Vb0uuWJqt+4l6S8FesbQe5NSwmW6wXMbZ/xbYFnWO+KHHrmbqs
Wh2PH01sy4+lj1eD9tE3MLBTiIA3o4Att1+LnF03jCkixtYFa6DhCTeWDGBvV4di
tK+Pss3mEEvv/MyvnUgzBDNlcCojeT+58Z6P4npz0pL6fu/1z20Dj2/QsJrK1jmB
EELybfQ2Noom5NzHPLhQcx8uoWlvXOOYB5KzybXFAQKBgQD4kcp+3RVWU3Nym5lt
8R1QkDkZWY1rPiY6pc/PS6s0GTQ9JwpWhItdA0Ddbq/ku/dhPoCFejdjRooXCFmZ
S95J+HVUM8viWkESm5rMuxQeq0eA/HzvErWeZMrtPj9w6Pzsa7pwuvBbj8DvHcba
wlAVtLj9WvDF48CHiETPSJUKIQKBgQD14vd/5CwF8L/AvPrXdFciUvz9knIPncBA
zl0wDh8w8M1gGPIceDdlQ+TUi/C7YwHZcqfGHwamtS2P5pzQNOPZdWip/Lnb/DTE
oytpABGHYVO/DCGMirH+lOT9jBZMxQ2YiNxyNShSK8Y5pghbBtDdvZ5Yi233xTGx
B0ZxF8slgQKBgQCJA+wH5Bvn2tYvXX/Eb30GNImU5J0keTyfZVjYXlB/xkjaUJPN
PwCHA80Ugffo8IebDdXQPZ8cVPy3V330FzLvuUoLYQXIvpFSDX3VzmV3xlgCzKqd
KcX9CnglMAPOXE9WjsdVbmYhQmtKIzLC0QtN+7Fgzmkb27/7tWygidTFoQKBgGxn
JNB4asQvsIorhrdILkYOqkSstKaX2LN5h3HxN+UetJyiQtM0WM/HT/I4w+gJLT6N
otPgx9DlXXHsrMPKHY24im0u403N8gGjIjdHdXLnAMHgekX4u6JOSGpTwV0VHvpZ
lD6xmtYnQFwYC+T9q0WnHFFTHyc2A8NVPsQLRl+BAn9jfbESnVpaWDpxSLygO2UZ
iUnhKhswJVVhwpqJ/3hNcueC4z1LABpCcE1iqsNFbgHCjBZiCCmYZ+z26oMIv9M2
d+fiepp1EKRDhycVr9Ae9ImfNidlypjsK5AAX14Zr54xnGQroEBBLN3deU/DdjQX
kee48EGqcaIuVcrp6840
-----END PRIVATE KEY-----
EOPRIVATEKEY;

    if ( empty($sf_client_id) || empty($sf_login_base) || empty($sf_jwt_user) || empty($sf_jwt_key) ) {
        return new WP_Error('sf_jwt_missing', 'Salesforce JWT settings are incomplete (Client ID / My Domain / JWT User / Private Key).');
    }
    $pem = trim($sf_jwt_key);
    if ( strpos($pem, 'BEGIN') === false ) {
        $pem = "-----BEGIN PRIVATE KEY-----\n"
             . trim(chunk_split(str_replace(["\r", "\n", " "], '', $pem), 64, "\n"))
             . "\n-----END PRIVATE KEY-----\n";
    }
    $pkey = openssl_pkey_get_private($pem);
    if ( ! $pkey ) return new WP_Error('sf_jwt_key', 'Invalid Salesforce private key (PEM).');

    $now = time();
    $hdr = ai_sc_b64url(json_encode(['alg'=>'RS256']));
    $pld = ai_sc_b64url(json_encode(['iss'=>$sf_client_id,'sub'=>$sf_jwt_user,'aud'=>untrailingslashit($sf_login_base),'exp'=>$now+180]));
    $signing_input = $hdr.'.'.$pld;
    $signature='';
    $ok = openssl_sign($signing_input, $signature, $pkey, OPENSSL_ALGO_SHA256);
    
    // --- changed: avoid deprecated free on PHP 8.3+
    if (PHP_VERSION_ID < 80300 && function_exists('openssl_pkey_free')) {
        @openssl_pkey_free($pkey);
    }
    
    if ( ! $ok ) return new WP_Error('sf_jwt_sign','Failed to sign JWT.');
    $jwt = $signing_input.'.'.ai_sc_b64url($signature);
    
    $token_url = trailingslashit($sf_login_base) . 'services/oauth2/token';
    $resp = wp_remote_post($token_url, ['timeout'=>20,'body'=>['grant_type'=>'urn:ietf:params:oauth:grant-type:jwt-bearer','assertion'=>$jwt]]);
    if ( is_wp_error($resp) ) return $resp;
    $code = wp_remote_retrieve_response_code($resp);
    $body = json_decode(wp_remote_retrieve_body($resp), true);
    if ( $code!==200 || empty($body['access_token']) || empty($body['instance_url']) ) {
        return new WP_Error('sf_jwt_token', 'JWT token exchange failed: HTTP '.$code.' '. wp_remote_retrieve_body($resp));
    }
    set_transient('ai_sc_sf_jwt_token', ['access_token'=>$body['access_token'],'instance_url'=>$body['instance_url'],'ts'=>time()], 10*MINUTE_IN_SECONDS);
    return $body;
}}

if ( ! function_exists('ai_sc_sf_extract_project_id') ) {
function ai_sc_sf_extract_project_id( $input ) {
    ai_sc_debug(__FUNCTION__ . '() start', array('args'=>func_get_args()));

    $in = trim( (string) $input );
    if ( $in === '' ) return '';
    if ( preg_match('#/lightning/r/Project__c/([A-Za-z0-9]{15,18})/#', $in, $m) ) return $m[1];
    if ( preg_match('/^[A-Za-z0-9]{15,18}$/', $in) ) return $in;
    return '';
}}

if ( ! function_exists('ai_sc_sf_fetch_project_assets') ) {
function ai_sc_sf_fetch_project_assets( $project_id, $sf_api_ver = 'v59.0' ) {
    ai_sc_debug(__FUNCTION__ . '() start', array('args'=>func_get_args()));

    $tok = get_transient('ai_sc_sf_jwt_token');
    if ( ! is_array($tok) || empty($tok['access_token']) || ( time() - intval($tok['ts'] ?? 0) > 9*60 ) ) {
        $tok = ai_sc_sf_get_access_token_jwt();
        if ( is_wp_error($tok) ) return $tok;
    }
    $access_token = $tok['access_token'];
    $instance_url = $tok['instance_url'];
    if ( empty($instance_url) || stripos($instance_url, 'http') !== 0 ) {
        return new WP_Error('sf_no_instance','Got token but missing instance_url');
    }
    $headers = [ 'Authorization'=>'Bearer '.$access_token ];

    // 1) Website__c
    $q1 = "SELECT Website__c FROM Project__c WHERE Id = '$project_id'";
    $u1 = trailingslashit($instance_url).'services/data/'.$sf_api_ver.'/query/?q='.urlencode($q1);
    $r1 = wp_remote_get($u1, ['timeout'=>20,'headers'=>$headers]);
    if ( is_wp_error($r1) ) return $r1;
    if ( 200 !== wp_remote_retrieve_response_code($r1) ) {
        return new WP_Error('sf_q1','Project__c lookup failed: '. wp_remote_retrieve_body($r1));
    }
    $b1 = json_decode(wp_remote_retrieve_body($r1), true);
    $website = isset($b1['records'][0]['Website__c']) ? trim($b1['records'][0]['Website__c']) : '';

    // 2) Files linked to Project
    $q2 = "SELECT ContentDocumentId, LinkedEntityId, ContentDocument.Title, ContentDocument.FileExtension, ContentDocument.LatestPublishedVersionId FROM ContentDocumentLink WHERE LinkedEntityId = '$project_id'";
    $u2 = trailingslashit($instance_url).'services/data/'.$sf_api_ver.'/query/?q='.urlencode($q2);
    $r2 = wp_remote_get($u2, ['timeout'=>20,'headers'=>$headers]);
    if ( is_wp_error($r2) ) return $r2;
    if ( 200 !== wp_remote_retrieve_response_code($r2) ) {
        return new WP_Error('sf_q2','File link lookup failed: '. wp_remote_retrieve_body($r2));
    }
    $recs = (json_decode(wp_remote_retrieve_body($r2), true))['records'] ?? [];

    $pick = ['sitemap'=>null,'copyscape'=>null];
    foreach ($recs as $row) {
        $title = $row['ContentDocument']['Title'] ?? '';
        $ext   = strtolower($row['ContentDocument']['FileExtension'] ?? '');
        if ($ext !== 'csv') continue;
        $name = $title.'.csv';
        if ( preg_match('/-sitemap\.csv$/i', $name) && empty($pick['sitemap']) )   $pick['sitemap']   = $row;
        if ( preg_match('/-copyscape\.csv$/i', $name) && empty($pick['copyscape']) ) $pick['copyscape'] = $row;
    }

    $out = ['website'=>$website, 'sitemap'=>null, 'copyscape'=>null, 'debug'=>['q1'=>$u1,'q2'=>$u2,'count'=>count($recs)]];
    $dl = function($verId) use ($instance_url, $sf_api_ver, $headers) {
        $vurl = trailingslashit($instance_url).'services/data/'.$sf_api_ver.'/sobjects/ContentVersion/'.$verId.'/VersionData';
        $resp = wp_remote_get($vurl, ['timeout'=>30,'headers'=>$headers]);
        if ( is_wp_error($resp) ) return $resp;
        if ( 200 !== wp_remote_retrieve_response_code($resp) ) return new WP_Error('sf_download','Download failed: '. wp_remote_retrieve_body($resp));
        return wp_remote_retrieve_body($resp);
    };

    foreach (['sitemap','copyscape'] as $key) {
        if ( ! empty($pick[$key]['ContentDocument']['LatestPublishedVersionId']) ) {
            $verId = $pick[$key]['ContentDocument']['LatestPublishedVersionId'];
            $blob  = $dl($verId);
            if ( ! is_wp_error($blob) ) {
                $name = ($pick[$key]['ContentDocument']['Title'] ?? 'file').'.csv';
                $out[$key] = ['name'=>$name,'data'=>$blob,'version'=>$verId];
            } else {
                $out[$key] = ['name'=>'','data'=>'','error'=>$blob->get_error_message()];
            }
        }
    }

    if ( empty($out['website']) && !empty($out['sitemap']['name']) && preg_match('/^([A-Za-z0-9.-]+)-sitemap\.csv$/', $out['sitemap']['name'], $m) ) {
        $out['website'] = 'https://'.$m[1];
    }

    return $out;
}}

// Salesforce URL detector — never show these in the sitemap
if ( ! function_exists('ai_sc_is_salesforce_url') ) {
    function ai_sc_is_salesforce_url( $url ) {
    ai_sc_debug(__FUNCTION__ . '() start', array('args'=>func_get_args()));

        $host = parse_url( $url, PHP_URL_HOST );
        if ( ! $host ) return false;
        $host = strtolower($host);
        return (
            strpos($host, '.lightning.force.com') !== false ||
            strpos($host, '.my.salesforce.com')    !== false ||
            strpos($host, '.salesforce.com')       !== false ||
            strpos($host, '.force.com')            !== false
        );
    }
}

// ======================================================================================

@set_time_limit(600); // increase to 10 minutes

// Increase WordPress admin timeout to get around the two minute timeout issue
add_filter('admin_post_max_size', function() { return '512M'; }); // big size if needed
add_filter('upload_max_size', function() { return '512M'; }); // big size if needed

// Extend WordPress nonce expiration ONLY for ai-smartcopy plugin page
add_filter('nonce_life', function($lifetime) {
    if ( isset($_GET['page']) && $_GET['page'] === 'ai-smartcopy' ) {
        return 24 * 60 * 60; // 24 hours in seconds
    }
    return $lifetime;
});


// track how many pages we’ll process
$page_count = 0;

/**
 * Show the “site too large” admin notice immediately.
 */
function scm_show_timeout_notice() {
    echo '<div class="notice notice-error is-dismissible">
            <p>This site is too large to check at once. Please try again or select fewer HTML files to download next time.</p>
          </div>';
    echo '<input type="hidden" id="csv-download-url" value="' 
                       . esc_url( $download_url ) . '" />';
}

if ( ! function_exists('delete_folder_recursively') ) {
    function delete_folder_recursively( $folder ) {
        if ( ! is_dir( $folder ) ) {
            return;
        }
        foreach ( scandir( $folder ) as $item ) {
            if ( $item === '.' || $item === '..' ) {
                continue;
            }
            $path = $folder . DIRECTORY_SEPARATOR . $item;
            if ( is_dir( $path ) ) {
                delete_folder_recursively( $path );
            } else {
                unlink( $path );
            }
        }
        rmdir( $folder );
    }
}

// ---------------------------------------------
// 1) Handle the POST and output CSV if found
// ---------------------------------------------
ai_sc_debug('POST received', array('keys'=>array_keys($_POST), 'phase'=>isset($_POST['copyscape_phase'])?$_POST['copyscape_phase']:null));
if (
    $_SERVER['REQUEST_METHOD'] === 'POST'
    && ! empty( $_POST['action'] )
    && $_POST['action'] === 'sitemap'
    && check_admin_referer( 'sitemap_nonce' )
) {
    // Checker variable for if a sitemap can't be pulled
    $completeWithoutIssue = false;
    
    $checkDeadline = microtime(true) + 105;   // 1 min 45 seconds before timeout message
    $heads_up_html     = '';                                   // placeholder for the warning
    $is_choices_screen = empty($_POST['confirm_page_choices']) // only true on the selection screen
                         && empty($_POST['copyscape_phase']);  // (not during Copyscape resume runs)

    
        // The uploaded CSV is now the source of the site and sitemap URLs.
        $raw_input = isset($_POST['domainToCheck']) ? trim( wp_strip_all_tags( $_POST['domainToCheck'] ) ) : '';
        $proj_id   = ''; // Keep the legacy branch unreachable during the transition away from Salesforce.
        $ai_sc_urls_override = []; // if set, we'll use these instead of crawling
        $ai_sc_copyscape_map = [];
        $sf_debug_msgs = [];

        $sitemap_upload = isset($_FILES['sitemap_csv']) && is_array($_FILES['sitemap_csv'])
            ? $_FILES['sitemap_csv']
            : null;
        $copyscape_upload = isset($_FILES['copyscape_csv']) && is_array($_FILES['copyscape_csv'])
            ? $_FILES['copyscape_csv']
            : null;

        $has_sitemap_upload = $sitemap_upload
            && UPLOAD_ERR_NO_FILE !== (int) ($sitemap_upload['error'] ?? UPLOAD_ERR_NO_FILE);
        $has_copyscape_upload = $copyscape_upload
            && UPLOAD_ERR_NO_FILE !== (int) ($copyscape_upload['error'] ?? UPLOAD_ERR_NO_FILE);

        $csv_upload_validation_error = '';
        $sitemap_filename_match = [];
        $copyscape_filename_match = [];

        if ( $has_sitemap_upload || $has_copyscape_upload ) {
            if ( ! $has_sitemap_upload || ! $has_copyscape_upload ) {
                $csv_upload_validation_error = 'Please upload both the sitemap CSV and the Copyscape CSV.';
            } else {
                $submitted_sitemap_name = sanitize_file_name( wp_unslash( $sitemap_upload['name'] ?? '' ) );
                $submitted_copyscape_name = sanitize_file_name( wp_unslash( $copyscape_upload['name'] ?? '' ) );
                $domain_pattern = '(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}';

                if ( ! preg_match('/^(' . $domain_pattern . ')-sitemap\.csv$/i', $submitted_sitemap_name, $sitemap_filename_match) ) {
                    $csv_upload_validation_error = 'The sitemap filename must start with a domain and end with "-sitemap.csv", such as moderncaredental.com-sitemap.csv.';
                } elseif ( ! preg_match('/^(' . $domain_pattern . ')-copyscape\.csv$/i', $submitted_copyscape_name, $copyscape_filename_match) ) {
                    $csv_upload_validation_error = 'The Copyscape filename must start with a domain and end with "-copyscape.csv", such as auroraangeles.com-copyscape.csv.';
                } else {
                    $submitted_sitemap_host = preg_replace('/^www\./i', '', strtolower($sitemap_filename_match[1]));
                    $submitted_copyscape_host = preg_replace('/^www\./i', '', strtolower($copyscape_filename_match[1]));
                    if ( $submitted_sitemap_host !== $submitted_copyscape_host ) {
                        $csv_upload_validation_error = sprintf(
                            'These files are for different websites: the sitemap is for %1$s, but the Copyscape report is for %2$s. Please upload two files for the same domain.',
                            $sitemap_filename_match[1],
                            $copyscape_filename_match[1]
                        );
                    }
                }
            }
        }

        if ( $csv_upload_validation_error !== '' ) {
            echo '<div class="notice notice-warning is-dismissible"><p><strong>CSV files were not uploaded.</strong> '
                . esc_html($csv_upload_validation_error)
                . ' Please choose the correct files below and try again.</p></div>';
            $completeWithoutIssue = true;
        } elseif ( $has_sitemap_upload || $has_copyscape_upload ) {
            $upload_error = (int) ($sitemap_upload['error'] ?? UPLOAD_ERR_NO_FILE);
            $upload_name  = sanitize_file_name( wp_unslash( $sitemap_upload['name'] ?? '' ) );
            $upload_size  = (int) ($sitemap_upload['size'] ?? 0);
            $upload_tmp   = $sitemap_upload['tmp_name'] ?? '';

            if ( UPLOAD_ERR_OK !== $upload_error ) {
                wp_die( esc_html__( 'The sitemap CSV could not be uploaded. Please try again.', 'inception-techboost' ) );
            }
            if ( $upload_size <= 0 || $upload_size > 5 * MB_IN_BYTES ) {
                wp_die( esc_html__( 'The sitemap CSV must be between 1 byte and 5 MB.', 'inception-techboost' ) );
            }
            if ( strtolower( pathinfo( $upload_name, PATHINFO_EXTENSION ) ) !== 'csv' ) {
                wp_die( esc_html__( 'Please upload a CSV file.', 'inception-techboost' ) );
            }
            $host = preg_replace('/^www\./i', '', strtolower($sitemap_filename_match[1]));

            $domain = 'https://' . $host;
            $_POST['domainToCheck'] = $domain;

            $handle = @fopen( $upload_tmp, 'r' );
            if ( ! $handle ) {
                wp_die( esc_html__( 'The uploaded sitemap CSV could not be read.', 'inception-techboost' ) );
            }

            $urls = [ $domain . '/' ];
            $row_count = 0;
            while ( false !== ($columns = fgetcsv($handle)) ) {
                $row_count++;
                if ( $row_count > 5000 ) {
                    fclose( $handle );
                    wp_die( esc_html__( 'The sitemap CSV contains more than 5,000 rows.', 'inception-techboost' ) );
                }

                $value = isset($columns[0]) ? trim( (string) $columns[0] ) : '';
                $value = preg_replace('/^\xEF\xBB\xBF/', '', $value);
                if ( $value === '' ) {
                    continue;
                }

                $label = strtolower( trim($value, " \t\n\r\0\x0B\"'") );
                if ( $row_count === 1 && ! preg_match('#^(?:https?://|/)#i', $value)
                    && (strpos($label, 'url') !== false || strpos($label, 'page') !== false)
                ) {
                    continue;
                }

                if ( preg_match('#^https?://#i', $value) ) {
                    $path = wp_parse_url( $value, PHP_URL_PATH );
                    $query = wp_parse_url( $value, PHP_URL_QUERY );
                    $value = is_string($path) && $path !== '' ? $path : '/';
                    if ( is_string($query) && $query !== '' ) {
                        $value .= '?' . $query;
                    }
                }

                $value = '/' . ltrim( $value, '/' );
                $url = esc_url_raw( $domain . $value );
                if ( $url !== '' ) {
                    $urls[] = $url;
                }
            }
            fclose( $handle );

            $seen = [];
            foreach ( $urls as $url ) {
                $key = untrailingslashit( $url );
                if ( isset($seen[$key]) ) {
                    continue;
                }
                $seen[$key] = true;
                $ai_sc_urls_override[] = $url;
            }

            if ( count($ai_sc_urls_override) < 2 ) {
                wp_die( esc_html__( 'No page paths were found in the uploaded sitemap CSV.', 'inception-techboost' ) );
            }

            $copyscape_error = (int) ($copyscape_upload['error'] ?? UPLOAD_ERR_NO_FILE);
            $copyscape_name  = sanitize_file_name( wp_unslash( $copyscape_upload['name'] ?? '' ) );
            $copyscape_size  = (int) ($copyscape_upload['size'] ?? 0);
            $copyscape_tmp   = $copyscape_upload['tmp_name'] ?? '';

            if ( UPLOAD_ERR_OK !== $copyscape_error ) {
                wp_die( esc_html__( 'The Copyscape CSV could not be uploaded. Please try again.', 'inception-techboost' ) );
            }
            if ( $copyscape_size <= 0 || $copyscape_size > 10 * MB_IN_BYTES ) {
                wp_die( esc_html__( 'The Copyscape CSV must be between 1 byte and 10 MB.', 'inception-techboost' ) );
            }
            if ( strtolower( pathinfo( $copyscape_name, PATHINFO_EXTENSION ) ) !== 'csv' ) {
                wp_die( esc_html__( 'Please upload a CSV file for the Copyscape results.', 'inception-techboost' ) );
            }

            $copyscape_host = preg_replace('/^www\./i', '', strtolower($copyscape_filename_match[1]));

            $copyscape_handle = @fopen( $copyscape_tmp, 'r' );
            if ( ! $copyscape_handle ) {
                wp_die( esc_html__( 'The uploaded Copyscape CSV could not be read.', 'inception-techboost' ) );
            }

            $header = fgetcsv( $copyscape_handle );
            if ( ! is_array($header) ) {
                fclose( $copyscape_handle );
                wp_die( esc_html__( 'The Copyscape CSV is empty.', 'inception-techboost' ) );
            }

            $header = array_map(
                static function($value) {
                    return strtolower( trim( preg_replace('/^\xEF\xBB\xBF/', '', (string) $value) ) );
                },
                $header
            );
            $url_column = null;
            $percentage_column = null;
            foreach ( $header as $index => $heading ) {
                if ( $url_column === null && strpos($heading, 'url') !== false ) {
                    $url_column = $index;
                }
                if ( $percentage_column === null
                    && ($heading === 'percentage' || strpos($heading, 'percent') !== false)
                ) {
                    $percentage_column = $index;
                }
            }

            if ( $url_column === null || $percentage_column === null ) {
                fclose( $copyscape_handle );
                wp_die( esc_html__( 'The Copyscape CSV must include Page URL and Percentage columns.', 'inception-techboost' ) );
            }

            $copyscape_rows = 0;
            $copyscape_line_count = 0;
            while ( false !== ($columns = fgetcsv($copyscape_handle)) ) {
                $copyscape_line_count++;
                if ( $copyscape_line_count > 5000 ) {
                    fclose( $copyscape_handle );
                    wp_die( esc_html__( 'The Copyscape CSV contains more than 5,000 result rows.', 'inception-techboost' ) );
                }

                $page_url = trim( (string) ($columns[$url_column] ?? '') );
                $percentage = trim( (string) ($columns[$percentage_column] ?? '') );
                if ( $page_url === '' || $percentage === '' ) {
                    continue;
                }

                if ( ! preg_match('#^https?://#i', $page_url) ) {
                    $page_url = $domain . '/' . ltrim( $page_url, '/' );
                }
                $key = ai_sc_norm_url_key( $page_url );
                if ( $key === '' ) {
                    continue;
                }

                $numeric_percentage = (float) preg_replace('/[^0-9.]/', '', $percentage);
                $ai_sc_copyscape_map[$key] = min( 100.0, max( 0.0, $numeric_percentage ) );
                $copyscape_rows++;
            }
            fclose( $copyscape_handle );

            if ( empty($ai_sc_copyscape_map) ) {
                wp_die( esc_html__( 'No usable URL percentages were found in the Copyscape CSV.', 'inception-techboost' ) );
            }

            $matched_copyscape_rows = 0;
            foreach ( $ai_sc_urls_override as $sitemap_url ) {
                if ( array_key_exists(ai_sc_norm_url_key($sitemap_url), $ai_sc_copyscape_map) ) {
                    $matched_copyscape_rows++;
                }
            }
            if ( $matched_copyscape_rows === 0 ) {
                wp_die( esc_html__( 'The Copyscape CSV does not contain any URLs from the uploaded sitemap.', 'inception-techboost' ) );
            }

            $sitemap_cache_key = 'ai_sc_csv_' . get_current_user_id() . '_' . md5($host);
            set_transient(
                $sitemap_cache_key,
                [
                    'domain'         => $domain,
                    'urls'           => $ai_sc_urls_override,
                    'copyscape_map'  => $ai_sc_copyscape_map,
                ],
                DAY_IN_SECONDS
            );

            $completeWithoutIssue = true;
            echo '<div class="notice notice-success"><p><strong>Website found:</strong> '
                . esc_html($domain)
                . ' &mdash; loaded '
                . intval(count($ai_sc_urls_override) - 1)
                . ' sitemap URL(s) and '
                . intval(count($ai_sc_copyscape_map))
                . ' Copyscape result(s), with '
                . intval($matched_copyscape_rows)
                . ' matching URL(s).</p></div>';
        } elseif ( $proj_id ) {
            $sf_assets = ai_sc_sf_fetch_project_assets( $proj_id, 'v59.0' );
            if ( is_wp_error($sf_assets) ) {
                echo '<div class="notice notice-error"><p><strong>Salesforce:</strong> ' . esc_html( $sf_assets->get_error_message() ) . '</p></div>';
                // fall back to normal domain handling below
                $domain = $raw_input;
                $domain = preg_replace( '#^https?://#i', '', $domain );
                $http_url = 'http://' . $domain;
                $response = wp_remote_head( $http_url, [ 'timeout' => 10 ] );
                if ( ! is_wp_error( $response ) ) {
                    $response_code = wp_remote_retrieve_response_code( $response );
                    if ( in_array( $response_code, [301, 302], true ) ) {
                        $location = wp_remote_retrieve_header( $response, 'Location' );
                        if ( $location && strpos( $location, 'https://' ) === 0 ) {
                            $domain = 'https://' . $domain;
                        }
                    }
                }
                $domain = untrailingslashit( esc_url_raw( $domain ) );
            } else {

// Debug panel
$sf_debug_msgs[] = 'Project Website__c: ' . esc_html( $sf_assets['website'] ?? '(empty)' );
$sf_debug_msgs[] = 'Linked file count: ' . intval( $sf_assets['debug']['count'] ?? 0 );
if ( ! empty($sf_assets['sitemap']['name']) ) $sf_debug_msgs[] = 'Found sitemap CSV: ' . esc_html($sf_assets['sitemap']['name']) . ' (Version ' . esc_html($sf_assets['sitemap']['version']) . ')';
else $sf_debug_msgs[] = 'No sitemap CSV found on Project';
if ( ! empty($sf_assets['copyscape']['name']) ) $sf_debug_msgs[] = 'Found copyscape CSV: ' . esc_html($sf_assets['copyscape']['name']) . ' (Version ' . esc_html($sf_assets['copyscape']['version']) . ')';
else $sf_debug_msgs[] = 'No copyscape CSV found on Project';

// Remove Comments when Debugging for Salesforce Notice
//echo '<div class="notice notice-info"><p><strong>Salesforce lookup succeeded.</strong><br>' . implode('<br>', $sf_debug_msgs) . '</p></div>';

echo '<p><strong>All pages that have been checked in Copyscape and that are less than 30% copied content can use AI SmartCopy to add an opening paragraph before keeping the rest of the content unchanged. All pages above 30% must be rewritten.</strong> Please select which pages or blogs you want to SmartCopy.<br></p>';

// Choose base domain (prefer sitemap filename’s domain)
$base_domain = '';
if ( ! empty($sf_assets['sitemap']['name']) && preg_match('/^([A-Za-z0-9.-]+)-sitemap\.csv$/i', $sf_assets['sitemap']['name'], $m_dom) ) {
    $base_domain = 'https://' . strtolower($m_dom[1]);
} elseif ( ! empty($sf_assets['website']) ) {
    $base_domain = untrailingslashit( esc_url_raw( $sf_assets['website'] ) );
} else {
    $base_domain = untrailingslashit( esc_url_raw( $raw_input ) );
}
$domain = $base_domain; // force the UI to this site

// Make sure any downstream code (or logs) sees the real site, not the Project link
$_POST['domainToCheck'] = $domain;

$ai_sc_urls_override = [];
$urls = [];

// --- Parse sitemap CSV using the chosen base domain ---
if ( ! empty($sf_assets['sitemap']['data']) ) {
    $csv = (string) $sf_assets['sitemap']['data'];
    if ( substr($csv,0,3) === "\xEF\xBB\xBF" ) { $csv = substr($csv,3); }
    $csv   = trim($csv);
    // split into non-empty lines
    $lines = preg_split('/\R+/', $csv, -1, PREG_SPLIT_NO_EMPTY);

    $has_header = false;
    $start = 0;
    if ($lines && count($lines) > 0) {
        $first_cols   = str_getcsv($lines[0]);
        $first_token  = isset($first_cols[0]) ? trim($first_cols[0], "\x00..\x1F\"' ") : '';
        // Treat as header only if it looks like labels (contains "url"/"page" and is not a path/URL)
        if ( $first_token !== '' && !preg_match('#^(https?://|/)#i', $first_token) ) {
            foreach ($first_cols as $fc) {
                $fcv = strtolower(trim($fc));
                if (strpos($fcv, 'url') !== false || strpos($fcv, 'page') !== false) { $has_header = true; break; }
            }
        }
        $start = $has_header ? 1 : 0;

        for ($i = $start; $i < count($lines); $i++) {
            $line = $lines[$i];
            if ($line === '') continue;
            $cols = str_getcsv($line);
            $u = trim($cols[0] ?? '');
            if ($u === '') continue;

            // Normalize to absolute using $base_domain
            if ( preg_match('#^https?://#i', $u) ) {
                $full = $u;
            } elseif ( $u[0] === '/' ) {
                $full = rtrim($base_domain,'/') . $u;
            } else {
                $full = rtrim($base_domain,'/') . '/' . ltrim($u,'/');
            }
            $urls[] = $full;
        }
    }

    // Always include homepage at the top
    $home = rtrim($base_domain,'/') . '/';
    array_unshift($urls, $home);

    // Deduplicate while preserving order (homepage stays at top)
    $seen = [];
    $dedup = [];
    foreach ($urls as $u) {
        $key = untrailingslashit( esc_url_raw($u) );
        if (isset($seen[$key])) continue;
        $seen[$key] = true;
        $dedup[] = $u;
    }
    $ai_sc_urls_override = $dedup;

    // Optional: quick debug to confirm header detection and counts
    //echo '<div class="notice notice-info"><p>'
    //   . 'Sitemap CSV header detected: <code>' . ($has_header ? 'yes' : 'no') . '</code>; '
    //   . 'URLs parsed (including homepage): <code>' . count($ai_sc_urls_override) . '</code>'
    //   . '</p></div>';
}

// --- Parse Copyscape CSV and normalize relative to the same $base_domain ---
if ( ! empty($sf_assets['copyscape']['data']) ) {
    $csv = (string) $sf_assets['copyscape']['data'];
    if ( substr($csv,0,3) === "\xEF\xBB\xBF" ) { $csv = substr($csv,3); }
    $csv   = trim($csv);
    $lines = preg_split('/\R+/', $csv, -1, PREG_SPLIT_NO_EMPTY);
    $map = [];

    if ($lines && count($lines) > 0) {
        // Assume first two columns = Page URL, Percentage
        $urlCol = 0; $pctCol = 1;

        // If a header line exists, prefer it to locate the columns
        $maybeHeader = str_getcsv($lines[0]);
        $firstToken  = isset($maybeHeader[0]) ? trim($maybeHeader[0], "\x00..\x1F\"' ") : '';
        $hasHeader   = false;
        if ($firstToken !== '' && !preg_match('#^(https?://|/)#i', $firstToken)) {
            $lower = array_map(function($v){ return strtolower(trim($v)); }, $maybeHeader);
            foreach ($lower as $k=>$hv) {
                if (strpos($hv,'url') !== false) { $urlCol = $k; break; }
            }
            foreach ($lower as $k=>$hv) {
                if ($hv === 'percentage' || strpos($hv,'percent') !== false) { $pctCol = $k; break; }
            }
            $hasHeader = true;
        }

        $start = $hasHeader ? 1 : 0;
        for ($i=$start; $i<count($lines); $i++) {
            $cols = str_getcsv($lines[$i]);
            $u = trim($cols[$urlCol] ?? '');
            $p = trim($cols[$pctCol] ?? '');

            if ($u === '') continue;

            // Normalize to absolute using $base_domain
            if ( ! preg_match('#^https?://#i', $u) ) {
                if ( $u[0] === '/' ) $u = rtrim($base_domain,'/') . $u;
                else $u = rtrim($base_domain,'/') . '/' . ltrim($u,'/');
            }
            $val  = floatval(str_replace('%','',$p));
            $key  = ai_sc_norm_url_key($u);
            if ($key !== '') {
                $map[$key] = $val;
            }
        }
    }

    if (!empty($map)) {
        // OPTIONAL: add an alias entry with trailing slash for the homepage
        $aug = [];
        foreach ($map as $k => $v) {
            $aug[$k] = $v;
            if (preg_match('#^https?://[^/]+$#', $k)) { // homepage w/o trailing slash
                $aug[$k.'/'] = $v; // add alias with slash
            }
        }
        echo '<script>window.hasCopyscapeFile=true;window.copyscapePctMap='.json_encode($aug).';</script>';
    } else {
        echo '<div class="notice notice-warning"><p>Copyscape CSV parsed but no rows detected.</p></div>';
    }
}
}
        } else {
            // Original domain normalization
            $domain = $raw_input;
            $domain = preg_replace( '#^https?://#i', '', $domain );
            $http_url = 'http://' . $domain;
            $response = wp_remote_head( $http_url, [ 'timeout' => 10 ] );
            if ( ! is_wp_error( $response ) ) {
                $response_code = wp_remote_retrieve_response_code( $response );
                if ( in_array( $response_code, [301, 302], true ) ) {
                    $location = wp_remote_retrieve_header( $response, 'Location' );
                    if ( $location && strpos( $location, 'https://' ) === 0 ) {
                        $domain = 'https://' . $domain;
                    }
                }
            }
            $domain = untrailingslashit( esc_url_raw( $domain ) );

            $cached_host = wp_parse_url( $domain, PHP_URL_HOST );
            if ( $cached_host ) {
                $sitemap_cache_key = 'ai_sc_csv_' . get_current_user_id() . '_' . md5(strtolower($cached_host));
                $cached_sitemap = get_transient( $sitemap_cache_key );
                if ( is_array($cached_sitemap)
                    && ! empty($cached_sitemap['urls'])
                    && is_array($cached_sitemap['urls'])
                ) {
                    $ai_sc_urls_override = $cached_sitemap['urls'];
                    $ai_sc_copyscape_map = isset($cached_sitemap['copyscape_map'])
                        && is_array($cached_sitemap['copyscape_map'])
                        ? $cached_sitemap['copyscape_map']
                        : [];
                }
            }
        }

        if ( ! empty($ai_sc_copyscape_map) ) {
            $batch_results = [];
            foreach ( $ai_sc_urls_override as $sitemap_url ) {
                $key = ai_sc_norm_url_key( $sitemap_url );
                if ( $key !== '' && array_key_exists($key, $ai_sc_copyscape_map) ) {
                    $batch_results[$sitemap_url] = [
                        'percentage' => $ai_sc_copyscape_map[$key],
                    ];
                }
            }
            echo '<script>window.hasCopyscapeFile=true;window.copyscapePctMap='
                . wp_json_encode($ai_sc_copyscape_map)
                . ';</script>';
        }
    // ──────────────────────────────────────────────────────────────
        // Warn if this site appears in ai_smartcopy.log (flexible match)
        // ──────────────────────────────────────────────────────────────
        $log_file = plugin_dir_path( __FILE__ ) . 'ai_smartcopy.log';
        
        if ( file_exists( $log_file ) ) {
            // Normalize the just-entered domain to a bare host (no www, lowercase)
            $queried_host = parse_url( $domain, PHP_URL_HOST );
            $queried_host = $queried_host ? preg_replace( '/^www\./i', '', strtolower( $queried_host ) ) : '';
        
            $already_checked = false;
            $last_time       = '';
            $last_user       = '';
            $match_count     = 0;
        
            // Read log line-by-line: "<time>\t<user_last_name>\t<domain>\t<copyscape_balance>"
            $fh = @fopen( $log_file, 'r' );
            if ( $fh ) {
                while ( ( $line = fgets( $fh ) ) !== false ) {
                    $line = trim( $line );
                    if ( $line === '' ) { continue; }
        
                    $parts = explode( "\t", $line );
                    // Need at least time, last name, and domain
                    if ( count( $parts ) >= 3 ) {
                        $time_in_log   = $parts[0];
                        $user_in_log   = $parts[1];
                        $domain_in_log = $parts[2];
        
                        // Normalize the logged domain to a bare host (no www, lowercase)
                        // Add scheme if missing so parse_url works reliably.
                        $candidate = preg_match( '#^https?://#i', $domain_in_log ) ? $domain_in_log : 'https://' . $domain_in_log;
                        $log_host  = parse_url( $candidate, PHP_URL_HOST );
                        $log_host  = $log_host ? preg_replace( '/^www\./i', '', strtolower( $log_host ) ) : '';
        
                        if ( $queried_host && $log_host && $log_host === $queried_host ) {
                            $already_checked = true;
                            $match_count++;
                            // Keep the last occurrence (file grows by appending)
                            $last_time = $time_in_log;
                            $last_user = $user_in_log;
                        }
                    }
                }
                fclose( $fh );
            }
        }

    // WordPress’s built-in sitemap index goes first
    $possible_sitemaps = [
        $domain . '/wp-sitemap.xml',
        $domain . '/sitemap.xml',
        $domain . '/sitemap_index.xml',
        $domain . '/post-sitemap.xml',
        $domain . '/page-sitemap.xml',
        $domain . '/blog-sitemap.xml',
    ];

    $csv_created = false;
    $zip_created = false;
    $copyscape_ran = false;

        // ---------------------------------------------
        // 2) Find all valid sitemap URLs
        // ---------------------------------------------
        $http_args = [
            'timeout'     => 15,
            'redirection' => 5,
            'headers'     => [
                'User-Agent'      => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
                'Accept'          => 'application/xml,text/xml,application/xhtml+xml,text/html;q=0.9,*/*;q=0.8',
                'Accept-Encoding' => 'gzip, deflate, br',
                'Accept-Language' => 'en-US,en;q=0.9',
            ],
        ];
        
        $found = ! empty($ai_sc_urls_override) ? [ 'uploaded sitemap CSV' ] : [];
        foreach ( empty($ai_sc_urls_override) ? $possible_sitemaps : [] as $url ) {
            $response = wp_remote_head( $url, $http_args );
            if ( is_wp_error( $response )
                || ! in_array( wp_remote_retrieve_response_code( $response ), [200, 301, 302], true )
            ) {
                $response = wp_remote_get( $url, $http_args );
            }
            if ( ! is_wp_error( $response )
                && 200 === wp_remote_retrieve_response_code( $response )
            ) {
                $found[] = $url;
            }
        }
        
        if ( empty( $found ) && empty($ai_sc_urls_override) ) {
            // Try checking robots.txt for another sitemap location
            $robots_url = $domain . '/robots.txt';
            $robots_resp = wp_remote_get( $robots_url, $http_args );
            if ( ! is_wp_error( $robots_resp ) && 200 === wp_remote_retrieve_response_code( $robots_resp ) ) {
                $robots_body = wp_remote_retrieve_body( $robots_resp );
                if ( preg_match_all( '/(?i)^sitemap:\s*(\S+)/m', $robots_body, $matches ) ) {
                    foreach ( $matches[1] as $sitemap_url ) {
                        // Sanitize and test the discovered sitemap URL
                        $sitemap_url = esc_url_raw( trim( $sitemap_url ) );
                        $test = wp_remote_get( $sitemap_url, $http_args );
                        if ( ! is_wp_error( $test ) && 200 === wp_remote_retrieve_response_code( $test ) ) {
                            $found[] = $sitemap_url;
                        }
                    }
                }
            }
        }

        //Debug to see if something has been found
        //echo '<pre style="background:#fff; padding:10px; border:1px solid #ccc;"><strong>$found contents:</strong><br>';
        //print_r( $found );
        //echo '</pre>';
        
        if ( empty( $found ) ) {
            echo '<div class="notice notice-error"><p><strong>Sorry, could not find a sitemap found at that domain.</strong> (Tried: '
                . implode( ', ', array_map( 'esc_html', $possible_sitemaps ) )
                . ')</p></div>';
        } else {
        // ---------------------------------------------
        // 3) Pull <loc> tags (with namespace) and build URL list
        // ---------------------------------------------
        $all_urls  = [];
        $blog_urls = [];
        $warnings  = [];
        libxml_use_internal_errors( true );

        $all_urls = $ai_sc_urls_override;
        foreach ( empty($ai_sc_urls_override) ? $found : [] as $sm_url ) {
            $resp = wp_remote_get( $sm_url, [ 'timeout' => 20 ] );
            if ( is_wp_error( $resp ) ) {
                $warnings[] = "Error fetching sitemap at $sm_url: " . $resp->get_error_message();
                continue;
            }

            $code = wp_remote_retrieve_response_code( $resp );
            $body = wp_remote_retrieve_body( $resp );
            
            // Check for block indicators
            if ( $code >= 400 || strlen( $body ) < 100 || stripos( $body, 'access denied' ) !== false || stripos( $body, 'sucuri' ) !== false || stripos( $body, 'cloudflare' ) !== false ) {
                echo '<div class="notice notice-error"><p><strong>Error: It looks like the server at <code>' . esc_html( $sm_url ) . '</code> is blocking requests from this function.</strong><br>';
                echo 'Please manually retrieve the sitemap.</p></div>';
                $completeWithoutIssue = true;
                continue;
            }

            if ( empty( trim( $body ) ) ) {
                $warnings[] = "Empty sitemap at $sm_url";
                continue;
            }
            libxml_clear_errors();
            $xml = simplexml_load_string( $body );
            if ( ! $xml ) {
                $errors = libxml_get_errors();
                $msgs   = array_map( function( $e ) { return trim( $e->message ); }, $errors );
                //$warnings[] = "Malformed XML at $sm_url: " . implode( "; ", $msgs );
                continue;
            }

            $namespaces = $xml->getDocNamespaces(false);
            $ns = array_shift($namespaces) ?: 'http://www.sitemaps.org/schemas/sitemap/0.9';
            $xml->registerXPathNamespace('sm', $ns);

            // URL entries
            $locs = $xml->xpath( '//*[local-name()="loc"]' ) ?: [];
            foreach ( $locs as $loc ) {
                $url = (string) $loc;
                $all_urls[] = $url;
                // if this sitemap's filename contains "post-sitemap" or "blog-sitemap", mark it as a blog URL
                if (
                    false !== strpos( $sm_url, 'post-sitemap' ) 
                    || false !== strpos( $sm_url, 'blog-sitemap' )
                ) {
                    $blog_urls[] = $url;
                }
            }

            // Nested sitemap-index entries
            $indexUrls = $xml->xpath( '//sm:sitemap/sm:loc' ) ?: [];
            foreach ( $indexUrls as $childLoc ) {
                $child_url  = (string) $childLoc;
                $child_resp = wp_remote_get( $child_url, [ 'timeout' => 20 ] );
                if ( is_wp_error( $child_resp ) ) {
                    $warnings[] = "Error fetching child sitemap at $child_url: " . $child_resp->get_error_message();
                    continue;
                }
                $child_body = wp_remote_retrieve_body( $child_resp );
                if ( empty( trim( $child_body ) ) ) {
                    $warnings[] = "Empty child sitemap at $child_url";
                    continue;
                }
                libxml_clear_errors();
                $child_xml = simplexml_load_string( $child_body );
                if ( ! $child_xml ) {
                    $errors = libxml_get_errors();
                    $msgs   = array_map( function( $e ) { return trim( $e->message ); }, $errors );
                    //$warnings[] = "Malformed XML at child sitemap $child_url: " . implode( "; ", $msgs );
                    continue;
                }
                $child_xml->registerXPathNamespace( 'sm', $ns );
                $child_locs = $child_xml->xpath( '//*[local-name()="loc"]' ) ?: [];
                if ( empty( $child_locs ) ) {
                    $warnings[] = "No <loc> tags in child sitemap at $child_url";
                } else {
                    foreach ( $child_locs as $c ) {
                        $url = (string) $c;
                        $all_urls[] = $url;
                        // If this child sitemap URL contains "post-sitemap" or "blog-sitemap", mark it as a blog URL too
                        if (
                            false !== strpos( $child_url, 'post-sitemap' ) 
                            || false !== strpos( $child_url, 'blog-sitemap' )
                        ) {
                            $blog_urls[] = $url;
                        }
                    }
                }
            }
        }

        // Show any warnings
        if ( ! empty( $warnings ) ) {
            echo '<div class="notice notice-warning"><ul>';
            foreach ( $warnings as $msg ) {
                echo '<li>' . esc_html( $msg ) . '</li>';
            }
            echo '</ul></div>';
        }

        // ---------------------------------------------
        // 4) Deduplicate & save only the slug paths to CSV
        // ---------------------------------------------
        $all_urls = array_unique( $all_urls );
        $blog_urls = array_unique( $blog_urls );
        
        // ——————————————————————————————
        // 4a) Filter out image URLs (png, jpg, gif, etc.) AND .xml URLs
        // ——————————————————————————————
        $all_urls = array_filter( $all_urls, function( $u ) {
            return ! preg_match(
                '/\.(?:jpe?g|png|gif|svg|webp|bmp|ico|xml)(?:[\?\#].*)?$/i',
                $u
            );
        } );
        
        $img_regex = '/\.(?:jpe?g|png|gif|svg|webp|bmp|ico|xml)(?:[\?\#].*)?$/i';
        $all_urls  = array_filter( $all_urls, function( $u ) use ( $img_regex ) {
            return ! preg_match( $img_regex, $u );
        } );
        $blog_urls = array_filter( $blog_urls, function( $u ) use ( $img_regex ) {
            return ! preg_match( $img_regex, $u );
        } );
        
        // —————————————————————————
        // 4b) Normalize http/https duplicates into one canonical URL
        // —————————————————————————
        $normalized_all = [];
        foreach ( $all_urls as $u ) {
            $parts = parse_url( $u );
            // grab path or fallback to '/'
            $path = isset( $parts['path'] ) && $parts['path'] !== '' ? $parts['path'] : '/';
            // preserve any query string
            $query = ! empty( $parts['query'] ) ? '?' . $parts['query'] : '';
            // rebuild using the canonical $domain (which you forced to https://...)
            $normalized_all[] = $domain . $path . $query;
        }
        $all_urls = array_unique( $normalized_all );
        
        // ---------------------------------------------
        // 4c) ADDITIONAL BLOG‑IDENTIFICATION CHECKS
        //    (before confirmation form)
        // ---------------------------------------------
        
        if ( empty( $blog_urls ) ) {
            // 1) Try explicit post‑ and blog‑sitemaps
            foreach ( [ '/post-sitemap.xml', '/blog-sitemap.xml' ] as $suffix ) {
                $sm = $domain . $suffix;
                $resp = wp_remote_get( $sm, [ 'timeout' => 10, 'redirection' => 5 ] );
                if ( ! is_wp_error( $resp ) && 200 === wp_remote_retrieve_response_code( $resp ) ) {
                    $xml = @simplexml_load_string( wp_remote_retrieve_body( $resp ) );
                    if ( $xml ) {
                        foreach ( $xml->xpath('//loc') ?: [] as $loc ) {
                            $blog_urls[] = (string) $loc;
                        }
                    }
                }
            }
            $blog_urls = array_unique( $blog_urls );
        }
        
        // --------------------------------------------------------
        // 1.5) Check for /blog/ page and pull any posts via RSS
        // --------------------------------------------------------
        $blog_page_url = $domain . '/blog/';
        $resp = wp_remote_head( $blog_page_url, [ 'timeout' => 10, 'redirection' => 5 ] );
        if ( ! is_wp_error( $resp ) && 200 === wp_remote_retrieve_response_code( $resp ) ) {
            $feed_resp = wp_remote_get( $domain . '/blog/feed/', [ 'timeout' => 10, 'redirection' => 5 ] );
            if ( ! is_wp_error( $feed_resp ) && 200 === wp_remote_retrieve_response_code( $feed_resp ) ) {
                libxml_use_internal_errors( true );
                $feed_xml = simplexml_load_string( wp_remote_retrieve_body( $feed_resp ) );
                libxml_clear_errors();
                if ( $feed_xml && isset( $feed_xml->channel->item ) ) {
                    foreach ( $feed_xml->channel->item as $item ) {
                        $blog_urls[] = (string) $item->link;
                    }
                    $blog_urls = array_unique( $blog_urls );
                }
            }
        }
        
        // --------------------------------------------------------
        // 1.6) Fallback: pull via WP REST API and filter for /blog/
        // --------------------------------------------------------
        $api_url = trailingslashit( $domain ) . 'wp-json/wp/v2/posts?per_page=100&_fields=link';
        $api_resp = wp_remote_get( $api_url, [
            'timeout'     => 10,
            'redirection' => 5,
            'headers'     => [ 'Accept' => 'application/json' ],
        ] );
        if ( ! is_wp_error( $api_resp ) && 200 === wp_remote_retrieve_response_code( $api_resp ) ) {
            $posts = json_decode( wp_remote_retrieve_body( $api_resp ), true );
            if ( is_array( $posts ) ) {
                foreach ( $posts as $post ) {
                    if ( 0 === strpos( $post['link'], $domain . '/blog/' ) ) {
                        $blog_urls[] = $post['link'];
                    }
                }
                $blog_urls = array_unique( $blog_urls );
            }
        }
        
        // --------------------------------------------------------
        // 1.7) Fallback: crawl /blog/ HTML for <a href="/blog/...">
        // --------------------------------------------------------
        $resp = wp_remote_get( $blog_page_url, [
            'timeout'     => 10,
            'redirection' => 5,
        ] );
        if ( ! is_wp_error( $resp ) && 200 === wp_remote_retrieve_response_code( $resp ) ) {
            $html = wp_remote_retrieve_body( $resp );
            if ( $html ) {
                libxml_use_internal_errors( true );
                $dom   = new DOMDocument();
                $dom->loadHTML( $html );
                libxml_clear_errors();
                $xpath    = new DOMXPath( $dom );
                $anchors  = $xpath->query( "//a[contains(@href, '/blog/')]" );
                foreach ( $anchors as $a ) {
                    $href = $a->getAttribute( 'href' );
                    // normalize to absolute URL:
                    if ( 0 === strpos( $href, 'http' ) ) {
                        $url = $href;
                    } elseif ( 0 === strpos( $href, '/' ) ) {
                        $url = rtrim( $domain, '/' ) . $href;
                    } else {
                        $url = rtrim( $domain, '/' ) . '/blog/' . ltrim( $href, '/' );
                    }
                    $blog_urls[] = $url;
                }
                $blog_urls = array_unique( $blog_urls );
            }
        }

        if ( empty( $blog_urls ) ) {
            // 2) Catch WP’s default “posts” sitemaps at /wp-sitemap.xml
            $index_resp = wp_remote_get( $domain . '/wp-sitemap.xml', [
                'timeout'     => 10,
                'redirection' => 5,
            ] );
            if ( ! is_wp_error( $index_resp )
                 && 200 === wp_remote_retrieve_response_code( $index_resp )
            ) {
                $body = wp_remote_retrieve_body( $index_resp );
                libxml_use_internal_errors( true );
                $idx_xml = simplexml_load_string( $body );
                libxml_clear_errors();
                if ( $idx_xml ) {
                    // register the default namespace
                    $idx_xml->registerXPathNamespace( 'sm', 
                        $idx_xml->getDocNamespaces(false)[''] 
                        ?: 'http://www.sitemaps.org/schemas/sitemap/0.9'
                    );
                    // grab ONLY the “posts” sub-sitemap locs (not the “pages” ones)
                    $postMaps = $idx_xml->xpath('//sm:loc[contains(.,"/wp-sitemap-posts-post-")]') ?: [];
                    foreach ( $postMaps as $pm ) {
                        $ps_url = (string) $pm;
                        $ps_resp = wp_remote_get( $ps_url, [ 'timeout'=>10, 'redirection'=>5 ] );
                        if ( is_wp_error( $ps_resp )
                             || 200 !== wp_remote_retrieve_response_code( $ps_resp )
                        ) {
                            continue;
                        }
                        $ps_body = wp_remote_retrieve_body( $ps_resp );
                        libxml_use_internal_errors( true );
                        $ps_xml = simplexml_load_string( $ps_body );
                        libxml_clear_errors();
                        if ( ! $ps_xml ) {
                            continue;
                        }
                        $ps_xml->registerXPathNamespace( 'sm', 
                            $idx_xml->getDocNamespaces(false)[''] 
                            ?: 'http://www.sitemaps.org/schemas/sitemap/0.9'
                        );
                        $post_locs = $ps_xml->xpath('//sm:loc') ?: [];
                        foreach ( $post_locs as $loc ) {
                            $blog_urls[] = (string) $loc;
                        }
                    }
                    $blog_urls = array_unique( $blog_urls );
                }
            }
        }

        if ( empty( $blog_urls ) ) {
            // 3) Scrape /sitemap page for Posts section via DOM + XPath
            $resp = wp_remote_get( $domain . '/sitemap', [
                'timeout'     => 10,
                'redirection' => 5,
            ] );
            if ( ! is_wp_error( $resp ) && 200 === wp_remote_retrieve_response_code( $resp ) ) {
                $html = wp_remote_retrieve_body( $resp );
        
                // load into DOM
                libxml_use_internal_errors( true );
                $doc   = new DOMDocument();
                @$doc->loadHTML( $html );
                libxml_clear_errors();
        
                $xpath = new DOMXPath( $doc );
                // find any header or strong tag that contains the word "Posts"
                $nodes = $xpath->query(
                    "//h1[contains(translate(., 'POSTS', 'posts'), 'posts')]
                     | //h2[contains(translate(., 'POSTS', 'posts'), 'posts')]
                     | //h3[contains(translate(., 'POSTS', 'posts'), 'posts')]
                     | //strong[contains(translate(., 'POSTS', 'posts'), 'posts')]"
                );
        
                if ( $nodes->length ) {
                    // pick the first “Posts” heading
                    $postsHeading = $nodes->item(0);
        
                    // collect all <a> under the next sibling <ul> (or <div>) until the next header
                    $sibling = $postsHeading->nextSibling;
                    while ( $sibling
                        && ! in_array( $sibling->nodeName, ['ul','div'], true )
                        && ! preg_match( '/^h[1-6]$/i', $sibling->nodeName )
                    ) {
                        $sibling = $sibling->nextSibling;
                    }
        
                    if ( $sibling && in_array( $sibling->nodeName, ['ul','div'], true ) ) {
                        foreach ( $sibling->getElementsByTagName('a') as $a ) {
                            $href = $a->getAttribute('href');
                            
                            // Immediately clean trailing junk from the href itself
                            $href = rtrim( $href, '_/' ); // Remove trailing underscores and slashes
                            $href = preg_replace( '/(?:%20)+$/', '', $href ); // Remove trailing %20s
                            
                            
                            // Your already‐normalized $domain, e.g. "https://inceptiononlinemarketing.com"
                            $plain_host = parse_url( $domain, PHP_URL_HOST ); 
                            
                            // 1) Trim whitespace
                            $href = trim( $a->getAttribute('href') );
                            
                            // 2) If it literally contains your host again, pull just the “/path” portion
                            if ( preg_match(
                                   '#(?:https?://)?(?:www\.)?'
                                   . preg_quote( $plain_host, '#' )
                                   . '(/.*)$#i',
                                   $href,
                                   $m
                               ) ) {
                                // $m[1] is "/blog/10-chiropractic-marketing-tips-2018_"
                                $href = untrailingslashit( $domain ) . $m[1];
                            
                            } elseif ( 0 === strpos( $href, '//' ) ) {
                                // protocol‐relative
                                $scheme = parse_url( $domain, PHP_URL_SCHEME ) ?: 'https';
                                $href   = $scheme . ':' . $href;
                            
                            } elseif ( preg_match( '#^https?://#i', $href ) ) {
                                // absolute URL—leave as is
                            
                            } else {
                                // truly relative path
                                $href = untrailingslashit( $domain ) . '/' . ltrim( $href, '/' );
                            }
                            $blog_urls[] = $href;
                        }
                    }
                }
        
                $blog_urls = array_unique( $blog_urls );
            }
        }
        
        // ——————————————————————————————
        // 4d) Scrape /sitemap page for Pages section too
        // ——————————————————————————————
        $sitemap_resp = wp_remote_get( $domain . '/sitemap', [
            'timeout'     => 10,
            'redirection' => 5,
        ] );
        if ( ! is_wp_error( $sitemap_resp )
             && 200 === wp_remote_retrieve_response_code( $sitemap_resp )
        ) {
            $html = wp_remote_retrieve_body( $sitemap_resp );
            libxml_use_internal_errors( true );
            $doc   = new DOMDocument();
            @$doc->loadHTML( $html );
            libxml_clear_errors();
            $xpath = new DOMXPath( $doc );
        
            // 1) Find the "Pages" heading
            $pageHeadings = $xpath->query(
                "//h1[contains(translate(., 'PAGES', 'pages'), 'pages')]"
              . "|//h2[contains(translate(., 'PAGES', 'pages'), 'pages')]"
              . "|//h3[contains(translate(., 'PAGES', 'pages'), 'pages')]"
              . "|//strong[contains(translate(., 'PAGES', 'pages'), 'pages')]"
            );
        
            if ( $pageHeadings->length ) {
                $heading = $pageHeadings->item(0);
                // 2) Walk to the next <ul> or <div> that holds the links
                $sibling = $heading->nextSibling;
                while ( $sibling
                    && ! in_array( $sibling->nodeName, ['ul','div'], true )
                    && ! preg_match( '/^h[1-6]$/i', $sibling->nodeName )
                ) {
                    $sibling = $sibling->nextSibling;
                }
                // 3) Collect all <a> under that container
                if ( $sibling && in_array( $sibling->nodeName, ['ul','div'], true ) ) {
                    foreach ( $sibling->getElementsByTagName('a') as $a ) {
                        $href = $a->getAttribute('href');
                        
                        // Immediately clean trailing junk from the href itself
                        $href = rtrim( $href, '_/' ); // Remove trailing underscores and slashes
                        $href = preg_replace( '/(?:%20)+$/', '', $href ); // Remove trailing %20s
                        
                        
                        // Your already‐normalized $domain, e.g. "https://inceptiononlinemarketing.com"
                        $plain_host = parse_url( $domain, PHP_URL_HOST ); 
                        
                        // 1) Trim whitespace
                        $href = trim( $a->getAttribute('href') );
                        
                        // 2) If it literally contains your host again, pull just the “/path” portion
                        if ( preg_match(
                               '#(?:https?://)?(?:www\.)?'
                               . preg_quote( $plain_host, '#' )
                               . '(/.*)$#i',
                               $href,
                               $m
                           ) ) {
                            // $m[1] is "/blog/10-chiropractic-marketing-tips-2018_"
                            $href = untrailingslashit( $domain ) . $m[1];
                        
                        } elseif ( 0 === strpos( $href, '//' ) ) {
                            // protocol‐relative
                            $scheme = parse_url( $domain, PHP_URL_SCHEME ) ?: 'https';
                            $href   = $scheme . ':' . $href;
                        
                        } elseif ( preg_match( '#^https?://#i', $href ) ) {
                            // absolute URL—leave as is
                        
                        } else {
                            // truly relative path
                            $href = untrailingslashit( $domain ) . '/' . ltrim( $href, '/' );
                        }

                        $all_urls[] = $href;
                    }
                }
            }
        }

        // ——————————————————————————————
        // 4e) Canonicalize host & rebuild from just path+query
        // ——————————————————————————————
        $pd        = parse_url( $domain );
        $scheme    = ( $pd['scheme'] ?? 'https' ) . '://';
        // force non-www
        $host_only = preg_replace( '/^www\./i', '', $pd['host'] ?? '' );
        $canonical = $scheme . $host_only;
        
        if ( microtime(true) >= $checkDeadline ) {
            scm_show_timeout_notice();
            return;
        }
        
        function normalize_urls( $list, $canonical ) {
            $out = [];
            $canonical_host = str_replace( ['https://','http://'], '', $canonical );
            $canonical_host = preg_replace( '/^www\./i', '', $canonical_host );
        
            foreach ( $list as $u ) {
                $parts = parse_url( $u );
                if ( ! $parts ) {
                    continue;
                }
        
                // 1) Normalize host (remove www.)
                $url_host = $parts['host'] ?? '';
                $url_host = preg_replace( '/^www\./i', '', $url_host );
        
                // 2) If host doesn't match canonical host, skip
                if ( $url_host && strcasecmp( $url_host, $canonical_host ) !== 0 ) {
                    continue;
                }
        
                // 3) Get raw path (or '/')
                $raw_path = $parts['path'] ?? '/';
        
                // 4) If path begins with an accidental host, strip it
                $strip_host_pattern = '#^//?(?:www\.)?' . preg_quote( $canonical_host, '#' ) . '#i';
                $clean_path = preg_replace( $strip_host_pattern, '', $raw_path );
        
                // 5) Force exactly one leading slash
                $path = '/' . ltrim( $clean_path, '/\\' );
        
                // 6) Grab any query string
                $query = ! empty( $parts['query'] ) ? '?' . $parts['query'] : '';
        
                // 7) Rebuild under your canonical host
                $out[] = $canonical . $path . $query;
            }
        
            // dedupe + reindex
            return array_values( array_unique( $out ) );
        }
        
        
        // Now normalize
        $all_urls  = normalize_urls( $all_urls,  $canonical );
        $blog_urls = normalize_urls( $blog_urls, $canonical );
        
        // --------------------------------------------------------
        // 4d) Merge blog URLs into the master list so they end up in the CSV
        // --------------------------------------------------------
        $all_urls = array_unique( array_merge( $all_urls, $blog_urls ) );
        
        // Normalize all homepage variants (strip www and trailing slashes for comparison)
        function normalize_home_url( $url ) {
            $parts = parse_url( trim( $url ) );
            if ( empty( $parts['host'] ) ) return '';
        
            $host = preg_replace( '/^www\./i', '', strtolower( $parts['host'] ) );
            $scheme = $parts['scheme'] ?? 'https';
            $path = $parts['path'] ?? '/';
        
            $normalized = $scheme . '://' . $host . rtrim( $path, '/' );
            return rtrim( $normalized, '/' );
        }
        
        // Filter out duplicate homepages
        $seen_homepages = [];
        $filtered_urls = [];
        
        foreach ( $all_urls as $url ) {
            $norm = normalize_home_url( $url );
        
            // If this is a homepage variant and we've already seen one, skip it
            if ( $norm === normalize_home_url( $domain ) ) {
                if ( in_array( $norm, $seen_homepages, true ) ) {
                    continue;
                }
                $seen_homepages[] = $norm;
            }
        
            $filtered_urls[] = $url;
        }
        
        $all_urls = array_values( array_unique( $filtered_urls ) );
        if ( microtime(true) >= $checkDeadline ) {
            scm_show_timeout_notice();
            return;
        }
        
        // Recount
        $page_count     = count( $all_urls );
        $blog_count     = count( $blog_urls );
        $pages_to_check = $page_count - $blog_count;

        if ( ! empty( $all_urls ) ) {
            $parsed_url = parse_url( $domain );
            $host       = preg_replace( '/^www\./', '', $parsed_url['host'] ?? 'domain' );
            $filename   = ABSPATH . $host . '-sitemap.csv';
            $out        = fopen( $filename, 'w' );

            if ( ! $out ) {
                echo '<div class="notice notice-error"><p>Could not open file for writing: ' 
                     . esc_html( $filename ) . '</p></div>';
            } else {
                $written = 0;
                
                // Sort slugs alphabetically (natural order, case-insensitive)
                sort( $all_urls, SORT_NATURAL | SORT_FLAG_CASE );
                foreach ( $all_urls as $link ) {
                    $link = trim( $link );
                    if ( empty( $link ) ) {
                        continue;
                    }
                
                    // grab the raw path
                    $path = parse_url( $link, PHP_URL_PATH );
                    if ( ! $path || $path === '/' ) {
                        continue;
                    }
                
                    // ensure it starts with a slash
                    if ( strpos( $path, '/' ) !== 0 ) {
                        $path = '/' . $path;
                    }
                
                    // strip any trailing underscores or URL‐encoded spaces
                    $path = rtrim( $path, '_' );
                    $path = preg_replace( '/(?:%20)+$/', '', $path );
                
                    fputcsv( $out, [ $path ] );
                    $written++;
                }
                fclose( $out );

                if ( $written > 0 ) {
                    $csv_created = true;
                    $completeWithoutIssue = true;
                    if ( microtime(true) >= $checkDeadline ) {
                        scm_show_timeout_notice();
                        return;
                    }
                    // Pass CSV download URL to JS
                    $download_url = home_url( '/' . basename( $filename ) );
                    echo '<input type="hidden" id="csv-download-url" value="' 
                       . esc_url( $download_url ) . '" />';
                   echo "<script>document.documentElement.setAttribute('data-csv-ready','1');</script>";
                } else {
                    echo '<div class="notice notice-warning"><p>No valid slugs found to write (all URLs were homepage or malformed).</p></div>';
                }
            }

            // ---------------------------------------------
            // 5) Download each page listed in sitemap as HTML
            // ---------------------------------------------
            $all_urls  = array_values( array_unique( $all_urls ) );
            $blog_urls = array_values( array_unique( $blog_urls ) );

            // ─── Ensure /blog/ URLs are all marked as blogs ───
            // Any URL whose path contains "/blog/" goes in $blog_urls
            $extra_blog = array_filter( $all_urls, function( $u ) {
                $path = parse_url( $u, PHP_URL_PATH );
                return ( false !== strpos( $path, 'blog/' ) );
            } );
            $blog_urls = array_unique( array_merge( $blog_urls, $extra_blog ) );
            
            // Recount using the expanded $blog_urls
            $page_count     = count( $all_urls );
            $blog_count     = count( $blog_urls );
            $pages_to_check = $page_count - $blog_count;
            
            // 5.a) show confirmation if needed
            if ( $page_count > 0
                 && empty( $_POST['confirm_page_choices'] )
            ) {
                
                if ( $is_choices_screen && $heads_up_html ) {
                    echo $heads_up_html;   // render warning ONLY on the selection page
                }
            // --------------------------------------
            // Skip Copyscape Balance Check & Logging
            // --------------------------------------
            
            // We no longer check Copyscape balance or write to ai_smartcopy.log.
            // Keep user/domain variables so the rest of the form still works.
            
            $user_last_name = isset( $_POST['userLastName'] )
                ? sanitize_text_field( $_POST['userLastName'] )
                : '';
            
            $current_user = wp_get_current_user();
            $username     = $current_user->user_login;
            
            $domain = isset( $_POST['domainToCheck'] )
                ? sanitize_text_field( $_POST['domainToCheck'] )
                : '';
            
            $time = current_time( 'Y-m-d H:i:s' );
            
            // (Optional) If you want to keep minimal debug logs for troubleshooting, uncomment below:
            // $log_file = plugin_dir_path( __FILE__ ) . 'ai_smartcopy.log';
            // file_put_contents( $log_file, sprintf("%s\t%s\t%s\t\n", $time, $user_last_name, $domain), FILE_APPEND );
            
            // Continue with the sitemap form
            echo '<form method="post" action="" style="margin-top:20px;">';
            echo '<input type="hidden" name="action" value="sitemap" />';
            echo '<input type="hidden" name="domainToCheck" value="' . esc_attr( $_POST['domainToCheck'] ) . '" />';
            wp_nonce_field('sitemap_nonce', '_wpnonce', true, true);
            
            // If Salesforce sitemap CSV loaded, override discovered URLs and force $domain
            if ( isset($ai_sc_urls_override) && is_array($ai_sc_urls_override) && ! empty($ai_sc_urls_override) ) {
                $all_urls = $ai_sc_urls_override;
            
                // Also force $domain to the CSV site (scheme + host of first URL)
                $first = $all_urls[0] ?? '';
                if ( $first ) {
                    $p = parse_url( $first );
                    if ( ! empty($p['scheme']) && ! empty($p['host']) ) {
                        $domain = $p['scheme'] . '://' . $p['host'];
                    }
                }
            }
    // Normalize homepage URL
            $homepage = normalize_home_url( $domain );
            
            // Remove all homepage variants from the list
            $others = array_filter( $all_urls, function( $u ) use ( $homepage ) {
                return normalize_home_url( $u ) !== $homepage;
            } );
            
            // Sort the rest
            sort( $others, SORT_STRING );
            
            // Now reinsert homepage (non-www, no trailing slash)
            $all_urls = array_merge( [ $homepage ], array_values( $others ) );

            // Safety filter: never include Salesforce project/login URLs in the sitemap view
            if ( isset($all_urls) && is_array($all_urls) ) {
                $all_urls = array_values(array_filter($all_urls, function($u){
                    return ! ai_sc_is_salesforce_url($u);
                }));
            }

            echo '<div id="pages-container" style="display: flex; flex-wrap: wrap; gap: 20px; margin-top:20px;">';
            
            foreach ( $all_urls as $raw_url ) {
                
                // 1) Quick `trim()` to remove any accidental whitespace
                $raw_url = trim( $raw_url );
            
                // 2) Parse it
                $parts = parse_url( $raw_url );
                if ( empty( $parts['host'] ) ) {
                    // something really wrong here, skip
                    continue;
                }
            
                // 3) Grab & clean the path
                $path = $parts['path'] ?? '/';
                // Remove any trailing “_” or URL-encoded spaces
                $path = preg_replace( '/(?:_%20?)+$|(?:%20)+$/', '', $path );
                $path = rtrim( $path, '_' );
                // Force exactly one leading slash
                $path = '/' . ltrim( $path, '/\\' );
            
                // 4) Rebuild the URL
                $scheme = $parts['scheme'] ?? 'https';
                $host   = $parts['host'];
                $url    = $scheme . '://' . $host . $path;
                if ( ! empty( $parts['query'] ) ) {
                    $url .= '?' . $parts['query'];
                }

                // treat as blog if detected earlier *or* if the path literally contains “/blog/”
                $is_blog = in_array( $url, $blog_urls )
                        || ( false !== strpos( parse_url( $url, PHP_URL_PATH ), '/blog/' ) );
                $default_skip = $is_blog ? 'checked' : '';
            
                echo '<div class="page-block" data-download="false" data-copyscape="false" data-skip="' . ( $is_blog ? 'true' : 'false' ) . '" style="flex: 1 1 300px; border: 1px solid #ccd0d4; border-radius: 8px; padding: 15px; background: #ffe6e6;">';
                
                // Allow breaks only at / and -
                $escaped = esc_html( $url );
                // inject <wbr> after every slash or hyphen
                $display = str_replace(
                    ['/', '-'],
                    ['/<wbr>', '-<wbr>'],
                    $escaped
                );
                
                echo '<div style="
                        font-weight:600;
                        font-size:14px;
                        margin-bottom:10px;
                        word-break: normal;
                        overflow-wrap: anywhere;
                    ">'
                    . '<a href="' . esc_url( $url ) . '" target="_blank" rel="noopener noreferrer">'
                    . $display .
                    '</a>'
                .'</div>';
                echo '<style>
                    .page-block a {
                        color: #005177; /* dark blue by default */
                        text-decoration: none;
                        transition: color 0.2s ease;
                    }
                    .page-block a:hover {
                        color: #000000; /* black on hover */
                        text-decoration: underline;
                    }
                </style>';

                // Options: SmartCopy vs Rewrite (based on similarity percentage)
                // Normalize percentage to 0–100 (supports 0–1 floats and strings like "42%")
                $pct_for_url = null;
                if ( isset($batch_results[$url]) ) {
                    $raw = null;
                    if ( isset($batch_results[$url]['percentage']) ) {
                        $raw = $batch_results[$url]['percentage'];
                    } elseif ( isset($batch_results[$url]['similarity']) ) {
                        $raw = $batch_results[$url]['similarity'];
                    }
                    if ($raw !== null) {
                        $num = is_string($raw) ? floatval(preg_replace('/[^0-9\.]/', '', $raw)) : floatval($raw);
                        $pct_for_url = $num;
                    }
                }
                // If percentage is missing => default to REWRITE; else REWRITE when >= 30
                $rewrite_mode = (!is_numeric($pct_for_url)) ? true : ($pct_for_url >= 30.0);

                if ( $rewrite_mode ) {
                    echo '<label style="display:block; margin-bottom:8px;">';
                    echo '<input type="checkbox" class="ai-type ai-rewrite-page" name="rewrite_page_urls[]" value="' . esc_attr( $url ) . '"> Rewrite Page Content - Page';
                    echo '</label>';

                    echo '<label style="display:block; margin-bottom:8px;">';
                    echo '<input type="checkbox" class="ai-type ai-rewrite-post" name="rewrite_post_urls[]" value="' . esc_attr( $url ) . '"> Rewrite Page Content - Blog';
                    echo '</label>';
                } else {
                    // Default: SmartCopy options
                    echo '<label style="display:block; margin-bottom:8px;">';
                    echo '<input type="checkbox" class="ai-type ai-page" name="ai_page_urls[]" value="' . esc_attr( $url ) . '"> AI SmartCopy - Page';
                    echo '</label>';

                    echo '<label style="display:block; margin-bottom:8px;">';
                    echo '<input type="checkbox" class="ai-type ai-post" name="ai_post_urls[]" value="' . esc_attr( $url ) . '"> AI SmartCopy - Blog Post';
                    echo '</label>';
                }
            
                echo '</div>';
            }
            
            echo '</div>'; // end flex container
            
            echo '<div style="margin-top:30px;">';
            echo '<button type="submit" name="confirm_page_choices" value="1" class="button button-primary" style="margin-right:10px;">Proceed With Selected Choices</button>';
            echo '<a href="" class="button">Cancel</a>';
            echo '</div>';
            
            echo '</form>';

/* --- REVISED: Instant overlay + guaranteed submit flag ----------------- */
$__gif = plugins_url('../search-and-replace/working-on-it.gif', __FILE__);
echo '<script>
(function(){
  var GIF_URL = ' . json_encode( esc_url($__gif) ) . ';
  var overlayShown = false;
  var clickedOnce = false;
  var t0 = 0;

  // Target the specific button and its form
  var submitBtn = document.querySelector(\'button[name="confirm_page_choices"][type="submit"]\');
  var form = submitBtn ? submitBtn.closest("form") : document.querySelector("form");
  if (!form || !submitBtn) return;

  function ensureCSS(){
    if (document.getElementById("ai-overlay-css")) return;
    var css = document.createElement("style");
    css.id = "ai-overlay-css";
    css.textContent = `
      #ai-overlay{
        position:fixed; inset:0; z-index:99999; background:#fff;
        display:flex; align-items:center; justify-content:center;
        font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,"Helvetica Neue",Arial,sans-serif;
        opacity:0; transition:opacity .25s ease;
        pointer-events:none; /* never trap input */
      }
      #ai-overlay.ai-show{ opacity:1; }
      #ai-card{
        width:min(720px,92vw);
        border:1px solid #d0d7de; border-radius:12px; background:#fff;
        padding:24px; box-shadow:0 8px 24px rgba(0,0,0,.12); text-align:center;
      }
      #ai-card h3{ margin:0 0 8px 0; font-size:20px; }
      #ai-card p{ margin:8px 0 14px 0; font-weight:600; }
    `;
    document.head.appendChild(css);
  }

  function showOverlay(){
    if (overlayShown) return;
    overlayShown = true;
    ensureCSS();
    if (!document.getElementById("ai-overlay")){
      var wrap = document.createElement("div");
      wrap.id = "ai-overlay";
      wrap.setAttribute("aria-live","polite");
      wrap.innerHTML = `
        <div id="ai-card" role="dialog" aria-label="AI SmartCopy is starting">
          <h3>AI SmartCopy</h3>
          <p id="ai-overlay-msg">Starting…</p>
          <div style="margin-top:12px;">
            <img src="` + GIF_URL + `" alt="Working…" style="width:360px;max-width:90vw;" />
          </div>
        </div>
      `;
      document.body.appendChild(wrap);
      requestAnimationFrame(function(){ wrap.classList.add("ai-show"); });
    }
  }

  // Ensure PHP always sees confirm_page_choices=1 (even if requestSubmit isn’t used)
  function injectConfirmHidden(){
    if (!form.querySelector(\'input[type="hidden"][name="confirm_page_choices"]\')){
      var hid = document.createElement("input");
      hid.type = "hidden";
      hid.name = "confirm_page_choices";
      hid.value = "1";
      form.appendChild(hid);
    }
  }

  function safeSubmit(){
    // Let overlay paint, then submit
    requestAnimationFrame(function(){
      setTimeout(function(){
        if (form.requestSubmit) {
          form.requestSubmit(submitBtn);
        } else {
          // Fallback path lacks submitter context; inject hidden to preserve intent
          injectConfirmHidden();
          form.submit();
        }
      }, 30);
    });

    // Watchdog: if still here after ~2.5s, force a standard submit
    t0 = (window.performance && performance.now) ? performance.now() : Date.now();
    setTimeout(function(){
      var now = (window.performance && performance.now) ? performance.now() : Date.now();
      if (document.visibilityState === "visible" && (now - t0) > 2400) {
        try {
          injectConfirmHidden();
          form.submit();
        } catch(e){}
      }
    }, 2500);
  }

  // Show overlay immediately on pointerdown (instant feedback)
  document.addEventListener("pointerdown", function(ev){
    if (ev.target && ev.target.closest(\'button[name="confirm_page_choices"][type="submit"]\')) {
      showOverlay();
    }
  }, {capture:true, passive:true});

  // Click on our button: prevent default ONCE so overlay can paint, then submit safely
  submitBtn.addEventListener("click", function(ev){
    if (clickedOnce) return;
    clickedOnce = true;
    showOverlay();
    ev.preventDefault();
    safeSubmit();
  }, {capture:true});

  // Any other submit path (Enter key, etc.): show overlay and ensure the hidden flag is set
  document.addEventListener("submit", function(ev){
    if (ev.target !== form) return;
    showOverlay();
    injectConfirmHidden(); // make sure PHP sees confirm_page_choices=1
    // do not preventDefault — allow native submit
  }, {capture:true});
})();
</script>';
/* ---------------------------------------------------------------------- */
echo '<script>
(function(){
  var hasCS = !!window.hasCopyscapeFile;
  var pctMap = window.copyscapePctMap || {};

    function norm(u){
      try {
        var a = document.createElement("a");
        a.href = u;
    
        var host = (a.host || "").toLowerCase();
        host = host.replace(/^www\./, "");
    
        var path = a.pathname || "/";
        if (path === "/") path = "";
        else if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
    
        return host + path; // NOTE: no scheme, matches PHP key
      } catch(e) { return u; }
    }

  function setLabelText(input, text){
    var label = input && input.closest("label");
    if (!label) return;
    var toRemove = [];
    for (var i=0;i<label.childNodes.length;i++){
      var n = label.childNodes[i];
      if (n.nodeType === 3) toRemove.push(n); // text node
    }
    toRemove.forEach(function(n){ label.removeChild(n); });
    label.appendChild(document.createTextNode(" " + text));
  }

  function showLabel(input, yes){
    var label = input && input.closest("label");
    if (!label) return;
    label.style.display = yes ? "block" : "none";
  }

  document.querySelectorAll(".page-block").forEach(function(block){
    var link = block.querySelector("a[href]");
    var url  = link ? link.href : "";
    var key  = norm(url);
    var perc = pctMap[key];

    // Both pairs (SmartCopy & Rewrite) may or may not be rendered server-side
    var scPage = block.querySelector("input.ai-page");
    var scPost = block.querySelector("input.ai-post");
    var rwPage = block.querySelector("input.ai-rewrite-page");
    var rwPost = block.querySelector("input.ai-rewrite-post");

    // Decide mode: if NO percentage => default to REWRITE; else REWRITE when >= 30
    var rewriteMode = false;
    if (perc == null || String(perc) === "") {
      rewriteMode = true; // default to Rewrite when missing percentage
    } else {
      var val = parseFloat(String(perc).replace("%",""));
      if (!isNaN(val)) {
        rewriteMode = (val >= 30);
      }
    }
    var labelText = (perc == null || String(perc) === "")
      ? "No Copyscape result - Rewrite required"
      : "Copyscape: " + String(perc).replace("%","") + "% - "
        + (rewriteMode ? "Rewrite required" : "Eligible for AI SmartCopy");

    // Ensure we have an active pair (repurpose if needed)
    var pageCb = null, postCb = null;

    if (rewriteMode) {
      if (rwPage && rwPost) {
        showLabel(rwPage, true); showLabel(rwPost, true);
        showLabel(scPage, false); showLabel(scPost, false);
        setLabelText(rwPage, "Rewrite Page Content - Page");
        setLabelText(rwPost, "Rewrite Page Content - Blog");
        pageCb = rwPage; postCb = rwPost;
      } else if (scPage && scPost) {
        scPage.classList.remove("ai-page"); scPage.classList.add("ai-rewrite-page");
        scPage.setAttribute("name","rewrite_page_urls[]");
        setLabelText(scPage, "Rewrite Page Content - Page");
        scPost.classList.remove("ai-post"); scPost.classList.add("ai-rewrite-post");
        scPost.setAttribute("name","rewrite_post_urls[]");
        setLabelText(scPost, "Rewrite Page Content - Blog");
        pageCb = scPage; postCb = scPost;
      }
    } else {
      if (scPage && scPost) {
        showLabel(scPage, true); showLabel(scPost, true);
        showLabel(rwPage, false); showLabel(rwPost, false);
        setLabelText(scPage, "AI SmartCopy - Page");
        setLabelText(scPost, "AI SmartCopy - Blog Post");
        pageCb = scPage; postCb = scPost;
      } else if (rwPage && rwPost) {
        rwPage.classList.remove("ai-rewrite-page"); rwPage.classList.add("ai-page");
        rwPage.setAttribute("name","ai_page_urls[]");
        setLabelText(rwPage, "AI SmartCopy - Page");
        rwPost.classList.remove("ai-rewrite-post"); rwPost.classList.add("ai-post");
        rwPost.setAttribute("name","ai_post_urls[]");
        setLabelText(rwPost, "AI SmartCopy - Blog Post");
        pageCb = rwPage; postCb = rwPost;
      }
    }

    // If no active pair, still show the badge and bail
    var headline = block.querySelector("div > a[href]") || block.firstElementChild;
    if (!pageCb || !postCb) {
      if (headline && !block.querySelector(".ai-dynamic-note")) {
        var note = document.createElement("div");
        note.className = "ai-dynamic-note";
        note.style.cssText = "margin:6px 0 10px 0; font-size:12px; font-weight:600; color:#333;";
        note.textContent = labelText;
        headline.parentNode.insertBefore(note, headline.nextSibling);
      } else {
        var note = block.querySelector(".ai-dynamic-note");
        if (note) note.textContent = labelText;
      }
      return;
    }

    // Mutual exclusivity
    function sync(e){
      if (e.target === pageCb && pageCb.checked) postCb.checked = false;
      if (e.target === postCb && postCb.checked) pageCb.checked = false;
    }
    pageCb.addEventListener("change", sync);
    postCb.addEventListener("change", sync);

    // Background color: missing percentage = white (as before)
    var initialBg = "#ffffff";
    if (perc != null && String(perc) !== "") {
      var val2 = parseFloat(String(perc).replace("%",""));
      if (!isNaN(val2)) {
        initialBg = (val2 >= 30) ? "#ffe6e6" : "#ffe8cc";
      }
    }
    block.style.backgroundColor = initialBg;
    block.dataset.initialBg = initialBg;

    // Badge under the URL
    if (headline && !block.querySelector(".ai-dynamic-note")) {
      var note2 = document.createElement("div");
      note2.className = "ai-dynamic-note";
      note2.style.cssText = "margin:6px 0 10px 0; font-size:12px; font-weight:600; color:#333;";
      note2.textContent = labelText;
      headline.parentNode.insertBefore(note2, headline.nextSibling);
    } else {
      var note2 = block.querySelector(".ai-dynamic-note");
      if (note2) note2.textContent = labelText;
    }

    // Turn green when either active checkbox is selected; revert when none
    function updateBg(){
      var any = !!(pageCb.checked || postCb.checked);
      block.style.backgroundColor = any ? "#e6f4ea" : (block.dataset.initialBg || "#ffffff");
    }
    pageCb.addEventListener("change", updateBg);
    postCb.addEventListener("change", updateBg);
    updateBg();
  });
})();
</script>';

                return; // wait for user submission
            }

            if ( isset($_POST['confirm_page_choices']) ) {
                @set_time_limit(0); // remove time limit for script
                ignore_user_abort(true); // keep running even if user navigates away

                $deadline = microtime(true) + 95;   // 1 min 35 seconds before timeout message
                
                // SAFETY: make sure our accumulators are arrays
                $created = [];
                $errors  = [];
                
                // NEW: local time-budget vars for any loops that reference them
                $started_at      = microtime(true);
                $time_limit_secs = 90;
            
                // 1) Grab the user’s selections
                $download_html_urls   = isset($_POST['download_html_urls'])   ? (array) $_POST['download_html_urls']   : [];
                $copyscape_check_urls = isset($_POST['copyscape_check_urls']) ? (array) $_POST['copyscape_check_urls'] : [];
                $skip_urls            = isset($_POST['skip_urls'])            ? (array) $_POST['skip_urls']            : [];

                // NEW: read the two REAL type checkboxes (none pre-checked by default)
                $ai_page_urls = isset($_POST['ai_page_urls']) ? (array) $_POST['ai_page_urls'] : [];
                $ai_post_urls = isset($_POST['ai_post_urls']) ? (array) $_POST['ai_post_urls'] : [];

                // Normalize to a consistent key (scheme+host+path, no trailing slash)
                $norm = function($urls){
                    $out = [];
                    foreach ((array)$urls as $u) {
                        $u = esc_url_raw($u);
                        if ($u === '') continue;
                        $p = @parse_url($u);
                        if (!$p || empty($p['scheme']) || empty($p['host'])) continue;
                        $path = isset($p['path']) ? rtrim($p['path'], '/') : '';
                        $key  = $p['scheme'].'://'.$p['host'].($path === '' ? '' : $path);
                        $out[] = $key;
                    }
                    return $out;
                };

                $ai_pages_norm = $norm($ai_page_urls);
                $ai_posts_norm = $norm($ai_post_urls);

                // NEW: Rewrite Page Content selections
                $rewrite_page_urls = isset($_POST['rewrite_page_urls']) ? (array) $_POST['rewrite_page_urls'] : [];
                $rewrite_post_urls = isset($_POST['rewrite_post_urls']) ? (array) $_POST['rewrite_post_urls'] : [];
                $rewrite_pages_norm = $norm($rewrite_page_urls);
                $rewrite_posts_norm = $norm($rewrite_post_urls);

                // Build a type map: url => action type ('smartcopy-page','smartcopy-post','rewrite-page','rewrite-post')
                $ai_type_map = [];
                foreach ($ai_pages_norm as $u)    { $ai_type_map[$u] = 'smartcopy-page'; }
                foreach ($ai_posts_norm as $u)    { $ai_type_map[$u] = 'smartcopy-post'; }
                foreach ($rewrite_pages_norm as $u){ $ai_type_map[$u] = 'rewrite-page'; }
                foreach ($rewrite_posts_norm as $u){ $ai_type_map[$u] = 'rewrite-post'; }

                // 2) Remove any “skip” URLs (kept for compatibility even if you removed Skip UI)
                $download_list   = array_diff( $download_html_urls, $skip_urls );
                $copyscape_list  = array_diff( $copyscape_check_urls, $skip_urls );

                // Build unified action list from the two type arrays
                $ai_action_list  = array_values(array_unique(array_merge($ai_pages_norm, $ai_posts_norm, $rewrite_pages_norm, $rewrite_posts_norm)));

                // Isolate resume state for this submission. Auto-posts carry
                // this value forward through the existing POST copy logic.
                $ai_run_id = isset($_POST['ai_run_id'])
                    ? sanitize_key( wp_unslash($_POST['ai_run_id']) )
                    : '';
                if ( $ai_run_id === '' ) {
                    $ai_run_id = str_replace('-', '', wp_generate_uuid4());
                    $_POST['ai_run_id'] = $ai_run_id;
                }

                $html_dir = ABSPATH . 'downloaded-pages/';
                wp_mkdir_p( $html_dir );

            if ( ! empty( $download_list ) && empty( $_POST['copyscape_phase'] ) ) {
                $saved_count = 0;
                $html_errors = [];
            
                // Create download history file
                $parsed_url = parse_url( $domain );
                $host = preg_replace( '/^www\./', '', $parsed_url['host'] ?? 'site' );
                $history_file = ABSPATH . 'downloaded-' . $host . '-pages.txt';
            
                $already_downloaded = [];
                if ( ! isset($history_file) || ! is_string($history_file) ) {
                    $history_file = '';
                }
                if ( $history_file && file_exists( $history_file ) ) {
                    echo '<a href="' . esc_url( $history_file_url ) . '">View History</a>';
                }
                
                // Use persisted file count + carry-over snapshot (to avoid regress after refresh)
                $already_downloaded_count = is_array($already_downloaded) ? count($already_downloaded) : 0;
                $posted_snapshot = isset($_POST['history_count_snapshot']) ? intval($_POST['history_count_snapshot']) : 0;
                if ( $posted_snapshot > $already_downloaded_count ) {
                    $already_downloaded_count = $posted_snapshot;
                }
            
                // Avoid modifying the original list in-place
                $download_list_full = array_diff( $download_list, $already_downloaded );
                $total_pages_initial = count( $download_html_urls ); // TOTAL originally selected
                $total_remaining = count( $download_list_full );
                // Time-boxed run: process as many as we can within ~99s
                $time_limit_secs    = 99;                 // requested window
                $started_at         = microtime(true);
                $processed_this_run = 0;
                $reached_time_limit = false;
                
                // We'll iterate the filtered list and stop when we hit the time budget
                $batch_to_download = $download_list_full;
            
                // Render HTML download progress bar before loop
                $history_count = is_array( $already_downloaded ) ? count( $already_downloaded ) : 0;
                $percentage = $total_pages_initial > 0 ? round(($history_count / $total_pages_initial) * 100) : 0;
                
                echo '<div id="progress-bar-container" style="margin-top:20px; background:#e0e0e0; border-radius:5px; overflow:hidden; height:25px;">';
                echo '<div id="progress-bar" style="width:'.$percentage.'%; background:#0073aa; color:#fff; text-align:center; height:100%; line-height:25px; font-weight:bold; transition:width 0.5s ease;">';
                echo $history_count . ' / ' . $total_pages_initial . ' (' . $percentage . '%)';
                echo '</div>';
                echo '</div>';
                echo '<p id="html-progress-message" style="font-weight:bold; margin:10px 0 5px 0;">Downloading Pages… please wait.</p>';
                echo '<p><img id="html-progress-gif" src="' . plugins_url( '../search-and-replace/working-on-it.gif', __FILE__ ) . '" alt="Please wait" style="width:400px; margin-top:10px;" /></p>';
                
                if (ob_get_level() === 0) ob_start();
                @ob_flush();
                @flush();

                foreach ( $batch_to_download as $link ) {
                    $link = trim( $link );
                    if ( empty( $link ) ) {
                        continue;
                    }
            
                    $url_to_fetch = ( $link === $domain ) ? $domain . '/' : $link;
                    $resp = wp_remote_get( $url_to_fetch, [ 'timeout' => 30 ] );
                    
                    // ——— ERROR: HTTP or redirect failure ———
                    if ( is_wp_error( $resp ) ) {
                        $html_errors[] = "Error fetching HTML for $link: " . $resp->get_error_message();
                        // mark it processed anyway:
                        file_put_contents( $history_file, $link . PHP_EOL, FILE_APPEND );   // <-- add
                        $saved_count++;                                                     // <-- add
                        $processed_this_run++;

                        // Leave a little headroom so we can render the redirect before the host kills the request
                        if ( (microtime(true) - $started_at) >= ($time_limit_secs - 3) ) { // ~96s
                            $reached_time_limit = true;
                            break;
                        }
                        continue;
                    }
            
                    $html_body = wp_remote_retrieve_body( $resp );
                    
                    // ——— ERROR: empty body ———
                    if ( empty( trim( $html_body ) ) ) {
                        $html_errors[] = "Empty HTML content at $link";
                        // mark it processed anyway:
                        file_put_contents( $history_file, $link . PHP_EOL, FILE_APPEND );   // <-- add
                        $saved_count++;                                                     // <-- add
                        $processed_this_run++;

                        // Leave a little headroom so we can render the redirect before the host kills the request
                        if ( (microtime(true) - $started_at) >= ($time_limit_secs - 3) ) { // ~96s
                            $reached_time_limit = true;
                            break;
                        }
                        continue;
                    }
            
                    $path = parse_url( $link, PHP_URL_PATH ) ?: '/';
                    $slug = trim( ltrim( $path, '/' ), '/' );
                    $slug = $slug === '' ? 'index' : str_replace( '/', '-', $slug );
                    $file = $slug . '.html';
            
                    if ( false === file_put_contents( $html_dir . $file, $html_body ) ) {
                        $html_errors[] = "Failed to save HTML for $link";
                        // even on filesystem‐write failure, mark it done:
                        file_put_contents( $history_file, $link . PHP_EOL, FILE_APPEND );   // <-- add
                        $saved_count++;
                        $processed_this_run++;

                        // Leave a little headroom so we can render the redirect before the host kills the request
                        if ( (microtime(true) - $started_at) >= ($time_limit_secs - 3) ) { // ~96s
                            $reached_time_limit = true;
                            break;
                        }
                    } else {
                        $saved_count++;
                        $processed_this_run++;

                        // Leave a little headroom so we can render the redirect before the host kills the request
                        if ( (microtime(true) - $started_at) >= ($time_limit_secs - 3) ) { // ~96s
                            $reached_time_limit = true;
                            break;
                        }
                        file_put_contents( $history_file, $link . PHP_EOL, FILE_APPEND );
                        
                        // Flush output to keep browser connection alive
                        if ( ob_get_level() === 0 ) ob_start();
                        echo ' '; // send a small output
                        @ob_flush();
                        @flush();
                    }
                    // --- Real-time HTML progress bar update ---
                    $history_count_live = (is_array($already_downloaded) ? count($already_downloaded) : 0) + $saved_count;
                    $percent_live = $total_pages_initial > 0 ? round(($history_count_live / $total_pages_initial) * 100) : 0;
                    
                    echo "<script>
                    (function(){
                      var bar = document.getElementById('progress-bar');
                      if (bar) {
                        bar.style.width = '{$percent_live}%';
                        bar.textContent = '{$history_count_live} / {$total_pages_initial} ({$percent_live}%)';
                      }
                    })();
                    </script>";
                    
                    @ob_flush();
                    @flush();
                }
            
                // Show results
                if ( ! empty( $html_errors ) ) {
                    echo '<div class="notice notice-error"><ul>';
                    foreach ( $html_errors as $err ) {
                        echo '<li>' . esc_html( $err ) . '</li>';
                    }
                    echo '</ul></div>';
                }
            
                $zip_created = true;

                ?>
                <?php
            
                // Check if more remain
                $remaining_urls = array_slice( $download_list_full, $processed_this_run );
                if ( count( $remaining_urls ) > 0 ) {
                    ?>
                    <form id="auto-continue-form" method="post" action="">
                        <?php $new_nonce = wp_create_nonce('sitemap_nonce'); ?>
                        <input type="hidden" name="action" value="sitemap" />
                        <input type="hidden" name="_wpnonce" value="<?php echo esc_attr( $new_nonce ); ?>" />
                        <input type="hidden" name="domainToCheck" value="<?php echo esc_attr( $_POST['domainToCheck'] ); ?>" />
                        <?php $history_count_after_batch = $already_downloaded_count + $saved_count; ?>
                        <input type="hidden" name="history_count_snapshot" value="<?php echo intval($history_count_after_batch); ?>" />
                          <?php
                          foreach ( $download_html_urls as   $url ) echo '<input type="hidden" name="download_html_urls[]"   value="' . esc_attr( $url ) . '">';
                          foreach ( $copyscape_check_urls as $url ) echo '<input type="hidden" name="copyscape_check_urls[]" value="' . esc_attr( $url ) . '">';
                          foreach ( $skip_urls as            $url ) echo '<input type="hidden" name="skip_urls[]"            value="' . esc_attr( $url ) . '">';
                          ?>
                        <?php if ( !empty($ai_page_urls) ) : foreach ( $ai_page_urls as $u ) : ?>
                          <input type="hidden" name="ai_page_urls[]" value="<?php echo esc_attr( $u ); ?>" />
                        <?php endforeach; endif; ?>
                        
                        <?php if ( !empty($ai_post_urls) ) : foreach ( $ai_post_urls as $u ) : ?>
                          <input type="hidden" name="ai_post_urls[]" value="<?php echo esc_attr( $u ); ?>" />
                        <?php endforeach; endif; ?>
                        <input type="hidden" name="confirm_page_choices" value="1" />
                    </form>
                    <script>
                        setTimeout(function() {
                            document.getElementById('auto-continue-form').submit();
                        }, 3000); // wait 3 seconds then submit
                    </script>
                    <?php
                    exit;
                }
            }
            // ---------------------------------------------
            // 5.b) Package HTML into a ZIP (only once, before Copyscape)
            // ---------------------------------------------
            if ( empty( $_POST['copyscape_phase'] ) ) {
                $parsed_url = parse_url( $domain );
                $host       = preg_replace( '/^www\./', '', $parsed_url['host'] ?? 'site' );
                $zip_path   = ABSPATH . $host . '-pages.zip';
            
                $zip = new ZipArchive();
                if ( $zip->open( $zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE ) === true ) {
                    foreach ( scandir( $html_dir ) as $file ) {
                        if ( in_array( $file, [ '.', '..' ], true ) ) {
                            continue;
                        }
                        $zip->addFile( $html_dir . $file, $file );
                    }
                    $zip->close();
            
                    // clean up the HTML folder & history.txt
                    delete_folder_recursively( $html_dir );
                    // maybe print a link to the run history if it exists
                    if ( ! isset($history_file) || ! is_string($history_file) ) {
                        $history_file = '';
                    }
                    if ( $history_file && file_exists( $history_file ) ) {
                        echo '<a href="' . esc_url( $history_file_url ) . '">View History</a>';
                    }
                } else {
                    echo '<div class="notice notice-error"><p>Could not create ZIP file: '
                       . esc_html( $zip_path ) . '</p></div>';
                }
            }
            
            if ( empty($ai_action_list) && isset($_POST['confirm_page_choices']) ) {
                echo '<div class="notice notice-warning"><p><strong>AI SmartCopy:</strong> No items selected. Choose Page or Blog Post for at least one URL.</p></div>';
            }

// === Helper: local-SEO meta description (<=155 chars) ===
if ( ! function_exists('ai_sc_generate_meta_desc') ) {
    function ai_sc_generate_meta_desc( $api_key, $model, $clinic_name, $city, $state, $keyphrase ) {
    ai_sc_debug(__FUNCTION__ . '() start', array('args'=>func_get_args()));

        $clinic  = trim( (string) $clinic_name );
        $city    = trim( (string) $city );
        $state   = trim( (string) $state );
        $phrase  = trim( (string) $keyphrase );

        // Guardrails: if anything critical is missing, fallback fast
        if ( $clinic === '' || $city === '' || $state === '' || $phrase === '' ) {
            $fallback = sprintf(
                '%s: %s in %s, %s. Call today for expert care.',
                $clinic ?: 'Our clinic', $phrase ?: 'Local care', $city ?: 'Your City', $state ?: 'ST'
            );
            return mb_substr( $fallback, 0, 155 );
        }

        $sys = "You write concise, compelling meta descriptions for local SEO. Hard limit: 155 characters. Never exceed 155.";
        $usr = sprintf(
            "Write a meta description (max 155 chars) for %s. Include clinic name, the keyphrase, and the location: %s, %s.\nKeyphrase: %s",
            $clinic, $city, $state, $phrase
        );

        // Call OpenAI Chat Completions
        $resp = wp_remote_post( 'https://api.openai.com/v1/chat/completions', [
            'timeout' => 20,
            'headers' => [
                'Authorization' => 'Bearer ' . $api_key,
                'Content-Type'  => 'application/json',
            ],
            'body'    => wp_json_encode([
                'model'    => $model ?: 'gpt-4o-mini',
                'messages' => [
                    ['role' => 'system', 'content' => $sys],
                    ['role' => 'user',   'content' => $usr],
                ],
                'temperature' => 0.3,
                'max_tokens'  => 120,
            ]),
        ] );

        if ( is_wp_error( $resp ) ) {
            $fallback = sprintf('%s: %s in %s, %s. Book now.', $clinic, $phrase, $city, $state);
            return mb_substr( $fallback, 0, 155 );
        }

        $code = wp_remote_retrieve_response_code( $resp );
        $body = json_decode( wp_remote_retrieve_body( $resp ), true );

        $text = '';
        if ( $code === 200 && isset($body['choices'][0]['message']['content']) ) {
            $text = trim( wp_strip_all_tags( $body['choices'][0]['message']['content'] ) );
        }

        if ( $text === '' ) {
            $fallback = sprintf('%s: %s in %s, %s. Book now.', $clinic, $phrase, $city, $state);
            $text = $fallback;
        }

        // Hard-limit to 155 chars
        return mb_substr( preg_replace('/\s+/', ' ', $text), 0, 155 );
    }
}

            // === NEW: AI SmartCopy: create pages from external URLs =======================
            if ( ! empty( $ai_action_list ) && empty( $_POST['copyscape_phase'] ) ) {
                $api_key = function_exists('isc_ai_sc_get_api_key') ? isc_ai_sc_get_api_key() : ( defined('OPENAI_API_KEY') ? OPENAI_API_KEY : '' );
                if ( ! $api_key ) {
                    echo '<div class="notice notice-error"><p><strong>AI SmartCopy:</strong> OpenAI API key is not set. Set it in AI SmartCopy Settings.</p></div>';
                } else {
                    $model   = get_option('isc_openai_model', 'gpt-4o-mini');
                    $city    = get_option('isc_city', 'CITY');
                    $state   = get_option('isc_state', 'STATE');
                    $home    = home_url('/');
            
                    // Helpers (declared once)
                    if ( ! function_exists('ai_sc_fetch_url_html') ) {
                        function ai_sc_fetch_url_html( $url ) {
    ai_sc_debug(__FUNCTION__ . '() start', array('args'=>func_get_args()));

                            $profiles = [
                                [
                                    'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
                                    'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
                                    'Accept-Language' => 'en-US,en;q=0.9',
                                    'Cache-Control' => 'no-cache',
                                    'Pragma' => 'no-cache',
                                    'Sec-Fetch-Dest' => 'document',
                                    'Sec-Fetch-Mode' => 'navigate',
                                    'Sec-Fetch-Site' => 'none',
                                    'Sec-Fetch-User' => '?1',
                                    'Upgrade-Insecure-Requests' => '1',
                                ],
                                [
                                    'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Safari/605.1.15',
                                    'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                                    'Accept-Language' => 'en-US,en;q=0.9',
                                    'Referer' => trailingslashit((string) parse_url($url, PHP_URL_SCHEME) . '://' . (string) parse_url($url, PHP_URL_HOST)),
                                ],
                            ];

                            $last_response = null;
                            $last_error = null;
                            $last_challenge = false;
                            foreach ($profiles as $headers) {
                                $response = wp_remote_get($url, [
                                    'timeout' => 30,
                                    'redirection' => 5,
                                    'httpversion' => '1.1',
                                    'headers' => $headers,
                                ]);

                                if (is_wp_error($response)) {
                                    $last_error = $response;
                                    continue;
                                }

                                $last_response = $response;
                                $code = (int) wp_remote_retrieve_response_code($response);
                                $body = (string) wp_remote_retrieve_body($response);
                                $challenge = preg_match(
                                    '/just a moment|attention required|access denied|verify you are human|checking your browser|cf-chl-|captcha|security check/i',
                                    substr($body, 0, 20000)
                                );
                                $last_challenge = (bool) $challenge;

                                if ($code === 200 && !$challenge && trim($body) !== '') {
                                    return $body;
                                }

                                if (!in_array($code, [0, 403, 429, 503], true) && !$challenge) {
                                    break;
                                }
                            }

                            if ($last_response) {
                                $code = (int) wp_remote_retrieve_response_code($last_response);
                                $body = (string) wp_remote_retrieve_body($last_response);
                                $server = trim((string) wp_remote_retrieve_header($last_response, 'server'));
                                $ray_id = trim((string) wp_remote_retrieve_header($last_response, 'cf-ray'));
                                $title = '';
                                if (preg_match('#<title[^>]*>(.*?)</title>#is', $body, $match)) {
                                    $title = trim(wp_strip_all_tags(html_entity_decode($match[1], ENT_QUOTES | ENT_HTML5, 'UTF-8')));
                                }

                                $details = [];
                                if ($server !== '') $details[] = 'Server: ' . $server;
                                if ($title !== '') $details[] = 'Response page: ' . $title;
                                if ($ray_id !== '') $details[] = 'Cloudflare Ray ID: ' . $ray_id;

                                $blocked = in_array($code, [403, 429, 503], true) || $last_challenge;
                                if ($blocked) {
                                    $details[] = 'The source site firewall is likely blocking server-to-server requests from this website host.';
                                }

                                return new WP_Error(
                                    $blocked ? 'fetch_blocked' : 'fetch_http_error',
                                    ($blocked ? 'Source page access was refused' : 'Source page request failed')
                                    . ' (HTTP ' . $code . ') for ' . esc_url_raw($url) . '. '
                                    . implode(' ', $details)
                                );
                            }

                            if (is_wp_error($last_error)) {
                                return new WP_Error(
                                    'fetch_connection_failed',
                                    'Could not connect to ' . esc_url_raw($url) . ': ' . $last_error->get_error_message()
                                );
                            }

                            return new WP_Error('fetch_failed', 'The source page returned no usable response for ' . esc_url_raw($url) . '.');
                        }
                    }
                    if ( ! function_exists('ai_sc_parse_title_from_html') ) {
                        function ai_sc_parse_title_from_html( $html, $fallback = 'Imported Page' ) {
    ai_sc_debug(__FUNCTION__ . '() start', array('args'=>func_get_args()));

                            $title = '';
                            if ( preg_match('#<title[^>]*>(.*?)</title>#is', $html, $m) ) {
                                $title = trim( wp_strip_all_tags( html_entity_decode($m[1], ENT_QUOTES | ENT_HTML5, 'UTF-8') ) );
                            }
                            if ( ! $title && preg_match('#<h1[^>]*>(.*?)</h1>#is', $html, $m) ) {
                                $title = trim( wp_strip_all_tags( html_entity_decode($m[1], ENT_QUOTES | ENT_HTML5, 'UTF-8') ) );
                            }
                            return $title ?: $fallback;
                        }
                    }
                    if ( ! function_exists('ai_sc_extract_primary_content_html') ) {
                        function ai_sc_extract_primary_content_html( $html ) {
    ai_sc_debug(__FUNCTION__ . '() start', array('args'=>func_get_args()));

                            // Strip scripts/styles early
                            $html = preg_replace('#<(script|style)[^>]*>.*?</\\1>#is', '', $html);
            
                            $doc = new DOMDocument();
                            libxml_use_internal_errors(true);
                            $ok  = $doc->loadHTML('<?xml encoding="utf-8" ?>' . $html);
                            libxml_clear_errors();
                            if ( ! $ok ) return $html;
            
                            // Remove chrome blocks
                            foreach (['header','nav','footer','aside','form','noscript'] as $tag) {
                                $nodes = $doc->getElementsByTagName($tag);
                                for ($i = $nodes->length - 1; $i >= 0; $i--) {
                                    $n = $nodes->item($i);
                                    $n->parentNode->removeChild($n);
                                }
                            }
            
                            // Gather candidates
                            $candidates = [];
                            foreach (['main','article'] as $t) {
                                $nodes = $doc->getElementsByTagName($t);
                                foreach ($nodes as $n) $candidates[] = $n;
                            }
                            $xp = new DOMXPath($doc);
                            foreach ([
                                "//*[@role='main']",
                                "//*[contains(translate(@id,'CONTENTMAINPRIMARY','contentmainprimary'),'content')]",
                                "//*[contains(translate(@id,'CONTENTMAINPRIMARY','contentmainprimary'),'main')]",
                                "//*[contains(translate(@id,'CONTENTMAINPRIMARY','contentmainprimary'),'primary')]",
                                "//*[contains(translate(@class,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'entry-content')]",
                                "//*[contains(translate(@class,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'post-content')]",
                                "//*[contains(translate(@class,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'page-content')]",
                                "//*[contains(translate(@class,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'content')]"
                            ] as $expr) {
                                foreach ($xp->query($expr) as $n) $candidates[] = $n;
                            }
                            if ( ! $candidates ) {
                                $b = $doc->getElementsByTagName('body');
                                if ($b->length) $candidates[] = $b->item(0);
                            }
            
                            $best = null; $best_len = -1;
                            foreach ($candidates as $node) {
                                $txt = trim( wp_strip_all_tags($node->textContent ?? '') );
                                $len = strlen($txt);
                                if ($len > $best_len) { $best_len = $len; $best = $node; }
                            }
                            if ( ! $best ) return $html;
            
                            $inner = '';
                            foreach ($best->childNodes as $child) {
                                $inner .= $best->ownerDocument->saveHTML($child);
                            }
                            return $inner ?: $html;
                        }
                    }
                    
                    // NEW: extract first <h1> text as plain string
                    if ( ! function_exists('ai_sc_extract_h1_from_html') ) {
                        function ai_sc_extract_h1_from_html( $html ) {
    ai_sc_debug(__FUNCTION__ . '() start', array('args'=>func_get_args()));

                            if ( preg_match('#<h1[^>]*>(.*?)</h1>#is', $html, $m) ) {
                                return trim( wp_strip_all_tags( html_entity_decode($m[1], ENT_QUOTES | ENT_HTML5, 'UTF-8') ) );
                            }
                            return '';
                        }
                    }
                    
                    // Strip non-text (images/icons) and fix CITY*/STATE* token issues & duplicate backlinks
                    if ( ! function_exists('isc_ai_sc_text_only_html') ) {
                        function isc_ai_sc_text_only_html( $html, $home, $city, $state ) {
                            if ( ! is_string($html) || $html === '' ) return '';
                    
                            // 1) Replace raw tokens first (case-insensitive)
                            $html = str_ireplace(['CITY*','STATE*'], [esc_html($city), esc_html($state)], $html);
                    
                            // 2) If we have "... in City State. <a ...>in City State</a>", collapse to a single linked phrase
                            $cRx = preg_quote($city, '~');  // city for regex
                            $sRx = preg_quote($state,'~');  // state for regex
                            $html = preg_replace(
                                '~\bin\s+'.$cRx.'\s+'.$sRx.'(\s*[.,!?])?\s*(<a\b[^>]*>\s*in\s+'.$cRx.'\s+'.$sRx.'\s*</a>)~i',
                                '$2$1',
                                $html
                            );
                    
                            // 3) DOM cleanup: remove images/icons/pictures/svg/i and common "icon" wrappers
                            $doc = new DOMDocument();
                            libxml_use_internal_errors(true);
                            $ok  = $doc->loadHTML('<?xml encoding="utf-8" ?>' . $html);
                            libxml_clear_errors();
                            if ( ! $ok ) return $html;
                    
                            $xp = new DOMXPath($doc);
                            $killQueries = [
                                '//img','//picture','//figure','//svg','//i',
                                '//*[@role="img"]',
                                '//*[contains(@class,"icon")]',
                                '//*[contains(@class,"fa-") or contains(@class," fa ") or contains(@class," fas") or contains(@class," far") or contains(@class," fab")]',
                                '//*[contains(@class,"material-icons")]',
                                '//*[contains(@class,"glyphicon")]',
                                '//*[contains(@class,"mdi")]',
                                '//*[contains(@class,"ion-")]',
                            ];
                            foreach ($killQueries as $q) {
                                foreach ($xp->query($q) as $n) {
                                    if ($n && $n->parentNode) $n->parentNode->removeChild($n);
                                }
                            }
                    
                            // Remove empty anchors that were only icons
                            foreach ($xp->query('//a') as $a) {
                                if (trim($a->textContent) === '') {
                                    if ($a->parentNode) $a->parentNode->removeChild($a);
                                }
                            }
                    
                            // Serialize body children back to HTML
                            $out = '';
                            $body = $doc->getElementsByTagName('body')->item(0);
                            if ($body) {
                                foreach ($body->childNodes as $child) {
                                    $out .= $doc->saveHTML($child);
                                }
                            }
                            return $out ?: $html;
                        }
                    }
                    
                    // === Helper: build a WordPress-safe slug from a URL's last path segment ===
                    if ( ! function_exists('isc_ai_sc_slug_from_url') ) {
                        function isc_ai_sc_slug_from_url( $url ) {
                            if ( ! is_string($url) || $url === '' ) return '';
                            $path = parse_url( $url, PHP_URL_PATH );
                            if ( ! $path ) return '';
                            // remove trailing slash to get the last segment reliably
                            $path = rtrim( $path, '/' );
                            $slug = basename( $path );
                            // decode and sanitize for use as WP post_name
                            $slug = sanitize_title( urldecode( $slug ) );
                            if ( $slug === '' ) {
                                // fallback when the URL has no last segment
                                $slug = 'page-' . time();
                            }
                            return $slug;
                        }
                    }
                    
                    // === Helper: retarget internal links to existing WP pages (exact, alias, then fuzzy) ===
                    if ( ! function_exists('isc_ai_sc_retarget_internal_links') ) {
                        function isc_ai_sc_retarget_internal_links( $html, $home ) {
                            if ( ! is_string($html) || $html === '' ) return $html;

                            $home_host = parse_url( $home, PHP_URL_HOST );

                            // 1) Build lookup of existing pages
                            $pages = get_posts([
                                'post_type'      => 'page',
                                'post_status'    => ['publish','pending','private'],
                                'posts_per_page' => -1,
                                'fields'         => 'ids',
                            ]);

                            $path_to_link = []; // "parent/child" => permalink
                            $slug_to_link = []; // "slug"        => permalink
                            foreach ( $pages as $pid ) {
                                $uri  = trim( get_page_uri( $pid ), '/' );
                                $slug = get_post_field( 'post_name', $pid );
                                $per  = get_permalink( $pid );
                                if ( $uri !== '' )  { $path_to_link[$uri] = $per; }
                                if ( $slug !== '' ) { $slug_to_link[$slug] = $per; }
                            }

                            // 2) Alias map: common link slugs => preferred alternatives on typical sites.
                            //    We try each candidate in order until we find one that exists.
                            $alias_map = [
                                'appointments' => ['special','schedule-appointment','appointment-request','request-appointment','book-appointment','contact-us','contact'],
                                'appointment'  => ['special','schedule-appointment','appointment-request','request-appointment','book-appointment','contact-us','contact'],
                                'contact'      => ['contact-us','contact-us-form','request-appointment','schedule-appointment'],
                                'locations'    => ['location','our-locations','find-us'],
                                'reviews'      => ['testimonials','patient-reviews'],
                            ];
                            // Allow customization via filter (optional)
                            if ( function_exists('apply_filters') ) {
                                $alias_map = apply_filters('isc_ai_sc_retarget_aliases', $alias_map);
                            }

                            // 3) Helper: does a given slug/path exist?
                            $find_existing = function( $seg ) use ( $slug_to_link, $path_to_link, $home ) {
                                $seg = trim($seg,'/');
                                if ($seg === '') return null;

                                // try hierarchical path
                                if ( isset($path_to_link[$seg]) ) return $path_to_link[$seg];

                                // try simple slug
                                $last = basename($seg);
                                if ( isset($slug_to_link[$last]) ) return $slug_to_link[$last];

                                // try WP resolver just in case (hierarchical)
                                if ( function_exists('url_to_postid') ) {
                                    $pid = url_to_postid( trailingslashit( $home ) . ltrim($seg,'/') );
                                    if ( $pid ) return get_permalink($pid);
                                }
                                return null;
                            };

                            // 4) DOM parse & rewrite <a href>
                            $doc = new DOMDocument();
                            libxml_use_internal_errors(true);
                            $doc->loadHTML( '<?xml encoding="utf-8" ?>' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );

                            $anchors = $doc->getElementsByTagName('a');
                            foreach ( $anchors as $a ) {
                                $href = $a->getAttribute('href');
                                if ( $href === '' ) continue;
                                if ( $href[0] === '#' ) continue;

                                $p = @parse_url( $href );
                                
                                // Skip non-http(s) schemes (mailto:, tel:, javascript:, etc.)
                                if ( $p && isset($p['scheme']) && ! in_array(strtolower($p['scheme']), ['http','https'], true) ) {
                                    continue;
                                }

                                $is_internal = false;
                                $path = ''; $query = ''; $frag = '';

                                if ( $p && isset($p['host']) ) {
                                    if ( strcasecmp( $p['host'], $home_host ) !== 0 ) continue; // external
                                    $path  = isset($p['path'])  ? ltrim($p['path'], '/') : '';
                                    $query = isset($p['query']) ? $p['query'] : '';
                                    $frag  = isset($p['fragment']) ? $p['fragment'] : '';
                                    $is_internal = true;
                                } else {
                                    $path  = isset($p['path'])  ? ltrim($p['path'], '/') : '';
                                    $query = isset($p['query']) ? $p['query'] : '';
                                    $frag  = isset($p['fragment']) ? $p['fragment'] : '';
                                    $is_internal = ($path !== '');
                                }

                                if ( ! $is_internal || $path === '' ) continue;

                                // Normalize path and last segment
                                $path = rtrim($path, '/');
                                if ( $path === '' ) continue;
                                $seg  = basename($path);

                                // 4a) Exact match
                                $target = $find_existing($path);
                                if ( ! $target ) $target = $find_existing($seg);

                                // 4b) Alias match (e.g., /appointments -> /schedule-appointment or /contact-us)
                                if ( ! $target ) {
                                    // singular check: appointments -> appointment
                                    $variants = [];
                                    if ( substr($seg,-1) === 's' ) {
                                        $variants[] = rtrim($seg,'s');
                                    }
                                    $variants[] = $seg;

                                    foreach ( $variants as $vseg ) {
                                        if ( isset($alias_map[$vseg]) ) {
                                            foreach ( $alias_map[$vseg] as $candidate ) {
                                                $cand = ltrim($candidate,'/');
                                                $hit = $find_existing($cand);
                                                if ( $hit ) { $target = $hit; break 2; }
                                            }
                                        }
                                    }
                                }

                                // 4c) Fuzzy slug fallback
                                if ( ! $target ) {
                                    $best_slug = null; $best_perm = null; $best_dist = PHP_INT_MAX;
                                    foreach ( $slug_to_link as $slug => $perm ) {
                                        $d = levenshtein( $seg, $slug );
                                        if ( $d < $best_dist ) {
                                            $best_dist = $d; $best_slug = $slug; $best_perm = $perm;
                                        }
                                    }
                                    $len = max( strlen($seg), 1 );
                                    if ( $best_perm && $best_dist <= max(2, (int)ceil($len/3)) ) {
                                        $target = $best_perm;
                                    }
                                }

                                // 4d) DEFAULT: if still no match, send to homepage
                                if ( ! $target ) {
                                    $target = $home; // e.g., https://yoursite.com/
                                }

                                if ( $target ) {
                                    // Reattach query & fragment if they existed
                                    if ( $query ) $target = add_query_arg( wp_parse_args($query, []), $target );
                                    if ( $frag )  $target .= '#' . $frag;
                                    $a->setAttribute('href', $target);
                                }
                            }

                            $out = $doc->saveHTML();
                            libxml_clear_errors();
                            return $out ?: $html;
                        }
                    }


            
  $created = []; $ai_errors = []; $ai_warnings = [];

// === AI SmartCopy: HANDOFF PHASE (show bar immediately, then start work) ===
if ( empty($_POST['ai_phase']) ) {
    // Calculate total (if available) so the initial label looks right
    $ai_total = isset($ai_action_list) && is_array($ai_action_list) ? count($ai_action_list) : 0;

    // Progress bar styles + container (same look/feel as your other bars)
    echo '<style>
      @keyframes aiMoveStripes {
        from { background-position: 0 0; }
        to   { background-position: 8px 0; }
      }
      .ai-pinstripe-bar {
        background-color: #0073AA;
        background-image: repeating-linear-gradient(
          45deg,
          rgba(255,255,255,0.3) 0,
          rgba(255,255,255,0.3) 1px,
          transparent 1px,
          transparent 4px
        );
        background-size: 8px 8px;
        border-radius: 6px;
        animation: aiMoveStripes 0.7s linear infinite;
      }
    </style>';

    $pct0 = ($ai_total > 0) ? 2 : 0;
    echo '<div id="ai-progress-container" style="margin-top:20px;">';
      echo '<div style="background:#eee; overflow:hidden; height:30px; width:100%; margin-bottom:10px; border-radius:6px;">';
        echo '<div id="ai-progress-bar" class="ai-pinstripe-bar" '
           . 'style="width:' . intval($pct0) . '%; background:#0073AA; color:#fff; height:30px; '
           . 'line-height:30px; text-align:center; font-weight:bold; transition: width 0.2s ease;">'
           . '0 / ' . intval($ai_total) . ' (' . intval($pct0) . '%)'
           . '</div>';
      echo '</div>';
      echo '<p id="ai-progress-message" style="font-weight:bold;">Preparing source…</p>';
      echo '<img id="ai-progress-gif" src="' . plugins_url('../search-and-replace/working-on-it.gif', __FILE__) . '" '
         . 'alt="Please wait" style="width:400px; margin:10px 0 20px 0;" />';
    echo '</div>';

    // Immediately flush so the 2% bar shows before the auto-submit
    if (ob_get_level() === 0) { @ob_start(); }
    echo str_repeat(' ', 32768); @ob_flush(); @flush();

    // Build a “phase 1” auto-submit with the original POST data + ai_phase=1
    echo '<form id="ai-handoff" method="post" action="">';

      // renew nonce to be safe for the follow-up POST
      $new_nonce = wp_create_nonce('sitemap_nonce');
      echo '<input type="hidden" name="_wpnonce" value="'.esc_attr($new_nonce).'">';

      // copy through everything that was posted on the initial submit
      foreach ( $_POST as $k => $v ) {
        if ($k === '_wpnonce') { continue; } // replaced above
        if (is_array($v)) {
          foreach ($v as $vv) {
            echo '<input type="hidden" name="'.esc_attr($k).'[]" value="'.esc_attr($vv).'">';
          }
        } else {
          echo '<input type="hidden" name="'.esc_attr($k).'" value="'.esc_attr($v).'">';
        }
      }

      // signal phase 1 (the long-running work)
      echo '<input type="hidden" name="ai_phase" value="1">';
    echo '</form>';

    // Let the browser paint the bar, then submit to start the heavy loop
    echo "<script>
      (function(){
        var f = document.getElementById('ai-handoff');
        var go = function(){ if (f) f.submit(); };
        if (window.requestAnimationFrame) {
          requestAnimationFrame(function(){ requestAnimationFrame(go); });
        } else {
          setTimeout(go, 120);
        }
      })();
    </script>";

    // Push bytes so the bar appears even on buffered setups
    echo str_repeat(' ', 2048);
    if (function_exists('ob_flush')) { @ob_flush(); }
    flush();
    exit; // IMPORTANT: stop Phase 0 here
}
// === END HANDOFF PHASE ==================================================

// === AI SmartCopy Progress Bar: PHASE-1 REPAINT + anti-buffer ===========
if (function_exists('ini_set')) {
    if (!headers_sent()) {
        @ini_set('zlib.output_compression', '0');
    }
  @ini_set('output_buffering', 'off');
}
if (!headers_sent()) {
  @header('Content-Type: text/html; charset=utf-8');
  @header('Cache-Control: no-cache, must-revalidate');
  // Helps on Nginx FastCGI
  @header('X-Accel-Buffering: no');
}
if (function_exists('apache_setenv')) { @apache_setenv('no-gzip', '1'); }

echo '<style>
  @keyframes aiMoveStripes { from{background-position:0 0} to{background-position:8px 0} }
  .ai-pinstripe-bar{
    background-color:#0073AA;
    background-image:repeating-linear-gradient(45deg,rgba(255,255,255,.3)0,rgba(255,255,255,.3)1px,transparent 1px,transparent 4px);
    background-size:8px 8px;border-radius:6px;animation:aiMoveStripes .7s linear infinite;
  }
</style>';

// === AI SmartCopy RESUME BOOTSTRAP =====================================
$__home_host = preg_replace('/^www\./','', parse_url( home_url('/'), PHP_URL_HOST ));
$__ai_run_id = isset($_POST['ai_run_id']) ? sanitize_key(wp_unslash($_POST['ai_run_id'])) : '';
if ( $__ai_run_id === '' ) {
  $__ai_run_id = str_replace('-', '', wp_generate_uuid4());
  $_POST['ai_run_id'] = $__ai_run_id;
}
$__ai_state_file = ABSPATH . 'ai-smartcopy-state-'
  . sanitize_file_name($__home_host)
  . '-' . get_current_user_id()
  . '-' . $__ai_run_id
  . '.json';

// Normalize original list once
$__ai_all_list = (is_array($ai_action_list) ? array_values(array_unique($ai_action_list)) : []);
$__ai_selection_hash = hash('sha256', wp_json_encode([
  'all' => $__ai_all_list,
  'type_map' => isset($ai_type_map) ? $ai_type_map : [],
]));

// Load or initialize state file
$__ai_state = file_exists($__ai_state_file) ? json_decode(@file_get_contents($__ai_state_file), true) : null;
if (!is_array($__ai_state)
    || empty($__ai_state['all'])
    || empty($__ai_state['selection_hash'])
    || !hash_equals($__ai_selection_hash, (string)$__ai_state['selection_hash'])
) {
  $__ai_state = [
    'all'     => $__ai_all_list,  // full immutable list in original order
    'done'    => [],              // URLs completed
    'queue'   => $__ai_all_list,  // remaining URLs to do (in order)
    'created' => [],              // WP post IDs created
    'ts'      => time(),
  
    'type_map' => isset($ai_type_map) ? $ai_type_map : [],
    'selection_hash' => $__ai_selection_hash,
  ];
  @file_put_contents($__ai_state_file, json_encode($__ai_state), LOCK_EX);
} else {
  // Keep state tidy on every load
  $__ai_state['all']   = array_values(array_intersect($__ai_state['all'], $__ai_all_list ?: $__ai_state['all']));
  $__ai_state['done']  = array_values(array_intersect($__ai_state['done'] ?: [], $__ai_state['all']));
  // If queue missing, rebuild from (all - done)
  if (!isset($__ai_state['queue']) || !is_array($__ai_state['queue'])) {
    $__ai_state['queue'] = array_values(array_diff($__ai_state['all'], $__ai_state['done']));
  } else {
    // Ensure queue only contains items still in ALL and not in DONE
    $__ai_state['queue'] = array_values(array_intersect($__ai_state['queue'], $__ai_state['all']));
    $__ai_state['queue'] = array_values(array_diff($__ai_state['queue'], $__ai_state['done']));
  }
}

// Use the queue for this run
$ai_action_list = $__ai_state['queue'];

// Resume-aware counts
$__ai_total_full   = count($__ai_state['all']);   // total selected originally
$__ai_done_prefill = count($__ai_state['done']);  // already completed before this request
$__ai_remaining    = count($ai_action_list);
$__ai_initial_percent_resume = ($__ai_total_full > 0) ? floor(($__ai_done_prefill / $__ai_total_full) * 100) : 0;
// =======================================================================


$__ai_total       = $__ai_remaining;              // remaining (for loop)
$__ai_done        = $__ai_done_prefill;           // already done
$__ai_initial_percent = $__ai_initial_percent_resume; // resume baseline

echo '<div id="ai-progress-container" style="margin-top:20px;">';
  echo '<div style="background:#eee;overflow:hidden;height:30px;width:100%;margin-bottom:10px;border-radius:6px;">';
    echo '<div id="ai-progress-bar" class="ai-pinstripe-bar" '
       . 'style="width:' . intval($__ai_initial_percent) . '%;background:#0073AA;color:#fff;height:30px;line-height:30px;'
       . 'text-align:center;font-weight:bold;transition:width .2s ease;">'
       . intval($__ai_done) . ' / ' . intval($__ai_total_full) . ' (' . intval($__ai_initial_percent) . '%)'
       . '</div>';
  echo '</div>';
  echo '<p id="ai-progress-message" style="font-weight:bold;">Preparing source…</p>';
  echo '<img id="ai-progress-gif" src="' . plugins_url('../search-and-replace/working-on-it.gif', __FILE__) . '" '
     . 'alt="Please wait" style="width:400px;margin:10px 0 20px 0;" />';
echo '</div>';

// Flush immediately so the user sees 2% before any PHP work (fetch/parse)
if (ob_get_level() === 0) { @ob_start(); }
echo str_repeat(' ', 32768); @ob_flush(); @flush();

// Make sure the bar paints before heavy work (strong anti-buffer)
if (function_exists('ini_set')) {
    if (!headers_sent()) {
        @ini_set('zlib.output_compression', '0'); 
    }
}
if (function_exists('apache_setenv')) { @apache_setenv('no-gzip', '1'); }
if (!headers_sent()) {
  @header('Cache-Control: no-cache, must-revalidate');
  @header('X-Accel-Buffering: no'); // Nginx FastCGI
}

// Make sure the bar paints before heavy work (anti-buffer) + overlay handoff
if (function_exists('ini_set')) { 
    if (!headers_sent()) {
        @ini_set('zlib.output_compression', '0'); 
    }
    @ini_set('output_buffering', 'off'); 
}
if (function_exists('apache_setenv')) { @apache_setenv('no-gzip', '1'); }
if (!headers_sent()) {
  @header('Cache-Control: no-cache, must-revalidate');
  @header('X-Accel-Buffering: no'); // Nginx FastCGI
}

// Push enough bytes to defeat proxy buffering so the bar can render
if (ob_get_level() === 0) { @ob_start(); }
echo str_repeat(' ', 65536); // 64KB
@ob_flush(); @flush();

// NEW: only micro-boot to 2–7% when starting from scratch (avoid 0/x flicker on resume)
if ($__ai_done_prefill === 0) {
    // Prime bar at **2%** and fade out any existing overlay
    echo "<script>(function(){
      var bar=document.getElementById('ai-progress-bar');
      var msg=document.getElementById('ai-progress-message');
      if(bar){ bar.dataset.pct='2'; bar.style.width='2%'; bar.textContent='0 / " . intval($__ai_total) . " (2%)'; }
      if(msg){ msg.textContent='AI SmartCopy… starting.'; }
      var o=document.getElementById('ai-overlay');
      if(o){ o.style.opacity='0'; setTimeout(function(){ if(o&&o.parentNode){ o.parentNode.removeChild(o); } }, 280); }
    })();</script>";
    echo str_repeat(' ', 4096);
    @ob_flush(); @flush();


    // Two quick micro-chunks to force an early paint
    echo "<script>document.getElementById('ai-progress-message')&&"
       . " (document.getElementById('ai-progress-message').textContent='AI SmartCopy… starting.');</script>";
    echo str_repeat(' ', 4096);
    @ob_flush(); @flush();
    usleep(60000); // 60ms
    echo "<script>(function(){var b=document.getElementById('ai-progress-bar');"
       . " if(b){b.style.width='5%';b.dataset.pct='5';b.textContent='0 / "
       . intval($__ai_total)
       . " (5%)';}})();</script>";
    echo str_repeat(' ', 4096);
    @ob_flush(); @flush();

    // --- INSERT: early OpenAI preflight cues (message + gentle bump) --------
    echo "<script>(function(){
      var bar=document.getElementById('ai-progress-bar');
      var msg=document.getElementById('ai-progress-message');
      function setMsg(t){ if(msg){ msg.textContent=t; } }

      // After ~300ms: if still ~2–5%, tell the user we’re preparing requests
      setTimeout(function(){
        if(!bar) return;
        var cur=parseInt(bar.dataset.pct||'0',10);
        if(cur<=5){ setMsg('Preparing requests…'); }
      }, 300);

      // After ~1200ms: if still <7%, announce OpenAI connect and bump to 7–8%
      setTimeout(function(){
        if(!bar) return;
        var cur=parseInt(bar.dataset.pct||'0',10);
        if (cur<7){
          cur = 7;
          bar.style.width = cur + '%';
          bar.dataset.pct = String(cur);
          bar.textContent = '0 / " . intval($__ai_total) . " (' + cur + '%)';
          setMsg('Connecting to OpenAI…');
        }
      }, 1200);
    })();</script>";
    echo str_repeat(' ', 2048);
    @ob_flush(); @flush();
} // <-- END micro-boot guard

// --- Heartbeat + micro-boot --------------------------------------------
echo "<script>
(function(){
  // shared state that PHP updates
    window._aiPB = {
      total: " . intval($__ai_total_full) . ",   // full total
      done:  " . intval($__ai_done) . ",         // already completed
      targetPct: " . intval($__ai_initial_percent) . ",
      hardDone: false
    };

  var bar = document.getElementById('ai-progress-bar');
  var msg = document.getElementById('ai-progress-message');

    // PRIME to resume baseline (not 2%)
    if (bar) {
      var base  = " . intval($__ai_initial_percent) . ";
      var done  = " . intval($__ai_done) . ";
      var total = " . intval($__ai_total_full) . ";
      bar.dataset.pct = String(base);
      bar.style.width = base + '%';
      bar.textContent = done + ' / ' + total + ' (' + base + '%)';
    }

  // Micro-boot: gently step up to 15% while waiting for first server tick
  // (stops as soon as server raises targetPct above current)
  var bootCap   = 15;
  var bootEvery = 900;  // ms
  var boot = setInterval(function(){
    if (!bar) return;
    var cur = parseInt(bar.dataset.pct || '0', 10);
    var tgt = Math.max(0, Math.min(100, Math.floor(window._aiPB.targetPct || 0)));
    if (tgt > cur || cur >= bootCap || window._aiPB.hardDone) { clearInterval(boot); return; }
    cur = Math.min(cur + 1, bootCap);
    bar.style.width = cur + '%';
    bar.dataset.pct = String(cur);
    bar.textContent = (window._aiPB.done || 0) + ' / ' + (window._aiPB.total || 0) + ' (' + cur + '%)';
  }, bootEvery);
  window._aiBoot = boot; 

  // Main heartbeat: smoothly chase server target and add tiny idle bumps, ≤98%
  var tickMs   = 800;
  var idleJump = 1;
  var hb = setInterval(function(){
    if (!bar) return;
    var cur = parseInt(bar.dataset.pct || '0', 10);
    var tgt = Math.max(0, Math.min(100, Math.floor(window._aiPB.targetPct || 0)));
    var softCap = window._aiPB.hardDone ? 100 : Math.min(98, tgt + 5);

    if (cur < tgt) {
      cur = Math.min(cur + Math.min(2, tgt - cur), softCap);
    } else if (!window._aiPB.hardDone && cur < softCap) {
      cur = Math.min(cur + idleJump, softCap);
    }
    if (cur > 100) cur = 100;

    bar.style.width = cur + '%';
    bar.dataset.pct = String(cur);
    bar.textContent = (window._aiPB.done || 0) + ' / ' + (window._aiPB.total || 0) + ' (' + cur + '%)';

    if (window._aiPB.hardDone && cur >= 100) clearInterval(hb);
  }, tickMs);
  window._aiHB = hb;
})();
</script>";
// ------------------------------------------------------------------------

// --- NEW: Work-units model (10 PHP + 16 Bash = 26 per item) -------------
// IMPORTANT: compute against FULL selection (persists across resumes)
$__PHP_STEPS        = 10;
$__BASH_STEPS       = 16;
$__PER_ITEM         = $__PHP_STEPS + $__BASH_STEPS; // 26
$__ai_total_full    = isset($__ai_total_full) ? $__ai_total_full : count($__ai_state['all']);
$__TOTAL_UNITS_FULL = max(1, $__ai_total_full * $__PER_ITEM);
// ------------------------------------------------------------------------

// === AI SmartCopy TIME BUDGET (auto-resume) =============================
$__run_start = microtime(true);
$__php_max   = (int)ini_get('max_execution_time');
if ($__php_max <= 0) { $__php_max = 120; } // many hosts ~120s hard cap
$__SAFETY    = 6;   // leave time for flushing + auto-submit
$__deadline  = $__run_start + max(60, $__php_max - $__SAFETY);

// Do NOT start a *new* item unless at least this many seconds remain.
// Tune to your slowest single page. 25s is a safe default.
$__NEXT_ITEM_SAFETY = 45;
// =======================================================================

foreach ( $ai_action_list as $link ) {
    $action_type = isset($__ai_state['type_map'][$link]) ? $__ai_state['type_map'][$link] : (isset($ai_type_map[$link]) ? $ai_type_map[$link] : 'smartcopy-page');

    // Keep source content and generated values isolated to the current URL.
    unset(
        $html,
        $title_guess,
        $content_html,
        $first_p,
        $new_p,
        $updated_html,
        $clean_html,
        $h1_text,
        $page_title_for_h1,
        $meta_desc,
        $external_h1,
        $external_title,
        $new_id
    );

        // PRE-FLIGHT: if we’re too close to the deadline, persist and auto-resume
    if ( microtime(true) >= ($__deadline - $__NEXT_ITEM_SAFETY) ) {
        // Persist state (queue unchanged because we have NOT marked $link done)
        if (!empty($created)) {
            $__ai_state['created'] = array_values(array_unique(array_merge($__ai_state['created'], array_map('intval', (array)$created))));
        }
        @file_put_contents($__ai_state_file, json_encode($__ai_state), LOCK_EX);

        // Tell the UI and re-post with ai_phase=1
        echo "<script>(function(){
          var m=document.getElementById('ai-progress-message');
          if(m){ m.textContent='Auto-resuming to avoid timeout…'; }
        })();</script>";
        if(function_exists('ob_flush')){ @ob_flush(); } flush();

        echo '<form id="ai-resume-now" method="post" action="">';
          $new_nonce = wp_create_nonce('sitemap_nonce');
          echo '<input type="hidden" name="_wpnonce" value="'.esc_attr($new_nonce).'">';
          foreach($_POST as $k=>$v){
            if($k==='_wpnonce') continue;
            if(is_array($v)){
              foreach($v as $vv){ echo '<input type="hidden" name="'.esc_attr($k).'[]" value="'.esc_attr($vv).'">'; }
            } else {
              echo '<input type="hidden" name="'.esc_attr($k).'" value="'.esc_attr($v).'">';
            }
          }
          echo '<input type="hidden" name="ai_phase" value="1">';
        echo '</form><script>setTimeout(function(){var f=document.getElementById("ai-resume-now"); if(f) f.submit();}, 200);</script>';
        exit;
    }

    // --- INSERT: show progress while the FIRST network fetch runs -------
    echo "<script>(function(){
      var m=document.getElementById('ai-progress-message');
      if(m){ m.textContent='Fetching source page HTML…'; }
      window._aiPB = window._aiPB || {total: ".intval($__ai_total).", done: (window._aiPB && window._aiPB.seeded ? window._aiPB.done : 0), targetPct: 0, hardDone:false};
      var bar=document.getElementById('ai-progress-bar');
      var cur=parseInt((bar && bar.dataset.pct) || '0', 10);
      var minTgt = (cur < 4) ? 4 : cur;   // gentle nudge to 4%
      if (window._aiPB.targetPct < minTgt) window._aiPB.targetPct = minTgt;
    })();</script>";
    echo str_repeat(' ', 2048);
    if(function_exists('ob_flush')){ @ob_flush(); } flush();
    // --------------------------------------------------------------------

    $html = ai_sc_fetch_url_html( $link );
    if ( is_wp_error($html) ) {
        $ai_errors[] = esc_html($link) . ': ' . $html->get_error_message();
        goto AISCPERITEMDONE;
    }

    // --- PHP pre-bash progress scaffold (UNITS) -------------------------
    $__php_ticks = 0;                         // ticks completed for THIS item (0..$__PHP_STEPS)

    $___tick = function($label) use (&$__php_ticks, $__PHP_STEPS, $__PER_ITEM, &$__ai_done, $__TOTAL_UNITS_FULL, $__ai_total_full) {
        $__php_ticks = min($__php_ticks + 1, $__PHP_STEPS);
        $units_done_now = ($__ai_done * $__PER_ITEM) + $__php_ticks;
        $pct = floor(($units_done_now / $__TOTAL_UNITS_FULL) * 100);
        if ($pct > 99) $pct = 99;
    
        $labelj = json_encode($label);
        echo "<script>(function(){
          window._aiPB = window._aiPB || {total: ".intval($__ai_total_full).", done: ".intval($__ai_done).", targetPct: 0, hardDone: false};
          window._aiPB.total = ".intval($__ai_total_full).";
          window._aiPB.done  = ".intval($__ai_done).";
          window._aiPB.targetPct = ".intval($pct).";
          var m=document.getElementById('ai-progress-message'); if(m){ m.textContent = ".$labelj."; }
        })();</script>";
        echo str_repeat(' ', 512);
        if(function_exists('ob_flush')){ @ob_flush(); } flush();
    };
    // --------------------------------------------------------------------

    // 1) Parsed title
    $title_guess  = ai_sc_parse_title_from_html( $html, 'Imported Page' );
    $___tick('AI SmartCopy… parsed title');
    
    // --- INSERT: show progress during parsing & extraction ------------------
    echo "<script>(function(){
      var m=document.getElementById('ai-progress-message');
      if(m){ m.textContent='Parsing and extracting primary content…'; }
      window._aiPB = window._aiPB || {total: ".intval($__ai_total).", done: (window._aiPB && window._aiPB.seeded ? window._aiPB.done : 0), targetPct: 0, hardDone:false};
      var bar=document.getElementById('ai-progress-bar');
      var cur=parseInt((bar && bar.dataset.pct) || '0', 10);
      var minTgt = (cur < 6) ? 6 : cur;   // nudge to ~6% while DOM work happens
      if (window._aiPB.targetPct < minTgt) window._aiPB.targetPct = minTgt;
    })();</script>";
    echo str_repeat(' ', 1024);
    if(function_exists('ob_flush')){ @ob_flush(); } flush();
    // -----------------------------------------------------------------------

    // 2) Extracted primary content
    $content_html = ai_sc_extract_primary_content_html( $html );
    $___tick('AI SmartCopy… extracted body content');

// --- INSERT: show progress during intro detection ----------------------
echo "<script>(function(){
  var m=document.getElementById('ai-progress-message');
  if(m){ m.textContent='Locating intro paragraph…'; }
  window._aiPB = window._aiPB || {total: ".intval($__ai_total).", done: (window._aiPB && window._aiPB.seeded ? window._aiPB.done : 0), targetPct: 0, hardDone:false};
  var bar=document.getElementById('ai-progress-bar');
  var cur=parseInt((bar && bar.dataset.pct) || '0', 10);
  var minTgt = (cur < 8) ? 8 : cur;   // nudge to ~8% pre-OpenAI
  if (window._aiPB.targetPct < minTgt) window._aiPB.targetPct = minTgt;
})();</script>";
echo str_repeat(' ', 1024);
if(function_exists('ob_flush')){ @ob_flush(); } flush();
// -----------------------------------------------------------------------

// 3) Found first paragraph
$first_p = function_exists('isc_ai_sc_extract_first_paragraph')
    ? isc_ai_sc_extract_first_paragraph($content_html)
    : (
        preg_match('#<p[^>]*>.*?</p>#is', $content_html, $mm)
            ? $mm[0]
            : '<p>' . esc_html( wp_trim_words( wp_strip_all_tags( $content_html ), 120 ) ) . '</p>'
      );
$___tick('AI SmartCopy… found first paragraph');

// === REWRITE BRANCH: if selected, bypass SmartCopy intro/meta and rewrite full content ===
if ( isset($action_type) && strpos($action_type, 'rewrite') === 0 ) {
    $msg = 'Rewriting full page content…';
    echo "<script>(function(){var m=document.getElementById('ai-progress-message'); if(m){ m.textContent=" . json_encode($msg) . "; }})();</script>";
    echo str_repeat(' ', 1024); if(function_exists('ob_flush')){ @ob_flush(); } flush();

    $clinic_name  = get_option('isc_clinic_name', get_bloginfo('name'));
    $openai_model = get_option('isc_openai_model', 'gpt-4o-mini');
    $api_key      = function_exists('isc_ai_sc_get_api_key')
        ? isc_ai_sc_get_api_key()
        : get_option('isc_openai_api_key', '');

    // Rewrite only the content extracted from this URL.
    $source_html = $content_html;

    $rewrite = ai_sc_rewrite_page_content($api_key, $openai_model, $clinic_name, $city, $state, $source_html);
    if ( is_wp_error($rewrite) ) {
        $ai_errors[] = esc_html($link) . ': ' . $rewrite->get_error_message();
    } else {
        $rw_title = trim((string)($rewrite['seo_title'] ?? ''));
        $rw_meta  = trim((string)($rewrite['meta_desc'] ?? ''));
        // Preserve the source URL slug; the OpenAI slug is only a fallback.
        $rw_slug  = isc_ai_sc_slug_from_url($link);
        if ($rw_slug === '') {
            $rw_slug = trim((string)($rewrite['slug'] ?? ''));
        }
        $rw_html  = (string)($rewrite['html'] ?? '');
        if ($rw_html === '') { $rw_html = $source_html; }
        if (!empty($rewrite['fidelity_warnings']) && is_array($rewrite['fidelity_warnings'])) {
            $review_messages = array_map('ai_sc_format_fidelity_warning', $rewrite['fidelity_warnings']);
            $ai_warnings[] = esc_html($link) . ': The item was published successfully. '
                . implode(' ', $review_messages);
        }

        // Give full rewrites the same first-paragraph homepage backlink as SmartCopy.
        $rewrite_first_p = isc_ai_sc_extract_first_paragraph($rw_html);
        if ($rewrite_first_p === '') {
            $rewrite_first_p = '<p>' . esc_html(wp_trim_words(wp_strip_all_tags($rw_html), 90)) . '</p>';
        }
        $rewrite_linked_p = isc_ai_sc_rewrite_paragraph_with_backlink(
            $rewrite_first_p,
            $openai_model,
            $api_key,
            $city,
            $state,
            home_url('/')
        );
        if (is_wp_error($rewrite_linked_p)) {
            $ai_errors[] = esc_html($link) . ': ' . $rewrite_linked_p->get_error_message();
            goto AISCPERITEMDONE;
        }
        $rw_html = isc_ai_sc_replace_first_paragraph($rw_html, $rewrite_linked_p);

        // Rewrite Page imports the page template; Rewrite Blog imports the post template.
        $rewrite_post_type = $action_type === 'rewrite-post' ? 'post' : 'page';
        $rewrite_h1 = trim((string) ai_sc_extract_h1_from_html($html));
        $external_title = $rw_title !== '' ? $rw_title : ($rewrite_h1 !== '' ? $rewrite_h1 : $title_guess);
        $external_title = ai_sc_ensure_city_state_title($external_title, $city, $state);
        $new_id = isc_ai_sc_import_template_content(
            $rewrite_post_type,
            $link,
            $external_title,
            $rw_html,
            $city,
            $state,
            $rw_meta,
            $rw_slug
        );

        if ( ! is_wp_error($new_id) && $new_id ) {
            $created[] = (int)$new_id;
        } else {
            $creation_error = is_wp_error($new_id)
                ? $new_id->get_error_message()
                : 'WordPress did not return a new content ID.';
            $ai_errors[] = ($external_title ?: 'Rewritten content')
                . ': failed to publish '
                . ($rewrite_post_type === 'post' ? 'blog post' : 'page')
                . '. '
                . $creation_error;
        }
    }

    // Move to next item (skip SmartCopy path)
    $__php_ticks = 0;
    echo str_repeat(' ', 1024); if(function_exists('ob_flush')) { @ob_flush(); } flush();
    // PERSIST STATE for rewrite too
    $__ai_state['done'][] = (string)$link;
    $__ai_state['done']   = array_values(array_unique($__ai_state['done']));
    if (!empty($created)) {
      $__ai_state['created'] = array_values(array_unique(array_merge($__ai_state['created'], array_map('intval', (array)$created))));
    }
    $__ai_state['queue'] = array_values(array_diff($__ai_state['all'], $__ai_state['done']));
    @file_put_contents($__ai_state_file, json_encode($__ai_state), LOCK_EX);

    // Jump to per-item update so the loop continues
    goto AISCPERITEMDONE;
}

$___tick('AI SmartCopy… continuing standard flow');

// --- INSERT: tell the user we’re connecting to OpenAI for the intro -----
echo "<script>(function(){
  var m=document.getElementById('ai-progress-message');
  if(m){ m.textContent='Connecting to OpenAI… (intro rewrite)'; }
  // nudge targetPct to at least 10–12% if it’s still below that
  window._aiPB = window._aiPB || {total: ".intval($__ai_total).", done: (window._aiPB && window._aiPB.seeded ? window._aiPB.done : 0), targetPct: 0, hardDone:false};
  var bar=document.getElementById('ai-progress-bar');
  var cur=parseInt((bar && bar.dataset.pct) || '0', 10);
  var minTgt = (cur < 10) ? 10 : cur;      // safe, your unit ticks will quickly take over
  if (window._aiPB.targetPct < minTgt) window._aiPB.targetPct = minTgt;
})();</script>";
echo str_repeat(' ', 1024);
if(function_exists('ob_flush')){ @ob_flush(); } flush();
// ------------------------------------------------------------------------

// 4) Rewrite first paragraph with OpenAI (heavy)
$___tick('AI SmartCopy… rewriting intro with AI');
$new_p = function_exists('isc_ai_sc_rewrite_paragraph_with_backlink')
    ? isc_ai_sc_rewrite_paragraph_with_backlink($first_p, $model, $api_key, $city, $state, $home)
    : new WP_Error('missing_helper','Rewrite helper not found.');
if ( is_wp_error($new_p) ) {
    $ai_errors[] = esc_html($link) . ': ' . $new_p->get_error_message();
    goto AISCPERITEMDONE;
}
$___tick('AI SmartCopy… rewrite complete');

    // 5) Replace first paragraph in content
    $updated_html = function_exists('isc_ai_sc_replace_first_paragraph')
        ? isc_ai_sc_replace_first_paragraph($content_html, $new_p)
        : (preg_replace('#<p[^>]*>.*?</p>#is', $new_p, $content_html, 1) ?: $new_p . "\n\n" . $content_html);
    $___tick('AI SmartCopy… injected intro');

    // 6) Build text-only HTML for shell fallback
    $clean_html = isc_ai_sc_text_only_html($updated_html, $home, $city, $state);
    $___tick('AI SmartCopy… cleaned HTML');

    // 7) Retarget internal links
    $updated_html = isc_ai_sc_retarget_internal_links( $updated_html, $home );
    $___tick('AI SmartCopy… retargeted links');

// 8) Grab H1
$h1_text  = ai_sc_extract_h1_from_html( $html );
$page_title_for_h1 = $h1_text ?: $title_guess;
$___tick('AI SmartCopy… extracted H1');

// 9) Generate meta description (heavy)
// --- INSERT: announce OpenAI meta pass + small safe nudge ---------------
echo "<script>(function(){
  var m=document.getElementById('ai-progress-message');
  if(m){ m.textContent='Connecting to OpenAI… (meta description)'; }
  window._aiPB = window._aiPB || {total: ".intval($__ai_total).", done: (window._aiPB && window._aiPB.seeded ? window._aiPB.done : 0), targetPct: 0, hardDone:false};
  var bar=document.getElementById('ai-progress-bar');
  var cur=parseInt((bar && bar.dataset.pct) || '0', 10);
  var minTgt = (cur < 16) ? 16 : cur;   // gentle; your unit math will pass this quickly
  if (window._aiPB.targetPct < minTgt) window._aiPB.targetPct = minTgt;
})();</script>";
echo str_repeat(' ', 1024);
if(function_exists('ob_flush')){ @ob_flush(); } flush();
// ------------------------------------------------------------------------

$___tick('AI SmartCopy… generating meta description');
$clinic_name  = get_option('isc_clinic_name', get_bloginfo('name'));
$keyphrase    = $page_title_for_h1;
$meta_desc    = ai_sc_generate_meta_desc($api_key, $model, $clinic_name, $city, $state, $keyphrase);
$___tick('AI SmartCopy… meta description ready');

    // 10) (Optional) choose image now (if you do it pre-bash)
    // If you prefer to do this later, move the tick accordingly.
    $___tick('AI SmartCopy… selecting image');
    // (image pick actually happens a bit later in your existing code)
    // --------------------------------------------------------------------

    // Decide shell mode: Beaver Builder shell page or HTML shell
    $mode     = get_option('isc_shell_mode','textarea');
    $shell_id = (int) get_option('isc_shell_page_id', 0);
    $h1_text  = ai_sc_extract_h1_from_html( $html );
    $page_title_for_h1 = $h1_text ?: $title_guess;
            
// 2) Build from chosen shell
if ($mode === 'page' && $shell_id > 0) {
    // Duplicate the shell EXACTLY via WP-CLI (keeps all modules intact)

    // These should already be set earlier in your loop:
    // $title_guess  = ai_sc_parse_title_from_html($html, 'Imported Page');
    // $h1_text      = ai_sc_extract_h1_from_html($html);
    // If you don't have them yet, compute them above this point.
    $external_h1 = isset($h1_text) && $h1_text ? $h1_text : ( isset($title_guess) && $title_guess ? $title_guess : 'Imported Page' );

    $new_id = isc_ai_sc_wpcli_duplicate_page( $shell_id, $external_h1 );

    // SAFETY: ensure accumulators are arrays before pushing (prevents scalar fatal)
    if ( ! isset($errors)  || ! is_array($errors) )  { $errors  = []; }
    if ( ! isset($created) || ! is_array($created) ) { $created = []; }

    if ( is_wp_error($new_id) || ! $new_id ) {
        $errors[] = $external_h1 . ': ' . ( is_wp_error($new_id) ? $new_id->get_error_message() : 'failed to create page.' );
    } else {
        // === Import shell via bash & replace placeholders on THAT imported page ===
        $uploads = wp_upload_dir();
        $tmp_dir = trailingslashit($uploads['basedir']) . 'ai-smartcopy';
        if ( ! file_exists($tmp_dir) ) wp_mkdir_p($tmp_dir);

        // Save the AI-processed CONTENT to a temp file (this replaces C*)
        $tmp_file = tempnam($tmp_dir, 'content_') . '.html';

$updated_html = isc_ai_sc_sanitize_import_html($updated_html);
$external_h1  = wp_strip_all_tags($external_h1); // H1 as plain text
file_put_contents($tmp_file, $updated_html);

// === NEW: Local-SEO meta description ===
// Use clinic name from settings (or site name), and keyphrase from H1 as a sensible default
$clinic_name  = get_option('isc_clinic_name', get_bloginfo('name'));
$keyphrase    = $external_h1; // you can swap to a different source if you have one
$meta_desc    = ai_sc_generate_meta_desc($api_key, $model, $clinic_name, $city, $state, $keyphrase);

// NEW: read selected AI types and pick the correct script per-URL
$ai_pages = isset($ai_pages_norm) ? $ai_pages_norm : [];
$ai_posts = isset($ai_posts_norm) ? $ai_posts_norm : [];

// Normalize the current link to the same key shape as above
$cur = esc_url_raw($link);
$pp  = @parse_url($cur);
$cur_path = isset($pp['path']) ? rtrim($pp['path'], '/') : '';
$current_link_key = (isset($pp['scheme'],$pp['host']) ? $pp['scheme'].'://'.$pp['host'].($cur_path === '' ? '' : $cur_path) : '');

$use_page = in_array($current_link_key, $ai_pages, true);
$use_post = in_array($current_link_key, $ai_posts, true);

// Enforce mutual exclusivity
if ( $use_page && $use_post ) {
    $ai_errors[] = 'Both "AI SmartCopy - Page" and "AI SmartCopy - Blog Post" selected for ' . esc_html( $link ) . ' — skipping.';
    @unlink($tmp_file);
    goto AISCPERITEMDONE;
}

// Decide script (DEFAULT to page if neither checkbox matched)
if ( $use_post ) {
    $script = plugin_dir_path(__FILE__) . 'bin/replace_for_post.sh';   // Blog Post
} else {
    $script = plugin_dir_path(__FILE__) . 'bin/replace_for_page.sh';   // Page (default)
}


// Sanity: script exists and is readable
if ( ! file_exists($script) ) {
    $ai_errors[] = 'Importer script not found: ' . esc_html( $script ) . ' for ' . esc_html($link);
    @unlink($tmp_file);
    goto AISCPERITEMDONE;
}

// === Pick image by imagegroup then label (using blog-template-list.php) ===
$image_url = isc_ai_sc_pick_image_url_for_page(
    $external_h1,
    $updated_html,
    isc_ai_sc_get_api_key(),
    get_option('isc_openai_model', 'gpt-4o-mini')
);
// Fallback to placeholder if AI returns nothing
if (! $image_url) {
    $image_url = 'https://www.chiro.inceptionimages.com/wp-content/uploads/2023/01/Placeholder-714x250-1.jpg';
}

// === NEW: build desired slug from the selected external URL ================
$desired_slug = isc_ai_sc_slug_from_url( $link );   // use the current URL in the loop

$domain = preg_replace('/^www\./','', parse_url( home_url(), PHP_URL_HOST ));

// NOTE: bash performs the import and echoes UPDATED:domain:NEWID
$cmd = 'bash ' . escapeshellarg($script)
     . ' ' . escapeshellarg($domain)
     . ' ' . escapeshellarg('0')
     . ' ' . escapeshellarg($external_h1)
     . ' ' . escapeshellarg($city)
     . ' ' . escapeshellarg($state)
     . ' ' . escapeshellarg($tmp_file)
     . ' ' . escapeshellarg($image_url)
     . ' ' . escapeshellarg($desired_slug)
     . ' ' . escapeshellarg($meta_desc)
     . ' 2>&1';

// ──────────────────────────────────────────────────────────────────────────
// REPLACE from here (was: $out = shell_exec($cmd); @unlink($tmp_file); … )
// with a streaming proc_open() that watches PROGRESS markers from bash:
$descriptorSpec = [
    0 => ["pipe", "r"], // stdin
    1 => ["pipe", "w"], // stdout
    2 => ["pipe", "w"]  // stderr
];

// SECONDARY PRE-FLIGHT: if we’re too close now, bail before starting bash
if ( microtime(true) >= ($__deadline - $__NEXT_ITEM_SAFETY) ) {
    // Persist current state without marking this $link done
    if (!empty($created)) {
        $__ai_state['created'] = array_values(array_unique(array_merge($__ai_state['created'], array_map('intval', (array)$created))));
    }
    @file_put_contents($__ai_state_file, json_encode($__ai_state), LOCK_EX);

    echo "<script>(function(){
      var m=document.getElementById('ai-progress-message');
      if(m){ m.textContent='Auto-resuming to avoid timeout…'; }
    })();</script>";
    if(function_exists('ob_flush')){ @ob_flush(); } flush();
    
    // NEW: seed next page with current progress (prevents 0/x dip)
    echo "<script>try{sessionStorage.setItem('aiPB_seed', JSON.stringify({done:".intval($__ai_done).", total:".intval($__ai_total_full)."}));}catch(e){}</script>";

    echo '<form id="ai-resume-now" method="post" action="">';
      $new_nonce = wp_create_nonce('sitemap_nonce');
      echo '<input type="hidden" name="_wpnonce" value="'.esc_attr($new_nonce).'">';
      foreach($_POST as $k=>$v){
        if($k==='_wpnonce') continue;
        if(is_array($v)){
          foreach($v as $vv){ echo '<input type="hidden" name="'.esc_attr($k).'[]" value="'.esc_attr($vv).'">'; }
        } else {
          echo '<input type="hidden" name="'.esc_attr($k).'" value="'.esc_attr($v).'">';
        }
      }
      echo '<input type="hidden" name="ai_phase" value="1">';
    echo '</form><script>setTimeout(function(){var f=document.getElementById("ai-resume-now"); if(f) f.submit();}, 200);</script>';
    exit;
}

$process = @proc_open($cmd, $descriptorSpec, $pipes);

$out_full = '';
// Bash sub-steps start at 0..$__BASH_STEPS for THIS item
$sub_total = $__BASH_STEPS;   // expect 16; if script prints PROGRESS:n/N, we’ll adopt its N
$sub_prog  = 0;               // 0..$sub_total

if (is_resource($process)) {
    // non-blocking so we can flush to browser
    stream_set_blocking($pipes[1], false);
    stream_set_blocking($pipes[2], false);

    // Keep reading until the process ends
    while (true) {
        $stdout = stream_get_contents($pipes[1]);
        $stderr = stream_get_contents($pipes[2]);

        if ($stdout !== false && $stdout !== '') {
            $out_full .= $stdout;

            // Parse lines for progress hints like:
            //   PROGRESS:7/12
            //   TICK
            foreach (preg_split("/\r\n|\n|\r/", $stdout) as $line) {
                $line = trim($line);
                if ($line === '') continue;

                // UPDATED:domain:NEWID (we still capture later too)
                if (preg_match('/^UPDATED:[^:]+:(\d+)/', $line)) {
                    // nothing to do here; we parse final output below
                }

                // PROGRESS:n/N
                if (preg_match('/^PROGRESS:(\d+)\/(\d+)/i', $line, $m)) {
                    $cur = max(0, (int)$m[1]);
                    $tot = max(1, (int)$m[2]);
                    $sub_total = $tot;
                    $sub_prog  = min($cur, $tot);
                } elseif (preg_match('/^TICK\b/i', $line)) {
                    // If the script only prints TICKs, we just increment a coarse counter
                    $sub_prog = min($sub_prog + 1, $sub_total);
                }

                // Units: completed items + PHP ticks (so far) + bash ticks (so far)
                $units_done_now = ($__ai_done * $__PER_ITEM) + min($__php_ticks, $__PHP_STEPS) + min($sub_prog, max(1,$sub_total));
                $overall_pct = floor(($units_done_now / $__TOTAL_UNITS_FULL) * 100);
                if ($overall_pct > 99) $overall_pct = 99;
                
                $__msg_js = json_encode('AI SmartCopy… ' . (string)$external_h1);
                
                echo "<script>(function(){
                  window._aiPB = window._aiPB || {total: ".intval($__ai_total_full).", done: ".intval($__ai_done).", targetPct: 0, hardDone: false};
                  window._aiPB.total = ".intval($__ai_total_full).";
                  window._aiPB.done  = ".intval($__ai_done).";
                  window._aiPB.targetPct = ".intval($overall_pct).";
                  var s=document.getElementById('ai-progress-message'); if(s){ s.textContent=".$__msg_js."; }
                })();</script>";
                echo str_repeat(' ', 512);
                if(function_exists('ob_flush')){ @ob_flush(); } flush();
                @set_time_limit(20);
            }
        }

        if ($stderr !== false && $stderr !== '') {
            $out_full .= $stderr; // keep everything for diagnostics
        }

        $status = proc_get_status($process);
        if (!$status['running']) {
            break;
        }

        // Small breather so we don’t spin hot
        usleep(120000); // 120ms
    }

    // Close pipes & get exit code
    foreach ($pipes as $p) { @fclose($p); }
    $exitCode = proc_close($process);
} else {
    // Fallback if proc_open failed
    $out_full = shell_exec($cmd);
    $exitCode = 0;
}

@unlink($tmp_file);

// Parse the NEW imported page ID (works for both streaming + fallback)
$new_import_id = 0;
if ( is_string($out_full) && preg_match('/UPDATED:[^:]+:(\d+)/', $out_full, $mm) ) {
    $new_import_id = (int) $mm[1];
} else {
    // DEBUG: write the last few KB so you can inspect what the script printed
    $uploads = wp_upload_dir();
    $dbg = trailingslashit($uploads['basedir']).'ai-smartcopy-last.out';
    @file_put_contents($dbg, is_string($out_full) ? $out_full : '');
}
if ( ! $new_import_id ) {
    $snippet = is_string($out_full) ? wp_trim_words( trim($out_full), 30, '…' ) : 'No output';
    $ai_errors[] = $external_h1 . ': import/S&R failed. Output: ' . esc_html( $snippet );
} else {
    $created[] = $new_import_id;
}


    }
} else {
    // HTML shell fallback
    $external_title = isset($h1_text) && $h1_text ? $h1_text : ( isset($title_guess) && $title_guess ? $title_guess : 'Imported Page' );
    $new_id = isc_ai_sc_build_from_html_shell($external_title, $clean_html, $city, $state); // CHANGED
    
    // SAFETY: ensure accumulators are arrays before pushing (prevents scalar fatal)
    if ( ! isset($errors)  || ! is_array($errors) )  { $errors  = []; }
    if ( ! isset($created) || ! is_array($created) ) { $created = []; }

    if (is_wp_error($new_id) || ! $new_id) {
        $errors[] = $external_title . ': failed to create page.';
    } else {
        $created[] = (int) $new_id;
    }
}

                        // SAFETY: ensure they are arrays before pushing (avoids scalar fatal)
                        if ( ! isset($errors)  || ! is_array($errors) )  { $errors  = []; }
                        if ( ! isset($created) || ! is_array($created) ) { $created = []; }
                
                        if ( is_wp_error($new_id) || ! $new_id ) {
                            $errors[] = $external_title . ': failed to create page.';
                        } else {
                            $created[] = (int) $new_id;
                        }
            

    AISCPERITEMDONE:
// === AI SmartCopy Progress Bar: PER-ITEM UPDATE (UNITS) ================
$__ai_done++;
$units_done_after   = $__ai_done * $__PER_ITEM;
$__ai_percent_units = floor(($units_done_after / $__TOTAL_UNITS_FULL) * 100);
if ($__ai_percent_units > 99 && $__ai_done < $__ai_total_full) { $__ai_percent_units = 99; }
    
    $__ai_link_label = mb_strimwidth( (string)$link, 0, 80, "…", 'UTF-8' );
    $__ai_link_label_js = json_encode('AI SmartCopy… finished ' . $__ai_link_label);
    
    echo "<script>(function(){
      window._aiPB = window._aiPB || {total: ".intval($__ai_total_full).", done: (window._aiPB && window._aiPB.seeded ? window._aiPB.done : 0), targetPct: 0, hardDone: false};
      window._aiPB.total = ".intval($__ai_total_full).";
      window._aiPB.done  = ".intval($__ai_done).";
      window._aiPB.targetPct = ".intval($__ai_percent_units).";
      var s=document.getElementById('ai-progress-message'); if(s){ s.textContent=".$__ai_link_label_js."; }
    })();</script>";
    
    // Reset per-item PHP tick counter for the next item
    $__php_ticks = 0;
    
    echo str_repeat(' ', 1024);
    if(function_exists('ob_flush')) { @ob_flush(); }
    flush();
    
    // --- PERSIST STATE (mark this link done, record created id) ------------
$__ai_state['done'][] = (string)$link;
$__ai_state['done']   = array_values(array_unique($__ai_state['done']));
if (!empty($created)) {
  // merge any new IDs found in this iteration
  $__ai_state['created'] = array_values(array_unique(array_merge($__ai_state['created'], array_map('intval', (array)$created))));
}
// Rebuild queue = (all - done), preserving original order
$__ai_state['queue'] = array_values(array_diff($__ai_state['all'], $__ai_state['done']));
@file_put_contents($__ai_state_file, json_encode($__ai_state), LOCK_EX);

// --- If we’re close to timeout and still have items, auto-resume --------
if ( microtime(true) >= ($__deadline - 2) && !empty($__ai_state['queue']) ) {
  // Nudge UI text
  echo "<script>(function(){
    var m=document.getElementById('ai-progress-message');
    if(m){ m.textContent='Saving progress… resuming automatically'; }
  })();</script>";
  if(function_exists('ob_flush')){ @ob_flush(); } flush();

  // Auto-resume form (ai_phase=1), replaying original POST payload
  echo '<form id="ai-resume" method="post" action="">';
    $new_nonce = wp_create_nonce('sitemap_nonce');
    echo '<input type="hidden" name="_wpnonce" value="'.esc_attr($new_nonce).'">';
    echo '<input type="hidden" id="ai_resume_done" value="'.intval($__ai_done).'">';
    echo '<input type="hidden" id="ai_resume_total" value="'.intval($__ai_total_full).'">';

    foreach ( $_POST as $k => $v ) {
      if ($k === '_wpnonce') continue;
      if (is_array($v)) {
        foreach ($v as $vv) {
          echo '<input type="hidden" name="'.esc_attr($k).'[]" value="'.esc_attr($vv).'">';
        }
      } else {
        echo '<input type="hidden" name="'.esc_attr($k).'" value="'.esc_attr($v).'">';
      }
    }
    echo '<input type="hidden" name="ai_phase" value="1">';
  echo '</form>';
  // NEW: seed next page so it boots at the exact same progress
  echo "<script>try{sessionStorage.setItem('aiPB_seed', JSON.stringify({done:".intval($__ai_done).", total:".intval($__ai_total_full)."}));}catch(e){}</script>";

  echo "<script>setTimeout(function(){ var f=document.getElementById('ai-resume'); if(f) f.submit(); }, 300);</script>";
  if(function_exists('ob_flush')){ @ob_flush(); } flush();
  exit;
}
// -----------------------------------------------------------------------

    @set_time_limit(30);
    // ===================================================================

} // <-- end foreach ($ai_action_list as $link)

// If the loop exited but the queue still has items, auto-resume now.
$__state_now = file_exists($__ai_state_file) ? json_decode(@file_get_contents($__ai_state_file), true) : null;
if (is_array($__state_now) && !empty($__state_now['queue'])) {
  echo "<script>(function(){
    var m=document.getElementById('ai-progress-message');
    if(m){ m.textContent='Resuming… (more pages in queue)'; }
  })();</script>";
  if(function_exists('ob_flush')){ @ob_flush(); } flush();

  echo '<form id="ai-resume-postloop" method="post" action="">';
    $new_nonce = wp_create_nonce('sitemap_nonce');
    echo '<input type="hidden" name="_wpnonce" value="'.esc_attr($new_nonce).'">';
    foreach ( $_POST as $k => $v ) {
      if ($k === '_wpnonce') continue;
      if (is_array($v)) {
        foreach ($v as $vv) {
          echo '<input type="hidden" name="'.esc_attr($k).'[]" value="'.esc_attr($vv).'">';
        }
      } else {
        echo '<input type="hidden" name="'.esc_attr($k).'" value="'.esc_attr($v).'">';
      }
    }
    echo '<input type="hidden" name="ai_phase" value="1">';
  echo '</form>';
  
  // NEW: seed next page so it boots at the exact same progress
  echo "<script>try{sessionStorage.setItem('aiPB_seed', JSON.stringify({done:".intval($__ai_done).", total:".intval($__ai_total_full)."}));}catch(e){}</script>";

  echo "<script>setTimeout(function(){ var f=document.getElementById('ai-resume-postloop'); if(f) f.submit(); }, 200);</script>";
  if(function_exists('ob_flush')){ @ob_flush(); } flush();
  exit;
}

// === CLEANUP: all items processed in this run? ==========================
$__remaining_after = 0;
$__tmp_state_for_success = null;
if (file_exists($__ai_state_file)) {
  $__tmp_state_for_success = json_decode(@file_get_contents($__ai_state_file), true);
  $__remaining_after = (is_array($__tmp_state_for_success) && !empty($__tmp_state_for_success['queue'])) ? count($__tmp_state_for_success['queue']) : 0;
}
// (Do not unlink here; we’ll remove after the success list renders)
// =======================================================================


// === AI SmartCopy Progress Bar: DONE ===================================
echo "<script>(function(){
  // mark complete
  window._aiPB = window._aiPB || {total: 0, done: (window._aiPB && window._aiPB.seeded ? window._aiPB.done : 0), targetPct: 0, hardDone: false};
  window._aiPB.done = " . intval($__ai_total_full) . ";
  window._aiPB.targetPct = 100;
  window._aiPB.hardDone = true;

  // stop any intervals if we set them earlier
  try { if (window._aiBoot) clearInterval(window._aiBoot); } catch(e){}
  try { if (window._aiHB)   clearInterval(window._aiHB);   } catch(e){}

  // fade out GIF then remove entire progress container (bar + message + gif)
  var cont = document.getElementById('ai-progress-container');
  var gif  = document.getElementById('ai-progress-gif');

  if (gif) {
    gif.style.transition = 'opacity .25s ease';
    gif.style.opacity = '0';
  }

  if (cont) {
    // quick micro-delay so the GIF fade can start, then fade container
    setTimeout(function(){
      cont.style.transition = 'opacity .28s ease, transform .28s ease';
      cont.style.opacity = '0';
      cont.style.transform = 'translateY(-4px)';
      setTimeout(function(){
        if (cont && cont.parentNode) cont.parentNode.removeChild(cont);
      }, 320);
    }, 80);
  }
})();</script>";
if (function_exists('ob_flush')) { @ob_flush(); }
flush();
// --- Hard cleanup: ensure no leftover UI affects layout -----------------
echo "<script>(function(){
  ['ai-progress-container','ai-overlay','ai-progress-gif'].forEach(function(id){
    var el = document.getElementById(id);
    if (el && el.parentNode) el.parentNode.removeChild(el);
  });
  var wrap = document.querySelector('#wpbody-content, .wrap');
  if (wrap) { wrap.style.minHeight=''; wrap.style.height=''; }
  var firstNotice = document.querySelector('.notice');
  if (firstNotice && firstNotice.scrollIntoView) {
    firstNotice.scrollIntoView({block:'start', behavior:'auto'});
  }
})();</script>";
if (function_exists('ob_flush')) { @ob_flush(); }
flush();
// -----------------------------------------------------------------------
            
                    // Build the final "created" list from persisted state (covers auto-resumes)
                    $__all_created_ids = [];
                    if (is_array($__tmp_state_for_success) && !empty($__tmp_state_for_success['created'])) {
                        $__all_created_ids = array_map('intval', $__tmp_state_for_success['created']);
                    }
                    if (empty($__all_created_ids) && !empty($created)) {
                        $__all_created_ids = array_map('intval', (array)$created);
                    }
                    $__all_created_ids = array_values(array_filter(array_unique($__all_created_ids)));
                    
                    if ( !empty($__all_created_ids) ) {
                        // Filter: keep unique, non-zero IDs; exclude the Shell Page ID
                        $shell_id = (int) get_option('isc_shell_page_id', 0);
                        $__all_created_ids = array_values(array_filter($__all_created_ids, function($pid) use ($shell_id) {
                            return $pid > 0 && $pid !== $shell_id;
                        }));
                    
                        $lines = [];
                        foreach ($__all_created_ids as $pid) {
                            $public_url = get_permalink($pid);
                            if ( ! $public_url ) continue;
                    
                            $ptype = get_post_type($pid);
                            $label = ($ptype === 'post') ? 'Blog Post' : (($ptype === 'page') ? 'Page' : ucfirst($ptype));
                    
                            $builder_url = add_query_arg('fl_builder', 1, $public_url);
                            $edit_url    = get_edit_post_link($pid, '');
                    
                            $lines[] = sprintf(
                                '<li>[%4$s] <a href="%1$s" target="_blank" rel="noopener noreferrer">%1$s</a> — <a href="%2$s" target="_blank" rel="noopener noreferrer">Builder</a> — <a href="%3$s" target="_blank" rel="noopener noreferrer">Edit</a></li>',
                                esc_url($public_url),
                                esc_url($builder_url),
                                esc_url($edit_url),
                                esc_html($label)
                            );
                        }
                    
                        $count = count($lines);
                    
                        printf(
                            '<div class="notice notice-success"><p><strong>AI SmartCopy:</strong> Created %d item(s).</p><ul style="margin-left:1.25em;list-style:disc;">%s</ul></div>',
                            $count,
                            implode("\n", $lines)
                        );
                    }
                    
                    if ( $ai_errors ) {
                        $safe_errors = array_map(
                            static function($error) {
                                return make_clickable(esc_html($error));
                            },
                            $ai_errors
                        );
                        echo '<div class="notice notice-error"><p><strong>AI SmartCopy errors:</strong><br>' . implode('<br>', $safe_errors) . '</p></div>';
                    }
                    if ( !empty($ai_warnings) ) {
                        $safe_warnings = array_map(
                            static function($warning) {
                                return make_clickable(esc_html($warning));
                            },
                            $ai_warnings
                        );
                        echo '<div class="notice notice-warning"><p><strong>AI SmartCopy: published items needing review</strong><br>' . implode('<br>', $safe_warnings) . '</p></div>';
                    }
                // Final tidy: remove state file if the queue is empty
                if (file_exists($__ai_state_file)) {
                  $st = json_decode(@file_get_contents($__ai_state_file), true);
                  if (is_array($st) && empty($st['queue'])) {
                    @unlink($__ai_state_file);
                  }
                }

                }
            }
            
// === NEW: Rewrite Page Content ================================================
if ( ! function_exists('ai_sc_rewrite_page_content') ) {
    /**
     * Rewrite existing HTML content using OpenAI per local SEO guidelines.
     * Returns array [ 'seo_title' => ..., 'meta_desc' => ..., 'slug' => ..., 'html' => ... ]
     */
    function ai_sc_rewrite_page_content($api_key, $model, $clinic, $city, $state, $source_html) {
        $prompt = "Act as a highly skilled SEO specialist and expert copywriter. Rewrite the provided webpage content to create a unique, plagiarism-free version optimized specifically for local SEO, using placeholders for easy customization: CLINIC NAME*, CITY*, and STATE*.\n\n"
                . "Follow Yoast SEO guidelines for readability, ideal length, keyword density, and key phrase distribution:\n"
                . "SEO Title:\n"
                . "Provide a catchy, keyword-rich SEO title (50–60 characters) containing the exact phrase CITY* STATE*, with no comma between the city and state.\n"
                . "Meta Description:\n"
                . "Craft a concise, compelling meta description (max 155 characters) featuring CLINIC NAME*, primary key phrase, CITY*, and STATE*.\n"
                . "Slug:\n"
                . "Recommend a short, keyword-focused URL slug optimized for local SEO (4–5 words, ideally including the primary key phrase and CITY*).\n"
                . "Rewrite Guidelines:\n"
                . "Rewrite clearly, naturally integrating:\n"
                . "Primary key phrase\nCLINIC NAME*\nCITY*, STATE*\n\n"
                . "Include subheadings containing secondary key phrases or relevant local keywords.\n\n"
                . "Maintain an informal, conversational, human-like tone:\n"
                . "Use personal pronouns\nInclude rhetorical questions, analogies, and metaphors\nVary sentence structure and wording to avoid repetitive, cookie-cutter content\n\n"
                . "Achieve high readability scores according to Yoast’s recommendations:\n"
                . "Short, clear sentences\nActive voice\nLogical transitions between paragraphs\nCarefully proofread and correct grammar and style to ensure professional quality.\n\n"
                . "Rewrite the following HTML content between <CONTENT> tags:\n<CONTENT>\n" . strip_tags((string)$source_html) . "\n</CONTENT>\n\n"
                . "Return JSON with keys: seo_title, meta_desc, slug, html.";
        // Use your existing OpenAI call helper
        $resp = ai_sc_openai_chat_json($api_key, $model, $prompt);
        if (is_wp_error($resp)) return $resp;
        // Basic decode and safety
        $data = json_decode($resp['text'] ?? '', true);
        if (!is_array($data)) {
            $data = ['seo_title'=>'', 'meta_desc'=>'', 'slug'=>'', 'html'=>$resp['text'] ?? ''];
        }
        return $data;
    }
}

// ——— clean up downloaded copyscape file from server  ———
if ( file_exists( $filename ) ) {
    @unlink( $filename );
}
// === END NEW: AI SmartCopy ====================================================

            // ---------------------------------------------
            // 5.b-continued) Expose the ZIP download URL
            // (on every pass, Copyscape or not)
            // ---------------------------------------------
            $parsed_url = parse_url( $domain );
            $host       = preg_replace( '/^www\./', '', $parsed_url['host'] ?? 'site' );
            $zip_path   = ABSPATH . $host . '-pages.zip';
            
            if ( file_exists( $zip_path ) ) {
                $zip_created = true;
                $zip_url = home_url( '/' . basename( $zip_path ) );
                echo '<input type="hidden" id="zip-download-url" value="' . esc_url( $zip_url ) . '" />';
            
                // Immediately trigger the ZIP download, then hide the HTML download UI
                echo "<script>
                  (function(){
                    var zipEl = document.getElementById('zip-download-url');
                    if (zipEl && zipEl.value) {
                      // Mark ready + keep URL as a fallback
                      document.documentElement.setAttribute('data-zip-ready','1');
                      document.documentElement.setAttribute('data-zip-url', zipEl.value);
                    }
                
                    // Hide HTML downloads bar/message/GIF
                    var bar = document.getElementById('progress-bar');
                    if (bar) {
                      var cont = bar.parentElement;
                      bar.style.display = 'none';
                      if (cont) cont.style.display = 'none';
                    }
                    var msg = document.getElementById('html-progress-message');
                    if (msg) msg.style.display = 'none';
                    var gif = document.getElementById('html-progress-gif');
                    if (gif) gif.style.display = 'none';
                  })();
                </script>";
            }

                /**
                 * Heuristic content extractor with “skip to content” support,
                 * boilerplate filtering, AND removal of CSS/JS blocks.
                 */
                function extractMainContent(string $html): string {
                    libxml_use_internal_errors(true);
                    $doc   = new DOMDocument();
                    $doc->loadHTML($html);
                    $xpath = new DOMXPath($doc);
                
                    // 0) Strip out all <script>, <style>, and <noscript> blocks
                    foreach ($xpath->query('//script | //style | //noscript') as $bad) {
                        $bad->parentNode->removeChild($bad);
                    }
                
                    // 1) “Skip to content” link?
                    $skipLinks = $xpath->query(
                        '//a[contains(translate(normalize-space(text()),"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"),"skip to content") 
                                and starts-with(@href,"#")]'
                    );
                    if ($skipLinks->length) {
                        $href = $skipLinks->item(0)->getAttribute('href');
                        $id   = ltrim($href, '#');
                        $nodes = $xpath->query('//*[@id="' . $id . '"]');
                        if ($nodes->length) {
                            $text = trim(strip_tags($doc->saveHTML($nodes->item(0))));
                            // collapse excessive whitespace
                            return preg_replace('/\s+/', ' ', $text);
                        }
                    }
                
                    // 2) candidate containers
                    $nodes     = $xpath->query('//main | //article | //section | //div');
                    $bestScore = 0;
                    $bestHTML  = '';
                
                    foreach ($nodes as $node) {
                        $tag = strtolower($node->nodeName);
                        if (in_array($tag, ['nav','header','footer','aside'], true)) {
                            continue;
                        }
                        $cls = trim($node->getAttribute('class') . ' ' . $node->getAttribute('id'));
                        if (
                            preg_match('/\b(nav|header|footer|sidebar|widget)\b/i',        $cls) ||
                            preg_match('/nav-/i',                                          $cls) ||
                            preg_match('/menu/i',                                          $cls) ||
                            preg_match('/contact-practitioner-content/i',                  $cls)
                        ) {
                            continue;
                        }
                
                        $text = trim($node->textContent);
                        $len  = mb_strlen($text);
                        if ($len < 200) {
                            continue;
                        }
                
                        $links       = $node->getElementsByTagName('a');
                        $linkTextLen = 0;
                        foreach ($links as $a) {
                            $linkTextLen += mb_strlen(trim($a->textContent));
                        }
                        $linkDensity = $linkTextLen / $len;
                        $score       = $len * (1 - $linkDensity);
                
                        if ($score > $bestScore) {
                            $bestScore = $score;
                            $bestHTML  = $doc->saveHTML($node);
                        }
                    }
                
                    if ($bestScore > 0) {
                        $clean = trim(strip_tags($bestHTML));
                        return preg_replace('/\s+/', ' ', $clean);
                    }
                
                    // fallback: entire page text
                    $full = trim(strip_tags($html));
                    return preg_replace('/\s+/', ' ', $full);
                }
                
                // ---------------------------------------------
                // 5.c) Copyscape API check (only after final batch)
                // ---------------------------------------------
                
                // Decide if Copyscape should start now:
                if ( ! isset($remaining_urls) || ! is_array($remaining_urls) ) {
                  $remaining_urls = [];
                }
                
                // Are there still HTML downloads to do in a follow-up pass?
                $has_more_downloads = count($remaining_urls) > 0;
                
                // Start Copyscape when no more HTML work is queued this run AND we have selections
                $should_run_copyscape = ( ! $has_more_downloads && ! empty($copyscape_list) );
                
                // After packaging ZIP, and BEFORE Copyscape logic runs:
                if ( $should_run_copyscape && empty($_POST['copyscape_phase']) ) {
                    ?>
                    <form id="handoff-to-copyscape" method="post" action="">
                        <?php $new_nonce = wp_create_nonce('sitemap_nonce'); ?>
                        <input type="hidden" name="action"               value="sitemap" />
                        <input type="hidden" name="_wpnonce"             value="<?php echo esc_attr($new_nonce); ?>" />
                        <input type="hidden" name="confirm_page_choices" value="1" />
                        <input type="hidden" name="copyscape_phase"      value="1" />
                        <input type="hidden" name="domainToCheck"        value="<?php echo esc_attr($_POST['domainToCheck']); ?>" />
                        <?php foreach ( $download_html_urls as $u )   echo '<input type="hidden" name="download_html_urls[]"   value="'.esc_attr($u).'">'; ?>
                        <?php foreach ( $copyscape_check_urls as $u ) echo '<input type="hidden" name="copyscape_check_urls[]" value="'.esc_attr($u).'">'; ?>
                        <?php foreach ( $skip_urls as $u )            echo '<input type="hidden" name="skip_urls[]"            value="'.esc_attr($u).'">'; ?>
                    </form>
                    <script>
                      // give the UI a beat to hide the HTML progress, then start Copyscape fresh
                      setTimeout(function(){ document.getElementById('handoff-to-copyscape').submit(); }, 300);
                    </script>
                    <?php
                    exit;
                }
                
                // ── PROGRESS BAR STYLES (safe to emit anytime) ──
                echo '<style>
                  @keyframes moveStripes {
                    from { background-position: 0 0; }
                    to   { background-position: 8px 0; }
                  }
                  .pinstripe-bar {
                    background-color: #0073AA;
                    background-image: repeating-linear-gradient(
                      45deg,
                      rgba(255,255,255,0.3) 0,
                      rgba(255,255,255,0.3) 1px,
                      transparent 1px,
                      transparent 4px
                    );
                    background-size: 8px 8px;
                    border-radius: 6px;
                    animation: moveStripes 0.7s linear infinite;
                  }
                </style>';
                
                    if ( $should_run_copyscape ) {
                    // Define $cs_history_file early to avoid undefined variable
                    $parsed_url = parse_url( $domain );
                    $host = preg_replace( '/^www\./', '', $parsed_url['host'] ?? 'site' );
                    $cs_history_file = ABSPATH . 'copyscape-history-' . $host . '.json';
                    
                    // Initialize Copyscape progress from history so refreshes keep the % and counts
                    $cs_results = file_exists( $cs_history_file )
                      ? json_decode( file_get_contents( $cs_history_file ), true )
                      : [];
                    $checked_count = is_array($cs_results) ? count($cs_results) : 0;
                    
                    // Be robust if $total_copyscape is not set yet
                    if ( !isset($total_copyscape) ) {
                      $total_copyscape = isset($copyscape_list) && is_array($copyscape_list) ? count($copyscape_list) : 0;
                    }
                    
                    $initial_percent = $total_copyscape > 0
                      ? round(($checked_count / $total_copyscape) * 100)
                      : 0;
                    
                    echo '<div id="progress-container-new" style="margin-top:20px;">';
                      echo '<div id="progress-bar-container" '
                         . 'style="background:#eee; overflow:hidden; height:30px; width:100%; '
                         . 'margin-bottom:10px; border-radius:6px;">';
                        echo '<div id="progress-bar-new" class="pinstripe-bar" '
                           . 'style="width:' . intval($initial_percent) . '%; background:#0073AA; color:#fff; height:30px; '
                           . 'line-height:30px; text-align:center; font-weight:bold; '
                           . 'transition: width 0.2s ease;">'
                           . intval($checked_count) . ' / ' . intval($total_copyscape) . ' (' . intval($initial_percent) . '%)'
                           . '</div>';
                      echo '</div>';
                      echo '<p id="progress-message" style="font-weight:bold;">Checking Copyscape… please wait.</p>';
                      echo '<img id="progress-gif" src="'
                         . plugins_url('../search-and-replace/working-on-it.gif', __FILE__)
                         . '" style="width:400px;margin-bottom:20px;" />';
                    echo '</div>';
                    
                    echo '<div id="cs-debug" style="max-height:160px; overflow:auto; font:12px/1.4 monospace; background:#f7f7f7; border:1px solid #ddd; border-radius:6px; padding:8px; margin-top:8px;">';
                    echo 'Copyscape debug running…';
                    echo '</div>';

                    // Hide the HTML-downloads progress UI as soon as Copyscape starts
                    echo "<script>
                      (function(){
                        // Old HTML downloads bar (legacy id)
                        var oldBar = document.getElementById('progress-bar'); // HTML downloads stage uses this id
                        var oldMessage = document.getElementById('html-progress-message'); // HTML downloads stage uses this id
                        var oldGif = document.getElementById('html-progress-gif'); // HTML downloads stage uses this id
                        if (oldBar) {
                          // Hide the bar itself
                          oldBar.style.display = 'none';
                          oldMessage.style.display = 'none';
                          oldGif.style.display = 'none';
                    
                          // Try to hide its container if present (covers common containers you used earlier)
                          var oldContainer = oldBar.closest('#progress-container, #progress-bar-container, .notice, div');
                          if (oldContainer) oldContainer.style.display = 'none';
                        }
                    
                        // Try to hide any HTML-downloads messages/GIFs if they exist
                        var oldMsg = document.getElementById('download-progress-message') || document.getElementById('html-progress-message') || document.getElementById('html-progress-gif');
                        if (oldMsg) oldMsg.style.display = 'none';
                    
                        var oldGif = document.getElementById('download-progress-gif') || document.getElementById('html-progress-gif');
                        if (oldGif) oldGif.style.display = 'none';
                      })();
                    </script>";
                    
                    // Flush so browser renders it immediately:
                    if ( function_exists('ob_flush') ) { ob_flush(); }
                    flush();
                } // end if ($should_run_copyscape)

                /**
                 * Extract visible text from HTML. Strips scripts/styles/noscript and squashes whitespace.
                 */
                function isc_extract_text($html) {
                    if (!is_string($html) || $html === '') return '';
                    // Remove scripts, styles, svg, noscript
                    $html = preg_replace('#<script\b[^>]*>.*?</script>#is', ' ', $html);
                    $html = preg_replace('#<style\b[^>]*>.*?</style>#is', ' ', $html);
                    $html = preg_replace('#<noscript\b[^>]*>.*?</noscript>#is', ' ', $html);
                    $html = preg_replace('#<svg\b[^>]*>.*?</svg>#is', ' ', $html);
                    // Strip tags
                    $text = strip_tags($html);
                    // Decode entities, normalize spaces
                    $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
                    $text = preg_replace('/\s+/u', ' ', $text);
                    return trim($text);
                }
                
                /**
                 * Tokenize to words (letters+numbers), lowercased.
                 */
                function isc_tokenize($text) {
                    $text = mb_strtolower($text, 'UTF-8');
                    preg_match_all('/[\p{L}\p{N}]+/u', $text, $m);
                    return $m[0] ?? [];
                }
                
                /**
                 * Fast Jaccard similarity on word sets. Returns 0..100 (percent).
                 */
                function isc_jaccard_similarity_percent($aText, $bText) {
                    $a = array_unique(isc_tokenize($aText));
                    $b = array_unique(isc_tokenize($bText));
                    if (empty($a) || empty($b)) return 0.0;
                    $inter = count(array_intersect($a, $b));
                    $union = count(array_unique(array_merge($a, $b)));
                    if ($union === 0) return 0.0;
                    return ($inter / $union) * 100.0;
                }
                
                /**
                 * Balanced similarity using Jaccard + similar_text as a backup signal.
                 * Returns a 0..100 percent.
                 */
                function isc_text_similarity_percent($aHtml, $bHtml, &$details = null) {
                    $a = isc_extract_text($aHtml);
                    $b = isc_extract_text($bHtml);
                    $details = ['jac' => 0.0, 'st' => 0.0, 'blend' => 0.0, 'alen' => strlen($a), 'blen' => strlen($b)];
                    if ($a === '' || $b === '') return 0.0;
                
                    $jac = isc_jaccard_similarity_percent($a, $b);
                    $details['jac'] = $jac;
                
                    if ($jac >= 65) {
                        $details['blend'] = $jac;
                        return $jac;
                    }
                
                    $aShort = mb_substr($a, 0, 250000);
                    $bShort = mb_substr($b, 0, 250000);
                    $st = 0.0;
                    similar_text($aShort, $bShort, $st);
                    $details['st'] = $st;
                
                    $blend = 0.6 * $jac + 0.4 * $st;
                    $details['blend'] = $blend;
                    return $blend;
                }
                
                /**
                 * Heuristic: is this a blocked/Cloudflare error (1005/1020/403) page?
                 * Checks body *and* response headers if available.
                 *
                 * @param string $html   Response body
                 * @param array  $headers Optional wp_remote_get() headers (lowercased keys preferred)
                 */
                function isc_is_blocked_copyscape_view($html, $headers = []) {
                    if (!is_string($html)) $html = '';
                    $h = mb_strtolower($html, 'UTF-8');
                
                    // Common Cloudflare/blocked markers
                    $needles = [
                        'error 1005',
                        'error code: 1005',
                        'error 1020',
                        'access denied',
                        'attention required! | cloudflare',
                        'what happened?',
                        'ray id',
                        'cf-error-details',
                        'cf-error-code',
                        'your ip',
                        'country or region',
                        'restricted',
                    ];
                    foreach ($needles as $n) {
                        if (strpos($h, $n) !== false) return true;
                    }
                
                    // Header-based hints (server: cloudflare, 403, etc.)
                    if (!empty($headers) && is_array($headers)) {
                        $lh = [];
                        foreach ($headers as $k => $v) $lh[strtolower($k)] = is_array($v) ? implode(';', $v) : $v;
                        $server = strtolower($lh['server'] ?? '');
                        $status = intval($lh['status'] ?? ($lh['response'] ?? 0));
                        if ($server && strpos($server, 'cloudflare') !== false) return true;
                        if ($status === 403) return true;
                    }
                
                    return false;
                }

                // ─── CHANGE #1: Dedupe the list up front ───
                $copyscape_list = array_unique( $copyscape_list );
                
                $copyscape_ran = false;
                
                // reuse the same decision
                if ( $should_run_copyscape ) {
                    if ( microtime(true) >= $deadline ) {
                        scm_show_timeout_notice();
                        return;
                    }
                
                // — determine host and history file
                $parsed_url      = parse_url( $domain );
                $host            = preg_replace( '/^www\./', '', $parsed_url['host'] ?? 'site' );
                $cs_history_file = ABSPATH . 'copyscape-history-' . $host . '.json';
            
                // — track which URLs have been submitted (never re-submit within THIS request only)
                $submitted_file = ABSPATH . 'copyscape-submitted-urls.txt';
                if ( file_exists($submitted_file) ) { @unlink($submitted_file); } // ensure fresh run
                $submitted_urls = [];
            
                // — debug log (always append)
                $debug_log = ABSPATH . 'copyscape-query-debug.log';
                $run_start = microtime(true);
                // --- INLINE DEBUG HELPERS ---
                /**
                 * Strip obvious chrome, then extract MAIN/article-ish text.
                 * Heuristics: prefer <main>, role="main", <article>, or nodes with "content"/"entry"/"post"/"page" in id/class.
                 * Falls back to body if nothing found.
                 */
                function isc_extract_main_text($html) {
                    if (!is_string($html) || $html === '') return '';
                
                    // 1) quick strip of scripts/styles/noscript/svg to speed DOM parse
                    $html = preg_replace('#<(script|style|noscript|svg)\b[^>]*>.*?</\1>#is', ' ', $html);
                
                    // 2) DOM parse
                    $dom = new DOMDocument();
                    libxml_use_internal_errors(true);
                    $loaded = $dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
                    libxml_clear_errors();
                    if (!$loaded) return isc_extract_text($html); // fallback to simpler stripper you already have
                
                    $xp = new DOMXPath($dom);
                
                    // Remove boilerplate nodes
                    $kill = [
                        '//header','//footer','//nav','//aside','//form','//iframe','//noscript',
                        '//*[contains(@class,"sidebar") or contains(@id,"sidebar")]',
                        '//*[contains(@class,"menu") or contains(@id,"menu")]',
                        '//*[contains(@class,"breadcrumb") or contains(@id,"breadcrumb")]',
                        '//*[contains(@class,"popup") or contains(@id,"popup")]'
                    ];
                    foreach ($kill as $q) {
                        foreach ($xp->query($q) as $n) { $n->parentNode && $n->parentNode->removeChild($n); }
                    }
                
                    // Candidates in preference order
                    $candidates = [
                        '//main',
                        '//*[@role="main"]',
                        '//article',
                        '//*[contains(translate(@id,"CONTENT","content"),"content") or contains(translate(@class,"CONTENT","content"),"content")]',
                        '//*[contains(translate(@id,"ENTRY","entry"),"entry") or contains(translate(@class,"ENTRY","entry"),"entry")]',
                        '//*[contains(translate(@id,"POST","post"),"post") or contains(translate(@class,"POST","post"),"post")]',
                        '//body'
                    ];
                
                    $bestText = '';
                    $bestLen  = 0;
                
                    foreach ($candidates as $q) {
                        $nodes = $xp->query($q);
                        foreach ($nodes as $node) {
                            // Prefer paragraph text inside the candidate
                            $ps = $xp->query('.//p|.//h1|.//h2|.//h3|.//li', $node);
                            $buf = '';
                            foreach ($ps as $p) {
                                $buf .= ' ' . trim($p->textContent);
                            }
                            $buf = preg_replace('/\s+/u', ' ', html_entity_decode($buf, ENT_QUOTES | ENT_HTML5, 'UTF-8'));
                            $len = strlen($buf);
                            if ($len > $bestLen) {
                                $bestLen  = $len;
                                $bestText = trim($buf);
                            }
                        }
                        if ($bestLen > 500) break; // good enough
                    }
                
                    // Normalize: remove very short lines and common CTA boilerplate
                    $bestText = preg_replace('/\b(call|schedule|book|contact|click|learn more|read more)\b.*?(\.|\!|\?)/i', '.', $bestText);
                    $bestText = preg_replace('/\s+/u', ' ', $bestText);
                
                    return $bestText ?: isc_extract_text($html); // last resort
                }

                function isc_normalize_for_tokens($text) {
                    $t = mb_strtolower($text, 'UTF-8');
                    // collapse whitespace; keep letters/numbers only (preserve basic diacritics)
                    $t = preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $t);
                    $t = preg_replace('/\s+/u', ' ', $t);
                    return trim($t);
                }
                
                function isc_tokens($text) {
                    $t = isc_normalize_for_tokens($text);
                    preg_match_all('/[\p{L}\p{N}]+/u', $t, $m);
                    return $m[0] ?? [];
                }
                
                /** Build k-word shingles (default k=5) */
                function isc_shingles(array $tokens, $k = 5) {
                    $out = [];
                    $n = count($tokens);
                    if ($n < $k) return $out;
                    for ($i = 0; $i <= $n - $k; $i++) {
                        $out[] = implode(' ', array_slice($tokens, $i, $k));
                    }
                    return $out;
                }
                
                /** Cosine similarity with raw term-frequency vectors */
                function isc_cosine_similarity(array $aTokens, array $bTokens) {
                    if (!$aTokens || !$bTokens) return 0.0;
                    $aFreq = array_count_values($aTokens);
                    $bFreq = array_count_values($bTokens);
                    $dot = 0.0; $a2 = 0.0; $b2 = 0.0;
                    foreach ($aFreq as $t => $c) {
                        $a2 += $c * $c;
                        if (isset($bFreq[$t])) $dot += $c * $bFreq[$t];
                    }
                    foreach ($bFreq as $c) $b2 += $c * $c;
                    if ($a2 == 0 || $b2 == 0) return 0.0;
                    return $dot / (sqrt($a2) * sqrt($b2)) * 100.0;
                }
                
                /**
                 * Rich similarity using MAIN content:
                 *  - containment of 5-gram shingles (overlap / min(lenA,lenB)) → emphasizes copied chunks
                 *  - cosine on tokens
                 *  - Jaccard on word sets (light weight)
                 * Blend: 0.6*containment + 0.25*cosine + 0.15*jaccard
                 * Returns 0..100 and fills $details with metric breakdown.
                 */
                function isc_rich_similarity_percent($aHtml, $bHtml, &$details = null) {
                    $aMain = isc_extract_main_text($aHtml);
                    $bMain = isc_extract_main_text($bHtml);
                
                    $aTokens = isc_tokens($aMain);
                    $bTokens = isc_tokens($bMain);
                
                    $aSet = array_unique($aTokens);
                    $bSet = array_unique($bTokens);
                
                    // Jaccard on sets
                    $inter = count(array_intersect($aSet, $bSet));
                    $union = count(array_unique(array_merge($aSet, $bSet)));
                    $jacc = $union ? ($inter / $union) * 100.0 : 0.0;
                
                    // 5-gram containment (not union!)
                    $a5 = isc_shingles($aTokens, 4);
                    $b5 = isc_shingles($bTokens, 4);
                    $a5set = array_flip($a5); // for O(1)
                    $b5set = array_flip($b5);
                    $overlap = 0;
                    if (count($a5) && count($b5)) {
                        // count overlap once using the smaller set
                        if (count($a5) <= count($b5)) {
                            foreach ($a5set as $s => $_) if (isset($b5set[$s])) $overlap++;
                            $contain = ($overlap / count($a5)) * 100.0;
                        } else {
                            foreach ($b5set as $s => $_) if (isset($a5set[$s])) $overlap++;
                            $contain = ($overlap / count($b5)) * 100.0;
                        }
                    } else {
                        $contain = 0.0;
                    }
                
                    // Cosine on tokens
                    $cos = isc_cosine_similarity($aTokens, $bTokens);
                
                    // Blend weighted toward containment
                    $blend = 0.6 * $contain + 0.25 * $cos + 0.15 * $jacc;
                
                    $details = [
                        'a_chars' => strlen($aMain),
                        'b_chars' => strlen($bMain),
                        'a_tokens' => count($aTokens),
                        'b_tokens' => count($bTokens),
                        'a_5grams' => count($a5),
                        'b_5grams' => count($b5),
                        'overlap_5grams' => $overlap,
                        'containment' => $contain,
                        'cosine' => $cos,
                        'jaccard' => $jacc,
                        'blend' => $blend,
                    ];
                
                    return $blend;
                }

                if (!function_exists('isc_dbg')) {
                  function isc_dbg($log_file, $run_start, $label, $msg) {
                    $elapsed = microtime(true) - $run_start;
                    $sec = sprintf("%0.2f", min($elapsed, 99));
                    @file_put_contents(
                      $log_file,
                      sprintf("[+%ss/99s] %-12s %s\n", $sec, $label, $msg),
                      FILE_APPEND | LOCK_EX
                    );
                  }
                }
                // live UI messages (tiny, safe)
                if (!function_exists('isc_ui_ping')) {
                  function isc_ui_ping($text) {
                    $safe = esc_js($text);
                    echo "<script>(function(){var m=document.getElementById('progress-message');if(m){m.textContent='$safe';}var d=document.getElementById('cs-debug');if(d){var p=document.createElement('div');p.textContent='$safe';d.appendChild(p);d.scrollTop=d.scrollHeight;}console.log('[CS]', '$safe');})();</script>";
                    if ( function_exists('ob_flush') ) { @ob_flush(); }
                    @flush();
                  }
                }

                file_put_contents(
                    $debug_log,
                    sprintf(
                        "[+0.00/99s] %s Copyscape run =====\n",
                        file_exists( $debug_log ) ? 'Resuming' : 'Starting new'
                    ),
                    FILE_APPEND | LOCK_EX
                );
            
                // — resume state
                $resume_file          = ABSPATH . 'copyscape-aborted-index.txt';
                $resume_page_url      = '';
                $resume_detail_index  = 0;
                $resume_page_found    = false;
                if ( file_exists( $resume_file ) ) {
                    // only use the *last* non-empty line
                    $lines = file( $resume_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
                    $last  = end( $lines );
                    list(
                      $resume_page_url,
                      $resume_detail_index,
                      $resume_viewurl,
                      $resume_saved_highest      // ← NEW
                    ) = explode( '|', $last, 4 );
                    $resume_detail_index  = intval( $resume_detail_index );
                    $resume_saved_highest = intval( $resume_saved_highest );

                    file_put_contents(
                        $debug_log,
                        sprintf(
                            "[%0.4f] Resuming Copyscape at page='%s', detail#=%d, viewurl='%s'\n",
                            microtime(true),
                            $resume_page_url,
                            $resume_detail_index,
                            $resume_viewurl
                        ),
                        FILE_APPEND|LOCK_EX
                    );
                }
            
                // — load past results
                $cs_results = file_exists( $cs_history_file )
                    ? json_decode( file_get_contents( $cs_history_file ), true )
                    : [];
            
                //
                // ── build batch list, keeping resume page first if needed ──
                //
                // 1) if resuming, keep resume page out of the skip list
                if ( $resume_page_url ) {
                    $skipped_urls = array_filter(
                        $submitted_urls,
                        function( $u ) use ( $resume_page_url ) {
                            return $u !== $resume_page_url;
                        }
                    );
                } else {
                    $skipped_urls = $submitted_urls;
                }
            
                // 2) diff against history + skipped
                $to_check_raw = array_values(array_diff($copyscape_list, array_keys($cs_results)));
            
                // 3) if resuming, prepend that page
                if ( $resume_page_url ) {
                    array_unshift($to_check_raw, $resume_page_url);
                    $to_check_raw = array_unique($to_check_raw);
                }
            
                // 4) queue + repeated batches until near deadline
                $total_copyscape = count( $copyscape_list );
                
                // Change this number to how many you want to send concurrently.
                $CS_BATCH_LIMIT = 3;
                
                // Safety window (seconds) to leave time to render + auto-resume form before PHP times out.
                $BATCH_SAFETY = 4;
                
                // Build a queue of URLs still to check this run
                $to_check_queue = array_values( $to_check_raw );
                
                // Copyscape credentials (once)
                if ( ! defined('COPySCAPE_USER') ) define('COPySCAPE_USER', 'inceptionseo');
                if ( ! defined('COPySCAPE_KEY') )  define('COPySCAPE_KEY',  'r1myvbn6637fskr2');
                $cs_user = COPySCAPE_USER;
                $cs_key  = COPySCAPE_KEY;

                // ── NEW: lightweight API response cache to avoid re-querying on resume ──
                $api_cache_dir = ABSPATH . 'copyscape-api-cache';
                if ( ! is_dir($api_cache_dir) ) { @mkdir($api_cache_dir, 0755, true); }
                
                while ( ! empty( $to_check_queue ) ) {
                    // Stop if we’re getting close to the deadline; we’ll auto-resume after refresh.
                    if ( microtime(true) >= ($deadline - $BATCH_SAFETY) ) {
                        break;
                    }
                
                    // Take up to $CS_BATCH_LIMIT from the queue
                    $batch_to_check = array_splice( $to_check_queue, 0, $CS_BATCH_LIMIT );
                
                    if ( empty( $batch_to_check ) ) {
                        break;
                    }
                
                    $multi   = curl_multi_init();
                    $handles = [];
                    $cached_bodies = []; // NEW: any per-URL cached API bodies we can use without re-query
                    $cache_index   = []; // NEW: map URL => cache file path
                
                    /** @var string $resume_page_url URL to resume on (from copyscape-aborted-index.txt) */
                    /** @var int    $resume_detail_index detail# to resume at (1-based) */
                    /** @var bool   $resume_page_found  becomes true once we hit the resume page */
                
                    foreach ( $batch_to_check as $url ) {
                        // Instead of writing to $submitted_file, just track in memory
                        if ( !in_array($url, $submitted_urls, true) ) {
                            $submitted_urls[] = $url;
                        }
                
                        // ── CSC DEBUG: log beginning of this page check ──
                        $elapsed = microtime(true) - $run_start;
                        $sec     = sprintf("%0.2f", min($elapsed, 99));
                        $status  = $elapsed >= 99 ? ' TIMEOUT' : '';
                        file_put_contents(
                            $debug_log,
                            sprintf("[+%s/99s%s] Starting Copyscape check for page: %s\n", $sec, $status, $url),
                            FILE_APPEND|LOCK_EX
                        );
                        isc_ui_ping('Starting page: ' . $url);
                
                        // ── MARK URL AS SUBMITTED IMMEDIATELY ──
                        if (!in_array($url, $submitted_urls, true)) {
                            $submitted_urls[] = $url; // in-memory only this request
                        }
                
                        // ── If resuming, skip everything before the resume page ──
                        if ( $resume_page_url && ! $resume_page_found && $url !== $resume_page_url ) {
                            file_put_contents(
                                $debug_log,
                                sprintf("[+%s/99s] Skipping pre-resume page: %s\n", $sec, $url),
                                FILE_APPEND|LOCK_EX
                            );
                            continue;
                        }
                
                        // ── RESUME ONLY: replay detail‐page checks from where we timed out ──
                        if ( $resume_page_url && $url === $resume_page_url && $resume_detail_index > 0 ) {
                            $resume_page_found = true;
                            @unlink( $resume_file );  // clear the abort marker
                
                            // debug header
                            file_put_contents(
                                $debug_log,
                                sprintf(
                                    "[+%0.2f] Resuming detail check for page: %s from detail#%d\n",
                                    microtime(true) - $run_start,
                                    $url,
                                    $resume_detail_index
                                ),
                                FILE_APPEND|LOCK_EX
                            );
                
                            // load what we previously saved for this URL
                            $saved    = $cs_results[ $url ] ?? [];
                            $viewurl  = $resume_viewurl;
                
                            // debug which URL we’re fetching
                            file_put_contents(
                              $debug_log,
                              sprintf(
                                "[+%0.2f] Fetching saved View Matches listing: %s\n",
                                microtime(true)-$run_start,
                                $viewurl
                              ),
                              FILE_APPEND|LOCK_EX
                            );
                            $highest    = intval( $saved['highest_percentage'] ?? 0 );
                            $percentage = $saved['percentage'] ?? '';
                            $matches    = $saved['matches']   ?? '';
                
                            // seed with whatever we saved at abort time, too:
                            $highest = max( $highest, $resume_saved_highest );
                
                            // fetch the saved View Matches listing
                            $resp = wp_remote_get( $viewurl, [ 'timeout' => 30 ] );
                            $html = is_wp_error($resp) ? '' : wp_remote_retrieve_body($resp);
                
                            // ── grab *both* absolute and relative view2 URLs ──
                            preg_match_all(
                                '/href="(\/compare\/[^"]+)"|href="(https?:\/\/view2\.copyscape\.com\/compare\/[^"]+)"/i',
                                $html,
                                $m
                            );
                            // merge the two capture groups, build full URLs
                            $detail_urls = [];
                            foreach ( array_unique( array_merge( $m[1], $m[2] ) ) as $u ) {
                                if ( ! $u ) continue;
                                $detail_urls[] = strpos( $u, '/' ) === 0
                                    ? 'https://view2.copyscape.com' . $u
                                    : $u;
                            }
                
                            file_put_contents(
                                $debug_log,
                                sprintf(
                                    "[+%0.2f] Found %d detail URLs to resume on\n",
                                    microtime(true) - $run_start,
                                    count($detail_urls)
                                ),
                                FILE_APPEND|LOCK_EX
                            );
                
                            // ── DETAIL PROGRESS BASELINE (resume) ──
                            $__resume_detail_total = max(1, count($detail_urls));
                            $__done_pages_resume   = count($cs_results);
                            $__share_resume        = $total_copyscape > 0 ? (100 / $total_copyscape) : 0;
                            $__resume_frac         = min($resume_detail_index, $__resume_detail_total) / $__resume_detail_total;
                            $__percent_baseline    = round($__done_pages_resume * $__share_resume + $__resume_frac * $__share_resume);
                
                            echo "<script>
                              (function(){
                                var bar = document.getElementById('progress-bar-new');
                                var msg = document.getElementById('progress-message');
                                if (bar) {
                                  bar.style.width  = '{$__percent_baseline}%';
                                  bar.textContent  = '{$__done_pages_resume} / {$total_copyscape} ({$__percent_baseline}%)';
                                }
                                if (msg) {
                                  msg.textContent  = 'Resuming… ({$__percent_baseline}%)';
                                }
                              })();
                            </script>";
                            if ( function_exists('ob_flush') ) { ob_flush(); }
                            flush();
                
                            // loop through each detail URL, *only* those after resume index
                            foreach ( $detail_urls as $idx => $detail_url ) {
                                $detail_no = $idx + 1;
                                if ( $detail_no <= $resume_detail_index ) {
                                    continue;
                                }
                
                                // debug each resumed detail check
                                file_put_contents(
                                    $debug_log,
                                    sprintf(
                                        "[+%0.2f] Checking resume detail#%d: %s\n",
                                        microtime(true) - $run_start,
                                        $detail_no,
                                        $detail_url
                                    ),
                                    FILE_APPEND|LOCK_EX
                                );
                
                                $resp2 = wp_remote_get( $detail_url, [ 'timeout' => 30 ] );
                                if ( is_wp_error($resp2) ) {
                                    file_put_contents(
                                        $debug_log,
                                        sprintf(
                                            "[+%0.2f] ERROR fetching detail#%d: %s\n",
                                            microtime(true) - $run_start,
                                            $detail_no,
                                            $resp2->get_error_message()
                                        ),
                                        FILE_APPEND|LOCK_EX
                                    );
                                    continue;
                                }
                
                                $html2 = wp_remote_retrieve_body($resp2);
                                if ( preg_match( '/matching\s+(\d{1,3})%/i', $html2, $mh ) ) {
                                    $val = intval( $mh[1] );
                                    if ( $val > $highest ) {
                                        $highest = $val;
                                        file_put_contents(
                                            $debug_log,
                                            sprintf(
                                                "[+%0.2f] New highest after detail#%d: %d%%\n",
                                                microtime(true) - $run_start,
                                                $detail_no,
                                                $highest
                                            ),
                                            FILE_APPEND|LOCK_EX
                                        );
                                    }
                                } else {
                                    file_put_contents(
                                        $debug_log,
                                        sprintf(
                                            "[+%0.2f] No \"matching NN%%\" found on detail#%d\n",
                                            microtime(true) - $run_start,
                                            $detail_no
                                        ),
                                        FILE_APPEND|LOCK_EX
                                    );
                                }
                                // ── DETAIL PROGRESS BAR UPDATE (resume, per detail) ──
                                $__done_pages_r   = count($cs_results);                  // completed pages (not the current one)
                                $__detail_done_r  = $detail_no;                          // includes the detail we just processed
                                $__detail_total_r = max(1, count($detail_urls));
                                $__share_r        = $total_copyscape > 0 ? (100 / $total_copyscape) : 0;
                                $__percent_live_r = round($__done_pages_r * $__share_r + ($__detail_done_r / $__detail_total_r) * $__share_r);
                
                                echo "<script>
                                  (function(){
                                    var bar = document.getElementById('progress-bar-new');
                                    var msg = document.getElementById('progress-message');
                                    if (bar) {
                                      bar.style.width  = '{$__percent_live_r}%';
                                      bar.textContent  = '{$__done_pages_r} / {$total_copyscape} ({$__percent_live_r}%)';
                                    }
                                    if (msg) {
                                      msg.textContent  = 'Checking Copyscape… ({$__percent_live_r}%)';
                                    }
                                  })();
                                </script>";
                                if ( function_exists('ob_flush') ) { ob_flush(); }
                                flush();
                            }
                
                            // finalize status & persist for this page
                            $status = $highest > 0 ? 'Copies found' : 'No copies';
                            $cs_results[ $url ] = [
                                'percentage'         => $percentage,
                                'highest_percentage' => (string)$highest,
                                'status'             => $status,
                                'matches'            => is_array($matches) ? implode(' | ', $matches) : ($matches !== '' ? $matches : ($viewurl ?: 'None')),
                                'viewurl'            => $viewurl,
                            ];
                            file_put_contents( $cs_history_file, json_encode( $cs_results ), LOCK_EX );
                            file_put_contents(
                                $debug_log,
                                sprintf(
                                    "[+%0.2f] Persisted resumed detail results for: %s\n\n",
                                    microtime(true) - $run_start,
                                    $url
                                ),
                                FILE_APPEND|LOCK_EX
                            );
                
                            // ── IN-PAGE PROGRESS BAR UPDATE (resume page complete) ──
                            $checked_count = count( $cs_results );
                            $percent       = $total_copyscape > 0
                                ? round( $checked_count / $total_copyscape * 100 )
                                : 0;
                
                            echo "<script>
                              (function(){
                                var bar = document.getElementById('progress-bar-new');
                                var msg = document.getElementById('progress-message');
                                if ( bar ) {
                                  bar.style.width  = '{$percent}%';
                                  bar.textContent  = '{$checked_count} / {$total_copyscape} ({$percent}%)';
                                }
                                if ( msg ) {
                                  msg.textContent  = 'Checked {$checked_count} of {$total_copyscape} pages…';
                                }
                              })();
                            </script>";
                            if ( function_exists('ob_flush') ) { ob_flush(); }
                            flush();
                
                            // done with this resume page—move on to next URL in batch
                            continue;
                        }
                
                        //
                        // ── normal page resume skip until we hit resume page ──
                        //
                        // ── CSC PAGE RESUME: skip pages before resume target ──
                        if ( $resume_page_url && ! $resume_page_found ) {
                            if ( $url !== $resume_page_url ) {
                                continue;
                            }
                            $resume_page_found = true;
                        }
                        // clear the abort file now that we're on the resume page
                        if ( $resume_page_found ) {
                            @unlink( $resume_file );
                        }
                
                        // ── CSC ABORT: check for timeout and record progress ──
                        if ( microtime(true) >= $deadline ) {
                            // ── CSC DEBUG: timeout hit, log with clamp at 99s ──
                            $elapsed      = microtime(true) - $run_start;
                            $sec          = sprintf("%0.2f", min($elapsed, 99));
                            $current_detail = isset($detail_index) ? $detail_index : 0;
                            file_put_contents(
                                $debug_log,
                                sprintf(
                                    "[+%s/99s TIMEOUT] Timeout at page %s, detail#%d – aborting and will resume\n",
                                    $sec,
                                    $url,
                                    $current_detail
                                ),
                                FILE_APPEND|LOCK_EX
                            );
                
                            // Determine detail index (default 0 if not in detail loop yet)
                            $current_detail_index = isset( $detail_index ) ? $detail_index : 0;
                            // save: [page_url|detail_number|view_matches_url]
                            file_put_contents(
                              ABSPATH . 'copyscape-aborted-index.txt',
                              implode('|', [
                                isset($page_url) && $page_url ? $page_url : $url,
                                $current_detail_index,        // ← use the safe index
                                isset($viewurl) ? $viewurl : '',
                                isset($highest) ? (int)$highest : 0
                              ]) . "\n",
                              LOCK_EX
                            );

                            // Auto-continue form to resume
                            ?>
                            <form id="auto-copyscape-form" method="post" action="">
                                <?php $new_nonce = wp_create_nonce('sitemap_nonce'); ?>
                                <input type="hidden" name="action"               value="sitemap" />
                                <input type="hidden" name="_wpnonce"              value="<?php echo esc_attr( $new_nonce ); ?>" />
                                <input type="hidden" name="confirm_page_choices" value="1" />
                                <input type="hidden" name="copyscape_phase"      value="1" />
                                <input type="hidden" name="domainToCheck"        value="<?php echo esc_attr( $_POST['domainToCheck'] ); ?>" />
                                <?php foreach ( $download_html_urls   as $u ) : ?>
                                    <input type="hidden" name="download_html_urls[]"   value="<?php echo esc_attr( $u ); ?>" />
                                <?php endforeach; ?>
                                <?php foreach ( $copyscape_check_urls as $u ) : ?>
                                    <input type="hidden" name="copyscape_check_urls[]" value="<?php echo esc_attr( $u ); ?>" />
                                <?php endforeach; ?>
                                <?php foreach ( $skip_urls            as $u ) : ?>
                                    <input type="hidden" name="skip_urls[]"            value="<?php echo esc_attr( $u ); ?>" />
                                <?php endforeach; ?>
                            </form>
                            <script>
                                setTimeout(function(){
                                    document.getElementById('auto-copyscape-form').submit();
                                }, 3000);
                            </script>
                            <?php
                            exit;
                        }
                
                        $ignore_host = preg_replace('/^www\\./','', parse_url($url, PHP_URL_HOST));

                        // ── NEW: if we have a cached API XML for this URL, use it and skip re-query
                        $cache_file = $api_cache_dir . '/' . sha1($url) . '.xml';
                        $cache_index[$url] = $cache_file;
                        if ( file_exists($cache_file) && filesize($cache_file) > 0 ) {
                            $cached_bodies[$url] = file_get_contents($cache_file);
                            file_put_contents(
                                $debug_log,
                                sprintf("[+%s/99s] Using cached API response for: %s\n", $sec, $url),
                                FILE_APPEND|LOCK_EX
                            );
                            isc_ui_ping('Cache hit for API: ' . $url);

                            // do NOT build query or add a curl handle
                            continue;
                        }
                
                        $query = null;
                        $debug  = "=== Copyscape Query for URL: $url ===\n";
                
                        // 1) fetch & extract main content
                        $html      = shell_exec( "wget -qO- " . escapeshellarg($url) );
                        $query     = null;
                        if ( $html !== null && strlen($html) > 200 ) {
                            $extracted = extractMainContent( $html );
                            if ( mb_strlen($extracted) >= 100 ) {
                                $query = $extracted;
                            }
                        }
                        if ( ! $query ) {
                            $query = $url;
                        }
                
                        // log for debug
                        file_put_contents(
                            $debug_log,
                            sprintf(
                                "[%0.4f] Query payload (%d bytes) for URL %s:\n%s\n",
                                microtime(true),
                                strlen( $query ),
                                $url,
                                $query
                            ),
                            FILE_APPEND|LOCK_EX
                        );
                
                        $is_url = (bool) filter_var($query, FILTER_VALIDATE_URL);
                
                        $ch = curl_init();
                        curl_setopt_array($ch, [
                            CURLOPT_RETURNTRANSFER => true,
                            CURLOPT_TIMEOUT        => 30,
                        ]);
                
                        if ( $is_url ) {
                            $params = [
                                'u' => $cs_user,
                                'k' => $cs_key,
                                'o' => 'csearch',
                                'q' => $query,
                                'i' => $ignore_host,
                                'c' => 3,
                                'f' => 'xml',
                                'l' => '0.30',
                            ];
                            curl_setopt($ch, CURLOPT_URL, 'https://www.copyscape.com/api/?' . http_build_query($params));
                        } else {
                            $post = [
                                'u' => $cs_user,
                                'k' => $cs_key,
                                'o' => 'csearch',
                                'e' => 'UTF-8',
                                't' => $query,
                                'i' => $ignore_host,
                                'c' => 3,
                                'f' => 'xml',
                                'l' => '0.30',
                            ];
                            curl_setopt($ch, CURLOPT_URL,        'https://www.copyscape.com/api/');
                            curl_setopt($ch, CURLOPT_POST,       true);
                            curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post));
                        }
                
                        curl_multi_add_handle($multi, $ch);
                        $handles[$url] = $ch;
                    } // end foreach $batch_to_check
                
                    // execute current batch in parallel
                    $running = null;
                    if ( ! empty($handles) ) {
                        do {
                            curl_multi_exec($multi, $running);
                            curl_multi_select($multi);
                        } while ($running > 0);
                    }

                    // ── NEW: collect raw API bodies for all handles first, persist to cache, then parse
                    foreach ($handles as $page_url => $ch) {
                        $body = curl_multi_getcontent($ch);
                        if ( isset($cache_index[$page_url]) && $body !== '' && $body !== false ) {
                            @file_put_contents($cache_index[$page_url], $body, LOCK_EX);
                        }
                        $cached_bodies[$page_url] = $body;
                        curl_multi_remove_handle($multi, $ch);
                        curl_close($ch);
                    }
                    curl_multi_close($multi);
                
                    // collect & parse current batch (from cached_bodies, which now includes newly fetched ones)
                    $batch_results = [];
                    foreach ($cached_bodies as $page_url => $body) {
                        libxml_use_internal_errors(true);
                        isc_dbg($debug_log, $run_start, 'API', 'Received body for ' . $page_url . ' (' . strlen((string)$body) . ' bytes)');
                        $xml = simplexml_load_string($body);
                
                        if (!$xml) {
                            $batch_results[$page_url] = [
                                'percentage' => '',
                                'status'     => 'Invalid API response',
                                'matches'    => '',
                                'viewurl'    => '',
                            ];
                        } elseif (isset($xml->error)) {
                            $batch_results[$page_url] = [
                                'percentage' => '',
                                'status'     => 'API Error: ' . (string)$xml->error,
                                'matches'    => '',
                                'viewurl'    => '',
                            ];
                        } else {
                            $percentage = (string)($xml->allpercentmatched ?? '');
                            $viewurl    = (string)($xml->allviewurl      ?? '');
                            $matches    = [];
                            // ── mark as "Copies found" if any percent > 0, otherwise "No copies"
                            $percentNum = floatval( $percentage );
                            if ( $percentNum > 0 ) {
                                // also collect any matched URLs
                                if (isset($xml->result) && count($xml->result)) {
                                    foreach ($xml->result as $r) {
                                        $matches[] = (string)$r->url;
                                    }
                                }
                                $status = 'Copies found';
                            } else {
                                $status = 'No copies';
                                // If API reported 0 but we have matches and a viewurl, we’ll perform a best-effort direct compare on the top result
                                if ($status === 'No copies' && $viewurl) {
                                    isc_dbg($debug_log, $run_start, 'FALLBACK', 'API 0% but has viewurl; will fetch detail listing for direct compare');
                                }
                            }
                
                            file_put_contents( $debug_log, "=== Copyscape debug for $page_url at " . date('c') . " ===\n", FILE_APPEND );
                
                            // ─── Instrumented debug version ───
                            $highest   = floatval( $percentage );
                            file_put_contents( $debug_log, "API percent: $percentage, viewurl: $viewurl\n", FILE_APPEND );
                            
                            // >>> ADDED: track best similarity found via fallback direct compares
                            $fallback_best = 0.0;
                
                            if ( intval( $percentage ) <= 50 && $viewurl ) {
                                file_put_contents( $debug_log, "  Fetching viewurl...\n", FILE_APPEND );
                                $resp2 = wp_remote_get( $viewurl, [ 'timeout' => 30 ] );
                                if ( is_wp_error( $resp2 ) ) {
                                    file_put_contents( $debug_log, "    ERROR fetching viewurl: " . $resp2->get_error_message() . "\n", FILE_APPEND );
                                } else {
                                    $html2 = wp_remote_retrieve_body( $resp2 );
                                    file_put_contents( $debug_log, "    Retrieved view matches listing, length: " . strlen($html2) . "\n", FILE_APPEND );
                
                                    // 1) grab every detail‐page link from the View Matches listing
                                    if ( preg_match_all(
                                        '/<a[^>]+href="(https?:\/\/view2\.copyscape\.com\/compare\/[^"]+)"/i',
                                        $html2,
                                        $links
                                    ) ) {
                                        $detail_urls = array_unique( $links[1] );
                                        // ── CSC DEBUG: detail-URL count with elapsed ──
                                        $elapsed     = microtime(true) - $run_start;
                                        $sec         = sprintf("%0.2f", min($elapsed, 99));
                                        // use a separate var for debug-only status so we don't clobber the real $status
                                        $debugStatus = $elapsed >= 99 ? ' TIMEOUT' : '';
                                        file_put_contents(
                                            $debug_log,
                                            sprintf(
                                                "[+%s/99s%s] Found %d detail URLs for page %s\n",
                                                $sec,
                                                $debugStatus,
                                                count($detail_urls),
                                                $page_url
                                            ),
                                            FILE_APPEND|LOCK_EX
                                        );
                
                                        // ── CSC DETAIL RESUME: initialize detail index ──
                                        $detail_index = 0;
                                        file_put_contents( $debug_log, "    Found " . count($detail_urls) . " detail URLs\n", FILE_APPEND );
                
                                        $original_host = preg_replace('/^www\./','',
                                            parse_url( $domain, PHP_URL_HOST )
                                        );
                
                                        foreach ( $detail_urls as $idx => $detail_url ) {
                                            // Increment detail index for resume tracking
                                            $detail_index++;
                
                                            // ── CSC DETAIL RESUME: skip already-processed links on this page ──
                                            if ( $resume_page_found && $page_url === $resume_page_url && $detail_index <= $resume_detail_index ) {
                                                continue; // skip until resume index
                                            }
                
                                            // ── CSC DETAIL ABORT: if we’ve hit 99 s, record & refresh immediately ──
                                            if ( microtime(true) >= $deadline ) {
                                                // log to debug
                                                $elapsed        = microtime(true) - $run_start;
                                                $sec            = sprintf("%0.2f", min($elapsed, 99));
                                                file_put_contents(
                                                    $debug_log,
                                                    sprintf(
                                                        "[+%s/99s TIMEOUT] Aborting at detail#%d for page %s – saving state and refreshing\n",
                                                        $sec,
                                                        $detail_index,
                                                        $page_url
                                                    ),
                                                    FILE_APPEND|LOCK_EX
                                                );
                
                                                // save: [page_url|detail_number|view_matches_url]
                                                file_put_contents(
                                                  ABSPATH . 'copyscape-aborted-index.txt',
                                                  implode(
                                                 '|',
                                                 [
                                                   $page_url,
                                                   $detail_index,
                                                   $viewurl,
                                                   (int)$highest
                                                 ]
                                                  ) . "\n",
                                                  LOCK_EX
                                                );
                
                                                // auto-submit form to resume
                                                ?>
                                                <form id="auto-copyscape-form" method="post" action="">
                                                    <?php $nonce = wp_create_nonce('sitemap_nonce'); ?>
                                                    <input type="hidden" name="action"               value="sitemap" />
                                                    <input type="hidden" name="_wpnonce"              value="<?php echo esc_attr( $nonce ); ?>" />
                                                    <input type="hidden" name="confirm_page_choices" value="1" />
                                                    <input type="hidden" name="copyscape_phase"      value="1" />
                                                    <input type="hidden" name="domainToCheck"        value="<?php echo esc_attr( $_POST['domainToCheck'] ); ?>" />
                                                    <?php foreach ( $download_html_urls   as $u ) : ?>
                                                      <input type="hidden" name="download_html_urls[]"   value="<?php echo esc_attr( $u ); ?>" />
                                                    <?php endforeach; ?>
                                                    <?php foreach ( $copyscape_check_urls as $u ) : ?>
                                                      <input type="hidden" name="copyscape_check_urls[]" value="<?php echo esc_attr( $u ); ?>" />
                                                    <?php endforeach; ?>
                                                    <?php foreach ( $skip_urls            as $u ) : ?>
                                                      <input type="hidden" name="skip_urls[]"            value="<?php echo esc_attr( $u ); ?>" />
                                                    <?php endforeach; ?>
                                                </form>
                                                <script>
                                                  document.getElementById('auto-copyscape-form').submit();
                                                </script>
                                                <?php
                                                exit;
                                            }
                
                                            // ── CSC DEBUG: individual detail check with elapsed ──
                                            $elapsed = microtime(true) - $run_start;
                                            $sec     = sprintf("%0.2f", min($elapsed, 99));
                                            $status  = $elapsed >= 99 ? ' TIMEOUT' : '';
                                            file_put_contents(
                                                $debug_log,
                                                sprintf("[+%s/99s%s] Checking detail#%d: %s\n", $sec, $status, $detail_index, $detail_url),
                                                FILE_APPEND|LOCK_EX
                                            );
                
                                            // find the matched page URL in the same order as Copyscape results
                                            $matched_url  = $matches[ $idx ] ?? '';
                                            $matched_host = $matched_url
                                                ? preg_replace('/^www\./','', parse_url($matched_url, PHP_URL_HOST) )
                                                : '';
                
                                            // SKIP if it’s the same domain
                                            if ( $matched_host === $original_host ) {
                                                file_put_contents( $debug_log,
                                                    "      Skipping same-domain match $matched_url\n",
                                                    FILE_APPEND
                                                );
                                                continue;
                                            }
                
                                            // otherwise, fetch & parse as before
                                            file_put_contents( $debug_log,
                                                "      Fetching detail page: $detail_url for match $matched_url\n",
                                                FILE_APPEND
                                            );
                                            $resp3 = wp_remote_get( $detail_url, [ 'timeout' => 30 ] );
                                            if ( is_wp_error( $resp3 ) ) {
                                                file_put_contents( $debug_log,
                                                    "        ERROR: " . $resp3->get_error_message() . "\n",
                                                    FILE_APPEND
                                                );
                                                continue;
                                            }
                                            $html3 = wp_remote_retrieve_body( $resp3 );
                                            
                                            $hdr3 = is_wp_error($resp3) ? [] : wp_remote_retrieve_headers($resp3);
                                            $hdr3 = is_array($hdr3) ? $hdr3 : (array)$hdr3;

                                            // --- Fallback when Copyscape view page is blocked (Error 1005 / Access denied) ---
                                            if ( isc_is_blocked_copyscape_view($html3) ) {
                                                isc_dbg($debug_log, $run_start, 'BLOCKED', 'Copyscape view blocked (Error 1005/Access denied) for ' . $detail_url);
                                                isc_ui_ping('Fallback: blocked detail page → comparing directly: ' . $matched_url);
                                            
                                                // Fetch the ORIGINAL page and the MATCHED page directly
                                                $req_args = [
                                                  'timeout'      => 30,
                                                  'redirection'  => 5,
                                                  'headers'      => [
                                                    'user-agent'      => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124 Safari/537.36',
                                                    'accept-language' => 'en-US,en;q=0.9',
                                                  ],
                                                ];
                                                
                                                $orig_resp = wp_remote_get( $page_url,   $req_args );
                                                $copy_resp = wp_remote_get( $matched_url, $req_args );
                                            
                                                if ( is_wp_error($orig_resp) || is_wp_error($copy_resp) ) {
                                                    isc_dbg($debug_log, $run_start, 'FALLBACK', 'Direct fetch failed: ' . ($orig_resp instanceof WP_Error ? $orig_resp->get_error_message() : 'ok') . ' | ' . ($copy_resp instanceof WP_Error ? $copy_resp->get_error_message() : 'ok'));
                                                    isc_ui_ping('Fallback: direct fetch failed (see log)');
                                                    if (empty($batch_results[$page_url]['status'])) {
                                                        $batch_results[$page_url]['status'] = 'Manual review (blocked & fetch failed)';
                                                    }
                                                    $batch_results[$page_url]['_fallback_reason'] = 'Copyscape view blocked, direct fetch failed';
                                                    continue;
                                                }
                                            
                                                $orig_html = wp_remote_retrieve_body($orig_resp);
                                                $copy_html = wp_remote_retrieve_body($copy_resp);
                                            
                                                // >>> ADDED: dump extracted MAIN content to files + log previews
                                                $content_dump_dir = ABSPATH . 'copyscape-content-dumps';
                                                if ( ! is_dir($content_dump_dir) ) { @mkdir($content_dump_dir, 0755, true); }
                                                $pair_hash = substr(sha1($page_url.'|'.$matched_url), 0, 10);
                                                $stamp     = date('Ymd_His');
    
                                                $orig_main  = isc_extract_main_text($orig_html);
                                                $copy_main  = isc_extract_main_text($copy_html);
                                                $orig_file  = $content_dump_dir . "/A-{$pair_hash}-{$stamp}.txt";
                                                $copy_file  = $content_dump_dir . "/B-{$pair_hash}-{$stamp}.txt";
                                                @file_put_contents($orig_file, $orig_main);
                                                @file_put_contents($copy_file, $copy_main);
    
                                                $orig_preview = mb_substr($orig_main, 0, 2000);
                                                $copy_preview = mb_substr($copy_main, 0, 2000);
                                                isc_dbg($debug_log, $run_start, 'CONTENT-A', "URL={$page_url} | main_len=" . strlen($orig_main) . " | dump={$orig_file}\n" . $orig_preview);
                                                isc_dbg($debug_log, $run_start, 'CONTENT-B', "URL={$matched_url} | main_len=" . strlen($copy_main) . " | dump={$copy_file}\n" . $copy_preview);

                                                $details = [];
                                                $sim = isc_rich_similarity_percent($orig_html, $copy_html, $details);
                                                isc_dbg($debug_log, $run_start, 'FALLBACK', sprintf(
                                                  "MAIN compare → contain=%.1f%% cosine=%.1f%% jacc=%.1f%% blend=%.1f%% | 5g overlap=%d of (A:%d,B:%d) tokens(A:%d,B:%d) chars(A:%d,B:%d) | %s",
                                                  $details['containment'], $details['cosine'], $details['jaccard'], $details['blend'],
                                                  $details['overlap_5grams'], $details['a_5grams'], $details['b_5grams'],
                                                  $details['a_tokens'], $details['b_tokens'],
                                                  $details['a_chars'], $details['b_chars'],
                                                  $matched_url
                                                ));
                                                isc_ui_ping(sprintf(
                                                  'Fallback MAIN: %.1f%% (contain=%.1f / cos=%.1f / jac=%.1f)',
                                                  $sim, $details['containment'], $details['cosine'], $details['jaccard']
                                                ));
                                            
                                                $flag_threshold   = 55.0; // likely copy
                                                $strong_threshold = 75.0; // very strong
                                            
                                                if ( !isset($batch_results[$page_url]) ) {
                                                    $batch_results[$page_url] = [
                                                        'percentage' => '',
                                                        'status'     => '',
                                                        'matches'    => [],
                                                        'viewurl'    => '',
                                                    ];
                                                }
                                                if (empty($batch_results[$page_url]['matches']) || !in_array($matched_url, $batch_results[$page_url]['matches'], true)) {
                                                    $batch_results[$page_url]['matches'][] = $matched_url;
                                                }
                                            
                                                if ( $sim >= $flag_threshold ) {
                                                    $existing = floatval($batch_results[$page_url]['percentage'] ?: 0);
                                                    $best     = max($existing, round($sim, 1));
                                                    $batch_results[$page_url]['percentage'] = (string)$best;
                                                    $batch_results[$page_url]['status']     = ($sim >= $strong_threshold)
                                                        ? 'Copies found (fallback – direct compare, Copyscape view blocked)'
                                                        : 'Possible copies (fallback – direct compare, Copyscape view blocked)';
                                                    $batch_results[$page_url]['_fallback_reason'] = 'Copyscape view blocked (Error 1005/Access denied)';
                                                } else {
                                                    if (empty($batch_results[$page_url]['status'])) {
                                                        $batch_results[$page_url]['status'] = 'Manual review (Copyscape view blocked)';
                                                    }
                                                    $batch_results[$page_url]['_fallback_reason'] = 'Copyscape view blocked, low direct similarity';
                                                }
                                            
                                                // don’t parse the blocked page further
                                                continue;
                                            }

                                            file_put_contents( $debug_log,
                                                "        Retrieved detail page, length: " . strlen($html3) . "\n",
                                                FILE_APPEND
                                            );
                
                                            // pull the header “matching NN%”
                                            $parsed_val = null;
                                            if ( preg_match( '/matching\s+(\d{1,3})%/i', $html3, $mh ) ) {
                                                $parsed_val = intval($mh[1]);
                                                isc_dbg($debug_log, $run_start, 'DETAIL', "Page '$page_url' detail#$detail_index matched: {$parsed_val}%");
                                                isc_ui_ping("Detail#$detail_index → {$parsed_val}% for: " . $matched_url);
                                            } else {
                                                isc_dbg($debug_log, $run_start, 'DETAIL', "No percentage header on detail#$detail_index for: " . $matched_url);
                                                isc_ui_ping("Detail#$detail_index → no % found (see log)");
                                            }
                                            
                                            // NEW: treat as blocked if detector says so OR if the detail shows 0%.
                                            // Many Cloudflare blocks make Copyscape render 'matching 0%'.
                                            $blocked = isc_is_blocked_copyscape_view($html3, $hdr3);
                                            $looks_zero = ($parsed_val !== null && $parsed_val === 0);
                                            
                                            // if blocked OR 0%, run the direct-compare fallback
                                            if ( $blocked || $looks_zero ) {
                                                if ($blocked) {
                                                    isc_dbg($debug_log, $run_start, 'BLOCKED', "Copyscape detail blocked for $detail_url");
                                                    isc_ui_ping('Fallback: compare directly (blocked view)');
                                                } else {
                                                    isc_dbg($debug_log, $run_start, 'FALLBACK', "Detail shows 0% — forcing direct compare for $matched_url");
                                                    isc_ui_ping('Fallback: compare directly (0% reported)');
                                                }
                                            
                                                $req_args = [
                                                  'timeout'      => 30,
                                                  'redirection'  => 5,
                                                  'headers'      => [
                                                    'user-agent'      => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124 Safari/537.36',
                                                    'accept-language' => 'en-US,en;q=0.9',
                                                  ],
                                                ];
                                                
                                                $orig_resp = wp_remote_get( $page_url,   $req_args );
                                                $copy_resp = wp_remote_get( $matched_url, $req_args );
                                            
                                                if ( is_wp_error($orig_resp) || is_wp_error($copy_resp) ) {
                                                    isc_dbg(
                                                      $debug_log, $run_start, 'FALLBACK',
                                                      'Direct fetch failed: ' .
                                                      ($orig_resp instanceof WP_Error ? $orig_resp->get_error_message() : 'ok') .
                                                      ' | ' .
                                                      ($copy_resp instanceof WP_Error ? $copy_resp->get_error_message() : 'ok')
                                                    );
                                                    isc_ui_ping('Fallback: direct fetch failed (see log)');
                                                    if (empty($batch_results[$page_url]['status'])) {
                                                        $batch_results[$page_url]['status'] = 'Manual review (blocked/0% & fetch failed)';
                                                    }
                                                    $batch_results[$page_url]['_fallback_reason'] = $blocked
                                                        ? 'Copyscape view blocked (Error 1005/403)'
                                                        : 'Detail showed 0%; direct fetch failed';
                                                    // Skip further parsing of this detail
                                                    continue;
                                                }
                                            
                                                $orig_html = wp_remote_retrieve_body($orig_resp);
                                                $copy_html = wp_remote_retrieve_body($copy_resp);
                                                
                                                // >>> ADDED: dump extracted MAIN content to files + log previews
                                                $content_dump_dir = ABSPATH . 'copyscape-content-dumps';
                                                if ( ! is_dir($content_dump_dir) ) { @mkdir($content_dump_dir, 0755, true); }
                                                $pair_hash = substr(sha1($page_url.'|'.$matched_url), 0, 10);
                                                $stamp     = date('Ymd_His');

                                                $orig_main  = isc_extract_main_text($orig_html);
                                                $copy_main  = isc_extract_main_text($copy_html);
                                                $orig_file  = $content_dump_dir . "/A-{$pair_hash}-{$stamp}.txt";
                                                $copy_file  = $content_dump_dir . "/B-{$pair_hash}-{$stamp}.txt";
                                                @file_put_contents($orig_file, $orig_main);
                                                @file_put_contents($copy_file, $copy_main);

                                                $orig_preview = mb_substr($orig_main, 0, 2000);
                                                $copy_preview = mb_substr($copy_main, 0, 2000);
                                                isc_dbg($debug_log, $run_start, 'CONTENT-A', "URL={$page_url} | main_len=" . strlen($orig_main) . " | dump={$orig_file}\n" . $orig_preview);
                                                isc_dbg($debug_log, $run_start, 'CONTENT-B', "URL={$matched_url} | main_len=" . strlen($copy_main) . " | dump={$copy_file}\n" . $copy_preview);

                                                $details = [];
                                                $sim = isc_rich_similarity_percent($orig_html, $copy_html, $details);
                                                
                                                $flag_threshold   = 55.0;
                                                $strong_threshold = 75.0;
                                                
                                                isc_dbg($debug_log, $run_start, 'FALLBACK', sprintf(
                                                  "MAIN compare → contain=%.1f%% cosine=%.1f%% jacc=%.1f%% blend=%.1f%% | 5g overlap=%d of (A:%d,B:%d) tokens(A:%d,B:%d) chars(A:%d,B:%d) | %s",
                                                  $details['containment'], $details['cosine'], $details['jaccard'], $details['blend'],
                                                  $details['overlap_5grams'], $details['a_5grams'], $details['b_5grams'],
                                                  $details['a_tokens'], $details['b_tokens'],
                                                  $details['a_chars'], $details['b_chars'],
                                                  $matched_url
                                                ));
                                                isc_ui_ping(sprintf(
                                                  'Fallback MAIN: %.1f%% (contain=%.1f / cos=%.1f / jac=%.1f)',
                                                  $sim, $details['containment'], $details['cosine'], $details['jaccard']
                                                ));
                                            
                                                if ( !isset($batch_results[$page_url]) ) {
                                                    $batch_results[$page_url] = [
                                                        'percentage' => '',
                                                        'status'     => '',
                                                        'matches'    => [],
                                                        'viewurl'    => '',
                                                    ];
                                                }
                                                if (empty($batch_results[$page_url]['matches']) || !in_array($matched_url, $batch_results[$page_url]['matches'], true)) {
                                                    $batch_results[$page_url]['matches'][] = $matched_url;
                                                }
                                                
                                                // >>> record fallback similarity and fold into "highest"
                                                $fallback_best = max($fallback_best, round($sim, 1));
                                                $highest       = max($highest, $fallback_best);

                                                if ( $sim >= $flag_threshold ) {
                                                    $existing = floatval($batch_results[$page_url]['percentage'] ?: 0);
                                                    $best     = max($existing, round($sim, 1));
                                                    $batch_results[$page_url]['percentage'] = (string)$best;
                                                    $batch_results[$page_url]['status']     = ($sim >= $strong_threshold)
                                                        ? 'Copies found (fallback – direct compare)'
                                                        : 'Possible copies (fallback – direct compare)';
                                                    $batch_results[$page_url]['_fallback_reason'] = $blocked
                                                        ? 'Copyscape view blocked (Error 1005/403)'
                                                        : 'Copyscape detail reported 0%';
                                                } else {
                                                    if (empty($batch_results[$page_url]['status'])) {
                                                        $batch_results[$page_url]['status'] = 'Manual review (fallback low similarity)';
                                                    }
                                                    $batch_results[$page_url]['_fallback_reason'] = $blocked
                                                        ? 'Copyscape view blocked, low similarity'
                                                        : 'Detail 0%, low similarity';
                                                }
                                            
                                                // No need to parse this detail further
                                                continue;
                                            }

                                            // ── DETAIL PROGRESS BAR UPDATE (per detail URL) ──
                                            $__done_pages   = count($cs_results);                       // fully completed pages (not including current)
                                            $__detail_done  = $detail_index;                            // how many detail URLs processed on this page so far
                                            $__detail_total = max(1, count($detail_urls));              // guard against division by zero
                                            $__share        = $total_copyscape > 0 ? (100 / $total_copyscape) : 0; // each page’s % share
                                            $__percent_live = round($__done_pages * $__share + ($__detail_done / $__detail_total) * $__share);
                
                                            echo "<script>
                                              (function(){
                                                var bar = document.getElementById('progress-bar-new');
                                                var msg = document.getElementById('progress-message');
                                                if (bar) {
                                                  bar.style.width  = '{$__percent_live}%';
                                                  bar.textContent  = '{$__done_pages} / {$total_copyscape} ({$__percent_live}%)';
                                                }
                                                if (msg) {
                                                  msg.textContent  = 'Checking Copyscape… ({$__percent_live}%)';
                                                }
                                              })();
                                            </script>";
                                            if ( function_exists('ob_flush') ) { ob_flush(); }
                                            flush();
                                        }
                                    } else {
                                        file_put_contents( $debug_log, "    No detail links found on view matches page\n", FILE_APPEND );
                                    }
                                }
                            }
                
                            // mark as Copies found only if $highest is set AND >0
                            if ( isset( $highest ) && $highest > 0 ) {
                                $status = 'Copies found';
                            } else {
                                $status = 'No copies';
                                // If API reported 0 but we have matches and a viewurl, we’ll perform a best-effort direct compare on the top result
                                if ($status === 'No copies' && $viewurl) {
                                    isc_dbg($debug_log, $run_start, 'FALLBACK', 'API 0% but has viewurl; will fetch detail listing for direct compare');
                                }
                            }
                            
                            // >>> ADDED: include any fallback % in our saved "highest_percentage"
                            if ( isset($fallback_best) ) {
                                $highest = max( floatval($highest), floatval($fallback_best) );
                            }
                
                            $batch_results[ $page_url ] = [
                                'percentage'         => $percentage,
                                'highest_percentage' => (string)round($highest, 1),
                                'status'             => $status,
                                'matches'            => !empty($matches) ? implode(' | ', $matches) : ($viewurl ?: 'None'),
                                'viewurl'            => $viewurl,
                            ];
                        }
                
                        // ── CSC SAVE PROGRESS: persist this page immediately ──
                        $partial_results = file_exists( $cs_history_file )
                            ? json_decode( file_get_contents( $cs_history_file ), true )
                            : [];
                        $partial_results[ $page_url ] = $batch_results[ $page_url ];
                        file_put_contents(
                            $cs_history_file,
                            json_encode( $partial_results ),
                            LOCK_EX
                        );
                        file_put_contents(
                            $debug_log,
                            sprintf(
                                "[+%0.2f/99s] Persisted Copyscape result for page: %s\n\n",
                                min( microtime(true) - $run_start, 99 ),
                                $page_url
                            ),
                            FILE_APPEND|LOCK_EX
                        );
                
                        // ── IN-PAGE PROGRESS BAR UPDATE ──
                
                        // keep in-memory results in sync
                        $cs_results[ $page_url ] = $batch_results[ $page_url ];
                
                        // recalc how many done
                        $checked_count = count( $cs_results );
                        $percent       = $total_copyscape > 0
                            ? round( $checked_count / $total_copyscape * 100 )
                            : 0;
                
                        // emit a tiny script to bump the bar
                        echo "<script>
                          (function(){
                            var bar = document.getElementById('progress-bar-new');
                            var msg = document.getElementById('progress-message');
                            if ( bar ) {
                              bar.style.width = '{$percent}%';
                              bar.textContent = '{$checked_count} / {$total_copyscape} ({$percent}%)';
                            }
                            if ( msg ) {
                              msg.textContent = 'Checked {$checked_count} of {$total_copyscape} pages…';
                            }
                          })();
                        </script>";
                
                        // flush so the browser runs it immediately
                        if ( function_exists('ob_flush') ) { ob_flush(); }
                        flush();
                    } // end foreach cached_bodies
                } // end while batches
                
                // If we still have pages left in the queue, refresh now to continue (single refresh, not every 3)
                if ( ! empty( $to_check_queue ) ) {
                    ?>
                    <form id="auto-copyscape-batch-form" method="post" action="">
                        <?php $new_nonce = wp_create_nonce('sitemap_nonce'); ?>
                        <input type="hidden" name="action"               value="sitemap" />
                        <input type="hidden" name="_wpnonce"             value="<?php echo esc_attr( $new_nonce ); ?>" />
                        <input type="hidden" name="confirm_page_choices" value="1" />
                        <input type="hidden" name="copyscape_phase"      value="1" />
                        <input type="hidden" name="domainToCheck"        value="<?php echo esc_attr( $_POST['domainToCheck'] ); ?>" />
                        <?php if ( !empty($download_html_urls) ) : foreach ( $download_html_urls as $u ) : ?>
                            <input type="hidden" name="download_html_urls[]" value="<?php echo esc_attr( $u ); ?>" />
                        <?php endforeach; endif; ?>
                        <?php if ( !empty($copyscape_check_urls) ) : foreach ( $copyscape_check_urls as $u ) : ?>
                            <input type="hidden" name="copyscape_check_urls[]" value="<?php echo esc_attr( $u ); ?>" />
                        <?php endforeach; endif; ?>
                        <?php if ( !empty($skip_urls) ) : foreach ( $skip_urls as $u ) : ?>
                            <input type="hidden" name="skip_urls[]" value="<?php echo esc_attr( $u ); ?>" />
                        <?php endforeach; endif; ?>
                    </form>
                    <script>
                      // brief pause so the UI can update the progress bar before continuing
                      setTimeout(function(){
                        var f = document.getElementById('auto-copyscape-batch-form');
                        if (f) f.submit();
                      }, 1200);
                    </script>
                    <?php
                    exit;
                }

                    // ─── Sort history by highest_percentage descending (fallback to percentage) ───
                    uasort( $cs_results, function( $a, $b ) {
                       $a_pct   = isset($a['percentage']) ? intval($a['percentage']) : 0;          // may be fallback
                       $a_high  = isset($a['highest_percentage']) ? intval($a['highest_percentage']) : 0; // Copyscape detail
                       $a_final = max($a_pct, $a_high);
    
                       $b_pct   = isset($b['percentage']) ? intval($b['percentage']) : 0;
                       $b_high  = isset($b['highest_percentage']) ? intval($b['highest_percentage']) : 0;
                       $b_final = max($b_pct, $b_high);
    
                       return $b_final <=> $a_final;
                    } );
                
                    // all batches done → write final CSV
                    $cs_filename = ABSPATH . $host . '-copyscape.csv';
                
                    if ($out = fopen($cs_filename,'w')) {
                        // header
                        fputcsv($out, ['Page URL','Percentage','Status','Matches','View Matches URL']);
                        
                        // ── CSC DEBUG: scan complete with elapsed ──
                        $elapsed = microtime(true) - $run_start;
                        $sec     = sprintf("%0.2f", min($elapsed, 99));
                        file_put_contents(
                            $debug_log,
                            sprintf("[+%s/99s] Completed full Copyscape scan; cleaning up resume data\n", $sec),
                            FILE_APPEND|LOCK_EX
                        );

                        foreach ($cs_results as $page_url => $data) {
                            // decide which to show: the highest of fallback (%) vs Copyscape detail high
                            $api  = isset($data['percentage']) ? intval($data['percentage']) : 0;              // may be fallback
                            $high = isset($data['highest_percentage']) ? intval($data['highest_percentage']) : 0; // Copyscape detail
                            $final = max($api, $high);
                        
                            // NEW: robust Matches column
                            $matches_col = '';
                            if (!empty($data['matches'])) {
                                $matches_col = is_array($data['matches'])
                                    ? implode(' | ', $data['matches'])
                                    : (string)$data['matches'];
                            } elseif (!empty($data['viewurl'])) {
                                // If we have a Copyscape view page, include it so you can click through
                                $matches_col = (string)$data['viewurl'];
                            } else {
                                $matches_col = 'None';
                            }
                            
                            fputcsv($out, [
                                $page_url,
                                $final,
                                $data['status'],
                                $matches_col,
                                $data['viewurl']
                            ]);
                        }
                        fclose($out);
                        echo '<input type="hidden" id="copyscape-download-url" value="'.esc_url(home_url('/'.basename($cs_filename))).'" />';
                        // ── FINALIZE UI: trigger download, then hide progress bar + gif ──
                        echo "<script>
                        (function(){
                          var u   = document.getElementById('copyscape-download-url');
                          var bar = document.getElementById('progress-bar-new');
                          var gif = document.getElementById('progress-gif');
                          var message = document.getElementById('progress-message');
                          var wrap = document.getElementById('progress-container-new');
                        
                          // Mark Copyscape as ready + keep URL
                          if (u && u.value) {
                            document.documentElement.setAttribute('data-cs-ready','1');
                            document.documentElement.setAttribute('data-cs-url', u.value);
                          }
                        
                          // Collapse the progress UI
                          if (wrap) { if (wrap.remove) wrap.remove(); else if (wrap.parentNode) wrap.parentNode.removeChild(wrap); }
                          if (bar) bar.style.display = 'none';
                          if (gif) gif.style.display = 'none';
                          if (message) message.style.display = 'none';
                        
                          // Show final message and fire downloads (once)
                          ensureFinalMessageAndDownloads();
                        })();
                        </script>";

                        @unlink($cs_history_file);
                        // ── CSC RESUME CLEANUP: remove stale resume file ──
                        $resume_file = ABSPATH . 'copyscape-aborted-index.txt';
                        if ( file_exists( $resume_file ) ) {
                            unlink( $resume_file );
                        }
                        
                        // ——— clean up debug logs COMMENT THIS OUT TO SAVE THE LOG WHEN DEBUGGING ———
                        $ai_debug = ABSPATH . 'ai-smartcopy-debug.log';
                        if ( file_exists( $ai_debug ) ) {
                            @unlink( $ai_debug );
                        }
                
                        $copyscape_ran = true;
                        $completeWithoutIssue = true;
                    } else {
                        echo '<div class="notice notice-error"><p>Could not write Copyscape CSV file: '.esc_html($cs_filename).'</p></div>';
                    }
                }
            }
        }
    }
    if ( $completeWithoutIssue !== true ) {
        echo '<div class="notice notice-error"><p>';
        echo 'Sorry, something prevented this site\'s sitemap from being pulled. Please manually retrieve the sitemap.';
        echo '</p></div>';
    }
}
?>

<div>
    <!-- Hidden "Please Wait" GIF -->
    <div id="please-wait"
         style="opacity: 0; height: 0; overflow: hidden; transition: opacity 1s, height 1s;">
        <p>
            <img src="<?php echo plugins_url( '../search-and-replace/working-on-it.gif', __FILE__ ); ?>"
                 alt="Please wait"
                 style="width: 400px;" />
        </p>
    </div>

    <p class="ai-sc-upload-intro">Upload the sitemap CSV first, then the matching Copyscape CSV. Both filenames must contain the same website domain.</p>
    <div id="ai-sc-upload-warning" class="notice notice-warning inline is-dismissible" role="alert" hidden>
        <p><strong>Check your CSV filenames.</strong> <span class="ai-sc-upload-warning-text"></span></p>
    </div>

    <form method="post" action="" id="inception-sitemap-form" enctype="multipart/form-data">
        <input type="hidden" name="page_count" id="page_count" value="<?php echo intval( $page_count ); ?>" />

        <div class="ai-sc-upload-grid">
            <div class="ai-sc-upload-card">
                <label for="sitemap_csv" class="ai-sc-upload-label">
                    <span class="ai-sc-upload-step">1</span>
                    <span>Sitemap CSV</span>
                </label>
                <p class="ai-sc-upload-help">Required format: domain.com-sitemap.csv</p>
                <input type="file"
                       name="sitemap_csv"
                       id="sitemap_csv"
                       class="ai-sc-file-input"
                       accept=".csv,text/csv"
                       required />
            </div>

            <div class="ai-sc-upload-card">
                <label for="copyscape_csv" class="ai-sc-upload-label">
                    <span class="ai-sc-upload-step">2</span>
                    <span>Copyscape CSV</span>
                </label>
                <p class="ai-sc-upload-help">Required format: domain.com-copyscape.csv</p>
                <input type="file"
                       name="copyscape_csv"
                       id="copyscape_csv"
                       class="ai-sc-file-input"
                       accept=".csv,text/csv"
                       disabled
                       required />
                <p class="description">Select the sitemap CSV first to enable this upload.</p>
            </div>
        </div>

        <input type="hidden" name="action" value="sitemap" />
        <?php wp_nonce_field('sitemap_nonce', '_wpnonce', true, true); ?>
        <?php submit_button( 'Upload CSV Files', 'primary ai-sc-upload-submit' ); ?>
    </form>
    <style>
      .ai-sc-upload-intro {
        max-width: 760px;
        margin: 16px 0;
        color: #50575e;
        font-size: 14px;
      }
      .ai-sc-upload-grid {
        display: grid;
        grid-template-columns: repeat(2, minmax(260px, 380px));
        gap: 18px;
        max-width: 780px;
      }
      #ai-sc-upload-warning {
        max-width: 732px;
        margin: 0 0 16px;
      }
      .ai-sc-upload-card {
        padding: 20px;
        border: 1px solid #c3c4c7;
        border-radius: 8px;
        background: #fff;
        box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
      }
      .ai-sc-upload-label {
        display: flex;
        align-items: center;
        gap: 10px;
        margin-bottom: 6px;
        color: #1d2327;
        font-size: 15px;
        font-weight: 600;
      }
      .ai-sc-upload-step {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 26px;
        height: 26px;
        border-radius: 50%;
        background: #2271b1;
        color: #fff;
        font-size: 13px;
        line-height: 1;
      }
      .ai-sc-upload-help {
        min-height: 20px;
        margin: 0 0 12px 36px;
        color: #646970;
      }
      .ai-sc-file-input {
        width: 100%;
        padding: 7px;
        border: 1px solid #8c8f94;
        border-radius: 5px;
        background: #f6f7f7;
        box-sizing: border-box;
        color: #2c3338;
        cursor: pointer;
      }
      .ai-sc-file-input::file-selector-button {
        margin: -7px 12px -7px -7px;
        padding: 10px 15px;
        border: 0;
        border-right: 1px solid #8c8f94;
        background: #2271b1;
        color: #fff;
        font-weight: 600;
        cursor: pointer;
      }
      .ai-sc-file-input:hover::file-selector-button {
        background: #135e96;
      }
      .ai-sc-file-input:focus {
        border-color: #2271b1;
        box-shadow: 0 0 0 1px #2271b1;
        outline: 2px solid transparent;
      }
      .ai-sc-file-input:disabled {
        border-color: #dcdcde;
        background: #f0f0f1;
        color: #a7aaad;
        cursor: not-allowed;
      }
      .ai-sc-file-input:disabled::file-selector-button {
        border-right-color: #dcdcde;
        background: #a7aaad;
        cursor: not-allowed;
      }
      #inception-sitemap-form .description {
        margin: 10px 0 0;
      }
      #inception-sitemap-form .ai-sc-upload-submit {
        min-height: 36px;
        padding: 0 18px;
        font-weight: 600;
      }
      .field-error {
        border: 2px solid #e00 !important;
        box-shadow: 0 0 3px rgba(255,0,0,0.5);
      }
      @media (max-width: 782px) {
        .ai-sc-upload-grid {
          grid-template-columns: 1fr;
        }
      }
    </style>
    <script>
    (function($){
      var domainPattern = '(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}',
          sitemapPattern = new RegExp('^(' + domainPattern + ')-sitemap\\.csv$', 'i'),
          copyscapePattern = new RegExp('^(' + domainPattern + ')-copyscape\\.csv$', 'i'),
          $warning = $('#ai-sc-upload-warning'),
          $warningText = $warning.find('.ai-sc-upload-warning-text');

      function selectedFilename(input) {
        if (input && input.files && input.files.length) {
          return input.files[0].name;
        }
        return $.trim($(input).val()).split(/[/\\]/).pop();
      }

      function normalizedDomain(domain) {
        return String(domain || '').toLowerCase().replace(/^www\./, '');
      }

      function showUploadWarning(message) {
        $warningText.text(message);
        $warning.prop('hidden', false).show();
      }

      function clearUploadWarning() {
        $warningText.text('');
        $warning.prop('hidden', true).hide();
      }

      function validateUploadFilenames(showIncompleteWarning) {
        var sitemapInput = document.getElementById('sitemap_csv'),
            copyscapeInput = document.getElementById('copyscape_csv'),
            sitemapName = selectedFilename(sitemapInput),
            copyscapeName = selectedFilename(copyscapeInput),
            sitemapMatch,
            copyscapeMatch;

        $('#sitemap_csv, #copyscape_csv').removeClass('field-error');

        if (!sitemapName) {
          if (showIncompleteWarning) {
            $('#sitemap_csv').addClass('field-error');
            showUploadWarning('Choose a sitemap file named like domain.com-sitemap.csv.');
          } else {
            clearUploadWarning();
          }
          return false;
        }

        sitemapMatch = sitemapName.match(sitemapPattern);
        if (!sitemapMatch) {
          $('#sitemap_csv').addClass('field-error');
          showUploadWarning('The sitemap filename must start with a domain and end with "-sitemap.csv", such as moderncaredental.com-sitemap.csv.');
          return false;
        }

        if (!copyscapeName) {
          if (showIncompleteWarning) {
            $('#copyscape_csv').addClass('field-error');
            showUploadWarning('Choose the matching Copyscape file named like domain.com-copyscape.csv.');
          } else {
            clearUploadWarning();
          }
          return false;
        }

        copyscapeMatch = copyscapeName.match(copyscapePattern);
        if (!copyscapeMatch) {
          $('#copyscape_csv').addClass('field-error');
          showUploadWarning('The Copyscape filename must start with a domain and end with "-copyscape.csv", such as auroraangeles.com-copyscape.csv.');
          return false;
        }

        if (normalizedDomain(sitemapMatch[1]) !== normalizedDomain(copyscapeMatch[1])) {
          $('#sitemap_csv, #copyscape_csv').addClass('field-error');
          showUploadWarning(
            'These files are for different websites: the sitemap is for '
            + sitemapMatch[1]
            + ', but the Copyscape report is for '
            + copyscapeMatch[1]
            + '. Please choose two files for the same domain.'
          );
          return false;
        }

        clearUploadWarning();
        return true;
      }

      $('#inception-sitemap-form').on('submit', function(e) {
        if (!validateUploadFilenames(true)) {
          e.preventDefault();
          return;
        }
    
        // on success, show GIF & disable button
        showGif();
        $(this).find('input[type=submit], button').prop('disabled', true);
      });
    
      $('#sitemap_csv').on('change', function(){
        var sitemapName = selectedFilename(this),
            hasSitemap = sitemapPattern.test(sitemapName);
        $('#copyscape_csv').prop('disabled', !hasSitemap);
        if ( hasSitemap ) {
          $(this).removeClass('field-error');
          validateUploadFilenames(false);
        } else {
          $('#copyscape_csv').val('').removeClass('field-error');
          validateUploadFilenames(false);
        }
      });
      $('#copyscape_csv').on('change', function(){
        validateUploadFilenames(false);
      });
    })(jQuery);
    </script>
</div>

<?php
// --- added: only compute ZIP path if we're in a sitemap POST with a defined $domain
$zip_exists = false;

// Make sure these exist even on the initial GET so notices don't fire.
$csv_created   = isset($csv_created)   ? $csv_created   : false;
$zip_created   = isset($zip_created)   ? $zip_created   : false;
$copyscape_ran = isset($copyscape_ran) ? $copyscape_ran : false;

if ( isset($_POST['action']) && $_POST['action'] === 'sitemap' && !empty($domain) ) {
    $zip_file   = trailingslashit(ABSPATH) . preg_replace('/^www\./','', parse_url($domain, PHP_URL_HOST)) . '-pages.zip';
    $zip_exists = file_exists($zip_file);
}

if ( isset( $_POST['action'] ) 
  && $_POST['action'] === 'sitemap' 
  && ( ! empty( $csv_created ) 
       || $zip_exists 
       || ! empty( $copyscape_ran ) ) ) :
?>
    <div id="download-again-container" style="display: none; margin-top: 20px;">
        <?php if ( ! empty( $csv_created ) ) : ?>
            <button type="button" class="button button-secondary isc-button-hero" onclick="downloadCsv()">Download CSV File Again</button>
        <?php endif; ?>

        <?php if ( ! empty( $zip_created ) ) : ?>
            <button type="button" class="button button-secondary button-hero" onclick="downloadZip()" style="margin-left: 10px;">Download HTML Pages ZIP File Again</button>
        <?php endif; ?>

        <?php if ( ! empty( $copyscape_ran ) ) : ?>
            <button type="button" class="button button-secondary button-hero" onclick="downloadCopyscape()" style="margin-left: 10px;">Download Copyscape Report File Again</button>
        <?php endif; ?>
    </div>
<?php endif; ?>

<script>
function showGif() {
    var gif = document.getElementById('please-wait');
    gif.style.opacity = '1';
    gif.style.height  = 'auto';
}

function downloadCsv() {
    var csvUrlInput = document.getElementById('csv-download-url');
    if (csvUrlInput) {
        var csvUrl = csvUrlInput.value;
        var a = document.createElement('a');
        a.href = csvUrl;
        a.download = '';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
    }
}

function downloadZip() {
    var zipUrlInput = document.getElementById('zip-download-url');
    if (zipUrlInput) {
        var zipUrl = zipUrlInput.value;
        var a = document.createElement('a');
        a.href = zipUrl;
        a.download = '';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
    }
}

function downloadCopyscape() {
    var url = document.getElementById('copyscape-download-url');
    if (url) {
        var a = document.createElement('a');
        a.href = url.value;
        a.download = '';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
    }
}

function _readyState() {
  var csvEl = document.getElementById('csv-download-url');
  var zipEl = document.getElementById('zip-download-url');
  var csEl  = document.getElementById('copyscape-download-url');
  return {
    csvUrl: csvEl ? csvEl.value : (document.documentElement.getAttribute('data-csv-url') || ''),
    zipUrl: zipEl ? zipEl.value : (document.documentElement.getAttribute('data-zip-url') || ''),
    csUrl:  csEl  ? csEl.value  : (document.documentElement.getAttribute('data-cs-url')  || ''),
    hasCsv: !!csvEl || document.documentElement.getAttribute('data-csv-ready') === '1',
    hasZip: !!zipEl || document.documentElement.getAttribute('data-zip-ready') === '1',
    hasCs:  !!csEl  || document.documentElement.getAttribute('data-cs-ready')  === '1'
  };
}

function ensureFinalMessageAndDownloads() {
  var st = _readyState();

  var finalText = '';
  if (!finalText) return;

  // Create or update the single success notice
  var container = document.querySelector('.wrap') || document.body;
  var msg = document.getElementById('final-success');
  if (!msg) {
    msg = document.createElement('div');
    msg.id = 'final-success';
    msg.className = 'notice notice-success is-dismissible';
    msg.innerHTML = '<p><strong>' + finalText + '</strong></p>';
    container.insertBefore(msg, container.firstChild);
  } else {
    msg.innerHTML = '<p><strong>' + finalText + '</strong></p>';
  }

  var again = document.getElementById('download-again-container');
  if (again) again.style.display = 'none';

  // Fire the downloads exactly once
  if (document.documentElement.getAttribute('data-dl-fired') !== '1') {
    document.documentElement.setAttribute('data-dl-fired','1');

    // ZIP
    if (st.hasZip) {
      var zipEl = document.getElementById('zip-download-url');
      if (zipEl) { downloadZip(); }
      else if (st.zipUrl) { var b = document.createElement('a'); b.href = st.zipUrl; b.download = ''; document.body.appendChild(b); b.click(); b.remove(); }
    }
    // Copyscape
    if (st.hasCs) {
      var csEl = document.getElementById('copyscape-download-url');
      if (csEl) { downloadCopyscape(); }
      else if (st.csUrl) { var c = document.createElement('a'); c.href = st.csUrl; c.download = ''; document.body.appendChild(c); c.click(); c.remove(); }
    }
  }
}

window.addEventListener('load', function() {
  ensureFinalMessageAndDownloads();

  // Hide phase-specific success notices if they slipped through
  document.querySelectorAll('.notice.notice-success').forEach(function(n){
    var t = (n.textContent || '').toLowerCase();
    if (t.includes('slugs to sitemap csv file') || t.includes('downloaded html pages!')) {
      n.style.display = 'none';
    }
  });
});
</script>
