<?php
/**
 * Manifest Validator
 *
 * Validates and sanitizes integration manifests to prevent XSS and security issues.
 *
 * @package FleekDash\Integration
 * @since 2.3.0
 *
 * SECURITY FIX #1: XSS Prevention
 * - Sanitizes all user-provided fields
 * - Whitelists allowed icons
 * - Validates URLs and paths
 * - Prevents code injection
 */

namespace FleekDash\Integration;

defined('ABSPATH') || exit;

class ManifestValidator
{
    /**
     * Allowed icon names (whitelist to prevent XSS)
     *
     * @var array
     */
    const ALLOWED_ICONS = [
        // Common icons
        'sparkles', 'message-square', 'settings', 'dashboard', 'users',
        'file', 'folder', 'mail', 'bell', 'heart', 'star', 'search',
        'plus', 'edit', 'trash', 'check', 'x', 'menu', 'more-vertical',

        // Business icons
        'briefcase', 'shopping-cart', 'credit-card', 'dollar-sign',
        'trending-up', 'trending-down', 'activity', 'bar-chart',

        // Communication
        'phone', 'video', 'mic', 'send', 'inbox', 'archive',

        // Tools
        'tool', 'wrench', 'code', 'terminal', 'database', 'server',
        'cloud', 'download', 'upload', 'link', 'external-link',

        // Default fallback
        'box', 'package', 'layers',
    ];

    /**
     * Allowed integration types
     *
     * @var array
     */
    const ALLOWED_TYPES = ['native', 'standalone', 'hybrid', 'internal', 'mu-plugin', 'premium-catalog'];

    /**
     * Allowed sandbox modes
     *
     * @var array
     */
    const ALLOWED_SANDBOX_MODES = ['strict', 'moderate', 'none'];

    /**
     * Validate manifest
     *
     * @param array $manifest Raw manifest data
     * @return array Validated manifest or WP_Error
     */
    public static function validate(array $manifest): array // NOSONAR php:S3776 - Central manifest sanitizer validates all fields in one pass.
    {
        $errors = [];

        // ===============================================================
        // REQUIRED FIELDS VALIDATION
        // ===============================================================

        $required_fields = [
            'integration_id',
            'name',
            'version',
            'integration_type',
            'plugin_file',
        ];

        foreach ($required_fields as $field) {
            if (empty($manifest[$field])) {
                $errors[] = sprintf('Missing required field: %s', $field);
            }
        }

        if (!empty($errors)) {
            return [
                'success' => false,
                'errors' => $errors,
            ];
        }

        // ===============================================================
        // SANITIZATION (XSS Prevention)
        // ===============================================================

        $validated = [];

        // Integration ID (alphanumeric + hyphens only)
        $validated['integration_id'] = self::sanitizeIntegrationId($manifest['integration_id']);
        if ($validated['integration_id'] !== $manifest['integration_id']) {
            $errors[] = 'Integration ID contains invalid characters. Only alphanumeric and hyphens allowed.';
        }

        // Name (sanitize HTML, max 100 chars)
        $validated['name'] = \sanitize_text_field($manifest['name']);
        if (strlen($validated['name']) > 100) {
            $errors[] = 'Name must be 100 characters or less.';
        }

        // Description (sanitize HTML, allow limited tags)
        $validated['description'] = isset($manifest['description'])
            ? wp_kses_post($manifest['description'])
            : '';

        // Version (semver format)
        $validated['version'] = self::sanitizeVersion($manifest['version']);
        if (!$validated['version']) {
            $errors[] = 'Invalid version format. Use semantic versioning (e.g., 1.0.0).';
        }

        // Author (sanitize HTML)
        $validated['author'] = isset($manifest['author'])
            ? \sanitize_text_field($manifest['author'])
            : '';

        // Author URI (validate URL)
        if (!empty($manifest['author_uri'])) {
            $validated['author_uri'] = esc_url_raw($manifest['author_uri']);
            if (empty($validated['author_uri'])) {
                $errors[] = 'Invalid author URI.';
            }
        }

        // Integration Type (enum validation)
        $validated['integration_type'] = strtolower($manifest['integration_type']);
        if (!in_array($validated['integration_type'], self::ALLOWED_TYPES, true)) {
            $errors[] = sprintf(
                'Invalid integration type. Allowed: %s',
                implode(', ', self::ALLOWED_TYPES)
            );
        }

        // Plugin File (validate path)
        $validated['plugin_file'] = self::sanitizePluginFile($manifest['plugin_file']);
        if (!$validated['plugin_file']) {
            $errors[] = 'Invalid plugin file path.';
        }

        // Icon (whitelist validation - SECURITY CRITICAL)
        if (!empty($manifest['icon'])) {
            $validated['icon'] = self::validateIcon($manifest['icon']);
            if (!$validated['icon']) {
                $errors[] = sprintf(
                    'Invalid icon. Allowed icons: %s',
                    implode(', ', array_slice(self::ALLOWED_ICONS, 0, 10)) . '...'
                );
            }
        } else {
            $validated['icon'] = 'package'; // default
        }

        // ===============================================================
        // ROUTES VALIDATION (if present)
        // ===============================================================

        if (!empty($manifest['routes'])) {
            $validated['routes'] = [];
            foreach ($manifest['routes'] as $route) {
                $validated_route = self::validateRoute($route);
                if (isset($validated_route['errors'])) {
                    $errors = array_merge($errors, $validated_route['errors']);
                } else {
                    $validated['routes'][] = $validated_route;
                }
            }
        }

        // ===============================================================
        // SETTINGS SCHEMA VALIDATION (if present)
        // ===============================================================

        if (!empty($manifest['settings_schema'])) {
            $validated['settings_schema'] = self::validateSettingsSchema($manifest['settings_schema']);
        }

        $validated['category'] = isset($manifest['category'])
            ? \sanitize_key((string) $manifest['category'])
            : 'plugin';
        $validated['source'] = isset($manifest['source'])
            ? \sanitize_key((string) $manifest['source'])
            : 'external';
        $validated['source_label'] = isset($manifest['source_label'])
            ? \sanitize_text_field((string) $manifest['source_label'])
            : '';
        $validated['allowed_roles'] = self::sanitizeRoles($manifest['allowed_roles'] ?? null);
        $validated['feature_key'] = isset($manifest['feature_key'])
            ? \sanitize_text_field((string) $manifest['feature_key'])
            : null;
        $validated['license_required'] = !empty($manifest['license_required']);
        $validated['required_tier'] = isset($manifest['required_tier'])
            ? \sanitize_text_field((string) $manifest['required_tier'])
            : null;

        foreach (['mode', 'preview_link', 'roadmap_link', 'upgrade_message', 'upgrade_email', 'upgrade_subject'] as $field) {
            if (isset($manifest[$field])) {
                $validated[$field] = is_string($manifest[$field])
                    ? \sanitize_text_field($manifest[$field])
                    : $manifest[$field];
            }
        }

        if (isset($manifest['tags']) && is_array($manifest['tags'])) {
            $validated['tags'] = array_map('sanitize_text_field', $manifest['tags']);
        }

        // ===============================================================
        // RETURN RESULT
        // ===============================================================

        if (!empty($errors)) {
            return [
                'success' => false,
                'errors' => $errors,
            ];
        }

        return [
            'success' => true,
            'manifest' => $validated,
            'hash' => self::generateManifestHash($validated),
        ];
    }

