<?php

/**
 * Enity layer for schedules
 *
 * Standard: Missing
 *
 * @package DUP_PRO
 * @subpackage classes/entities
 * @copyright (c) 2017, Snapcreek LLC
 * @license https://opensource.org/licenses/GPL-3.0 GNU Public License
 * @since 3.0.0
 *
 * @todo Finish Docs
 */

defined('ABSPATH') || defined('DUPXABSPATH') || exit;

require_once(DUPLICATOR_PRO_PLUGIN_PATH . '/classes/entities/class.json.entity.base.php');
require_once(DUPLICATOR_PRO_PLUGIN_PATH . '/classes/entities/class.package.template.entity.php');
require_once(DUPLICATOR_PRO_PLUGIN_PATH . '/classes/entities/class.system.global.entity.php');
require_once(DUPLICATOR_PRO_PLUGIN_PATH . '/vendor/cron-expression/src/includeCronLib.php');

if (!class_exists('DUP_PRO_Brand_Entity')) {
    require_once(DUPLICATOR_PRO_PLUGIN_PATH . '/classes/entities/class.brand.entity.php');
}

abstract class DUP_PRO_Schedule_Repeat_Types
{
    const Daily   = 0;
    const Weekly  = 1;
    const Monthly = 2;
    const Hourly  = 3;
}

abstract class DUP_PRO_Schedule_Days
{
    const Monday    = 1;
    const Tuesday   = 2;
    const Wednesday = 4;
    const Thursday  = 8;
    const Friday    = 16;
    const Saturday  = 32;
    const Sunday    = 64;
}

class DUP_PRO_Schedule_Entity extends DUP_PRO_JSON_Entity_Base
{
    const CRON_PARSER_LIB_CSD_PARSER = 'csd-parser';
    const CRON_PARSER_LIB_CRON_EXP   = 'cron-exp';

    public $name            = '';
    public $template_id     = -1;
    public $start_ticks;
    public $repeat_type     = DUP_PRO_Schedule_Repeat_Types::Weekly;
    public $active          = true;
    public $next_run_time   = -1;
    public $run_every       = 1;
    public $weekly_days     = 0;    // for now just using a single day but they can be bit encoded here
    public $day_of_month    = 1;
    public $cron_string     = '';
    public $last_run_time   = -1;
    public $last_run_status = DUP_PRO_Package_Build_Outcome::FAILURE;
    public $times_run       = 0;
    public $storage_ids;

    function __construct()
    {
        parent::__construct();
        $this->verifiers['name'] = new DUP_PRO_Required_Verifier("Name must not be blank");
        //  $this->start_ticks = time();

        $this->storage_ids = array();
        array_push($this->storage_ids, DUP_PRO_Virtual_Storage_IDs::Default_Local);

        $this->name = DUP_PRO_U::__('New Schedule');
    }

    public static function create_from_data($data)
    {
        $instance = new DUP_PRO_Schedule_Entity();

        $instance->name            = $data->name;
        $instance->template_id     = $data->template_id;
        $instance->start_ticks     = $data->start_ticks;
        $instance->repeat_type     = $data->repeat_type;
        $instance->active          = $data->active;
        $instance->cron_string     = $data->cron_string;
        $instance->next_run_time   = $instance->get_next_run_time();
        $instance->run_every       = $data->run_every;
        $instance->weekly_days     = $data->weekly_days;    // for now just using a single day but they can be bit encoded here
        $instance->day_of_month    = $data->day_of_month;
        $instance->last_run_time   = -1;
        $instance->last_run_status = DUP_PRO_Package_Build_Outcome::FAILURE;
        $instance->times_run       = 0;
        $instance->storage_ids     = $data->storage_ids;

        return $instance;
    }

