<?php

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

class SEO_Fury_404_Monitor {

    private $table_name;
    private $redirects_table;

    public function __construct($table_name, $redirects_table) {
        $this->table_name = $table_name;
        $this->redirects_table = $redirects_table;
        $this->init_hooks();
    }

private function init_hooks() {
    add_filter('status_header', array($this, 'check_404_status'), 10, 2);
    add_action('template_redirect', array($this, 'log_404_error'), 999);

    add_action('wp_ajax_seo_load_404_log', array($this, 'ajax_load_404_log'));
    add_action('wp_ajax_seo_clear_404_log', array($this, 'ajax_clear_404_log'));
    add_action('wp_ajax_seo_find_similar_pages', array($this, 'ajax_find_similar_pages'));
    add_action('wp_ajax_seo_mark_404_resolved', array($this, 'ajax_mark_404_resolved'));
    add_action('wp_ajax_seo_delete_404_entry', array($this, 'ajax_delete_404_entry'));
    add_action('wp_ajax_seo_create_redirect_from_404', array($this, 'ajax_create_redirect_from_404'));
    add_action('wp_ajax_seo_export_404_logs_csv', array($this, 'ajax_export_404_logs_csv'));
    add_action('seo_fury_404_email_digest', array($this, 'send_digest_email'));
}

    public function check_404_status($status_header, $code) {
        $this->debug_log('check_404_status called', array('code' => $code, 'status_header' => $status_header));
        // This filter fires for ANY 404 status header, including ones other
        // plugins emit for REST/AJAX/cron/feed sub-requests. The force path
        // skips is_404(), so without these context guards we would log a lot of
        // non-page 404s (false positives).
        if ($code == 404
            && ! is_admin()
            && ! wp_doing_ajax()
            && ! ( defined( 'REST_REQUEST' ) && REST_REQUEST )
            && ! ( defined( 'DOING_CRON' ) && DOING_CRON )
            && ! is_feed()
        ) {
            $this->debug_log('404 status detected in check_404_status, calling log_404_error with force=true');
            $this->log_404_error(true);
        }
        return $status_header;
    }

    public function register_settings() {
        $sanitize_checkbox = array('sanitize_callback' => 'absint');
        $sanitize_int      = array('sanitize_callback' => 'absint');

        register_setting('seo_fury_404_group', 'seo_fury_404_monitoring_enabled', $sanitize_checkbox);
        register_setting('seo_fury_404_group', 'seo_fury_404_ignore_bots', $sanitize_checkbox);
        register_setting('seo_fury_404_group', 'seo_fury_404_trust_proxy', $sanitize_checkbox);
        register_setting('seo_fury_404_group', 'seo_fury_404_auto_cleanup_days', $sanitize_int);
        register_setting('seo_fury_404_group', 'seo_fury_404_debug_log', $sanitize_checkbox);
        register_setting('seo_fury_404_group', 'seo_fury_404_email_enabled', $sanitize_checkbox);
        register_setting('seo_fury_404_group', 'seo_fury_404_email_address', array('sanitize_callback' => 'sanitize_email'));
        register_setting('seo_fury_404_group', 'seo_fury_404_email_frequency', array('sanitize_callback' => 'sanitize_text_field'));
        register_setting('seo_fury_404_group', 'seo_fury_404_email_threshold', $sanitize_int);

        add_settings_section(
            'seo_fury_404_section',
            __( '404 Monitor Settings', 'seo-fury' ),
            array($this, 'section_callback'),
            'seo_fury_404_group'
        );

        add_settings_field(
            'seo_fury_404_monitoring_enabled',
            __( 'Enable Monitoring', 'seo-fury' ),
            array($this, 'monitoring_enabled_callback'),
            'seo_fury_404_group',
            'seo_fury_404_section'
        );

        add_settings_field(
            'seo_fury_404_ignore_bots',
            __( 'Ignore Bots', 'seo-fury' ),
            array($this, 'ignore_bots_callback'),
            'seo_fury_404_group',
            'seo_fury_404_section'
        );

        add_settings_field(
            'seo_fury_404_trust_proxy',
            __( 'Behind a Proxy / CDN', 'seo-fury' ),
            array($this, 'trust_proxy_callback'),
            'seo_fury_404_group',
            'seo_fury_404_section'
        );

        add_settings_field(
            'seo_fury_404_auto_cleanup',
            __( 'Auto Cleanup', 'seo-fury' ),
            array($this, 'auto_cleanup_callback'),
            'seo_fury_404_group',
            'seo_fury_404_section'
        );

        add_settings_field(
            'seo_fury_404_debug_log',
            __( 'Debug Logging', 'seo-fury' ),
            array($this, 'debug_log_callback'),
            'seo_fury_404_group',
            'seo_fury_404_section'
        );

        add_settings_field(
            'seo_fury_404_email_enabled',
            __( 'Email Notifications', 'seo-fury' ),
            array($this, 'email_enabled_callback'),
            'seo_fury_404_group',
            'seo_fury_404_section'
        );

        add_settings_field(
            'seo_fury_404_email_address',
            __( 'Notification Email', 'seo-fury' ),
            array($this, 'email_address_callback'),
            'seo_fury_404_group',
            'seo_fury_404_section'
        );

        add_settings_field(
            'seo_fury_404_email_frequency',
            __( 'Email Frequency', 'seo-fury' ),
            array($this, 'email_frequency_callback'),
            'seo_fury_404_group',
            'seo_fury_404_section'
        );

        add_settings_field(
            'seo_fury_404_email_threshold',
            __( 'Email Threshold', 'seo-fury' ),
            array($this, 'email_threshold_callback'),
            'seo_fury_404_group',
            'seo_fury_404_section'
        );
    }

