<?php
if (!defined('ABSPATH')) {
    exit;
}


/**
 * Class responsible for tracking and logging 404 errors
 *
 * @link       https://aysa.ai
 * @since      1.0.0
 *
 * @package    Aysa
 * @subpackage Aysa/includes
 */

class Aysa_404_Tracker {

    /**
     * The ID of this plugin.
     *
     * @since    1.0.0
     * @access   private
     * @var      string    $plugin_name    The ID of this plugin.
     */
    private $plugin_name;

    /**
     * The version of this plugin.
     *
     * @since    1.0.0
     * @access   private
     * @var      string    $version    The current version of this plugin.
     */
    private $version;

    /**
     * Table name for 404 errors
     *
     * @since    1.0.0
     * @access   private
     * @var      string    $table_name    The name of the database table.
     */
    private $table_name;

    /**
     * Initialize the class and set its properties.
     *
     * @since    1.0.0
     * @param    string    $plugin_name    The name of this plugin.
     * @param    string    $version        The version of this plugin.
     */
    public function __construct($plugin_name, $version) {
        global $wpdb;
        
        $this->plugin_name = $plugin_name;
        $this->version = $version;
        $this->table_name = esc_sql($wpdb->prefix . 'aysa_404_errors');
    }

    /**
     * Initialize 404 tracker
     *
     * @since    1.0.0
     */
    public function init() {
        // Create database table if it doesn't exist
        $this->create_table();
        
        // Hook into template_redirect to catch 404 errors with high priority
        add_action('template_redirect', array($this, 'track_404'), 5);
        
        // Register AJAX handlers for admin actions
        add_action('wp_ajax_aysa_get_404_errors', array($this, 'get_404_errors'));
        add_action('wp_ajax_aysa_create_redirect', array($this, 'create_redirect'));
        add_action('wp_ajax_aysa_mark_gone', array($this, 'mark_gone'));
        add_action('wp_ajax_aysa_ignore_404', array($this, 'ignore_404'));
    }

    /**
     * Resolve project id for wallet tracking.
     *
     * @return int
     */
    private function get_project_id_for_wallet() {
        $project_id = intval(get_option('aysa_project_id', 0));
        if ($project_id > 0) {
            return $project_id;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reviewed callback inventory: this read-only/core lifecycle request is authenticated by WordPress or an enclosing guard and performs no unapproved write.
        if (isset($_REQUEST['project_id'])) {
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reviewed callback inventory: this read-only/core lifecycle request is authenticated by WordPress or an enclosing guard and performs no unapproved write.
            $project_id = intval(wp_unslash($_REQUEST['project_id']));
            if ($project_id > 0) {
                return $project_id;
            }
        }

        return 0;
    }

    /**
     * Track wallet usage for technical 404 actions.
     *
     * @param string $action_key
     * @param string $reference
     * @param int    $fallback_cost_cents
     * @param array  $meta
     * @param string $endpoint
     * @return void
     */
    private function track_wallet_usage($action_key, $reference, $fallback_cost_cents, $meta = array(), $endpoint = '') {
        $project_id = $this->get_project_id_for_wallet();
        if ($project_id <= 0 || !defined('AYSA_PLUGIN_DIR')) {
            return;
        }

        if (!class_exists('Aysa_Wallet_Manager')) {
            $wallet_file = AYSA_PLUGIN_DIR . 'includes/class-aysa-wallet-manager.php';
            if (file_exists($wallet_file)) {
                require_once $wallet_file;
            }
        }
        if (!class_exists('Aysa_Wallet_Manager')) {
            return;
        }

        $wallet = Aysa_Wallet_Manager::get_instance();
        if (!method_exists($wallet, 'track_action_usage')) {
            return;
        }

        $wallet->track_action_usage(
            $project_id,
            sanitize_key((string) $action_key),
            (string) $reference,
            is_array($meta) ? $meta : array(),
            intval($fallback_cost_cents),
            (string) $endpoint
        );
    }

    /**
     * Create the database table for storing 404 errors
     *
     * @since    1.0.0
     */
    private function get_safe_charset_collate() {
        global $wpdb;
        $charset_collate = $wpdb->get_charset_collate();
        
        if (strpos($charset_collate, 'UTF-8') !== false) {
            $charset_collate = str_replace('UTF-8', 'utf8mb4', $charset_collate);
        }
        
        // Fallback to safe default if charset_collate is empty or invalid
        if (empty($charset_collate) || strpos($charset_collate, 'UTF-8') !== false) {
            $charset_collate = 'DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
        }
        
        return $charset_collate;
    }
    
    public function create_table() {
        global $wpdb;
        
        $charset_collate = $this->get_safe_charset_collate();
        aysa_debug_log('[AYSA DEBUG] 404 Tracker using charset: ' . $charset_collate);
        
        $sql = "CREATE TABLE IF NOT EXISTS {$this->table_name} (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            url varchar(512) NOT NULL,
            referer varchar(512) DEFAULT '',
            user_agent varchar(512) DEFAULT '',
            ip_address varchar(45) DEFAULT '',
            hit_count int(11) DEFAULT 1,
            first_hit datetime DEFAULT CURRENT_TIMESTAMP,
            last_hit datetime DEFAULT CURRENT_TIMESTAMP,
            is_bot tinyint(1) DEFAULT 0,
            is_ignored tinyint(1) DEFAULT 0,
            suggested_url varchar(512) DEFAULT '',
            PRIMARY KEY  (id),
            KEY url (url(191)),
            KEY is_ignored (is_ignored)
        ) $charset_collate;";
        
        require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
        dbDelta($sql);
    }

