<?php
/**
 * DBA_Anti_Orphan_Scanner — Site-wide Orphan Page Detection Engine
 *
 * ═══════════════════════════════════════════════════════════════════════
 *  v2.19.0 — MULTI-SOURCE LINK DETECTION:
 *    In addition to post_content links, the scanner now also parses:
 *      1. WordPress nav menus (all registered menu locations + unassigned menus)
 *      2. Sidebar & footer widget areas (text, custom HTML, nav menu widgets)
 *      3. Reusable blocks / synced patterns (wp_block post type)
 *
 *  WPML AWARE:
 *    - Scans each language independently
 *    - Only counts links FROM same-language pages
 *
 *  STOPPABLE:
 *    - Checks a transient flag each iteration so the admin can abort mid-scan
 *
 * @since 2.19.0
 */

if ( ! defined( 'ABSPATH' ) ) exit;

class DBA_Anti_Orphan_Scanner {

    const RESULTS_KEY  = 'dba_anti_orphan_results';
    const LINK_MAP_KEY = 'dba_anti_orphan_link_map';
    const PROGRESS_KEY = 'dba_anti_orphan_progress';
    const SETTINGS_KEY = 'dba_anti_orphan_settings';
    const STOP_FLAG_KEY = 'dba_anti_orphan_stop';
    const BATCH_SIZE   = 500;
    const SITE_DOMAIN  = 'damalion.com';

    /* ══════════════════════════════════════════════════════════════
     *  STOP / ABORT SUPPORT
     * ══════════════════════════════════════════════════════════════ */

    public static function request_stop() {
        set_transient( self::STOP_FLAG_KEY, 'stop', 600 );
    }

    public static function clear_stop_flag() {
        delete_transient( self::STOP_FLAG_KEY );
    }

    public static function should_stop() {
        return get_transient( self::STOP_FLAG_KEY ) === 'stop';
    }

    /* ══════════════════════════════════════════════════════════════
     *  MAIN SCAN
     * ══════════════════════════════════════════════════════════════ */

