<?php
/**
 * 👥 ROLES CONTROLLER - V2.2 PURE
 *
 * @package FleekDash\Controllers
 * @since 2.2.0
 */

namespace FleekDash\Controllers;

use FleekDash\Services\ResponseService;
use WP_REST_Request;

class RolesController
{
    private const DEFAULT_ROLES = [
        'administrator',
        'editor',
        'author',
        'contributor',
        'subscriber',
    ];

    private const CRITICAL_CAPABILITIES = [
        'administrator' => [
            'manage_options',
            'edit_users',
            'manage_categories',
            'activate_plugins',
            'install_plugins',
            'edit_plugins',
            'switch_themes',
            'edit_themes',
            'update_plugins',
            'delete_plugins',
            'update_themes',
            'install_themes',
            'delete_themes',
            'edit_theme_options',
            'create_users',
            'delete_users',
            'promote_users',
            'list_users',
            'remove_users',
        ],
        'editor' => [
            'moderate_comments',
            'manage_categories',
            'manage_links',
        ],
        'author' => [
            'upload_files',
            'edit_posts',
            'publish_posts',
        ],
    ];

    private const USER_COUNTS_CACHE_KEY = 'fleekdash_roles_user_counts';
    private const USER_COUNTS_CACHE_TTL = 300;

    private function normalizeCapabilities($capabilities): array
    {
        $normalized = [];
        if (empty($capabilities)) {
            return $normalized;
        }

        if (is_array($capabilities)) {
            $this->appendArrayCapabilities($capabilities, $normalized);
        } elseif (is_string($capabilities)) {
            $this->appendStringCapability($capabilities, $normalized);
        }

        return $normalized;
    }

    private function appendArrayCapabilities(array $capabilities, array &$normalized): void
    {
        foreach ($capabilities as $key => $value) {
            if (is_string($key)) {
                $capability = \sanitize_key($key);
                if ($capability !== '') {
                    $normalized[$capability] = is_bool($value) ? $value : (bool) $value;
                }
                continue;
            }

            if (is_string($value)) {
                $this->appendStringCapability($value, $normalized);
            }
        }
    }

    private function appendStringCapability(string $rawCapability, array &$normalized): void
    {
        $capability = \sanitize_key($rawCapability);
        if ($capability !== '') {
            $normalized[$capability] = true;
        }
    }

    private function getCriticalCapabilities(string $roleId): array
    {
        return self::CRITICAL_CAPABILITIES[$roleId] ?? [];
    }

    private function wantsUserCountsFromRequest(WP_REST_Request $request): bool
    {
        $value = $request->get_param('with_user_counts');
        return $value === '1' || $value === 1 || $value === true;
    }

    private function getRoleUserCounts(): array
    {
        $result = [];
        if (\current_user_can('list_users')) {
            $cached = \get_transient(self::USER_COUNTS_CACHE_KEY);
            if (is_array($cached)) {
                $result = $cached;
            } elseif (\function_exists('count_users')) {
                $counts = \count_users();
                $result = $counts['avail_roles'] ?? [];
                \set_transient(self::USER_COUNTS_CACHE_KEY, $result, self::USER_COUNTS_CACHE_TTL);
            }
        }
        return $result;
    }

    private function formatRole(
        string $roleId,
        array $roleData,
        array $roleUserCounts = [],
        bool $includeUserCount = true
    ): array {
        $rawCapabilities = $roleData['capabilities'] ?? [];
        $capabilities = [];
        if (is_array($rawCapabilities)) {
            $capabilities = array_values(
                array_unique(
                    array_filter(
                        array_map(
                            static fn ($capability, $granted) => $granted ? \sanitize_key($capability) : null,
                            array_keys($rawCapabilities),
                            $rawCapabilities
                        )
                    )
                )
            );
        }

        $result = [
            'id' => $roleId,
            'name' => $roleData['name'] ?? $roleId,
            'display_name' => $roleData['name'] ?? $roleId,
            'capabilities' => $capabilities,
            'is_default' => in_array($roleId, self::DEFAULT_ROLES, true),
        ];

        if ($includeUserCount) {
            $result['user_count'] = isset($roleUserCounts[$roleId]) ? (int) $roleUserCounts[$roleId] : 0;
        }

        return $result;
    }

