<?php

/**
 * LinkCentral Link Helpers
 *
 * This file contains helper functions for LinkCentral links, such as getting the complete URL for a link and sanitizing slugs.
 */
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
// Exit if accessed directly
/**
 * Sanitize LinkCentral slug/post name while preserving forward slashes
 *
 * @param string $input The input string to sanitize
 * @param array $options Optional parameters to control sanitization behavior
 *                      'for_input' => bool - True for input sanitization (more permissive), false for output (more restrictive)
 *                      'to_lowercase' => bool - Whether to convert to lowercase (default: true for input, false for output)
 *                      'remove_path_traversal' => bool - Whether to remove .. sequences (default: true)
 *                      'trim_slashes' => bool - Whether to trim leading/trailing slashes (default: true)
 * @return string The sanitized string
 */
function linkcentral_sanitize_slug(  $input, $options = array()  ) {
    // Default options
    $defaults = array(
        'for_input'             => true,
        'to_lowercase'          => null,
        'remove_path_traversal' => true,
        'trim_slashes'          => true,
    );
    $options = wp_parse_args( $options, $defaults );
    // Set lowercase default based on context
    if ( $options['to_lowercase'] === null ) {
        $options['to_lowercase'] = $options['for_input'];
    }
    $sanitized = $input;
    // Convert to lowercase if requested (typically for input)
    if ( $options['to_lowercase'] ) {
        $sanitized = strtolower( $sanitized );
    }
    // Remove path traversal attempts
    if ( $options['remove_path_traversal'] ) {
        $sanitized = str_replace( '..', '', $sanitized );
    }
    // Preserve legacy ".php" path segments (e.g. "index.php/go/slug")
    // while still blocking arbitrary dot usage in slugs.
    $php_segment_placeholder = '__LC_PHP_SEGMENT_PLACEHOLDER__';
    $sanitized = preg_replace( '/\\.php(?=\\/|$)/i', $php_segment_placeholder, $sanitized );
    if ( $options['for_input'] ) {
        // Input sanitization: replace invalid characters with hyphens, more permissive
        $sanitized = preg_replace( '/[^a-zA-Z0-9\\/\\-_]+/', '-', $sanitized );
        // Remove leading and trailing hyphens and slashes
        if ( $options['trim_slashes'] ) {
            $sanitized = trim( $sanitized, '-/' );
        }
        // Replace multiple consecutive hyphens with a single hyphen
        $sanitized = preg_replace( '/-+/', '-', $sanitized );
    } else {
        // Output sanitization: remove invalid characters completely, more restrictive
        $sanitized = preg_replace( '/[^a-zA-Z0-9\\-_\\/]/', '', $sanitized );
        // Remove leading and trailing slashes only
        if ( $options['trim_slashes'] ) {
            $sanitized = trim( $sanitized, '/' );
        }
    }
    // Replace multiple consecutive forward slashes with a single forward slash
    $sanitized = preg_replace( '/\\/+/', '/', $sanitized );
    // Restore only allowed legacy ".php" segments.
    $sanitized = str_replace( $php_segment_placeholder, '.php', $sanitized );
    return $sanitized;
}

/**
 * Sanitize LinkCentral slug for input (when creating/editing posts)
 * 
 * @param string $slug The slug to sanitize
 * @return string The sanitized slug
 */
function linkcentral_sanitize_input_slug(  $slug  ) {
    return linkcentral_sanitize_slug( $slug, array(
        'for_input'             => true,
        'to_lowercase'          => false,
        'remove_path_traversal' => true,
        'trim_slashes'          => true,
    ) );
}

/**
 * Sanitize LinkCentral post name for URL output (when generating URLs)
 * 
 * @param string $post_name The post name to sanitize
 * @return string The sanitized post name
 */
function linkcentral_sanitize_output_slug(  $post_name  ) {
    return linkcentral_sanitize_slug( $post_name, array(
        'for_input'             => false,
        'to_lowercase'          => false,
        'remove_path_traversal' => true,
        'trim_slashes'          => true,
    ) );
}