    public static function run_scan( $language = '' ) {
        $start = microtime( true );

        self::clear_stop_flag();
        self::update_progress( 'Initializing scan...', 0, 'running' );

        $languages = self::get_active_languages();
        if ( ! empty( $language ) ) {
            $languages = array( $language );
        }

        error_log( 'DBA Anti-Orphan: Scanning ' . count( $languages ) . ' languages: ' . implode( ', ', $languages ) );

        $all_results = array();
        $global_stats = array(
            'total_pages'   => 0,
            'total_orphans' => 0,
            'total_weak'    => 0,
            'total_linked'  => 0,
            'by_language'   => array(),
            'scan_time'     => 0,
            'scan_date'     => current_time( 'mysql' ),
            'structural_links' => 0,
        );

        $lang_count = count( $languages );
        $lang_idx   = 0;
        $errors     = array();
        $stopped    = false;
        $pct        = 0;

        /* ── Phase 1: Collect structural links (menus + widgets + reusable blocks) ── */
        self::update_progress( 'Collecting nav menu links (SQL + API)...', 2, 'running' );
        $nav_links = self::collect_nav_menu_links();

        if ( self::should_stop() ) {
            self::update_progress( 'Stopped by user.', 0, 'stopped' );
            return array( 'stopped' => true );
        }

        self::update_progress( 'Collecting widget & footer links...', 4, 'running' );
        $widget_links = self::collect_widget_links();

        if ( self::should_stop() ) {
            self::update_progress( 'Stopped by user.', 0, 'stopped' );
            return array( 'stopped' => true );
        }

        self::update_progress( 'Collecting reusable block links...', 6, 'running' );
        $reusable_links = self::collect_reusable_block_links();

        $structural_links = array_unique( array_merge( $nav_links, $widget_links, $reusable_links ) );
        $global_stats['structural_links'] = count( $structural_links );
        $global_stats['structural_detail'] = array(
            'nav_menus'       => count( $nav_links ),
            'widgets'         => count( $widget_links ),
            'reusable_blocks' => count( $reusable_links ),
            'unique_total'    => count( $structural_links ),
            'sample_urls'     => array_slice( $structural_links, 0, 10 ),
        );

        error_log( 'DBA Anti-Orphan: Structural links found — '
            . count( $nav_links ) . ' nav menus, '
            . count( $widget_links ) . ' widgets, '
            . count( $reusable_links ) . ' reusable blocks ('
            . count( $structural_links ) . ' unique total).' );

        if ( ! empty( $structural_links ) ) {
            error_log( 'DBA Anti-Orphan: Sample structural URLs: ' . implode( ', ', array_slice( $structural_links, 0, 5 ) ) );
        }

        /* ── Phase 2: Scan each language ── */
        foreach ( $languages as $lang ) {
            if ( self::should_stop() ) {
                $stopped = true;
                break;
            }

            $lang_idx++;
            $pct = 8 + intval( ( $lang_idx / $lang_count ) * 88 );
            self::update_progress( "Scanning language: {$lang} ({$lang_idx}/{$lang_count})", $pct, 'running' );

            try {
                $result = self::scan_language( $lang, $structural_links );
                $all_results[ $lang ] = $result;

                $linked = max( 0, $result['total'] - $result['orphan_count'] - $result['weak_count'] );
                $global_stats['total_pages']   += $result['total'];
                $global_stats['total_orphans'] += $result['orphan_count'];
                $global_stats['total_weak']    += $result['weak_count'];
                $global_stats['total_linked']  += $linked;
                $global_stats['by_language'][ $lang ] = array(
                    'total'         => $result['total'],
                    'orphans'       => $result['orphan_count'],
                    'weak'          => $result['weak_count'],
                    'linked'        => $linked,
                    'language_name' => $result['language_name'],
                );
            } catch ( \Throwable $e ) {
                $err_msg = 'Language ' . $lang . ': ' . $e->getMessage() . ' in ' . basename( $e->getFile() ) . ':' . $e->getLine();
                $errors[] = $err_msg;
                error_log( 'DBA Anti-Orphan ERROR: ' . $err_msg );

                $global_stats['by_language'][ $lang ] = array(
                    'total'         => 0,
                    'orphans'       => 0,
                    'weak'          => 0,
                    'linked'        => 0,
                    'language_name' => self::get_language_name( $lang ),
                    'error'         => $err_msg,
                );
            }
        }

        $global_stats['errors']  = $errors;
        $global_stats['stopped'] = $stopped;
        $global_stats['scan_time'] = round( microtime( true ) - $start, 2 );

        update_option( self::RESULTS_KEY, array(
            'stats'   => $global_stats,
            'details' => $all_results,
        ), false );

        $final_state = $stopped ? 'stopped' : 'done';
        $final_msg   = $stopped
            ? 'Scan stopped by user after ' . $global_stats['scan_time'] . 's.'
            : 'Scan complete!';

        self::update_progress( $final_msg, $stopped ? $pct : 100, $final_state );

        error_log( 'DBA Anti-Orphan: Scan ' . $final_state . ' in ' . $global_stats['scan_time'] . 's — '
            . $global_stats['total_pages'] . ' pages, '
            . $global_stats['total_orphans'] . ' orphans, '
            . $global_stats['total_weak'] . ' weak' );

        return $global_stats;
    }

    /* ══════════════════════════════════════════════════════════════
     *  STRUCTURAL LINK COLLECTION — Nav Menus
     * ══════════════════════════════════════════════════════════════ */

