<?php

/**
 * ---------------------------------------------------------------------
 *
 * GLPI - Gestionnaire Libre de Parc Informatique
 *
 * http://glpi-project.org
 *
 * @copyright 2015-2024 Teclib' and contributors.
 * @copyright 2003-2014 by the INDEPNET Development Team.
 * @licence   https://www.gnu.org/licenses/gpl-3.0.html
 *
 * ---------------------------------------------------------------------
 *
 * LICENSE
 *
 * This file is part of GLPI.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * ---------------------------------------------------------------------
 */

/**
 * Base class for recurrent tickets and changes
 *
 * @since 10.0.0
 */
abstract class CommonITILRecurrent extends CommonDropdown
{
    use Glpi\Features\Clonable;

    /**
     * @var bool From CommonDBTM
     */
    public $dohistory = true;

    /**
     * @var string From CommonDropdown
     */
    public $first_level_menu = "helpdesk";

    /**
     * @var bool From CommonDropdown
     */
    public $display_dropdowntitle = false;

    /**
     * @var bool From CommonDropdown
     */
    public $can_be_translated = false;

    /**
     * Concrete items to be instanciated
     */
    abstract public static function getConcreteClass();

    /**
     * Template class to use to create the concrete items
     */
    abstract public static function getTemplateClass();

    /**
     * Predefined field class to use to set the concrete items's data
     */
    abstract public static function getPredefinedFieldsClass();

    public static function displayTabContentForItem(
        CommonGLPI $item,
        $tabnum = 1,
        $withtemplate = 0
    ) {
       // Tabs on CommonITILRecurrent items
        if ($item instanceof self) {
            switch ($tabnum) {
                // First tab : display next creation date
                case 1:
                    $item->showInfos();
                    return true;
            }
        }

        return false;
    }