    // If it should run, queue up a package then update the run time
    public function process()
    {
        DUP_PRO_LOG::trace("process");
        $now = time();

        if ($this->next_run_time != -1) {
            if ($this->active && ($this->next_run_time <= $now)) {
                $next_run_time_string = DUP_PRO_DATE::getLocalTimeFromGMTTicks($this->next_run_time);
                $now_string           = DUP_PRO_DATE::getLocalTimeFromGMTTicks($this->next_run_time);

                DUP_PRO_LOG::trace("NEXT RUN IS NOW! $next_run_time_string <= $now_string so trying to queue package");

                $this->insert_new_package();

                $this->next_run_time = $this->get_next_run_time();
                $this->save();

                $next_run_time_string = DUP_PRO_DATE::getLocalTimeFromGMTTicks($this->next_run_time);
                DUP_PRO_LOG::trace("******PACKAGE JUST CREATED. UPDATED NEXT RUN TIME TO $next_run_time_string");
            } else {
                DUP_PRO_LOG::trace("active and runtime=$this->next_run_time >= $now");
            }
        }
    }

    public function copy_from_source_id($source_id)
    {

        /* @var $source_schedule DUP_PRO_Schedule_Entity */
        $source_schedule = self::get_by_id($source_id);

        $this->active          = $source_schedule->active;
        $this->cron_string     = $source_schedule->cron_string;
        $this->day_of_month    = $source_schedule->day_of_month;
        $this->last_run_status = $source_schedule->last_run_status;
        $this->name            = sprintf(DUP_PRO_U::__('%1$s - Copy'), $source_schedule->name);
        //$this->last_run_time = $source_schedule->last_run_time;
        //$this->next_run_time = $source_schedule
        $this->repeat_type     = $source_schedule->repeat_type;
        $this->run_every       = $source_schedule->run_every;
        $this->start_ticks     = $source_schedule->start_ticks;
        $this->storage_ids     = $source_schedule->storage_ids; // Arrays are copied
        $this->template_id     = $source_schedule->template_id;
        //$this->times_run;
        $this->weekly_days     = $source_schedule->weekly_days;
    }