    public static function collect_nav_menu_links() {
        global $wpdb;
        $urls = array();

        // ── METHOD 1: Direct SQL on wp_postmeta — bulletproof, bypasses WPML filters ──

        // 1a. Custom URL menu items (_menu_item_type = 'custom')
        $custom_urls = $wpdb->get_col(
            "SELECT pm_url.meta_value
             FROM {$wpdb->postmeta} pm_url
             INNER JOIN {$wpdb->posts} p ON p.ID = pm_url.post_id
             WHERE pm_url.meta_key = '_menu_item_url'
               AND pm_url.meta_value != ''
               AND p.post_type = 'nav_menu_item'
               AND p.post_status = 'publish'"
        );

        if ( ! empty( $custom_urls ) ) {
            foreach ( $custom_urls as $url ) {
                if ( self::is_internal_link( $url ) ) {
                    $clean = self::normalize_url( $url );
                    if ( $clean ) $urls[] = $clean;
                }
            }
        }

        error_log( 'DBA Anti-Orphan: Nav menu custom URLs found (SQL): ' . count( $custom_urls ) );

        // 1b. Post/page menu items (_menu_item_type = 'post_type') — resolve object_id to permalink
        $object_ids = $wpdb->get_col(
            "SELECT pm_obj.meta_value
             FROM {$wpdb->postmeta} pm_obj
             INNER JOIN {$wpdb->postmeta} pm_type ON pm_type.post_id = pm_obj.post_id
             INNER JOIN {$wpdb->posts} p ON p.ID = pm_obj.post_id
             WHERE pm_obj.meta_key = '_menu_item_object_id'
               AND pm_type.meta_key = '_menu_item_type'
               AND pm_type.meta_value IN ('post_type', 'post_type_archive')
               AND p.post_type = 'nav_menu_item'
               AND p.post_status = 'publish'"
        );

        if ( ! empty( $object_ids ) ) {
            foreach ( $object_ids as $obj_id ) {
                $obj_id = intval( $obj_id );
                if ( ! $obj_id ) continue;
                $url = get_permalink( $obj_id );
                if ( $url && self::is_internal_link( $url ) ) {
                    $clean = self::normalize_url( $url );
                    if ( $clean ) $urls[] = $clean;
                }
            }
        }

        error_log( 'DBA Anti-Orphan: Nav menu object IDs found (SQL): ' . count( $object_ids ) );

        // ── METHOD 2: WP API as fallback — in case SQL missed something ──
        $menu_ids_done = array();

        // Try get_nav_menu_locations (may be empty with WPML)
        $locations = get_nav_menu_locations();
        if ( ! empty( $locations ) ) {
            foreach ( $locations as $location => $menu_id ) {
                if ( ! $menu_id || isset( $menu_ids_done[ $menu_id ] ) ) continue;
                $menu_ids_done[ $menu_id ] = true;

                $items = wp_get_nav_menu_items( $menu_id );
                if ( ! is_array( $items ) ) continue;

                foreach ( $items as $item ) {
                    if ( ! empty( $item->url ) && self::is_internal_link( $item->url ) ) {
                        $clean = self::normalize_url( $item->url );
                        if ( $clean ) $urls[] = $clean;
                    }
                }
            }
        }

        // Also try all menus
        $all_menus = wp_get_nav_menus();
        if ( is_array( $all_menus ) ) {
            foreach ( $all_menus as $menu ) {
                if ( isset( $menu_ids_done[ $menu->term_id ] ) ) continue;
                $menu_ids_done[ $menu->term_id ] = true;

                $items = wp_get_nav_menu_items( $menu->term_id );
                if ( ! is_array( $items ) ) continue;

                foreach ( $items as $item ) {
                    if ( ! empty( $item->url ) && self::is_internal_link( $item->url ) ) {
                        $clean = self::normalize_url( $item->url );
                        if ( $clean ) $urls[] = $clean;
                    }
                }
            }
        }

        $unique = array_unique( $urls );
        error_log( 'DBA Anti-Orphan: Total unique nav menu internal URLs: ' . count( $unique ) );
        return $unique;
    }

    /* ══════════════════════════════════════════════════════════════
     *  STRUCTURAL LINK COLLECTION — Widgets
     * ══════════════════════════════════════════════════════════════ */

    public static function collect_widget_links() {
        $urls = array();

        $sidebars_widgets = wp_get_sidebars_widgets();
        if ( ! is_array( $sidebars_widgets ) ) return $urls;

        foreach ( $sidebars_widgets as $sidebar_id => $widget_ids ) {
            if ( $sidebar_id === 'wp_inactive_widgets' || empty( $widget_ids ) ) continue;

            foreach ( $widget_ids as $widget_id ) {
                $urls = array_merge( $urls, self::extract_links_from_widget( $widget_id ) );
            }
        }

        return array_unique( $urls );
    }