    public function section_callback() {
        echo '<p>' . esc_html__( 'Track and analyze 404 errors on your site.', 'seo-fury' ) . '</p>';
    }

    public function monitoring_enabled_callback() {
        $enabled = get_option('seo_fury_404_monitoring_enabled', 1);
        ?>
        <label>
            <input type="checkbox" name="seo_fury_404_monitoring_enabled" value="1" <?php checked($enabled, 1); ?>>
            <?php esc_html_e( 'Enable 404 tracking', 'seo-fury' ); ?>
        </label>
        <?php
    }

    public function ignore_bots_callback() {
        $enabled = get_option('seo_fury_404_ignore_bots', 1);
        ?>
        <label>
            <input type="checkbox" name="seo_fury_404_ignore_bots" value="1" <?php checked($enabled, 1); ?>>
            <?php esc_html_e( 'Do not log requests from search bots', 'seo-fury' ); ?>
        </label>
        <p class="description"><?php esc_html_e( 'Ignores Googlebot, Bingbot, Yandex, and other known bots.', 'seo-fury' ); ?></p>
        <?php
    }

    public function trust_proxy_callback() {
        $enabled = get_option('seo_fury_404_trust_proxy', 0);
        ?>
        <label>
            <input type="checkbox" name="seo_fury_404_trust_proxy" value="1" <?php checked($enabled, 1); ?>>
            <?php esc_html_e( 'My site is behind a reverse proxy or CDN (e.g. Cloudflare, Nginx, a load balancer)', 'seo-fury' ); ?>
        </label>
        <p class="description"><?php esc_html_e( 'When enabled, the real visitor IP is read from forwarded headers (CF-Connecting-IP, X-Forwarded-For). Leave OFF on standard hosting — otherwise visitors can spoof the logged IP address.', 'seo-fury' ); ?></p>
        <?php
    }

    public function auto_cleanup_callback() {
        $days = get_option('seo_fury_404_auto_cleanup_days', 30);
        ?>
        <input type="number" name="seo_fury_404_auto_cleanup_days" value="<?php echo esc_attr($days); ?>" min="0" max="365" class="small-text">
        <span><?php esc_html_e( 'days', 'seo-fury' ); ?></span>
        <p class="description"><?php esc_html_e( 'Automatically delete entries older than this number of days. 0 = never delete', 'seo-fury' ); ?></p>
        <?php
    }

    public function debug_log_callback() {
        $enabled = get_option('seo_fury_404_debug_log', false);
        ?>
        <label>
            <input type="checkbox" name="seo_fury_404_debug_log" value="1" <?php checked($enabled, 1); ?>>
            <?php esc_html_e( 'Enable verbose logging for diagnostics', 'seo-fury' ); ?>
        </label>
        <p class="description"><?php esc_html_e( 'Writes detailed module logs to debug.log. Use for troubleshooting. Logs are in wp-content/debug.log (when WP_DEBUG_LOG is enabled).', 'seo-fury' ); ?></p>
        <?php
    }

