<?php
/**
 * Permission Manager
 *
 * PHP mirror of the TypeScript permission system.
 * Main entry point for permission checks.
 *
 * @package FleekDash\Core
 * @since 2.3.0
 */

namespace FleekDash\Core;

defined('ABSPATH') || exit;

class PermissionFeatureResolver {
    /**
     * Legacy -> canonical feature keys (Supabase `feature_key`, kebab-case).
     * Keep in sync with src/__platform__/auth/feature-access/utils/feature-key-aliases.ts.
     */
    private const FEATURE_KEY_ALIASES = [
        'theme_management' => 'theme-manager',
        'theme_editor' => 'theme-manager',
        'theme_live_editor' => 'theme-manager',
        'theme_manager' => 'theme-manager',
        'menu_management' => 'menu-builder',
        'menu_builder' => 'menu-builder',
        'ai_assistant' => 'ai-assistant',
        'ai_image_generation' => 'ai-assistant',
        'integration_openai' => 'ai-assistant',
        'integration_analytics' => 'growth-tracker',
        'advanced_analytics' => 'growth-tracker',
        'growth_tracker' => 'growth-tracker',
        'role_management' => 'role-management',
        'user_management' => 'user-management',
        'content_views' => 'content-views',
        'content_table' => 'content-table',
        'content_calendar' => 'content-calendar',
        'content_kanban' => 'content-kanban',
        'unified_dashboard' => 'unified-dashboard',
        'universal_search' => 'universal-search',
        'custom_login' => 'custom-login',
        'window_manager' => 'window-manager',
        'language_management' => 'language-management',
        'debug_tools' => 'debug-tools',
        'performance_tools' => 'performance-tools',
    ];

    /**
     * Normalize legacy feature keys to canonical keys.
     *
     * @param string $feature_key Feature key (legacy or canonical)
     * @return string Canonical feature key
     */
    public static function normalizeFeatureKey(string $feature_key): string {
        $normalized = self::FEATURE_KEY_ALIASES[$feature_key] ?? $feature_key;
        return is_string($normalized) ? $normalized : $feature_key;
    }

    /**
     * Public entry point to check if current user has a feature.
     *
     * @param string $feature_key Feature key
     * @param string $app_name    Registered app (default fleekdash)
     * @return bool
     */
    public static function hasFeature(string $feature_key, string $app_name = 'fleekdash'): bool {
        return self::checkFeature($feature_key, $app_name);
    }

    /**
     * Check feature (plan/license + Labs), same source as GET /features/user.
     *
     * @param string $feature_key Feature key
     * @param string $app_name    Registered app
     * @return bool Whether user has feature
     */
    private static function checkFeature(string $feature_key, string $app_name): bool {
        $normalized_feature_key = self::normalizeFeatureKey($feature_key);
        $license_status = \get_option('fleekdash_v2_license_status', 'inactive');
        $has_feature = false;
        $can_check_features = $license_status === 'active'
            && class_exists('\FleekDash\Core\FeaturesManager');

        if ($can_check_features) {
            try {
                $features_data = \FleekDash\Core\FeaturesManager::getInstance()->getCachedFeatures($app_name);
                $has_cached_features = !empty($features_data) && isset($features_data['features']);

                if ($has_cached_features) {
                    $resolved_features = self::featuresWithLabsResolved($features_data);
                    $has_feature = self::findFeatureEnabled($resolved_features, $normalized_feature_key);
                } else {
                    $has_feature = self::checkFeatureViaController($normalized_feature_key, $app_name);
                }
            } catch (\Exception $e) {
                \error_log('[PermissionManager] Feature check error: ' . $e->getMessage());
            }
        }

        return $has_feature;
    }

    /**
     * Apply Labs overrides to raw tier features (transient payload).
     *
     * Matches FeatureController::buildUserFeaturesCachedRestResponse so PermissionManager
     * and FeatureGate / REST agree on is_enabled for beta and experimental features.
     *
     * @param array<string, mixed> $cached_payload Raw transient from FeatureController
     * @return array<int|string, mixed> Features list (numeric or keyed)
     */
    private static function featuresWithLabsResolved(array $cached_payload): array {
        $features = $cached_payload['features'] ?? [];
        if (!\is_array($features)) {
            return [];
        }

        if ($features === [] || !\class_exists('\FleekDash\Services\LabsFeaturesService')) {
            return $features;
        }

        $limits = $cached_payload['limits'] ?? [];
        if (!\is_array($limits)) {
            $limits = [];
        }

        $enhanced = \FleekDash\Services\LabsFeaturesService::init()->enhanceFeaturesData([
            'features' => $features,
            'limits' => $limits,
        ]);
        $out = $enhanced['features'] ?? [];

        return \is_array($out) ? $out : $features;
    }