    private static function extract_links_from_widget( $widget_id ) {
        $urls = array();

        if ( ! preg_match( '/^(.+)-(\d+)$/', $widget_id, $m ) ) return $urls;
        $type = $m[1];
        $num  = (int) $m[2];

        $instances = get_option( 'widget_' . $type, array() );
        if ( ! isset( $instances[ $num ] ) || ! is_array( $instances[ $num ] ) ) return $urls;

        $instance = $instances[ $num ];

        // Nav Menu widget → follow the menu
        if ( $type === 'nav_menu' && ! empty( $instance['nav_menu'] ) ) {
            $items = wp_get_nav_menu_items( (int) $instance['nav_menu'] );
            if ( is_array( $items ) ) {
                foreach ( $items as $item ) {
                    if ( ! empty( $item->url ) && self::is_internal_link( $item->url ) ) {
                        $clean = self::normalize_url( $item->url );
                        if ( $clean ) $urls[] = $clean;
                    }
                }
            }
            return $urls;
        }

        // Text / Custom HTML / Block widgets → parse HTML
        foreach ( array( 'text', 'content', 'filter' ) as $field ) {
            if ( ! empty( $instance[ $field ] ) && is_string( $instance[ $field ] ) ) {
                $urls = array_merge( $urls, self::extract_internal_links( $instance[ $field ] ) );
            }
        }

        return $urls;
    }

    /* ══════════════════════════════════════════════════════════════
     *  STRUCTURAL LINK COLLECTION — Reusable Blocks / Patterns
     * ══════════════════════════════════════════════════════════════ */

    public static function collect_reusable_block_links() {
        global $wpdb;
        $urls = array();

        $blocks = $wpdb->get_results(
            "SELECT post_content FROM {$wpdb->posts}
             WHERE post_type = 'wp_block'
               AND post_status = 'publish'
               AND post_content LIKE '%href%'"
        );

        if ( ! empty( $blocks ) ) {
            foreach ( $blocks as $block ) {
                $urls = array_merge( $urls, self::extract_internal_links( $block->post_content ) );
            }
        }

        return array_unique( $urls );
    }

    /* ══════════════════════════════════════════════════════════════
     *  PER-LANGUAGE SCAN
     * ══════════════════════════════════════════════════════════════ */