    private function getFormattedRolesForResponse(WP_REST_Request $request): array
    {
        if (!function_exists('get_editable_roles')) {
            require_once ABSPATH . 'wp-admin/includes/user.php';
        }

        $wpRoles = get_editable_roles();
        if (!is_array($wpRoles)) {
            return [];
        }

        $wantsCounts = $this->wantsUserCountsFromRequest($request);
        $includeUserCount = $wantsCounts && \current_user_can('list_users');
        $roleUserCounts = $includeUserCount ? $this->getRoleUserCounts() : [];

        $roles = [];
        foreach ($wpRoles as $roleId => $roleData) {
            if (is_array($roleData)) {
                $roles[] = $this->formatRole($roleId, $roleData, $roleUserCounts, $includeUserCount);
            }
        }

        return $roles;
    }

    private function prepareCreateRolePayload(WP_REST_Request $request): array
    {
        $body = $request->get_json_params();
        $rawRoleId = $body['id'] ?? null;
        $roleId = \sanitize_key((string) $rawRoleId);
        $displayName = $body['display_name'] ?? null;
        $finalDisplayName = $displayName
            ? \sanitize_text_field($displayName)
            : ucwords(str_replace('_', ' ', $roleId));

        return [
            'rawRoleId' => $rawRoleId,
            'roleId' => $roleId,
            'displayName' => $finalDisplayName,
            'normalizedCapabilities' => $this->normalizeCapabilities($body['capabilities'] ?? []),
        ];
    }

    private function buildCreateRoleFailureResponse(array $payload)
    {
        $roleId = $payload['roleId'];
        $rawRoleId = $payload['rawRoleId'];
        $status = 500;
        $message = 'Failed to create role';

        if (empty($rawRoleId)) {
            $status = 400;
            $message = 'Role ID is required';
        } elseif (empty($roleId)) {
            $status = 400;
            $message = 'Invalid role ID';
        } elseif (get_role($roleId)) {
            $status = 409;
            $message = 'Role already exists';
        }

        return ResponseService::error($message, null, $status);
    }

    private function canRenameRole(string $roleId, ?string $displayName): bool
    {
        if ($displayName === null) {
            return true;
        }
        if ($displayName === '') {
            return false;
        }
        return !in_array($roleId, self::DEFAULT_ROLES, true);
    }

    private function applyRoleDisplayName(\WP_Roles $wpRoles, string $roleId, ?string $displayName): void
    {
        if ($displayName !== null) {
            $wpRoles->roles[$roleId]['name'] = $displayName;
        }
    }

    private function syncRoleCapabilities(\WP_Role $role, array $normalizedCapabilities): void
    {
        foreach (array_keys($role->capabilities) as $capability) {
            $capabilityKey = \sanitize_key($capability);
            $shouldHaveCap = $normalizedCapabilities[$capabilityKey] ?? false;
            if (!$shouldHaveCap) {
                $role->remove_cap($capabilityKey);
            }
        }

        foreach ($normalizedCapabilities as $capability => $grant) {
            if ($grant) {
                $role->add_cap($capability, true);
            } else {
                $role->remove_cap($capability);
            }
        }
    }

    private function applyRoleCapabilities(\WP_Role $role, string $roleId, $capabilities): array
    {
        $updatedCapabilities = $role->capabilities;
        if ($capabilities === null) {
            return $updatedCapabilities;
        }

        $normalizedCapabilities = $this->normalizeCapabilities($capabilities);
        if (in_array($roleId, self::DEFAULT_ROLES, true)) {
            foreach ($this->getCriticalCapabilities($roleId) as $criticalCapability) {
                $normalizedCapabilities[$criticalCapability] = true;
            }
        }

        $this->syncRoleCapabilities($role, $normalizedCapabilities);
        return $role->capabilities;
    }

    private function updateWordPressRolesOption(\WP_Roles $wpRoles): void
    {
        \update_option($wpRoles->role_key, $wpRoles->roles);
    }

    public function getRoles(WP_REST_Request $request)
    {
        try {
            $roles = $this->getFormattedRolesForResponse($request);
            $response = ResponseService::success('Roles retrieved', $roles);
            return ResponseService::withNoCacheHeaders($response);
        } catch (\Throwable $e) {
            $message = $e->getMessage() !== '' ? $e->getMessage() : 'Unknown error';
            return ResponseService::error('Failed to get roles', ['error' => $message], 500);
        }
    }