    /**
     * Sanitize integration ID
     *
     * @param string $id Raw ID
     * @return string Sanitized ID
     */
    public static function sanitizeIntegrationId(string $id): string
    {
        // Allow only: lowercase alphanumeric + hyphens
        $id = strtolower($id);
        $id = preg_replace('/[^a-z0-9\-]/', '', $id);
        return substr($id, 0, 100);
    }

    /**
     * Sanitize version
     *
     * @param string $version Raw version
     * @return string|false Sanitized version or false if invalid
     */
    private static function sanitizeVersion(string $version)
    {
        // Validate semver format (e.g., 1.0.0, 2.1.3-beta)
        if (preg_match('/^(\d+)\.(\d+)\.(\d+)(-[a-zA-Z0-9\-]+)?$/', $version)) {
            return $version;
        }
        return false;
    }

    /**
     * Sanitize plugin file
     *
     * @param string $file Raw file path
     * @return string|false Sanitized path or false if invalid
     */
    private static function sanitizePluginFile(string $file)
    {
        // Validate plugin file path format
        if (preg_match('/^[a-zA-Z0-9\-_\/]+\.php$/', $file)) {
            return \sanitize_text_field($file);
        }
        return false;
    }

    /**
     * Validate icon (whitelist check)
     *
     * @param string $icon Raw icon name
     * @return string|false Valid icon or false
     */
    private static function validateIcon(string $icon)
    {
        $icon = strtolower(\sanitize_text_field($icon));
        return in_array($icon, self::ALLOWED_ICONS, true) ? $icon : false;
    }