    public static function scan_language( $lang, $structural_links = array() ) {
        global $wpdb;

        $wpml_active = defined( 'ICL_SITEPRESS_VERSION' ) || function_exists( 'icl_get_languages' );

        $posts = self::get_posts_for_language( $lang, $wpml_active );

        if ( empty( $posts ) ) {
            return array(
                'total' => 0, 'orphans' => array(), 'weak' => array(),
                'orphan_count' => 0, 'weak_count' => 0, 'link_map' => array(),
                'language_name' => self::get_language_name( $lang ),
            );
        }

        // Build URL → post_id lookup
        $url_to_id  = array();
        $id_to_info = array();
        foreach ( $posts as $p ) {
            $url = get_permalink( $p->ID );
            if ( ! $url ) continue;
            $url_clean = strtolower( rtrim( $url, '/' ) );
            $url_to_id[ $url_clean ] = $p->ID;
            $id_to_info[ $p->ID ] = array(
                'id'             => $p->ID,
                'title'          => $p->post_title,
                'url'            => $url,
                'type'           => $p->post_type,
                'date'           => $p->post_date,
                'language'       => $lang,
                'inbound'        => array(),
                'inbound_count'  => 0,
                'in_nav'         => false,
            );
        }

        // ── Credit structural links (menus, widgets, reusable blocks) ──
        foreach ( $structural_links as $struct_url ) {
            $struct_clean = strtolower( rtrim( $struct_url, '/' ) );
            if ( isset( $url_to_id[ $struct_clean ] ) ) {
                $target_id = $url_to_id[ $struct_clean ];
                $id_to_info[ $target_id ]['inbound'][] = array(
                    'from_id'    => 0,
                    'from_title' => '🧭 Nav Menu / Widget / Reusable Block',
                    'from_url'   => '',
                );
                $id_to_info[ $target_id ]['inbound_count']++;
                $id_to_info[ $target_id ]['in_nav'] = true;
            }
        }

        // ── Count links from post_content ──
        foreach ( $posts as $p ) {
            if ( self::should_stop() ) break;

            $source_url = get_permalink( $p->ID );
            if ( ! $source_url ) continue;

            $internal_links = self::extract_internal_links( $p->post_content );

            foreach ( $internal_links as $link_url ) {
                $link_clean = strtolower( rtrim( $link_url, '/' ) );

                if ( isset( $url_to_id[ $link_clean ] ) ) {
                    $target_id = $url_to_id[ $link_clean ];
                    if ( $target_id !== $p->ID ) {
                        $id_to_info[ $target_id ]['inbound'][] = array(
                            'from_id'    => $p->ID,
                            'from_title' => $p->post_title,
                            'from_url'   => $source_url,
                        );
                        $id_to_info[ $target_id ]['inbound_count']++;
                    }
                }
            }
        }

        $homepage_id = self::get_homepage_id_for_language( $lang, $wpml_active );

        // Classify
        $orphans = array();
        $weak    = array();

        foreach ( $id_to_info as $id => $info ) {
            if ( $id == $homepage_id ) continue;

            $url_lower = strtolower( $info['url'] );
            if ( strpos( $url_lower, 'lets-connect' ) !== false ) continue;
            if ( strpos( $url_lower, '/contact' ) !== false ) continue;
            if ( strpos( $url_lower, '/blog/' ) !== false && substr_count( $url_lower, '/' ) <= 4 ) continue;

            if ( $info['inbound_count'] === 0 ) {
                $orphans[] = $info;
            } elseif ( $info['inbound_count'] === 1 ) {
                $weak[] = $info;
            }
        }

        usort( $orphans, function( $a, $b ) {
            return strcmp( $b['date'], $a['date'] );
        });

        update_option( self::LINK_MAP_KEY . '_' . $lang, $id_to_info, false );

        $orphans_capped = $orphans;  // No cap — fix_all needs complete list
        $weak_capped    = array_slice( $weak, 0, 500 );

        foreach ( $orphans_capped as &$o ) { unset( $o['inbound'] ); }
        foreach ( $weak_capped as &$w ) {
            $w['inbound'] = array_slice( $w['inbound'], 0, 1 );
        }
        unset( $o, $w );

        return array(
            'total'         => count( $posts ),
            'orphans'       => $orphans_capped,
            'weak'          => $weak_capped,
            'orphan_count'  => count( $orphans ),
            'weak_count'    => count( $weak ),
            'language_name' => self::get_language_name( $lang ),
        );
    }

    /* ══════════════════════════════════════════════════════════════
     *  WPML HELPERS
     * ══════════════════════════════════════════════════════════════ */

    public static function get_active_languages() {
        if ( function_exists( 'icl_get_languages' ) ) {
            $langs = icl_get_languages( 'skip_missing=0' );
            if ( ! empty( $langs ) ) return array_keys( $langs );
        }

        global $wpdb;
        $icl_table = $wpdb->prefix . 'icl_translations';
        $table_exists = $wpdb->get_var( "SHOW TABLES LIKE '{$icl_table}'" ) === $icl_table;

        if ( $table_exists ) {
            $langs = $wpdb->get_col(
                "SELECT DISTINCT language_code FROM {$icl_table}
                 WHERE element_type LIKE 'post_%'
                 AND language_code != ''
                 ORDER BY language_code"
            );
            if ( ! empty( $langs ) ) return $langs;
        }

        return array( 'en' );
    }