    /**
     * Track 404 errors
     *
     * @since    1.0.0
     */
    public function track_404() {
        global $wpdb;
        
        // Debug: Log that the function was called
        aysa_debug_log('[AYSA 404 TRACKER] track_404() function called');
        
        // Only run on 404 pages
        if (!is_404()) {
            aysa_debug_log('[AYSA 404 TRACKER] Not a 404 page, skipping tracking');
            return;
        }
        
        // Debug logging
        aysa_debug_log('[AYSA 404 TRACKER] 404 error detected, starting tracking...');
        
        // Get current URL (same method as 301 Redirects plugin)
        $url = isset($_SERVER['REQUEST_URI']) ? wp_strip_all_tags(wp_unslash($_SERVER['REQUEST_URI'])) : '';
        aysa_debug_log('[AYSA 404 TRACKER] Tracking URL: ' . $url);
        
        // Get referrer (same method as 301 Redirects plugin)
        $referer = isset($_SERVER['HTTP_REFERER']) ? wp_strip_all_tags(wp_unslash($_SERVER['HTTP_REFERER'])) : '';
        
        // Get user agent (same method as 301 Redirects plugin)
        $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? wp_strip_all_tags(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
        
        // Get IP address
        $ip_address = $this->get_ip_address();
        
        // Check if it's a bot
        $is_bot = $this->is_bot($user_agent) ? 1 : 0;
        aysa_debug_log('[AYSA 404 TRACKER] Bot detection: ' . ($is_bot ? 'YES' : 'NO'));
        
        // Skip bot filtering - track ALL traffic like 301 Redirects plugin
        // This ensures we capture all 404 errors including bot traffic
        aysa_debug_log('[AYSA 404 TRACKER] Tracking all traffic including bots');
        
        // Skip exclusion patterns check - track ALL 404s like 301 Redirects plugin
        // This ensures we capture all 404 errors without filtering
        aysa_debug_log('[AYSA 404 TRACKER] Tracking all 404s without exclusion patterns');
        
        // Generate suggestions
        $suggested_url = $this->generate_suggestion($url);
        
        // Use MySQL datetime format
        $current_time = current_time('mysql');
        
        // Check if this URL is already logged
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $existing = $wpdb->get_row($wpdb->prepare(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Internal AYSA 404 tracker table name is derived from the WordPress prefix and a fixed suffix.
            "SELECT id, hit_count, referer FROM {$this->table_name} WHERE url = %s",
            $url
        ));
        
        if ($existing) {
            // Update existing record
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
            $result = $wpdb->update(
                $this->table_name,
                array(
                    'hit_count' => $existing->hit_count + 1,
                    'last_hit' => $current_time,
                    'referer' => $referer ?: $existing->referer,
                ),
                array('id' => $existing->id),
                array('%d', '%s', '%s'),
                array('%d')
            );
            aysa_debug_log('[AYSA 404 TRACKER] Updated existing record (ID: ' . $existing->id . ', new count: ' . ($existing->hit_count + 1) . ')');
        } else {
            // Insert new record
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
            $result = $wpdb->insert(
                $this->table_name,
                array(
                    'url' => $url,
                    'referer' => $referer,
                    'user_agent' => $user_agent,
                    'ip_address' => $ip_address,
                    'is_bot' => $is_bot,
                    'suggested_url' => $suggested_url,
                    'first_hit' => $current_time,
                    'last_hit' => $current_time,
                    'hit_count' => 1
                ),
                array('%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%d')
            );
            if ($result !== false) {
                aysa_debug_log('[AYSA 404 TRACKER] Inserted new 404 record (ID: ' . $wpdb->insert_id . ')');
            } else {
                aysa_debug_log('[AYSA 404 TRACKER] ERROR: Failed to insert 404 record: ' . $wpdb->last_error);
            }
        }
    }

    /**
     * Get the client IP address
     *
     * @since    1.0.0
     * @return   string    The IP address
     */
    private function get_ip_address() {
        $ip_keys = array(
            'HTTP_CLIENT_IP',
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_FORWARDED',
            'HTTP_X_CLUSTER_CLIENT_IP',
            'HTTP_FORWARDED_FOR',
            'HTTP_FORWARDED',
            'REMOTE_ADDR'
        );
        
        foreach ($ip_keys as $key) {
                $candidate_ip = isset($_SERVER[$key]) ? sanitize_text_field(wp_unslash($_SERVER[$key])) : '';
                if ($candidate_ip !== '' && filter_var($candidate_ip, FILTER_VALIDATE_IP)) {
                    return $candidate_ip;
            }
        }
        
        return '0.0.0.0';
    }

    /**
     * Check if user agent is a bot
     *
     * @since    1.0.0
     * @param    string    $user_agent    The user agent string
     * @return   bool      True if it's a bot, false otherwise
     */
    private function is_bot($user_agent) {
        // More specific bot patterns to avoid false positives
        $bot_patterns = array(
            'googlebot', 'bingbot', 'yandexbot', 'baiduspider', 'facebookexternalhit',
            'twitterbot', 'rogerbot', 'linkedinbot', 'embedly', 'quora link preview',
            'showyoubot', 'outbrain', 'pinterest', 'slackbot', 'vkShare', 'W3C_Validator',
            'slurp', 'mediapartners', 'ia_archiver', 'msnbot', 'duckduckbot'
        );
        
        $user_agent = strtolower($user_agent);
        
        // Skip if user agent is empty or very short
        if (empty($user_agent) || strlen($user_agent) < 10) {
            return false;
        }
        
        // Check for specific bot patterns
        foreach ($bot_patterns as $pattern) {
            if (strpos($user_agent, $pattern) !== false) {
                aysa_debug_log('[AYSA 404 TRACKER] Bot detected: ' . $pattern . ' in: ' . $user_agent);
                return true;
            }
        }
        
        // Additional check for generic bot patterns (more restrictive)
        $generic_bot_patterns = array('crawler/', 'spider/', 'robot/', 'crawling/');
        foreach ($generic_bot_patterns as $pattern) {
            if (strpos($user_agent, $pattern) !== false) {
                aysa_debug_log('[AYSA 404 TRACKER] Generic bot detected: ' . $pattern . ' in: ' . $user_agent);
                return true;
            }
        }
        
        return false;
    }

    /**
     * Generate URL suggestion for a 404 error
     *
     * @since    1.0.0
     * @param    string    $url    The 404 URL
     * @return   string    Suggested URL or empty string
     */
    private function generate_suggestion($url) {
        global $wpdb;
        
        // Get the path part of the URL
        $path = wp_parse_url($url, PHP_URL_PATH);
        if (empty($path)) {
            return '';
        }
        
        // Remove trailing slash
        $path = rtrim($path, '/');
        
        // Try to find similar posts or pages
        $slug = basename($path);
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $similar_post = $wpdb->get_row($wpdb->prepare(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- WordPress owns the posts table identifier; all request-derived values use placeholders below.
            "
            SELECT ID, post_type, post_name 
            FROM {$wpdb->posts} 
            WHERE post_status = 'publish' 
            AND (post_type = 'post' OR post_type = 'page')
            ORDER BY CASE
                WHEN post_name = %s THEN 1
                WHEN post_name LIKE %s THEN 2
                WHEN post_name SOUNDS LIKE %s THEN 3
                ELSE 4
            END
            LIMIT 1
            ",
            $slug,
            '%' . $wpdb->esc_like($slug) . '%',
            $slug
        ));
        
        if ($similar_post) {
            return get_permalink($similar_post->ID);
        }
        
        // Try removing file extensions
        if (preg_match('/\.(html|php|aspx?)$/', $path)) {
            $clean_path = preg_replace('/\.(html|php|aspx?)$/', '', $path);
            return home_url($clean_path);
        }
        
        // If we have a long path, suggest parent path
        $path_parts = explode('/', trim($path, '/'));
        if (count($path_parts) > 1) {
            array_pop($path_parts);
            $parent_path = '/' . implode('/', $path_parts);
            
            // Check if parent path exists
            $parent_url = home_url($parent_path);
            $response = wp_remote_head($parent_url);
            
            if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
                return $parent_url;
            }
        }
        
        // No suggestion found
        return '';
    }

