<?php

/**
 * PHP Advanced Patterns — Zero-Dependency Design Pattern Library
 *
 * A single-file library providing battle-tested software design patterns for PHP 8.0+.
 * No external dependencies.
 *
 * Classes:
 *   Pipeline       — Composable middleware-style data processing
 *   Result / Ok / Err — Monadic error handling without exceptions
 *   EventEmitter   — Publish/subscribe event system
 *   Container      — Lightweight dependency injection container
 *   Retry          — Configurable retry with backoff strategies
 *   CircuitBreaker — Fault tolerance for external service calls
 *
 * @version 1.0.0
 * @author  Ahmed Habib
 * @license MIT
 */

namespace PhpAdvancedPatterns;

// ─── Pipeline (Composable Data Processing) ──────────────────────────────────

class Pipeline
{
    /** @var callable[] */
    private array $stages = [];

    public function __construct(callable ...$stages)
    {
        $this->stages = $stages;
    }

    /** Add a stage to the pipeline. */
    public function pipe(callable $stage): self
    {
        $clone = clone $this;
        $clone->stages[] = $stage;
        return $clone;
    }

    /** Add multiple stages at once. */
    public function through(callable ...$stages): self
    {
        $clone = clone $this;
        foreach ($stages as $stage) {
            $clone->stages[] = $stage;
        }
        return $clone;
    }

    /** Process the payload through all stages. */
    public function process(mixed $payload): mixed
    {
        foreach ($this->stages as $stage) {
            $payload = $stage($payload);
        }
        return $payload;
    }

    /** Process and return a Result (catches exceptions as Err). */
    public function processSafe(mixed $payload): Result
    {
        try {
            return Ok::of($this->process($payload));
        } catch (\Throwable $e) {
            return Err::of($e->getMessage());
        }
    }

    /** Create a pipeline with conditional stages. */
    public function when(bool $condition, callable $stage): self
    {
        if ($condition) {
            return $this->pipe($stage);
        }
        return $this;
    }

    /** Return the number of stages. */
    public function count(): int
    {
        return count($this->stages);
    }

    /** Create a new pipeline. */
    public static function of(callable ...$stages): self
    {
        return new self(...$stages);
    }

    /** Create from an array of callables. */
    public static function from(array $stages): self
    {
        return new self(...$stages);
    }
}

// ─── Result / Ok / Err (Monadic Error Handling) ─────────────────────────────

abstract class Result
{
    /** Check if the result is Ok. */
    abstract public function isOk(): bool;

    /** Check if the result is Err. */
    abstract public function isErr(): bool;

    /** Get the value or throw if Err. */
    abstract public function unwrap(): mixed;

    /** Get the error or throw if Ok. */
    abstract public function unwrapErr(): mixed;

    /** Get the value or return a default. */
    abstract public function unwrapOr(mixed $default): mixed;

    /** Apply a callback if Ok, return self if Err. */
    abstract public function map(callable $fn): self;

    /** Apply a callback if Err, return self if Ok. */
    abstract public function mapErr(callable $fn): self;

    /** Chain an operation that returns a Result. */
    abstract public function andThen(callable $fn): self;

    /** Return the other Result if Err, or self if Ok. */
    abstract public function orElse(callable $fn): self;

    /** Execute a callback for side effects without modifying the Result. */
    abstract public function tap(callable $fn): self;

    /** Convert to an array with 'ok' or 'err' key. */
    abstract public function toArray(): array;

    /** Create a Result from a callable (catches exceptions as Err). */
    public static function try(callable $fn): self
    {
        try {
            return Ok::of($fn());
        } catch (\Throwable $e) {
            return Err::of($e->getMessage());
        }
    }

    /** Collect an array of Results into a single Result. All must be Ok. */
    public static function all(array $results): self
    {
        $values = [];
        foreach ($results as $result) {
            if ($result->isErr()) {
                return $result;
            }
            $values[] = $result->unwrap();
        }
        return Ok::of($values);
    }
}

class Ok extends Result
{
    private mixed $value;

    public function __construct(mixed $value)
    {
        $this->value = $value;
    }

    public function isOk(): bool { return true; }
    public function isErr(): bool { return false; }

    public function unwrap(): mixed { return $this->value; }

    public function unwrapErr(): mixed
    {
        throw new \LogicException('Called unwrapErr() on an Ok value.');
    }

