<?php
namespace DevOwl\RealProductManagerWpClient;

use WP_User;

// @codeCoverageIgnoreStart
defined('ABSPATH') or die('No script kiddies please!'); // Avoid direct file request
// @codeCoverageIgnoreEnd

/**
 * Utils functionality.
 */
class Utils {
    use UtilsProvider;

    /**
     * Check if a string starts with a given needle.
     *
     * @param string $haystack The string to search in
     * @param string $needle The starting string
     * @see https://stackoverflow.com/a/834355/5506547
     */
    public static function startsWith($haystack, $needle) {
        if (!is_string($haystack) || !is_string($needle)) {
            return false;
        }

        $length = strlen($needle);
        return substr($haystack, 0, $length) === $needle;
    }

    /**
     * Check if the current page request gets redirected.
     */
    public static function isRedirected() {
        foreach (headers_list() as $line) {
            $header = strtolower($line);

            if (self::startsWith($header, 'location:')) {
                return true;
            }
        }

        return false;
    }

    /**
     * Get the IP address of the current request.
     */
    public static function getIpAddress() {
        if (isset($_SERVER['HTTP_X_REAL_IP'])) {
            return sanitize_text_field(wp_unslash($_SERVER['HTTP_X_REAL_IP']));
        } elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
            // `HTTP_X_FORWARDED_FOR` can contain multiple IPs
            return (string) rest_is_ip_address(
                trim(current(preg_split('/,/', sanitize_text_field(wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR'])))))
            );
        } elseif (isset($_SERVER['REMOTE_ADDR'])) {
            return sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']));
        }

        return null;
    }

    /**
     * Get the full name for the currently logged in user.
     *
     * @param WP_User $user Fall back to current user
     */
    public static function getUserFullName($user = null) {
        $user = $user === null ? wp_get_current_user() : $user;

        if ($user instanceof WP_User) {
            $names = [];

            if (!empty($user->first_name)) {
                $names[] = $user->first_name;
            }

            if (!empty($user->last_name)) {
                $names[] = $user->last_name;
            }

            return count($names) > 0 ? join(' ', $names) : '';
        }

        return '';
    }

    /**
     * Get the raw value of a `wp_options` value by respecting the object cache. It is not modified
     * through option-filters.
     *
     * @param string $optionName
     * @param mixed $default
     */
    public static function getOptionRaw($optionName, $default = false) {
        // Force so the options cache is filled
        get_option($optionName);

        // Directly read from our cache cause we want to skip `site_url` / `option_site_url` filters (https://git.io/JOnGV)
        // Why `alloptions`? Due to the fact that `siteurl` is `autoloaded=yes`, it is loaded via `wp_load_alloptions` and filled
        // to the cache key `alloptions`. The filters are used by WPML and PolyLang but we do not care about them. Non-autoloaded
        // options are read from `notoptions`.
        foreach (['alloptions', 'notoptions'] as $cacheKey) {
            $cache = wp_cache_get($cacheKey, 'options');
            if (is_array($cache) && isset($cache[$optionName])) {
                return maybe_unserialize($cache[$optionName]);
            }
        }

        // Fallback to directly read the option from the `options` cache
        $directFromCache = wp_cache_get($optionName, 'options');
        if ($directFromCache !== false) {
            return maybe_unserialize($directFromCache);
        }

        return $default;
    }

    /**
     * Get current home url, normalized without schema, `www` subdomain and path.
     * This avoids general conflicts for situations, when customers move their
     * HTTP site to HTTPS.
     *
     * @return string Can be empty, e.g. for WP CLI and WP Cronjob when Object Cache is active
     */
    public static function getCurrentHostname() {
        // In multisite, `switch_to_blog()` must resolve the hostname from the current blog's
        // persisted options. A global `WP_SITEURL` constant can be request-dependent and would
        // otherwise make every subsite look like the same host.
        if (!is_multisite() && defined('WP_SITEURL')) {
            // Constant is defined (https://wordpress.org/support/article/changing-the-site-url/#edit-wp-config-php)
            $site_url = constant('WP_SITEURL');
        } else {
            $site_url = self::getOptionRaw('siteurl', site_url());
        }

        $url = parse_url($site_url, PHP_URL_HOST);
        $url = preg_replace('/^www\./', '', $url);

        // Remove default ports (https://regex101.com/r/eyxvPE/1)
        $url = preg_replace('/:(80|443)$/', '', $url);
        $url = strtolower($url);

        /**
         * This filter allows you to connect multiple hosts / subdomains to one main host and reduce the number of needed license keys.
         *
         * This filter is only allowed to link thematically, related subdomains / domains, so that one license key is valid for these subdomains / domains.
         *
         * ### Use case example: Multilingual through multisite
         *
         * You have a multisite and each subsite represents a language. When opening the license activation in dialog and you need to
         * activate a license for each language:
         *
         * - English (example.com)
         * - German (de.example.com)
         * - Spanish (es.example.com)
         *
         * You can add the following filter to your `functions.php` to map `de.` and `es.` to the main host `example.com`
         *
         * ```php
         * <?php
         * add_filter('DevOwl/RealProductManager/HostMap/ConnectThematic', function($host) {
         *   return "example.com";
         * });
         * ```
         *
         * Afterward, only one license is requested for `example.com`.
         *
         * ### Use case example: Shop and blog
         *
         * You have a multisite and there you maintain pages for different organizations. Each organization has a blog and a store.
         * When opening the license activation in dialog and you need to activate a license for each language:
         *
         * - Main (example.com)
         * - Organization 1 (org1.com)
         * - Organization 1 shop (shop.org1.com)
         * - Organization 2 (org2.com)
         * - Organization 2 shop (shop.org2.com)
         *
         * Now, instead of needed two license keys for each organization you can connect it thematically:
         *
         * ```php
         * <?php
         * add_filter('DevOwl/RealProductManager/HostMap/ConnectThematic', function($host) {
         *   if ($host === 'shop.org1.com') {
         *     return 'org1.com';
         *   }
         *
         *   if ($host === 'shop.org2.com') {
         *     return 'org2.com';
         *   }
         *
         *   return $host;
         * });
         * ```
         *
         * Afterward, only three licenses are requested for `example.com`, `org1.com` and `org2.com`.
         *
         * @hook DevOwl/RealProductManager/HostMap/ConnectThematic
         * @param {string} $host
         * @param {int} $blogId
         * @since 1.7.9
         * @return {string}
         * @see https://devowl.io/knowledge-base/licensing-of-multiple-websites/
         */
        return apply_filters('DevOwl/RealProductManager/HostMap/ConnectThematic', $url, get_current_blog_id());
    }

    /**
     * To avoid issues with multisites without own domains, we need to map blog ids
     * to their `site_url`'s host so we can determine the used license for a given blog.
     *
     * @param int[] $blogIds
     */
    public static function mapBlogsToHosts($blogIds) {
        // Map blog ids to potential hostnames and reverse
        $hostnames = [
            'blog' => [],
            'host' => [],
        ];
        $isMu = is_multisite();
        foreach ($blogIds as $blogId) {
            if ($isMu) {
                switch_to_blog($blogId);
            }

            $host = self::getCurrentHostname();

            $hostnames['blog'][$blogId] = $host;
            $hostnames['host'][$host][] = $blogId;

            if ($isMu) {
                restore_current_blog();
            }
        }
        return $hostnames;
    }
}
