<?php
// Debug logging function for Broken Link Checker
function blc_debug_log( $message ) {
    $logfile = WP_CONTENT_DIR . '/blc-debug.log';
    $time    = date( 'Y-m-d H:i:s' );
    error_log( "[$time] $message" . PHP_EOL, 3, $logfile );
}
@set_time_limit(0); // no time limit

// 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 broken-link-checker plugin page
add_filter('nonce_life', function($lifetime) {
    if ( isset($_GET['page']) && $_GET['page'] === 'broken-link-checker' ) {
        return 24 * 60 * 60; // 24 hours in seconds
    }
    return $lifetime;
});

global $wpdb; // For database operations

// Set the table name with the proper prefix.
$table_name = $wpdb->prefix . 'approved_links';

// Check if the table exists.
$table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") == $table_name;

// Check if the 'created_at' column exists in the table.
$column_exists = $wpdb->get_var("SHOW COLUMNS FROM `$table_name` LIKE 'created_at'");

if (!$table_exists || !$column_exists) {
    // If the table exists but does not have the 'created_at' column, drop and recreate it.
    if ($table_exists) {
        $wpdb->query("DROP TABLE IF EXISTS `$table_name`");
    }

    // Create the table with the correct structure.
    $charset_collate = $wpdb->get_charset_collate();
    $sql = "CREATE TABLE $table_name (
        id mediumint(9) NOT NULL AUTO_INCREMENT,
        link text NOT NULL,
        created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
        PRIMARY KEY  (id)
    ) $charset_collate;";
    
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
}

// Clear out approved links older than 8 hours.
$wpdb->query(
    $wpdb->prepare(
        "DELETE FROM {$wpdb->prefix}approved_links WHERE created_at < NOW() - INTERVAL 8 HOUR"
    )
);

// *** Process "Clear Approved Links" button ***
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['clear_approved_links'])) {
    // Delete all records from the approved links table.
    $wpdb->query("DELETE FROM {$wpdb->prefix}approved_links");
    // Output a script that alerts the user and refreshes the page.
    echo "<script>alert('All previously approved links have now been cleared.'); window.location.href = window.location.href;</script>";
    exit;
}

/**
 * Normalize a URL by removing non-printable characters, resolving internal links, and trimming whitespace.
 *
 * @param string $url
 * @return string The normalized URL.
 */
function normalize_url($url) {
    // Remove non-printable/control characters (ASCII 0x00-0x1F and 0x7F).
    $url = preg_replace('/[\x00-\x1F\x7F]+/', '', $url);
    // Resolve internal links.
    $url = resolve_internal_links($url);
    // Trim whitespace.
    return trim($url);
}

/**
 * Check if a given link is approved by comparing it to all approved links.
 * If any approved link has at least $threshold percent similarity to $link, returns true.
 *
 * @param string $link The normalized broken link.
 * @param array  $approved_links An array of normalized approved links.
 * @param int    $threshold Percentage threshold (default 95).
 * @return bool True if a match is found.
 */
function is_link_approved($link, $approved_links, $threshold = 95) {
    // Hardcoded permanent whitelist
    $permanent_whitelist = [
        'https://www.hhs.gov/hipaa/filing-a-complaint/what-to-expect/index.html',
    ];

    // Check if the link is in the permanent whitelist
    if (in_array($link, $permanent_whitelist, true)) {
        blc_debug_log("Permanent whitelist matched → $link");
        return true;
    }

    // Fuzzy matching against database-approved links
    foreach ($approved_links as $approved) {
        similar_text($link, $approved, $percent);
        if ($percent >= $threshold) {
            return true;
        }
    }

    return false;
}

/**
 * Check the HTTP status of a URL.
 *
 * This function runs the check using the URL exactly as it is.
 *
 * @param string $url
 * @return array Associative array with keys 'code' and 'message'.
 */
function check_all_link_status($url) {
    static $cache = [];

    if (isset($cache[$url])) {
        blc_debug_log("Cache hit for URL: $url, code: {$cache[$url]['code']}");
        return $cache[$url];
    }
    
    // start timer for debug
    $start = microtime( true );
    blc_debug_log( "GET check for URL: $url" );

    $args = array(
        'timeout'     => 5,     // total seconds allowed
        'redirection' => 5,     // follow up to 5 redirects
        'blocking'    => true,
        'sslverify'   => false, // skip cert checks
        'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' .
                        '(KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36'
    );

    $response = wp_remote_get($url, $args);
    $duration = round( microtime( true ) - $start, 2 );
    blc_debug_log( "Request to $url took {$duration}s" );
    
    if (is_wp_error($response) || empty($response['response']['code'])) {
        $code = is_wp_error($response) ? 0 : 0;
        $msg  = is_wp_error($response) ? $response->get_error_message() : 'No response';

        // Fallback for cURL error 35: use direct cURL with relaxed SSL
        if (strpos($msg, 'cURL error 35') !== false) {
            blc_debug_log("GET cURL error 35 for URL: $url, attempting direct cURL fallback");
            $ch = curl_init($url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_NOBODY, true);
            curl_setopt($ch, CURLOPT_TIMEOUT, 5);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
            curl_exec($ch);
            $curl_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);
            blc_debug_log("curl fallback code for URL: $url, code: $curl_code");
            if ($curl_code >= 200 && $curl_code < 400) {
                $cache[$url] = [
                    'code'    => $curl_code,
                    'message' => 'OK (curl fallback)'
                ];
                return $cache[$url];
            }
        }

        blc_debug_log("GET response for URL: $url, code: $code, message: $msg");
        $cache[$url] = [
            'code'    => $code,
            'message' => $msg
        ];
        return $cache[$url];
    }

    // successfully got a code
    $code = (int) wp_remote_retrieve_response_code($response);
    $msg  = wp_remote_retrieve_response_message($response);
    blc_debug_log("GET response for URL: $url, code: $code, message: $msg");

    $cache[$url] = [
        'code'    => $code,
        'message' => $msg
    ];
    return $cache[$url];
}

/**
 * Extract all unique anchor links from post content.
 */
function get_all_links_from_content($content) {
    preg_match_all('/<a\s[^>]*href=["\']?([^"\' >]+)["\']?/i', $content, $matches);
    $links = $matches[1] ?? [];
    // Filter out unwanted links.
    return array_filter(array_unique($links), function($link) {
        return !(
            strpos($link, 'https://www.google.com/maps/embed') !== false ||
            strpos($link, 'tel:') === 0 ||
            strpos($link, '#') === 0 ||
            strpos($link, 'mailto:') === 0 ||
            in_array($link, ['javascript:void(0)', 'javascript:void(0);', 'void(0)', 'http://void(0)'])
        );
    });
}

/**
 * Extract all unique image sources from post content.
 */
function get_all_image_src_from_content($content) {
    preg_match_all('/<img\s[^>]*src=["\']?([^"\' >]+)["\']?/i', $content, $matches);
    $images = $matches[1] ?? [];
    return array_filter(array_unique($images), function($src) {
        return !empty($src);
    });
}

/**
 * Extract all unique iframe sources from post content.
 */
function get_all_iframe_src_from_content($content) {
    preg_match_all('/<iframe\s[^>]*src=["\']?([^"\' >]+)["\']?/i', $content, $matches);
    $iframes = $matches[1] ?? [];
    return array_filter(array_unique($iframes), function($src) {
        return !empty($src);
    });
}

/**
 * Resolve internal links to full URLs.
 *
 * - If the link starts with "../", remove all leading "../" segments and prepend the home URL.
 * - If the link starts with "//" (protocol‑relative), leave it as is.
 * - If the link starts with "/" (but not with "//"), prepend the home URL.
 * - Otherwise, if it’s already a valid full URL, return it unchanged.
 *
 * @param string $link
 * @return string
 */
function resolve_internal_links($link) {
    if (substr($link, 0, 3) === '../') {
        while (substr($link, 0, 3) === '../') {
            $link = substr($link, 3);
        }
        return trailingslashit(home_url()) . ltrim($link, '/');
    }
    if (substr($link, 0, 2) === '//') {
        return $link;
    }
    if (substr($link, 0, 1) === '/') {
        return trailingslashit(home_url()) . ltrim($link, '/');
    }
    if (filter_var($link, FILTER_VALIDATE_URL)) {
        return $link;
    }
    return $link;
}

