<?php

declare(strict_types=1);

namespace WPUltimateSecurity\UltimateSecurity\Support\Helpers;

/**
 * String Helper Class
 *
 * Provides utilities for string manipulation including random generation,
 * UUID creation, truncation, slugification, and data masking.
 *
 * @package WPUltimateSecurity\UltimateSecurity\Support\Helpers
 * @since 1.0.0
 */
class StringHelper
{
    /**
     * Character sets for random string generation.
     *
     * @var array<string, string>
     */
    private const CHARSETS = [
        'alphanumeric' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
        'alpha' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
        'alpha_lower' => 'abcdefghijklmnopqrstuvwxyz',
        'alpha_upper' => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
        'numeric' => '0123456789',
        'hex' => '0123456789abcdef',
        'hex_upper' => '0123456789ABCDEF',
        'special' => '!@#$%^&*()_+-=[]{}|;:,.<>?',
        'alphanumeric_special' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?',
    ];

    /**
     * Generate a cryptographically secure random string.
     *
     * @param int $length The desired length of the string (default: 32)
     * @param string $charset The character set to use (default: 'alphanumeric')
     *                        Options: alphanumeric, alpha, alpha_lower, alpha_upper,
     *                        numeric, hex, hex_upper, special, alphanumeric_special,
     *                        or a custom string of characters
     * @return string The generated random string
     * @throws \InvalidArgumentException If length is less than 1
     */
    public static function random(int $length = 32, string $charset = 'alphanumeric'): string
    {
        if ($length < 1) {
            throw new \InvalidArgumentException('Length must be at least 1');
        }

        // Use predefined charset or custom string
        $characters = self::CHARSETS[$charset] ?? $charset;
        $charsetLength = strlen($characters);

        if ($charsetLength === 0) {
            throw new \InvalidArgumentException('Character set cannot be empty');
        }

        $result = '';

        // Use WordPress's wp_generate_password for simple cases
        if ($charset === 'alphanumeric') {
            return wp_generate_password($length, false, false);
        }

        // For other charsets, use random_int for cryptographic security
        for ($i = 0; $i < $length; $i++) {
            $result .= $characters[random_int(0, $charsetLength - 1)];
        }

        return $result;
    }

    /**
     * Generate a UUID v4 (random).
     *
     * Generates a universally unique identifier following RFC 4122 version 4.
     *
     * @return string A UUID in the format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
     */
    public static function uuid(): string
    {
        // Use wp_generate_uuid4 if available (WordPress 4.7+)
        if (function_exists('wp_generate_uuid4')) {
            return wp_generate_uuid4();
        }

        // Fallback implementation
        $data = random_bytes(16);

        // Set version to 4 (random)
        $data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
        // Set variant to RFC 4122
        $data[8] = chr((ord($data[8]) & 0x3f) | 0x80);

        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
    }

    /**
     * Safely truncate a string with optional suffix.
     *
     * Truncates at word boundaries when possible to avoid cutting words in half.
     *
     * @param string $string The string to truncate
     * @param int $length Maximum length (including suffix)
     * @param string $suffix The suffix to append when truncated (default: '...')
     * @param bool $wordBoundary Whether to truncate at word boundaries (default: true)
     * @return string The truncated string
     */
    public static function truncate(
        string $string,
        int $length,
        string $suffix = '...',
        bool $wordBoundary = true
    ): string {
        if ($length < 1) {
            return '';
        }

        // If string fits, return as-is
        if (mb_strlen($string) <= $length) {
            return $string;
        }

        $suffixLength = mb_strlen($suffix);
        $maxLength = $length - $suffixLength;

        if ($maxLength < 1) {
            return mb_substr($suffix, 0, $length);
        }

        $truncated = mb_substr($string, 0, $maxLength);

        // Try to truncate at word boundary
        if ($wordBoundary) {
            $lastSpace = mb_strrpos($truncated, ' ');
            if ($lastSpace !== false && $lastSpace > $maxLength * 0.5) {
                $truncated = mb_substr($truncated, 0, $lastSpace);
            }
        }

        return rtrim($truncated) . $suffix;
    }