    public function unwrapOr(mixed $default): mixed { return $this->value; }

    public function map(callable $fn): Result { return new self($fn($this->value)); }
    public function mapErr(callable $fn): Result { return $this; }

    public function andThen(callable $fn): Result { return $fn($this->value); }
    public function orElse(callable $fn): Result { return $this; }

    public function tap(callable $fn): Result
    {
        $fn($this->value);
        return $this;
    }

    public function toArray(): array { return ['ok' => $this->value]; }

    public static function of(mixed $value): self { return new self($value); }
}

class Err extends Result
{
    private mixed $error;

    public function __construct(mixed $error)
    {
        $this->error = $error;
    }

    public function isOk(): bool { return false; }
    public function isErr(): bool { return true; }

    public function unwrap(): mixed
    {
        throw new \RuntimeException(
            is_string($this->error) ? $this->error : 'Called unwrap() on an Err value.'
        );
    }

    public function unwrapErr(): mixed { return $this->error; }
    public function unwrapOr(mixed $default): mixed { return $default; }

    public function map(callable $fn): Result { return $this; }
    public function mapErr(callable $fn): Result { return new self($fn($this->error)); }

    public function andThen(callable $fn): Result { return $this; }
    public function orElse(callable $fn): Result { return $fn($this->error); }

    public function tap(callable $fn): Result { return $this; }

    public function toArray(): array { return ['err' => $this->error]; }

    public static function of(mixed $error): self { return new self($error); }
}

// ─── EventEmitter (Publish / Subscribe) ─────────────────────────────────────

class EventEmitter
{
    /** @var array<string, array<int, array{callable, bool}>> */
    private array $listeners = [];

    /** Register a listener for an event. */
    public function on(string $event, callable $listener): self
    {
        $this->listeners[$event][] = ['fn' => $listener, 'once' => false];
        return $this;
    }

    /** Register a one-time listener. */
    public function once(string $event, callable $listener): self
    {
        $this->listeners[$event][] = ['fn' => $listener, 'once' => true];
        return $this;
    }

    /** Emit an event with optional arguments. Returns the number of listeners called. */
    public function emit(string $event, mixed ...$args): int
    {
        if (!isset($this->listeners[$event])) {
            return 0;
        }

        $called = 0;
        $remaining = [];

        foreach ($this->listeners[$event] as $entry) {
            ($entry['fn'])(...$args);
            $called++;

            if (!$entry['once']) {
                $remaining[] = $entry;
            }
        }

        $this->listeners[$event] = $remaining;
        return $called;
    }

    /** Remove a specific listener from an event. */
    public function off(string $event, callable $listener): self
    {
        if (!isset($this->listeners[$event])) {
            return $this;
        }

        $this->listeners[$event] = array_values(array_filter(
            $this->listeners[$event],
            fn($entry) => $entry['fn'] !== $listener
        ));

        if (empty($this->listeners[$event])) {
            unset($this->listeners[$event]);
        }

        return $this;
    }

    /** Remove all listeners for an event, or all events if no name given. */
    public function removeAll(?string $event = null): self
    {
        if ($event === null) {
            $this->listeners = [];
        } else {
            unset($this->listeners[$event]);
        }
        return $this;
    }

    /** Get the count of listeners for an event. */
    public function listenerCount(string $event): int
    {
        return count($this->listeners[$event] ?? []);
    }

    /** Get all registered event names. */
    public function eventNames(): array
    {
        return array_keys($this->listeners);
    }

    /** Check if an event has any listeners. */
    public function hasListeners(string $event): bool
    {
        return !empty($this->listeners[$event]);
    }
}

// ─── Container (Dependency Injection) ───────────────────────────────────────

class Container
{
    /** @var array<string, callable> */
    private array $bindings = [];

    /** @var array<string, mixed> */
    private array $singletons = [];

    /** @var array<string, bool> */
    private array $isSingleton = [];

    /** @var array<string, string> */
    private array $aliases = [];

    /** Register a binding. */
    public function bind(string $id, callable $factory): self
    {
        $this->bindings[$id] = $factory;
        return $this;
    }

    /** Register a singleton binding (resolved once, then cached). */
    public function singleton(string $id, callable $factory): self
    {
        $this->bindings[$id] = $factory;
        $this->isSingleton[$id] = true;
        return $this;
    }