// Initialize the broken links map.
$broken_links_map = [];

// Fetch approved links from the approved links table and normalize them.
$approved_links = $wpdb->get_col("SELECT link FROM {$wpdb->prefix}approved_links");
$approved_links = array_map('normalize_url', $approved_links);

// Run the scan ONLY if the "Check Broken Links" button was pressed.
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['check_broken_links'])) {
    // ── PROGRESS BAR PINSTRIPE ANIMATION CSS ──
    echo '<style>
      @keyframes moveStripes {
        from { background-position: 0 0; }
        to   { background-position: 8px 0; }
      }
    .pinstripe-bar {
      /* base color */
      background-color: #0073AA;
      /* fine white pinstripes at 45° */
      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;
      /* round the corners */
      border-radius: 6px;
      /* animate the stripes sliding */
      animation: moveStripes 0.7s linear infinite;
    }
    </style>';
    
    // ── PROGRESS BAR CONTAINER (start) ──
    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;">';
    // Added back in solid progress bar color - remove "background:#0073AA;" for pinstripe effect
    echo '    <div id="progress-bar" class="pinstripe-bar" '
       . 'style="width:0%; background:#0073AA; color:#fff; height:30px; line-height:30px; '
       . 'text-align:center; font-weight:bold; transition: width 0.2s ease;">';
    echo '      0%';
    echo '    </div>';
    echo '  </div>';
    echo '  <p id="progress-message" style="font-weight:bold;">Finding and verifying broken links... please be patient.</p>';
    echo '    <img id="progress-gif" src="' . plugins_url('../search-and-replace/working-on-it.gif', __FILE__) . '" alt="Working on it" style="width: 400px; margin-bottom: 20px;" />';
        // ── CURRENT PAGE BOX (initially hidden) ──
    echo '<div id="page-name-box" style="display:none; margin-top:10px; padding:10px; background:#fff; border:1px solid #ccc; font-weight:bold;">';
    echo '  This one is taking a bit longer than usual! Currently scanning: <a id="current-page-link" href="" target="_blank" style="text-decoration:underline;"></a>';
    echo '</div>';
    // initialize our JS timer handle
    echo '<script>window.pageNameTimer = null;</script>';
    echo '</div>';
    // Flush out so the browser renders the bar immediately
    if ( function_exists('ob_flush') ) { ob_flush(); }
    flush();

    // ── RETRIEVE OR INIT FULL MAP ──
    $transient_key = 'blc_broken_links_map';
    $full_map = get_transient($transient_key);
    if (!is_array($full_map)) {
        $full_map = [];
    }

    // ── RETRIEVE OR INIT TEMPLATE‐IMAGE MAP ──
    $template_key   = 'blc_template_images_map';
    $template_map   = get_transient($template_key);
    if ( ! is_array( $template_map ) ) {
        $template_map = [];
    }

    // ── DEBUG: dump raw POST ──
    blc_debug_log( '$_POST payload: ' . print_r( $_POST, true ) );
    
    // ── BLC PROGRESS: persist excluded slugs to disk ──
    $progress_file = ABSPATH . 'blc-progress.txt';
    $exclude_file  = ABSPATH . 'blc-exclude.txt';
    
    // If user just submitted checkboxes, save them to blc-exclude.txt
    if ( isset( $_POST['exclude_slugs'] ) && is_array( $_POST['exclude_slugs'] ) ) {
        $slugs = array_map( 'sanitize_key', $_POST['exclude_slugs'] );
        // Load existing slugs (if any)
        $existing = file_exists( $exclude_file )
            ? file( $exclude_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES )
            : [];
        
        // Merge & dedupe
        $all_slugs = array_unique( array_merge( $existing, $slugs ) );
        
        // Write back the full, unique list
        file_put_contents( $exclude_file, implode( PHP_EOL, $all_slugs ) );
    }
    
    // Load the exclude‐slugs from disk (if any)
    if ( file_exists( $exclude_file ) ) {
        $exclude_slugs = file(
            $exclude_file,
            FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES
        );
    } else {
        $exclude_slugs = [];
    }
    
    // Convert slugs → IDs once, for your skip logic
    $exclude_ids = [];
    foreach ( $exclude_slugs as $slug ) {
        if ( $p = get_page_by_path( $slug, OBJECT, ['page','post','fl-theme-layout'] ) ) {
            $exclude_ids[] = $p->ID;
        }
    }

    // ── BATCH SETUP ──
    $batch_size = 20; // small enough each request finishes under ~2 min
    $batch_page = isset($_POST['batch_page']) ? intval($_POST['batch_page']) : 1;

    // ── THIS BATCH’S MAP ──
    $batch_map = [];

    // === SCAN POSTS, PAGES, AND THEME LAYOUTS ===
    $args = array(
        'post_type'      => array('page','post','fl-theme-layout'),
        'posts_per_page' => $batch_size,
        'paged'          => $batch_page,
        'post_status'    => array('publish','draft','private','pending','future'),
        'orderby'        => 'title',
        'order'          => 'ASC',
    );
    $query = new WP_Query($args);
    
    // ── PROGRESS BAR: total pages to scan ──
    $total_posts = $query->found_posts;
    
    // ── INITIAL PROGRESS BAR ON LOAD ──
    $pages_before = ( $batch_page - 1 ) * $batch_size;
    $initial_percent = $total_posts > 0
        ? round( $pages_before / $total_posts * 100 )
        : 0;
    
    // push it to the bar before any scanning begins
    echo "<script>
      (function(){
        var bar = document.getElementById('progress-bar');
        if (!bar) return;
        bar.style.width = '{$initial_percent}%';
        bar.innerText   = '{$initial_percent}%';
      })();
    </script>";
    
    // flush so the browser paints it immediately
    if ( function_exists('ob_flush') ) { ob_flush(); }
    flush();

    // ── BLC PROGRESS: save this batch’s slugs ──
    $progress_file = ABSPATH . 'blc-progress.txt';
    if ( ! empty( $query->posts ) ) {
        $slugs_in_batch = wp_list_pluck( $query->posts, 'post_name' );
        file_put_contents( $progress_file, implode( PHP_EOL, $slugs_in_batch ) );
    }
    
    // ── BLC BATCH TIMING: start timer once for this whole batch ──
    $blc_batch_start         = microtime(true);
    $blc_batch_last_logged_sec = 0;
    $blc_time_limit          = 99; // seconds
    blc_debug_log( "Batch timer started" );
    
    // ── BLC PAGE RESUME: load resume target from blc-aborted-index.txt ──
    $resume_file_global = ABSPATH . 'blc-aborted-index.txt';
    $resume_slug_global  = '';
    $resume_link_index   = 0;
    $resume_page_found   = false;
    if ( file_exists( $resume_file_global ) ) {
        list( $resume_slug_global, $resume_link_index ) = explode( '|', trim( file_get_contents( $resume_file_global ) ) );
        blc_debug_log( "Batch resume target: page slug '{$resume_slug_global}', link index {$resume_link_index}" );
    }

    if ($query->have_posts()) {
        while ($query->have_posts()) {
            $query->the_post();
            $post_id = get_the_ID();
            $title = get_the_title( $post_id );
            $url = get_permalink( $post_id );
            
            // ── PAGE-NAME TIMER RESET & SETUP (with 100% check) ──
            
            // calculate how far we are
            $current_in_batch = $query->current_post + 1;
            $pages_done       = ($batch_page - 1) * $batch_size + $current_in_batch;
            $percent = $total_posts > 0 
                ? round( $pages_done / $total_posts * 100 ) 
                : 0;
            
            // update the bar
            echo "<script>
              (function(){
                var bar = document.getElementById('progress-bar');
                if (bar) {
                  bar.style.width = '{$percent}%';
                  bar.innerText   = '{$percent}%';
                }
              })();
            </script>";
            
            // now the timer for the page‐name box
            echo '<script>
              (function(){
                var title   = ' . json_encode($title) . ';
                var url     = ' . json_encode($url) . ';
                var percent = ' . intval($percent) . ';
                var link    = document.getElementById("current-page-link");
                var box     = document.getElementById("page-name-box");
            
                // always hide and reset old timer first
                if (box) box.style.display = "none";
                if (window.pageNameTimer) clearTimeout(window.pageNameTimer);
            
                window.pageNameTimer = setTimeout(function(){
                  if (!box) return;
            
                  if (percent === 100) {
                    // final message
                    box.innerHTML = "All links scanned! Wrapping up and preparing final report...";
                  } else if (link) {
                    // still scanning a slow page
                    link.innerText = title;
                    link.href      = url;
                  }
            
                  box.style.display = "block";
                }, 7000);
              })();
            </script>';
            if (function_exists('ob_flush')) { ob_flush(); }
            flush();
            
            // ── BLC PAGE RESUME: skip fully scanned pages ──
            $current_slug = get_post_field( 'post_name', $post_id );
            if ( $resume_slug_global && ! $resume_page_found ) {
                if ( $current_slug !== $resume_slug_global ) {
                    blc_debug_log( "Skipping page before resume target: '{$current_slug}'" );
                    continue;
                }
                // we’ve reached the page we want to resume on
                $resume_page_found = true;
            }

            // ── BLC PROGRESS: skip entire page and all its links/images ──
            if ( in_array( $post_id, $exclude_ids, true ) ) {
                blc_debug_log( "Skipping excluded page ID {$post_id} — no links or images pulled" );
                continue;
            }
            // ── BLC DEBUG: log which page we’re scanning ──
            $post_title = get_the_title( $post_id );
            $post_link  = get_permalink( $post_id );
            blc_debug_log( "=== Scanning Page ID {$post_id}: '{$post_title}' ({$post_link}) ===" );

// ── BLOG SLUG ≔ blog: check ALL _wp_attached_file for leading semicolons ──
if ( 'page' === get_post_type( $post_id )
     && 'blog' === get_post_field( 'post_name', $post_id )
) {
    global $wpdb;

    // grab ANY meta_value that starts with ";"
    $meta_values = $wpdb->get_col( $wpdb->prepare(
        "SELECT meta_value
         FROM {$wpdb->postmeta}
         WHERE meta_key = %s
           AND meta_value LIKE %s",
        '_wp_attached_file',
        $wpdb->esc_like( ';' ) . '%'
    ) );

    foreach ( array_unique( $meta_values ) as $raw_meta ) {
        // strip only the semicolon for key/lookup
        $stripped = ltrim( $raw_meta, ';' );

        // if it’s already an absolute URL, use it; otherwise build from uploads folder
        if ( preg_match( '#^https?://#i', $stripped ) ) {
            $file_url = $stripped;
        } else {
            $file_url = content_url( '/uploads/' . $stripped );
        }

        $display = ';' . $file_url;
        $key     = normalize_url( $file_url );

        blc_debug_log(
            "Blog-meta: semicolon‑prefixed _wp_attached_file detected → $display"
        );

        // merge it into this batch’s map so it survives to the final table
        if ( ! isset( $batch_map[ $key ] ) ) {
            $batch_map[ $key ] = [
                'display_link' => $display,
                'status'       => 0,
                'pages'        => [],
                'types'        => [ 'Blog Meta Image' ],
            ];
        }
        // tag it to your /blog page
        $page_key = 'page-' . $post_id;
        if ( ! isset( $batch_map[ $key ]['pages'][ $page_key ] ) ) {
            $batch_map[ $key ]['pages'][ $page_key ] = [
                'title' => get_the_title( $post_id ),
                // ensure we always point at /blog
                'url'   => get_permalink( $post_id ),
            ];
        }
    }
}


            // ── BLC LINK‑LEVEL RESUME: prepare to skip links inside this page ──
            $link_counter     = 0;
            $blc_resume_index = ( $resume_page_found && $current_slug === $resume_slug_global )
                               ? intval( $resume_link_index )
                               : 0;
            if ( $blc_resume_index > 0 ) {
                blc_debug_log( "Resuming link scan at index {$blc_resume_index} on page '{$current_slug}'" );
            }

            //$excluded_ids = [ XXX, XXXX ];  // replace with your page IDs for pages you do NOT want checked
            //if ( in_array( $post_id, $excluded_ids, true ) ) {
            //    continue; // skip this page entirely
            //}
            $content   = get_post_field('post_content',$post_id,'raw');
            $content   = str_replace('---','-–-',$content);
            $anchor_links = get_all_links_from_content($content);
            // ── BLC DEBUG: log all links on this page ──
            blc_debug_log( "Anchor links on page ID {$post_id}: " . print_r( $anchor_links, true ) );

            $image_links  = get_all_image_src_from_content($content);
            // ── BLC DEBUG: log all image URLs on this page ──
            blc_debug_log( "Image sources on page ID {$post_id}: " . print_r( $image_links, true ) );

            $iframe_links = get_all_iframe_src_from_content($content);
            // ——— NEW: pull <video> and <source> URLs from the content ———
            $video_links = [];
            
            // <video src="…">
            if ( preg_match_all(
                '/<video[^>]+\bsrc=["\']([^"\']+)["\']/i',
                $content,
                $vid_matches
            ) ) {
                $video_links = array_merge( $video_links, $vid_matches[1] );
            }
            
            // <source src="…" inside <video> or <audio>
            if ( preg_match_all(
                '/<source[^>]+\bsrc=["\']([^"\']+)["\']/i',
                $content,
                $src_matches
            ) ) {
                $video_links = array_merge( $video_links, $src_matches[1] );
            }
            
            // Dedupe and add to your media_links array
            $video_links = array_unique( $video_links );
            $media_links['Video'] = $video_links;
            
            $media_links  = array(
                'Link'   => $anchor_links,
                'Image'  => $image_links,
                'iframe' => $iframe_links,
            );

// 1) Pull the builder data and re‑serialize it for a global search
$fl_data_raw = get_post_meta( $post_id, '_fl_builder_data', true );
$fl_data_str = maybe_serialize( $fl_data_raw );

// ── CATCH semicolon‑prefixed inceptionimages.com BUILDER‑IMAGE URLs ──
if ( preg_match_all(
    '/;https?:\/\/[^"\\\']*?inceptionimages\\.com[^"\\\']*\\.(?:jpe?g|png|gif|webp)(?:\\?[^\\s"\\\']*)?/i',
    $fl_data_str,
    $m
) ) {
    foreach ( array_unique( $m[0] ) as $broken_src ) {
        // strip the leading semicolon
        $url    = ltrim( $broken_src, ';' );
        $actual = normalize_url( $url );
        blc_debug_log( "Builder: semicolon‑prefixed inceptionimages.com image detected, marking broken → $broken_src" );

        // ── RECORD BROKEN LINK ──
        if ( ! isset( $batch_map[ $actual ] ) ) {
            $batch_map[ $actual ] = [
                'display_link' => $broken_src,
                'status'       => 0,
                'pages'        => [],
                'types'        => [ 'Image' ],
            ];
        }
        $page_key = 'post-' . $post_id;
        if ( ! isset( $batch_map[ $actual ]['pages'][ $page_key ] ) ) {
            $batch_map[ $actual ]['pages'][ $page_key ] = [
                'title' => get_the_title( $post_id ),
                'url'   => get_permalink( $post_id ),
            ];
        }
    }
}

// 2) Regex out _any_ URL containing our two domains
if ( preg_match_all(
    '/https?:\/\/[^"\']*?(?:templates\.inception-example\.com|inceptionimages\.com|chirobump\.com)\/wp-content\/uploads\/[^\s"\']+\.(?:jpe?g|png|gif|webp|mp4|webm|ogg)(?:\?[^\s"\']*)?/i',
    $fl_data_str,
    $matches
) ) {
    foreach ( array_unique( $matches[0] ) as $found_url ) {
        $actual = normalize_url( $found_url );
        $host   = parse_url( $actual, PHP_URL_HOST );

        // double‑check the host match (belt‑and‑suspenders)
        if ( $host
          && ( stripos( $host, 'templates.inception-example.com' ) !== false
             || stripos( $host, 'inceptionimages.com' )      !== false
             || stripos( $host, 'chirobump.com' )      !== false )
        ) {
            if ( ! isset( $template_map[ $actual ] ) ) {
                $template_map[ $actual ] = [
                    'display_link' => $actual,
                    'pages'        => [],
                ];
            }
            $page_key = 'post-' . $post_id;
            if ( ! isset( $template_map[ $actual ]['pages'][ $page_key ] ) ) {
                $template_map[ $actual ]['pages'][ $page_key ] = [
                    'title' => get_the_title( $post_id ),
                    'url'   => get_permalink( $post_id ),
                ];
            }
        }
    }
}


foreach ( $media_links as $type => $links ) {
    foreach ( $links as $link ) {
        // increment global link counter
        $link_counter++;
        // if we’re resuming, skip up to that index
        if ( $blc_resume_index > 0 && $link_counter < $blc_resume_index ) {
            blc_debug_log( "Skipping already scanned link #{$link_counter} → {$link}" );
            continue;
        }

        // ── BLC TIMING: check elapsed time each iteration ──
        $elapsed = floor( microtime(true) - $blc_batch_start );
        if ( $elapsed > $blc_last_logged_sec ) {
            blc_debug_log( "Batch elapsed {$elapsed} seconds" );
            $blc_last_logged_sec = $elapsed;
        }
        if ( $elapsed >= $blc_time_limit ) {
            blc_debug_log( "Page ID {$post_id} exceeded {$blc_time_limit}s – aborting remaining links" );
            // ── BLC ABORTED: record slug once ──
            if ( ! $blc_abort ) {
                $slug = get_post_field( 'post_name', $post_id );

                // ── BLC ABORTED: record last processed link index ──
                file_put_contents(
                    ABSPATH . 'blc-aborted-index.txt',
                    $slug . '|' . $link_counter . PHP_EOL,
                    LOCK_EX
                );
                blc_debug_log( "Recorded aborted slug '{$slug}' to blc-aborted-index.txt" );
            }
            $blc_abort = true;
        }
        if ( $blc_abort ) {
            break;
        }

        blc_debug_log( "Media: raw link → $link" );
        $resolved = resolve_internal_links( $link );
        $actual   = normalize_url( $resolved );
        blc_debug_log( "Media: normalized → $actual" );

        // ——— NEW: detect any image from those two hosts ———
        if ( 'Image' === $type ) {
            $host = parse_url( $actual, PHP_URL_HOST );
            if ( $host
              && ( stripos( $host, 'templates.inception-example.com' ) !== false
                 || stripos( $host, 'inceptionimages.com' )      !== false
                 || stripos( $host, 'chirobump.com' )      !== false )
            ) {
                if ( ! isset( $template_map[ $actual ] ) ) {
                    $template_map[ $actual ] = [
                        'display_link' => $actual,
                        'pages'        => [],
                    ];
                }
                $page_key = 'post-' . $post_id;
                if ( ! isset( $template_map[ $actual ]['pages'][ $page_key ] ) ) {
                    $template_map[ $actual ]['pages'][ $page_key ] = [
                        'title' => get_the_title( $post_id ),
                        'url'   => get_permalink( $post_id ),
                    ];
                }
                // skip the rest of your broken-link checks for this URL
                continue;
            }
        }
        
        if ( 'Video' === $type ) {
            $host = parse_url( $actual, PHP_URL_HOST );
            if ( $host
              && ( stripos( $host, 'templates.inception-example.com' ) !== false
                 || stripos( $host, 'inceptionimages.com' )      !== false
                 || stripos( $host, 'chirobump.com' )           !== false )
            ) {
                if ( ! isset( $template_map[ $actual ] ) ) {
                    $template_map[ $actual ] = [
                        'display_link' => $actual,
                        'pages'        => [],
                    ];
                }
                $page_key = 'post-' . $post_id;
                if ( ! isset( $template_map[ $actual ]['pages'][ $page_key ] ) ) {
                    $template_map[ $actual ]['pages'][ $page_key ] = [
                        'title' => get_the_title( $post_id ),
                        'url'   => get_permalink( $post_id ),
                    ];
                }
                // skip further broken‑link logic for these template videos
                continue;
            }
        }


        // ── SKIP INTERNAL LINKS VIA WP DATABASE ──
        $home_host = parse_url( home_url(),     PHP_URL_HOST );
        $url_host  = parse_url( $actual,        PHP_URL_HOST );
        if ( $url_host === $home_host ) {
            $internal_post = url_to_postid( $actual );
            if ( $internal_post ) {
                blc_debug_log( "Media: internal post {$internal_post} exists, skipping → $actual" );
                continue;
            }
        }

        if ( is_link_approved( $actual, $approved_links ) ) {
            blc_debug_log( "Media: approved, skipping → $actual" );
            continue;
        }
        blc_debug_log( "Media: not approved, checking GET → $actual" );

        // Primary GET
        $status = check_all_link_status( $actual );
        blc_debug_log( "Media: GET result → $actual, code: {$status['code']}" );

        // If GET fails, fallback to WP HTTP w/ sslverify off
        if ( $status['code'] === 0 && strpos( $status['message'], 'cURL error 35' ) !== false ) {
            blc_debug_log( "Media: GET cURL 35, retrying with sslverify=false → $actual" );
            $retry = wp_remote_get( $actual, [
                'timeout'   => 15,
                'sslverify' => false,
                'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' .
                                                '(KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36',
            ]);
            if ( ! is_wp_error( $retry ) ) {
                $r_code = wp_remote_retrieve_response_code( $retry );
                blc_debug_log( "Media: sslverify retry code → $actual, code: $r_code" );
                $status = [ 'code' => $r_code, 'message' => '' ];
            }
        }

        // Now double‐check broken via HEAD
        if ( in_array( $status['code'], [404,403,502,0], true ) ) {
            blc_debug_log( "Media: confirming broken with HEAD → $actual" );
            $head = wp_remote_request( $actual, [
                'method'     => 'HEAD',
                'timeout'    => 15,
                'sslverify'  => false,
                'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' .
                                                '(KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36',
            ]);
            if ( ! is_wp_error( $head ) ) {
                $h_code = wp_remote_retrieve_response_code( $head );
                blc_debug_log( "Media: HEAD confirm → $actual, code: $h_code" );
                if ( $h_code >= 200 && $h_code < 400 ) {
                    blc_debug_log( "Media: HEAD ok, skipping → $actual" );
                    continue;
                }
            } else {
                blc_debug_log( "Media: HEAD error → $actual, " . $head->get_error_message() );
            }

            // ── LOCAL FILE CHECK ──
            $uploads = wp_upload_dir();
            if ( 0 === strpos( $actual, $uploads['baseurl'] ) ) {
                $relative = parse_url( $actual, PHP_URL_PATH );
                $filepath = ABSPATH . ltrim( $relative, '/' );
                if ( file_exists( $filepath ) ) {
                    blc_debug_log( "Media: local file exists, skipping → $actual" );
                    continue;
                }
            }
            
            // ── RECORD BROKEN LINK ──
            blc_debug_log( "Media: recording broken → $actual, code: {$status['code']}" );
            if ( ! isset( $batch_map[ $actual ] ) ) {
                $batch_map[ $actual ] = [
                    'display_link' => $actual,
                    'status'       => $status['code'],
                    'pages'        => [],
                    'types'        => [ $type ],
                ];
            } elseif ( ! in_array( $type, $batch_map[ $actual ]['types'], true ) ) {
                $batch_map[ $actual ]['types'][] = $type;
            }
            
            if ( ! isset( $batch_map[ $actual ]['pages'][ $post_id ] ) ) {
                $batch_map[ $actual ]['pages'][ $post_id ] = [
                    'title' => get_the_title(),
                    'url'   => $post_link,
                ];
            }
        } else {
            blc_debug_log( "Media: GET ok, not broken → $actual" );
        }
    }
}

        if ( $blc_abort ) {
            continue; // go on to the next post in the batch
        }

            // Featured image check
            if ('post' === get_post_type()) {
                $feat_url = get_the_post_thumbnail_url($post_id, 'full');
                
            // ── CATCH semicolon‑prefixed inceptionimages.com featured images ──
            if (
                substr( $feat_url, 0, 1 ) === ';'
                && ( $host = parse_url( ltrim( $feat_url, ';' ), PHP_URL_HOST ) )
                && stripos( $host, 'inceptionimages.com' ) !== false
            ) {
                blc_debug_log( "Featured: semicolon‑prefixed inceptionimages.com image detected → $feat_url" );
        
                // normalize without semicolon for keying
                $url    = ltrim( $feat_url, ';' );
                $actual = normalize_url( $url );
        
                // record into this batch’s map (so it merges into full_map)
                if ( ! isset( $batch_map[ $actual ] ) ) {
                    $batch_map[ $actual ] = [
                        'display_link' => $feat_url,
                        'status'       => 0,
                        'pages'        => [],
                        'types'        => [ 'Featured Image (Blog)' ],
                    ];
                } elseif ( ! in_array( 'Featured Image (Blog)', $batch_map[ $actual ]['types'], true ) ) {
                    $batch_map[ $actual ]['types'][] = 'Featured Image (Blog)';
                }
                // attach the page
                $page_key = 'post-' . $post_id;
                if ( ! isset( $batch_map[ $actual ]['pages'][ $page_key ] ) ) {
                    $batch_map[ $actual ]['pages'][ $page_key ] = [
                        'title' => get_the_title(),
                        'url'   => $post_link,
                    ];
                }
        
                // skip the normal HTTP checks so it stays broken
                continue;
            }

                if ($feat_url) {
                    $actual_link = normalize_url($feat_url);
                    if (!is_link_approved($actual_link, $approved_links)) {
                        $status = check_all_link_status($actual_link);
                        if (in_array($status['code'], [404, 403, 0])) {
                            blc_debug_log( "HEAD check for URL: $actual_link" );
                            
                            $head_args = [
                                'method'     => 'HEAD',
                                'timeout'    => 15,
                                'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' .
                                                '(KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36',
                            ];
                            $head_resp = wp_remote_request( $actual_link, $head_args );
                            
                            if ( is_wp_error( $head_resp ) ) {
                                $err = $head_resp->get_error_message();
                                blc_debug_log( "HEAD response error for URL: $actual_link, error: $err" );
                            } else {
                                $head_code = wp_remote_retrieve_response_code( $head_resp );
                                $head_msg  = wp_remote_retrieve_response_message( $head_resp );
                                blc_debug_log( "HEAD response for URL: $actual_link, code: $head_code, message: $head_msg" );
                                if ( $head_code >= 200 && $head_code < 400 ) {
                                    continue;
                                }
                            }

                            if (!isset($broken_links_map[$actual_link])) {
                                $broken_links_map[$actual_link] = [
                                    'display_link' => $actual_link,
                                    'status'       => $status['code'],
                                    'pages'        => [],
                                    'types'        => ['Featured Image (Blog)'],
                                ];
                            } else {
                                if (!in_array('Featured Image (Blog)', $broken_links_map[$actual_link]['types'])) {
                                    $broken_links_map[$actual_link]['types'][] = 'Featured Image (Blog)';
                                }
                            }
                            $page_key = $post_id;
                            if (!isset($broken_links_map[$actual_link]['pages'][$page_key])) {
                                $broken_links_map[$actual_link]['pages'][$page_key] = [
                                    'title' => get_the_title(),
                                    'url'   => $post_link,
                                ];
                            }
                        }
                    }
                }
            }
            // ── PROGRESS BAR UPDATE: after each page ──
            $current_in_batch = $query->current_post + 1;
            $pages_done       = ( $batch_page - 1 ) * $batch_size + $current_in_batch;
            if ( $total_posts > 0 ) {
                $percent = round( $pages_done / $total_posts * 100 );
                echo "<script>
                  (function(){
                    var bar = document.getElementById('progress-bar');
                    if (bar) {
                      bar.style.width = '{$percent}%';
                      bar.innerText  = '{$percent}%';
                      bar.classList.add('pulse');
                      setTimeout(function(){ bar.classList.remove('pulse') }, 400);
                    }
                  })();
                </script>";
                if ( function_exists('ob_flush') ) { ob_flush(); }
                flush();
            }
        }
        wp_reset_postdata();
        
        // ── HIDE CURRENT‐PAGE BOX WHEN SHOWING NEXT BATCH ──
        echo '<script>
          (function(){
            var box = document.getElementById("page-name-box");
            if (box) {
              box.style.display = "none";
            }
            if (window.pageNameTimer) {
              clearTimeout(window.pageNameTimer);
            }
          })();
        </script>';
    }

    // ── DEBUG: before merge ──
    blc_debug_log( "Batch {$batch_page}: batch_map count = " . count( $batch_map ) );
    blc_debug_log( "Batch {$batch_page}: full_map before merge = " . count( $full_map ) );

    // ── MERGE THIS BATCH INTO THE FULL MAP ──
    foreach ( $batch_map as $link => $data ) {
        blc_debug_log( "Batch {$batch_page}: processing link: $link" );
        if ( ! isset( $full_map[ $link ] ) ) {
            $full_map[ $link ] = $data;
            blc_debug_log( "Batch {$batch_page}: added link to full_map: $link" );
        } else {
            blc_debug_log( "Batch {$batch_page}: merging into existing link: $link" );
            // merge types
            foreach ( $data['types'] as $t ) {
                if ( ! in_array( $t, $full_map[ $link ]['types'] ) ) {
                    $full_map[ $link ]['types'][] = $t;
                    blc_debug_log( "Batch {$batch_page}: merged type '$t' for $link" );
                }
            }
            // merge pages
            foreach ( $data['pages'] as $pid => $page ) {
                if ( ! isset( $full_map[ $link ]['pages'][ $pid ] ) ) {
                    $full_map[ $link ]['pages'][ $pid ] = $page;
                    blc_debug_log( "Batch {$batch_page}: merged page ID $pid for $link" );
                }
            }
        }
    }

    // ── DEBUG: after merge ──
    blc_debug_log( "Batch {$batch_page}: full_map after merge = " . count( $full_map ) );

    // ── SAVE ACCUMULATED MAP FOR NEXT BATCH ──
    set_transient( $transient_key, $full_map, HOUR_IN_SECONDS );
    set_transient( $template_key,   $template_map, HOUR_IN_SECONDS );

// ── PROGRESS & NEXT BATCH ──
$max_pages  = $query->max_num_pages;
$percentage = round(($batch_page / $max_pages) * 100);

?>
<script>
document.addEventListener('DOMContentLoaded', function() {
    // Smoothly animate the progress bar
    var progressBar = document.getElementById('progress-bar');
    var progressMessage = document.getElementById('progress-message');
    var percent = (function(){
      if (!progressBar) return 0;
      // strip off the “%” and parse as int
      return parseInt(progressBar.style.width, 10) || 0;
    })();
    
    // Update message
    if (percent < 100) {
        progressMessage.innerText = 'Finding and verifying broken links... please be patient.';
        setTimeout(function() {
            document.getElementById('batchForm').submit();
        }, 3000); // 3 second delay between batches
    } else {
        progressMessage.innerText = 'Finished scanning. Preparing results...';
        document.getElementById('progress-container-new').style.display = 'none';
    }
});
</script>
<?php

// Prepare for the next batch
if ( $batch_page < $max_pages ) {
    // ── BLC RESUME CLEANUP: if we didn’t abort, remove stale resume file ──
    if ( ! $blc_abort && file_exists( ABSPATH . 'blc-aborted-index.txt' ) ) {
        unlink( ABSPATH . 'blc-aborted-index.txt' );
        blc_debug_log( "Removed blc-aborted-index.txt after successful batch" );
    }
    
    // ——— SHOW NEXT-BATCH PAGES ———
    $next_page = ( $blc_abort ? $batch_page : $batch_page + 1 );
    $next_args = [
        'post_type'      => ['page','post','fl-theme-layout'],
        'posts_per_page' => $batch_size,
        'paged'          => $next_page,
        'post_status'    => ['publish','draft','private','pending','future'],
        'orderby'        => 'title',
        'order'          => 'ASC',
        'post__not_in'   => $exclude_ids,      // ← skip your excluded pages
    ];
    $next_query = new WP_Query( $next_args );
    if ( $next_query->have_posts() ) {
        echo '<div style="margin:1em 0;padding:0.5em;border:1px solid #ccc;background:#f9f9f9;">';
        $total_batches = (int) $next_query->max_num_pages;
        $resume_file = ABSPATH . 'blc-aborted-index.txt';
        if ( file_exists( $resume_file ) ) {
            echo '<strong>This one has a lot of links and images to check. Still scanning batch (#'
               . esc_html( $next_page ) . '/'
               . esc_html( $total_batches )
               . '):</strong>';
        } else {
            echo '<strong>Next batch (#'
               . esc_html( $next_page ) . '/'
               . esc_html( $total_batches )
               . ') will scan:</strong>';
        }
        echo '<ul style="margin:0.5em 0 0 1em;">';
        while ( $next_query->have_posts() ) {
            $next_query->the_post();
            echo '<li>'
               . esc_html( get_the_title() )
               . ' &mdash; <a href="' . esc_url( get_permalink() ) . '" target="_blank">'
               . esc_url( get_permalink() )
               . '</a></li>';
        }
        echo '</ul></div>';
        wp_reset_postdata();
    }
    // ————————————————————————

    echo '<form id="batchForm" method="post">';
    echo '<input type="hidden" name="check_broken_links" value="1">';
    echo '<input type="hidden" name="batch_page" value="' . $next_page . '">';
    echo '</form>';
    
    // Flush the buffer to ensure the browser receives the progress update
    if ( function_exists('fastcgi_finish_request') ) {
        fastcgi_finish_request();
    } else {
        @flush();
        @ob_flush();
    }
    exit;
}

    if ( $batch_page < $max_pages ) {
        // ── BLC RESUME CLEANUP: if we didn’t abort, remove stale resume file ──
        if ( ! $blc_abort && file_exists( ABSPATH . 'blc-aborted-index.txt' ) ) {
            unlink( ABSPATH . 'blc-aborted-index.txt' );
            blc_debug_log( "Removed blc-aborted-index.txt after successful batch" );
        }

        // ——— SHOW NEXT-BATCH PAGES ———
        $next_page = ( $blc_abort ? $batch_page : $batch_page + 1 );
        $next_args = [
            'post_type'      => ['page','post','fl-theme-layout'],
            'posts_per_page' => $batch_size,
            'paged'          => $next_page,
            'post_status'    => ['publish','draft','private','pending','future'],
            'orderby'        => 'title',
            'order'          => 'ASC',
            'post__not_in'   => $exclude_ids,      // ← skip your excluded pages
        ];
        $next_query = new WP_Query( $next_args );
        if ( $next_query->have_posts() ) {
            echo '<div style="margin:1em 0;padding:0.5em;border:1px solid #ccc;background:#f9f9f9;">';
            $total_batches = (int) $next_query->max_num_pages;
            $resume_file = ABSPATH . 'blc-aborted-index.txt';
            if ( file_exists( $resume_file ) ) {
                echo '<strong>This one has a lot of links and images to check. Still scanning batch (#'
                   . esc_html( $next_page ) . '/'
                   . esc_html( $total_batches )
                   . '):</strong>';
            } else {
                echo '<strong>Next batch (#'
                   . esc_html( $next_page ) . '/'
                   . esc_html( $total_batches )
                   . ') will scan:</strong>';
            }
            echo '<ul style="margin:0.5em 0 0 1em;">';
            while ( $next_query->have_posts() ) {
                $next_query->the_post();
                echo '<li>'
                   . esc_html( get_the_title() )
                   . ' &mdash; <a href="' . esc_url( get_permalink() ) . '" target="_blank">'
                   . esc_url( get_permalink() )
                   . '</a></li>';
            }
            echo '</ul></div>';
            wp_reset_postdata();
        }
        // ————————————————————————

        echo '<form id="batchForm" method="post">';
        echo '<input type="hidden" name="check_broken_links" value="1">';
        echo '<input type="hidden" name="batch_page" value="' . $next_page . '">';
        echo '</form>';
        echo '<script>document.getElementById("batchForm").submit();</script>';
        
        // ensure output is sent, then terminate cleanly
        if ( function_exists('fastcgi_finish_request') ) {
            fastcgi_finish_request();
        } else {
            @flush();
            @ob_flush();
        }
        exit;
    }
    
// === SCAN THE wp_options THEME MODS ===
$theme_mods   = get_option( 'theme_mods_bb-theme-child' );
if ( $theme_mods ) {
    $sections      = [
        'fl-head-code'   => 'Head Code',
        'fl-header-code' => 'Header Code',
        'fl-footer-code' => 'Footer Code',
    ];
    $home_url      = home_url( '/' );
    $customizer_url = admin_url( 'customize.php?url=' . urlencode( $home_url ) );

    foreach ( $sections as $mod_key => $section_label ) {
        if ( empty( $theme_mods[ $mod_key ] ) ) {
            continue;
        }
        $content = $theme_mods[ $mod_key ];

        // find any src="…" or src='…'
        preg_match_all( '/src=["\']?([^"\'\s>]+)["\']?/i', $content, $matches );
        if ( ! empty( $matches[1] ) ) {
            foreach ( $matches[1] as $link ) {
                if ( stripos( $link, 'http://' ) !== 0 && stripos( $link, 'https://' ) !== 0 && stripos( $link, 'ns' ) !== 0 ) {
                    continue;
                }
                $resolved = resolve_internal_links( $link );
                $actual   = normalize_url( $resolved );
                if ( is_link_approved( $actual, $approved_links ) ) {
                    continue;
                }

                // initial GET check
                $status = check_all_link_status( $actual );
                if ( in_array( $status['code'], [404, 403, 0], true ) ) {
                    // double‐check with HEAD
                    $head_args = [
                        'method'     => 'HEAD',
                        'timeout'    => 15,
                        'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' .
                                        '(KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36',
                    ];
                    $head_resp = wp_remote_request( $actual, $head_args );
                    if ( ! is_wp_error( $head_resp ) ) {
                        $head_code = wp_remote_retrieve_response_code( $head_resp );
                        if ( $head_code >= 200 && $head_code < 400 ) {
                            continue;
                        }
                    }

                    // record into this batch’s map
                    if ( ! isset( $full_map[ $actual ] ) ) {
                        $full_map[ $actual ] = [
                            'display_link' => $actual,
                            'status'       => $status['code'],
                            'pages'        => [],
                            'types'        => [ $section_label ],
                        ];
                    } elseif ( ! in_array( $section_label, $full_map[ $actual ]['types'], true ) ) {
                        $full_map[ $actual ]['types'][] = $section_label;
                    }

                    // give each entry a unique “page” key
                    $page_key = 'mod-' . $mod_key;
                    if ( ! isset( $full_map[ $actual ]['pages'][ $page_key ] ) ) {
                        $full_map[ $actual ]['pages'][ $page_key ] = [
                            'title' => $section_label,
                            'url'   => $customizer_url,
                        ];
                    }
                }
            }
        }

        // Special GTM marker check
        if ( strpos( $content, '**GOOGLETAGMANAGER**' ) !== false ) {
            $gtm_marker = 'https://www.googletagmanager.com/ns.html?id=**GOOGLETAGMANAGER**';
            blc_debug_log("GTM marker found in {$section_label}, adding to full_map → $gtm_marker");
        
            if ( ! is_link_approved( $gtm_marker, $approved_links ) ) {
                if ( ! isset( $full_map[ $gtm_marker ] ) ) {
                    $full_map[ $gtm_marker ] = [
                        'display_link' => $gtm_marker,
                        'status'       => 404,
                        'pages'        => [],
                        'types'        => [ $section_label ],
                    ];
                } elseif ( ! in_array( $section_label, $full_map[ $gtm_marker ]['types'], true ) ) {
                    $full_map[ $gtm_marker ]['types'][] = $section_label;
                }

                // Give it a unique "page" key to avoid overwrites
                $page_key = 'mod-' . $mod_key . '-gtm';
                if ( ! isset( $full_map[ $gtm_marker ]['pages'][ $page_key ] ) ) {
                    $full_map[ $gtm_marker ]['pages'][ $page_key ] = [
                        'title' => $section_label . ' (GTM Marker)',
                        'url'   => $customizer_url,
                    ];
                }
            }
        }
    }
}

// ── FINAL BATCH: CLEAN UP & RENDER ──
delete_transient($transient_key);
$broken_links_map = $full_map;

// pull and delete your template‐image map
$template_images_map = get_transient( $template_key );
delete_transient(      $template_key );

// ── 4b) CHECK TEMPLATE MEDIA FOR ACTUAL BROKEN STATUS ──
if ( ! empty( $template_images_map ) ) {
    foreach ( $template_images_map as $url => $info ) {

        // Skip if already flagged
        if ( isset( $broken_links_map[ $url ] ) ) {
            continue;
        }

        // Detect video vs image
        $is_video = (bool) preg_match( '/\.(?:mp4|webm|ogg)(?:$|\?)/i', $url );

        // WP HTTP args: follow redirects, fake browser UA
        $http_args = [
            'timeout'     => 15,
            'redirection' => 5,
            'sslverify'   => false,
            'user-agent'  => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
        ];

        // 1) Try a GET
        $resp = wp_remote_get( $url, $http_args );
        if ( is_wp_error( $resp ) ) {
            // Retry GET once more
            $resp = wp_remote_get( $url, $http_args );
        }
        $code = is_wp_error( $resp )
              ? 0
              : wp_remote_retrieve_response_code( $resp );

        // 2) If still not 200–299, HEAD‑retry
        if ( $code < 200 || $code > 299 ) {
            $head = wp_remote_request( $url, array_merge( $http_args, ['method'=>'HEAD'] ) );
            if ( ! is_wp_error( $head ) ) {
                $h_code = wp_remote_retrieve_response_code( $head );
                // treat HEAD 200–299 as OK
                if ( $h_code >= 200 && $h_code < 300 ) {
                    continue;
                }
            }
            $code = is_wp_error( $head ) ? $code : $h_code;
        }

        // 3) Only flag 404 or 410
        if ( in_array( $code, [404, 410], true ) ) {
            $broken_links_map[ $url ] = [
                'display_link' => $url,
                'status'       => $code,
                'pages'        => $info['pages'],
                'types'        => [ $is_video ? 'Master Site Video' : 'Master Site Image' ],
            ];
        }
    }
}

}

