<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Mcp\Action;

/**
 * The entire expression language of the catalog, in one place. Two clauses need to point at a
 * caller argument — `verify.expect` values and `journal.scope[].by` values — and both must point
 * at it the same way, or the validator and the invoker will disagree about what is legal.
 *
 * The grammar is deliberately closed: the WHOLE string is either `{{args.<field>}}` or
 * `{{count(args.<field>)}}`. No concatenation, no nesting, no arithmetic. Anything that merely
 * looks like a placeholder ({@see self::looksLikeOne()}) but does not parse is a catalog error
 * rather than a literal, so a typo can never be silently written to the database as a string.
 */
final class Placeholder
{
    /**
     * The one grammar every reference obeys. It is also PDO's named-parameter grammar and the
     * `:name` scan in {@see ActionInvocation::namedParameters()} — which is why a field whose base
     * name steps outside it (`redirect-id`) is unreachable from BOTH clauses at once: the
     * placeholder will not parse, and `:redirect-id` binds `:redirect`. {@see self::isReferenceable()}
     */
    private const IDENTIFIER = '[A-Za-z_][A-Za-z0-9_]*';

    private const ARGUMENT = '/^\{\{args\.(' . self::IDENTIFIER . ')\}\}\z/';

    private const COUNT = '/^\{\{count\(args\.(' . self::IDENTIFIER . ')\)\}\}\z/';

    /** The text a malformed placeholder aims at, so the error can name the real cause. */
    private const TARGET = '/^\{\{(?:count\()?args\.([^{}()]*)\)?\}\}\z/';

    private function __construct(
        public readonly string $field,
        public readonly bool $countsElements,
    ) {
    }

    /** True for any string that intends to be a placeholder, valid or not. */
    public static function looksLikeOne(string $value): bool
    {
        return \str_contains($value, '{{') || \str_contains($value, '}}');
    }

    /** Null when $value is not a well-formed placeholder. */
    public static function parse(string $value): ?self
    {
        $matches = [];
        if (\preg_match(self::ARGUMENT, $value, $matches) === 1) {
            return new self($matches[1], false);
        }
        if (\preg_match(self::COUNT, $value, $matches) === 1) {
            return new self($matches[1], true);
        }

        return null;
    }

    /**
     * True when an argument name can actually be pointed at. `required: true` makes a field
     * MANDATORY, not REFERENCEABLE — a name outside this grammar satisfies neither clause, so an
     * entry whose only key field is one of them cannot be verified at all.
     */
    public static function isReferenceable(string $baseName): bool
    {
        return \preg_match('/^' . self::IDENTIFIER . '\z/', $baseName) === 1;
    }

    /**
     * The argument name a malformed `{{…}}` expression was aiming at, or null when the string is
     * not even shaped like a reference. Diagnostics only — never a successful parse.
     */
    public static function target(string $value): ?string
    {
        $matches = [];

        return \preg_match(self::TARGET, $value, $matches) === 1 && $matches[1] !== ''
            ? $matches[1]
            : null;
    }

    /**
     * The argument name a field declaration refers to: `kZahlungsart[]` and
     * `fAufpreis_{kZahlungsart}` are bound as `kZahlungsart` and `fAufpreis_` respectively.
     */
    public static function baseName(string $fieldName): string
    {
        $name = \preg_replace('/\[\]$/', '', $fieldName) ?? $fieldName;

        return \preg_replace('/\{[^{}]*\}/', '', $name) ?? $name;
    }
}
