<?php

/**
 * LinkCentral Redirection Class
 *
 * This class handles the redirection and click tracking functionality for LinkCentral.
 */
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
// Exit if accessed directly
class LinkCentral_Redirection {
    private $url_prefix;

    private $visitor_manager;

    private $geolocation;

    private $dynamic_rules;

    private $click_tracker;

    private $webhook_handler;

    /**
     * Constructor.
     */
    public function __construct(
        $url_prefix,
        $visitor_manager,
        $geolocation,
        $dynamic_rules,
        $click_tracker,
        $webhook_handler
    ) {
        $this->url_prefix = $url_prefix;
        $this->visitor_manager = $visitor_manager;
        $this->geolocation = $geolocation;
        $this->dynamic_rules = $dynamic_rules;
        $this->click_tracker = $click_tracker;
        $this->webhook_handler = $webhook_handler;
    }

    /**
     * Initialize the redirection functionality.
     * 
     * This uses a two-hook approach for optimal performance and WordPress compatibility:
     * 
     * 1. PREFIXED REDIRECTS (early 'init' hook):
     *    - URLs like /{linkcentral_url_prefix}/slug are clearly LinkCentral links
     *    - Caught early before WordPress parses queries, loads themes, etc.
     *    - Maximum performance: checks if the link is a valid LinkCentral link (eg did not disable slug prefix) and redirects immediately if it is
     * 
     * 2. "INDEX.PHP" DIRECT REDIRECTS (early 'init' hook, priority 2):
     *    - Prefix-disabled links on index.php permalink structures (e.g. index.php/slug)
     *    - Legacy slugs stored with index.php in the slug (e.g. index.php/link/slug)
     *    - WordPress often mis-routes these to unrelated posts before a 404 is set
     *    - Intercept early when the path matches a known LinkCentral slug
     *
     * 3. DIRECT REDIRECTS (late 'template_redirect' hook):
     *    - URLs like /slug could be pages, posts, or LinkCentral links
     *    - Let WordPress do its routing first to determine if content exists. If it does, those have priority over LinkCentral links.
     *    - Check for LinkCentral when WordPress finds nothing (is_404)
     * 
     */
    public function init() {
        // Hook early in WordPress process to catch prefixed redirects
        add_action( 'init', array($this, 'maybe_handle_prefixed_redirect'), 1 );
        // index.php permalink paths must be caught before WordPress rewrite routing
        add_action( 'init', array($this, 'maybe_handle_index_php_direct_redirect'), 2 );
        // Hook later to catch direct slugs only when WordPress finds nothing
        add_action( 'template_redirect', array($this, 'maybe_handle_direct_redirect') );
    }

    /**
     * Whether redirection should be skipped for the current request.
     */
    private function should_skip_redirect_request() {
        return is_admin() || defined( 'DOING_AJAX' ) && DOING_AJAX || defined( 'DOING_CRON' ) && DOING_CRON;
    }

    /**
     * Get the current request path (without query string).
     */
    private function get_request_path() {
        $request_uri = ( isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '' );
        return (string) wp_parse_url( $request_uri, PHP_URL_PATH );
    }

    /**
     * Get the WordPress site path (subdirectory installs).
     */
    private function get_site_path() {
        $site_url = wp_parse_url( site_url(), PHP_URL_PATH );
        return ( !empty( $site_url ) ? rtrim( $site_url, '/' ) : '' );
    }

    /**
     * Get the request path relative to the WordPress site path.
     */
    private function get_relative_request_path( $path = null, $site_path = null ) {
        if ( $path === null ) {
            $path = $this->get_request_path();
        }
        if ( $site_path === null ) {
            $site_path = $this->get_site_path();
        }
        $relative_path = ( $site_path ? substr( (string) $path, strlen( $site_path ) ) : (string) $path );
        return trim( $relative_path, '/' );
    }

    /**
     * Whether a slug should be ignored because it looks like a static file request.
     */
    private function slug_looks_like_static_file( $slug ) {
        return strpos( basename( $slug ), '.' ) !== false;
    }

    /**
     * Find a prefix-disabled LinkCentral link ID by slug.
     */
    private function get_direct_link_id_by_slug( $slug ) {
        global $wpdb;
        $case_sensitive = get_option( 'linkcentral_case_sensitive_redirects', false );
        if ( $case_sensitive ) {
            return $wpdb->get_var( $wpdb->prepare(
                "SELECT p.ID FROM {$wpdb->posts} p\n                 INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id\n                 WHERE p.post_name = %s COLLATE utf8mb4_bin\n                 AND p.post_type = %s\n                 AND p.post_status IN ('publish', 'private', 'draft')\n                 AND pm.meta_key = %s\n                 AND pm.meta_value = '1'\n                 LIMIT 1",
                $slug,
                'linkcentral_link',
                '_linkcentral_disable_slug_prefix'
            ) );
        }
        return $wpdb->get_var( $wpdb->prepare(
            "SELECT p.ID FROM {$wpdb->posts} p\n             INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id\n             WHERE p.post_name = %s\n             AND p.post_type = %s\n             AND p.post_status IN ('publish', 'private', 'draft')\n             AND pm.meta_key = %s\n             AND pm.meta_value = '1'\n             LIMIT 1",
            $slug,
            'linkcentral_link',
            '_linkcentral_disable_slug_prefix'
        ) );
    }