// Process approval if the "Approve Selected Links" button was pressed.
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['approve_links_submit']) && !empty($_POST['approved_links'])) {
    $approved_links_input = $_POST['approved_links'];
    foreach ($approved_links_input as $approved_link) {
        // Before inserting, check if the link already exists.
        $exists = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$wpdb->prefix}approved_links WHERE link = %s",
                $approved_link
            )
        );
        if (!$exists) {
            $wpdb->insert(
                "{$wpdb->prefix}approved_links",
                array(
                    'link'       => sanitize_text_field($approved_link),
                    'created_at' => current_time('mysql'),
                ),
                array('%s', '%s')
            );
        }
    }
    echo "Approved " . count($approved_links_input) . " link(s).";
    exit;
}
?>

<div class="wrap">
    <h1>Inception Broken Link Checker</h1>
    <p><strong>Reminder:</strong> Checking all links, images, and iframes may take several minutes depending on the links on the site. <strong>Please be patient!</strong></p>

    <?php
        $is_get        = ( 'GET' === $_SERVER['REQUEST_METHOD'] );
        $progress_file = ABSPATH . 'blc-progress.txt';
    ?>
    <form id="checkForm" method="post">
        <?php if ( $is_get && file_exists( $progress_file ) ) : ?>
            <h3 style="margin-bottom:0.5em;">Looks like your previous check timed out somewhere on one of these pages below - likely a page with a bunch of external links/images.</h3>
            <h3 style="margin-bottom:0.5em;">Click to skip checking any of the following pages:</h3>
            <?php
                $paths = file(
                    $progress_file,
                    FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES
                );
                foreach ( $paths as $slug ) {
                    $page = get_page_by_path( $slug, OBJECT, ['page','post','fl-theme-layout'] );
                    if ( ! $page ) {
                        continue;
                    }
                    printf(
                        '<label style="display:block; margin:2px 0;">'
                      .     '<input type="checkbox" name="exclude_slugs[]" value="%1$s"> %2$s'
                      .  '</label>',
                        esc_attr( $slug ),
                        esc_html( get_the_title( $page->ID ) )
                    );
                }
            ?>
            <h3 style="margin-bottom:0.5em;">If you skip checking a page, make sure to note it in the project, and manually check the page thoroughly!</h3>
            <br>
        <?php endif; ?>
        <input
            type="submit"
            name="check_broken_links"
            value="<?php echo ( $is_get && file_exists( $progress_file ) )
                ? 'Check For Broken Links Again'
                : 'Check For Broken Links'; ?>"
            class="button button-primary"
        >
    </form>

    <!-- Loading GIF container -->
    <?php
    $loading_style = ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['check_broken_links'])) ? 'display:block;' : 'display:none;';
    ?>
    <div id="loadingGif" style="<?php echo $loading_style; ?> margin-top:20px;">
        <p>
            <img src="<?php echo plugins_url('../search-and-replace/working-on-it.gif', __FILE__); ?>" alt="Working on it" style="width: 400px;" />
        </p>
    </div>

    <?php if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['check_broken_links'])) : ?>
        <!-- Results Container: always output this container -->
        <div id="resultsContainer">
            <?php
                // ── DOUBLE-CHECK EACH “BROKEN” LINK VIA GET ──
                foreach ( $broken_links_map as $link => $info ) {
                
                    // ── Builder: always treat semicolon-prefixed inceptionimages.com images as broken ──
                    if (
                        isset( $info['display_link'] )
                        && 0 === strpos( $info['display_link'], ';' )
                        && stripos( parse_url( $link, PHP_URL_HOST ), 'inceptionimages.com' ) !== false
                    ) {
                        blc_debug_log( "Builder: skipping recheck for semicolon-prefixed inceptionimages.com image → $link" );
                        continue;
                    }
                
                    $recheck = check_all_link_status( $link );
                    // if it now returns a “good” HTTP status, drop it
                    if ( ! in_array( $recheck['code'], [404, 403, 502, 0], true ) ) {
                        unset( $broken_links_map[ $link ] );
                    }
                }
            ?>