/**
 * Get the complete URL for a LinkCentral link
 * 
 * @param int         $link_id              The link ID
 * @param string|null $post_name            Optional post name (will be fetched if not provided)
 * @param mixed       $disable_slug_prefix  Pass preloaded meta value to skip the extra get_post_meta
 *                                          call. Use null (default) to fetch from meta automatically.
 * @return string The complete URL for the link
 */
function linkcentral_get_link_url(  $link_id, $post_name = null, $disable_slug_prefix = null  ) {
    if ( empty( $post_name ) ) {
        $link = get_post( $link_id );
        if ( !$link || $link->post_type !== 'linkcentral_link' ) {
            return '';
        }
        $post_name = $link->post_name;
    }
    // Only query meta when the caller has not already provided the preloaded value.
    if ( $disable_slug_prefix === null ) {
        $disable_slug_prefix = get_post_meta( $link_id, '_linkcentral_disable_slug_prefix', true );
    }
    $sanitized_slug = linkcentral_sanitize_output_slug( $post_name );
    if ( $disable_slug_prefix ) {
        return home_url( '/' . $sanitized_slug );
    }
    $url_prefix = get_option( 'linkcentral_url_prefix', 'go' );
    return home_url( '/' . $url_prefix . '/' . $sanitized_slug );
}

/**
 * Sanitize destination URL for LinkCentral links
 * More forgiving than esc_url_raw to support custom schemes, relative paths, and special protocols
 *
 * @param string $url The URL to sanitize
 * @return string The sanitized URL
 */
function linkcentral_sanitize_destination_url(  $url  ) {
    $url = trim( $url );
    // Empty URLs return empty
    if ( empty( $url ) ) {
        return '';
    }
    // Remove any null bytes and control characters for security
    $url = str_replace( array(
        "\x00",
        "\r",
        "\n",
        "\t"
    ), '', $url );
    // Check if it's a standard HTTP/HTTPS URL - use WordPress sanitization for these
    if ( preg_match( '/^https?:\\/\\//', $url ) ) {
        return esc_url_raw( $url );
    }
    // For custom schemes and relative paths, do basic sanitization
    // Remove dangerous characters but preserve the structure
    $url = preg_replace( '/[<>"\'`\\s]/', '', $url );
    // Ensure it doesn't contain javascript: or data: schemes for security
    if ( preg_match( '/^(javascript|data|vbscript):/i', $url ) ) {
        return '';
    }
    return $url;
}

/**
 * Truncate long URLs for display in tables
 *
 * @param string $url The URL to truncate
 * @param int $max_length Maximum length (default: 50)
 * @return string HTML string with truncated URL and tooltip
 */
function linkcentral_truncate_url(  $url, $max_length = 100  ) {
    if ( empty( $url ) || strlen( $url ) <= $max_length ) {
        return esc_html( $url );
    }
    $truncated_text = substr( $url, 0, $max_length - 3 );
    return '<span class="linkcentral-truncated-url" data-full-url="' . esc_attr( $url ) . '">' . esc_html( $truncated_text ) . '<span class="linkcentral-truncate-dots">...</span></span>';
}

/**
 * LinkCentral Link Attributes Helper Class
 *
 * This class handles the common logic for processing link attributes like CSS classes,
 * nofollow, sponsored, new tab, and parameter forwarding that is used across multiple components.
 */
class LinkCentral_Link_Attributes {
    private $global_css_classes;

    private $global_nofollow;

    private $global_nofollow_scope;

    private $global_nofollow_external_domains;

    private $global_sponsored;

    private $global_sponsored_scope;

    private $global_sponsored_external_domains;

    private $global_parameter_forwarding;

    private $global_other_attributes;

    private $global_new_tab;

    private $global_new_tab_scope;

    private $global_new_tab_external_domains;

