<?php
/**
 * REST API Controller
 *
 * @package Jeremias\WcLicensedProduct\Api
 */

declare(strict_types=1);

namespace Jeremias\WcLicensedProduct\Api;

use Jeremias\WcLicensedProduct\License\LicenseManager;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

/**
 * Handles REST API endpoints for license validation
 */
final class RestApiController
{
    private const NAMESPACE = 'wc-licensed-product/v1';

    /**
     * Default rate limit: requests per window per IP
     */
    private const DEFAULT_RATE_LIMIT = 30;

    /**
     * Default rate limit window in seconds
     */
    private const DEFAULT_RATE_WINDOW = 60;

    /**
     * Get the configured rate limit (requests per window)
     */
    private function getRateLimit(): int
    {
        return defined('WC_LICENSE_RATE_LIMIT')
            ? (int) WC_LICENSE_RATE_LIMIT
            : self::DEFAULT_RATE_LIMIT;
    }

    /**
     * Get the configured rate limit window in seconds
     */
    private function getRateWindow(): int
    {
        return defined('WC_LICENSE_RATE_WINDOW')
            ? (int) WC_LICENSE_RATE_WINDOW
            : self::DEFAULT_RATE_WINDOW;
    }

    private LicenseManager $licenseManager;

    public function __construct(LicenseManager $licenseManager)
    {
        $this->licenseManager = $licenseManager;
        $this->registerHooks();
    }

    /**
     * Register WordPress hooks
     */
    private function registerHooks(): void
    {
        add_action('rest_api_init', [$this, 'registerRoutes']);
    }

    /**
     * Check rate limit for current IP
     *
     * @return WP_REST_Response|null Returns error response if rate limited, null if OK
     */
    private function checkRateLimit(): ?WP_REST_Response
    {
        $ip = $this->getClientIp();
        $transientKey = 'wclp_rate_' . md5($ip);
        $rateLimit = $this->getRateLimit();
        $rateWindow = $this->getRateWindow();

        $data = get_transient($transientKey);

        if ($data === false) {
            // First request, start counting
            set_transient($transientKey, ['count' => 1, 'start' => time()], $rateWindow);
            return null;
        }

        $count = (int) ($data['count'] ?? 0);
        $start = (int) ($data['start'] ?? time());

        // Check if window has expired
        if (time() - $start >= $rateWindow) {
            // Reset counter
            set_transient($transientKey, ['count' => 1, 'start' => time()], $rateWindow);
            return null;
        }

        // Check if limit exceeded
        if ($count >= $rateLimit) {
            $retryAfter = $rateWindow - (time() - $start);
            $response = new WP_REST_Response([
                'success' => false,
                'error' => 'rate_limit_exceeded',
                'message' => __('Too many requests. Please try again later.', 'wc-licensed-product'),
                'retry_after' => $retryAfter,
            ], 429);
            $response->header('Retry-After', (string) $retryAfter);
            return $response;
        }

        // Increment counter
        set_transient($transientKey, ['count' => $count + 1, 'start' => $start], $rateWindow);
        return null;
    }

    /**
     * Get client IP address
     *
     * Security note: Only trust proxy headers when explicitly configured.
     * Set WC_LICENSE_TRUSTED_PROXIES constant or configure trusted_proxies
     * in wp-config.php to enable proxy header support.
     *
     * @return string Client IP address
     */
    private function getClientIp(): string
    {
        // Get the direct connection IP first
        $remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';

        // Only check proxy headers if we're behind a trusted proxy
        if ($this->isTrustedProxy($remoteAddr)) {
            // Check headers in order of trust preference
            $headers = [
                'HTTP_CF_CONNECTING_IP', // Cloudflare
                'HTTP_X_FORWARDED_FOR',
                'HTTP_X_REAL_IP',
            ];

            foreach ($headers as $header) {
                if (!empty($_SERVER[$header])) {
                    $ips = explode(',', $_SERVER[$header]);
                    $ip = trim($ips[0]);
                    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                        return $ip;
                    }
                }
            }
        }

        // Validate and return direct connection IP
        if (filter_var($remoteAddr, FILTER_VALIDATE_IP)) {
            return $remoteAddr;
        }