    public static function get_posts_for_language( $lang, $wpml_active = true ) {
        global $wpdb;

        $post_types = array( 'post', 'page' );
        $pt_placeholders = implode( ',', array_fill( 0, count( $post_types ), '%s' ) );

        if ( $wpml_active ) {
            $icl_table = $wpdb->prefix . 'icl_translations';
            $table_exists = $wpdb->get_var( "SHOW TABLES LIKE '{$icl_table}'" ) === $icl_table;

            if ( $table_exists ) {
                $et_conditions = array();
                $et_values = array();
                foreach ( $post_types as $pt ) {
                    $et_conditions[] = 't.element_type = %s';
                    $et_values[] = 'post_' . $pt;
                }
                $et_sql = implode( ' OR ', $et_conditions );
                $sql_params = array_merge( $et_values, array( $lang ) );

                $posts = $wpdb->get_results( $wpdb->prepare(
                    "SELECT p.ID, p.post_title, p.post_content, p.post_type, p.post_date, p.post_name
                     FROM {$wpdb->posts} p
                     INNER JOIN {$icl_table} t ON t.element_id = p.ID
                     WHERE p.post_status = 'publish'
                       AND p.post_type IN ({$pt_placeholders})
                       AND ({$et_sql})
                       AND t.language_code = %s
                     ORDER BY p.post_date DESC",
                    array_merge( $post_types, $sql_params )
                ) );

                return $posts ?: array();
            }
        }

        $posts = $wpdb->get_results( $wpdb->prepare(
            "SELECT ID, post_title, post_content, post_type, post_date, post_name
             FROM {$wpdb->posts}
             WHERE post_status = 'publish'
               AND post_type IN ({$pt_placeholders})
             ORDER BY post_date DESC",
            $post_types
        ) );

        return $posts ?: array();
    }

    public static function get_homepage_id_for_language( $lang, $wpml_active = true ) {
        $front_page_id = (int) get_option( 'page_on_front', 0 );
        if ( ! $front_page_id ) return 0;

        if ( $wpml_active && function_exists( 'icl_object_id' ) ) {
            return (int) icl_object_id( $front_page_id, 'page', false, $lang );
        }

        return $front_page_id;
    }

    public static function get_language_name( $lang ) {
        $names = array(
            'en' => 'English', 'fr' => 'French', 'nl' => 'Dutch',
            'de' => 'German', 'it' => 'Italian', 'es' => 'Spanish',
            'pt-pt' => 'Portuguese', 'pt-br' => 'Brazilian Portuguese',
            'zh-hans' => 'Chinese (Simplified)', 'ja' => 'Japanese',
            'ko' => 'Korean', 'ar' => 'Arabic', 'ru' => 'Russian',
            'pl' => 'Polish', 'cs' => 'Czech', 'ro' => 'Romanian',
            'hu' => 'Hungarian', 'sv' => 'Swedish', 'da' => 'Danish',
            'fi' => 'Finnish', 'el' => 'Greek', 'tr' => 'Turkish',
            'he' => 'Hebrew', 'th' => 'Thai', 'vi' => 'Vietnamese',
        );
        return isset( $names[ $lang ] ) ? $names[ $lang ] : strtoupper( $lang );
    }

    /* ══════════════════════════════════════════════════════════════
     *  LINK EXTRACTION
     * ══════════════════════════════════════════════════════════════ */

    public static function extract_internal_links( $html ) {
        if ( empty( $html ) ) return array();

        $links = array();

        if ( preg_match_all( '/href=["\']([^"\']+)["\']/i', $html, $matches ) ) {
            foreach ( $matches[1] as $url ) {
                if ( self::is_internal_link( $url ) ) {
                    $clean = self::normalize_url( $url );
                    if ( $clean ) $links[] = $clean;
                }
            }
        }

        return array_unique( $links );
    }

    public static function is_internal_link( $url ) {
        if ( empty( $url ) ) return false;

        if ( strpos( $url, '/' ) === 0 && strpos( $url, '//' ) !== 0 ) return true;

        $host = wp_parse_url( $url, PHP_URL_HOST );
        if ( ! $host ) return false;

        $host = strtolower( $host );
        return ( $host === self::SITE_DOMAIN || $host === 'www.' . self::SITE_DOMAIN );
    }