    /**
     * Constructor - initialize global settings
     */
    public function __construct() {
        $this->global_css_classes = get_option( 'linkcentral_custom_css_classes', '' );
        $this->global_nofollow = get_option( 'linkcentral_global_nofollow', false );
        $this->global_nofollow_scope = get_option( 'linkcentral_global_nofollow_scope', 'all' );
        $this->global_nofollow_external_domains = get_option( 'linkcentral_global_nofollow_external_domains', array() );
        $this->global_sponsored = get_option( 'linkcentral_global_sponsored', false );
        $this->global_sponsored_scope = get_option( 'linkcentral_global_sponsored_scope', 'all' );
        $this->global_sponsored_external_domains = get_option( 'linkcentral_global_sponsored_external_domains', array() );
        $this->global_parameter_forwarding = get_option( 'linkcentral_global_parameter_forwarding', false );
        $this->global_other_attributes = get_option( 'linkcentral_custom_other_attributes', '' );
        $this->global_new_tab = get_option( 'linkcentral_global_new_tab', false );
        $this->global_new_tab_scope = get_option( 'linkcentral_global_new_tab_scope', 'all' );
        $this->global_new_tab_external_domains = get_option( 'linkcentral_global_new_tab_external_domains', array() );
    }

    /**
     * Check if a destination URL is external (different host from the current site).
     *
     * @param string $destination_url The destination URL to check.
     * @return bool True if external, false if internal or unparseable.
     */
    private function is_external_url( $destination_url ) {
        if ( empty( $destination_url ) || !is_string( $destination_url ) ) {
            return false;
        }
        $destination_host = wp_parse_url( $destination_url, PHP_URL_HOST );
        $home_host = wp_parse_url( home_url(), PHP_URL_HOST );
        if ( $destination_host === null || $home_host === null ) {
            return false;
        }
        return strtolower( $destination_host ) !== strtolower( $home_host );
    }

    /**
     * Normalize host/domain for consistent matching.
     *
     * @param string $host Raw host.
     * @return string Normalized host.
     */
    private function normalize_host( $host ) {
        if ( empty( $host ) || !is_string( $host ) ) {
            return '';
        }
        $normalized = strtolower( trim( $host, ". \t\n\r\x00\v" ) );
        $normalized = preg_replace( '/^www\\./', '', $normalized );
        $normalized = preg_replace( '/[^a-z0-9.-]/', '', $normalized );
        return ( is_string( $normalized ) ? $normalized : '' );
    }

    /**
     * Check whether destination URL host matches configured nofollow domains.
     *
     * @param string $destination_url Destination URL.
     * @return bool True when domain list matches destination host.
     */
    private function matches_external_domains( $destination_url, $domains ) {
        if ( empty( $destination_url ) || !is_string( $destination_url ) ) {
            return false;
        }
        $destination_host = wp_parse_url( $destination_url, PHP_URL_HOST );
        $destination_host = $this->normalize_host( (string) $destination_host );
        if ( $destination_host === '' ) {
            return false;
        }
        $domains = ( is_array( $domains ) ? $domains : array() );
        if ( empty( $domains ) ) {
            return false;
        }
        foreach ( $domains as $domain ) {
            $domain = $this->normalize_host( (string) $domain );
            if ( $domain === '' ) {
                continue;
            }
            if ( $destination_host === $domain || substr( $destination_host, -strlen( '.' . $domain ) ) === '.' . $domain ) {
                return true;
            }
        }
        return false;
    }

    /**
     * Get CSS classes for a link based on link data
     *
     * @param array $link_data Link data array containing css_classes_option and custom_css_classes
     * @return string CSS classes to apply
     */
    public function get_css_classes_from_data( $link_data ) {
        switch ( $link_data['css_classes_option'] ) {
            case 'replace':
                return $link_data['custom_css_classes'];
            case 'append':
                return trim( $this->global_css_classes . ' ' . $link_data['custom_css_classes'] );
            case 'default':
            default:
                return $this->global_css_classes;
        }
    }

    /**
     * Get CSS classes for a link based on link ID (for shortcode usage)
     *
     * @param int $link_id The link ID
     * @return string CSS classes to apply
     */
    public function get_css_classes_from_id( $link_id ) {
        return $this->get_css_classes_from_data( [
            'css_classes_option' => get_post_meta( $link_id, '_linkcentral_css_classes_option', true ),
            'custom_css_classes' => get_post_meta( $link_id, '_linkcentral_custom_css_classes', true ),
        ] );
    }