        return '0.0.0.0';
    }

    /**
     * Check if the given IP is a trusted proxy
     *
     * @param string $ip The IP address to check
     * @return bool Whether the IP is a trusted proxy
     */
    private function isTrustedProxy(string $ip): bool
    {
        // Check if trusted proxies are configured
        if (!defined('WC_LICENSE_TRUSTED_PROXIES')) {
            return false;
        }

        $trustedProxies = WC_LICENSE_TRUSTED_PROXIES;

        // Handle string constant (comma-separated list)
        if (is_string($trustedProxies)) {
            $trustedProxies = array_map('trim', explode(',', $trustedProxies));
        }

        if (!is_array($trustedProxies)) {
            return false;
        }

        // Check for special keywords
        if (in_array('CLOUDFLARE', $trustedProxies, true)) {
            // Cloudflare IP ranges (simplified - in production, fetch from Cloudflare API)
            if ($this->isCloudflareIp($ip)) {
                return true;
            }
        }

        // Check direct IP match or CIDR notation
        foreach ($trustedProxies as $proxy) {
            if ($proxy === $ip) {
                return true;
            }

            // Support CIDR notation
            if (str_contains($proxy, '/') && $this->ipMatchesCidr($ip, $proxy)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check if IP is in Cloudflare range
     *
     * @param string $ip The IP to check
     * @return bool Whether IP belongs to Cloudflare
     */
    private function isCloudflareIp(string $ip): bool
    {
        // Cloudflare IPv4 ranges (as of 2024)
        $cloudflareRanges = [
            '173.245.48.0/20',
            '103.21.244.0/22',
            '103.22.200.0/22',
            '103.31.4.0/22',
            '141.101.64.0/18',
            '108.162.192.0/18',
            '190.93.240.0/20',
            '188.114.96.0/20',
            '197.234.240.0/22',
            '198.41.128.0/17',
            '162.158.0.0/15',
            '104.16.0.0/13',
            '104.24.0.0/14',
            '172.64.0.0/13',
            '131.0.72.0/22',
        ];

        foreach ($cloudflareRanges as $range) {
            if ($this->ipMatchesCidr($ip, $range)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check if an IP matches a CIDR range
     *
     * @param string $ip The IP to check
     * @param string $cidr The CIDR range (e.g., "192.168.1.0/24")
     * @return bool Whether the IP matches the CIDR range
     */
    private function ipMatchesCidr(string $ip, string $cidr): bool
    {
        [$subnet, $bits] = explode('/', $cidr);

        if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ||
            !filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            return false;
        }

        $ipLong = ip2long($ip);
        $subnetLong = ip2long($subnet);
        $mask = -1 << (32 - (int) $bits);

        return ($ipLong & $mask) === ($subnetLong & $mask);
    }

    /**
     * Register REST API routes
     */
    public function registerRoutes(): void
    {
        // Validate license endpoint (public)
        register_rest_route(self::NAMESPACE, '/validate', [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => [$this, 'validateLicense'],
            'permission_callback' => '__return_true',
            'args' => [
                'license_key' => [
                    'required' => true,
                    'type' => 'string',
                    'sanitize_callback' => 'sanitize_text_field',
                    'validate_callback' => function ($value): bool {
                        $len = strlen($value);
                        return !empty($value) && $len >= 8 && $len <= 64;
                    },
                ],
                'domain' => [
                    'required' => true,
                    'type' => 'string',
                    'sanitize_callback' => 'sanitize_text_field',
                    'validate_callback' => function ($value): bool {
                        return !empty($value) && strlen($value) <= 255;
                    },
                ],
            ],
        ]);

        // Check license status endpoint (public)
        register_rest_route(self::NAMESPACE, '/status', [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => [$this, 'checkStatus'],
            'permission_callback' => '__return_true',
            'args' => [
                'license_key' => [
                    'required' => true,
                    'type' => 'string',
                    'sanitize_callback' => 'sanitize_text_field',
                    'validate_callback' => function ($value): bool {
                        $len = strlen($value);
                        return !empty($value) && $len >= 8 && $len <= 64;
                    },
                ],
            ],
        ]);

        // Activate license on domain endpoint (public)
        register_rest_route(self::NAMESPACE, '/activate', [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => [$this, 'activateLicense'],
            'permission_callback' => '__return_true',
            'args' => [
                'license_key' => [
                    'required' => true,
                    'type' => 'string',
                    'sanitize_callback' => 'sanitize_text_field',
                    'validate_callback' => function ($value): bool {
                        $len = strlen($value);
                        return !empty($value) && $len >= 8 && $len <= 64;
                    },
                ],
                'domain' => [
                    'required' => true,
                    'type' => 'string',
                    'sanitize_callback' => 'sanitize_text_field',
                ],
            ],
        ]);
    }

    /**
     * Validate license endpoint
     */
    public function validateLicense(WP_REST_Request $request): WP_REST_Response
    {
        $rateLimitResponse = $this->checkRateLimit();
        if ($rateLimitResponse !== null) {
            return $rateLimitResponse;
        }

        $licenseKey = $request->get_param('license_key');
        $domain = $request->get_param('domain');

        $result = $this->licenseManager->validateLicense($licenseKey, $domain);

        $statusCode = $this->getStatusCodeForResult($result);

        return new WP_REST_Response($result, $statusCode);
    }

    /**
     * Get HTTP status code based on validation result
     *
     * @param array $result The validation result
     * @return int HTTP status code
     */
    private function getStatusCodeForResult(array $result): int
    {
        if ($result['valid']) {
            return 200;
        }

        $error = $result['error'] ?? '';

        return match ($error) {
            'license_not_found' => 404,
            'activation_failed' => 500,
            default => 403,
        };
    }

    /**
     * Check license status endpoint
     */
    public function checkStatus(WP_REST_Request $request): WP_REST_Response
    {
        $rateLimitResponse = $this->checkRateLimit();
        if ($rateLimitResponse !== null) {
            return $rateLimitResponse;
        }

        $licenseKey = $request->get_param('license_key');
        $license = $this->licenseManager->getLicenseByKey($licenseKey);

        if (!$license) {
            return new WP_REST_Response([
                'valid' => false,
                'error' => 'license_not_found',
                'message' => __('License key not found.', 'wc-licensed-product'),
            ], 404);
        }

        return new WP_REST_Response([
            'valid' => $license->isValid(),
            'status' => $license->getStatus(),
            'domain' => $license->getDomain(),
            'expires_at' => $license->getExpiresAt()?->format('Y-m-d'),
            'activations_count' => $license->getActivationsCount(),
            'max_activations' => $license->getMaxActivations(),
        ]);
    }

    /**
     * Activate license on domain endpoint
     */
    public function activateLicense(WP_REST_Request $request): WP_REST_Response
    {
        $rateLimitResponse = $this->checkRateLimit();
        if ($rateLimitResponse !== null) {
            return $rateLimitResponse;
        }

        $licenseKey = $request->get_param('license_key');
        $domain = $request->get_param('domain');

        $license = $this->licenseManager->getLicenseByKey($licenseKey);

        if (!$license) {
            return new WP_REST_Response([
                'success' => false,
                'error' => 'license_not_found',
                'message' => __('License key not found.', 'wc-licensed-product'),
            ], 404);
        }

        if (!$license->isValid()) {
            return new WP_REST_Response([
                'success' => false,
                'error' => 'license_invalid',
                'message' => __('This license is not valid.', 'wc-licensed-product'),
            ], 403);
        }

        $normalizedDomain = $this->licenseManager->normalizeDomain($domain);

        // Check if already activated on this domain
        if ($license->getDomain() === $normalizedDomain) {
            return new WP_REST_Response([
                'success' => true,
                'message' => __('License is already activated for this domain.', 'wc-licensed-product'),
            ]);
        }

        // Check if can activate on another domain
        if (!$license->canActivate()) {
            return new WP_REST_Response([
                'success' => false,
                'error' => 'max_activations_reached',
                'message' => __('Maximum number of activations reached.', 'wc-licensed-product'),
            ], 403);
        }

        // Update domain (in this simple implementation, we replace the domain)
        $success = $this->licenseManager->updateLicenseDomain($license->getId(), $domain);

        if (!$success) {
            return new WP_REST_Response([
                'success' => false,
                'error' => 'activation_failed',
                'message' => __('Failed to activate license.', 'wc-licensed-product'),
            ], 500);
        }

        return new WP_REST_Response([
            'success' => true,
            'message' => __('License activated successfully.', 'wc-licensed-product'),
        ]);
    }
}
