<?php

namespace ipl\Web\FormElement;

use DateTime;
use InvalidArgumentException;
use ipl\Html\Attributes;
use ipl\Html\FormElement\FieldsetElement;
use ipl\Html\HtmlElement;
use ipl\Scheduler\Contract\Frequency;
use ipl\Scheduler\Cron;
use ipl\Scheduler\OneOff;
use ipl\Scheduler\RRule;
use ipl\Validator\BetweenValidator;
use ipl\Validator\CallbackValidator;
use ipl\Web\FormElement\ScheduleElement\AnnuallyFields;
use ipl\Web\FormElement\ScheduleElement\Common\FieldsProtector;
use ipl\Web\FormElement\ScheduleElement\MonthlyFields;
use ipl\Web\FormElement\ScheduleElement\WeeklyFields;
use LogicException;
use Psr\Http\Message\RequestInterface;

class ScheduleElement extends FieldsetElement
{
    use FieldsProtector;

    /** @var string Plain cron expressions */
    protected const CRON_EXPR = 'cron_expr';

    /** @var string Configure the individual expression parts manually */
    protected const CUSTOM_EXPR = 'custom';

    /** @var string Used to run a one-off task */
    protected const NO_REPEAT = 'none';

    protected $defaultAttributes = ['class' => 'schedule-element'];

    /** @var array A list of allowed frequencies used to configure custom expressions */
    protected $customFrequencies = [];

    /** @var array */
    protected $advanced = [];

    /** @var array */
    protected $regulars = [];

    /** @var string Schedule frequency of this element */
    protected $frequency = self::NO_REPEAT;

    /** @var string */
    protected $customFrequency;

    /** @var DateTime */
    protected $start;

    /** @var WeeklyFields Weekly parts of this schedule element */
    protected $weeklyField;

    /** @var MonthlyFields Monthly parts of this schedule element */
    protected $monthlyFields;

    /** @var AnnuallyFields Annually parts of this schedule element */
    protected $annuallyFields;

    protected function init(): void
    {
        $this->addDefaultPluginLoader('element', 'ipl\\Web\\FormElement\\ScheduleElement', '');

        $this->start = new DateTime();

        $this->regulars = [
            RRule::MINUTELY  => $this->translate('Minutely'),
            RRule::HOURLY    => $this->translate('Hourly'),
            RRule::DAILY     => $this->translate('Daily'),
            RRule::WEEKLY    => $this->translate('Weekly'),
            RRule::MONTHLY   => $this->translate('Monthly'),
            RRule::QUARTERLY => $this->translate('Quarterly'),
            RRule::YEARLY    => $this->translate('Annually'),
        ];

        $this->customFrequencies = array_slice($this->regulars, 2);
        unset($this->customFrequencies[RRule::QUARTERLY]);

        $this->advanced = [
            static::CUSTOM_EXPR => $this->translate('Custom…'),
            static::CRON_EXPR   => $this->translate('Cron Expression…')
        ];
    }

    /**
     * Create and return the weekly fields element
     *
     * @return WeeklyFields
     */
    protected function weeklyField(): WeeklyFields
    {
        if ($this->weeklyField === null) {
            $this->weeklyField = $this->createElement('weeklyFields', 'weekly-fields', [
                'default'   => $this->start->format('D'),
                'protector' => function (string $day) {
                    return $this->protectId($day);
                },
            ]);
        }

        return $this->weeklyField;
    }

    /**
     * Create and return the monthly fields element
     *
     * @return MonthlyFields
     */
    protected function monthlyFields(): MonthlyFields
    {
        if ($this->monthlyFields === null) {
            $this->monthlyFields = $this->createElement('monthlyFields', 'monthly-fields', [
                'default'         => $this->start->format('j'),
                'availableFields' => (int) $this->start->format('t'),
                'protector'       => function ($day) {
                    return $this->protectId($day);
                }
            ]);
        }

        return $this->monthlyFields;
    }

    /**
     * Create and return the annually fields element
     *
     * @return AnnuallyFields
     */
    protected function annuallyFields(): AnnuallyFields
    {
        if ($this->annuallyFields === null) {
            $this->annuallyFields = $this->createElement('annuallyFields', 'annually-fields', [
                'default'   => $this->start->format('M'),
                'protector' => function ($month) {
                    return $this->protectId($month);
                }
            ]);
        }

        return $this->annuallyFields;
    }