    /**
     * Build slug candidates for prefix-disabled links from the request path.
     *
     * On index.php permalink structures, home_url() may add "index.php/" even when the
     * stored slug does not include it (e.g. slug "myslug" -> index.php/myslug).
     * Legacy imports may also store the full path (e.g. index.php/link/myslug).
     */
    private function get_direct_slug_candidates( $relative_path ) {
        $relative_path = trim( $relative_path, '/' );
        if ( $relative_path === '' ) {
            return array();
        }
        $candidates = array($relative_path);
        if ( stripos( $relative_path, 'index.php/' ) === 0 ) {
            $without_index_php = trim( substr( $relative_path, strlen( 'index.php/' ) ), '/' );
            if ( $without_index_php !== '' && $without_index_php !== $relative_path ) {
                $candidates[] = $without_index_php;
            }
        }
        return array_values( array_unique( $candidates ) );
    }

    /**
     * Try to resolve and redirect a prefix-disabled link from a request path.
     *
     * @return bool True when a redirect was performed.
     */
    private function try_handle_direct_redirect_for_path( $relative_path ) {
        foreach ( $this->get_direct_slug_candidates( $relative_path ) as $candidate ) {
            if ( strlen( $candidate ) > 200 ) {
                continue;
            }
            if ( $this->slug_looks_like_static_file( $candidate ) ) {
                continue;
            }
            $link_id = $this->get_direct_link_id_by_slug( $candidate );
            if ( !empty( $link_id ) ) {
                $this->process_redirect( $candidate, $link_id );
                exit;
            }
        }
        return false;
    }

    /**
     * Build prefixed URL path bases, including index.php permastruct variants.
     */
    private function get_prefixed_path_bases( $site_path, $url_prefix ) {
        $bases = array(rtrim( $site_path, '/' ) . '/' . $url_prefix);
        if ( stripos( $url_prefix, 'index.php/' ) !== 0 ) {
            $bases[] = rtrim( $site_path, '/' ) . '/index.php/' . $url_prefix;
        }
        return $bases;
    }

    /**
     * Check if the current request is for a prefixed LinkCentral link and handle redirection if needed.
     */
    public function maybe_handle_prefixed_redirect() {
        if ( $this->should_skip_redirect_request() ) {
            return;
        }
        $path = $this->get_request_path();
        $url_prefix = trim( $this->url_prefix, '/' );
        $site_path = $this->get_site_path();
        $matched_prefix = null;
        $slug = '';
        foreach ( $this->get_prefixed_path_bases( $site_path, $url_prefix ) as $prefix_base ) {
            if ( $path === $prefix_base ) {
                global $wp_query;
                $wp_query->set_404();
                return;
            }
            $prefix_pattern = $prefix_base . '/';
            if ( strpos( $path, $prefix_pattern ) === 0 ) {
                $matched_prefix = $prefix_pattern;
                $slug = trim( substr( $path, strlen( $prefix_pattern ) ), '/' );
                break;
            }
        }
        if ( $matched_prefix === null ) {
            return;
        }
        if ( empty( $slug ) ) {
            global $wp_query;
            $wp_query->set_404();
            return;
        }
        global $wpdb;
        $case_sensitive = get_option( 'linkcentral_case_sensitive_redirects', false );
        if ( $case_sensitive ) {
            $link = $wpdb->get_row( $wpdb->prepare( "SELECT p.ID, pm.meta_value as prefix_disabled\n                 FROM {$wpdb->posts} p\n                 LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_linkcentral_disable_slug_prefix'\n                 WHERE p.post_name = %s COLLATE utf8mb4_bin\n                 AND p.post_type = %s\n                 AND p.post_status IN ('publish', 'private', 'draft')\n                 LIMIT 1", $slug, 'linkcentral_link' ) );
        } else {
            $link = $wpdb->get_row( $wpdb->prepare( "SELECT p.ID, pm.meta_value as prefix_disabled\n                 FROM {$wpdb->posts} p\n                 LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_linkcentral_disable_slug_prefix'\n                 WHERE p.post_name = %s\n                 AND p.post_type = %s\n                 AND p.post_status IN ('publish', 'private', 'draft')\n                 LIMIT 1", $slug, 'linkcentral_link' ) );
        }
        if ( !$link || !empty( $link->prefix_disabled ) ) {
            global $wp_query;
            $wp_query->set_404();
            return;
        }
        $this->process_redirect( $slug, $link->ID );
        exit;
    }