    public function insert_new_package($run_now = false)
    {
        global $wp_version;
        global $wpdb;

        /* @var $global DUP_PRO_Global_Entity */
        $global = DUP_PRO_Global_Entity::get_instance();

        DUP_PRO_LOG::trace("archive build mode before calling insert new package " . $global->archive_build_mode);

        DUP_PRO_LOG::trace("Inserting new package for schedule $this->name");

        /* @var $schedule DUP_PRO_Schedule_Entity */
        $template = DUP_PRO_Package_Template_Entity::get_by_id((int) $this->template_id);

        if ($template != null) {
            $package = new DUP_PRO_Package();

            $package->template_name = $template->name;

            $dbversion  = DUP_PRO_DB::getVersion();
            $dbversion  = is_null($dbversion) ? '- unknown -' : $dbversion;
            $dbcomments = DUP_PRO_DB::getVariable('version_comment');
            $dbcomments = is_null($dbcomments) ? '- unknown -' : $dbcomments;

            //PACKAGE
            $package->Created    = gmdate("Y-m-d H:i:s");
            $package->Version    = DUPLICATOR_PRO_VERSION;
            $package->VersionOS  = defined('PHP_OS') ? PHP_OS : 'unknown';
            $package->VersionWP  = $wp_version;
            $package->VersionPHP = phpversion();
            $package->VersionDB  = $dbversion;
            $package->Name       = $this->generate_package_name();
            $package->Hash       = $package->make_hash();
            $package->NameHash   = "{$package->Name}_{$package->Hash}";
            $package->Notes      = sprintf(DUP_PRO_U::esc_html__('Created by schedule %1$s'), $this->name);
            if ($run_now) {
                $package->Type = DUP_PRO_PackageType::RUN_NOW;
            } else {
                $package->Type = DUP_PRO_PackageType::SCHEDULED;
            }

            //BRAND
            $brand_data = DUP_PRO_Brand_Entity::get_by_id((int) $template->installer_opts_brand, true);

            $package->Brand    = $brand_data->name;
            $package->Brand_ID = (int) $brand_data->id;

            //MULTISITE
            $package->Multisite->FilterSites = $template->filter_sites;

            //ARCHIVE
            if ($global->archive_build_mode == DUP_PRO_Archive_Build_Mode::DupArchive) {
                $package->Archive->Format = 'DAF';
            } else {
                $package->Archive->Format = 'ZIP';
            }

            $package->Archive->ExportOnlyDB = $template->archive_export_onlydb;
            $package->Archive->FilterOn     = $template->archive_filter_on;
            $package->Archive->FilterDirs   = $template->archive_filter_dirs;
            $package->Archive->FilterExts   = $template->archive_filter_exts;
            $package->Archive->FilterFiles  = $template->archive_filter_files;

            //INSTALLER
            $package->Installer->OptsDBHost     = $template->installer_opts_db_host;
            $package->Installer->OptsDBName     = $template->installer_opts_db_name;
            $package->Installer->OptsDBUser     = $template->installer_opts_db_user;
            $package->Installer->OptsSecureOn   = $template->installer_opts_secure_on;
            $package->Installer->OptsSecurePass = $template->installer_opts_secure_pass;
            $package->Installer->OptsSkipScan   = $template->installer_opts_skip_scan;

            // CPANEL
            $package->Installer->OptsCPNLHost     = $template->installer_opts_cpnl_host;
            $package->Installer->OptsCPNLUser     = $template->installer_opts_cpnl_user;
            $package->Installer->OptsCPNLPass     = $template->installer_opts_cpnl_pass;
            $package->Installer->OptsCPNLEnable   = $template->installer_opts_cpnl_enable;
            $package->Installer->OptsCPNLConnect  = false;
            //CPANEL DB
            //1 = Create New, 2 = Connect Remove
            $package->Installer->OptsCPNLDBAction = $template->installer_opts_cpnl_db_action;
            $package->Installer->OptsCPNLDBHost   = $template->installer_opts_cpnl_db_host;
            $package->Installer->OptsCPNLDBName   = $template->installer_opts_cpnl_db_name;
            $package->Installer->OptsCPNLDBUser   = $template->installer_opts_cpnl_db_user;

            //DATABASE
            $package->Database->FilterOn     = $template->database_filter_on;
            $package->Database->FilterTables = $template->database_filter_tables;
            $package->Database->Comments     = esc_html($dbcomments);

            $package->Status      = DUP_PRO_PackageStatus::PRE_PROCESS;
            $package->schedule_id = $this->id;
            $package->add_upload_infos($this->storage_ids);

            $package->build_progress->set_build_mode();

            $packageObj = json_encode($package);

            /* @var $system_global DUP_PRO_System_Global_Entity */
            $system_global                   = DUP_PRO_System_Global_Entity::get_instance();
            $system_global->clear_recommended_fixes();
            $system_global->package_check_ts = 0;
            $system_global->save();

            $results = $wpdb->insert($wpdb->base_prefix . "duplicator_pro_packages", array(
                'name'    => $package->Name,
                'hash'    => $package->Hash,
                'status'  => DUP_PRO_PackageStatus::PRE_PROCESS,
                'created' => current_time('mysql'/* , get_option('gmt_offset', 1) */),
                'owner'   => DUP_PRO_U::__('System'),
                'package' => $packageObj));

            if ($results === false) {
                //$error_result = $wpdb->print_error();
                DUP_PRO_LOG::trace("Problem inserting package: {$wpdb->last_error}");

                DUP_PRO_Log::error("Duplicator is unable to insert a package record into the database table.", "'{$wpdb->last_error}'");
            }
        } else {
            $name = isset($schedule->name) ? $schedule->name : '';
            DUP_PRO_LOG::traceError("No settings object exists for schedule {$name}!");
        }

        DUP_PRO_LOG::trace("archive build mode after calling insert new package " . $global->archive_build_mode);
    }

    /**
     *
     * @return boolean|\DUP_PRO_Package_Template_Entity
     */
    public function getTemplate()
    {
        if ($this->template_id > 0) {
            $template = DUP_PRO_Package_Template_Entity::get_by_id($this->template_id);
        } else {
            $template = null;
        }

        if (!$template instanceof DUP_PRO_Package_Template_Entity) {
            return false;
        }

        return $template;
    }

