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

class RANKFORGE_Redirects {
    private static $instance = null;
    private $table;

    // #18: redirect lookups used to fire a DB query on EVERY front-end request
    // (find_redirect via template_redirect). The full rule set is small and
    // changes rarely, so cache it as a source_url => row map in the object
    // cache + a transient fallback, invalidated whenever a rule changes.
    const MAP_CACHE_KEY = 'rankforge_redirect_map';
    const CACHE_GROUP   = 'rankforge';

    public static function instance() { if (!self::$instance) self::$instance = new self(); return self::$instance; }

    private function __construct() {
        global $wpdb;
        $this->table = $wpdb->prefix . 'rankforge_redirects';
        $this->init_hooks();
    }

    private function init_hooks() {
        // Schema creation moved to RANKFORGE_Schema_Manager (priority 1 on
        // plugins_loaded). This hook stays as a backstop in case the
        // manager hasn't run yet (e.g. plugin loaded out of order in a
        // mu-plugin context). It's a no-op once the manager has succeeded.
        add_action('plugins_loaded', [$this, 'maybe_create_table'], 20);

        // Handle redirects on frontend (early priority)
        add_action('template_redirect', [$this, 'handle_redirect'], 1);

        // Auto-redirect when a post slug changes.
        add_action('post_updated', [$this, 'auto_redirect_on_slug_change'], 10, 3);

        // Track 404s for suggestions
        add_action('template_redirect', [$this, 'track_404'], 99);

        // AJAX — always registered; license check is inside the handler
        add_action('wp_ajax_rankforge_save_redirect', [$this, 'ajax_save']);
        add_action('wp_ajax_rankforge_delete_redirect', [$this, 'ajax_delete']);
    }

    /**
 * Backstop wrapper — defers to the central schema manager, which is
 * the only place that owns the actual DDL.
 */
    public function maybe_create_table() {
        if ( ! class_exists( 'RANKFORGE_Schema_Manager' ) ) {
            require_once RANKFORGE_DIR . 'includes/class-schema-manager.php';
        }
        RANKFORGE_Schema_Manager::maybe_upgrade();
    }

    /**
 * @deprecated 1.0.1 Schema lives in RANKFORGE_Schema_Manager. This
 * shim stays so any external caller keeps working.
 */
    public static function create_table() {
        if ( ! class_exists( 'RANKFORGE_Schema_Manager' ) ) {
            require_once RANKFORGE_DIR . 'includes/class-schema-manager.php';
        }
        RANKFORGE_Schema_Manager::create_redirects();
    }

    /**
 * Handle redirect on frontend
 */
    public function handle_redirect() {
        if (is_admin()) return;
        if (empty($this->table)) return;
        $request = $this->get_current_path();
        $map = $this->get_redirect_map();
        $key = trim((string) $request, '/');
        // No DB query on the hot path — the lookup is an in-memory array hit.
        $redirect = $map[$key] ?? null;

        if ($redirect) {
            $home_host      = wp_parse_url(home_url(), PHP_URL_HOST);
            $target_host    = wp_parse_url($redirect->target_url, PHP_URL_HOST);
            $allow_external = !empty($redirect->allow_external);
            if ($target_host && $target_host !== $home_host && !$allow_external) {
                return;
            }

            // Increment hit count
            global $wpdb;
            // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; one-time/admin read
            $wpdb->query($wpdb->prepare(
                "UPDATE {$this->table} SET hits = hits + 1, last_hit = %s WHERE id = %d",
                current_time('mysql'), $redirect->id
            ));
            // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter

            wp_redirect($redirect->target_url, (int) $redirect->redirect_type); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- external checkout URL by design
            exit;
        }
    }

    /**
 * Auto-create 301 redirect when a post slug changes
 */
    public function auto_redirect_on_slug_change($post_id, $post_after, $post_before) {
        if (wp_is_post_revision($post_id)) return;
        if ($post_before->post_status !== 'publish') return;
        if ($post_before->post_name === $post_after->post_name) return;
        if (empty($post_before->post_name)) return;

        $old_url = get_permalink($post_before);
        // Rebuild old permalink manually since get_permalink uses new slug
        $old_path = str_replace(home_url(), '', $old_url);
        $old_path = str_replace($post_after->post_name, $post_before->post_name, $old_path);

        $new_url = get_permalink($post_id);

        if ($old_path && $new_url && $old_path !== $new_url) {
            $this->add(trim($old_path, '/'), $new_url, 301, true);
        }
    }