    public function email_enabled_callback() {
        $enabled = get_option('seo_fury_404_email_enabled', 0);
        ?>
        <label>
            <input type="checkbox" name="seo_fury_404_email_enabled" value="1" <?php checked($enabled, 1); ?>>
            <?php esc_html_e( 'Send email notifications for new 404 errors', 'seo-fury' ); ?>
        </label>
        <?php
    }

    public function email_address_callback() {
        $email = get_option('seo_fury_404_email_address', get_option('admin_email'));
        ?>
        <input type="email" name="seo_fury_404_email_address" value="<?php echo esc_attr($email); ?>" class="regular-text">
        <p class="description"><?php esc_html_e( 'Email address to receive 404 notifications. Defaults to admin email.', 'seo-fury' ); ?></p>
        <?php
    }

    public function email_frequency_callback() {
        $frequency = get_option('seo_fury_404_email_frequency', 'daily');
        ?>
        <select name="seo_fury_404_email_frequency">
            <option value="instant" <?php selected($frequency, 'instant'); ?>><?php esc_html_e( 'Instant (each new 404)', 'seo-fury' ); ?></option>
            <option value="hourly" <?php selected($frequency, 'hourly'); ?>><?php esc_html_e( 'Hourly digest', 'seo-fury' ); ?></option>
            <option value="daily" <?php selected($frequency, 'daily'); ?>><?php esc_html_e( 'Daily digest', 'seo-fury' ); ?></option>
            <option value="weekly" <?php selected($frequency, 'weekly'); ?>><?php esc_html_e( 'Weekly digest', 'seo-fury' ); ?></option>
        </select>
        <p class="description"><?php esc_html_e( 'How often to send 404 notification emails.', 'seo-fury' ); ?></p>
        <?php
    }

    public function email_threshold_callback() {
        $threshold = get_option('seo_fury_404_email_threshold', 5);
        ?>
        <input type="number" name="seo_fury_404_email_threshold" value="<?php echo esc_attr($threshold); ?>" min="1" max="100" class="small-text">
        <p class="description"><?php esc_html_e( 'For digest mode: minimum number of new 404 errors before sending an email.', 'seo-fury' ); ?></p>
        <?php
    }

    /**
     * Send or queue a 404 email notification.
     *
     * @param string $url    The 404 URL.
     * @param string $referer The referer URL.
     * @param int    $hits   Number of hits.
     */
    private function maybe_send_404_email($url, $referer = '', $hits = 1) {
        $enabled = get_option('seo_fury_404_email_enabled', 0);
        if (!$enabled) {
            return;
        }

        $frequency = get_option('seo_fury_404_email_frequency', 'daily');

        if ($frequency === 'instant') {
            $this->send_instant_404_email($url, $referer, $hits);
        } else {
            $this->queue_404_for_digest($url, $referer, $hits);
        }
    }

    /**
     * Send an instant email for a single 404.
     */
    private function send_instant_404_email($url, $referer, $hits) {
        $to = get_option('seo_fury_404_email_address', get_option('admin_email'));
        if (!$to) return;

        $site_name = get_bloginfo('name');
        $brand = function_exists('seo_fury_get_brand_name') ? seo_fury_get_brand_name() : 'SEO Fury';

        $subject = sprintf(
            /* translators: 1: brand name, 2: site name */
            __('[%1$s] New 404 error on %2$s', 'seo-fury'),
            $brand,
            $site_name
        );

        /* translators: %s: URL of the 404 page */
        $message = sprintf(__("A new 404 error was detected on your site:\n\nURL: %s", 'seo-fury'), $this->log_url_to_absolute($url));
        if ($referer) {
            /* translators: %s: referrer URL */
            $message .= sprintf(__("\nReferrer: %s", 'seo-fury'), $referer);
        }
        if ($hits > 1) {
            /* translators: %d: number of hits */
            $message .= sprintf(__("\nTotal hits: %d", 'seo-fury'), $hits);
        }
        /* translators: %s: admin URL to manage 404 errors */
        $message .= sprintf(__("\n\nManage 404 errors: %s", 'seo-fury'), admin_url('admin.php?page=seo-fury&tab=log404'));

        wp_mail($to, $subject, $message);
    }

    /**
     * Queue a 404 URL for digest email.
     */
    private function queue_404_for_digest($url, $referer, $hits) {
        $queue = get_option('seo_fury_404_email_queue', array());
        $queue[] = array(
            'url'     => $url,
            'referer' => $referer,
            'hits'    => $hits,
            'time'    => current_time('mysql'),
        );
        // Keep max 200 in queue
        if (count($queue) > 200) {
            $queue = array_slice($queue, -200);
        }
        update_option('seo_fury_404_email_queue', $queue, false);

        // Schedule cron if not already
        $this->ensure_digest_cron();
    }