    public function recoveableHtmlInfo($isList = false)
    {
        if (($template = $this->getTemplate()) === false) {
            return false;
        }

        $schedule = $this;
        require DUPLICATOR_PRO_PLUGIN_PATH . '/views/tools/templates/widget/recoveable-template-info.php';
    }

    private function generate_package_name()
    {
        $ticks = time();
        $ticks += ((int) get_option('gmt_offset') * 3600);

        //Remove specail_chars from final result
        $special_chars = array(".", "-");
        $name          = date('Y-m-d_H-i-s', $ticks) . '_' . $this->name . '_' . sanitize_title(get_bloginfo('name', 'display'));

        // Get away from using sanitize_file_name since it has dependencies on user which is problematic for schedules
        // $name = substr(sanitize_file_name($name), 0, 40);

        $name = str_replace($special_chars, '', $name);

        // Yanked from core logic of sanitize_file_name
        $sanitize_special_chars = array("?", "[", "]", "/", "\\", "=", "<", ">", ":", ";", ",", "'", "\"", "&", "$", "#", "*", "(", ")", "|", "~", "`", "!", "{", "}", "%", "+", chr(0));

        $name = preg_replace("#\x{00a0}#siu", ' ', $name);
        $name = str_replace($sanitize_special_chars, '', $name);
        $name = str_replace(array('%20', '+'), '-', $name);
        $name = preg_replace('/[\r\n\t -]+/', '-', $name);
        $name = trim($name, '.-_');

        $name = substr($name, 0, 40);

        return $name;
    }

    public function save()
    {
        DUP_PRO_LOG::trace("saving schedule $this->id");
        // If we are inactive clear out any packages associated with us

        parent::save();
    }

    // Return the next run time in UTC
    public function get_next_run_time()
    {
        if ($this->active) {
            $now = time();

            $now += (int) get_option('gmt_offset') * 3600;
            $now += 60;

            switch (DUP_PRO_Global_Entity::get_instance()->cron_parser_lib) {
                case self::CRON_PARSER_LIB_CRON_EXP:
                    $date = new DateTime();
                    $date->setTimestamp($now);
                    $cronEx = Duplicator\Extlibs\Cron\CronExpression::factory($this->cron_string);
                    $next_run_time = $cronEx->getNextRunDate($date)->getTimestamp();
                    break;
                case self::CRON_PARSER_LIB_CSD_PARSER:
                default:
                    // RSR making default since there is an odd bug where cron parse lib not set!
                    // self::CRON_PARSER_LIB_CSD_PARSER:
                    //OLD LIB (had a bug in it)
                    require_once(DUPLICATOR_PRO_PLUGIN_PATH . '/lib/cron/csd_parser.php');
                    // RSR was having problems with this - I THINK its because it considers when you are in the same
                    // minute as not really being the next but the same
                    $parser = new csd_parser($this->cron_string, $now);
                    $next_run_time = $parser->get();
                    break;
            }

            // Have to negate the offset and add. For instance for az time -7 we want the next run timt to be 7 ahead
            // in utc time
            $next_run_time -= ((int) get_option('gmt_offset') * 3600);
            return $next_run_time;
        } else {
            return -1;
        }
    }

    public function set_weekdays_from_request($request)
    {
        $weekday = $request['weekday'];
        if (in_array('mon', $weekday)) {
            $this->weekly_days |= DUP_PRO_Schedule_Days::Monday;
        } else {
            $this->weekly_days &= ~DUP_PRO_Schedule_Days::Monday;
        }

        if (in_array('tue', $weekday)) {
            $this->weekly_days |= DUP_PRO_Schedule_Days::Tuesday;
        } else {
            $this->weekly_days &= ~DUP_PRO_Schedule_Days::Tuesday;
        }

        if (in_array('wed', $weekday)) {
            $this->weekly_days |= DUP_PRO_Schedule_Days::Wednesday;
        } else {
            $this->weekly_days &= ~DUP_PRO_Schedule_Days::Wednesday;
        }

        if (in_array('thu', $weekday)) {
            $this->weekly_days |= DUP_PRO_Schedule_Days::Thursday;
        } else {
            $this->weekly_days &= ~DUP_PRO_Schedule_Days::Thursday;
        }

        if (in_array('fri', $weekday)) {
            $this->weekly_days |= DUP_PRO_Schedule_Days::Friday;
        } else {
            $this->weekly_days &= ~DUP_PRO_Schedule_Days::Friday;
        }

        if (in_array('sat', $weekday)) {
            $this->weekly_days |= DUP_PRO_Schedule_Days::Saturday;
        } else {
            $this->weekly_days &= ~DUP_PRO_Schedule_Days::Saturday;
        }

        if (in_array('sun', $weekday)) {
            $this->weekly_days |= DUP_PRO_Schedule_Days::Sunday;
        } else {
            $this->weekly_days &= ~DUP_PRO_Schedule_Days::Sunday;
        }
    }