<?php
// Debug: log exactly what we're about to render
blc_debug_log( 'Rendering table with keys: ' . print_r( array_keys( $broken_links_map ), true ) );
blc_debug_log( 'Rendering table count: ' . count( $broken_links_map ) );
?>

<?php
// ── DELETE DEBUG LOG, COMMENT OUT THESE LINES TO KEEP THE DEBUG LOG ──
$debug_log = WP_CONTENT_DIR . '/blc-debug.log';
if ( file_exists( $debug_log ) ) {
    @unlink( $debug_log );
}

// ── BLC PROGRESS: finished, clean up ──
$progress_file = ABSPATH . 'blc-progress.txt';
if ( file_exists( $progress_file ) ) {
    unlink( $progress_file );
}
$exclude_file = ABSPATH . 'blc-exclude.txt';
if ( file_exists( $exclude_file ) ) {
    unlink( $exclude_file );
}

// ── BEGIN RENDERING RESULTS TABLE ──
if ( ! empty( $broken_links_map ) ) :
?>
    <p><strong>Broken Items Found:</strong> Select the items you wish to approve and then click "Approve Selected Links".</p>
                
    <!-- Approval form that submits to a hidden iframe -->
    <form id="approveForm" method="post" target="dummyframe">
        <table class="wp-list-table widefat fixed striped">
            <thead>
                <tr>
                    <th style="width:40px;">#</th>
                    <th>Page Titles</th>
                    <th>Page URLs</th>
                    <th>Broken URL</th>
                    <th>Type</th>
                    <th style="width:40px;">
                        <input type="checkbox" id="select-all" style="margin-left: 0px;">
                    </th>                        
                </tr>
            </thead>
            <tbody>
                <?php
                $count = 0;
                foreach ( $broken_links_map as $actual_link => $data ) {
                    $count++;
                    $display_link    = $data['display_link'];
                    // Page titles & URLs
                    if ( count( $data['pages'] ) > 1 ) {
                        $pages_titles_html = '<ul style="list-style-type: disc; margin:0; padding-left:20px;">';
                        $pages_urls_html   = '<ul style="list-style-type: disc; margin:0; padding-left:20px;">';
                        foreach ( $data['pages'] as $page ) {
                            $pages_titles_html .= '<li>' . esc_html( $page['title'] ) . '</li>';
                            $pages_urls_html   .= '<li><a href="' . esc_attr( $page['url'] ) . '" target="_blank">' . esc_html( $page['url'] ) . '</a></li>';
                        }
                        $pages_titles_html .= '</ul>';
                        $pages_urls_html   .= '</ul>';
                    } else {
                        $page               = reset( $data['pages'] );
                        $pages_titles_html  = esc_html( $page['title'] );
                        $pages_urls_html    = '<a href="' . esc_attr( $page['url'] ) . '" target="_blank">' . esc_html( $page['url'] ) . '</a>';
                    }
                    // Link output
                    if ( 0 === strpos( $display_link, 'http' ) ) {
                        $link_output = '<a href="' . esc_url( $display_link ) . '" target="_blank">' . esc_html( $display_link ) . '</a>';
                    } else {
                        $link_output = esc_html( $display_link );
                    }
                    $types_html = implode( ', ', $data['types'] );
                    echo '<tr data-link="' . esc_attr( $display_link ) . '">';
                    echo '<td style="width:40px;">' . $count . '</td>';
                    echo '<td>' . $pages_titles_html . '</td>';
                    echo '<td>' . $pages_urls_html . '</td>';
                    echo '<td>' . $link_output . '</td>';
                    echo '<td>' . esc_html( $types_html ) . '</td>';
                    echo '<td style="width:40px;"><input type="checkbox" name="approved_links[]" value="' . esc_attr( $display_link ) . '"></td>';
                    echo '</tr>';
                }
                ?>
            </tbody>
        </table>
        <p>
            <input type="submit" name="approve_links_submit" value="Approve Selected Links" class="button button-primary">
        </p>
    </form>
    <div id="approvalMessage" style="display:none; margin-top: 10px; color: green;"></div>