    /** Register an alias for a binding. */
    public function alias(string $alias, string $id): self
    {
        $this->aliases[$alias] = $id;
        return $this;
    }

    /** Register a pre-built instance. */
    public function instance(string $id, mixed $value): self
    {
        $this->singletons[$id] = $value;
        $this->isSingleton[$id] = true;
        return $this;
    }

    /** Resolve a binding by ID. */
    public function get(string $id): mixed
    {
        // Resolve alias
        $id = $this->aliases[$id] ?? $id;

        // Return cached singleton
        if (isset($this->singletons[$id])) {
            return $this->singletons[$id];
        }

        if (!isset($this->bindings[$id])) {
            throw new \RuntimeException("No binding found for [{$id}].");
        }

        $result = ($this->bindings[$id])($this);

        if (!empty($this->isSingleton[$id])) {
            $this->singletons[$id] = $result;
        }

        return $result;
    }

    /** Check if a binding exists. */
    public function has(string $id): bool
    {
        $id = $this->aliases[$id] ?? $id;
        return isset($this->bindings[$id]) || isset($this->singletons[$id]);
    }

    /** Remove a binding. */
    public function forget(string $id): self
    {
        unset($this->bindings[$id], $this->singletons[$id], $this->isSingleton[$id]);
        return $this;
    }

    /** Get all registered binding IDs. */
    public function keys(): array
    {
        return array_unique(array_merge(
            array_keys($this->bindings),
            array_keys($this->singletons)
        ));
    }

    /** Invoke a callable with auto-resolved dependencies. */
    public function call(callable $fn, array $params = []): mixed
    {
        return $fn($this, ...$params);
    }
}

// ─── Retry (Configurable Retry with Backoff) ────────────────────────────────

class Retry
{
    private int $maxAttempts = 3;
    private int $delayMs = 100;
    private float $multiplier = 2.0;
    private int $maxDelayMs = 30000;
    private ?\Closure $retryIf = null;
    private ?\Closure $onRetry = null;

    /** Set the maximum number of attempts. */
    public function attempts(int $max): self
    {
        $this->maxAttempts = max(1, $max);
        return $this;
    }

    /** Set the initial delay in milliseconds. */
    public function delay(int $ms): self
    {
        $this->delayMs = max(0, $ms);
        return $this;
    }

    /** Set the backoff multiplier. */
    public function multiplier(float $factor): self
    {
        $this->multiplier = max(1.0, $factor);
        return $this;
    }

    /** Set the maximum delay cap in milliseconds. */
    public function maxDelay(int $ms): self
    {
        $this->maxDelayMs = max(0, $ms);
        return $this;
    }

    /** Set a condition for when to retry (receives the exception). */
    public function retryIf(callable $fn): self
    {
        $this->retryIf = $fn;
        return $this;
    }

    /** Set a callback called before each retry (receives attempt number and exception). */
    public function onRetry(callable $fn): self
    {
        $this->onRetry = $fn;
        return $this;
    }

    /** Execute the callable with retry logic. Returns a Result. */
    public function run(callable $fn): Result
    {
        $currentDelay = $this->delayMs;
        $lastError = null;

        for ($attempt = 1; $attempt <= $this->maxAttempts; $attempt++) {
            try {
                return Ok::of($fn($attempt));
            } catch (\Throwable $e) {
                $lastError = $e;

                // Check retry condition
                if ($this->retryIf !== null && !($this->retryIf)($e, $attempt)) {
                    return Err::of($e->getMessage());
                }

                // Don't wait after the last attempt
                if ($attempt < $this->maxAttempts) {
                    if ($this->onRetry !== null) {
                        ($this->onRetry)($attempt, $e);
                    }

                    if ($currentDelay > 0) {
                        usleep($currentDelay * 1000);
                    }

                    $currentDelay = (int) min($currentDelay * $this->multiplier, $this->maxDelayMs);
                }
            }
        }

        return Err::of($lastError ? $lastError->getMessage() : 'All retry attempts exhausted.');
    }

    /** Create a new Retry instance. */
    public static function of(): self
    {
        return new self();
    }

    /** Quick retry: run a callable with default settings. */
    public static function times(int $attempts, callable $fn): Result
    {
        return (new self())->attempts($attempts)->run($fn);
    }
}

