<?php

declare(strict_types=1);

namespace Instinctor\Core;


if (!defined('ABSPATH')) exit;
use Instinctor\Core\Security\RateLimit;
use Instinctor\Core\Security\Spam;
use Instinctor\Core\Security\Escape;

/**
 * Security — static facade over all T7 security utilities.
 *
 * Brings together:
 *   Security\RateLimit — sliding-window rate limiting + brute-force protection
 *   Security\Spam      — time-based honeypot
 *   Security\Escape    — output escaping (esc_html, esc_attr, esc_url, kses)
 *
 * ── Rate limiting ─────────────────────────────────────────────────────────────
 *
 *   Security::rateLimit('login', $ip)          // 5/min — returns false if exceeded
 *   Security::rateLimit('register', $ip)       // 5/min
 *   Security::rateLimit('checkout', $ip)       // 10/min
 *   Security::rateLimit('contact_form', $ip)   // 3/min
 *   Security::rateLimit('rest_api', $ip)       // 60/min
 *   Security::rateLimit('custom_key', $ip, 20, 120)  // 20/2min, custom
 *
 *   Security::clearRateLimit('login:127.0.0.1')  // clear after success
 *
 * ── Brute-force ───────────────────────────────────────────────────────────────
 *
 *   Security::recordLogin($identifier, $ip, $success)
 *   Security::isLockedOut($identifier, $ip)   // true = deny login
 *
 * ── Honeypot ──────────────────────────────────────────────────────────────────
 *
 *   echo Security::honeypotField()            // in form template
 *   Security::validateHoneypot()              // on submit
 *
 * ── Escaping ──────────────────────────────────────────────────────────────────
 *
 *   Security::esc($value)                     // html-safe for content
 *   Security::attr($value)                    // html-safe for attributes
 *   Security::url($href)                      // url-safe href
 *   Security::kses($html, ['p', 'strong'])    // strip disallowed tags
 *   Security::ksesPost($richHtml)             // full post-content allowed set
 */
class Security
{
    // ── Predefined rate-limit buckets ─────────────────────────────────────────
    //   [limit, window_seconds]
    private const PRESETS = [
        'login'        => [5,  60],
        'register'     => [5,  60],
        'checkout'     => [10, 60],
        'contact_form' => [3,  60],
        'rest_api'     => [60, 60],
    ];

    // -------------------------------------------------------------------------
    // Rate limiting
    // -------------------------------------------------------------------------

    /**
     * Check a rate limit bucket. Returns false if the limit is exceeded.
     *
     * @param string $preset  Preset name ('login', 'checkout', etc.) or custom key
     * @param string $ip      Client IP address
     * @param int    $limit   Hits allowed per window (ignored for presets)
     * @param int    $window  Window size in seconds (ignored for presets)
     */
    public static function rateLimit(
        string $preset,
        string $ip,
        int $limit = 30,
        int $window = 60
    ): bool {
        [$l, $w] = self::PRESETS[$preset] ?? [$limit, $window];
        $key = $preset . ':' . $ip;

        return RateLimit::check($key, $l, $w);
    }

    /**
     * Clear a rate limit bucket (e.g. after successful login).
     */
    public static function clearRateLimit(string $key): void
    {
        RateLimit::clear($key);
    }

    /**
     * Get current hit count for a bucket.
     */
    public static function rateLimitHits(string $key): int
    {
        return RateLimit::hits($key);
    }

    // -------------------------------------------------------------------------
    // Brute-force / login attempts
    // -------------------------------------------------------------------------

    /**
     * Record a login attempt.
     *
     * @param string $identifier  Email or username
     * @param string $ip          Client IP
     * @param bool   $success     Whether the attempt succeeded
     */
    public static function recordLogin(string $identifier, string $ip, bool $success): void
    {
        RateLimit::recordLoginAttempt($identifier, $ip, $success);
    }

    /**
     * Check whether an IP or identifier is locked out due to repeated failures.
     */
    public static function isLockedOut(string $identifier, string $ip): bool
    {
        return RateLimit::isLockedOut($identifier, $ip);
    }

    /**
     * Prune old login attempt records.
     * Auto-called daily via CronService.
     */
    public static function pruneLoginAttempts(int $days = 30): int
    {
        return RateLimit::pruneLoginAttempts($days);
    }

    // -------------------------------------------------------------------------
    // Spam / Honeypot
    // -------------------------------------------------------------------------

    /**
     * Render hidden honeypot + timing fields for a form.
     */
    public static function honeypotField(): string
    {
        return Spam::honeypotField();
    }

    /**
     * Validate submitted honeypot fields.
     * Returns false if the submission looks like a bot.
     */
    public static function validateHoneypot(array $post = []): bool
    {
        return Spam::validateHoneypot($post);
    }

    /**
     * Classified honeypot check. Prefer this anywhere a failure would otherwise
     * be answered with a fake success — only Spam::RESULT_BOT_* may be swallowed
     * silently, everything else has to reach the sender. See Spam::checkHoneypot.
     */
    public static function checkHoneypot(array $post = []): string
    {
        return Spam::checkHoneypot($post);
    }

    // -------------------------------------------------------------------------
    // Escaping (delegates to Escape class)
    // -------------------------------------------------------------------------

    /** Escape for HTML content between tags. */
    public static function esc(mixed $value): string
    {
        return Escape::html($value);
    }

    /** Escape for HTML attribute values. */
    public static function attr(mixed $value): string
    {
        return Escape::attr($value);
    }

    /** Escape and validate a URL for use in href/src. */
    public static function url(string $url, array $protocols = []): string
    {
        return Escape::url($url, $protocols);
    }

    /** Escape for textarea content. */
    public static function textarea(mixed $value): string
    {
        return Escape::textarea($value);
    }

    /** Escape for inline CSS attribute values. */
    public static function css(string $value): string
    {
        return Escape::css($value);
    }

    /** Strip all but allowed HTML tags. */
    public static function kses(string $html, array $allowed = []): string
    {
        return Escape::kses($html, $allowed);
    }

    /** Allow standard post-content HTML tags. */
    public static function ksesPost(string $html): string
    {
        return Escape::ksesPost($html);
    }

    // -------------------------------------------------------------------------
    // IP helpers
    // -------------------------------------------------------------------------

    /**
     * Get the real client IP, respecting common proxy headers.
     * Sanitized to a valid IP string.
     */
    public static function clientIp(): string
    {
        $headers = [
            'HTTP_CF_CONNECTING_IP',   // Cloudflare
            'HTTP_X_REAL_IP',          // nginx proxy
            'HTTP_X_FORWARDED_FOR',    // standard proxy header (may be comma-separated)
            'REMOTE_ADDR',             // direct connection
        ];

        foreach ($headers as $header) {
            if (empty($_SERVER[$header])) {
                continue;
            }

            $ip = trim(explode(',', sanitize_text_field(wp_unslash($_SERVER[$header] ?? '')))[0]);

            if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                return $ip;
            }
        }

        // Fallback — accept private-range IPs (dev environments)
        $remoteAddr = sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'));
        return filter_var($remoteAddr, FILTER_VALIDATE_IP)
            ? $remoteAddr
            : '127.0.0.1';
    }
}
