<?php

declare(strict_types=1);

namespace Atdt;

/**
 * ACS mini-language evaluator (SPEC §8.4).
 *
 * Grammar:
 *   expr   := term ('|' term)*          OR
 *   term   := factor+                   adjacency = AND
 *   factor := '!' factor | '(' expr ')' | token
 *   token  := s<digits> | f<A-Z> | v
 *
 * Empty string allows everyone; sysop (level 255) always passes; a malformed
 * string fails CLOSED (deny) and logs nothing here — callers may log.
 */
final class Acs
{
    private string $s;
    private int $i = 0;
    private int $level;
    private string $flags;
    private bool $validated;

    private function __construct(string $acs, int $level, string $flags, bool $validated)
    {
        $this->s = $acs;
        $this->level = $level;
        $this->flags = $flags;
        $this->validated = $validated;
    }

    /**
     * Evaluate $acs against a user shape: ['level' => int, 'flags' => string,
     * 'status' => string]. Missing keys mean an anonymous caller (level 0).
     *
     * @param array<string, mixed>|null $user
     */
    public static function check(string $acs, ?array $user): bool
    {
        $level = (int)($user['level'] ?? 0);
        if ($level >= 255) {
            return true; // sysop always passes (§8.4)
        }
        $acs = preg_replace('/\s+/', '', $acs) ?? '';
        if ($acs === '') {
            return true;
        }
        $p = new self(
            $acs,
            $level,
            (string)($user['flags'] ?? ''),
            (string)($user['status'] ?? '') === 'active'
        );
        try {
            $result = $p->expr();
            if ($p->i !== strlen($p->s)) {
                return false; // trailing garbage → fail closed
            }
            return $result;
        } catch (\InvalidArgumentException $e) {
            return false; // malformed → fail closed
        }
    }

    private function expr(): bool
    {
        $result = $this->term();
        while ($this->peek() === '|') {
            $this->i++;
            $rhs = $this->term();  // no short-circuit: keep parsing strict
            $result = $result || $rhs;
        }
        return $result;
    }

    private function term(): bool
    {
        $result = $this->factor();
        while (true) {
            $c = $this->peek();
            if ($c === null || $c === '|' || $c === ')') {
                return $result;
            }
            $rhs = $this->factor();
            $result = $result && $rhs;
        }
    }

    private function factor(): bool
    {
        $c = $this->peek();
        if ($c === '!') {
            $this->i++;
            return !$this->factor();
        }
        if ($c === '(') {
            $this->i++;
            $result = $this->expr();
            if ($this->peek() !== ')') {
                throw new \InvalidArgumentException('unclosed group');
            }
            $this->i++;
            return $result;
        }
        return $this->token();
    }

    private function token(): bool
    {
        $c = $this->peek();
        if ($c === 's') {
            $this->i++;
            $start = $this->i;
            while (($d = $this->peek()) !== null && $d >= '0' && $d <= '9') {
                $this->i++;
            }
            if ($this->i === $start) {
                throw new \InvalidArgumentException('s needs digits');
            }
            return $this->level >= (int)substr($this->s, $start, $this->i - $start);
        }
        if ($c === 'f') {
            $this->i++;
            $flag = $this->peek();
            if ($flag === null || $flag < 'A' || $flag > 'Z') {
                throw new \InvalidArgumentException('f needs A-Z');
            }
            $this->i++;
            return str_contains($this->flags, $flag);
        }
        if ($c === 'v') {
            $this->i++;
            return $this->validated;
        }
        throw new \InvalidArgumentException('unexpected ' . var_export($c, true));
    }

    private function peek(): ?string
    {
        return $this->i < strlen($this->s) ? $this->s[$this->i] : null;
    }
}