    /**
     * Find whether a specific feature key is enabled in a features array.
     *
     * @param array  $features    Features array from FeatureController/FeaturesManager
     * @param string $feature_key Feature key to look up
     * @return bool
     */
    private static function findFeatureEnabled(array $features, string $feature_key): bool {
        $normalized_feature_key = self::normalizeFeatureKey($feature_key);
        $lookup_keys = [$normalized_feature_key];
        foreach (self::FEATURE_KEY_ALIASES as $legacy_key => $canonical_key) {
            if ($canonical_key === $normalized_feature_key) {
                $lookup_keys[] = $legacy_key;
            }
        }

        $is_enabled = false;
        if (isset($features[0])) {
            foreach ($features as $feature) {
                if (in_array(($feature['key'] ?? ''), $lookup_keys, true)) {
                    $is_enabled = !empty($feature['is_enabled']);
                    break;
                }
            }
        } else {
            foreach ($lookup_keys as $lookup_key) {
                if (!empty($features[$lookup_key]['is_enabled'])) {
                    $is_enabled = true;
                    break;
                }
            }
        }

        return $is_enabled;
    }

    /**
     * Fallback feature check via FeatureController (full /features/user payload, Labs applied).
     *
     * @param string $feature_key Feature key
     * @param string $app_name    Registered app
     * @return bool
     */
    private static function checkFeatureViaController(string $feature_key, string $app_name): bool {
        $has_feature = false;

        if (class_exists('\FleekDash\API\FeatureController')) {
            try {
                $controller = \FleekDash\API\FeatureController::getInstance();
                $request = new \WP_REST_Request('GET', '/fleekdash/v1/features/user');
                $request->set_param('app_name', $app_name);

                $response = $controller->getUserFeatures($request);
                if (!\is_wp_error($response)) {
                    $data = $response->get_data();
                    $features = $data['features'] ?? [];
                    $has_feature = self::findFeatureEnabled($features, $feature_key);
                }
            } catch (\Exception $e) {
                \error_log('[PermissionManager] FeatureController fallback error: ' . $e->getMessage());
            }
        }

        return $has_feature;
    }
}

class PermissionManager {
    /**
     * Whether the system is initialized
     */
    private static bool $initialized = false;

    /**
     * Load-once cache: user_id => [permission_id => override row]
     * Populated on first check per user per request.
     *
     * @var array<int, array<string, mixed>>
     */
    private static array $userOverridesCache = [];

    /**
     * Cache key prefix
     */
    private const CACHE_PREFIX = 'fleekdash_permission_';

    /**
     * Cache TTL (5 minutes)
     */
    private const CACHE_TTL = 300;

    /**
     * Register a permission node (Proxy to PermissionRegistry)
     *
     * @param array $permission Permission definition
     * @return void
     */
    public static function register(array $permission): void {
        PermissionRegistry::register($permission);
    }

    /**
     * Register multiple permissions (Proxy to PermissionRegistry)
     *
     * @param array $permissions Array of permission definitions
     * @return void
     */
    public static function registerMany(array $permissions): void {
        PermissionRegistry::registerMany($permissions);
    }

    /**
     * Get permission definition by ID (Proxy to PermissionRegistry)
     *
     * @param string $id Permission ID
     * @return array|null Permission definition or null if not found
     */
    public static function get(string $id): ?array {
        return PermissionRegistry::get($id);
    }

    /**
     * Check if permission exists (Proxy to PermissionRegistry)
     *
     * @param string $id Permission ID
     * @return bool Whether permission exists
     */
    public static function has(string $id): bool {
        return PermissionRegistry::has($id);
    }

    /**
     * Check if user has permission
     *
     * @param string $permission_id Permission ID to check
     * @param int|null $user_id User ID (null = current user)
     * @return array Result with 'allowed', 'reason', and 'metadata'
     */
    public static function check(string $permission_id, ?int $user_id = null): array {
        $node = PermissionRegistry::get($permission_id);
        $user_id = $user_id ?? \get_current_user_id();
        $cache_key = self::CACHE_PREFIX . $permission_id . '_' . $user_id;
        $cached = \get_transient($cache_key);

        if ($cached !== false) {
            return $cached;
        }

        if (!$node) {
            return [
                'allowed' => false,
                'reason' => 'not_found',
                'metadata' => self::createEmptyMetadata(),
            ];
        }

        $result = self::resolvePermission($node, $user_id);
        \set_transient($cache_key, $result, self::CACHE_TTL);
        return $result;
    }