    /**
     * Get nofollow attribute for a link based on link data
     *
     * @param array $link_data Link data array containing nofollow setting and optionally destination_url
     * @return string 'nofollow' if should be applied, empty string otherwise
     */
    public function get_nofollow_from_data( $link_data ) {
        if ( $link_data['nofollow'] === 'yes' ) {
            return 'nofollow';
        } elseif ( $link_data['nofollow'] === 'no' ) {
            return '';
        } else {
            if ( !$this->global_nofollow ) {
                return '';
            }
            $scope = $this->global_nofollow_scope;
            if ( $scope === 'all' ) {
                return 'nofollow';
            }
            $destination_url = ( isset( $link_data['destination_url'] ) ? $link_data['destination_url'] : '' );
            $is_external = $this->is_external_url( $destination_url );
            if ( $scope === 'external' && $is_external ) {
                return 'nofollow';
            }
            if ( $scope === 'internal' && !$is_external ) {
                return 'nofollow';
            }
            if ( $scope === 'external_domains' && $is_external && $this->matches_external_domains( $destination_url, $this->global_nofollow_external_domains ) ) {
                return 'nofollow';
            }
            return '';
        }
    }

    /**
     * Get nofollow attribute for a link based on link ID (for shortcode usage)
     *
     * @param int $link_id The link ID
     * @return string 'nofollow' if should be applied, empty string otherwise
     */
    public function get_nofollow_from_id( $link_id ) {
        return $this->get_nofollow_from_data( [
            'nofollow'        => get_post_meta( $link_id, '_linkcentral_nofollow', true ),
            'destination_url' => get_post_meta( $link_id, '_linkcentral_destination_url', true ),
        ] );
    }

    /**
     * Get sponsored attribute for a link based on link data
     *
     * @param array $link_data Link data array containing sponsored setting and optionally destination_url
     * @return string 'sponsored' if should be applied, empty string otherwise
     */
    public function get_sponsored_from_data( $link_data ) {
        if ( $link_data['sponsored'] === 'yes' ) {
            return 'sponsored';
        } elseif ( $link_data['sponsored'] === 'no' ) {
            return '';
        } else {
            if ( !$this->global_sponsored ) {
                return '';
            }
            $scope = $this->global_sponsored_scope;
            if ( $scope === 'all' ) {
                return 'sponsored';
            }
            $destination_url = ( isset( $link_data['destination_url'] ) ? $link_data['destination_url'] : '' );
            $is_external = $this->is_external_url( $destination_url );
            if ( $scope === 'external' && $is_external ) {
                return 'sponsored';
            }
            if ( $scope === 'internal' && !$is_external ) {
                return 'sponsored';
            }
            if ( $scope === 'external_domains' && $is_external && $this->matches_external_domains( $destination_url, $this->global_sponsored_external_domains ) ) {
                return 'sponsored';
            }
            return '';
        }
    }

    /**
     * Get sponsored attribute for a link based on link ID (for shortcode usage)
     *
     * @param int $link_id The link ID
     * @return string 'sponsored' if should be applied, empty string otherwise
     */
    public function get_sponsored_from_id( $link_id ) {
        return $this->get_sponsored_from_data( [
            'sponsored'       => get_post_meta( $link_id, '_linkcentral_sponsored', true ),
            'destination_url' => get_post_meta( $link_id, '_linkcentral_destination_url', true ),
        ] );
    }

    /**
     * Get other attributes for a link based on link data
     *
     * @param array $link_data Link data array containing other_attributes_option and custom_other_attributes
     * @return string Other attributes to apply
     */
    public function get_other_attributes_from_data( $link_data ) {
        $option = ( isset( $link_data['other_attributes_option'] ) ? $link_data['other_attributes_option'] : 'default' );
        $custom = ( isset( $link_data['custom_other_attributes'] ) ? $link_data['custom_other_attributes'] : '' );
        switch ( $option ) {
            case 'replace':
                return $custom;
            case 'append':
                return trim( $this->global_other_attributes . ' ' . $custom );
            case 'default':
            default:
                return $this->global_other_attributes;
        }
    }

    /**
     * Get other attributes for a link based on link ID (for shortcode usage)
     *
     * @param int $link_id The link ID
     * @return string Other attributes to apply
     */
    public function get_other_attributes_from_id( $link_id ) {
        return $this->get_other_attributes_from_data( [
            'other_attributes_option' => get_post_meta( $link_id, '_linkcentral_other_attributes_option', true ),
            'custom_other_attributes' => get_post_meta( $link_id, '_linkcentral_custom_other_attributes', true ),
        ] );
    }