    /**
     * Handle prefix-disabled links requested via an index.php permalink path.
     *
     * WordPress often resolves these to unrelated posts instead of returning 404.
     */
    public function maybe_handle_index_php_direct_redirect() {
        if ( $this->should_skip_redirect_request() ) {
            return;
        }
        $relative_path = $this->get_relative_request_path();
        if ( $relative_path === '' || stripos( $relative_path, 'index.php/' ) !== 0 ) {
            return;
        }
        $this->try_handle_direct_redirect_for_path( $relative_path );
    }

    /**
     * Check if the current request should be handled as a direct LinkCentral slug.
     * Only runs when WordPress hasn't found any content (404 situation).
     */
    public function maybe_handle_direct_redirect() {
        // Only check for direct slugs if WordPress is about to show a 404
        if ( !is_404() ) {
            return;
        }
        if ( $this->should_skip_redirect_request() ) {
            return;
        }
        $relative_path = $this->get_relative_request_path();
        if ( $relative_path === '' ) {
            return;
        }
        $this->try_handle_direct_redirect_for_path( $relative_path );
    }

    /**
     * Process the redirect for a LinkCentral link.
     */
    private function process_redirect( $slug, $link_id ) {
        $link = get_post( $link_id );
        if ( !$link || $link->post_type !== 'linkcentral_link' ) {
            // Invalid link ID or wrong post type
            return;
        }
        // Fire a hook that developers can use when a valid link is clicked
        // Passes: link ID, slug, and the full link post object
        do_action(
            'linkcentral_link_clicked',
            $link->ID,
            $slug,
            $link
        );
        // Check if the link is a draft, set as private, or scheduled for future
        if ( $link->post_status === 'draft' ) {
            wp_die( '<h3>' . esc_html__( 'Inactive link', 'linkcentral' ) . '</h3><p>' . esc_html__( 'This link is currently not accessible because it is saved as a draft.', 'linkcentral' ) . '</p>', esc_html__( 'Inaccessible Link', 'linkcentral' ), array(
                'response' => 404,
            ) );
        } elseif ( $link->post_status === 'private' || $link->post_status === 'future' ) {
            if ( !current_user_can( 'edit_posts' ) ) {
                wp_die( esc_html__( 'This link is currently not accessible.', 'linkcentral' ), esc_html__( 'Inaccessible Link', 'linkcentral' ), array(
                    'response' => 404,
                ) );
            }
        }
        // Check if the link is password protected
        if ( post_password_required( $link->ID ) ) {
            // Display the custom password form
            include LINKCENTRAL_PLUGIN_DIR . 'views/password-form.php';
            exit;
        }
        // Get destination URL
        $reporting_disabled = get_option( 'linkcentral_disable_reporting', false );
        // Try to get cached URL (only when reporting enabled)
        if ( !$reporting_disabled ) {
            $destination_url = $this->visitor_manager->get_cached_destination_url( $link->ID );
        }
        // Calculate destination URL if not cached
        if ( empty( $destination_url ) ) {
            // Get the default destination URL
            $destination_url = get_post_meta( $link->ID, '_linkcentral_destination_url', true );
            // Check for dynamic rules
            if ( linkcentral_fs()->is_premium() ) {
                // specifically checking for premium version and NOT for active subscription so that dynamic rules keep working in case license briefly expires
                $dynamic_rules_data = get_post_meta( $link->ID, '_linkcentral_dynamic_rules', true );
                $dynamic_destination_url = $this->dynamic_rules->get_dynamic_destination_url( $link->ID, $dynamic_rules_data );
                if ( $dynamic_destination_url ) {
                    $destination_url = $dynamic_destination_url;
                }
            }
            // Cache the result (only when reporting enabled)
            if ( $destination_url && !$reporting_disabled ) {
                $this->visitor_manager->set_cached_destination_url( $link->ID, $destination_url );
            }
        }
        // If no destination URL is found, show an error
        if ( empty( $destination_url ) ) {
            wp_die( '<h3>' . esc_html__( 'Configuration Error', 'linkcentral' ) . '</h3><p>' . esc_html__( 'This link has no destination URL configured.', 'linkcentral' ) . '</p>', esc_html__( 'Link Error', 'linkcentral' ), array(
                'response' => 500,
            ) );
        }
        $this->click_tracker->record_click( $link->ID, $link->post_name, $destination_url );
        $redirection_type = $this->get_redirection_type( $link->ID );
        // Fire a hook right before the redirect happens
        do_action(
            'linkcentral_before_redirect',
            $link->ID,
            $slug,
            $destination_url,
            $redirection_type,
            $link
        );
        // Set no-cache headers
        nocache_headers();
        header( 'X-Redirect-Powered-By: LinkCentral' );
        // Perform the redirection
        // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- External redirect is expected
        wp_redirect( $destination_url, $redirection_type );
        exit;
    }

    /**
     * Get the redirection type for a link.
     */
    private function get_redirection_type( $link_id ) {
        $redirection_type = get_post_meta( $link_id, '_linkcentral_redirection_type', true );
        if ( $redirection_type === 'default' || !$redirection_type ) {
            $redirection_type = get_option( 'linkcentral_global_redirection_type', '307' );
        }
        return intval( $redirection_type );
    }

}