    /**
     * Get whether this element is rendering a cron expression
     *
     * @return bool
     */
    public function hasCronExpression(): bool
    {
        return $this->getFrequency() === static::CRON_EXPR;
    }

    /**
     * Get the frequency of this element
     *
     * @return string
     */
    public function getFrequency(): string
    {
        return $this->getPopulatedValue('frequency', $this->frequency);
    }

    /**
     * Set the custom frequency of this schedule element
     *
     * @param string $frequency
     *
     * @return $this
     */
    public function setFrequency(string $frequency): self
    {
        if (
            $frequency !== static::NO_REPEAT
            && ! isset($this->regulars[$frequency])
            && ! isset($this->advanced[$frequency])
        ) {
            throw new InvalidArgumentException(sprintf('Invalid frequency provided: %s', $frequency));
        }

        $this->frequency = $frequency;

        return $this;
    }

    /**
     * Get custom frequency of this element
     *
     * @return ?string
     */
    public function getCustomFrequency(): ?string
    {
        return $this->getValue('custom-frequency', $this->customFrequency);
    }

    /**
     * Set custom frequency of this element
     *
     * @param string $frequency
     *
     * @return $this
     */
    public function setCustomFrequency(string $frequency): self
    {
        if (! isset($this->customFrequencies[$frequency])) {
            throw new InvalidArgumentException(sprintf('Invalid custom frequency provided: %s', $frequency));
        }

        $this->customFrequency = $frequency;

        return $this;
    }

    /**
     * Set start time of the parsed expressions
     *
     * @param DateTime $start
     *
     * @return $this
     */
    public function setStart(DateTime $start): self
    {
        $this->start = $start;

        // Forward the start time update to the sub elements as well!
        $this->weeklyField()->setDefault($start->format('D'));
        $this->annuallyFields()->setDefault($start->format('M'));
        $this->monthlyFields()
            ->setDefault((int) $start->format('j'))
            ->setAvailableFields((int) $start->format('t'));

        return $this;
    }

    public function getValue($name = null, $default = null)
    {
        if ($name !== null || ! $this->hasBeenValidated()) {
            return parent::getValue($name, $default);
        }

        $frequency = $this->getFrequency();
        $start = parent::getValue('start');
        switch ($frequency) {
            case static::NO_REPEAT:
                return new OneOff($start);
            case static::CRON_EXPR:
                $rule = new Cron(parent::getValue('cron_expression'));

                break;
            case RRule::MINUTELY:
            case RRule::HOURLY:
            case RRule::DAILY:
            case RRule::WEEKLY:
            case RRule::MONTHLY:
            case RRule::QUARTERLY:
            case RRule::YEARLY:
                $rule = RRule::fromFrequency($frequency);

                break;
            default: // static::CUSTOM_EXPR
                $interval = parent::getValue('interval', 1);
                $customFrequency = parent::getValue('custom-frequency', RRule::DAILY);
                switch ($customFrequency) {
                    case RRule::DAILY:
                        if ($interval === '*') {
                            $interval = 1;
                        }

                        $rule = new RRule("FREQ=DAILY;INTERVAL=$interval");

                        break;
                    case RRule::WEEKLY:
                        $byDay = implode(',', $this->weeklyField()->getSelectedWeekDays());

                        $rule = new RRule("FREQ=WEEKLY;INTERVAL=$interval;BYDAY=$byDay");

                        break;
                    /** @noinspection PhpMissingBreakStatementInspection */
                    case RRule::MONTHLY:
                        $runsOn = $this->monthlyFields()->getValue('runsOn', MonthlyFields::RUNS_EACH);
                        if ($runsOn === MonthlyFields::RUNS_EACH) {
                            $byMonth = implode(',', $this->monthlyFields()->getSelectedDays());

                            $rule = new RRule("FREQ=MONTHLY;INTERVAL=$interval;BYMONTHDAY=$byMonth");

                            break;
                        }
                    // Fall-through to the next switch case
                    case RRule::YEARLY:
                        $rule = "FREQ=MONTHLY;INTERVAL=$interval;";
                        if ($customFrequency === RRule::YEARLY) {
                            $runsOn = $this->annuallyFields()->getValue('runsOnThe', 'n');
                            $month = $this->annuallyFields()->getValue('month', (int) $this->start->format('m'));
                            if (is_string($month)) {
                                $datetime = DateTime::createFromFormat('!M', $month);
                                if (! $datetime) {
                                    throw new InvalidArgumentException(sprintf('Invalid month provided: %s', $month));
                                }

                                $month = (int) $datetime->format('m');
                            }

                            $rule = "FREQ=YEARLY;INTERVAL=1;BYMONTH=$month;";
                            if ($runsOn === 'n') {
                                $rule = new RRule($rule);

                                break;
                            }
                        }

                        $element = $this->monthlyFields();
                        if ($customFrequency === RRule::YEARLY) {
                            $element = $this->annuallyFields();
                        }

                        $runDay = $element->getValue('day', $element::$everyDay);
                        $ordinal = $element->getValue('ordinal', $element::$first);
                        $position = $element->getOrdinalAsInteger($ordinal);

                        if ($runDay === $element::$everyDay) {
                            $rule .= "BYDAY=MO,TU,WE,TH,FR,SA,SU;BYSETPOS=$position";
                        } elseif ($runDay === $element::$everyWeekday) {
                            $rule .= "BYDAY=MO,TU,WE,TH,FR;BYSETPOS=$position";
                        } elseif ($runDay === $element::$everyWeekend) {
                            $rule .= "BYDAY=SA,SU;BYSETPOS=$position";
                        } else {
                            $rule .= sprintf('BYDAY=%d%s', $position, $runDay);
                        }

                        $rule = new RRule($rule);

                        break;
                    default:
                        throw new LogicException(sprintf('Custom frequency %s is not supported!', $customFrequency));
                }
        }

        $rule->startAt($start);
        if (parent::getValue('use-end-time', 'n') === 'y') {
            $rule->endAt(parent::getValue('end'));
        }

        // Sync the start time and first recurrence of the rule
        if (! $this->hasCronExpression() && $this->getFrequency() !== static::NO_REPEAT) {
            $nextDue = $rule->getNextRecurrences($start)->current() ?? $start;
            $rule->startAt($nextDue);
        }

        return $rule;
    }