    /**
     * Tokenize a URL/path for semantic matching.
     *
     * @since 1.0.0
     * @param string $value
     * @return array
     */
    private function tokenize_for_redirect_match($value) {
        $value = strtolower(urldecode((string) $value));
        $value = remove_accents($value);
        $value = preg_replace('/\?.*$/', '', $value);
        $value = preg_replace('/\.[a-z0-9]{1,5}$/', '', $value);
        $value = str_replace(array('-', '_', '/', '.'), ' ', $value);
        $parts = preg_split('/\s+/', trim((string) $value));
        if (!is_array($parts)) {
            return array();
        }

        $stop = array(
            'www', 'http', 'https', 'com', 'ro', 'eu', 'net', 'org',
            'si', 'sau', 'de', 'la', 'cu', 'in', 'din', 'pe', 'pentru', 'un', 'o', 'the', 'and', 'or',
            'category', 'categorie', 'product', 'produs', 'page', 'pagina', 'post', 'blog', 'tag'
        );
        $out = array();
        foreach ($parts as $token) {
            $token = trim((string) $token);
            if ($token === '' || strlen($token) < 2) {
                continue;
            }
            if (preg_match('/^\d+$/', $token)) {
                continue;
            }
            if (in_array($token, $stop, true)) {
                continue;
            }
            $out[$token] = $token;
            if (count($out) >= 16) {
                break;
            }
        }
        return array_values($out);
    }

