<?php

declare(strict_types=1);

namespace App\Framework\Async;

use Generator;
use Iterator;

/**
 * Async Stream für deklarative Stream-Processing-Pipeline
 *
 * Ermöglicht funktionale Transformationen auf großen Datenmengen
 * mit Lazy Evaluation und optionaler paralleler Verarbeitung.
 */
final readonly class AsyncStream
{
    private function __construct(
        private Iterator|Generator $source,
        private FiberManager       $fiberManager
    ) {
    }

    /**
     * Erstellt einen Stream aus einem Iterator oder Array
     */
    public static function from(Iterator|array $source, FiberManager $fiberManager): self
    {
        $iterator = is_array($source) ? new \ArrayIterator($source) : $source;
        return new self($iterator, $fiberManager);
    }

    /**
     * Erstellt einen Stream aus einem Generator
     */
    public static function fromGenerator(callable $generator, FiberManager $fiberManager): self
    {
        return new self($generator(), $fiberManager);
    }

    /**
     * Erstellt einen Stream aus einem Range
     */
    public static function range(int $start, int $end, FiberManager $fiberManager): self
    {
        $generator = function () use ($start, $end) {
            for ($i = $start; $i <= $end; $i++) {
                yield $i;
            }
        };
        return new self($generator(), $fiberManager);
    }

    /**
     * Erstellt einen Stream aus einem AsyncChannel
     */
    public static function fromChannel(AsyncChannel $channel, FiberManager $fiberManager): self
    {
        $generator = function () use ($channel) {
            try {
                while (true) {
                    yield $channel->receive();
                }
            } catch (ChannelClosedException) {
                // Channel geschlossen, Stream endet
            }
        };
        return new self($generator(), $fiberManager);
    }

    /**
     * Wendet eine Transformation auf jedes Element an
     */
    public function map(callable $transformer): self
    {
        $generator = function () use ($transformer) {
            foreach ($this->source as $item) {
                yield $transformer($item);
            }
        };
        return new self($generator(), $this->fiberManager);
    }

    /**
     * Filtert Elemente basierend auf einem Prädikat
     */
    public function filter(callable $predicate): self
    {
        $generator = function () use ($predicate) {
            foreach ($this->source as $item) {
                if ($predicate($item)) {
                    yield $item;
                }
            }
        };
        return new self($generator(), $this->fiberManager);
    }

    /**
     * Nimmt nur die ersten N Elemente
     */
    public function take(int $count): self
    {
        $generator = function () use ($count) {
            $taken = 0;
            foreach ($this->source as $item) {
                if ($taken >= $count) {
                    break;
                }
                yield $item;
                $taken++;
            }
        };
        return new self($generator(), $this->fiberManager);
    }

    /**
     * Überspringt die ersten N Elemente
     */
    public function skip(int $count): self
    {
        $generator = function () use ($count) {
            $skipped = 0;
            foreach ($this->source as $item) {
                if ($skipped < $count) {
                    $skipped++;
                    continue;
                }
                yield $item;
            }
        };
        return new self($generator(), $this->fiberManager);
    }

    /**
     * Teilt Stream in Chunks fester Größe
     */
    public function chunk(int $size): self
    {
        $generator = function () use ($size) {
            $chunk = [];
            foreach ($this->source as $item) {
                $chunk[] = $item;
                if (count($chunk) >= $size) {
                    yield $chunk;
                    $chunk = [];
                }
            }
            // Letzter Chunk (falls nicht voll)
            if (!empty($chunk)) {
                yield $chunk;
            }
        };
        return new self($generator(), $this->fiberManager);
    }

    /**
     * Verarbeitet Stream parallel mit N Workers
     */
    public function parallel(int $workers): self
    {
        $generator = function () use ($workers) {
            $pool = new AsyncPool($this->fiberManager, maxConcurrency: $workers);
            $results = [];
            $index = 0;

            foreach ($this->source as $item) {
                $currentIndex = $index++;
                $pool->add(function () use ($item) {
                    return $item;
                }, (string) $currentIndex);
            }

            $processedResults = $pool->execute();

            // Sortiere Ergebnisse nach Original-Reihenfolge
            ksort($processedResults);

            foreach ($processedResults as $result) {
                yield $result;
            }
        };
        return new self($generator(), $this->fiberManager);
    }

    /**
     * Flat-mapped nested structures
     */
    public function flatMap(callable $transformer): self
    {
        $generator = function () use ($transformer) {
            foreach ($this->source as $item) {
                $transformed = $transformer($item);
                if (is_iterable($transformed)) {
                    foreach ($transformed as $subItem) {
                        yield $subItem;
                    }
                } else {
                    yield $transformed;
                }
            }
        };
        return new self($generator(), $this->fiberManager);
    }

    /**
     * Führt eine Aktion für jedes Element aus (Terminal Operation)
     */
    public function forEach(callable $action): void
    {
        foreach ($this->source as $item) {
            $action($item);
        }
    }

    /**
     * Reduziert den Stream auf einen einzelnen Wert (Terminal Operation)
     */
    public function reduce(callable $reducer, mixed $initial = null): mixed
    {
        $accumulator = $initial;
        foreach ($this->source as $item) {
            $accumulator = $reducer($accumulator, $item);
        }
        return $accumulator;
    }

    /**
     * Sammelt alle Elemente in ein Array (Terminal Operation)
     */
    public function toArray(): array
    {
        return iterator_to_array($this->source, false);
    }

    /**
     * Zählt die Anzahl der Elemente (Terminal Operation)
     */
    public function count(): int
    {
        $count = 0;
        foreach ($this->source as $_) {
            $count++;
        }
        return $count;
    }

    /**
     * Prüft ob mindestens ein Element das Prädikat erfüllt (Terminal Operation)
     */
    public function any(callable $predicate): bool
    {
        foreach ($this->source as $item) {
            if ($predicate($item)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Prüft ob alle Elemente das Prädikat erfüllen (Terminal Operation)
     */
    public function all(callable $predicate): bool
    {
        foreach ($this->source as $item) {
            if (!$predicate($item)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Findet das erste Element das Prädikat erfüllt (Terminal Operation)
     */
    public function first(?callable $predicate = null): mixed
    {
        foreach ($this->source as $item) {
            if ($predicate === null || $predicate($item)) {
                return $item;
            }
        }
        return null;
    }

    /**
     * Sendet alle Elemente in einen AsyncChannel (Terminal Operation)
     */
    public function toChannel(AsyncChannel $channel): void
    {
        foreach ($this->source as $item) {
            $channel->send($item);
        }
        $channel->close();
    }

    /**
     * Gruppiert Elemente nach Key-Funktion (Terminal Operation)
     */
    public function groupBy(callable $keySelector): array
    {
        $groups = [];
        foreach ($this->source as $item) {
            $key = $keySelector($item);
            if (!isset($groups[$key])) {
                $groups[$key] = [];
            }
            $groups[$key][] = $item;
        }
        return $groups;
    }

    /**
     * Sortiert Stream (Terminal Operation, lädt alle Elemente in Memory)
     */
    public function sorted(?callable $comparator = null): self
    {
        $items = $this->toArray();
        if ($comparator !== null) {
            usort($items, $comparator);
        } else {
            sort($items);
        }
        return self::from($items, $this->fiberManager);
    }

    /**
     * Entfernt Duplikate basierend auf Wert oder Key-Funktion
     */
    public function distinct(?callable $keySelector = null): self
    {
        $generator = function () use ($keySelector) {
            $seen = [];
            foreach ($this->source as $item) {
                $key = $keySelector !== null ? $keySelector($item) : $item;
                $keyString = is_scalar($key) ? (string) $key : serialize($key);

                if (!isset($seen[$keyString])) {
                    $seen[$keyString] = true;
                    yield $item;
                }
            }
        };
        return new self($generator(), $this->fiberManager);
    }

    /**
     * Führt eine Aktion für jedes Element aus ohne Stream zu ändern
     */
    public function tap(callable $action): self
    {
        $generator = function () use ($action) {
            foreach ($this->source as $item) {
                $action($item);
                yield $item;
            }
        };
        return new self($generator(), $this->fiberManager);
    }
}