    public function createRole(WP_REST_Request $request)
    {
        try {
            $payload = $this->prepareCreateRolePayload($request);
            $response = null;
            $canCreate = !empty($payload['rawRoleId'])
                && !empty($payload['roleId'])
                && !get_role($payload['roleId']);

            if ($canCreate) {
                $result = add_role(
                    $payload['roleId'],
                    $payload['displayName'],
                    $payload['normalizedCapabilities']
                );
                if ($result instanceof \WP_Role) {
                    $wpRoles = wp_roles();
                    if ($wpRoles instanceof \WP_Roles) {
                        $this->updateWordPressRolesOption($wpRoles);
                    }
                    \delete_transient(self::USER_COUNTS_CACHE_KEY);
                    $response = ResponseService::success('Role created', $this->formatRole($payload['roleId'], [
                        'name' => $payload['displayName'],
                        'capabilities' => $result->capabilities,
                    ]));
                }
            }

            if ($response === null) {
                $response = $this->buildCreateRoleFailureResponse($payload);
            }
            return $response;
        } catch (\Throwable $e) {
            return ResponseService::error('Failed to create role', ['error' => $e->getMessage()], 500);
        }
    }

    public function updateRole(WP_REST_Request $request)
    {
        try {
            $roleId = (string) $request->get_param('role_id');
            $role = get_role($roleId);
            $wpRoles = wp_roles();
            $body = $request->get_json_params();
            $displayName = $body['display_name'] ?? null;
            $displayName = $displayName !== null ? \sanitize_text_field($displayName) : null;
            $capabilities = $body['capabilities'] ?? null;
            $response = null;

            if (!$role instanceof \WP_Role) {
                $response = ResponseService::error('Role not found', null, 404);
            } elseif (!$wpRoles instanceof \WP_Roles) {
                $response = ResponseService::error('Failed to update role', ['error' => 'WP_Roles unavailable'], 500);
            } elseif (!$this->canRenameRole($roleId, $displayName)) {
                $isDefaultRename = $displayName !== null && in_array($roleId, self::DEFAULT_ROLES, true);
                $response = $isDefaultRename
                    ? ResponseService::error('Cannot rename default WordPress roles', null, 400)
                    : ResponseService::error('Display name cannot be empty', null, 400);
            } else {
                $this->applyRoleDisplayName($wpRoles, $roleId, $displayName);
                $updatedCapabilities = $this->applyRoleCapabilities($role, $roleId, $capabilities);
                $wpRoles->roles[$roleId]['capabilities'] = $updatedCapabilities;
                $this->updateWordPressRolesOption($wpRoles);
                \delete_transient(self::USER_COUNTS_CACHE_KEY);
                $response = ResponseService::success('Role updated', $this->formatRole($roleId, [
                    'name' => $wpRoles->roles[$roleId]['name'] ?? $roleId,
                    'capabilities' => $updatedCapabilities,
                ]));
            }

            return $response;
        } catch (\Throwable $e) {
            return ResponseService::error('Failed to update role', ['error' => $e->getMessage()], 500);
        }
    }

    public function deleteRole(WP_REST_Request $request)
    {
        try {
            $roleId = $request->get_param('role_id');
            $response = null;
            if (in_array($roleId, self::DEFAULT_ROLES, true)) {
                $response = ResponseService::error('Cannot delete default WordPress roles', null, 403);
            } else {
                remove_role($roleId);
                \delete_transient(self::USER_COUNTS_CACHE_KEY);
                $response = ResponseService::success('Role deleted');
            }

            return $response;
        } catch (\Throwable $e) {
            return ResponseService::error('Failed to delete role', ['error' => $e->getMessage()], 500);
        }
    }

    public function getCapabilities()
    {
        try {
            global $wp_roles;
            $allCaps = [];
            foreach ($wp_roles->roles as $roleData) {
                $allCaps = array_merge($allCaps, array_keys($roleData['capabilities']));
            }

            $allCaps = array_unique($allCaps);
            sort($allCaps);
            $response = ResponseService::success('Capabilities retrieved', array_values($allCaps));
            return ResponseService::withNoCacheHeaders($response);
        } catch (\Throwable $e) {
            return ResponseService::error('Failed to get capabilities', ['error' => $e->getMessage()], 500);
        }
    }
}