<?php else: ?>
    <?php blc_debug_log( 'No broken links to render in table.' ); ?>
    <p>No broken links found after final verification.</p>
<?php endif; ?>

        </div>
        <!-- Dancing GIF container: always output this (hidden by default if broken links exist) -->
        <div id="dancingGif" style="display:<?php echo empty($broken_links_map) ? 'block' : 'none'; ?>; margin-top:20px;">
            <p><strong>No additional broken links found!</strong></p>
            <img src="<?php echo plugins_url('dancing.gif', __FILE__); ?>" alt="Dancing" style="width: 400px;" />
        </div>
        
        <!-- Clear Approved Links Button -->
        <form id="clearApprovedForm" method="post" style="margin-top:20px;">
            <input type="submit" name="clear_approved_links" value="Clear Approved Links" class="button button-secondary" onclick="return confirm('Are you sure you want to clear all approved links?');">
        </form>
    <?php endif; ?>
</div>

<!-- Commented Out Media Table for Troubleshooting - Remove Lines Top & Bottom Lines with * To View -->
<?php /*
<?php if ( ! empty( $template_images_map ) ) : ?>
    <h2>Images on templates.inception-example.com or inceptionimages.com or chirobump.com</h2>
    <table class="widefat">
        <thead>
            <tr>
                <th>#</th>
                <th>Page Title</th>
                <th>Page URL</th>
                <th>Image URL</th>
                <th>Preview</th>
            </tr>
        </thead>
        <tbody>
            <?php
            $i = 0;
            foreach ( $template_images_map as $img_url => $info ) {
                $i++;
                $page = reset( $info['pages'] );
                ?>
                <tr>
                    <td><?php echo esc_html( $i ); ?></td>
                    <td><?php echo esc_html( $page['title'] ); ?></td>
                    <td>
                        <a href="<?php echo esc_url( $page['url'] ); ?>" target="_blank">
                            <?php echo esc_html( $page['url'] ); ?>
                        </a>
                    </td>
                    <td>
                        <a href="<?php echo esc_url( $info['display_link'] ); ?>" target="_blank">
                            <?php echo esc_html( $info['display_link'] ); ?>
                        </a>
                    </td>
                    <td>
                      <?php if ( preg_match( '/\.(?:mp4|webm|ogg)(?:$|\?)/i', $info['display_link'] ) ) : ?>
                        <video style="max-width:200px; height:auto;" controls>
                          <source src="<?php echo esc_url( $info['display_link'] ); ?>">
                        </video>
                      <?php else: ?>
                        <img
                          src="<?php echo esc_url( $info['display_link'] ); ?>"
                          style="max-width:200px; height:auto;"
                          alt=""
                        />
                      <?php endif; ?>
                    </td>
                </tr>
            <?php } ?>
        </tbody>
    </table>
<?php endif; ?>
*/ ?>

