<?php

declare(strict_types=1);

namespace KupShop\SynchronizationBundle\Synchronization;

class Job
{
    public string $id;
    public string $cronExpression = '*/20 * * * *';
    /** Max execution time in minutes. Defaults to 30 minutes. */
    public ?int $maxExecutionTime = 30;

    public function __construct(
        public string $name,
        public \Closure $fn,
        public int $priority = 0,
        public ?\Closure $truthTest = null,
        public ?string $description = null,
        public readonly bool $enableManualRun = true,
    ) {
        $this->id = md5($this->name);
    }

    public static function create(string $name, callable $fn, int $priority = 0, ?string $description = null, bool $enableManualRun = true): self
    {
        return new static($name, $fn, $priority, description: $description, enableManualRun: $enableManualRun);
    }

    /**
     * Sets max execution time of job in minutes.
     */
    public function setMaxExecutionTime(?int $maxExecutionTime): self
    {
        $this->maxExecutionTime = $maxExecutionTime;

        return $this;
    }

    public function when(callable $fn): self
    {
        $this->truthTest = $fn;

        return $this;
    }

    public function at($expression)
    {
        $this->cronExpression = $expression;

        return $this;
    }

    /**
     * Set the execution time to every minute.
     */
    public function everyMinute(?int $minute = null): self
    {
        $minuteExpression = '*';
        if ($minute !== null) {
            $c = $this->validateCronSequence($minute);
            $minuteExpression = '*/'.$c['minute'];
        }

        return $this->at($minuteExpression.' * * * *');
    }

    /**
     * Set the execution time to every hour.
     */
    public function hourly(int $minute = 0): self
    {
        $c = $this->validateCronSequence($minute);

        return $this->at("{$c['minute']} * * * *");
    }

    /**
     * Set the execution time to once a day.
     */
    public function daily(int $hour = 0, int $minute = 0): self
    {
        $c = $this->validateCronSequence($minute, $hour);

        return $this->at("{$c['minute']} {$c['hour']} * * *");
    }

    /**
     * Set the execution time to once a week.
     */
    public function weekly(int $weekday = 0, int $hour = 0, int $minute = 0): self
    {
        $c = $this->validateCronSequence($minute, $hour, null, null, $weekday);

        return $this->at("{$c['minute']} {$c['hour']} * * {$c['weekday']}");
    }

    /**
     * Set the execution time to once a month.
     */
    public function monthly(int|string $month = '*', int $day = 1, int $hour = 0, int $minute = 0): self
    {
        $c = $this->validateCronSequence($minute, $hour, $day, $month);

        return $this->at("{$c['minute']} {$c['hour']} {$c['day']} {$c['month']} *");
    }

    /**
     * Set the execution time to every Sunday.
     */
    public function sunday(int $hour = 0, int $minute = 0): self
    {
        return $this->weekly(0, $hour, $minute);
    }

    /**
     * Set the execution time to every Monday.
     */
    public function monday(int $hour = 0, int $minute = 0): self
    {
        return $this->weekly(1, $hour, $minute);
    }

    /**
     * Set the execution time to every Tuesday.
     */
    public function tuesday(int $hour = 0, int $minute = 0): self
    {
        return $this->weekly(2, $hour, $minute);
    }

    /**
     * Set the execution time to every Wednesday.
     */
    public function wednesday(int $hour = 0, int $minute = 0): self
    {
        return $this->weekly(3, $hour, $minute);
    }

    /**
     * Set the execution time to every Thursday.
     */
    public function thursday(int $hour = 0, int $minute = 0): self
    {
        return $this->weekly(4, $hour, $minute);
    }

    /**
     * Set the execution time to every Friday.
     */
    public function friday(int $hour = 0, int $minute = 0): self
    {
        return $this->weekly(5, $hour, $minute);
    }

    /**
     * Set the execution time to every Saturday.
     */
    public function saturday(int $hour = 0, int $minute = 0): self
    {
        return $this->weekly(6, $hour, $minute);
    }

    /**
     * Set the execution time to every January.
     */
    public function january(int $day = 1, int $hour = 0, int $minute = 0): self
    {
        return $this->monthly(1, $day, $hour, $minute);
    }

    /**
     * Set the execution time to every February.
     */
    public function february(int $day = 1, int $hour = 0, int $minute = 0): self
    {
        return $this->monthly(2, $day, $hour, $minute);
    }

    /**
     * Set the execution time to every March.
     */
    public function march(int $day = 1, int $hour = 0, int $minute = 0): self
    {
        return $this->monthly(3, $day, $hour, $minute);
    }

    /**
     * Set the execution time to every April.
     */
    public function april(int $day = 1, int $hour = 0, int $minute = 0): self
    {
        return $this->monthly(4, $day, $hour, $minute);
    }

    /**
     * Set the execution time to every May.
     */
    public function may(int $day = 1, int $hour = 0, int $minute = 0): self
    {
        return $this->monthly(5, $day, $hour, $minute);
    }

    /**
     * Set the execution time to every June.
     */
    public function june(int $day = 1, int $hour = 0, int $minute = 0): self
    {
        return $this->monthly(6, $day, $hour, $minute);
    }

    /**
     * Set the execution time to every July.
     */
    public function july(int $day = 1, int $hour = 0, int $minute = 0): self
    {
        return $this->monthly(7, $day, $hour, $minute);
    }

    /**
     * Set the execution time to every August.
     */
    public function august(int $day = 1, int $hour = 0, int $minute = 0): self
    {
        return $this->monthly(8, $day, $hour, $minute);
    }

    /**
     * Set the execution time to every September.
     */
    public function september(int $day = 1, int $hour = 0, int $minute = 0): self
    {
        return $this->monthly(9, $day, $hour, $minute);
    }

    /**
     * Set the execution time to every October.
     */
    public function october(int $day = 1, int $hour = 0, int $minute = 0): self
    {
        return $this->monthly(10, $day, $hour, $minute);
    }

    /**
     * Set the execution time to every November.
     */
    public function november(int $day = 1, int $hour = 0, int $minute = 0): self
    {
        return $this->monthly(11, $day, $hour, $minute);
    }

    /**
     * Set the execution time to every December.
     */
    public function december(int $day = 1, int $hour = 0, int $minute = 0): self
    {
        return $this->monthly(12, $day, $hour, $minute);
    }

    /**
     * Validate sequence of cron expression.
     */
    private function validateCronSequence(?int $minute = null, ?int $hour = null, ?int $day = null, int|string|null $month = null, ?int $weekday = null): array
    {
        return [
            'minute' => $this->validateCronRange($minute, 0, 59),
            'hour' => $this->validateCronRange($hour, 0, 23),
            'day' => $this->validateCronRange($day, 1, 31),
            'month' => $this->validateCronRange($month, 1, 12),
            'weekday' => $this->validateCronRange($weekday, 0, 6),
        ];
    }

    /**
     * Validate sequence of cron expression.
     */
    private function validateCronRange(int|string|null $value, int $min, int $max): int|string
    {
        if ($value === null || $value === '*') {
            return '*';
        }

        if (!is_numeric($value)
            || !($value >= $min && $value <= $max)
        ) {
            throw new \InvalidArgumentException(
                "Invalid value: it should be '*' or between {$min} and {$max}."
            );
        }

        return (int) $value;
    }
}