    /**
     * Validate route
     *
     * @param array $route Raw route data
     * @return array Validated route or errors
     */
    private static function validateRoute(array $route): array // NOSONAR php:S3776 - Route validation handles URL, permissions, and sandbox fields.
    {
        $errors = [];
        $validated = [];

        // Path (alphanumeric + hyphens + slashes)
        if (empty($route['path'])) {
            $errors[] = 'Route path is required.';
        } else {
            $validated['path'] = preg_replace('/[^a-z0-9\-\/]/', '', strtolower($route['path']));
        }

        // Title (sanitize HTML)
        if (empty($route['title'])) {
            $errors[] = 'Route title is required.';
        } else {
            $validated['title'] = \sanitize_text_field($route['title']);
        }

        // Component URL (validate URL)
        if (empty($route['component_url'])) {
            $errors[] = 'Component URL is required.';
        } else {
            // Allow both absolute and relative URLs
            $url = $route['component_url'];
            if (strpos($url, '/') === 0) {
                // Relative URL - validate path
                $validated['component_url'] = \sanitize_text_field($url);
            } else {
                // Absolute URL - validate
                $validated['component_url'] = esc_url_raw($url);
                if (empty($validated['component_url'])) {
                    $errors[] = 'Invalid component URL.';
                }
            }
        }

        // Icon (optional, whitelist)
        if (!empty($route['icon'])) {
            $validated['icon'] = self::validateIcon($route['icon']);
        }

        // Permissions (array of strings)
        if (!empty($route['permissions'])) {
            $validated['permissions'] = array_map('sanitize_text_field', (array) $route['permissions']);
        }

        // Sandbox mode (enum)
        if (!empty($route['sandbox_mode'])) {
            $mode = strtolower($route['sandbox_mode']);
            if (in_array($mode, self::ALLOWED_SANDBOX_MODES, true)) {
                $validated['sandbox_mode'] = $mode;
            }
        }

        if (!empty($errors)) {
            return ['errors' => $errors];
        }

        return $validated;
    }

    /**
     * Validate settings schema
     *
     * @param array $schema Raw schema
     * @return array Validated schema
     */
    private static function validateSettingsSchema(array $schema): array // NOSONAR php:S3776 - Schema validation normalizes supported field metadata.
    {
        $validated = [];

        foreach ($schema as $key => $setting) {
            // Sanitize key
            $clean_key = \sanitize_key($key);

            $validated[$clean_key] = [
                'type' => isset($setting['type']) ? \sanitize_text_field($setting['type']) : 'string',
                'label' => isset($setting['label']) ? \sanitize_text_field($setting['label']) : $clean_key,
                'description' => isset($setting['description']) ? \sanitize_text_field($setting['description']) : '',
                'required' => isset($setting['required']) && $setting['required'],
                'encrypted' => isset($setting['encrypted']) && $setting['encrypted'],
            ];

            if (isset($setting['options']) && is_array($setting['options'])) {
                $validated[$clean_key]['options'] = array_map('sanitize_text_field', $setting['options']);
            }

            foreach (['min', 'max', 'step'] as $numeric_key) {
                if (isset($setting[$numeric_key])) {
                    $validated[$clean_key][$numeric_key] = is_numeric($setting[$numeric_key])
                        ? (float) $setting[$numeric_key]
                        : $setting[$numeric_key];
                }
            }

            // Default value (type-specific sanitization)
            if (isset($setting['default'])) {
                $validated[$clean_key]['default'] = self::sanitizeSettingValue(
                    $setting['default'],
                    $validated[$clean_key]['type']
                );
            }
        }

        return $validated;
    }

    /**
     * Sanitize setting value based on type
     *
     * @param mixed $value Raw value
     * @param string $type Value type
     * @return mixed Sanitized value
     */
    public static function sanitizeSettingValue($value, string $type) // NOSONAR php:S1142 - Type sanitizer returns the sanitized value per supported type.
    {
        switch ($type) {
            case 'number':
            case 'integer':
                return intval($value);

            case 'boolean':
                return (bool) $value;

            case 'email':
                return \sanitize_email($value);

            case 'url':
                return esc_url_raw($value);

            case 'password':
                // Don't sanitize passwords (they can have special chars)
                return $value;

            case 'json':
                return is_string($value) ? $value : \wp_json_encode($value);

            case 'text':
            case 'textarea':
                return sanitize_textarea_field($value);

            case 'string':
            default:
                return \sanitize_text_field($value);
        }
    }

    /**
     * @param mixed $roles
     * @return array<int, string>|null
     */
    private static function sanitizeRoles($roles): ?array
    {
        if (is_string($roles)) {
            $decoded = json_decode($roles, true);
            $roles = is_array($decoded) ? $decoded : array_filter(array_map('trim', explode(',', $roles)));
        }

        if (!is_array($roles) || empty($roles)) {
            return null;
        }

        return array_values(array_filter(array_map('sanitize_key', $roles)));
    }

    /**
     * Generate manifest hash
     *
     * @param array $manifest Validated manifest
     * @return string SHA256 hash
     */
    private static function generateManifestHash(array $manifest): string
    {
        return hash('sha256', \wp_json_encode($manifest));
    }

    /**
     * Check if icon is allowed
     *
     * @param string $icon Icon name
     * @return bool True if allowed
     */
    public static function isIconAllowed(string $icon): bool
    {
        return in_array(strtolower($icon), self::ALLOWED_ICONS, true);
    }

    /**
     * Get allowed icons list
     *
     * @return array List of allowed icons
     */
    public static function getAllowedIcons(): array
    {
        return self::ALLOWED_ICONS;
    }
}
