<?php
/** 
 * @package     VikRestaurants
 * @subpackage  core
 * @author      E4J s.r.l.
 * @copyright   Copyright (C) 2023 E4J s.r.l. All Rights Reserved.
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
 * @link        https://vikwp.com
 */

// No direct access
defined('ABSPATH') or die('No script kiddies please!');

use E4J\VikRestaurants\Cron\Installable;

/**
 * VikRestaurants cron job model.
 *
 * @since 1.10
 */
class VikRestaurantsModelCronjob extends JModelVRE
{
	/**
     * Method used to dispatch a cron job.
     *
     * @param   int          $cronId     The ID of the cron to launch.
     * @param   string|null  $secureKey  The secure key to execute the command. Leave empty to
     *                                   ignore the validation.
     *
     * @return  bool  True on success, false otherwise.
     */
    public function dispatch(int $cronId, ?string $secureKey = null)
    {
        $db = JFactory::getDbo();
        $config = VREFactory::getConfig();
        
        if (empty($cronId))
        {
            $this->setError(new InvalidArgumentException('Missing CRON ID', 400));
            return false;
        }
        
        // validate secure key only in case the 2nd argument was passed
        if (!is_null($secureKey))
        {
            // match the specified secure key with the one stored in the configuration
            $match = md5($config->get('cron_secure_key'));
            
            if (strcmp($match, $secureKey))
            {
                $this->setError(new Exception('CRON secure key is not correct', 403));
                return false;
            }
        }

        // load cron job parameters
        $cron = $this->getItem($cronId);
        
        if (!$cron || !$cron->published)
        {
            $this->setError(new Exception(sprintf('Cron job [%d] not found', (int) $cronId), 404));
            return false;
        }
        
        try
        {
            // dispatch cron job
            $job = VREFactory::getCronDispatcher()->getCron($cron->class, $cron->id);
        }
        catch (Exception $error)
        {
            // unable to load the cron job, propagate the error message
            $this->setError($error);
            return false;
        }
        
        // get response
        $response = $job->run($cron->params);

        if (!$response->hasContent())
        {
            // add a sample content to properly register the log
            $response->setContent('Execution completed.');
        }

        // prepare log data
        $log = [
            'content'    => $response->getContent(),
            'metadata'   => $response->getMetadata(),
            'status'     => (int) $response->isSuccess(),
            'mailed'     => (int) $response->isNotify(),
            'id_cronjob' => $cron->id,
        ];

        /**
         * Fetch the preferences and check whether the e-mail notifications should be allowed or not.
         * This because, in case the cron job executes very frequently, the system might send hundreds
         * of error e-mails.
         */
        if ($response->isNotify() && $cron->resume_notif && JFactory::getDate($cron->resume_notif) > JFactory::getDate())
        {
            // notification still paused
            $log['mailed'] = 2;

            // prevent notification
            $response->setNotify(false);
        }

        // store log details
        JModelVRE::getInstance('cronjoblog')->save($log);
        
        // mail response
        if ($response->isNotify())
        {
            $mail = new E4J\VikRestaurants\Mail\Mail;

            // set default sender
            $mail->setSender(VikRestaurants::getSenderMail(), $config->get('restname'));

            // fetch e-mail subject
            $mail->setSubject(JText::sprintf('VRECRONJOBNOTIFYSUBJECT', $config->get('restname')));

            // start the e-mail body with the cron title
            $body = sprintf("<b>Cron #%d - %s (%s)</b><br><br>", $cron->id, $cron->name, $cron->class);

            // fetch e-mail body
            $body .= JText::sprintf(
                'VRECRONJOBNOTIFYCONTENT', 
                JHtml::fetch('date', $response->getLastUpdate(), JText::translate('DATE_FORMAT_LC6')), 
                $response->isSuccess() ? 'OK' : 'ERROR',
                $response->getContent()
            );

            // add a link in the footer to temporarily pause the notifications
            $pauseUri = VREFactory::getPlatform()->getUri()->route('index.php?option=com_vikrestaurants&task=cron_pause_notif&id=' . $cron->id . '&key=' . $config->get('cron_secure_key', ''));
            $body .= '<br />' . JText::translate('VRECRONJOBNOTIFYFOOTER') . '<br /><br /><a href="' . $pauseUri . '" target="_blank">' . $pauseUri . '</a>';

            $mail->setBody($body);
            $mail->setHtml(true);

            // set all the administrators as recipient
            $mail->setRecipients(VikRestaurants::getAdminMailList());

            // deliver e-mail notification
            (new E4J\VikRestaurants\Mail\MailDeliverer)->send($mail);
        }

        return true;
    }