<!-- Hidden iframe for form submission -->
<iframe name="dummyframe" id="dummyframe" style="display:none;"></iframe>

<!-- jQuery for fade in/out effects and handling form submission -->
<script type="text/javascript">
jQuery(document).ready(function($) {
    // When the check form is submitted, hide its button and show the loading GIF.
    $('#checkForm').on('submit', function() {
        $(this).find('input[name="check_broken_links"]').hide();
        $('#loadingGif').fadeIn(500);
    });
    
    // When the page loads and the results table is present, hide the loading GIF and show the check button.
    if ($('.wp-list-table').length) {
        $('#loadingGif').hide();
        $('#checkForm').find('input[name="check_broken_links"]').show();
    }
    
    // If the dancing GIF container is visible on load (i.e. no broken links), hide the loading GIF.
    if ($('#dancingGif').is(':visible')) {
        $('#loadingGif').fadeOut(500);
    }
    
    // Function to update row numbers.
    function updateRowNumbers() {
        $('.wp-list-table tbody tr').each(function(index) {
            $(this).find('td:first').text(index + 1);
        });
    }
    
    // When approving links, fade out and remove the approved rows, then update row numbers.
    $('#approveForm').on('submit', function(e) {
        $('#loadingGif').fadeIn(500);
        $(this).find('input[type=checkbox]:checked').each(function() {
            $(this).closest('tr').fadeOut(500, function() {
                $(this).remove();
                updateRowNumbers();
                // If no rows remain in the table body, hide the results container and show the dancing GIF.
                if ($('.wp-list-table tbody tr').length === 0) {
                    $('#resultsContainer').fadeOut(500);
                    $('#dancingGif').fadeIn(500);
                }
            });
        });
    });
    
    $('#dummyframe').on('load', function() {
        var response = $(this).contents().find('body').text();
        $('#loadingGif').delay(500).fadeOut(500);
    });
    
    // Handler for the "select all" checkbox.
    $('#select-all').on('change', function() {
        $('input[name="approved_links[]"]').prop('checked', this.checked);
    });
});
</script>

<script>
    let lastChecked = null;
    const checkboxes = document.querySelectorAll('input[type="checkbox"][name="approved_links[]"]');
    checkboxes.forEach((checkbox) => {
        checkbox.addEventListener("click", function (event) {
            if (lastChecked && event.shiftKey) {
                let start = Array.from(checkboxes).indexOf(lastChecked);
                let end = Array.from(checkboxes).indexOf(this);
                let range = [start, end].sort((a, b) => a - b);
                for (let i = range[0]; i <= range[1]; i++) {
                    checkboxes[i].checked = lastChecked.checked;
                }
            }
            lastChecked = this;
        });
    });
    const selectAll = document.getElementById("select-all");
    if (selectAll) {
        selectAll.addEventListener("click", function () {
            checkboxes.forEach((checkbox) => {
                checkbox.checked = selectAll.checked;
            });
        });
    }
</script>