    /**
     * Get rel attributes array for a link based on link data
     *
     * @param array $link_data Link data array
     * @return array Array of rel attributes to apply
     */
    public function get_rel_attributes_from_data( $link_data ) {
        $rel_attributes = array();
        $nofollow = $this->get_nofollow_from_data( $link_data );
        if ( !empty( $nofollow ) ) {
            $rel_attributes[] = $nofollow;
        }
        $sponsored = $this->get_sponsored_from_data( $link_data );
        if ( !empty( $sponsored ) ) {
            $rel_attributes[] = $sponsored;
        }
        // Add other attributes to rel
        $other_attributes = $this->get_other_attributes_from_data( $link_data );
        if ( !empty( $other_attributes ) ) {
            $rel_attributes[] = $other_attributes;
        }
        return $rel_attributes;
    }

    /**
     * Get rel attributes array for a link based on link ID (for shortcode usage)
     *
     * @param int $link_id The link ID
     * @return array Array of rel attributes to apply
     */
    public function get_rel_attributes_from_id( $link_id ) {
        return $this->get_rel_attributes_from_data( [
            'nofollow'                => get_post_meta( $link_id, '_linkcentral_nofollow', true ),
            'sponsored'               => get_post_meta( $link_id, '_linkcentral_sponsored', true ),
            'other_attributes_option' => get_post_meta( $link_id, '_linkcentral_other_attributes_option', true ),
            'custom_other_attributes' => get_post_meta( $link_id, '_linkcentral_custom_other_attributes', true ),
            'destination_url'         => get_post_meta( $link_id, '_linkcentral_destination_url', true ),
        ] );
    }

    /**
     * Determine if link should open in a new tab based on link data
     *
     * @param array $link_data Link data array containing new_tab setting and optionally destination_url
     * @return bool Whether the link should open in a new tab
     */
    public function get_new_tab_from_data( $link_data ) {
        $new_tab = ( isset( $link_data['new_tab'] ) ? $link_data['new_tab'] : '' );
        if ( $new_tab === 'yes' ) {
            return true;
        } elseif ( $new_tab === 'no' ) {
            return false;
        } else {
            if ( !$this->global_new_tab ) {
                return false;
            }
            $scope = $this->global_new_tab_scope;
            if ( $scope === 'all' ) {
                return true;
            }
            $destination_url = ( isset( $link_data['destination_url'] ) ? $link_data['destination_url'] : '' );
            $is_external = $this->is_external_url( $destination_url );
            if ( $scope === 'external' && $is_external ) {
                return true;
            }
            if ( $scope === 'internal' && !$is_external ) {
                return true;
            }
            if ( $scope === 'external_domains' && $is_external && $this->matches_external_domains( $destination_url, $this->global_new_tab_external_domains ) ) {
                return true;
            }
            return false;
        }
    }

    /**
     * Determine if link should open in a new tab based on link ID
     *
     * @param int $link_id The link ID
     * @return bool Whether the link should open in a new tab
     */
    public function get_new_tab_from_id( $link_id ) {
        return $this->get_new_tab_from_data( [
            'new_tab'         => get_post_meta( $link_id, '_linkcentral_new_tab', true ),
            'destination_url' => get_post_meta( $link_id, '_linkcentral_destination_url', true ),
        ] );
    }

    /**
     * Build HTML attributes string for a link
     *
     * @param array $rel_attributes Array of rel attributes
     * @param string $css_classes CSS classes string
     * @param string $target Target attribute (optional)
     * @return string HTML attributes string
     */
    public function build_html_attributes( $rel_attributes = array(), $css_classes = '', $target = '' ) {
        $attributes = array();
        if ( !empty( $rel_attributes ) ) {
            $attributes[] = 'rel="' . esc_attr( implode( ' ', $rel_attributes ) ) . '"';
        }
        if ( !empty( $css_classes ) ) {
            $attributes[] = 'class="' . esc_attr( $css_classes ) . '"';
        }
        if ( !empty( $target ) ) {
            $attributes[] = 'target="' . esc_attr( $target ) . '"';
        }
        if ( empty( $attributes ) ) {
            return '';
        }
        return ' ' . implode( ' ', $attributes );
    }

}