    /**
     * Resolve permission logic
     *
     * @param array $node Permission node
     * @param int $user_id User ID
     * @return array Result
     */
    private static function resolvePermission(array $node, int $user_id): array {
        $metadata = [
            'hasFeature' => empty($node['required_feature']),
            'hasCapability' => empty($node['required_capability']),
            'isAdmin' => false,
            'parentBlocked' => false,
        ];

        $allowed = true;
        $reason = 'allowed';

        // 1. Check feature
        if (!empty($node['required_feature'])) {
            $metadata['hasFeature'] = PermissionFeatureResolver::hasFeature($node['required_feature']);
            if (!$metadata['hasFeature']) {
                $allowed = false;
                $reason = 'feature_missing';
            }
        }

        // 2. Check capability
        if ($allowed && !empty($node['required_capability'])) {
            $metadata['hasCapability'] = \user_can($user_id, $node['required_capability']);
            if (!$metadata['hasCapability']) {
                $allowed = false;
                $reason = 'capability_missing';
            }
        }

        // 3. Check hierarchy
        if ($allowed && !empty($node['parent'])) {
            $parent_result = self::check($node['parent'], $user_id);
            if (!$parent_result['allowed']) {
                $allowed = false;
                $reason = 'parent_blocked';
                $metadata['parentBlocked'] = true;
            }
        }

        // 4. Check admin-only
        if ($allowed) {
            $is_admin = \user_can($user_id, 'manage_options');
            $metadata['isAdmin'] = $is_admin;
            if (!empty($node['admin_only']) && !$is_admin) {
                $allowed = false;
                $reason = 'admin_only';
            }
        }

        return ['allowed' => $allowed, 'reason' => $reason, 'metadata' => $metadata];
    }

    /**
     * Check permission with override support
     *
     * @param string $permission_id Permission ID to check
     * @param int|null $user_id User ID (null = current user)
     * @return array Result with 'allowed', 'reason', and 'metadata'
     */
    public static function checkWithOverride(string $permission_id, ?int $user_id = null): array {
        $node = PermissionRegistry::get($permission_id);
        $user_id = $user_id ?? \get_current_user_id();
        $result = null;

        if (!$node) {
            $result = [
                'allowed' => false,
                'reason' => 'not_found',
                'metadata' => self::createEmptyMetadata(),
            ];
        } else {
            self::loadUserOverrides($user_id);
            $overrides = self::$userOverridesCache[$user_id] ?? [];
            $override = $overrides[$permission_id] ?? null;
            $parent_override = !empty($node['parent']) ? ($overrides[$node['parent']] ?? null) : null;

            if ($override) {
                $result = self::applyOverride($permission_id, $user_id, $override, false);
            } elseif ($parent_override) {
                $result = self::applyOverride($permission_id, $user_id, $parent_override, true);
            } else {
                $result = self::check($permission_id, $user_id);
            }
        }

        return $result;
    }

    /**
     * Load all overrides for a user (load-once per request)
     *
     * @param int $user_id User ID
     * @return void
     */
    private static function loadUserOverrides(int $user_id): void {
        if (isset(self::$userOverridesCache[$user_id])) {
            return;
        }

        if (!class_exists('\FleekDash\Core\PermissionOverrideManager')) {
            self::$userOverridesCache[$user_id] = [];
            return;
        }

        self::$userOverridesCache[$user_id] = PermissionOverrideManager::getActiveForUser($user_id);
    }

    /**
     * Clear user overrides cache (for invalidation on create/update/delete)
     *
     * @param int $user_id User ID
     * @return void
     */
    public static function clearUserOverridesCache(int $user_id): void {
        unset(self::$userOverridesCache[$user_id]);
    }

    /**
     * Apply override result (feature check always runs first - cannot bypass plan)
     *
     * @param string $permission_id Permission ID
     * @param int $user_id User ID
     * @param array $override Override row
     * @param bool $inherited Whether override is inherited from parent
     * @return array Result
     */
    private static function applyOverride(string $permission_id, int $user_id, array $override, bool $inherited = false): array {
        $node = PermissionRegistry::get($permission_id);

        $metadata = array_merge(self::createEmptyMetadata(), [
            'hasOverride' => true,
            'overrideType' => $override['override_type'] ?? null,
            'overrideReason' => $override['reason'] ?? null,
            'overrideInherited' => $inherited,
        ]);

        $allowed = true;
        $reason = 'allowed';

        if ($node && !empty($node['required_feature'])) {
            $has_feature = PermissionFeatureResolver::hasFeature($node['required_feature']);
            $metadata['hasFeature'] = $has_feature;

            if (!$has_feature) {
                $metadata['overrideBlockedByFeature'] = true;
                $allowed = false;
                $reason = 'feature_missing';
            }
        }

        if ($allowed) {
            $is_allow = in_array($override['override_type'] ?? '', ['ALLOW', 'TEMP_ALLOW'], true);
            $allowed = $is_allow;
            $reason = $is_allow ? 'allowed' : 'override_denied';

            $metadata['hasCapability'] = null;
            if ($node && !empty($node['required_capability'])) {
                $metadata['hasCapability'] = \user_can($user_id, $node['required_capability']);
            }
        }

        return [
            'allowed' => $allowed,
            'reason' => $reason,
            'metadata' => $metadata,
        ];
    }