    public static function normalize_url( $url ) {
        $parsed = wp_parse_url( $url );
        if ( ! $parsed ) return '';

        $scheme = isset( $parsed['scheme'] ) ? $parsed['scheme'] : 'https';
        $host   = isset( $parsed['host'] ) ? strtolower( $parsed['host'] ) : 'www.' . self::SITE_DOMAIN;
        $path   = isset( $parsed['path'] ) ? $parsed['path'] : '/';

        $skip_patterns = array(
            '#/wp-content/#i', '#/wp-admin/#i', '#/wp-json/#i',
            '#/feed/?$#i', '#/xmlrpc#i', '#\.(jpg|jpeg|png|gif|svg|pdf|css|js)$#i',
            '#/category/#i', '#/tag/#i', '#/author/#i',
        );
        foreach ( $skip_patterns as $pattern ) {
            if ( preg_match( $pattern, $path ) ) return '';
        }

        return $scheme . '://' . $host . rtrim( $path, '/' );
    }

    /* ══════════════════════════════════════════════════════════════
     *  RESET
     * ══════════════════════════════════════════════════════════════ */

    public static function reset_all() {
        delete_option( self::RESULTS_KEY );
        delete_transient( self::PROGRESS_KEY );
        delete_transient( self::STOP_FLAG_KEY );

        global $wpdb;
        $wpdb->query(
            "DELETE FROM {$wpdb->options}
             WHERE option_name LIKE 'dba_anti_orphan_link_map_%'"
        );

        return true;
    }

    /* ══════════════════════════════════════════════════════════════
     *  RESULTS & PROGRESS
     * ══════════════════════════════════════════════════════════════ */

    public static function get_results() {
        return get_option( self::RESULTS_KEY, array() );
    }

    public static function get_orphans( $lang = 'en' ) {
        $results = self::get_results();
        if ( empty( $results['details'][ $lang ] ) ) return array();
        return $results['details'][ $lang ]['orphans'];
    }

    public static function get_weak_pages( $lang = 'en' ) {
        $results = self::get_results();
        if ( empty( $results['details'][ $lang ] ) ) return array();
        return $results['details'][ $lang ]['weak'];
    }

    public static function get_inbound_count( $post_id, $lang = 'en' ) {
        $link_map = get_option( self::LINK_MAP_KEY . '_' . $lang, array() );
        if ( empty( $link_map[ $post_id ] ) ) return 0;
        return $link_map[ $post_id ]['inbound_count'];
    }

    /**
     * @param string $state  'running' | 'done' | 'stopped' | 'error' | 'idle'
     */
    public static function update_progress( $message, $percent, $state = 'running' ) {
        set_transient( self::PROGRESS_KEY, array(
            'message' => $message,
            'percent' => intval( $percent ),
            'state'   => $state,
            'time'    => current_time( 'mysql' ),
        ), 300 );
    }

    public static function get_progress() {
        return get_transient( self::PROGRESS_KEY ) ?: array(
            'message' => 'No scan running.',
            'percent' => 0,
            'state'   => 'idle',
        );
    }

    public static function get_settings() {
        return wp_parse_args( get_option( self::SETTINGS_KEY, array() ), array(
            'enabled'          => true,
            'auto_fix'         => false,
            'max_links'        => 2,
            'min_content_len'  => 500,
            'skip_post_types'  => array(),
            'cron_schedule'    => 'weekly',
            'email_report'     => false,
            'email_address'    => get_option( 'admin_email' ),
        ) );
    }

    public static function save_settings( $settings ) {
        update_option( self::SETTINGS_KEY, $settings, false );
    }

    public static function get_memory_limit_bytes() {
        $limit = ini_get( 'memory_limit' );
        if ( $limit === '-1' ) return PHP_INT_MAX;
        $val = intval( $limit );
        $unit = strtolower( substr( trim( $limit ), -1 ) );
        switch ( $unit ) {
            case 'g': $val *= 1024;
            case 'm': $val *= 1024;
            case 'k': $val *= 1024;
        }
        return $val;
    }
}