    /**
     * Ensure the digest cron event is scheduled.
     */
    private function ensure_digest_cron() {
        if (!wp_next_scheduled('seo_fury_404_email_digest')) {
            $frequency = get_option('seo_fury_404_email_frequency', 'daily');
            $recurrence = $frequency; // hourly, daily, weekly — all valid WP cron schedules
            if (!in_array($recurrence, array('hourly', 'daily', 'weekly'), true)) {
                $recurrence = 'daily';
            }
            wp_schedule_event(time(), $recurrence, 'seo_fury_404_email_digest');
        }
    }

    /**
     * Cron callback: send the digest email.
     */
    public function send_digest_email() {
        $enabled = get_option('seo_fury_404_email_enabled', 0);
        if (!$enabled) return;

        $queue = get_option('seo_fury_404_email_queue', array());
        if (empty($queue)) return;

        $threshold = get_option('seo_fury_404_email_threshold', 5);
        if (count($queue) < $threshold) return;

        $to = get_option('seo_fury_404_email_address', get_option('admin_email'));
        if (!$to) return;

        $site_name = get_bloginfo('name');
        $brand = function_exists('seo_fury_get_brand_name') ? seo_fury_get_brand_name() : 'SEO Fury';

        $subject = sprintf(
            /* translators: 1: brand name, 2: count, 3: site name */
            __('[%1$s] %2$d new 404 errors on %3$s', 'seo-fury'),
            $brand,
            count($queue),
            $site_name
        );

        /* translators: %d: number of new 404 errors */
        $message = sprintf(__("%d new 404 errors were detected on your site:\n\n", 'seo-fury'), count($queue));

        foreach (array_slice($queue, 0, 50) as $item) {
            $message .= '- ' . $this->log_url_to_absolute($item['url']);
            if (!empty($item['referer'])) {
                $message .= ' (ref: ' . $item['referer'] . ')';
            }
            $message .= "\n";
        }

        if (count($queue) > 50) {
            /* translators: %d: number of additional 404 errors not shown */
            $message .= sprintf(__("\n...and %d more.\n", 'seo-fury'), count($queue) - 50);
        }

        /* translators: %s: admin URL to manage 404 errors */
        $message .= sprintf(__("\nManage 404 errors: %s", 'seo-fury'), admin_url('admin.php?page=seo-fury&tab=log404'));

        wp_mail($to, $subject, $message);

        // Clear the queue
        delete_option('seo_fury_404_email_queue');
    }

    private function debug_log($message, $data = null) {
    }

