<?php

declare(strict_types=1);

namespace Instinctor\Core\Security;

/**
 * Escape — output escaping utilities.
 *
 * Standalone-safe equivalents of WordPress output escaping functions.
 * On WordPress these delegate to the native WP functions (already tested,
 * handles edge cases correctly). On Standalone they use pure PHP.
 *
 * Usage:
 *   echo Escape::html($userInput);
 *   echo '<a href="' . Escape::attr($url) . '">' . Escape::html($label) . '</a>';
 *   echo '<a href="' . Escape::url($href) . '">';
 *   echo Escape::textarea($value);
 *   $safe = Escape::kses($html, ['p', 'strong', 'em', 'a']);
 *   $safe = Escape::ksesPost($richContent);
 *
 * Intentionally NOT providing an Escape::js() that allows arbitrary JS —
 * use json_encode() for passing PHP data to JavaScript instead.
 */

if (!defined('ABSPATH')) exit;
class Escape
{
    // -------------------------------------------------------------------------
    // Core escaping
    // -------------------------------------------------------------------------

    /**
     * Escape for HTML content (between tags).
     * Equivalent to esc_html().
     */
    public static function html(mixed $value): string
    {
        $string = (string) $value;

        if (function_exists('esc_html')) {
            return esc_html($string);
        }

        return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE, 'UTF-8');
    }

    /**
     * Escape for HTML attribute values.
     * Equivalent to esc_attr().
     */
    public static function attr(mixed $value): string
    {
        $string = (string) $value;

        if (function_exists('esc_attr')) {
            return esc_attr($string);
        }

        return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE, 'UTF-8');
    }

    /**
     * Escape and sanitize a URL for use in HTML attributes (href, src, action).
     * Equivalent to esc_url().
     *
     * - Strips disallowed protocols (only http, https, mailto, tel, data:image allowed by default)
     * - Encodes dangerous characters
     */
    public static function url(string $url, array $allowedProtocols = []): string
    {
        if (function_exists('esc_url')) {
            return esc_url($url, $allowedProtocols ?: null);
        }

        $url = trim($url);

        if (empty($url)) {
            return '';
        }

        // Strip dangerous protocols — allow only safe ones
        $allowed = $allowedProtocols ?: ['http', 'https', 'mailto', 'tel'];
        $scheme   = wp_parse_url($url, PHP_URL_SCHEME);

        if ($scheme && !in_array(strtolower($scheme), $allowed, true)) {
            return '';
        }

        // Encode non-ASCII and special chars
        $url = preg_replace_callback(
            '/[^\x20-\x7E]/',
            static fn ($m) => rawurlencode($m[0]),
            $url
        );

        // Strip any remaining javascript: or data: (not image) fragments
        $url = preg_replace('/javascript\s*:/i', '', $url);
        $url = preg_replace('/data\s*:\s*(?!image\/(png|jpg|jpeg|gif|webp|svg\+xml))/i', '', $url);

        return htmlspecialchars($url, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    }

    /**
     * Escape for use inside a <textarea>.
     * Same as esc_textarea() — converts < > & " etc.
     */
    public static function textarea(mixed $value): string
    {
        $string = (string) $value;

        if (function_exists('esc_textarea')) {
            return esc_textarea($string);
        }

        return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE, 'UTF-8');
    }

    /**
     * Escape for inline CSS values.
     * Equivalent to esc_attr() but with additional CSS-specific sanitization.
     */
    public static function css(string $value): string
    {
        // Remove comments, null bytes, and anything that could break out of a style attribute
        $value = preg_replace('/\/\*.*?\*\//s', '', $value);
        $value = str_replace(["\0", "\n", "\r", "\t"], ' ', $value);
        $value = preg_replace('/(expression|javascript|vbscript|behavior)\s*\(/i', '', $value);

        return self::attr($value);
    }

    // -------------------------------------------------------------------------
    // HTML filtering (kses)
    // -------------------------------------------------------------------------

    /**
     * Allow only specified HTML tags and attributes; strip everything else.
     * Equivalent to wp_kses().
     *
     * $allowedTags can be:
     *   - array of tag names (attrs auto-set to safe defaults)
     *   - full wp_kses-style array: ['a' => ['href' => [], 'title' => []], ...]
     *
     * @param string       $html
     * @param array|string $allowedTags
     */
    public static function kses(string $html, array|string $allowedTags = []): string
    {
        // wp_kses is always available at runtime: WordPress core provides it on
        // WP, and bootstrap/wp-shims.php provides it on standalone. Build a
        // wp_kses-compatible array if given a flat tag list.
        if (isset($allowedTags[0]) && is_string($allowedTags[0])) {
            $allowedTags = self::buildKsesArray($allowedTags);
        }
        return wp_kses($html, $allowedTags);
    }

    /**
     * Allow the standard set of safe HTML tags used in post content.
     * Equivalent to wp_kses_post().
     */
    public static function ksesPost(string $html): string
    {
        if (function_exists('wp_kses_post')) {
            return wp_kses_post($html);
        }

        return self::kses($html, self::postAllowedTags());
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    /**
     * Build a wp_kses-compatible allowed-tags array from a flat list of tag names.
     * Includes safe default attributes for common tags.
     */
    private static function buildKsesArray(array $tags): array
    {
        $safeGlobal = ['class' => [], 'id' => [], 'style' => [], 'title' => [], 'lang' => [], 'dir' => []];
        $linkAttrs  = array_merge($safeGlobal, ['href' => [], 'target' => [], 'rel' => [], 'download' => []]);
        $imgAttrs   = array_merge($safeGlobal, ['src' => [], 'alt' => [], 'width' => [], 'height' => [], 'loading' => []]);

        $map = [
            'a'          => $linkAttrs,
            'abbr'       => $safeGlobal,
            'b'          => $safeGlobal,
            'blockquote' => array_merge($safeGlobal, ['cite' => []]),
            'br'         => [],
            'caption'    => $safeGlobal,
            'cite'       => $safeGlobal,
            'code'       => $safeGlobal,
            'del'        => array_merge($safeGlobal, ['datetime' => []]),
            'em'         => $safeGlobal,
            'h1'         => $safeGlobal,
            'h2'         => $safeGlobal,
            'h3'         => $safeGlobal,
            'h4'         => $safeGlobal,
            'h5'         => $safeGlobal,
            'h6'         => $safeGlobal,
            'hr'         => $safeGlobal,
            'i'          => $safeGlobal,
            'img'        => $imgAttrs,
            'ins'        => array_merge($safeGlobal, ['datetime' => [], 'cite' => []]),
            'li'         => $safeGlobal,
            'ol'         => $safeGlobal,
            'p'          => $safeGlobal,
            'pre'        => $safeGlobal,
            'q'          => array_merge($safeGlobal, ['cite' => []]),
            's'          => $safeGlobal,
            'span'       => $safeGlobal,
            'strong'     => $safeGlobal,
            'sub'        => $safeGlobal,
            'sup'        => $safeGlobal,
            'table'      => $safeGlobal,
            'tbody'      => $safeGlobal,
            'td'         => array_merge($safeGlobal, ['colspan' => [], 'rowspan' => []]),
            'th'         => array_merge($safeGlobal, ['colspan' => [], 'rowspan' => [], 'scope' => []]),
            'thead'      => $safeGlobal,
            'tr'         => $safeGlobal,
            'u'          => $safeGlobal,
            'ul'         => $safeGlobal,
        ];

        $result = [];
        foreach ($tags as $tag) {
            $result[$tag] = $map[$tag] ?? $safeGlobal;
        }

        return $result;
    }

    /**
     * Standard allowed tags for post-like rich content (kses_post equivalent).
     */
    private static function postAllowedTags(): array
    {
        return self::buildKsesArray([
            'a', 'abbr', 'b', 'blockquote', 'br', 'caption', 'cite', 'code',
            'del', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img',
            'ins', 'li', 'ol', 'p', 'pre', 'q', 's', 'span', 'strong', 'sub',
            'sup', 'table', 'tbody', 'td', 'th', 'thead', 'tr', 'u', 'ul',
        ]);
    }
}