    public function setValue($value)
    {
        $values = $value;
        $rule = $value;
        if ($rule instanceof Frequency) {
            if ($rule->getStart()) {
                $this->setStart($rule->getStart());
            }

            $values = [];
            if ($rule->getEnd() && ! $rule instanceof OneOff) {
                $values['use-end-time'] = 'y';
                $values['end'] = $rule->getEnd();
            }

            if ($rule instanceof OneOff) {
                $values['frequency'] = static::NO_REPEAT;
            } elseif ($rule instanceof Cron) {
                $values['cron_expression'] = $rule->getExpression();
                $values['frequency'] = static::CRON_EXPR;

                $this->setFrequency(static::CRON_EXPR);
            } elseif ($rule instanceof RRule) {
                $values['interval'] = $rule->getInterval();
                switch ($rule->getFrequency()) {
                    case RRule::DAILY:
                        if ($rule->getInterval() <= 1 && strpos($rule->getString(), 'INTERVAL=') === false) {
                            $this->setFrequency(RRule::DAILY);
                        } else {
                            $this
                                ->setFrequency(static::CUSTOM_EXPR)
                                ->setCustomFrequency(RRule::DAILY);
                        }

                        break;
                    case RRule::WEEKLY:
                        if (! $rule->getByDay() || empty($rule->getByDay())) {
                            $this->setFrequency(RRule::WEEKLY);
                        } else {
                            $values['weekly-fields'] = $this->weeklyField()->loadWeekDays($rule->getByDay());
                            $this
                                ->setFrequency(static::CUSTOM_EXPR)
                                ->setCustomFrequency(RRule::WEEKLY);
                        }

                        break;
                    case RRule::MONTHLY:
                    case RRule::YEARLY:
                        $isMonthly = $rule->getFrequency() === RRule::MONTHLY;
                        if ($rule->getByDay() || $rule->getByMonthDay() || $rule->getByMonth()) {
                            $this->setFrequency(static::CUSTOM_EXPR);

                            if ($isMonthly) {
                                $values['monthly-fields'] = $this->monthlyFields()->loadRRule($rule);
                                $this->setCustomFrequency(RRule::MONTHLY);
                            } else {
                                $values['annually-fields'] = $this->annuallyFields()->loadRRule($rule);
                                $this->setCustomFrequency(RRule::YEARLY);
                            }
                        } elseif ($isMonthly && $rule->getInterval() === 3) {
                            $this->setFrequency(RRule::QUARTERLY);
                        } else {
                            $this->setFrequency($rule->getFrequency());
                        }

                        break;
                    default:
                        $this->setFrequency($rule->getFrequency());
                }

                $values['frequency'] = $this->getFrequency();
                $values['custom-frequency'] = $this->getCustomFrequency();
            }
        }

        return parent::setValue($values);
    }