    public function is_day_set($day_string)
    {
        $day_bit = 0;

        switch ($day_string) {
            case 'mon':
                $day_bit = DUP_PRO_Schedule_Days::Monday;
                break;

            case 'tue':
                $day_bit = DUP_PRO_Schedule_Days::Tuesday;
                break;

            case 'wed':
                $day_bit = DUP_PRO_Schedule_Days::Wednesday;
                break;

            case 'thu':
                $day_bit = DUP_PRO_Schedule_Days::Thursday;
                break;

            case 'fri':
                $day_bit = DUP_PRO_Schedule_Days::Friday;
                break;

            case 'sat':
                $day_bit = DUP_PRO_Schedule_Days::Saturday;
                break;

            case 'sun':
                $day_bit = DUP_PRO_Schedule_Days::Sunday;
                break;
        }

        $is_set = (($this->weekly_days & $day_bit) != 0);

        $isset_string = $is_set ? 'yes' : 'no';

        //DUP_PRO_U::debug("is_day_set:daystring=$day_string daybit=$day_bit)");
        // DUP_PRO_U::debug("is set = $isset_string");

        return $is_set;
    }

    public static function get_all()
    {
        return self::get_by_type(get_class());
    }

    public static function delete_by_id($list_id)
    {
        parent::delete_by_id_base($list_id);
    }

    public static function get_active()
    {
        $schedules        = self::get_all();
        $active_schedules = array();

        foreach ($schedules as $schedule) {
            /* @var $schedule DUP_PRO_Schedule_Entity */
            if ($schedule->active) {
                array_push($active_schedules, $schedule);
            }
        }
        return $active_schedules;
    }

    // [0] = hour; [1] = minute;
    public function get_start_time_piece($piece)
    {
        switch ($piece) {
            case 0:
                return (int) date('G', $this->start_ticks);
                break;

            case 1:
                return (int) date('i', $this->start_ticks);
                break;

            default:
                return -1;
        }
    }

    public function get_next_run_time_string()
    {
        if ($this->next_run_time == -1) {
            return DUP_PRO_U::__('Unscheduled');
        } else {
            $local_run_time = $this->next_run_time + ((int) get_option('gmt_offset') * 3600);

            $date_portion = date(get_option('date_format', 'n/j/y') . ' G:i', $local_run_time);

            $repeat_portion = $this->get_repeat_text();

            return "$date_portion - $repeat_portion";
        }
    }

    public function get_last_ran_string()
    {
        if ($this->last_run_time == -1) {
            return DUP_PRO_U::__('Never Ran');
        } else {
            $local_run_time = $this->last_run_time + ((int) get_option('gmt_offset') * 3600);

            $date_portion = date(get_option('date_format', 'n/j/y') . ' G:i', $local_run_time);

            $status_portion = (($this->last_run_status == DUP_PRO_Package_Build_Outcome::SUCCESS) ? DUP_PRO_U::__('Success') : DUP_PRO_U::__('Failed'));

            return "$date_portion - $status_portion";
        }
    }

    public function set_start_date_time($start_time_string, $start_date_string = '2015/1/1')
    {
        $this->start_ticks = strtotime("$start_date_string $start_time_string");
        DUP_PRO_LOG::trace("start ticks = $this->start_ticks for $start_time_string $start_date_string");
    }

