<?php

declare(strict_types=1);

namespace App\Framework\Core\ValueObjects;

use InvalidArgumentException;

/**
 * Represents a normalized score value between 0.0 and 1.0
 */
final readonly class Score
{
    public function __construct(
        private float $value,
        private float $min = 0.0,
        private float $max = 1.0
    ) {
        $this->validate();
    }

    /**
     * Create score from percentage
     */
    public static function fromPercentage(Percentage $percentage): self
    {
        return new self($percentage->toDecimal());
    }

    /**
     * Create score from ratio
     */
    public static function fromRatio(int $numerator, int $denominator): self
    {
        if ($denominator <= 0) {
            throw new InvalidArgumentException('Denominator must be greater than 0');
        }

        if ($numerator < 0) {
            throw new InvalidArgumentException('Numerator must be non-negative');
        }

        return new self(min(1.0, $numerator / $denominator));
    }

    /**
     * Create critical level score (0.9)
     */
    public static function critical(): self
    {
        return new self(0.9);
    }

    /**
     * Create high level score (0.7)
     */
    public static function high(): self
    {
        return new self(0.7);
    }

    /**
     * Create medium level score (0.5)
     */
    public static function medium(): self
    {
        return new self(0.5);
    }

    /**
     * Create low level score (0.2)
     */
    public static function low(): self
    {
        return new self(0.2);
    }

    /**
     * Create minimum score (0.0)
     */
    public static function zero(): self
    {
        return new self(0.0);
    }

    /**
     * Create maximum score (1.0)
     */
    public static function max(): self
    {
        return new self(1.0);
    }

    /**
     * Get the raw score value
     */
    public function value(): float
    {
        return $this->value;
    }

    /**
     * Get score as decimal value (alias for value())
     */
    public function toDecimal(): float
    {
        return $this->value;
    }

    /**
     * Convert to percentage using framework Percentage object
     */
    public function toPercentage(): Percentage
    {
        return Percentage::fromDecimal($this->value);
    }

    /**
     * Get score level using ScoreLevel enum
     */
    public function toLevel(): ScoreLevel
    {
        return ScoreLevel::fromScore($this->value);
    }

    /**
     * Check if score is at or above a certain level
     */
    public function isAtLevel(ScoreLevel $level): bool
    {
        return $this->value >= $level->getThreshold();
    }

    /**
     * Check if score is critical level (>= 0.9)
     */
    public function isCritical(): bool
    {
        return $this->isAtLevel(ScoreLevel::CRITICAL);
    }

    /**
     * Check if score is high level (>= 0.7)
     */
    public function isHigh(): bool
    {
        return $this->isAtLevel(ScoreLevel::HIGH);
    }

    /**
     * Check if score is medium level (>= 0.3)
     */
    public function isMedium(): bool
    {
        return $this->isAtLevel(ScoreLevel::MEDIUM);
    }

    /**
     * Check if score is low level (< 0.3)
     */
    public function isLow(): bool
    {
        return ! $this->isAtLevel(ScoreLevel::MEDIUM);
    }

    /**
     * Combine with another score using weighted average
     */
    public function combine(Score $other, float $weight = 0.5): self
    {
        if ($weight < 0.0 || $weight > 1.0) {
            throw new InvalidArgumentException('Weight must be between 0.0 and 1.0');
        }

        $combinedValue = ($this->value * $weight) + ($other->value * (1.0 - $weight));

        return new self($combinedValue);
    }

    /**
     * Add another score (clamped to max)
     */
    public function add(Score $other): self
    {
        return new self(min(1.0, $this->value + $other->value));
    }

    /**
     * Multiply by factor (clamped to max)
     */
    public function multiply(float $factor): self
    {
        if ($factor < 0.0) {
            throw new InvalidArgumentException('Factor must be non-negative');
        }

        return new self(min(1.0, $this->value * $factor));
    }

    /**
     * Check if score is above threshold
     */
    public function isAbove(Score $threshold): bool
    {
        return $this->value > $threshold->value;
    }

    /**
     * Check if score is below threshold
     */
    public function isBelow(Score $threshold): bool
    {
        return $this->value < $threshold->value;
    }

    /**
     * Get inverted score (1.0 - value)
     */
    public function invert(): self
    {
        return new self(1.0 - $this->value);
    }

    /**
     * Normalize to custom range
     */
    public function normalize(float $newMin, float $newMax): float
    {
        if ($newMin >= $newMax) {
            throw new InvalidArgumentException('New minimum must be less than new maximum');
        }

        return $newMin + ($this->value * ($newMax - $newMin));
    }

    /**
     * Convert to array for serialization
     */
    public function toArray(): array
    {
        return [
            'value' => $this->value,
            'percentage' => $this->toPercentage()->getValue(),
            'level' => $this->toLevel()->value,
            'min' => $this->min,
            'max' => $this->max,
        ];
    }

    /**
     * Create from array
     */
    public static function fromArray(array $data): self
    {
        return new self(
            value: $data['value'],
            min: $data['min'] ?? 0.0,
            max: $data['max'] ?? 1.0
        );
    }

    /**
     * String representation
     */
    public function toString(): string
    {
        return sprintf('%.3f (%s)', $this->value, $this->toLevel()->value);
    }

    /**
     * Validate score value
     */
    private function validate(): void
    {
        if ($this->min >= $this->max) {
            throw new InvalidArgumentException('Minimum must be less than maximum');
        }

        if ($this->value < $this->min || $this->value > $this->max) {
            throw new InvalidArgumentException(
                sprintf('Score value %.3f must be between %.3f and %.3f', $this->value, $this->min, $this->max)
            );
        }
    }

    /**
     * Calculate weighted average of multiple scores
     */
    public static function weightedAverage(array $scores, array $weights = []): self
    {
        if (empty($scores)) {
            throw new InvalidArgumentException('Scores array cannot be empty');
        }

        if (! empty($weights) && count($scores) !== count($weights)) {
            throw new InvalidArgumentException('Scores and weights arrays must have the same length');
        }

        // Use equal weights if no weights provided
        if (empty($weights)) {
            $weights = array_fill(0, count($scores), 1.0 / count($scores));
        }

        // Normalize weights to sum to 1.0
        $totalWeight = array_sum($weights);
        if ($totalWeight <= 0.0) {
            throw new InvalidArgumentException('Total weight must be greater than 0');
        }

        $normalizedWeights = array_map(fn ($w) => $w / $totalWeight, $weights);

        $weightedSum = 0.0;
        foreach ($scores as $index => $score) {
            if (! $score instanceof Score) {
                throw new InvalidArgumentException('All items must be Score instances');
            }
            $weightedSum += $score->value * $normalizedWeights[$index];
        }

        return new self($weightedSum);
    }
}