    protected function assemble()
    {
        $start = $this->getPopulatedValue('start') ?: $this->start;
        if (! $start instanceof DateTime) {
            $start = new DateTime($start);
        }
        $this->setStart($start);

        $autosubmit = ! $this->hasCronExpression() && $this->getFrequency() !== static::NO_REPEAT;
        $this->addElement('localDateTime', 'start', [
            'class'       => $autosubmit ? 'autosubmit' : null,
            'required'    => true,
            'label'       => $this->translate('Start'),
            'value'       => $start,
            'description' => $this->translate('Start time of this schedule')
        ]);

        $this->addElement('checkbox', 'use-end-time', [
            'required' => false,
            'class'    => 'autosubmit',
            'disabled' => $this->getPopulatedValue('frequency', static::NO_REPEAT) === static::NO_REPEAT ?: null,
            'value'    => $this->getPopulatedValue('use-end-time', 'n'),
            'label'    => $this->translate('Use End Time')
        ]);

        if ($this->getPopulatedValue('use-end-time', 'n') === 'y') {
            $end = $this->getPopulatedValue('end', new DateTime());
            if (! $end instanceof DateTime) {
                $end = new DateTime($end);
            }

            $this->addElement('localDateTime', 'end', [
                'class'       => ! $this->hasCronExpression() ? 'autosubmit' : null,
                'required'    => true,
                'value'       => $end,
                'label'       => $this->translate('End'),
                'description' => $this->translate('End time of this schedule')
            ]);
        }

        $this->addElement('select', 'frequency', [
            'required'    => false,
            'class'       => 'autosubmit',
            'label'       => $this->translate('Frequency'),
            'description' => $this->translate('Specifies how often this job run should be recurring'),
            'options'     => [
                static::NO_REPEAT            => $this->translate('None'),
                $this->translate('Regular')  => $this->regulars,
                $this->translate('Advanced') => $this->advanced
            ],
        ]);

        if ($this->getFrequency() === static::CUSTOM_EXPR) {
            $this->addElement('select', 'custom-frequency', [
                'required'    => false,
                'class'       => 'autosubmit',
                'value'       => parent::getValue('custom-frequency'),
                'options'     => $this->customFrequencies,
                'label'       => $this->translate('Custom Frequency'),
                'description' => $this->translate('Specifies how often this job run should be recurring')
            ]);

            switch (parent::getValue('custom-frequency', RRule::DAILY)) {
                case RRule::DAILY:
                    $this->assembleCommonElements();

                    break;
                case RRule::WEEKLY:
                    $this->assembleCommonElements();
                    $this->addElement($this->weeklyField());

                    break;
                case RRule::MONTHLY:
                    $this->assembleCommonElements();
                    $this->addElement($this->monthlyFields());

                    break;
                case RRule::YEARLY:
                    $this->addElement($this->annuallyFields());
            }
        } elseif ($this->hasCronExpression()) {
            $this->addElement('text', 'cron_expression', [
                'required'    => true,
                'label'       => $this->translate('Cron Expression'),
                'description' => $this->translate('Job cron Schedule'),
                'validators' => [
                    new CallbackValidator(function ($value, CallbackValidator $validator) {
                        if ($value && ! Cron::isValid($value)) {
                            $validator->addMessage($this->translate('Invalid CRON expression'));

                            return false;
                        }

                        return true;
                    })
                ]
            ]);
        }

        if ($this->getFrequency() !== static::NO_REPEAT && ! $this->hasCronExpression()) {
            $this->addElement(
                'recurrence',
                'schedule-recurrences',
                [
                    'id'        => $this->protectId('schedule-recurrences'),
                    'label'     => $this->translate('Next occurrences'),
                    'validate'  => function (): array {
                        $isValid = $this->isValid();
                        $reason = null;
                        if (! $isValid && $this->getFrequency() === static::CUSTOM_EXPR) {
                            if (
                                $this->getCustomFrequency() !== RRule::YEARLY
                                && ! $this->getElement('interval')->isValid()
                            ) {
                                $reason = current($this->getElement('interval')->getMessages());
                            } else {
                                $frequency = $this->getCustomFrequency();
                                switch ($frequency) {
                                    case RRule::WEEKLY:
                                        $reason = current($this->weeklyField()->getMessages());

                                        break;
                                    case RRule::MONTHLY:
                                        $reason = current($this->monthlyFields()->getMessages());

                                        break;
                                    default: // annually
                                        $reason = current($this->annuallyFields()->getMessages());

                                        break;
                                }
                            }
                        }

                        return [$isValid, $reason];
                    },
                    'frequency' => function (): Frequency {
                        if ($this->getFrequency() === static::CUSTOM_EXPR) {
                            $rule = $this->getValue();
                        } else {
                            $rule = RRule::fromFrequency($this->getFrequency());
                        }

                        $now = new DateTime();
                        $start = $this->getValue('start');
                        if ($start < $now) {
                            $now->setTime($start->format('H'), $start->format('i'), $start->format('s'));
                            $start = $now;
                        }

                        $rule->startAt($start);
                        if ($this->getPopulatedValue('use-end-time') === 'y') {
                            $rule->endAt($this->getValue('end'));
                        }

                        return $rule;
                    }
                ]
            );
        }
    }