    public static function get_by_id($id)
    {
        //Schedule Run Now = -1 don't search for id
        if ($id != -1) {
            return self::get_by_id_and_type($id, get_class());
        } else {
            return null;
        }
    }

    public static function get_by_template_id($template_id)
    {
        $schedules          = self::get_all();
        $filtered_schedules = array();

        foreach ($schedules as $schedule) {
            /* @var $schedule DUP_PRO_Schedule_Entity */
            if ($schedule->template_id == $template_id) {
                array_push($filtered_schedules, $schedule);
            }
        }

        DUP_PRO_LOG::trace("get by template id $template_id schedules = " . count($filtered_schedules));

        return $filtered_schedules;
    }

    public function get_repeat_text()
    {
        switch ($this->repeat_type) {
            case DUP_PRO_Schedule_Repeat_Types::Daily:
                return DUP_PRO_U::__('Daily');
                break;

            case DUP_PRO_Schedule_Repeat_Types::Weekly:
                return DUP_PRO_U::__('Weekly');
                break;

            case DUP_PRO_Schedule_Repeat_Types::Monthly:
                return DUP_PRO_U::__('Monthly');
                break;

            case DUP_PRO_Schedule_Repeat_Types::Hourly:
                return DUP_PRO_U::__('Hourly');
                break;

            default:
                return DUP_PRO_U::__('Unknown');
        }
    }

    public function build_cron_string()
    {
        // Special cron string for debugging if name set to 'bobtest'
        if ($this->name == 'bobtest') {
            $this->cron_string = '*/5 * * * *';
        } else {
            $start_hour = $this->get_start_time_piece(0);
            $start_min  = $this->get_start_time_piece(1);

            if ($this->run_every == 1) {
                $run_every_string = '*';
            } else {
                $run_every_string = "*/$this->run_every";
            }

            // Generated cron patterns using http://www.cronmaker.com/
            switch ($this->repeat_type) {
                case DUP_PRO_Schedule_Repeat_Types::Hourly:
                    $this->cron_string = "$start_min $run_every_string * * *";
                    break;

                case DUP_PRO_Schedule_Repeat_Types::Daily:
                    $this->cron_string = "$start_min $start_hour $run_every_string * *";
                    break;

                case DUP_PRO_Schedule_Repeat_Types::Weekly:
                    $day_of_week_string = $this->get_day_of_week_string();
                    $this->cron_string  = "$start_min $start_hour * * $day_of_week_string";

                    DUP_PRO_LOG::trace("day of week cron string: $this->cron_string");
                    break;

                case DUP_PRO_Schedule_Repeat_Types::Monthly:
                    $this->cron_string = "$start_min $start_hour $this->day_of_month $run_every_string *";
                    break;
            }
        }

        DUP_PRO_LOG::trace("cron string = $this->cron_string");
    }

    private function get_day_of_week_string()
    {
        $day_array = array();

        DUP_PRO_LOG::trace("weekly days=$this->weekly_days");


        if (($this->weekly_days & DUP_PRO_Schedule_Days::Monday) != 0) {
            array_push($day_array, '1');
        }

        if (($this->weekly_days & DUP_PRO_Schedule_Days::Tuesday) != 0) {
            array_push($day_array, '2');
        }

        if (($this->weekly_days & DUP_PRO_Schedule_Days::Wednesday) != 0) {
            array_push($day_array, '3');
        }

        if (($this->weekly_days & DUP_PRO_Schedule_Days::Thursday) != 0) {
            array_push($day_array, '4');
        }

        if (($this->weekly_days & DUP_PRO_Schedule_Days::Friday) != 0) {
            array_push($day_array, '5');
        }

        if (($this->weekly_days & DUP_PRO_Schedule_Days::Saturday) != 0) {
            array_push($day_array, '6');
        }

        if (($this->weekly_days & DUP_PRO_Schedule_Days::Sunday) != 0) {
            array_push($day_array, '0');
        }
        return implode(',', $day_array);
    }
}
