<?php

declare(strict_types=1);

namespace App\Framework\CircuitBreaker\FailurePredicate;

use Throwable;

/**
 * Composite predicate that combines multiple predicates with logical operators
 */
final readonly class CompositeFailurePredicate implements FailurePredicate
{
    public function __construct(
        /**
         * @var array<FailurePredicate>
         */
        private array $predicates,
        private LogicalOperator $operator = LogicalOperator::AND,
    ) {
    }

    public function shouldTrigger(Throwable $exception, string $service): bool
    {
        if (empty($this->predicates)) {
            return false;
        }

        return match ($this->operator) {
            LogicalOperator::AND => $this->evaluateAnd($exception, $service),
            LogicalOperator::OR => $this->evaluateOr($exception, $service),
            LogicalOperator::NOT => $this->evaluateNot($exception, $service),
        };
    }

    public function getIdentifier(): string
    {
        $identifiers = array_map(fn (FailurePredicate $p) => $p->getIdentifier(), $this->predicates);

        return 'composite(' . $this->operator->value . ':' . implode(',', $identifiers) . ')';
    }

    public function getDescription(): string
    {
        $descriptions = array_map(fn (FailurePredicate $p) => $p->getDescription(), $this->predicates);

        return match ($this->operator) {
            LogicalOperator::AND => '(' . implode(' AND ', $descriptions) . ')',
            LogicalOperator::OR => '(' . implode(' OR ', $descriptions) . ')',
            LogicalOperator::NOT => 'NOT (' . implode(', ', $descriptions) . ')',
        };
    }

    /**
     * Create AND composite predicate
     *
     * @param array<FailurePredicate> $predicates
     */
    public static function and(array $predicates): self
    {
        return new self($predicates, LogicalOperator::AND);
    }

    /**
     * Create OR composite predicate
     *
     * @param array<FailurePredicate> $predicates
     */
    public static function or(array $predicates): self
    {
        return new self($predicates, LogicalOperator::OR);
    }

    /**
     * Create NOT composite predicate
     *
     * @param array<FailurePredicate> $predicates
     */
    public static function not(array $predicates): self
    {
        return new self($predicates, LogicalOperator::NOT);
    }

    private function evaluateAnd(Throwable $exception, string $service): bool
    {
        foreach ($this->predicates as $predicate) {
            if (! $predicate->shouldTrigger($exception, $service)) {
                return false;
            }
        }

        return true;
    }

    private function evaluateOr(Throwable $exception, string $service): bool
    {
        foreach ($this->predicates as $predicate) {
            if ($predicate->shouldTrigger($exception, $service)) {
                return true;
            }
        }

        return false;
    }

    private function evaluateNot(Throwable $exception, string $service): bool
    {
        foreach ($this->predicates as $predicate) {
            if ($predicate->shouldTrigger($exception, $service)) {
                return false;
            }
        }

        return true;
    }
}