    /**
     * Build candidate pool for redirect suggestions.
     *
     * @since 1.0.0
     * @return array
     */
    private function get_redirect_candidate_pool() {
        global $wpdb;

        $cache_key = 'aysa_404_redirect_pool_' . intval(get_current_blog_id());
        $cached = get_transient($cache_key);
        if (is_array($cached) && !empty($cached)) {
            return $cached;
        }

        $pool = array();
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $post_rows = $wpdb->get_results(
            "SELECT ID, post_name, post_title, post_type
             FROM {$wpdb->posts}
             WHERE post_status = 'publish'
               AND post_type IN ('page','post','product')
             ORDER BY post_modified_gmt DESC
             LIMIT 1200"
        );
        if (is_array($post_rows)) {
            foreach ($post_rows as $row) {
                $url = get_permalink(intval($row->ID));
                if (!$url) {
                    continue;
                }
                $path = (string) wp_parse_url($url, PHP_URL_PATH);
                if ($path === '') {
                    continue;
                }
                $tokens = $this->tokenize_for_redirect_match($path . ' ' . (string) $row->post_title . ' ' . (string) $row->post_name);
                $pool[] = array(
                    'url' => $url,
                    'path' => $path,
                    'title' => (string) $row->post_title,
                    'type' => (string) $row->post_type,
                    'tokens' => $tokens,
                    'slug' => (string) $row->post_name,
                );
            }
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $term_rows = $wpdb->get_results(
            "SELECT t.term_id, t.name, t.slug, tt.taxonomy
             FROM {$wpdb->terms} t
             INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_id = t.term_id
             WHERE tt.taxonomy IN ('category','product_cat')
             ORDER BY t.term_id DESC
             LIMIT 600"
        );
        if (is_array($term_rows)) {
            foreach ($term_rows as $row) {
                $link = get_term_link(intval($row->term_id), (string) $row->taxonomy);
                if (is_wp_error($link) || empty($link)) {
                    continue;
                }
                $path = (string) wp_parse_url((string) $link, PHP_URL_PATH);
                if ($path === '') {
                    continue;
                }
                $tokens = $this->tokenize_for_redirect_match($path . ' ' . (string) $row->name . ' ' . (string) $row->slug);
                $pool[] = array(
                    'url' => (string) $link,
                    'path' => $path,
                    'title' => (string) $row->name,
                    'type' => (string) $row->taxonomy,
                    'tokens' => $tokens,
                    'slug' => (string) $row->slug,
                );
            }
        }

        set_transient($cache_key, $pool, 10 * MINUTE_IN_SECONDS);
        return $pool;
    }

    /**
     * Suggest best redirect target using semantic + lexical scoring.
     *
     * @since 1.0.0
     * @param string $url
     * @return array
     */
    private function suggest_best_redirect_target($url) {
        $normalized = $this->normalize_404_url($url);
        if ($normalized === '') {
            return array(
                'target_url' => '',
                'confidence' => 0,
                'status' => 'none',
                'action' => 'none',
                'reason' => 'empty_source',
            );
        }

        if ($this->is_security_noise_url($normalized)) {
            return array(
                'target_url' => '',
                'confidence' => 0,
                'status' => 'ignored',
                'action' => 'ignore',
                'reason' => 'security_noise',
            );
        }

        $source_path = (string) wp_parse_url($normalized, PHP_URL_PATH);
        if ($source_path === '') {
            $source_path = $normalized;
        }
        $source_slug = sanitize_title((string) basename(rtrim($source_path, '/')));
        $source_tokens = $this->tokenize_for_redirect_match($source_path);
        if (empty($source_tokens) && $source_slug !== '') {
            $source_tokens = array($source_slug);
        }

        $pool = $this->get_redirect_candidate_pool();
        if (empty($pool)) {
            return array(
                'target_url' => '',
                'confidence' => 0,
                'status' => 'none',
                'action' => 'none',
                'reason' => 'no_candidates',
            );
        }

        $best = null;
        $best_score = -1;
        foreach ($pool as $candidate) {
            $candidate_path = (string) ($candidate['path'] ?? '');
            if ($candidate_path === '' || $candidate_path === '/') {
                continue;
            }
            if ($candidate_path === $source_path || rtrim($candidate_path, '/') === rtrim($source_path, '/')) {
                continue;
            }

            $candidate_tokens = isset($candidate['tokens']) && is_array($candidate['tokens']) ? $candidate['tokens'] : array();
            $common = count(array_intersect($source_tokens, $candidate_tokens));
            $source_count = max(1, count($source_tokens));
            $candidate_count = max(1, count($candidate_tokens));
            $union = max(1, count(array_unique(array_merge($source_tokens, $candidate_tokens))));

            $coverage = $common / $source_count;
            $jaccard = $common / $union;
            $slug_sim = 0;
            $candidate_slug = sanitize_title((string) ($candidate['slug'] ?? basename(rtrim($candidate_path, '/'))));
            if ($source_slug !== '' && $candidate_slug !== '') {
                similar_text($source_slug, $candidate_slug, $slug_pct);
                $slug_sim = max(0, min(1, ((float) $slug_pct) / 100.0));
            }

            $score = ($coverage * 55.0) + ($jaccard * 25.0) + ($slug_sim * 20.0);
            if ($source_slug !== '' && $candidate_slug === $source_slug) {
                $score += 15.0;
            }
            if ($common === 0) {
                $score -= 15.0;
            }
            if (strpos($candidate_path, '/product/') !== false || strpos($candidate_path, '/categorie/') !== false) {
                $score += 3.0;
            }

            $score = max(0, min(100, $score));
            if ($score > $best_score) {
                $best_score = $score;
                $best = $candidate;
            }
        }

        if (!$best || $best_score < 35) {
            return array(
                'target_url' => '',
                'confidence' => intval(round(max(0, $best_score))),
                'status' => 'none',
                'action' => 'none',
                'reason' => 'no_reliable_match',
            );
        }

        $confidence = intval(round($best_score));
        $status = 'weak';
        if ($confidence >= 85) {
            $status = 'high';
        } elseif ($confidence >= 65) {
            $status = 'review';
        }

        return array(
            'target_url' => (string) ($best['url'] ?? ''),
            'confidence' => $confidence,
            'status' => $status,
            'action' => 'redirect',
            'reason' => $status === 'high' ? 'high_semantic_match' : ($status === 'review' ? 'medium_semantic_match' : 'low_semantic_match'),
            'match_type' => (string) ($best['type'] ?? ''),
            'match_title' => (string) ($best['title'] ?? ''),
        );
    }

    /**
     * Resolve project id for technical audit state option.
     *
     * @since 1.0.0
     * @return int
     */
    private function resolve_site_audit_project_id() {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reviewed callback inventory: this read-only/core lifecycle request is authenticated by WordPress or an enclosing guard and performs no unapproved write.
        if (isset($_REQUEST['project_id'])) {
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reviewed callback inventory: this read-only/core lifecycle request is authenticated by WordPress or an enclosing guard and performs no unapproved write.
            $request_project_id = intval(wp_unslash($_REQUEST['project_id']));
            if ($request_project_id > 0) {
                return $request_project_id;
            }
        }

        $project_id = intval(get_option('aysa_project_id', 0));
        if ($project_id > 0) {
            return $project_id;
        }

        $host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
        if ($host === 'localhost' || $host === '127.0.0.1' || substr($host, -10) === '.localhost') {
            return defined('AYSA_LOCAL_DEV_PROJECT_ID') ? intval(AYSA_LOCAL_DEV_PROJECT_ID) : 22;
        }

        return 0;
    }

    /**
     * Normalize URL/path for matching records from different sources.
     *
     * @since 1.0.0
     * @param string $url Raw URL or path.
     * @return string
     */
    private function normalize_404_url($url) {
        $url = trim((string) $url);
        if ($url === '') {
            return '';
        }

        if (preg_match('#^https?://#i', $url)) {
            $path = (string) wp_parse_url($url, PHP_URL_PATH);
            $query = (string) wp_parse_url($url, PHP_URL_QUERY);
            $normalized = $path === '' ? '/' : $path;
            if ($query !== '') {
                $normalized .= '?' . $query;
            }
            return $normalized;
        }

        if ($url[0] !== '/') {
            $url = '/' . $url;
        }

        return $url;
    }

    /**
     * Detect likely security-scanner/probe noise URLs.
     *
     * @since 1.0.0
     * @param string $url
     * @return bool
     */
    private function is_security_noise_url($url) {
        $classification = $this->classify_404_noise($url);
        return in_array((string) ($classification['class'] ?? ''), array('security_noise', 'junk_query'), true);
    }

    /**
     * Classify a 404 into SEO-relevant or noise buckets.
     *
     * @since 1.0.0
     * @param string $url
     * @param string $referer
     * @param string $user_agent
     * @return array
     */
    private function classify_404_noise($url, $referer = '', $user_agent = '') {
        $normalized = strtolower($this->normalize_404_url($url));
        $referer = strtolower(trim((string) $referer));
        $user_agent = strtolower(trim((string) $user_agent));
        if ($normalized === '') {
            return array('class' => 'general', 'reason' => 'empty_url');
        }

        $path = (string) wp_parse_url($normalized, PHP_URL_PATH);
        $query = (string) wp_parse_url($normalized, PHP_URL_QUERY);
        $path = $path !== '' ? strtolower($path) : $normalized;
        $query_l = strtolower($query);

        $security_patterns = array(
            '#^/\.well-known/(?!traffic-advice)#',
            '#(^|/)(cgi-bin|phpmyadmin|pma|myadmin)(/|$)#',
            '#(^|/)(vendor|storage|backup|backups|dump|dumps|database)(/|$)#',
            '#(^|/)(\.git|\.svn|\.hg|\.bzr)(/|$)#',
            '#(^|/)(wp-config|config|composer\.(json|lock)|package(-lock)?\.json|yarn\.lock)(\.|/|$)#',
            '#(^|/)(artisan|adminer|eval-stdin|shell|webshell|alfanew|wso|minishell)(\.|/|$)#',
            '#(^|/)(xmlrpc\.php|wlwmanifest\.xml|\.env|\.aws|id_rsa|passwd|shadow)(/|$|\.|-)#',
            '#(^|/)(boaform|hudson|jenkins|actuator|server-status|phpinfo)(/|$)#',
            '#\.(php[0-9]*|phtml|phar|asp|aspx|jsp|cgi|pl|env|ini|log|sql|bak|old|save|swp|dist|zip|tar|gz|tgz|7z)(\?|$)#',
        );
        foreach ($security_patterns as $pattern) {
            if (preg_match($pattern, $path)) {
                return array('class' => 'security_noise', 'reason' => 'scanner_path');
            }
        }

        $junk_query_signals = array(
            'author=',
            'rest_route=',
            'xdebug_session_start=',
            'password=',
            'token=',
            'cmd=',
            'exec=',
            '../',
            '%2e%2e',
            '<script',
            'union select',
            'base64_',
        );
        foreach ($junk_query_signals as $signal) {
            if ($query_l !== '' && strpos($query_l, $signal) !== false) {
                return array('class' => 'junk_query', 'reason' => 'suspicious_query');
            }
        }

        if ($this->is_admin_noise_referer($referer)) {
            return array('class' => 'admin_noise', 'reason' => 'admin_referer');
        }

        if ($user_agent !== '' && preg_match('#(masscan|sqlmap|nikto|nuclei|acunetix|nessus|zgrab|gobuster|dirbuster|curl|python-requests|go-http-client)#', $user_agent)) {
            return array('class' => 'security_noise', 'reason' => 'scanner_user_agent');
        }

        return array('class' => 'seo_candidate', 'reason' => 'public_404');
    }

    /**
     * Detect internal/admin-generated 404 noise (not public SEO issues).
     *
     * @since 1.0.0
     * @param string $referer
     * @return bool
     */
    private function is_admin_noise_referer($referer) {
        $referer = strtolower(trim((string) $referer));
        if ($referer === '') {
            return false;
        }
        if (strpos($referer, '/wp-admin/') !== false) {
            return true;
        }
        if (strpos($referer, 'post.php?post=') !== false || strpos($referer, 'action=edit') !== false) {
            return true;
        }
        if (strpos($referer, '/wp-json/') !== false) {
            return true;
        }
        return false;
    }

    /**
     * Collect URLs recursively from site audit payload chunks.
     *
     * @since 1.0.0
     * @param mixed $node
     * @param array $collector
     */
    private function collect_site_audit_urls_recursive($node, &$collector) {
        if (is_string($node)) {
            $value = trim($node);
            if ($value === '') {
                return;
            }
            if (preg_match('#^https?://#i', $value) || preg_match('#^/[^\\s]*$#', $value)) {
                $normalized = $this->normalize_404_url($value);
                if ($normalized !== '') {
                    $collector[$normalized] = $normalized;
                }
            }
            return;
        }

        if (!is_array($node)) {
            return;
        }

        foreach ($node as $key => $value) {
            $key_l = strtolower((string) $key);
            if (in_array($key_l, array('url', 'page_url', 'page', 'source_url', 'destination_url'), true) && is_string($value)) {
                $normalized = $this->normalize_404_url($value);
                if ($normalized !== '') {
                    $collector[$normalized] = $normalized;
                }
                continue;
            }
            if (is_array($value)) {
                $this->collect_site_audit_urls_recursive($value, $collector);
            } elseif (is_string($value)) {
                $this->collect_site_audit_urls_recursive($value, $collector);
            }
        }
    }

    /**
     * Build additional 404 rows from latest site audit snapshot.
     *
     * @since 1.0.0
     * @param string $filter
     * @param string $search
     * @return array
     */
    private function get_site_audit_404_rows($filter = 'all', $search = '') {
        global $wpdb;

        $project_id = $this->resolve_site_audit_project_id();
        if ($project_id <= 0) {
            return array();
        }

        $state_key = 'aysa_technical_site_audit_state_' . intval($project_id);
        $state = get_option($state_key, array());
        if (!is_array($state) || empty($state['audit_snapshot']) || !is_array($state['audit_snapshot'])) {
            return array();
        }

        $snapshot = $state['audit_snapshot'];
        $updated_at = !empty($state['updated_at']) ? (string) $state['updated_at'] : current_time('mysql');
        $updated_ts = strtotime($updated_at);
        if ($updated_ts === false) {
            $updated_ts = current_time('timestamp');
        }

        $url_set = array();

        if (!empty($snapshot['pages']) && is_array($snapshot['pages'])) {
            foreach ($snapshot['pages'] as $page) {
                if (!is_array($page)) {
                    continue;
                }
                $code = isset($page['status_code']) ? intval($page['status_code']) : 0;
                if ($code !== 404) {
                    continue;
                }
                $raw_url = isset($page['url']) ? (string) $page['url'] : '';
                $normalized = $this->normalize_404_url($raw_url);
                if ($normalized !== '') {
                    $url_set[$normalized] = $normalized;
                }
            }
        }

        if (!empty($snapshot['issues']) && is_array($snapshot['issues'])) {
            foreach ($snapshot['issues'] as $issue) {
                if (!is_array($issue)) {
                    continue;
                }
                $title = strtolower((string) ($issue['title'] ?? ''));
                $type = strtolower((string) ($issue['type'] ?? ''));
                $category = strtolower((string) ($issue['category'] ?? ''));
                $is_404_issue = (strpos($title, '404') !== false || strpos($type, '404') !== false || strpos($category, '404') !== false);
                if (!$is_404_issue) {
                    continue;
                }
                $this->collect_site_audit_urls_recursive($issue, $url_set);
            }
        }

        if (empty($url_set)) {
            return array();
        }

        $rows = array();
        $search = trim((string) $search);
        foreach ($url_set as $normalized_url) {
            if ($search !== '' && stripos($normalized_url, $search) === false) {
                continue;
            }

            // Only "all" and "recent" make sense for snapshot-only rows.
            if ($filter === 'high-impact' || $filter === 'with-referer' || $filter === 'crawlers') {
                continue;
            }
            if ($filter === 'recent' && (current_time('timestamp') - $updated_ts) > WEEK_IN_SECONDS) {
                continue;
            }
            if ($filter === 'security-noise' && !$this->is_security_noise_url($normalized_url)) {
                continue;
            }

            $row = new stdClass();
            $row->id = 0;
            $row->url = $normalized_url;
            $row->referer = '';
            $row->user_agent = '';
            $row->ip_address = '';
            $row->hit_count = 0;
            $row->is_bot = 0;
            $row->is_ignored = 0;
            $row->suggested_url = '';
            $row->last_hit = $updated_at;
            $row->timestamp = intval(get_gmt_from_date($updated_at, 'U'));
            $row->source = 'site_audit';
            $row->read_only = 1;
            $classification = $this->classify_404_noise($normalized_url);
            $row->noise_class = (string) ($classification['class'] ?? 'seo_candidate');
            $row->noise_reason = (string) ($classification['reason'] ?? 'public_404');
            $row->is_security_noise = in_array($row->noise_class, array('security_noise', 'junk_query'), true) ? 1 : 0;
            $row->is_admin_noise = $row->noise_class === 'admin_noise' ? 1 : 0;

            // Ensure Site Audit rows can still be acted on (redirect/gone/ignore) via local DB id.
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
            $existing_id = intval($wpdb->get_var($wpdb->prepare(
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Internal AYSA 404 tracker table name is derived from the WordPress prefix and a fixed suffix.
                "SELECT id FROM {$this->table_name} WHERE url = %s LIMIT 1",
                $normalized_url
            )));
            if ($existing_id > 0) {
                $row->id = $existing_id;
                $row->read_only = 0;
            } else {
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
                $inserted = $wpdb->insert(
                    $this->table_name,
                    array(
                        'url' => $normalized_url,
                        'referer' => '',
                        'user_agent' => 'site_audit',
                        'ip_address' => '',
                        'is_bot' => 0,
                        'is_ignored' => 0,
                        'suggested_url' => '',
                        'first_hit' => $updated_at,
                        'last_hit' => $updated_at,
                        'hit_count' => 0,
                    ),
                    array('%s', '%s', '%s', '%s', '%d', '%d', '%s', '%s', '%s', '%d')
                );
                if ($inserted !== false && intval($wpdb->insert_id) > 0) {
                    $row->id = intval($wpdb->insert_id);
                    $row->read_only = 0;
                }
            }
            $rows[] = $row;
        }

        return $rows;
    }

    /**
     * Build additional 404 rows from local tracker table.
     *
     * @since 1.0.0
     * @param string $filter
     * @param string $search
     * @return array
     */
    private function get_local_tracker_404_rows($filter = 'all', $search = '') {
        global $wpdb;

        $where = array('is_ignored = 0');
        $params = array();
        $search = trim((string) $search);

        if ($search !== '') {
            $like = '%' . $wpdb->esc_like($search) . '%';
            $where[] = '(url LIKE %s OR referer LIKE %s)';
            $params[] = $like;
            $params[] = $like;
        }

        if ($filter === 'crawlers') {
            $where[] = 'is_bot = 1';
        } elseif ($filter === 'with-referer') {
            $where[] = "referer <> ''";
        } elseif ($filter === 'recent') {
            $where[] = 'last_hit >= %s';
            $params[] = gmdate('Y-m-d H:i:s', current_time('timestamp', true) - WEEK_IN_SECONDS);
        }

        $sql = "SELECT * FROM {$this->table_name}";
        if (!empty($where)) {
            $sql .= ' WHERE ' . implode(' AND ', $where);
        }
        $sql .= ' ORDER BY last_hit DESC LIMIT 500';

        if (!empty($params)) {
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query: table identifiers and structure are internal, and request-derived values are prepared before execution. Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
            $results = $wpdb->get_results($wpdb->prepare($sql, $params));
        } else {
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query: table identifiers and structure are internal, and request-derived values are prepared before execution. Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
            $results = $wpdb->get_results($sql);
        }

        if (!is_array($results)) {
            return array();
        }

        $rows = array();
        foreach ($results as $row) {
            if (!is_object($row)) {
                continue;
            }

            $normalized = $this->normalize_404_url(isset($row->url) ? $row->url : '');
            if ($normalized === '') {
                continue;
            }

            $row->url = $normalized;
            $row->source = 'tracker';
            $row->read_only = 0;
            $classification = $this->classify_404_noise(
                $normalized,
                isset($row->referer) ? $row->referer : '',
                isset($row->user_agent) ? $row->user_agent : ''
            );
            $row->noise_class = (string) ($classification['class'] ?? 'seo_candidate');
            $row->noise_reason = (string) ($classification['reason'] ?? 'public_404');
            $row->is_security_noise = in_array($row->noise_class, array('security_noise', 'junk_query'), true) ? 1 : 0;
            $row->is_admin_noise = $row->noise_class === 'admin_noise' ? 1 : 0;
            $timestamp = isset($row->last_hit) ? strtotime((string) $row->last_hit) : false;
            $row->timestamp = $timestamp !== false ? intval($timestamp) : current_time('timestamp');
            $rows[] = $row;
        }

        return $rows;
    }

    /**
     * AJAX handler for getting 404 errors
     *
     * @since    1.0.0
     */
    public function get_404_errors() {
        global $wpdb;
        
        // Debug logging
        aysa_debug_log('[AYSA 404 TRACKER] get_404_errors() called');
        
        // Check nonce for security
        if (!check_ajax_referer('aysa_nonce', 'nonce', false)) {
            aysa_debug_log('[AYSA 404 TRACKER] Nonce verification failed');
            wp_send_json_error('Security check failed');
        }
        
        // Make sure user has permission
        if (!current_user_can('manage_options')) {
            aysa_debug_log('[AYSA 404 TRACKER] Permission denied for user');
            wp_send_json_error(__('Permission denied', 'aysaaiseo'));
        }
        
        // Get parameters
        $page = isset($_GET['page']) ? intval(wp_unslash($_GET['page'])) : 1;
        $per_page = isset($_GET['per_page']) ? intval(wp_unslash($_GET['per_page'])) : 25;
        $filter = isset($_GET['filter']) ? sanitize_text_field(wp_unslash($_GET['filter'])) : 'all';
        $search = isset($_GET['search']) ? sanitize_text_field(wp_unslash($_GET['search'])) : '';
        $sort_by = isset($_GET['sort_by']) ? sanitize_key((string) wp_unslash($_GET['sort_by'])) : 'last_hit';
        $sort_dir = isset($_GET['sort_dir']) ? strtolower(sanitize_key((string) wp_unslash($_GET['sort_dir']))) : 'desc';
        if (!in_array($sort_dir, array('asc', 'desc'), true)) {
            $sort_dir = 'desc';
        }
        $allowed_sort_by = array('last_hit', 'url', 'hits', 'referer', 'source', 'confidence');
        if (!in_array($sort_by, $allowed_sort_by, true)) {
            $sort_by = 'last_hit';
        }
        
        // Calculate offset
        $offset = ($page - 1) * $per_page;
        
        $site_audit_rows = $this->get_site_audit_404_rows($filter, $search);
        $tracker_rows = $this->get_local_tracker_404_rows($filter, $search);
        aysa_debug_log('[AYSA 404 TRACKER] Found ' . count($site_audit_rows) . ' site audit 404 rows');
        aysa_debug_log('[AYSA 404 TRACKER] Found ' . count($tracker_rows) . ' local tracker 404 rows');

        $merged = array();
        $tracker_count = 0;
        $site_audit_count = 0;
        $seen = array();
        foreach ($site_audit_rows as $row) {
            $normalized = $this->normalize_404_url(isset($row->url) ? $row->url : '');
            if ($normalized !== '' && isset($seen[$normalized])) {
                continue;
            }
            if ($normalized !== '') {
                $seen[$normalized] = true;
            }
            $merged[] = $row;
            $site_audit_count++;
        }
        foreach ($tracker_rows as $row) {
            $normalized = $this->normalize_404_url(isset($row->url) ? $row->url : '');
            if ($normalized !== '' && isset($seen[$normalized])) {
                continue;
            }
            if ($normalized !== '') {
                $seen[$normalized] = true;
            }
            $merged[] = $row;
            $tracker_count++;
        }

        if ($filter === 'all') {
            // Default view should focus on actionable public 404s.
            $merged = array_values(array_filter($merged, function($row) {
                $is_security = isset($row->is_security_noise)
                    ? intval($row->is_security_noise) === 1
                    : $this->is_security_noise_url(isset($row->url) ? $row->url : '');
                $is_admin = isset($row->is_admin_noise)
                    ? intval($row->is_admin_noise) === 1
                    : $this->is_admin_noise_referer(isset($row->referer) ? $row->referer : '');
                return !$is_security && !$is_admin;
            }));
            $tracker_count = count(array_filter($merged, function($row) {
                return isset($row->source) && $row->source === 'tracker';
            }));
            $site_audit_count = count(array_filter($merged, function($row) {
                return isset($row->source) && $row->source === 'site_audit';
            }));
        } elseif ($filter === 'all-raw') {
            // Keep as-is from Site Audit snapshot.
        } elseif ($filter === 'security-noise') {
            $merged = array_values(array_filter($merged, function($row) {
                if (isset($row->is_security_noise)) {
                    return intval($row->is_security_noise) === 1;
                }
                return $this->is_security_noise_url(isset($row->url) ? $row->url : '');
            }));
            $tracker_count = count(array_filter($merged, function($row) {
                return isset($row->source) && $row->source === 'tracker';
            }));
            $site_audit_count = count(array_filter($merged, function($row) {
                return isset($row->source) && $row->source === 'site_audit';
            }));
        } elseif ($filter === 'internal-admin') {
            $merged = array_values(array_filter($merged, function($row) {
                if (isset($row->is_admin_noise)) {
                    return intval($row->is_admin_noise) === 1;
                }
                return $this->is_admin_noise_referer(isset($row->referer) ? $row->referer : '');
            }));
            $tracker_count = count(array_filter($merged, function($row) {
                return isset($row->source) && $row->source === 'tracker';
            }));
            $site_audit_count = count(array_filter($merged, function($row) {
                return isset($row->source) && $row->source === 'site_audit';
            }));
        }

        if ($sort_by === 'confidence') {
            foreach ($merged as &$row) {
                $best = $this->suggest_best_redirect_target(isset($row->url) ? $row->url : '');
                $row->_sort_confidence = isset($best['confidence']) ? intval($best['confidence']) : 0;
                $row->_best_redirect_data = $best;
            }
            unset($row);
        }

        usort($merged, function($a, $b) use ($sort_by, $sort_dir) {
            $multiplier = ($sort_dir === 'asc') ? 1 : -1;
            $av = null;
            $bv = null;

            switch ($sort_by) {
                case 'url':
                    $av = strtolower((string) ($a->url ?? ''));
                    $bv = strtolower((string) ($b->url ?? ''));
                    break;
                case 'hits':
                    $av = intval($a->hit_count ?? 0);
                    $bv = intval($b->hit_count ?? 0);
                    break;
                case 'referer':
                    $av = strtolower((string) ($a->referer ?? ''));
                    $bv = strtolower((string) ($b->referer ?? ''));
                    break;
                case 'source':
                    $av = strtolower((string) ($a->source ?? ''));
                    $bv = strtolower((string) ($b->source ?? ''));
                    break;
                case 'confidence':
                    $av = intval($a->_sort_confidence ?? 0);
                    $bv = intval($b->_sort_confidence ?? 0);
                    break;
                case 'last_hit':
                default:
                    $av = intval($a->timestamp ?? 0);
                    $bv = intval($b->timestamp ?? 0);
                    break;
            }

            if ($av === $bv) {
                return 0;
            }
            if (is_int($av) && is_int($bv)) {
                return ($av < $bv ? -1 : 1) * $multiplier;
            }

            return (strcmp((string) $av, (string) $bv) < 0 ? -1 : 1) * $multiplier;
        });

        $total = count($merged);
        $errors = array_slice($merged, $offset, $per_page);

        foreach ($errors as &$error_row) {
            $best = (isset($error_row->_best_redirect_data) && is_array($error_row->_best_redirect_data))
                ? $error_row->_best_redirect_data
                : $this->suggest_best_redirect_target(isset($error_row->url) ? $error_row->url : '');
            $legacy_suggested = isset($error_row->suggested_url) ? trim((string) $error_row->suggested_url) : '';
            $best_url = isset($best['target_url']) ? (string) $best['target_url'] : '';
            if ($best_url === '' && $legacy_suggested !== '') {
                $best_url = $legacy_suggested;
            }
            $error_row->best_redirect_url = $best_url;
            $error_row->best_redirect_confidence = isset($best['confidence']) ? intval($best['confidence']) : 0;
            $error_row->best_redirect_status = isset($best['status']) ? (string) $best['status'] : 'none';
            $error_row->best_redirect_action = isset($best['action']) ? (string) $best['action'] : 'none';
            $error_row->best_redirect_reason = isset($best['reason']) ? (string) $best['reason'] : '';
            $error_row->best_redirect_match_type = isset($best['match_type']) ? (string) $best['match_type'] : '';
            $error_row->best_redirect_match_title = isset($best['match_title']) ? (string) $best['match_title'] : '';
            unset($error_row->_best_redirect_data, $error_row->_sort_confidence);
        }
        unset($error_row);

        $this->track_wallet_usage(
            'aysa_get_404_errors',
            'get_404_errors',
            12,
            array(
                'module' => 'technical_optimization',
                'filter' => $filter,
                'search' => $search,
                'page' => intval($page),
                'per_page' => intval($per_page),
                'result_count' => count($errors),
                'total' => intval($total),
            ),
            '/v1/technical/404/list'
        );

        // Send response
        wp_send_json_success(array(
            'errors' => $errors,
            'total' => intval($total),
            'pages' => ceil($total / $per_page),
            'current_page' => $page,
            'per_page' => $per_page,
            'sort_by' => $sort_by,
            'sort_dir' => $sort_dir,
            'pagination' => array(
                'current_page' => $page,
                'total_pages' => max(1, (int) ceil($total / max(1, $per_page))),
                'per_page' => $per_page,
                'total' => intval($total),
            ),
            'data_sources' => array(
                'tracker' => $tracker_count,
                'site_audit' => $site_audit_count,
            ),
        ));
    }

    /**
     * Create a 301 redirect for a 404 error
     * 
     * @since 1.0.0
     */
    public function create_redirect() {
        global $wpdb;
        
        // Check nonce for security
        check_ajax_referer('aysa_nonce', 'nonce');
        
        // Make sure user has permission
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        // Get parameters
        $error_id = isset($_POST['error_id']) ? intval(wp_unslash($_POST['error_id'])) : 0;
        $target_url = isset($_POST['target_url']) ? esc_url_raw(wp_unslash($_POST['target_url'])) : '';
        
        // Validate data
        if (empty($error_id) || empty($target_url)) {
            wp_send_json_error('Missing required parameters');
        }
        
        // Get error details
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $error = $wpdb->get_row($wpdb->prepare(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Internal AYSA 404 tracker table name is derived from the WordPress prefix and a fixed suffix.
            "SELECT * FROM {$this->table_name} WHERE id = %d",
            $error_id
        ));
        
        if (!$error) {
            wp_send_json_error('Error not found');
        }
        
        // Get the path part of the source URL
        $source_url = wp_parse_url($error->url, PHP_URL_PATH);
        if (empty($source_url)) {
            $source_url = '/';
        }
        
        // Get the target URL path if it's a local URL
        if (strpos($target_url, home_url()) === 0) {
            $target_url = wp_parse_url($target_url, PHP_URL_PATH);
        }
        
        // Add to .htaccess file
        $htaccess_file = ABSPATH . '.htaccess';
        
        // Check if file exists and is writable
        if (!file_exists($htaccess_file)) {
            wp_send_json_error('The .htaccess file does not exist in the WordPress root directory');
        }
        
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Admin-authorized WordPress-root file operations retain a fail-closed writability preflight before the guarded write.
        if (!is_writable($htaccess_file)) {
            wp_send_json_error('The .htaccess file is not writable. Please check file permissions');
        }
        
        // Get current .htaccess content
        $htaccess_content = file_get_contents($htaccess_file);
        
        // Check if our section exists
        $aysa_section_start = '# BEGIN Aysa Redirects';
        $aysa_section_end = '# END Aysa Redirects';
        
        $redirect_rule = 'RedirectMatch 301 ^' . preg_quote($source_url, '/') . '$ ' . $target_url;
        
        if (strpos($htaccess_content, $aysa_section_start) !== false) {
            // Section exists, add redirect inside
            $pattern = '/' . preg_quote($aysa_section_start, '/') . '(.*?)' . preg_quote($aysa_section_end, '/') . '/s';
            if (preg_match($pattern, $htaccess_content, $matches)) {
                $existing_content = $matches[1];
                
                // Check if this redirect already exists
                if (strpos($existing_content, $redirect_rule) !== false) {
                    wp_send_json_error('This redirect already exists in .htaccess');
                }
                
                // Add new redirect rule
                $new_content = $aysa_section_start . "\n" . $existing_content . $redirect_rule . "\n" . $aysa_section_end;
                $htaccess_content = preg_replace($pattern, $new_content, $htaccess_content);
            }
        } else {
            // Section doesn't exist, add it at the beginning of the file
            $new_section = $aysa_section_start . "\n" . $redirect_rule . "\n" . $aysa_section_end . "\n\n";
            $htaccess_content = $new_section . $htaccess_content;
        }
        
        // Write the updated content back to .htaccess
        // phpcs:ignore PluginCheck.CodeAnalysis.WriteFile.ABSPATHDetected -- Admin-authorized redirect operation intentionally updates site-root .htaccess after nonce, capability, path and writability checks.
        $result = file_put_contents($htaccess_file, $htaccess_content);
        
        if ($result === false) {
            wp_send_json_error('Failed to write to .htaccess file');
        }
        
        // Update error status in database
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $wpdb->update(
            $this->table_name,
            array('is_ignored' => 1),
            array('id' => $error_id),
            array('%d'),
            array('%d')
        );
        
        // Calculate confidence score for redirect
        $confidence_score = null;
        if (class_exists('Aysa_Confidence_Scorer')) {
            $confidence_scorer = new Aysa_Confidence_Scorer();
            $confidence_score = $confidence_scorer->calculate_redirect_confidence($error->url, $target_url);
        }
        
        if (class_exists('Aysa_Audit_Logger')) {
            $audit_logger = new Aysa_Audit_Logger();
            $audit_logger->log_action(
                'redirect_creation',
                0,
                'redirect',
                array('url' => $error->url),
                array('url' => $target_url, 'type' => '301'),
                $confidence_score,
                false
            );
        }
        
        do_action('aysa_automation_completed', array(
            'action_type' => 'redirect_creation',
            'object_id' => $error_id,
            'object_type' => 'redirect',
            'confidence_score' => $confidence_score,
            'data' => array(
                'source_url' => $error->url,
                'target_url' => $target_url
            )
        ));

        $this->track_wallet_usage(
            'aysa_create_redirect',
            'create_redirect',
            12,
            array(
                'module' => 'technical_optimization',
                'error_id' => intval($error_id),
                'source' => (string) $error->url,
                'target' => (string) $target_url,
            ),
            '/v1/technical/404/redirect'
        );
        
        // Send success response
        wp_send_json_success(array(
            'message' => 'Redirect created successfully',
            'source' => $error->url,
            'target' => $target_url,
            'confidence_score' => $confidence_score
        ));
    }
    
    /**
     * Mark a URL as gone (410)
     * 
     * @since 1.0.0
     */
    public function mark_gone() {
        global $wpdb;
        
        // Check nonce for security
        check_ajax_referer('aysa_nonce', 'nonce');
        
        // Make sure user has permission
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        // Get parameters
        $error_id = isset($_POST['error_id']) ? intval(wp_unslash($_POST['error_id'])) : 0;
        $reason = isset($_POST['reason']) ? sanitize_text_field(wp_unslash($_POST['reason'])) : '';
        
        // Validate data
        if (empty($error_id)) {
            wp_send_json_error('Missing required parameters');
        }
        
        // Get error details
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $error = $wpdb->get_row($wpdb->prepare(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Internal AYSA 404 tracker table name is derived from the WordPress prefix and a fixed suffix.
            "SELECT * FROM {$this->table_name} WHERE id = %d",
            $error_id
        ));
        
        if (!$error) {
            wp_send_json_error('Error not found');
        }
        
        // Get the path part of the URL
        $url_path = wp_parse_url($error->url, PHP_URL_PATH);
        if (empty($url_path)) {
            $url_path = '/';
        }
        
        // Add to .htaccess file
        $htaccess_file = ABSPATH . '.htaccess';
        
        // Check if file exists and is writable
        if (!file_exists($htaccess_file)) {
            wp_send_json_error('The .htaccess file does not exist in the WordPress root directory');
        }
        
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Admin-authorized WordPress-root file operations retain a fail-closed writability preflight before the guarded write.
        if (!is_writable($htaccess_file)) {
            wp_send_json_error('The .htaccess file is not writable. Please check file permissions');
        }
        
        // Get current .htaccess content
        $htaccess_content = file_get_contents($htaccess_file);
        
        // Check if our section exists
        $aysa_section_start = '# BEGIN Aysa Redirects';
        $aysa_section_end = '# END Aysa Redirects';
        
        $gone_rule = 'RedirectMatch 410 ^' . preg_quote($url_path, '/') . '$';
        
        if (strpos($htaccess_content, $aysa_section_start) !== false) {
            // Section exists, add gone rule inside
            $pattern = '/' . preg_quote($aysa_section_start, '/') . '(.*?)' . preg_quote($aysa_section_end, '/') . '/s';
            if (preg_match($pattern, $htaccess_content, $matches)) {
                $existing_content = $matches[1];
                
                // Check if this gone rule already exists
                if (strpos($existing_content, $gone_rule) !== false) {
                    wp_send_json_error('This gone rule already exists in .htaccess');
                }
                
                // Check for 301 redirect for the same URL and remove it
                $redirect_pattern = 'RedirectMatch 301 \^' . preg_quote(preg_quote($url_path, '/'), '/') . '\$';
                $cleaned_content = preg_replace('/[\r\n]*' . $redirect_pattern . '.*[\r\n]*/i', "\n", $existing_content);
                
                // Add new gone rule
                $new_content = $aysa_section_start . "\n" . $cleaned_content . $gone_rule . "\n" . $aysa_section_end;
                $htaccess_content = preg_replace($pattern, $new_content, $htaccess_content);
            }
        } else {
            // Section doesn't exist, add it at the beginning of the file
            $new_section = $aysa_section_start . "\n" . $gone_rule . "\n" . $aysa_section_end . "\n\n";
            $htaccess_content = $new_section . $htaccess_content;
        }
        
        // Write the updated content back to .htaccess
        // phpcs:ignore PluginCheck.CodeAnalysis.WriteFile.ABSPATHDetected -- Admin-authorized redirect operation intentionally updates site-root .htaccess after nonce, capability, path and writability checks.
        $result = file_put_contents($htaccess_file, $htaccess_content);
        
        if ($result === false) {
            wp_send_json_error('Failed to write to .htaccess file');
        }
        
        // Update error status in database
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $wpdb->update(
            $this->table_name,
            array('is_ignored' => 1),
            array('id' => $error_id),
            array('%d'),
            array('%d')
        );
        
        do_action('aysa_automation_completed', array(
            'action_type' => 'gone_url_creation',
            'object_id' => $error_id,
            'object_type' => 'gone_url',
            'confidence_score' => 90.0,
            'data' => array(
                'url' => $error->url
            )
        ));

        $this->track_wallet_usage(
            'aysa_mark_gone',
            'mark_gone',
            12,
            array(
                'module' => 'technical_optimization',
                'error_id' => intval($error_id),
                'url' => (string) $error->url,
                'reason' => (string) $reason,
            ),
            '/v1/technical/404/gone'
        );
        
        // Send success response
        wp_send_json_success(array(
            'message' => 'URL marked as gone (410) successfully',
            'url' => $error->url
        ));
    }
    
    /**
     * Ignore a 404 error (don't show it in the list anymore)
     * 
     * @since 1.0.0
     */
    public function ignore_404() {
        global $wpdb;
        
        // Check nonce for security
        check_ajax_referer('aysa_nonce', 'nonce');
        
        // Make sure user has permission
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Permission denied');
        }
        
        // Get parameters
        $error_id = isset($_POST['error_id']) ? intval(wp_unslash($_POST['error_id'])) : 0;
        
        // Validate data
        if (empty($error_id)) {
            wp_send_json_error('Missing required parameters');
        }
        
        // Update error status in database
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $result = $wpdb->update(
            $this->table_name,
            array('is_ignored' => 1),
            array('id' => $error_id),
            array('%d'),
            array('%d')
        );
        
        if ($result === false) {
            wp_send_json_error('Failed to ignore 404 error');
        }

        $this->track_wallet_usage(
            'aysa_ignore_404',
            'ignore_404',
            12,
            array(
                'module' => 'technical_optimization',
                'error_id' => intval($error_id),
            ),
            '/v1/technical/404/ignore'
        );
        
        // Send success response
        wp_send_json_success(array(
            'message' => '404 error ignored successfully'
        ));
    }
}                        
