<?php

namespace App\Policy\Service;

use App\Policy\Entity\Policy;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

/**
 * ارزیابی شرط یک قانون در برابر «حقایق» یک درخواست.
 *
 * هیچ کد دلخواهی اجرا نمی‌شود: فیلد باید در فهرست بستهٔ {@see PolicySchema} باشد و
 * عملگر از شش عملگر ثابت. شرطی که فیلد ناشناخته دارد **در زمان ساخت** رد می‌شود، نه
 * در زمان اجرا — قانونی که موقع رزرو بیمار بترکد، بدترین جای ممکن برای شکستن است.
 */
final class ConditionEvaluator
{
    /** قانونِ در حال ارزیابی — فقط برای اینکه لاگِ فیلدِ غایب بگوید کدام قانون بود. */
    private ?Policy $policy = null;

    public function __construct(
        private readonly PolicySchema     $schema,
        private readonly FieldRegistry    $fields,
        private readonly OperatorRegistry $operators,
        private readonly LoggerInterface  $logger = new NullLogger(),
    ) {}

    /**
     * @param array<string, mixed> $facts
     */
    public function matches(Policy $policy, array $facts): bool
    {
        $this->policy = $policy;

        $condition  = $policy->getCondition();
        $conditions = $condition['conditions'] ?? [];

        // شرط خالی یعنی «همیشه» — قانونِ بی‌قید و شرط کاملاً معتبر است.
        if ($conditions === []) {
            return true;
        }

        $mode = ($condition['match'] ?? 'all') === 'any' ? 'any' : 'all';

        foreach ($conditions as $clause) {
            $result = $this->evaluateClause($clause, $facts);

            if ($mode === 'any' && $result) {
                return true;
            }

            if ($mode === 'all' && !$result) {
                return false;
            }
        }

        return $mode === 'all';
    }

    /** @param array<string, mixed> $facts */
    private function evaluateClause(mixed $clause, array $facts): bool
    {
        if (!is_array($clause) || !is_string($clause['field'] ?? null)) {
            return false;
        }

        $field    = $clause['field'];
        $operator = $clause['operator'] ?? PolicySchema::OP_EQUALS;
        $expected = $clause['value'] ?? null;

        // فیلدی که در حقایق این درخواست نیست، شرط را **رد** می‌کند نه اینکه نادیده
        // بگیرد: قانون «سن زیر ۱۸» وقتی سن نامشخص است نباید بی‌صدا صادق شود.
        //
        // ولی رد کردنِ خاموش هم بد است: قانونی که هر بار به این خط می‌رسد، عملاً
        // خاموش است و کسی خبردار نمی‌شود. لاگ تنها چیزی است که این را قابل کشف می‌کند.
        if (!$this->fields->supplies($field, $facts)) {
            $this->logger->warning('policy condition skipped: fact missing', [
                'policy_uuid' => $this->policy?->getUuid(),
                'category'    => $this->policy?->getCategory(),
                'field'       => $field,
                'known_facts' => array_keys($facts),
            ]);

            return false;
        }

        return $this->operators->evaluate($operator, $this->fields->extract($field, $facts), $expected);
    }

    /**
     * اعتبارسنجی ساختار شرط در **زمان ساخت**.
     *
     * @param array<string, mixed> $condition
     * @throws AppException
     */
    public function assertValid(string $category, array $condition): void
    {
        // کلید ناشناس در ریشهٔ شرط **خطاست**: `{"all": [...]}` به‌جای
        // `{"match": "all", "conditions": [...]}` شرطی خالی می‌سازد که همیشه صادق
        // است — یعنی قانون روی همه‌چیز اجرا می‌شود بی‌آنکه کسی بفهمد.
        $unknown = array_diff(array_keys($condition), ['match', 'conditions']);

        if ($unknown !== []) {
            throw new AppException(
                ErrorCodes::ERR_VALIDATION_001,
                sprintf('کلید «%s» در شرط شناخته نمی‌شود؛ ساختار درست {match, conditions} است', (string) reset($unknown)),
                422,
                'condition',
            );
        }

        if (isset($condition['match']) && !in_array($condition['match'], ['all', 'any'], true)) {
            throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'مقدار match باید all یا any باشد', 422, 'condition');
        }

        if (isset($condition['conditions']) && !is_array($condition['conditions'])) {
            throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'conditions باید فهرست باشد', 422, 'condition');
        }

        foreach (($condition['conditions'] ?? []) as $clause) {
            if (!is_array($clause) || !is_string($clause['field'] ?? null)) {
                throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'هر شرط باید فیلد داشته باشد', 422, 'condition');
            }

            if (!$this->schema->allowsField($category, $clause['field'])) {
                throw new AppException(
                    ErrorCodes::ERR_VALIDATION_001,
                    sprintf(
                        'فیلد «%s» برای دستهٔ «%s» مجاز نیست. مجازها: %s',
                        $clause['field'],
                        $category,
                        implode('، ', $this->schema->fieldsFor($category)),
                    ),
                    422,
                    'condition',
                );
            }

            $operator = $clause['operator'] ?? PolicySchema::OP_EQUALS;

            if (!is_string($operator) || !$this->operators->has($operator)) {
                throw new AppException(
                    ErrorCodes::ERR_VALIDATION_001,
                    sprintf('عملگر «%s» شناخته نمی‌شود', is_string($operator) ? $operator : '—'),
                    422,
                    'condition',
                );
            }
        }
    }

    /**
     * ورودی مستقیم از JSON کاربر می‌آید، پس نوعش `mixed` است نه آرایهٔ ساختاریافته —
     * اعتبارسنجی همین‌جا همان چیزی است که ساختار را تضمین می‌کند.
     *
     * @param list<mixed> $effects
     * @throws AppException
     */
    public function assertEffectsValid(string $category, array $effects): void
    {
        if ($effects === []) {
            throw new AppException(ErrorCodes::ERR_VALIDATION_002, 'قانون باید حداقل یک اثر داشته باشد', 422, 'effects');
        }

        foreach ($effects as $effect) {
            if (!is_array($effect) || !is_string($effect['type'] ?? null)) {
                throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'هر اثر باید نوع داشته باشد', 422, 'effects');
            }

            if (!$this->schema->allowsEffect($category, $effect['type'])) {
                throw new AppException(
                    ErrorCodes::ERR_VALIDATION_001,
                    sprintf(
                        'اثر «%s» با دستهٔ «%s» سازگار نیست. مجازها: %s',
                        $effect['type'],
                        $category,
                        implode('، ', PolicySchema::EFFECTS[$category] ?? []),
                    ),
                    422,
                    'effects',
                );
            }
        }
    }
}