    /**
     * Assemble common parts for all the frequencies
     */
    private function assembleCommonElements(): void
    {
        $repeat = $this->getCustomFrequency();
        if ($repeat === RRule::WEEKLY) {
            $text = $this->translate('week(s) on');
            $max = 53;
        } elseif ($repeat === RRule::MONTHLY) {
            $text = $this->translate('month(s)');
            $max = 12;
        } else {
            $text = $this->translate('day(s)');
            $max = 31;
        }

        $options = ['min' => 1, 'max' => $max];
        $this->addElement('number', 'interval', [
            'class'      => 'autosubmit',
            'value'      => 1,
            'min'        => 1,
            'max'        => $max,
            'validators' => [new BetweenValidator($options)]
        ]);

        $numberSpecifier = HtmlElement::create('div', ['class' => 'number-specifier']);
        $element = $this->getElement('interval');
        $element->prependWrapper($numberSpecifier);

        $numberSpecifier->prependHtml(HtmlElement::create('span', null, $this->translate('Every')));
        $numberSpecifier->addHtml($element);
        $numberSpecifier->addHtml(HtmlElement::create('span', null, $text));
    }

    /**
     * Get prepared multipart updates
     *
     * @param RequestInterface $request
     *
     * @return array
     */
    public function prepareMultipartUpdate(RequestInterface $request): array
    {
        $autoSubmittedBy = $request->getHeader('X-Icinga-AutoSubmittedBy');
        $pattern = '/\[(weekly-fields|monthly-fields|annually-fields)]\[(ordinal|month|day(\d+)?|[A-Z]{2})]$/';

        $partUpdates = [];
        if (
            $autoSubmittedBy
            && (
                preg_match('/\[(start|end)]$/', $autoSubmittedBy[0], $matches)
                || preg_match($pattern, $autoSubmittedBy[0])
                || preg_match('/\[interval]/', $autoSubmittedBy[0])
            )
        ) {
            $this->ensureAssembled();

            $partUpdates[] = $this->getElement('schedule-recurrences');
            if (
                $this->getFrequency() === static::CUSTOM_EXPR
                && $this->getCustomFrequency() === RRule::MONTHLY
                && isset($matches[1])
                && $matches[1] === 'start'
            ) {
                // To update the available fields/days based on the provided start time
                $partUpdates[] = $this->monthlyFields();
            }
        }

        return $partUpdates;
    }

    protected function registerAttributeCallbacks(Attributes $attributes)
    {
        parent::registerAttributeCallbacks($attributes);

        $attributes->registerAttributeCallback('protector', null, [$this, 'setIdProtector']);
    }
}