    /**
     * Convert a string to a URL-friendly slug (kebab-case).
     *
     * @param string $string The string to slugify
     * @param string $separator The separator to use (default: '-')
     * @param bool $lowercase Whether to convert to lowercase (default: true)
     * @return string The slugified string
     */
    public static function slugify(string $string, string $separator = '-', bool $lowercase = true): string
    {
        // Use WordPress's sanitize_title for basic slugification
        $slug = sanitize_title($string, '', 'save');

        // Replace underscores with the separator if needed
        if ($separator !== '-') {
            $slug = str_replace('-', $separator, $slug);
        }

        // Apply lowercase if needed (sanitize_title already does this)
        if (!$lowercase) {
            // Restore original casing while keeping the slug structure
            $slug = remove_accents($string);
            $slug = preg_replace('/[^a-zA-Z0-9\s]/', '', $slug);
            $slug = preg_replace('/\s+/', $separator, $slug);
            $slug = trim($slug, $separator);
        }

        return $slug;
    }

    /**
     * Check if a string starts with a given substring.
     *
     * @param string $haystack The string to search in
     * @param string $needle The substring to search for
     * @return bool True if haystack starts with needle
     */
    public static function startsWith(string $haystack, string $needle): bool
    {
        return str_starts_with($haystack, $needle);
    }

    /**
     * Check if a string ends with a given substring.
     *
     * @param string $haystack The string to search in
     * @param string $needle The substring to search for
     * @return bool True if haystack ends with needle
     */
    public static function endsWith(string $haystack, string $needle): bool
    {
        return str_ends_with($haystack, $needle);
    }

    /**
     * Check if a string contains a given substring.
     *
     * @param string $haystack The string to search in
     * @param string $needle The substring to search for
     * @return bool True if haystack contains needle
     */
    public static function contains(string $haystack, string $needle): bool
    {
        return str_contains($haystack, $needle);
    }

    /**
     * Mask sensitive data by replacing middle characters with asterisks.
     *
     * @param string $string The string to mask
     * @param int $visibleStart Number of characters to show at the start (default: 2)
     * @param int $visibleEnd Number of characters to show at the end (default: 2)
     * @param string $maskChar The character to use for masking (default: '*')
     * @return string The masked string
     */
    public static function mask(
        string $string,
        int $visibleStart = 2,
        int $visibleEnd = 2,
        string $maskChar = '*'
    ): string {
        $length = mb_strlen($string);

        // If string is too short, mask everything
        if ($length <= $visibleStart + $visibleEnd) {
            return str_repeat($maskChar, $length);
        }

        $start = mb_substr($string, 0, $visibleStart);
        $end = mb_substr($string, -$visibleEnd);
        $maskLength = $length - $visibleStart - $visibleEnd;

        return $start . str_repeat($maskChar, $maskLength) . $end;
    }

    /**
     * Mask an email address for privacy.
     *
     * Preserves the first character of the local part and domain,
     * plus the TLD.
     *
     * @param string $email The email to mask
     * @return string The masked email (e.g., j***@e***.com)
     */
    public static function maskEmail(string $email): string
    {
        if (!is_email($email)) {
            return self::mask($email);
        }

        $parts = explode('@', $email);
        if (count($parts) !== 2) {
            return self::mask($email);
        }

        [$local, $domain] = $parts;

        // Mask local part
        $localMasked = self::mask($local, 1, 0);

        // Mask domain (preserve first char and TLD)
        $domainParts = explode('.', $domain);
        $tld = array_pop($domainParts);
        $domainMain = implode('.', $domainParts);
        $domainMasked = self::mask($domainMain, 1, 0) . '.' . $tld;

        return $localMasked . '@' . $domainMasked;
    }

    /**
     * Mask a phone number for privacy.
     *
     * @param string $phone The phone number to mask
     * @param int $visibleEnd Number of digits to show at the end (default: 4)
     * @return string The masked phone number
     */
    public static function maskPhone(string $phone, int $visibleEnd = 4): string
    {
        // Remove non-numeric characters for counting
        $digits = preg_replace('/[^0-9]/', '', $phone);
        $digitCount = strlen($digits);

        if ($digitCount <= $visibleEnd) {
            return str_repeat('*', $digitCount);
        }

        // Mask all but last N digits
        $masked = '';
        $digitIndex = 0;
        $maskCount = $digitCount - $visibleEnd;

        for ($i = 0; $i < strlen($phone); $i++) {
            $char = $phone[$i];
            if (ctype_digit($char)) {
                $masked .= $digitIndex < $maskCount ? '*' : $char;
                $digitIndex++;
            } else {
                $masked .= $char;
            }
        }

        return $masked;
    }

    /**
     * Convert a string to camelCase.
     *
     * @param string $string The string to convert
     * @return string The camelCase string
     */
    public static function camelCase(string $string): string
    {
        $string = self::pascalCase($string);
        return lcfirst($string);
    }

