<?php

declare(strict_types=1);

namespace App\Framework\Performance\ValueObjects;

use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use Countable;
use InvalidArgumentException;
use IteratorAggregate;
use Traversable;

/**
 * Type-safe collection of performance measurements
 */
final class MeasurementCollection implements Countable, IteratorAggregate
{
    /** @var array<int, Measurement> */
    private array $measurements = [];

    public function __construct(array $measurements = [])
    {
        foreach ($measurements as $measurement) {
            $this->add($measurement);
        }
    }

    public function add(Measurement $measurement): void
    {
        $this->measurements[] = $measurement;
    }

    public function count(): int
    {
        return count($this->measurements);
    }

    public function isEmpty(): bool
    {
        return empty($this->measurements);
    }

    public function getIterator(): Traversable
    {
        yield from $this->measurements;
    }

    /**
     * @return array<int, Measurement>
     */
    public function toArray(): array
    {
        return $this->measurements;
    }

    public function getTotalDuration(): Duration
    {
        if ($this->isEmpty()) {
            return Duration::zero();
        }

        $total = Duration::zero();
        foreach ($this->measurements as $measurement) {
            $total = $total->add($measurement->getDuration());
        }

        return $total;
    }

    public function getAverageDuration(): Duration
    {
        if ($this->isEmpty()) {
            return Duration::zero();
        }

        return Duration::fromSeconds(
            $this->getTotalDuration()->toSeconds() / $this->count()
        );
    }

    public function getMinDuration(): ?Duration
    {
        if ($this->isEmpty()) {
            return null;
        }

        $min = null;
        foreach ($this->measurements as $measurement) {
            $duration = $measurement->getDuration();
            if ($min === null || $duration->lessThan($min)) {
                $min = $duration;
            }
        }

        return $min;
    }

    public function getMaxDuration(): ?Duration
    {
        if ($this->isEmpty()) {
            return null;
        }

        $max = null;
        foreach ($this->measurements as $measurement) {
            $duration = $measurement->getDuration();
            if ($max === null || $duration->greaterThan($max)) {
                $max = $duration;
            }
        }

        return $max;
    }

    public function getTotalMemory(): Byte
    {
        if ($this->isEmpty()) {
            return Byte::zero();
        }

        $total = Byte::zero();
        foreach ($this->measurements as $measurement) {
            $total = $total->add($measurement->getMemory());
        }

        return $total;
    }

    public function getAverageMemory(): Byte
    {
        if ($this->isEmpty()) {
            return Byte::zero();
        }

        return Byte::fromBytes(
            (int) round($this->getTotalMemory()->toBytes() / $this->count())
        );
    }

    public function getMinMemory(): ?Byte
    {
        if ($this->isEmpty()) {
            return null;
        }

        $min = null;
        foreach ($this->measurements as $measurement) {
            $memory = $measurement->getMemory();
            if ($min === null || $memory->lessThan($min)) {
                $min = $memory;
            }
        }

        return $min;
    }

    public function getMaxMemory(): ?Byte
    {
        if ($this->isEmpty()) {
            return null;
        }

        $max = null;
        foreach ($this->measurements as $measurement) {
            $memory = $measurement->getMemory();
            if ($max === null || $memory->greaterThan($max)) {
                $max = $memory;
            }
        }

        return $max;
    }

    public function getFirst(): ?Measurement
    {
        return $this->measurements[0] ?? null;
    }

    public function getLast(): ?Measurement
    {
        $count = $this->count();

        return $count > 0 ? $this->measurements[$count - 1] : null;
    }

    /**
     * Filter measurements by minimum duration
     */
    public function filterByMinDuration(Duration $minDuration): self
    {
        $filtered = array_filter(
            $this->measurements,
            fn (Measurement $m) => $m->getDuration()->greaterThan($minDuration) || $m->getDuration()->equals($minDuration)
        );

        return new self($filtered);
    }

    /**
     * Sort measurements by duration (slowest first)
     */
    public function sortByDuration(bool $ascending = false): self
    {
        $sorted = $this->measurements;

        usort($sorted, function (Measurement $a, Measurement $b) use ($ascending) {
            $result = $a->getDuration()->greaterThan($b->getDuration()) ? 1 : -1;

            return $ascending ? -$result : $result;
        });

        return new self($sorted);
    }

    /**
     * Get top N measurements by duration
     */
    public function getTopByDuration(int $limit): self
    {
        if ($limit <= 0) {
            throw new InvalidArgumentException('Limit must be positive');
        }

        $sorted = $this->sortByDuration();

        return new self(array_slice($sorted->toArray(), 0, $limit));
    }

    /**
     * Export to array format
     */
    public function export(): array
    {
        return [
            'count' => $this->count(),
            'total_duration_ms' => $this->getTotalDuration()->toMilliseconds(),
            'avg_duration_ms' => $this->getAverageDuration()->toMilliseconds(),
            'min_duration_ms' => $this->getMinDuration()?->toMilliseconds() ?? 0,
            'max_duration_ms' => $this->getMaxDuration()?->toMilliseconds() ?? 0,
            'total_memory_bytes' => $this->getTotalMemory()->toBytes(),
            'avg_memory_bytes' => $this->getAverageMemory()->toBytes(),
            'min_memory_bytes' => $this->getMinMemory()?->toBytes() ?? 0,
            'max_memory_bytes' => $this->getMaxMemory()?->toBytes() ?? 0,
            'measurements' => array_map(fn (Measurement $m) => $m->toArray(), $this->measurements),
        ];
    }
}