    public function log_404_error($force_404 = false) {
        $this->debug_log('log_404_error called', array('force_404' => $force_404, 'hook' => current_filter()));

        $monitoring_enabled = get_option('seo_fury_404_monitoring_enabled', 1);
        $this->debug_log('Monitoring enabled check', array('enabled' => $monitoring_enabled));
        if (!$monitoring_enabled) {
            $this->debug_log('Monitoring disabled, exiting');
            return;
        }

        if (is_admin()) {
            $this->debug_log('Admin request, exiting');
            return;
        }

        if (defined('DOING_AJAX') && DOING_AJAX) {
            $this->debug_log('AJAX request, exiting');
            return;
        }

        if (defined('DOING_CRON') && DOING_CRON) {
            $this->debug_log('CRON request, exiting');
            return;
        }

        $url = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
        if (empty($url) && isset($_SERVER['REDIRECT_URL'])) {
            $url = sanitize_text_field(wp_unslash($_SERVER['REDIRECT_URL']));
        }
        $this->debug_log('URL extracted', array('url' => $url, 'REQUEST_URI' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '', 'REDIRECT_URL' => isset($_SERVER['REDIRECT_URL']) ? $_SERVER['REDIRECT_URL'] : ''));
        if (empty($url)) {
            $this->debug_log('Empty URL, exiting');
            return;
        }

        $extensions = array('jpg', 'jpeg', 'png', 'gif', 'css', 'js', 'ico', 'svg', 'woff', 'woff2', 'ttf', 'eot');
        $url_path = wp_parse_url($url, PHP_URL_PATH);
        if ($url_path) {
            $ext = strtolower(pathinfo($url_path, PATHINFO_EXTENSION));
            if (in_array($ext, $extensions)) {
                $this->debug_log('File extension ignored', array('ext' => $ext, 'url_path' => $url_path));
                return;
            }
        }

        if (!$force_404) {
            global $wp_query;
            $is_404 = false;

            if (function_exists('is_404') && is_404()) {
                $is_404 = true;
                $this->debug_log('is_404() returned true');
            } else {
                $this->debug_log('is_404() returned false');
            }

            if (!$is_404 && isset($wp_query) && is_object($wp_query)) {
                if (method_exists($wp_query, 'is_404') && $wp_query->is_404()) {
                    $is_404 = true;
                    $this->debug_log('$wp_query->is_404() returned true');
                }
                if (!$is_404 && isset($wp_query->query_vars['error']) && $wp_query->query_vars['error'] == '404') {
                    $is_404 = true;
                    $this->debug_log('query_vars[error] = 404');
                }
                $this->debug_log('$wp_query check', array('exists' => isset($wp_query), 'is_object' => isset($wp_query) && is_object($wp_query), 'query_vars' => isset($wp_query->query_vars) ? $wp_query->query_vars : null));
            } else {
                $this->debug_log('$wp_query not available or not object', array('exists' => isset($wp_query), 'is_object' => isset($wp_query) && is_object($wp_query)));
            }

            $this->debug_log('Final is_404 check', array('is_404' => $is_404, 'force_404' => $force_404));
            if (!$is_404) {
                $this->debug_log('Not a 404 error, exiting');
                return;
            }
        } else {
            $this->debug_log('Force 404 mode, skipping is_404() check');
        }

        global $wpdb;
        $debug = defined('WP_DEBUG') && WP_DEBUG;

        // Cache the table-exists check so a 404 flood (bot/scanner storm)
        // doesn't run a SHOW TABLES query on every uncached front-end request.
        $table_exists = get_transient('seo_fury_404_table_ok');
        if (!$table_exists) {
            $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $this->table_name));
            $this->debug_log('Table check', array('table_name' => $this->table_name, 'exists' => $table_exists));
            if (!$table_exists) {
                $this->debug_log('Table does not exist, attempting to create');
                $this->maybe_create_table();
                $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $this->table_name));
                if (!$table_exists) {
                    $this->debug_log('Table creation failed', array('last_error' => $wpdb->last_error));
                    return;
                }
                $this->debug_log('Table created successfully');
            }
            set_transient('seo_fury_404_table_ok', 1, DAY_IN_SECONDS);
        }

        static $logged_urls = array();
        $url_key = md5($url . (isset($_SERVER['HTTP_REFERER']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_REFERER'])) : ''));
        if (isset($logged_urls[$url_key])) {
            $this->debug_log('Already logged this URL in this request', array('url_key' => $url_key));
            return;
        }
        $logged_urls[$url_key] = true;
        $referer = isset($_SERVER['HTTP_REFERER']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_REFERER'])) : '';
        $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
        $ip_address = $this->get_client_ip();

        $this->debug_log('Preparing to log 404', array('url' => $url, 'ip' => $ip_address, 'user_agent' => substr($user_agent, 0, 50)));

        $ignore_bots = get_option('seo_fury_404_ignore_bots', 1);
        $is_bot = $this->is_bot($user_agent);
        $this->debug_log('Bot check', array('ignore_bots' => $ignore_bots, 'is_bot' => $is_bot, 'user_agent' => $user_agent));
        if ($ignore_bots && $is_bot) {
            $this->debug_log('Bot detected, exiting');
            return;
        }

        $existing = $wpdb->get_row($wpdb->prepare(
            "SELECT id, hits FROM {$this->table_name} WHERE url = %s AND resolved = 0",
            $url
        ));

        $this->debug_log('Existing record check', array('exists' => !empty($existing), 'id' => $existing ? $existing->id : null, 'hits' => $existing ? $existing->hits : null));

        if ($existing) {
            $updated = $wpdb->update(
                $this->table_name,
                array(
                    'hits' => $existing->hits + 1,
                    'last_seen' => current_time('mysql'),
                    'referer' => $referer,
                    'user_agent' => $user_agent,
                    'ip_address' => $ip_address
                ),
                array('id' => $existing->id),
                array('%d', '%s', '%s', '%s', '%s'),
                array('%d')
            );
            $this->debug_log('Update result', array('updated' => $updated, 'new_hits' => $existing->hits + 1, 'db_error' => $wpdb->last_error));
            if ($debug && $updated === false) {

            }
        } else {
            try {
                $core = SEO_Fury_Core::get_instance();
                $pro_features = $core->get_pro_features();

                if ($pro_features) {
                    $current_count = $wpdb->get_var("SELECT COUNT(*) FROM {$this->table_name}"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is set in constructor, not user input

                    if ($pro_features->is_limit_reached('max_404_logs', $current_count)) {
                        if (!$pro_features->is_pro()) {
                            $limit = $pro_features->get_limit('max_404_logs');
                            $to_delete = $current_count - $limit + 1;

                            if ($to_delete > 0) {
                                $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                                    $wpdb->prepare(
                                        "DELETE FROM {$this->table_name} ORDER BY first_seen ASC LIMIT %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from constructor
                                        $to_delete
                                    )
                                );
                            }
                        } else {
                            return;
                        }
                    }
                }
            } catch (Exception $e) {
            }

            $this->debug_log('Creating new 404 entry');
            $inserted = $wpdb->insert(
                $this->table_name,
                array(
                    'url' => $url,
                    'referer' => $referer,
                    'user_agent' => $user_agent,
                    'ip_address' => $ip_address,
                    'hits' => 1,
                    'first_seen' => current_time('mysql'),
                    'last_seen' => current_time('mysql'),
                    'resolved' => 0
                ),
                array('%s', '%s', '%s', '%s', '%d', '%s', '%s', '%d')
            );
            $this->debug_log('Insert result', array('inserted' => $inserted, 'insert_id' => $wpdb->insert_id, 'db_error' => $wpdb->last_error));
            if ($debug && $inserted === false) {
            }
            // Send email notification for new 404
            if ($inserted) {
                $this->maybe_send_404_email($url, $referer, 1);
            }
        }

        $this->debug_log('404 error logged successfully', array('url' => $url));

        if (wp_rand(1, 100) === 1) {
            $this->auto_cleanup();
        }
    }

    /**
     * Turn a logged 404 path (which already starts at the host root, e.g.
     * "/blog/missing") into an absolute URL. Using home_url() here would
     * double a subdirectory ("/blog/blog/missing"), so we prepend only the
     * scheme + host (+ port).
     */
    private function log_url_to_absolute($url) {
        if (preg_match('#^https?://#i', (string) $url)) {
            return $url;
        }
        $parts = wp_parse_url(home_url());
        $base  = (isset($parts['scheme']) ? $parts['scheme'] : 'https') . '://' . (isset($parts['host']) ? $parts['host'] : '');
        if (isset($parts['port'])) {
            $base .= ':' . $parts['port'];
        }
        return $base . '/' . ltrim((string) $url, '/');
    }

    private function get_client_ip() {
        // REMOTE_ADDR is the only header a client cannot spoof. Forwarded
        // headers (X-Forwarded-For, CF-Connecting-IP, Client-IP) are trusted
        // ONLY when the site explicitly declares it sits behind a reverse
        // proxy / CDN — otherwise any visitor could forge the logged IP.
        if ( get_option( 'seo_fury_404_trust_proxy', 0 ) ) {
            $candidates = array();

            if ( ! empty( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
                $candidates[] = wp_unslash( $_SERVER['HTTP_CF_CONNECTING_IP'] );
            }
            if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
                // May be a comma-separated chain; the left-most entry is the client.
                $parts = explode( ',', wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) );
                $candidates[] = trim( $parts[0] );
            }
            if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
                $candidates[] = wp_unslash( $_SERVER['HTTP_CLIENT_IP'] );
            }

            foreach ( $candidates as $candidate ) {
                $candidate = sanitize_text_field( $candidate );
                if ( filter_var( $candidate, FILTER_VALIDATE_IP ) ) {
                    return $candidate;
                }
            }
        }

        $remote = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';

        return filter_var( $remote, FILTER_VALIDATE_IP ) ? $remote : '';
    }

    private function is_bot($user_agent) {
        $bots = array(
            'googlebot', 'bingbot', 'slurp', 'duckduckbot', 'baiduspider',
            'yandexbot', 'sogou', 'exabot', 'facebot', 'ia_archiver'
        );

        $user_agent_lower = strtolower($user_agent);

        foreach ($bots as $bot) {
            if (strpos($user_agent_lower, $bot) !== false) {
                return true;
            }
        }

        return false;
    }

    private function maybe_create_table() {
        global $wpdb;

        $charset_collate = $wpdb->get_charset_collate();

        $sql = "CREATE TABLE IF NOT EXISTS {$this->table_name} (
            id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            url varchar(500) NOT NULL,
            referer varchar(500) DEFAULT NULL,
            user_agent varchar(255) DEFAULT NULL,
            ip_address varchar(45) DEFAULT NULL,
            hits bigint(20) UNSIGNED NOT NULL DEFAULT 1,
            first_seen datetime DEFAULT CURRENT_TIMESTAMP,
            last_seen datetime DEFAULT CURRENT_TIMESTAMP,
            resolved tinyint(1) NOT NULL DEFAULT 0,
            PRIMARY KEY (id),
            KEY url (url(191)),
            KEY last_seen (last_seen)
        ) $charset_collate;";

        require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
        dbDelta($sql);
    }

    private function auto_cleanup() {
        $days = get_option('seo_fury_404_auto_cleanup_days', 30);

        if ($days <= 0) {
            return;
        }

        global $wpdb;

        $wpdb->query($wpdb->prepare(
            "DELETE FROM {$this->table_name} WHERE last_seen < DATE_SUB(NOW(), INTERVAL %d DAY)",
            $days
        ));
    }

    public function ajax_load_404_log() {
        check_ajax_referer('seo_404_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error( __( 'Insufficient permissions', 'seo-fury' ) );
        }

        global $wpdb;

        $logs = $wpdb->get_results(
            "SELECT * FROM {$this->table_name} ORDER BY last_seen DESC LIMIT 100"
        );

        $total = $wpdb->get_var("SELECT COUNT(*) FROM {$this->table_name}"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $resolved = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$this->table_name} WHERE resolved = %d", 1)); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $total_hits = $wpdb->get_var("SELECT SUM(hits) FROM {$this->table_name}"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

        wp_send_json_success(array(
            'logs' => $logs,
            'stats' => array(
                'total' => $total,
                'resolved' => $resolved,
                'active' => $total - $resolved,
                'total_hits' => $total_hits
            )
        ));
    }

    public function ajax_clear_404_log() {
        check_ajax_referer('seo_404_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error( __( 'Insufficient permissions', 'seo-fury' ) );
        }

        global $wpdb;

        $wpdb->query("TRUNCATE TABLE {$this->table_name}"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is set in constructor, not user input

        wp_send_json_success( __( 'Logs cleared', 'seo-fury' ) );
    }

    public function ajax_find_similar_pages() {
        check_ajax_referer('seo_404_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error( __( 'Insufficient permissions', 'seo-fury' ) );
        }

        $url = isset($_POST['url']) ? sanitize_text_field($_POST['url']) : '';

        if (empty($url)) {
            wp_send_json_error( __( 'URL is required', 'seo-fury' ) );
        }

        $slug = basename(wp_parse_url($url, PHP_URL_PATH));
        $slug = sanitize_title($slug);

        $keywords = preg_split('/[-_\s]+/', $slug);
        $keywords = array_filter($keywords, function($word) {
            return strlen($word) > 2;
        });

        if (empty($keywords)) {
            wp_send_json_success(array('pages' => array()));
            return;
        }

        global $wpdb;

        $search_query = '%' . $wpdb->esc_like(implode('%', $keywords)) . '%';

        $pages = $wpdb->get_results($wpdb->prepare(
            "SELECT ID, post_title, post_name, post_type
            FROM {$wpdb->posts}
            WHERE post_status = 'publish'
            AND (post_title LIKE %s OR post_name LIKE %s)
            ORDER BY post_modified DESC
            LIMIT 20",
            $search_query,
            $search_query
        ));

        $results = array();
        foreach ($pages as $page) {
            $results[] = array(
                'id' => $page->ID,
                'title' => $page->post_title,
                'url' => get_permalink($page->ID),
                'type' => $page->post_type
            );
        }

        wp_send_json_success(array('pages' => $results));
    }

    public function ajax_mark_404_resolved() {
        check_ajax_referer('seo_404_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error( __( 'Insufficient permissions', 'seo-fury' ) );
        }

        $id = isset($_POST['id']) ? intval($_POST['id']) : 0;

        if ($id <= 0) {
            wp_send_json_error( __( 'Invalid ID', 'seo-fury' ) );
        }

        global $wpdb;

        $wpdb->update(
            $this->table_name,
            array('resolved' => 1),
            array('id' => $id),
            array('%d'),
            array('%d')
        );

        wp_send_json_success( __( 'Marked as resolved', 'seo-fury' ) );
    }

    public function ajax_delete_404_entry() {
        check_ajax_referer('seo_404_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error( __( 'Insufficient permissions', 'seo-fury' ) );
        }

        $id = isset($_POST['id']) ? intval($_POST['id']) : 0;

        if ($id <= 0) {
            wp_send_json_error( __( 'Invalid ID', 'seo-fury' ) );
        }

        global $wpdb;

        $wpdb->delete(
            $this->table_name,
            array('id' => $id),
            array('%d')
        );

        wp_send_json_success( __( 'Entry deleted', 'seo-fury' ) );
    }

public function ajax_create_redirect_from_404() {
    check_ajax_referer('seo_404_nonce', 'nonce');

    if (!current_user_can('manage_options')) {
        wp_send_json_error( __( 'Insufficient permissions', 'seo-fury' ) );
    }

    $log_id = isset($_POST['log_id']) ? intval($_POST['log_id']) : 0;
    $source_url = isset($_POST['source_url']) ? sanitize_text_field($_POST['source_url']) : '';
    $target_url = isset($_POST['target_url']) ? sanitize_text_field($_POST['target_url']) : '';
    $redirect_type = isset($_POST['redirect_type']) ? intval($_POST['redirect_type']) : 301;

    if (empty($source_url) || empty($target_url)) {
        wp_send_json_error( __( 'Please fill in all fields', 'seo-fury' ) );
    }

    global $wpdb;

    $source_url = strtok($source_url, '?');
    $source_url = strtok($source_url, '#');

    $parsed = wp_parse_url($source_url);
    if (isset($parsed['path'])) {
        $source_url = $parsed['path'];
    }

    $source_url = preg_replace('#/+#', '/', $source_url);

    if ($source_url !== '/') {
        $source_url = rtrim($source_url, '/');
    }

    if (!preg_match('#^https?://#', $target_url)) {
        if (strpos($target_url, '/') !== 0) {
            $target_url = '/' . $target_url;
        }
    }

    $exists = $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(*) FROM {$this->redirects_table} WHERE source_url = %s",
        $source_url
    ));

    if ($exists > 0) {
        wp_send_json_error( __( 'A redirect with this source already exists', 'seo-fury' ) );
    }

    $result = $wpdb->insert(
        $this->redirects_table,
        array(
            'source_url' => $source_url,
            'target_url' => $target_url,
            'redirect_type' => $redirect_type,
            'hits' => 0,
            'active' => 1,
            'created' => current_time('mysql'),
        ),
        array('%s', '%s', '%d', '%d', '%d', '%s')
    );

    if ($result) {
        if ($log_id > 0) {
            $wpdb->update(
                $this->table_name,
                array('resolved' => 1),
                array('id' => $log_id),
                array('%d'),
                array('%d')
            );
        }

        wp_send_json_success( __( 'Redirect created successfully!', 'seo-fury' ) );
    } else {
        wp_send_json_error( __( 'Error creating redirect: ', 'seo-fury' ) . $wpdb->last_error );
    }
}