    /**
     * Basic item loading implementation.
     *
     * @param   mixed  $pk   An optional primary key value to load the row by, or an array of fields to match.
     *                       If not set the instance property value is used.
     * @param   bool   $new  True to return an empty object if missing.
     *
     * @return  mixed  The record object on success, null otherwise.
     */
    public function getItem($pk, $new = false)
    {
        // load item through parent
        $item = parent::getItem($pk, $new);

        if ($item)
        {
            $item->params = $item->params ? (array) json_decode($item->params, true) : [];
        }

        return $item;
    }

    /**
     * Basic save implementation.
     *
     * @param   mixed  $data  Either an array or an object of data to save.
     *
     * @return  mixed  The ID of the record on success, false otherwise.
     */
    public function save($data)
    {
        $data = (array) $data;

        $app = JFactory::getApplication();
        $job = null;

        // check whether the driver has been specified
        if (!empty($data['class']))
        {
            try
            {
                // get cron job
                $job = VREFactory::getCronDispatcher()->getCron($data['class'], $data['id'] ?? null);
            }
            catch (Exception $error)
            {
                // unable to load the cron job, propagate the error message
                $this->setError($error);
                return false;
            }

            // load configuration
            foreach ($job->getForm() as $name => $field)
            {
                if (in_array($field['type'] ?? '', ['textarea', 'editor']))
                {
                    // use raw filter
                    $filter = 'raw';
                }
                else
                {
                    // use string filter
                    $filter = 'string';
                }

                // get parameter from request
                $data['params'][$name] = $app->input->get('cronform_' . $name, '', $filter);

                if ($filter == 'raw')
                {
                    // sanitize HTML
                    $data['params'][$name] = JComponentHelper::filterText($data['params'][$name]);
                }
                
                // validate field if required
                if (!empty($field['required']) && $data['params'][$name] === '')
                {
                    // register error message
                    $this->setError(JText::sprintf('VRE_MISSING_REQ_FIELD', $field['label'] ?? $name));

                    // unsafe record
                    return false;
                }
            }
        }

        // attempt to save the cron job
        $id = parent::save($data);

        if (!$id)
        {
            // an error occurred
            return false;
        }

        // check whether we created a new cron job and it supports an installation process
        if (empty($data['id']) && $job instanceof Installable)
        {
            try 
            {
                // perform installation
                $job->install((int) $id);
            }
            catch (Exception $error)
            {
                // enqueue error message
                $this->setError($error);
            }
        }

        return $id;
    }

    /**
     * Extend delete implementation to delete any related records
     * stored within a separated table.
     *
     * @param   mixed  $ids  Either the record ID or a list of records.
     *
     * @return  bool   True on success, false otherwise.
     */
    public function delete($ids)
    {
        // only int values are accepted
        $ids = array_map('intval', (array) $ids);

        $db = JFactory::getDbo();

        if ($ids)
        {
            // get cron jobs classes
            $query = $db->getQuery(true)
                ->select($db->qn(['id', 'class']))
                ->from($db->qn('#__vikrestaurants_cronjob'))
                ->where($db->qn('id') . ' IN (' . implode(',', $ids) . ')');

            $db->setQuery($query);
            
            foreach ($db->loadObjectList() as $obj)
            {
                try
                {
                    // create cron job instance
                    $job = VREFactory::getCronDispatcher()->getCron($obj->class, $obj->id);

                    // check whether the cron job supports an installation process
                    if ($job instanceof Installable)
                    {
                        // uninstall cron job
                        $job->uninstall((int) $obj->id);
                    }
                }
                catch (Exception $error)
                {
                    // enqueue error message
                    $this->setError($error);
                }
            }
        }

        // invoke parent first
        if (!parent::delete($ids))
        {
            // nothing to delete
            return false;
        }

        // load any children logs
        $query = $db->getQuery(true)
            ->select($db->qn('id'))
            ->from($db->qn('#__vikrestaurants_cronjob_log'))
            ->where($db->qn('id_cronjob') . ' IN (' . implode(',', $ids) . ')' );

        $db->setQuery($query);

        if ($logIds = $db->loadColumn())
        {
            // delete children logs
            JModelVRE::getInstance('cronjoblog')->delete($logIds);
        }

        return true;
    }

    /**
     * Pauses the notifications for the specified cron job for the
     * number of given days.
     * 
     * @param   int  $cronId  The ID of the cron to pause.
     * @param   int  $days    The number of days since now.
     * 
     * @return  JDate|false   The date when the pause will end on success,
     *                        false otherwise.
     */
    public function pauseNotifications(int $cronId, int $days)
    {
        // create threshold date
        $threshold = JFactory::getDate('+' . abs($days) . ' days');

        // attempt to save the cron job
        $result = $this->save([
            'id' => $cronId,
            'resume_notif' => $threshold->toSql(),
        ]);

        if (!$result)
        {
            return false;
        }

        // return the date when the notifications will be resumed
        return $threshold;
    }
}