// ─── CircuitBreaker (Fault Tolerance) ───────────────────────────────────────

class CircuitBreaker
{
    private const STATE_CLOSED = 'closed';
    private const STATE_OPEN = 'open';
    private const STATE_HALF_OPEN = 'half_open';

    private string $state = self::STATE_CLOSED;
    private int $failureCount = 0;
    private int $successCount = 0;
    private int $failureThreshold;
    private int $successThreshold;
    private int $timeoutSeconds;
    private ?float $lastFailureTime = null;
    private ?\Closure $onStateChange = null;

    public function __construct(
        int $failureThreshold = 5,
        int $timeoutSeconds = 60,
        int $successThreshold = 2
    ) {
        $this->failureThreshold = $failureThreshold;
        $this->timeoutSeconds = $timeoutSeconds;
        $this->successThreshold = $successThreshold;
    }

    /** Execute a callable through the circuit breaker. */
    public function call(callable $fn): Result
    {
        if ($this->state === self::STATE_OPEN) {
            if ($this->shouldAttemptReset()) {
                $this->transitionTo(self::STATE_HALF_OPEN);
            } else {
                return Err::of('Circuit breaker is open.');
            }
        }

        try {
            $result = $fn();
            $this->onSuccess();
            return Ok::of($result);
        } catch (\Throwable $e) {
            $this->onFailure();
            return Err::of($e->getMessage());
        }
    }

    /** Get the current state. */
    public function getState(): string
    {
        return $this->state;
    }

    /** Check if the circuit is closed (healthy). */
    public function isClosed(): bool
    {
        return $this->state === self::STATE_CLOSED;
    }

    /** Check if the circuit is open (tripped). */
    public function isOpen(): bool
    {
        return $this->state === self::STATE_OPEN;
    }

    /** Check if the circuit is half-open (testing). */
    public function isHalfOpen(): bool
    {
        return $this->state === self::STATE_HALF_OPEN;
    }

    /** Get the current failure count. */
    public function getFailureCount(): int
    {
        return $this->failureCount;
    }

    /** Register a callback for state changes. */
    public function onStateChange(callable $fn): self
    {
        $this->onStateChange = $fn;
        return $this;
    }

    /** Manually reset the circuit breaker to closed state. */
    public function reset(): self
    {
        $this->failureCount = 0;
        $this->successCount = 0;
        $this->lastFailureTime = null;
        $this->transitionTo(self::STATE_CLOSED);
        return $this;
    }

    /** Get circuit breaker stats. */
    public function stats(): array
    {
        return [
            'state'             => $this->state,
            'failure_count'     => $this->failureCount,
            'success_count'     => $this->successCount,
            'failure_threshold' => $this->failureThreshold,
            'success_threshold' => $this->successThreshold,
            'timeout_seconds'   => $this->timeoutSeconds,
        ];
    }

    /** Create a new CircuitBreaker. */
    public static function create(
        int $failureThreshold = 5,
        int $timeoutSeconds = 60,
        int $successThreshold = 2
    ): self {
        return new self($failureThreshold, $timeoutSeconds, $successThreshold);
    }

    private function onSuccess(): void
    {
        if ($this->state === self::STATE_HALF_OPEN) {
            $this->successCount++;
            if ($this->successCount >= $this->successThreshold) {
                $this->failureCount = 0;
                $this->successCount = 0;
                $this->transitionTo(self::STATE_CLOSED);
            }
        } else {
            $this->failureCount = 0;
        }
    }

    private function onFailure(): void
    {
        $this->failureCount++;
        $this->lastFailureTime = microtime(true);

        if ($this->state === self::STATE_HALF_OPEN) {
            $this->successCount = 0;
            $this->transitionTo(self::STATE_OPEN);
        } elseif ($this->failureCount >= $this->failureThreshold) {
            $this->transitionTo(self::STATE_OPEN);
        }
    }

    private function shouldAttemptReset(): bool
    {
        if ($this->lastFailureTime === null) {
            return true;
        }
        return (microtime(true) - $this->lastFailureTime) >= $this->timeoutSeconds;
    }

    private function transitionTo(string $state): void
    {
        $old = $this->state;
        $this->state = $state;

        if ($old !== $state && $this->onStateChange !== null) {
            ($this->onStateChange)($old, $state);
        }
    }
}