    /**
 * Track 404 URLs for redirect suggestions
 */
    public function track_404() {
        if (!is_404()) return;

        $path = $this->get_current_path();
        if (empty($path)) return;

        $ip_hash = md5(sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'] ?? 'unknown')));
        $rl_key  = 'rankforge_404_rl_' . $ip_hash;
        $hits    = (int) get_transient($rl_key);
        if ($hits >= 20) return;
        set_transient($rl_key, $hits + 1, MINUTE_IN_SECONDS);

        $path = substr($path, 0, 500);

        // Store in transient for admin display
        $not_found = get_transient('rankforge_404_log') ?: [];
        $not_found[$path] = ($not_found[$path] ?? 0) + 1;

        // Keep only top 50
        arsort($not_found);
        $not_found = array_slice($not_found, 0, 50, true);

        set_transient('rankforge_404_log', $not_found, WEEK_IN_SECONDS);
    }

    /**
 * Add a redirect
 */
    public function add($source, $target, $type = 301, $auto = false, $allow_external = false) {
        global $wpdb;

        $source = trim($source, '/');

        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; one-time/admin read
        $existing = $wpdb->get_var($wpdb->prepare(
            "SELECT id FROM {$this->table} WHERE source_url = %s", $source
        ));
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter

        if ($existing) {
            $wpdb->update($this->table, [ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table; direct query is intended.
                'target_url'     => esc_url_raw($target),
                'redirect_type'  => (int) $type,
                'allow_external' => $allow_external ? 1 : 0,
            ], ['id' => $existing]);
            $this->flush_redirect_map();
            return (int) $existing;
        }

        $wpdb->insert($this->table, [ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table; direct query is intended.
            'source_url'     => sanitize_text_field($source),
            'target_url'     => esc_url_raw($target),
            'redirect_type'  => (int) $type,
            'auto_created'   => $auto ? 1 : 0,
            'allow_external' => $allow_external ? 1 : 0,
        ]);
        $this->flush_redirect_map();
        return $wpdb->insert_id;
    }

    /**
 * Cached source_url => row map for the front-end redirect lookup (#18).
 * Order: object cache → transient → one DB read (then warm both caches).
 * Invalidated by flush_redirect_map whenever a rule is added/updated/deleted.
 */
    private function get_redirect_map() {
        $cached = wp_cache_get(self::MAP_CACHE_KEY, self::CACHE_GROUP);
        if (is_array($cached)) return $cached;

        $cached = get_transient(self::MAP_CACHE_KEY);
        if (is_array($cached)) {
            wp_cache_set(self::MAP_CACHE_KEY, $cached, self::CACHE_GROUP);
            return $cached;
        }

        global $wpdb;
        // Only the columns the front-end redirect needs; bounded for safety.
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; one-time/admin read
        $rows = $wpdb->get_results(
            "SELECT id, source_url, target_url, redirect_type, allow_external
             FROM {$this->table} LIMIT 5000"
        );
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $map = self::build_redirect_map(is_array($rows) ? $rows : []);

        wp_cache_set(self::MAP_CACHE_KEY, $map, self::CACHE_GROUP);
        set_transient(self::MAP_CACHE_KEY, $map, DAY_IN_SECONDS);
        return $map;
    }

    /**
 * Pure builder: rows → [trimmed source_url => row]. Kept static + pure so
 * the keying contract is unit-testable without WordPress.
 */
    public static function build_redirect_map(array $rows): array {
        $map = [];
        foreach ($rows as $r) {
            if (!is_object($r) || !isset($r->source_url)) continue;
            $map[trim((string) $r->source_url, '/')] = $r;
        }
        return $map;
    }

    /** Drop the cached redirect map after any rule change. */
    private function flush_redirect_map() {
        wp_cache_delete(self::MAP_CACHE_KEY, self::CACHE_GROUP);
        delete_transient(self::MAP_CACHE_KEY);
    }

    /**
 * Get all redirects
 */
    public function get_all() {
        global $wpdb;
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; one-time/admin read
        return $wpdb->get_results("SELECT * FROM {$this->table} ORDER BY hits DESC, created_at DESC LIMIT 500");
    }

    /**
 * Get 404 log
 */
    public function get_404_log() {
        return get_transient('rankforge_404_log') ?: [];
    }

    private function get_current_path() {
        $request = sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? ''));
        $path = trim(wp_parse_url($request, PHP_URL_PATH), '/');
        $home_path = trim(wp_parse_url(home_url(), PHP_URL_PATH), '/');
        if ($home_path && strpos($path, $home_path) === 0) {
            $path = substr($path, strlen($home_path));
        }
        return trim($path, '/');
    }

    // AJAX handlers
    public function ajax_save() {
        check_ajax_referer('rankforge_nonce', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error();

        $source = sanitize_text_field(wp_unslash($_POST['source_url'] ?? ''));
        $target = esc_url_raw(wp_unslash($_POST['target_url'] ?? ''));
        $type = intval($_POST['redirect_type'] ?? 301);
        $allow_external = !empty($_POST['allow_external']) && current_user_can('manage_options');

        if (empty($source)) wp_send_json_error(['message' => 'Source URL required']);

        $home_host   = wp_parse_url(home_url(), PHP_URL_HOST);
        $target_host = $target ? wp_parse_url($target, PHP_URL_HOST) : '';
        if ($target_host && $target_host !== $home_host && !$allow_external) {
            wp_send_json_error(['message' => __('External redirect targets are not allowed. Enable "Allow external" to redirect off-site.', 'rankforge')]);
        }

        // Reject a redirect whose target resolves to the same on-site path as the
        // source — it would loop forever (source matches, redirect fires, repeat).
        // A relative target ("/path") has no host, so wp_parse_url returns null;
        // treat null/empty host as same-site.
        if (empty($target_host) || $target_host === $home_host) {
            $norm_path = function ($u) {
                $p = wp_parse_url((string) $u, PHP_URL_PATH);
                $p = is_string($p) ? $p : (string) $u;
                $p = trim($p, '/');
                $home_path = trim((string) wp_parse_url(home_url(), PHP_URL_PATH), '/');
                if ($home_path !== '' && strpos($p, $home_path) === 0) {
                    $p = trim(substr($p, strlen($home_path)), '/');
                }
                return $p;
            };
            $src_norm = $norm_path($source);
            $tgt_norm = $norm_path($target);
            if ($src_norm !== '' && $src_norm === $tgt_norm) {
                wp_send_json_error(['message' => __('Source and target are the same URL — this would create a redirect loop.', 'rankforge')]);
            }
        }

        $id = $this->add($source, $target, $type, false, $allow_external);
        wp_send_json_success(['id' => $id]);
    }

    public function ajax_delete() {
        check_ajax_referer('rankforge_nonce', 'nonce');
        if (!current_user_can('manage_options')) wp_send_json_error();

        global $wpdb;
        $wpdb->delete($this->table, ['id' => intval($_POST['redirect_id'] ?? 0)]); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table; direct query is intended.
        $this->flush_redirect_map();
        wp_send_json_success();
    }

    /**
 * Render admin page
 */
    public function render_page() {
        $redirects = $this->get_all();
        $log_404 = $this->get_404_log();
        ?>
        <div class="wrap">
            <?php RANKFORGE_Admin_Tabs::render('rankforge-redirects'); ?>
            <h1><?php esc_html_e('Redirect Manager', 'rankforge'); ?><?php Forge_Docs_Link::render('rankforge', 'redirect-manager-pro', __('Redirects', 'rankforge'), __('301/302/307/410 rules, wildcard + regex, auto-redirect on slug change.', 'rankforge')); ?></h1>

            <!-- Add redirect form -->
            <div style="background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin:16px 0;max-width:700px;">
                <h3 style="margin-top:0;"><?php esc_html_e('Add Redirect', 'rankforge'); ?></h3>
                <div style="display:flex;gap:8px;align-items:end;flex-wrap:wrap;">
                    <div>
                        <label style="display:block;font-size:14px;font-weight:600;margin-bottom:4px;"><?php esc_html_e('From URL', 'rankforge'); ?></label>
                        <input type="text" id="sf-redirect-source" placeholder="/old-page" style="width:220px;">
                    </div>
                    <div>
                        <label style="display:block;font-size:14px;font-weight:600;margin-bottom:4px;"><?php esc_html_e('To URL', 'rankforge'); ?></label>
                        <input type="text" id="sf-redirect-target" placeholder="<?php echo esc_attr(home_url('/new-page')); ?>" style="width:250px;">
                    </div>
                    <div>
                        <label style="display:block;font-size:14px;font-weight:600;margin-bottom:4px;"><?php esc_html_e('Type', 'rankforge'); ?></label>
                        <select id="sf-redirect-type">
                            <option value="301"><?php esc_html_e('301 Permanent', 'rankforge'); ?></option>
                            <option value="302"><?php esc_html_e('302 Temporary', 'rankforge'); ?></option>
                            <option value="307"><?php esc_html_e('307 Temporary', 'rankforge'); ?></option>
                        </select>
                    </div>
                    <button type="button" class="button button-primary" id="sf-add-redirect"><?php esc_html_e('Add', 'rankforge'); ?></button>
                </div>
                <div style="margin-top:10px;">
                    <label style="font-size:14px;color:#475569;">
                        <input type="checkbox" id="sf-redirect-allow-external" style="vertical-align:middle;">
                        <?php esc_html_e('Allow redirect to external domain (off-site)', 'rankforge'); ?>
                    </label>
                </div>
            </div>

            <!-- Redirects list -->
            <h2><?php esc_html_e('Active Redirects', 'rankforge'); ?> (<?php echo count($redirects); ?>)</h2>
            <table class="widefat striped">
                <thead><tr><th><?php esc_html_e('From', 'rankforge'); ?></th><th><?php esc_html_e('To', 'rankforge'); ?></th><th><?php esc_html_e('Type', 'rankforge'); ?></th><th><?php esc_html_e('Hits', 'rankforge'); ?></th><th><?php esc_html_e('Auto', 'rankforge'); ?></th><th></th></tr></thead>
                <tbody>
                <?php foreach ($redirects as $r): ?>
                <tr>
                    <td><code><?php echo esc_html($r->source_url); ?></code></td>
                    <td><a href="<?php echo esc_url($r->target_url); ?>" target="_blank"><?php echo esc_html(mb_substr($r->target_url, 0, 60)); ?></a></td>
                    <td><?php echo (int) $r->redirect_type; ?></td>
                    <td><?php echo (int) $r->hits; ?></td>
                    <td><?php echo $r->auto_created ? esc_html__('Auto', 'rankforge') : esc_html__('Manual', 'rankforge'); ?></td>
                    <td><button class="button button-small sf-delete-redirect" data-id="<?php echo esc_attr( $r->id ); ?>" style="color:#dc2626;"><?php esc_html_e('Delete', 'rankforge'); ?></button></td>
                </tr>
                <?php endforeach; ?>
                <?php if (empty($redirects)): ?><tr><td colspan="6"><?php esc_html_e('No redirects yet.', 'rankforge'); ?></td></tr><?php endif; ?>
                </tbody>
            </table>

            <?php if (!empty($log_404)): ?>
            <h2 style="margin-top:24px;"><?php esc_html_e('Recent 404 URLs', 'rankforge'); ?></h2>
            <p class="description"><?php esc_html_e('Pages visitors tried to access but got 404. Consider adding redirects.', 'rankforge'); ?></p>
            <table class="widefat striped" style="max-width:600px;">
                <thead><tr><th><?php esc_html_e('URL', 'rankforge'); ?></th><th><?php esc_html_e('Hits', 'rankforge'); ?></th></tr></thead>
                <tbody>
                <?php foreach (array_slice($log_404, 0, 20, true) as $url => $hits): ?>
                <tr><td><code><?php echo esc_html($url); ?></code></td><td><?php echo (int) $hits; ?></td></tr>
                <?php endforeach; ?>
                </tbody>
            </table>
            <?php endif; ?>
        </div>

        <script>
        jQuery(function($) {
 // fall back to native alert/confirm if sfModal failed to load.
            function sfa(msg, opts){ return (window.sfModal && window.sfModal.alert) ? window.sfModal.alert(msg, opts) : (alert(msg), Promise.resolve(true)); }
            function sfc(msg, opts){ return (window.sfModal && window.sfModal.confirm) ? window.sfModal.confirm(msg, opts) : Promise.resolve(confirm(msg)); }
            $('#sf-add-redirect').on('click', function() {
                $.post(rankforgeAdmin.ajaxUrl, {
                    action: 'rankforge_save_redirect', nonce: rankforgeAdmin.nonce,
                    source_url: $('#sf-redirect-source').val(),
                    target_url: $('#sf-redirect-target').val(),
                    redirect_type: $('#sf-redirect-type').val(),
                    allow_external: $('#sf-redirect-allow-external').is(':checked') ? 1 : 0
                }, function(r) {
                    if (r.success) {
                        location.reload();
                    } else if (r.data && r.data.upgrade) {
                        sfa('<?php echo esc_js(__('Redirects require a Pro license. Please upgrade to unlock this feature.', 'rankforge')); ?>', { title: '<?php echo esc_js(__('Pro feature', 'rankforge')); ?>', variant: 'info' });
                    } else {
                        sfa((r.data && r.data.message) || '<?php echo esc_js(__('Could not save redirect.', 'rankforge')); ?>', { title: '<?php echo esc_js(__('Could not save redirect', 'rankforge')); ?>', variant: 'danger' });
                    }
                });
            });
            $('.sf-delete-redirect').on('click', function() {
                var id = $(this).data('id');
                sfc('<?php echo esc_js(__('Delete this redirect?', 'rankforge')); ?>', { title: '<?php echo esc_js(__('Delete redirect?', 'rankforge')); ?>', okText: '<?php echo esc_js(__('Delete', 'rankforge')); ?>', variant: 'danger' }).then(function(ok){
                    if (!ok) return;
                    $.post(rankforgeAdmin.ajaxUrl, {
                        action: 'rankforge_delete_redirect', nonce: rankforgeAdmin.nonce,
                        redirect_id: id
                    }, function(r) { if (r.success) location.reload(); });
                });
            });
        });
        </script>
        <?php
    }
}
