<?php
declare(strict_types=1);

namespace Ekesto\CacheInvalidator\Settings;

use Ekesto\CacheInvalidator\Core\FeatureFlags;
use Ekesto\CacheInvalidator\Settings;
use Ekesto\CacheAutopilot\AccessControl;

/**
 * REST endpoints for the Cache Autopilot access-control configuration.
 *
 * All endpoints require Cache Autopilot settings access. Lite resolves this to
 * manage_options; Pro also honors deliberate role/user delegation.
 */
final class AccessControlRestController
{
    private const ROUTE_BASE = '/access-control';
    private const ROUTE_USERS = '/access-control/users';
    private const ROUTE_ROLES = '/access-control/roles';

    public function registerRestRoutes(): void
    {
        if (defined('LSCWP_V')) {
            add_filter('rest_pre_dispatch', [$this, 'preventLscCaching'], 10, 3);
        }

        $manageOnly = fn(): bool => $this->canManageAccessControl();

        register_rest_route(Settings::REST_NAMESPACE, self::ROUTE_BASE, [
            [
                'methods' => 'GET',
                'callback' => [$this, 'getConfig'],
                'permission_callback' => $manageOnly,
            ],
            [
                'methods' => 'POST',
                'callback' => [$this, 'updateConfig'],
                'permission_callback' => $manageOnly,
                'args' => [
                    'users' => [
                        'type' => 'object',
                        'required' => false,
                    ],
                    'roles' => [
                        'type' => 'object',
                        'required' => false,
                    ],
                ],
            ],
        ]);

        register_rest_route(Settings::REST_NAMESPACE, self::ROUTE_USERS, [
            'methods' => 'GET',
            'callback' => [$this, 'searchUsers'],
            'permission_callback' => $manageOnly,
            'args' => [
                'search' => [
                    'type' => 'string',
                    'required' => false,
                    'default' => '',
                    'sanitize_callback' => static fn($v): string => is_string($v) ? $v : '',
                ],
            ],
        ]);

        register_rest_route(Settings::REST_NAMESPACE, self::ROUTE_ROLES, [
            'methods' => 'GET',
            'callback' => [$this, 'listRoles'],
            'permission_callback' => $manageOnly,
        ]);
    }

    /**
     * Enforce the product access boundary for access-control assignments.
     *
     * @return bool
     */
    private function canManageAccessControl(): bool
    {
        return AccessControl::userCanManage();
    }

    public function preventLscCaching($result, $server, $request)
    {
        $route = $request->get_route();
        if (is_string($route) && str_starts_with($route, '/' . Settings::REST_NAMESPACE . self::ROUTE_BASE)) {
            // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- LiteSpeed Cache third-party control hook.
            do_action('litespeed_control_set_nocache', 'ekesto-cache-invalidator access-control REST');
        }
        return $result;
    }

    public function getConfig(\WP_REST_Request $request): \WP_REST_Response
    {
        unset($request);
        return $this->applyRestNoCache(
            new \WP_REST_Response($this->buildConfigPayload(), 200)
        );
    }

    public function updateConfig(\WP_REST_Request $request): \WP_REST_Response
    {
        // In Lite, access-control overrides are inactive (admins only). Freeze the
        // write so the stored premium configuration is preserved — ignored at
        // runtime, never overwritten or deleted. The response returns the
        // unchanged stored config.
        if (FeatureFlags::isPremium()) {
            $payload = [
                'users' => $this->coerceAssocIntKeyed($request->get_param('users')),
                'roles' => $this->coerceAssocStringKeyed($request->get_param('roles')),
            ];
            AccessControl::saveStoredConfig($payload);
        }

        return $this->applyRestNoCache(
            new \WP_REST_Response($this->buildConfigPayload(), 200)
        );
    }

    public function searchUsers(\WP_REST_Request $request): \WP_REST_Response
    {
        $search = trim((string) $request->get_param('search'));
        $roleByKey = $this->buildRoleLabelMap();
        $args = [
            'number' => 20,
            'fields' => 'all_with_meta',
            'role__not_in' => ['administrator'],
        ];
        if ($search !== '') {
            $args['search'] = '*' . $search . '*';
            $args['search_columns'] = ['user_login', 'user_nicename', 'display_name', 'user_email'];
        }

        $users = get_users($args);
        $out = [];
        foreach ($users as $user) {
            if (!($user instanceof \WP_User)) {
                continue;
            }
            $out[] = $this->buildUserPayload($user, $roleByKey);
        }
        return $this->applyRestNoCache(new \WP_REST_Response($out, 200));
    }

    public function listRoles(\WP_REST_Request $request): \WP_REST_Response
    {
        unset($request);
        return $this->applyRestNoCache(
            new \WP_REST_Response($this->describeRoles(), 200)
        );
    }