public function ajax_export_404_logs_csv() {
    check_ajax_referer('seo_404_nonce', 'nonce');
    if (!current_user_can('manage_options')) {
        wp_send_json_error( __( 'Insufficient permissions', 'seo-fury' ) );
    }
    global $wpdb;
    $rows = $wpdb->get_results(
        "SELECT url, hits, first_seen, last_seen, resolved FROM {$this->table_name} ORDER BY last_seen DESC",
        ARRAY_A
    );
    header('Content-Type: text/csv; charset=utf-8');
    header('Content-Disposition: attachment; filename=seo-fury-404-logs.csv');
    header('Pragma: no-cache');
    header('Expires: 0');
    $output = fopen('php://output', 'w'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
    fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF));
    fwrite($output, "sep=;\n"); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
    fputcsv($output, array( __( 'URL', 'seo-fury' ), __( 'Hits', 'seo-fury' ), __( 'First Seen', 'seo-fury' ), __( 'Last Seen', 'seo-fury' ), __( 'Status', 'seo-fury' ) ), ';');

    foreach ($rows as $row) {
        fputcsv($output, array(
            seo_fury_csv_safe($row['url']),
            $row['hits'],
            $row['first_seen'],
            $row['last_seen'],
            $row['resolved'] ? __( 'Resolved', 'seo-fury' ) : __( 'Active', 'seo-fury' )
        ), ';');
    }
    fclose($output); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
    exit;
}

}