    public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0)
    {
       // Only display tab if user can read ITILTemplates
        if (!Session::haveRight('itiltemplate', READ)) {
            return '';
        }

       // Tabs on CommonITILRecurrent items
        if ($item instanceof self) {
            $ong = [];
            $ong[1] = _n('Information', 'Information', Session::getPluralNumber());
            return $ong;
        }

        return '';
    }

    public function getCloneRelations(): array
    {
        return [];
    }

    public function defineTabs($options = [])
    {
        $ong = [];
        $this->addDefaultFormTab($ong);
        $this->addStandardTab(static::class, $ong, $options);
        $this->addStandardTab('Log', $ong, $options);

        return $ong;
    }

    public function prepareInputForAdd($input)
    {
        if (isset($input['periodicity'])) {
            $input['next_creation_date'] = $this->computeNextCreationDate(
                $input['begin_date'],
                $input['end_date'],
                $input['periodicity'],
                $input['create_before'],
                $input['calendars_id']
            );
        }

        return $input;
    }

    public function prepareInputForUpdate($input)
    {
        if (
            isset($input['begin_date'])
            && isset($input['periodicity'])
            && isset($input['create_before'])
        ) {
            $input['next_creation_date'] = $this->computeNextCreationDate(
                $input['begin_date'],
                $input['end_date'],
                $input['periodicity'],
                $input['create_before'],
                $input['calendars_id']
            );
        }

        return $input;
    }

    public function getAdditionalFields()
    {
        return [
            [
                'name'  => 'is_active',
                'label' => __('Active'),
                'type'  => 'bool',
                'list'  => false
            ],
            [
                'name'  => static::getTemplateClass()::getForeignKeyField(),
                'label' => static::getTemplateClass()::getTypeName(1),
                'type'  => 'dropdownValue',
                'list'  => true
            ],
            [
                'name'  => 'begin_date',
                'label' => __('Start date'),
                'type'  => 'datetime',
                'list'  => false
            ],
            [
                'name'  => 'end_date',
                'label' => __('End date'),
                'type'  => 'datetime',
                'list'  => false
            ],
            [
                'name'  => 'periodicity',
                'label' => __('Periodicity'),
                'type'  => 'specific_timestamp',
                'min'   => DAY_TIMESTAMP,
                'step'  => DAY_TIMESTAMP,
                'max'   => 2 * MONTH_TIMESTAMP
            ],
            [
                'name'  => 'create_before',
                'label' => __('Preliminary creation'),
                'type'  => 'timestamp',
                'max'   => 2 * WEEK_TIMESTAMP,
                'step'  => HOUR_TIMESTAMP
            ],
            [
                'name'  => 'calendars_id',
                'label' => _n('Calendar', 'Calendars', 1),
                'type'  => 'dropdownValue',
                'list'  => true
            ],
        ];
    }

    public function displaySpecificTypeField($ID, $field = [], array $options = [])
    {
        switch ($field['name']) {
            case 'periodicity':
                $this->displayPeriodicityInput();
                break;
        }
    }

    public static function getSpecificValueToDisplay(
        $field,
        $values,
        array $options = []
    ) {
        if (!is_array($values)) {
            $values = [$field => $values];
        }

        switch ($field) {
            case 'periodicity':
                if (preg_match('/([0-9]+)MONTH/', $values[$field], $matches)) {
                    return sprintf(_n('%d month', '%d months', $matches[1]), $matches[1]);
                }
                if (preg_match('/([0-9]+)YEAR/', $values[$field], $matches)) {
                    return sprintf(_n('%d year', '%d years', $matches[1]), $matches[1]);
                }
                return Html::timestampToString($values[$field], false);
            break;
        }

        return parent::getSpecificValueToDisplay($field, $values, $options);
    }

    /**
     * Display periodicity field
     * The displayed dropdown offer the following options:
     *    1 to 24 hours
     *    1 to 30 days
     *    1 to 11 months
     *    1 to 10 years
     */
    public function displayPeriodicityInput(): void
    {
        $possible_values = [];

       // Hours
        for ($i = 1; $i < 24; $i++) {
            $possible_values[$i * HOUR_TIMESTAMP] = sprintf(_n('%d hour', '%d hours', $i), $i);
        }

       // Days
        for ($i = 1; $i <= 30; $i++) {
            $possible_values[$i * DAY_TIMESTAMP] = sprintf(_n('%d day', '%d days', $i), $i);
        }

       // Months
        for ($i = 1; $i < 12; $i++) {
            $possible_values[$i . 'MONTH'] = sprintf(_n('%d month', '%d months', $i), $i);
        }

       // Years
        for ($i = 1; $i < 11; $i++) {
            $possible_values[$i . 'YEAR'] = sprintf(_n('%d year', '%d years', $i), $i);
        }

        Dropdown::showFromArray('periodicity', $possible_values, [
            'value' => $this->fields['periodicity']
        ]);
    }

    public function rawSearchOptions()
    {
        $tab = parent::rawSearchOptions();

        $tab[] = [
            'id'       => '11',
            'table'    => $this->getTable(),
            'field'    => 'is_active',
            'name'     => __('Active'),
            'datatype' => 'bool'
        ];

        $tab[] = [
            'id'       => '12',
            'table'    => static::getTemplateClass()::getTable(),
            'field'    => 'name',
            'name'     => static::getTemplateClass()::getTypeName(1),
            'datatype' => 'itemlink'
        ];

        $tab[] = [
            'id'       => '13',
            'table'    => $this->getTable(),
            'field'    => 'begin_date',
            'name'     => __('Start date'),
            'datatype' => 'datetime'
        ];

        $tab[] = [
            'id'       => '17',
            'table'    => $this->getTable(),
            'field'    => 'end_date',
            'name'     => __('End date'),
            'datatype' => 'datetime'
        ];

        $tab[] = [
            'id'       => '15',
            'table'    => $this->getTable(),
            'field'    => 'periodicity',
            'name'     => __('Periodicity'),
            'datatype' => 'specific'
        ];

        $tab[] = [
            'id'       => '14',
            'table'    => $this->getTable(),
            'field'    => 'create_before',
            'name'     => __('Preliminary creation'),
            'datatype' => 'timestamp'
        ];

        $tab[] = [
            'id'       => '18',
            'table'    => 'glpi_calendars',
            'field'    => 'name',
            'name'     => _n('Calendar', 'Calendars', 1),
            'datatype' => 'itemlink'
        ];

        return $tab;
    }

    /**
     * Show next creation date
     */
    public function showInfos(): void
    {
        if (!is_null($this->fields['next_creation_date'])) {
            echo "<div class='center'>";
           //TRANS: %s is the date of next creation
            echo sprintf(
                __('Next creation on %s'),
                Html::convDateTime($this->fields['next_creation_date'])
            );
            echo "</div>";
        }
    }

    /**
     * Compute next creation date of an item.
     *
     * @param string         $begin_date     Begin date of the recurrent item in 'Y-m-d H:i:s' format.
     * @param string         $end_date       End date of the recurrent item in 'Y-m-d H:i:s' format,
     *                                       or 'NULL' or empty value.
     * @param string|integer $periodicity    Periodicity of creation, could be:
     *                                        - an integer corresponding to seconds,
     *                                        - a string using "/([0-9]+)(MONTH|YEAR)/" pattern.
     * @param int            $create_before  Anticipated creation delay in seconds.
     * @param int|null       $calendars_id   ID of the calendar to use to restrict creation to working hours,
     *                                       or 0 / null for no calendar.
     *
     * @return string  Next creation date in 'Y-m-d H:i:s' format.
     */
    public function computeNextCreationDate(
        ?string $begin_date,
        ?string $end_date,
        $periodicity,
        ?int $create_before,
        ?int $calendars_id
    ): string {
        $now = strtotime(Session::getCurrentTime());
        $periodicity_pattern = '/([0-9]+)(MONTH|YEAR)/';

        if ($begin_date === null || DateTime::createFromFormat('Y-m-d H:i:s', $begin_date) === false) {
           // Invalid begin date.
            return 'NULL';
        }

        $has_end_date = $end_date !== null && DateTime::createFromFormat('Y-m-d H:i:s', $end_date) !== false;
        if ($has_end_date && strtotime($end_date) < $now) {
           // End date is in past.
            return 'NULL';
        }

        if (
            !is_int($periodicity) && !preg_match('/^\d+$/', $periodicity)
            && !preg_match($periodicity_pattern, $periodicity)
        ) {
           // Invalid periodicity.
            return 'NULL';
        }

       // Compute periodicity values
        $periodicity_as_interval = null;
        $periodicity_in_seconds = $periodicity;
        $matches = [];
        if (preg_match($periodicity_pattern, $periodicity, $matches)) {
            $periodicity_as_interval = "{$matches[1]} {$matches[2]}";
            $periodicity_in_seconds  = (int)$matches[1]
            * MONTH_TIMESTAMP
            * ('YEAR' === $matches[2] ? 12 : 1);
        } else if ($periodicity % DAY_TIMESTAMP == 0) {
            $periodicity_as_interval = ($periodicity / DAY_TIMESTAMP) . ' DAY';
        } else {
            $periodicity_as_interval = ($periodicity / HOUR_TIMESTAMP) . ' HOUR';
        }

       // Check that anticipated creation delay is greater than periodicity.
        if ($create_before > $periodicity_in_seconds) {
            Session::addMessageAfterRedirect(
                __('Invalid frequency. It must be greater than the preliminary creation.'),
                false,
                ERROR
            );
            return 'NULL';
        }

        $calendar = new Calendar();
        $is_calendar_valid = $calendars_id && $calendar->getFromDB($calendars_id) && $calendar->hasAWorkingDay();

        if (!$is_calendar_valid || $periodicity_in_seconds >= DAY_TIMESTAMP) {
           // Compute next occurence without using the calendar if calendar is not valid
           // or if periodicity is at least one day.

           // First occurence of creation
            $occurence_time = strtotime($begin_date);
            $creation_time  = $occurence_time - $create_before;

           // Add steps while creation time is in past
            while ($creation_time < $now) {
                $creation_time  = strtotime("+ $periodicity_as_interval", $creation_time);
                $occurence_time = $creation_time + $create_before;

               // Stop if end date reached
                if ($has_end_date && $occurence_time > strtotime($end_date)) {
                    return 'NULL';
                }
            }

            if ($is_calendar_valid) {
               // Jump to next working day if occurence is outside working days.
                while (
                    $calendar->isHoliday(date('Y-m-d', $occurence_time))
                    || !$calendar->isAWorkingDay($occurence_time)
                ) {
                    $occurence_time = strtotime('+ 1 day', $occurence_time);
                }
               // Jump to next working hour if occurence is outside working hours.
                if (!$calendar->isAWorkingHour($occurence_time)) {
                    $tmp_search_time = null;

                    // Find the first calendar segment that is after the current date
                    do {
                        if ($tmp_search_time === null) {
                            // On the first iteration, we work with the start of the day
                            $tmp_search_time = date('Y-m-d', $occurence_time);
                        } else {
                            // If we iterate a second time, this mean the date returned was too early
                            // We will add the periodicity once again to try to get a valid date
                            $tmp_search_time = date(
                                'Y-m-d H:i:s',
                                strtotime("+ $periodicity_as_interval", strtotime($occurence_date))
                            );
                        }

                        $occurence_date = $calendar->computeEndDate(
                            $tmp_search_time,
                            0 // 0 second delay to get the first working "second"
                        );
                    } while ($occurence_date < date('Y-m-d H:i:s', $now));

                    $occurence_time = strtotime($occurence_date);
                }
                $creation_time  = $occurence_time - $create_before;
            }
        } else {
           // Base computation on calendar if calendar is valid

            $occurence_date = $calendar->computeEndDate(
                $begin_date,
                0 // 0 second delay to get the first working "second"
            );
            $occurence_time = strtotime($occurence_date);
            $creation_time  = $occurence_time - $create_before;

            while ($creation_time < $now) {
                $occurence_date = $calendar->computeEndDate(
                    date('Y-m-d H:i:s', $occurence_time),
                    $periodicity_in_seconds,
                    0,
                    $periodicity_in_seconds >= DAY_TIMESTAMP
                );
                $occurence_time = strtotime($occurence_date);
                $creation_time  = $occurence_time - $create_before;

               // Stop if end date reached
                if ($has_end_date && $occurence_time > strtotime($end_date)) {
                     return 'NULL';
                }
            };
        }

        return date("Y-m-d H:i:s", $creation_time);
    }

    /**
     * Get create time
     *
     * @return int|false
     */
    public function getCreateTime()
    {
        return strtotime($this->fields['next_creation_date']) + $this->fields['create_before'];
    }

    /**
     * Handle predefined fields
     *
     * @param array $predefined
     * @param array $input
     *
     * @return array The modified $input
     */
    public function handlePredefinedFields(
        array $predefined,
        array $input
    ): array {
        if (count($predefined)) {
            foreach ($predefined as $predeffield => $predefvalue) {
                $input[$predeffield] = $predefvalue;
            }
        }

       // Set date to creation date
        $input['date'] = date('Y-m-d H:i:s', $this->getCreateTime());
        if (isset($predefined['date'])) {
            $input['date'] = Html::computeGenericDateTimeSearch(
                $predefined['date'],
                false,
                $this->getCreateTime()
            );
        }

       // Compute time_to_resolve if predefined based on create date
        if (isset($predefined['time_to_resolve'])) {
            $input['time_to_resolve'] = Html::computeGenericDateTimeSearch(
                $predefined['time_to_resolve'],
                false,
                $this->getCreateTime()
            );
        }

        return $input;
    }

    /**
     * Create an item based on the specified template
     *
     * @param CommonITILObject|null $created_item   Will contain the created item instance
     *
     * @return boolean
     */
    public function createItem(?CommonITILObject &$created_item = null)
    {
        $result = false;
        $concrete_class = static::getConcreteClass();
        $template_class = static::getTemplateClass();
        $fields_class = static::getPredefinedFieldsClass();
        $tmpl_fk = $template_class::getForeignKeyField();

        /** @var ITILTemplate */
        $template = new $template_class();

       // Create item based on specified template and entity information
        if ($template->getFromDB($this->fields[$tmpl_fk])) {
           // Get default values for item
            $input = $concrete_class::getDefaultValues($this->fields['entities_id']);

           // Apply itiltemplates predefined values
            /** @var ITILTemplatePredefinedField */
            $fields = new $fields_class();
            $predefined = $fields->getPredefinedFields($this->fields[$tmpl_fk], true);
            $input = $this->handlePredefinedFields($predefined, $input);

            if (array_key_exists('status', $predefined)) {
                $input['_do_not_compute_status'] = true;
            }

           // Set entity
            $input['entities_id'] = $this->fields['entities_id'];
            $input['_auto_import'] = true;

            /** @var CommonITILObject */
            $item = new $concrete_class();
            $input  = Toolbox::addslashes_deep($input);

            if ($items_id = $item->add($input)) {
                $created_item = $item;
                $msg = sprintf(
                    __('%s %d successfully created'),
                    $concrete_class::getTypeName(1),
                    $items_id
                );
                $result = true;
            } else {
                $msg = sprintf(
                    __('%s creation failed (check mandatory fields)'),
                    $concrete_class::getTypeName(1)
                );
            }
        } else {
            $msg = sprintf(
                __('%s creation failed (no template)'),
                $concrete_class::getTypeName(1)
            );
        }

        Log::history(
            $this->fields['id'],
            static::class,
            [0, '', addslashes($msg)],
            '',
            Log::HISTORY_LOG_SIMPLE_MESSAGE
        );

       // Compute next creation date
        $input = [
            'id'                 => $this->getId(),
            'next_creation_date' => $this->computeNextCreationDate(
                $this->fields['begin_date'],
                $this->fields['end_date'],
                $this->fields['periodicity'],
                $this->fields['create_before'],
                $this->fields['calendars_id']
            )
        ];
        $this->update($input);

        return $result;
    }

    public static function getIcon()
    {
        return "ti ti-alarm";
    }
}
