<?php

/**
 * @package    Grav\Framework\Acl
 *
 * @copyright  Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
 * @license    MIT License; see LICENSE file for details.
 */

namespace Grav\Framework\Acl;

use ArrayIterator;
use Countable;
use Grav\Common\Utils;
use IteratorAggregate;
use JsonSerializable;
use Traversable;
use function count;
use function is_array;
use function is_bool;
use function is_string;
use function strlen;

/**
 * Class Access
 * @package Grav\Framework\Acl
 * @implements IteratorAggregate<string,bool|array|null>
 */
class Access implements JsonSerializable, IteratorAggregate, Countable
{
    /** @var string */
    private $name;
    /** @var array */
    private $rules;
    /** @var array */
    private $ops;
    /** @var array<string,bool|array|null> */
    private $acl = [];
    /** @var array */
    private $inherited = [];

    /**
     * Access constructor.
     * @param string|array|null $acl
     * @param array|null $rules
     * @param string $name
     */
    public function __construct($acl = null, array $rules = null, string $name = '')
    {
        $this->name = $name;
        $this->rules = $rules ?? [];
        $this->ops = ['+' => true, '-' => false];
        if (is_string($acl)) {
            $this->acl = $this->resolvePermissions($acl);
        } elseif (is_array($acl)) {
            $this->acl = $this->normalizeAcl($acl);
        }
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * @param Access $parent
     * @param string|null $name
     * @return void
     */
    public function inherit(Access $parent, string $name = null)
    {
        // Remove cached null actions from acl.
        $acl = $this->getAllActions();
        // Get only inherited actions.
        $inherited = array_diff_key($parent->getAllActions(), $acl);

        $this->inherited += $parent->inherited + array_fill_keys(array_keys($inherited), $name ?? $parent->getName());
        $this->acl = array_replace($acl, $inherited);
    }

    /**
     * Checks user authorization to the action.
     *
     * @param  string $action
     * @param  string|null $scope
     * @return bool|null
     */
    public function authorize(string $action, string $scope = null): ?bool
    {
        if (null !== $scope) {
            $action = $scope !== 'test' ? "{$scope}.{$action}" : $action;
        }

        return $this->get($action);
    }

    /**
     * @return array
     */
    public function toArray(): array
    {
        return Utils::arrayUnflattenDotNotation($this->acl);
    }

    /**
     * @return array
     */
    public function getAllActions(): array
    {
        return array_filter($this->acl, static function($val) { return $val !== null; });
    }

    /**
     * @return array
     */
    public function jsonSerialize(): array
    {
        return $this->toArray();
    }

    /**
     * @param string $action
     * @return bool|null
     */
    public function get(string $action)
    {
        // Get access value.
        if (isset($this->acl[$action])) {
            return $this->acl[$action];
        }

        // If no value is defined, check the parent access (all true|false).
        $pos = strrpos($action, '.');
        $value = $pos ? $this->get(substr($action, 0, $pos)) : null;

        // Cache result for faster lookup.
        $this->acl[$action] = $value;

        return $value;
    }

    /**
     * @param string $action
     * @return bool
     */
    public function isInherited(string $action): bool
    {
        return isset($this->inherited[$action]);
    }

    /**
     * @param string $action
     * @return string|null
     */
    public function getInherited(string $action): ?string
    {
        return $this->inherited[$action] ?? null;
    }

    /**
     * @return Traversable
     */
    public function getIterator(): Traversable
    {
        return new ArrayIterator($this->acl);
    }

    /**
     * @return int
     */
    public function count(): int
    {
        return count($this->acl);
    }

    /**
     * @param array $acl
     * @return array
     */
    protected function normalizeAcl(array $acl): array
    {
        if (empty($acl)) {
            return [];
        }

        // Normalize access control list.
        $list = [];
        foreach (Utils::arrayFlattenDotNotation($acl) as $key => $value) {
            if (is_bool($value)) {
                $list[$key] = $value;
            } elseif ($value === 0 || $value === 1) {
                $list[$key] = (bool)$value;
            } elseif($value === null) {
                continue;
            } elseif ($this->rules && is_string($value)) {
                $list[$key] = $this->resolvePermissions($value);
            } elseif (Utils::isPositive($value)) {
                $list[$key] = true;
            } elseif (Utils::isNegative($value)) {
                $list[$key] = false;
            }
        }

        return $list;
    }

    /**
     * @param string $access
     * @return array
     */
    protected function resolvePermissions(string $access): array
    {
        $len = strlen($access);
        $op = true;
        $list = [];
        for($count = 0; $count < $len; $count++) {
            $letter = $access[$count];
            if (isset($this->rules[$letter])) {
                $list[$this->rules[$letter]] = $op;
                $op = true;
            } elseif (isset($this->ops[$letter])) {
                $op = $this->ops[$letter];
            }
        }

        return $list;
    }
}
