<?php

declare(strict_types=1);

namespace App\Framework\Scheduler\ValueObjects;

/**
 * Value Object for cron expressions with validation
 */
final readonly class CronExpression
{
    public function __construct(
        public string $expression
    ) {
        $this->validate();
    }

    public static function fromString(string $expression): self
    {
        return new self(trim($expression));
    }

    public static function everyMinute(): self
    {
        return new self('* * * * *');
    }

    public static function hourly(): self
    {
        return new self('0 * * * *');
    }

    public static function daily(): self
    {
        return new self('0 0 * * *');
    }

    public static function weekly(): self
    {
        return new self('0 0 * * 0');
    }

    public static function monthly(): self
    {
        return new self('0 0 1 * *');
    }

    public static function yearly(): self
    {
        return new self('0 0 1 1 *');
    }

    public static function at(int $hour, int $minute = 0): self
    {
        if ($hour < 0 || $hour > 23) {
            throw new \InvalidArgumentException('Hour must be between 0 and 23');
        }
        if ($minute < 0 || $minute > 59) {
            throw new \InvalidArgumentException('Minute must be between 0 and 59');
        }

        return new self("{$minute} {$hour} * * *");
    }

    public static function weekdaysAt(int $hour, int $minute = 0): self
    {
        if ($hour < 0 || $hour > 23) {
            throw new \InvalidArgumentException('Hour must be between 0 and 23');
        }
        if ($minute < 0 || $minute > 59) {
            throw new \InvalidArgumentException('Minute must be between 0 and 59');
        }

        return new self("{$minute} {$hour} * * 1-5");
    }

    public function toString(): string
    {
        return $this->expression;
    }

    public function equals(self $other): bool
    {
        return $this->expression === $other->expression;
    }

    public function getParts(): array
    {
        return explode(' ', $this->expression);
    }

    public function getMinute(): string
    {
        return $this->getParts()[0];
    }

    public function getHour(): string
    {
        return $this->getParts()[1];
    }

    public function getDayOfMonth(): string
    {
        return $this->getParts()[2];
    }

    public function getMonth(): string
    {
        return $this->getParts()[3];
    }

    public function getDayOfWeek(): string
    {
        return $this->getParts()[4];
    }

    public function getDescription(): string
    {
        return match ($this->expression) {
            '* * * * *' => 'Every minute',
            '0 * * * *' => 'Every hour',
            '0 0 * * *' => 'Daily at midnight',
            '0 0 * * 0' => 'Weekly on Sunday at midnight',
            '0 0 1 * *' => 'Monthly on the 1st at midnight',
            '0 0 1 1 *' => 'Yearly on January 1st at midnight',
            default => "Cron: {$this->expression}"
        };
    }

    private function validate(): void
    {
        if (empty($this->expression)) {
            throw new \InvalidArgumentException('Cron expression cannot be empty');
        }

        $parts = explode(' ', $this->expression);

        if (count($parts) !== 5) {
            throw new \InvalidArgumentException(
                'Cron expression must have exactly 5 parts (minute hour day month weekday)'
            );
        }

        [$minute, $hour, $day, $month, $weekday] = $parts;

        $this->validateField($minute, 0, 59, 'minute');
        $this->validateField($hour, 0, 23, 'hour');
        $this->validateField($day, 1, 31, 'day');
        $this->validateField($month, 1, 12, 'month');
        $this->validateField($weekday, 0, 7, 'weekday'); // 0 and 7 both represent Sunday
    }

    private function validateField(string $field, int $min, int $max, string $fieldName): void
    {
        // Allow wildcards
        if ($field === '*') {
            return;
        }

        // Handle ranges (e.g., 1-5)
        if (str_contains($field, '-')) {
            $range = explode('-', $field);
            if (count($range) !== 2) {
                throw new \InvalidArgumentException("Invalid range in {$fieldName}: {$field}");
            }

            $start = (int) $range[0];
            $end = (int) $range[1];

            if ($start < $min || $start > $max || $end < $min || $end > $max || $start > $end) {
                throw new \InvalidArgumentException("Invalid range in {$fieldName}: {$field}");
            }

            return;
        }

        // Handle step values (e.g., */5)
        if (str_contains($field, '/')) {
            $step = explode('/', $field);
            if (count($step) !== 2) {
                throw new \InvalidArgumentException("Invalid step in {$fieldName}: {$field}");
            }

            $stepValue = (int) $step[1];
            if ($stepValue <= 0) {
                throw new \InvalidArgumentException("Step value must be positive in {$fieldName}: {$field}");
            }

            // Validate base (e.g., the '*' in '*/5')
            if ($step[0] !== '*') {
                $this->validateField($step[0], $min, $max, $fieldName);
            }

            return;
        }

        // Handle comma-separated values (e.g., 1,3,5)
        if (str_contains($field, ',')) {
            $values = explode(',', $field);
            foreach ($values as $value) {
                $this->validateField(trim($value), $min, $max, $fieldName);
            }

            return;
        }

        // Single numeric value
        if (! is_numeric($field)) {
            throw new \InvalidArgumentException("Invalid {$fieldName} value: {$field}");
        }

        $value = (int) $field;
        if ($value < $min || $value > $max) {
            throw new \InvalidArgumentException(
                "{$fieldName} must be between {$min} and {$max}, got {$value}"
            );
        }

        // Special case for weekday: both 0 and 7 represent Sunday
        if ($fieldName === 'weekday' && $value === 7) {
            return; // 7 is valid for Sunday
        }
    }
}