    /**
     * Check if user has feature (plan/license) - public for API/override conflict checks
     *
     * @param string $feature_key Feature key
     * @return bool Whether user has feature
     */
    public static function hasFeature(string $feature_key): bool {
        return PermissionFeatureResolver::hasFeature($feature_key);
    }

    /**
     * Collect permission IDs to inject in SSR.
     *
     * We inject all registered permissions (roots + children) so hooks using
     * granular IDs like "user_management.view" resolve synchronously on first
     * render and do not fallback to async checks.
     *
     * @return array<int, string>
     */
    private static function getSSRPermissionIds(): array {
        $permission_ids = [];
        $registry = PermissionRegistry::getAll();

        foreach ($registry as $permission_id => $_definition) {
            if (is_string($permission_id) && $permission_id !== '') {
                $permission_ids[] = $permission_id;
            }
        }

        return $permission_ids;
    }

    /**
     * Inject SSR permission data.
     *
     * Injects pre-resolved permission results into window.FleekDash.permissions
     * so the frontend cache is warm on first render — eliminating loading states
     * and flash-of-permitted-content for the most common permissions.
     *
     * Hook: admin_enqueue_scripts priority 6.
     * FeaturesManager::inject_features() runs at priority 5 and populates the
     * features transient via get_user_features(). By running at priority 6,
     * checkFeature() finds a warm transient and avoids an extra HTTP request
     * to Supabase. Previously this ran on admin_init where the transient was
     * always empty, causing checkFeature() to return false for every permission.
     *
     * @param string $hook_suffix Current admin screen basename (e.g. index.php) — passed by WordPress to admin_enqueue_scripts.
     * @return void
     */
    public static function injectSSRData(string $hook_suffix = ''): void {
        \assert(\is_string($hook_suffix));

        $user_id = \get_current_user_id();

        if (!$user_id) {
            return;
        }

        $permissions = [];
        foreach (self::getSSRPermissionIds() as $permission_id) {
            if (!PermissionRegistry::has($permission_id)) {
                continue;
            }
            $result = self::checkWithOverride($permission_id, $user_id);
            $permissions[$permission_id] = [
                'allowed'  => $result['allowed'],
                'reason'   => $result['reason'],
                'metadata' => $result['metadata'],
            ];
        }

        // window.FleekDash is set by a classic <script> in admin_head (priority 0).
        // Running here at admin_print_footer_scripts priority 1 guarantees the
        // object exists before we append .permissions.
        \add_action('admin_print_footer_scripts', function() use ($permissions) {
            echo '<script type="text/javascript">';
            echo 'window.FleekDash = window.FleekDash || {};';
            echo 'window.FleekDash.permissions = ' . \wp_json_encode($permissions) . ';';
            echo '</script>';
        }, 1);
    }

    /**
     * Clear permission cache
     *
     * @param string|null $permission_id Specific permission (null = all)
     * @param int|null $user_id Specific user (null = all)
     * @return void
     */
    public static function clearCache(?string $permission_id = null, ?int $user_id = null): void {
        if ($user_id) {
            self::clearUserOverridesCache($user_id);
        }

        if ($permission_id && $user_id) {
            $cache_key = self::CACHE_PREFIX . $permission_id . '_' . $user_id;
            \delete_transient($cache_key);
        } else {
            global $wpdb;
            $wpdb->query(
                $wpdb->prepare(
                    "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
                    '_transient_' . self::CACHE_PREFIX . '%'
                )
            );

            if (!$user_id) {
                self::$userOverridesCache = [];
            }
        }
    }

    /**
     * Create empty metadata (public for use by PermissionMiddleware).
     *
     * @return array Empty metadata
     */
    public static function createEmptyMetadata(): array {
        return [
            'hasFeature' => false,
            'hasCapability' => false,
            'isAdmin' => false,
            'parentBlocked' => false,
        ];
    }

    /**
     * Mark as initialized
     *
     * @return void
     */
    public static function markInitialized(): void {
        self::$initialized = true;
    }

    /**
     * Check if initialized
     *
     * @return bool Whether initialized
     */
    public static function isInitialized(): bool {
        return self::$initialized;
    }

    /**
     * Clear all permissions (for testing)
     *
     * @return void
     */
    public static function clear(): void {
        PermissionRegistry::clear();
        self::$initialized = false;
    }
}