    /**
     * Convert a string to PascalCase.
     *
     * @param string $string The string to convert
     * @return string The PascalCase string
     */
    public static function pascalCase(string $string): string
    {
        // Replace separators with spaces
        $string = str_replace(['-', '_'], ' ', $string);
        // Capitalize each word
        $string = ucwords($string);
        // Remove spaces
        return str_replace(' ', '', $string);
    }

    /**
     * Convert a string to snake_case.
     *
     * @param string $string The string to convert
     * @return string The snake_case string
     */
    public static function snakeCase(string $string): string
    {
        // Insert underscores before uppercase letters
        $string = preg_replace('/([a-z])([A-Z])/', '$1_$2', $string);
        // Replace spaces and hyphens with underscores
        $string = preg_replace('/[\s\-]+/', '_', $string);
        // Convert to lowercase
        return strtolower($string);
    }

    /**
     * Convert a string to kebab-case.
     *
     * @param string $string The string to convert
     * @return string The kebab-case string
     */
    public static function kebabCase(string $string): string
    {
        return str_replace('_', '-', self::snakeCase($string));
    }

    /**
     * Limit a string to a number of words.
     *
     * @param string $string The string to limit
     * @param int $words Maximum number of words
     * @param string $suffix Suffix to append when truncated (default: '...')
     * @return string The limited string
     */
    public static function limitWords(string $string, int $words, string $suffix = '...'): string
    {
        $wordArray = preg_split('/\s+/', $string, $words + 1, PREG_SPLIT_NO_EMPTY);

        if (count($wordArray) <= $words) {
            return $string;
        }

        array_pop($wordArray);
        return implode(' ', $wordArray) . $suffix;
    }

    /**
     * Check if a string is a valid JSON.
     *
     * @param string $string The string to check
     * @return bool True if the string is valid JSON
     */
    public static function isJson(string $string): bool
    {
        if ($string === '') {
            return false;
        }

        json_decode($string);
        return json_last_error() === JSON_ERROR_NONE;
    }

    /**
     * Extract all URLs from a string.
     *
     * @param string $string The string to search
     * @return array<string> Array of found URLs
     */
    public static function extractUrls(string $string): array
    {
        $pattern = '/https?:\/\/[^\s<>"\']+/i';
        preg_match_all($pattern, $string, $matches);
        return $matches[0] ?? [];
    }

    /**
     * Extract all email addresses from a string.
     *
     * @param string $string The string to search
     * @return array<string> Array of found email addresses
     */
    public static function extractEmails(string $string): array
    {
        $pattern = '/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/';
        preg_match_all($pattern, $string, $matches);
        return array_filter($matches[0] ?? [], 'is_email');
    }

    /**
     * Generate a secure token suitable for URLs.
     *
     * @param int $length The desired length (default: 32)
     * @return string A URL-safe token
     */
    public static function urlSafeToken(int $length = 32): string
    {
        $bytes = (int) ceil($length * 3 / 4);
        $token = base64_encode(random_bytes($bytes));

        // Make URL-safe
        $token = strtr($token, '+/', '-_');
        $token = rtrim($token, '=');

        return substr($token, 0, $length);
    }

    /**
     * Remove all whitespace from a string.
     *
     * @param string $string The string to process
     * @return string The string without whitespace
     */
    public static function removeWhitespace(string $string): string
    {
        return preg_replace('/\s+/', '', $string);
    }

    /**
     * Normalize whitespace in a string (collapse multiple spaces to single).
     *
     * @param string $string The string to normalize
     * @return string The normalized string
     */
    public static function normalizeWhitespace(string $string): string
    {
        return preg_replace('/\s+/', ' ', trim($string));
    }

    /**
     * Escape a string for use in a regular expression.
     *
     * @param string $string The string to escape
     * @param string $delimiter The regex delimiter (default: '/')
     * @return string The escaped string
     */
    public static function escapeRegex(string $string, string $delimiter = '/'): string
    {
        return preg_quote($string, $delimiter);
    }

    /**
     * Check if a string matches a wildcard pattern.
     *
     * @param string $string The string to check
     * @param string $pattern The pattern (* for any characters, ? for single character)
     * @return bool True if the string matches the pattern
     */
    public static function matchesWildcard(string $string, string $pattern): bool
    {
        $regex = str_replace(
            ['\*', '\?'],
            ['.*', '.'],
            preg_quote($pattern, '/')
        );

        return preg_match('/^' . $regex . '$/i', $string) === 1;
    }
}