    /**
     * @return array{
     *     users: array<int,array{
     *         id:int,
     *         login:string,
     *         display_name:string,
     *         email:string,
     *         first_name:string,
     *         last_name:string,
     *         role:string,
     *         role_name:string,
     *         roles:array<int,string>,
     *         mode:string
     *     }>,
     *     roles: array<int,array{slug:string,name:string,mode:string}>
     * }
     */
    private function buildConfigPayload(): array
    {
        $config = AccessControl::getStoredConfig();
        $roleByKey = $this->buildRoleLabelMap();

        $users = [];
        foreach ($config['users'] as $id => $mode) {
            $user = get_userdata($id);
            if (!($user instanceof \WP_User)) {
                continue;
            }
            $userPayload = $this->buildUserPayload($user, $roleByKey);
            $userPayload['mode'] = (string) $mode;
            $users[] = $userPayload;
        }

        $roleDefs = $this->describeRoles();
        $roles = [];
        foreach ($config['roles'] as $slug => $mode) {
            $roles[] = [
                'slug' => (string) $slug,
                'name' => $roleByKey[$slug] ?? ucfirst((string) $slug),
                'mode' => (string) $mode,
            ];
        }

        return [
            'users' => $users,
            'roles' => $roles,
            'available_roles' => $roleDefs,
        ];
    }

    /**
     * @return array<int,array{slug:string,name:string}>
     */
    private function describeRoles(): array
    {
        if (!function_exists('wp_roles')) {
            return [];
        }
        $roles = wp_roles()->roles;
        $out = [];
        foreach ($roles as $slug => $data) {
            $slug = (string) $slug;
            if ($slug === 'administrator') {
                continue;
            }
            $name = isset($data['name']) && is_string($data['name']) ? $data['name'] : ucfirst($slug);
            $out[] = ['slug' => $slug, 'name' => $name];
        }
        return $out;
    }

    /**
     * @return array<string,string>
     */
    private function buildRoleLabelMap(): array
    {
        if (!function_exists('wp_roles')) {
            return [];
        }

        $roles = wp_roles()->roles;
        $out = [];
        foreach ($roles as $slug => $data) {
            $slug = (string) $slug;
            if ($slug === '') {
                continue;
            }
            $name = isset($data['name']) && is_string($data['name']) ? $data['name'] : ucfirst($slug);
            $out[$slug] = $name;
        }

        return $out;
    }

    /**
     * @param array<string,string> $roleByKey
     * @return array{
     *     id:int,
     *     login:string,
     *     display_name:string,
     *     email:string,
     *     first_name:string,
     *     last_name:string,
     *     role:string,
     *     role_name:string,
     *     roles:array<int,string>
     * }
     */
    private function buildUserPayload(\WP_User $user, array $roleByKey): array
    {
        $roles = array_values(array_filter(array_map('strval', (array) $user->roles)));
        $primaryRole = $roles !== [] ? $roles[0] : '';

        return [
            'id' => (int) $user->ID,
            'login' => (string) $user->user_login,
            'display_name' => (string) $user->display_name,
            'email' => (string) $user->user_email,
            'first_name' => (string) $user->first_name,
            'last_name' => (string) $user->last_name,
            'role' => $primaryRole,
            'role_name' => $primaryRole !== '' ? ($roleByKey[$primaryRole] ?? ucfirst($primaryRole)) : '',
            'roles' => $roles,
        ];
    }

    /**
     * @param mixed $raw
     * @return array<int,string>
     */
    private function coerceAssocIntKeyed($raw): array
    {
        if (!is_array($raw)) {
            return [];
        }
        $out = [];
        foreach ($raw as $key => $value) {
            $id = (int) $key;
            if ($id <= 0) {
                continue;
            }
            if (is_string($value) && $value !== '') {
                $out[$id] = $value;
            }
        }
        return $out;
    }

    /**
     * @param mixed $raw
     * @return array<string,string>
     */
    private function coerceAssocStringKeyed($raw): array
    {
        if (!is_array($raw)) {
            return [];
        }
        $out = [];
        foreach ($raw as $key => $value) {
            if (!is_string($key) || $key === '') {
                continue;
            }
            if (is_string($value) && $value !== '') {
                $out[$key] = $value;
            }
        }
        return $out;
    }

    private function applyRestNoCache(\WP_REST_Response $response): \WP_REST_Response
    {
        if (!defined('DONOTCACHEPAGE')) {
            // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress/cache-plugin global constant used to prevent caching of authenticated REST responses.
            define('DONOTCACHEPAGE', true);
        }
        add_filter('rest_send_nocache_headers', '__return_true', PHP_INT_MAX);
        $response->header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0, private');
        $response->header('Pragma', 'no-cache');
        $response->header('Expires', '0');
        $headers = $response->get_headers();
        $vary = isset($headers['Vary']) ? (string) $headers['Vary'] : '';
        if ($vary === '') {
            $response->header('Vary', 'Cookie');
        } elseif (stripos($vary, 'Cookie') === false) {
            $response->header('Vary', $vary . ', Cookie');
        }
        return $response;
    }
}
