<?php
declare(strict_types=1);

namespace Ekesto\CacheAutopilot;

use Ekesto\CacheAutopilot\Access\AccessOverrideProviderRegistry;

/**
 * Cache Autopilot Access Control.
 *
 * Central resolver + writer for the `ekesto_ca_access_control` option.
 * Consumed directly by ECI and via a shim by ECW.
 *
 * - Administrators are always 'manage'.
 * - Everyone else defaults to 'none'.
 * - Role-based access grants manage when any assigned role is configured.
 * - User-specific overrides grant manage when present.
 */
final class AccessControl
{
    public const OPTION_KEY = 'ekesto_ca_access_control';

    public const MODE_NONE = 'none';
    public const MODE_MANAGE = 'manage';

    /** @var array<int,string> Per-request cache keyed by user id. */
    private static array $cache = [];

    public static function getUserMode(int $user_id): string
    {
        if ($user_id <= 0) {
            return self::MODE_NONE;
        }

        if (isset(self::$cache[$user_id])) {
            return self::$cache[$user_id];
        }

        $user = get_userdata($user_id);
        if (!($user instanceof \WP_User)) {
            return self::$cache[$user_id] = self::MODE_NONE;
        }

        if (self::hasAdminCapability($user_id)) {
            return self::$cache[$user_id] = self::MODE_MANAGE;
        }

        // Non-admin access is resolved by the override provider. Lite ships a
        // no-op provider (admins only); the premium build registers a provider
        // that honors stored role/user overrides. The stored option is never
        // consulted directly here, so Lite ignores (but preserves) it.
        $mode = AccessOverrideProviderRegistry::current()->resolveOverrideMode($user_id, $user);
        if ($mode !== self::MODE_MANAGE) {
            $mode = self::MODE_NONE;
        }

        return self::$cache[$user_id] = $mode;
    }

    public static function userCanManage(?int $user_id = null): bool
    {
        return self::getUserMode(self::resolveUserId($user_id)) === self::MODE_MANAGE;
    }

    /**
     * Invalidate the per-request cache. Pass a user id to flush a single entry,
     * or null to flush everything.
     */
    public static function flushCache(?int $user_id = null): void
    {
        if ($user_id === null) {
            self::$cache = [];
            return;
        }
        unset(self::$cache[$user_id]);
    }

    /**
     * Return the stored option as a sanitized, well-shaped array.
     *
     * @return array{users: array<int,string>, roles: array<string,string>}
     */
    public static function getStoredConfig(): array
    {
        $raw = get_option(self::OPTION_KEY, []);
        return self::sanitize(is_array($raw) ? $raw : []);
    }

    /**
     * Persist a sanitized access-control config. Flushes the per-request cache.
     */
    public static function saveStoredConfig(array $config): bool
    {
        $clean = self::sanitize($config);
        $result = update_option(self::OPTION_KEY, $clean, false);
        self::flushCache();
        return (bool) $result;
    }

    /**
     * Register the option-save hook so external `update_option` calls also flush.
     * Safe to call multiple times (WordPress dedupes identical callbacks).
     */
    public static function registerHooks(): void
    {
        add_action('update_option_' . self::OPTION_KEY, [self::class, 'onOptionUpdated']);
        add_action('add_option_' . self::OPTION_KEY, [self::class, 'onOptionUpdated']);
        add_action('delete_option_' . self::OPTION_KEY, [self::class, 'onOptionUpdated']);
    }

    public static function onOptionUpdated(): void
    {
        self::flushCache();
    }

    /**
     * Return whether the user has the administrator-level settings capability.
     *
     * @param int $userId User ID.
     * @return bool
     */
    private static function hasAdminCapability(int $userId): bool
    {
        if ($userId === get_current_user_id()) {
            return current_user_can('manage_options');
        }

        return user_can($userId, 'manage_options');
    }

    private static function resolveUserId(?int $user_id): int
    {
        if ($user_id !== null) {
            return $user_id;
        }
        return function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
    }

    /**
     * @return array{users: array<int,string>, roles: array<string,string>}
     */
    private static function sanitize(array $raw): array
    {
        $users = [];
        $roles = [];

        $rawUsers = isset($raw['users']) && is_array($raw['users']) ? $raw['users'] : [];
        foreach ($rawUsers as $id => $mode) {
            $id = (int) $id;
            if ($id <= 0) {
                continue;
            }
            $mode = is_string($mode) ? $mode : '';
            if ($mode === self::MODE_MANAGE) {
                $users[$id] = $mode;
            }
        }

        $rawRoles = isset($raw['roles']) && is_array($raw['roles']) ? $raw['roles'] : [];
        foreach ($rawRoles as $slug => $mode) {
            $slug = is_string($slug) ? sanitize_key($slug) : '';
            if ($slug === '' || $slug === 'administrator') {
                continue;
            }
            $mode = is_string($mode) ? $mode : '';
            if ($mode === self::MODE_MANAGE) {
                $roles[$slug] = $mode;
            }
        }

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