<?php

class ActionScheduler_wpPostStore extends ActionScheduler_Store
{
    const POST_TYPE = 'scheduled-action';
    const GROUP_TAXONOMY = 'action-group';
    const SCHEDULE_META_KEY = '_action_manager_schedule';
    const DEPENDENCIES_MET = 'as-post-store-dependencies-met';
    private $claim_before_date = null;
    protected $local_timezone = NULL;
    public function save_action(ActionScheduler_Action $action, DateTime $scheduled_date = NULL)
    {
        try {
            $this->validate_action($action);
            $post_array = $this->create_post_array($action, $scheduled_date);
            $post_id = $this->save_post_array($post_array);
            $this->save_post_schedule($post_id, $action->get_schedule());
            $this->save_action_group($post_id, $action->get_group());
            do_action('action_scheduler_stored_action', $post_id);
            return $post_id;
        } catch (Exception $e) {
            throw new RuntimeException(sprintf(__('Error saving action: %s', 'woocommerce'), $e->getMessage()), 0);
        }
    }
    protected function create_post_array(ActionScheduler_Action $action, DateTime $scheduled_date = NULL)
    {
        $post = array('post_type' => self::POST_TYPE, 'post_title' => $action->get_hook(), 'post_content' => json_encode($action->get_args()), 'post_status' => $action->is_finished() ? 'publish' : 'pending', 'post_date_gmt' => $this->get_scheduled_date_string($action, $scheduled_date), 'post_date' => $this->get_scheduled_date_string_local($action, $scheduled_date));
        return $post;
    }
    protected function save_post_array($post_array)
    {
        add_filter('wp_insert_post_data', array($this, 'filter_insert_post_data'), 10, 1);
        add_filter('pre_wp_unique_post_slug', array($this, 'set_unique_post_slug'), 10, 5);
        $has_kses = false !== has_filter('content_save_pre', 'wp_filter_post_kses');
        if ($has_kses) {
            kses_remove_filters();
        }
        $post_id = wp_insert_post($post_array);
        if ($has_kses) {
            kses_init_filters();
        }
        remove_filter('wp_insert_post_data', array($this, 'filter_insert_post_data'), 10);
        remove_filter('pre_wp_unique_post_slug', array($this, 'set_unique_post_slug'), 10);
        if (is_wp_error($post_id) || empty($post_id)) {
            throw new RuntimeException(__('Unable to save action.', 'woocommerce'));
        }
        return $post_id;
    }
    public function filter_insert_post_data($postdata)
    {
        if ($postdata['post_type'] == self::POST_TYPE) {
            $postdata['post_author'] = 0;
            if ($postdata['post_status'] == 'future') {
                $postdata['post_status'] = 'publish';
            }
        }
        return $postdata;
    }
    public function set_unique_post_slug($override_slug, $slug, $post_ID, $post_status, $post_type)
    {
        if (self::POST_TYPE == $post_type) {
            $override_slug = uniqid(self::POST_TYPE . '-', true) . '-' . wp_generate_password(32, false);
        }
        return $override_slug;
    }
    protected function save_post_schedule($post_id, $schedule)
    {
        update_post_meta($post_id, self::SCHEDULE_META_KEY, $schedule);
    }
    protected function save_action_group($post_id, $group)
    {
        if (empty($group)) {
            wp_set_object_terms($post_id, array(), self::GROUP_TAXONOMY, FALSE);
        } else {
            wp_set_object_terms($post_id, array($group), self::GROUP_TAXONOMY, FALSE);
        }
    }
    public function fetch_action($action_id)
    {
        $post = $this->get_post($action_id);
        if (empty($post) || $post->post_type != self::POST_TYPE) {
            return $this->get_null_action();
        }
        try {
            $action = $this->make_action_from_post($post);
        } catch (ActionScheduler_InvalidActionException $exception) {
            do_action('action_scheduler_failed_fetch_action', $post->ID, $exception);
            return $this->get_null_action();
        }
        return $action;
    }
    protected function get_post($action_id)
    {
        if (empty($action_id)) {
            return NULL;
        }
        return get_post($action_id);
    }
    protected function get_null_action()
    {
        return new ActionScheduler_NullAction();
    }
    protected function make_action_from_post($post)
    {
        $hook = $post->post_title;
        $args = json_decode($post->post_content, true);
        $this->validate_args($args, $post->ID);
        $schedule = get_post_meta($post->ID, self::SCHEDULE_META_KEY, true);
        $this->validate_schedule($schedule, $post->ID);
        $group = wp_get_object_terms($post->ID, self::GROUP_TAXONOMY, array('fields' => 'names'));
        $group = empty($group) ? '' : reset($group);
        return ActionScheduler::factory()->get_stored_action($this->get_action_status_by_post_status($post->post_status), $hook, $args, $schedule, $group);
    }
    protected function get_action_status_by_post_status($post_status)
    {
        switch ($post_status) {
            case 'publish':
                $action_status = self::STATUS_COMPLETE;
                break;
            case 'trash':
                $action_status = self::STATUS_CANCELED;
                break;
            default:
                if (!array_key_exists($post_status, $this->get_status_labels())) {
                    throw new InvalidArgumentException(sprintf('Invalid post status: "%s". No matching action status available.', $post_status));
                }
                $action_status = $post_status;
                break;
        }
        return $action_status;
    }
    protected function get_post_status_by_action_status($action_status)
    {
        switch ($action_status) {
            case self::STATUS_COMPLETE:
                $post_status = 'publish';
                break;
            case self::STATUS_CANCELED:
                $post_status = 'trash';
                break;
            default:
                if (!array_key_exists($action_status, $this->get_status_labels())) {
                    throw new InvalidArgumentException(sprintf('Invalid action status: "%s".', $action_status));
                }
                $post_status = $action_status;
                break;
        }
        return $post_status;
    }
    protected function get_query_actions_sql(array $query, $select_or_count = 'select')
    {
        if (!in_array($select_or_count, array('select', 'count'))) {
            throw new InvalidArgumentException(__('Invalid schedule. Cannot save action.', 'woocommerce'));
        }
        $query = wp_parse_args($query, array('hook' => '', 'args' => NULL, 'date' => NULL, 'date_compare' => '<=', 'modified' => NULL, 'modified_compare' => '<=', 'group' => '', 'status' => '', 'claimed' => NULL, 'per_page' => 5, 'offset' => 0, 'orderby' => 'date', 'order' => 'ASC', 'search' => ''));
        global $wpdb;
        $sql = 'count' === $select_or_count ? 'SELECT count(p.ID)' : 'SELECT p.ID ';
        $sql .= "FROM {$wpdb->posts} p";
        $sql_params = array();
        if (empty($query['group']) && 'group' === $query['orderby']) {
            $sql .= " LEFT JOIN {$wpdb->term_relationships} tr ON tr.object_id=p.ID";
            $sql .= " LEFT JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id=tt.term_taxonomy_id";
            $sql .= " LEFT JOIN {$wpdb->terms} t ON tt.term_id=t.term_id";
        } elseif (!empty($query['group'])) {
            $sql .= " INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id=p.ID";
            $sql .= " INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id=tt.term_taxonomy_id";
            $sql .= " INNER JOIN {$wpdb->terms} t ON tt.term_id=t.term_id";
            $sql .= " AND t.slug=%s";
            $sql_params[] = $query['group'];
        }
        $sql .= " WHERE post_type=%s";
        $sql_params[] = self::POST_TYPE;
        if ($query['hook']) {
            $sql .= " AND p.post_title=%s";
            $sql_params[] = $query['hook'];
        }
        if (!is_null($query['args'])) {
            $sql .= " AND p.post_content=%s";
            $sql_params[] = json_encode($query['args']);
        }
        if ($query['status']) {
            $post_statuses = array_map(array($this, 'get_post_status_by_action_status'), (array) $query['status']);
            $placeholders = array_fill(0, count($post_statuses), '%s');
            $sql .= ' AND p.post_status IN (' . join(', ', $placeholders) . ')';
            $sql_params = array_merge($sql_params, array_values($post_statuses));
        }
        if ($query['date'] instanceof DateTime) {
            $date = clone $query['date'];
            $date->setTimezone(new DateTimeZone('UTC'));
            $date_string = $date->format('Y-m-d H:i:s');
            $comparator = $this->validate_sql_comparator($query['date_compare']);
            $sql .= " AND p.post_date_gmt {$comparator} %s";
            $sql_params[] = $date_string;
        }
        if ($query['modified'] instanceof DateTime) {
            $modified = clone $query['modified'];
            $modified->setTimezone(new DateTimeZone('UTC'));
            $date_string = $modified->format('Y-m-d H:i:s');
            $comparator = $this->validate_sql_comparator($query['modified_compare']);
            $sql .= " AND p.post_modified_gmt {$comparator} %s";
            $sql_params[] = $date_string;
        }
        if ($query['claimed'] === TRUE) {
            $sql .= " AND p.post_password != ''";
        } elseif ($query['claimed'] === FALSE) {
            $sql .= " AND p.post_password = ''";
        } elseif (!is_null($query['claimed'])) {
            $sql .= " AND p.post_password = %s";
            $sql_params[] = $query['claimed'];
        }
        if (!empty($query['search'])) {
            $sql .= " AND (p.post_title LIKE %s OR p.post_content LIKE %s OR p.post_password LIKE %s)";
            for ($i = 0; $i < 3; $i++) {
                $sql_params[] = sprintf('%%%s%%', $query['search']);
            }
        }
        if ('select' === $select_or_count) {
            switch ($query['orderby']) {
                case 'hook':
                    $orderby = 'p.post_title';
                    break;
                case 'group':
                    $orderby = 't.name';
                    break;
                case 'status':
                    $orderby = 'p.post_status';
                    break;
                case 'modified':
                    $orderby = 'p.post_modified';
                    break;
                case 'claim_id':
                    $orderby = 'p.post_password';
                    break;
                case 'schedule':
                case 'date':
                default:
                    $orderby = 'p.post_date_gmt';
                    break;
            }
            if ('ASC' === strtoupper($query['order'])) {
                $order = 'ASC';
            } else {
                $order = 'DESC';
            }
            $sql .= " ORDER BY {$orderby} {$order}";
            if ($query['per_page'] > 0) {
                $sql .= " LIMIT %d, %d";
                $sql_params[] = $query['offset'];
                $sql_params[] = $query['per_page'];
            }
        }
        return $wpdb->prepare($sql, $sql_params);
    }
    public function query_actions($query = array(), $query_type = 'select')
    {
        global $wpdb;
        $sql = $this->get_query_actions_sql($query, $query_type);
        return 'count' === $query_type ? $wpdb->get_var($sql) : $wpdb->get_col($sql);
    }
    public function action_counts()
    {
        $action_counts_by_status = array();
        $action_stati_and_labels = $this->get_status_labels();
        $posts_count_by_status = (array) wp_count_posts(self::POST_TYPE, 'readable');
        foreach ($posts_count_by_status as $post_status_name => $count) {
            try {
                $action_status_name = $this->get_action_status_by_post_status($post_status_name);
            } catch (Exception $e) {
                continue;
            }
            if (array_key_exists($action_status_name, $action_stati_and_labels)) {
                $action_counts_by_status[$action_status_name] = $count;
            }
        }
        return $action_counts_by_status;
    }
    public function cancel_action($action_id)
    {
        $post = get_post($action_id);
        if (empty($post) || $post->post_type != self::POST_TYPE) {
            throw new InvalidArgumentException(sprintf(__('Unidentified action %s', 'woocommerce'), $action_id));
        }
        do_action('action_scheduler_canceled_action', $action_id);
        add_filter('pre_wp_unique_post_slug', array($this, 'set_unique_post_slug'), 10, 5);
        wp_trash_post($action_id);
        remove_filter('pre_wp_unique_post_slug', array($this, 'set_unique_post_slug'), 10);
    }
    public function delete_action($action_id)
    {
        $post = get_post($action_id);
        if (empty($post) || $post->post_type != self::POST_TYPE) {
            throw new InvalidArgumentException(sprintf(__('Unidentified action %s', 'woocommerce'), $action_id));
        }
        do_action('action_scheduler_deleted_action', $action_id);
        wp_delete_post($action_id, TRUE);
    }
    public function get_date($action_id)
    {
        $next = $this->get_date_gmt($action_id);
        return ActionScheduler_TimezoneHelper::set_local_timezone($next);
    }
    public function get_date_gmt($action_id)
    {
        $post = get_post($action_id);
        if (empty($post) || $post->post_type != self::POST_TYPE) {
            throw new InvalidArgumentException(sprintf(__('Unidentified action %s', 'woocommerce'), $action_id));
        }
        if ($post->post_status == 'publish') {
            return as_get_datetime_object($post->post_modified_gmt);
        } else {
            return as_get_datetime_object($post->post_date_gmt);
        }
    }
    public function stake_claim($max_actions = 10, DateTime $before_date = null, $hooks = array(), $group = '')
    {
        $this->claim_before_date = $before_date;
        $claim_id = $this->generate_claim_id();
        $this->claim_actions($claim_id, $max_actions, $before_date, $hooks, $group);
        $action_ids = $this->find_actions_by_claim_id($claim_id);
        $this->claim_before_date = null;
        return new ActionScheduler_ActionClaim($claim_id, $action_ids);
    }
    public function get_claim_count()
    {
        global $wpdb;
        $sql = "SELECT COUNT(DISTINCT post_password) FROM {$wpdb->posts} WHERE post_password != '' AND post_type = %s AND post_status IN ('in-progress','pending')";
        $sql = $wpdb->prepare($sql, array(self::POST_TYPE));
        return $wpdb->get_var($sql);
    }
    protected function generate_claim_id()
    {
        $claim_id = md5(microtime(true) . rand(0, 1000));
        return substr($claim_id, 0, 20);
    }
    protected function claim_actions($claim_id, $limit, DateTime $before_date = null, $hooks = array(), $group = '')
    {
        $date = null === $before_date ? as_get_datetime_object() : clone $before_date;
        $limit_ids = !empty($group);
        $ids = $limit_ids ? $this->get_actions_by_group($group, $limit, $date) : array();
        if ($limit_ids && 0 === count($ids)) {
            return 0;
        }
        global $wpdb;
        $update = "UPDATE {$wpdb->posts} SET post_password = %s, post_modified_gmt = %s, post_modified = %s";
        $params = array($claim_id, current_time('mysql', true), current_time('mysql'));
        $where = "WHERE post_type = %s AND post_status = %s AND post_password = ''";
        $params[] = self::POST_TYPE;
        $params[] = ActionScheduler_Store::STATUS_PENDING;
        if (!empty($hooks)) {
            $placeholders = array_fill(0, count($hooks), '%s');
            $where .= ' AND post_title IN (' . join(', ', $placeholders) . ')';
            $params = array_merge($params, array_values($hooks));
        }
        if ($limit_ids) {
            $where .= ' AND ID IN (' . join(',', $ids) . ')';
        } else {
            $where .= ' AND post_date_gmt <= %s';
            $params[] = $date->format('Y-m-d H:i:s');
        }
        $order = 'ORDER BY menu_order ASC, post_date_gmt ASC, ID ASC LIMIT %d';
        $params[] = $limit;
        $rows_affected = $wpdb->query($wpdb->prepare("{$update} {$where} {$order}", $params));
        if ($rows_affected === false) {
            throw new RuntimeException(__('Unable to claim actions. Database error.', 'woocommerce'));
        }
        return (int) $rows_affected;
    }
    protected function get_actions_by_group($group, $limit, DateTime $date)
    {
        if (!term_exists($group, self::GROUP_TAXONOMY)) {
            throw new InvalidArgumentException(sprintf(__('The group "%s" does not exist.', 'woocommerce'), $group));
        }
        $query = new WP_Query();
        $query_args = array('fields' => 'ids', 'post_type' => self::POST_TYPE, 'post_status' => ActionScheduler_Store::STATUS_PENDING, 'has_password' => false, 'posts_per_page' => $limit * 3, 'suppress_filters' => true, 'no_found_rows' => true, 'orderby' => array('menu_order' => 'ASC', 'date' => 'ASC', 'ID' => 'ASC'), 'date_query' => array('column' => 'post_date_gmt', 'before' => $date->format('Y-m-d H:i'), 'inclusive' => true), 'tax_query' => array(array('taxonomy' => self::GROUP_TAXONOMY, 'field' => 'slug', 'terms' => $group, 'include_children' => false)));
        return $query->query($query_args);
    }
    public function find_actions_by_claim_id($claim_id)
    {
        global $wpdb;
        $sql = "SELECT ID, post_date_gmt FROM {$wpdb->posts} WHERE post_type = %s AND post_password = %s";
        $sql = $wpdb->prepare($sql, array(self::POST_TYPE, $claim_id));
        $action_ids = array();
        $before_date = isset($this->claim_before_date) ? $this->claim_before_date : as_get_datetime_object();
        $cut_off = $before_date->format('Y-m-d H:i:s');
        foreach ($wpdb->get_results($sql) as $claimed_action) {
            if ($claimed_action->post_date_gmt <= $cut_off) {
                $action_ids[] = absint($claimed_action->ID);
            }
        }
        return $action_ids;
    }
    public function release_claim(ActionScheduler_ActionClaim $claim)
    {
        $action_ids = $this->find_actions_by_claim_id($claim->get_id());
        if (empty($action_ids)) {
            return;
        }
        $action_id_string = implode(',', array_map('intval', $action_ids));
        global $wpdb;
        $sql = "UPDATE {$wpdb->posts} SET post_password = '' WHERE ID IN ({$action_id_string}) AND post_password = %s";
        $sql = $wpdb->prepare($sql, array($claim->get_id()));
        $result = $wpdb->query($sql);
        if ($result === false) {
            throw new RuntimeException(sprintf(__('Unable to unlock claim %s. Database error.', 'woocommerce'), $claim->get_id()));
        }
    }
    public function unclaim_action($action_id)
    {
        global $wpdb;
        $sql = "UPDATE {$wpdb->posts} SET post_password = '' WHERE ID = %d AND post_type = %s";
        $sql = $wpdb->prepare($sql, $action_id, self::POST_TYPE);
        $result = $wpdb->query($sql);
        if ($result === false) {
            throw new RuntimeException(sprintf(__('Unable to unlock claim on action %s. Database error.', 'woocommerce'), $action_id));
        }
    }
    public function mark_failure($action_id)
    {
        global $wpdb;
        $sql = "UPDATE {$wpdb->posts} SET post_status = %s WHERE ID = %d AND post_type = %s";
        $sql = $wpdb->prepare($sql, self::STATUS_FAILED, $action_id, self::POST_TYPE);
        $result = $wpdb->query($sql);
        if ($result === false) {
            throw new RuntimeException(sprintf(__('Unable to mark failure on action %s. Database error.', 'woocommerce'), $action_id));
        }
    }
    public function get_claim_id($action_id)
    {
        return $this->get_post_column($action_id, 'post_password');
    }
    public function get_status($action_id)
    {
        $status = $this->get_post_column($action_id, 'post_status');
        if ($status === null) {
            throw new InvalidArgumentException(__('Invalid action ID. No status found.', 'woocommerce'));
        }
        return $this->get_action_status_by_post_status($status);
    }
    private function get_post_column($action_id, $column_name)
    {
        global $wpdb;
        return $wpdb->get_var($wpdb->prepare("SELECT {$column_name} FROM {$wpdb->posts} WHERE ID=%d AND post_type=%s", $action_id, self::POST_TYPE));
    }
    public function log_execution($action_id)
    {
        global $wpdb;
        $sql = "UPDATE {$wpdb->posts} SET menu_order = menu_order+1, post_status=%s, post_modified_gmt = %s, post_modified = %s WHERE ID = %d AND post_type = %s";
        $sql = $wpdb->prepare($sql, self::STATUS_RUNNING, current_time('mysql', true), current_time('mysql'), $action_id, self::POST_TYPE);
        $wpdb->query($sql);
    }
    public function mark_complete($action_id)
    {
        $post = get_post($action_id);
        if (empty($post) || $post->post_type != self::POST_TYPE) {
            throw new InvalidArgumentException(sprintf(__('Unidentified action %s', 'woocommerce'), $action_id));
        }
        add_filter('wp_insert_post_data', array($this, 'filter_insert_post_data'), 10, 1);
        add_filter('pre_wp_unique_post_slug', array($this, 'set_unique_post_slug'), 10, 5);
        $result = wp_update_post(array('ID' => $action_id, 'post_status' => 'publish'), TRUE);
        remove_filter('wp_insert_post_data', array($this, 'filter_insert_post_data'), 10);
        remove_filter('pre_wp_unique_post_slug', array($this, 'set_unique_post_slug'), 10);
        if (is_wp_error($result)) {
            throw new RuntimeException($result->get_error_message());
        }
    }
    public function mark_migrated($action_id)
    {
        wp_update_post(array('ID' => $action_id, 'post_status' => 'migrated'));
    }
    public function migration_dependencies_met($setting)
    {
        global $wpdb;
        $dependencies_met = get_transient(self::DEPENDENCIES_MET);
        if (empty($dependencies_met)) {
            $maximum_args_length = apply_filters('action_scheduler_maximum_args_length', 191);
            $found_action = $wpdb->get_var($wpdb->prepare("SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND CHAR_LENGTH(post_content) > %d LIMIT 1", $maximum_args_length, self::POST_TYPE));
            $dependencies_met = $found_action ? 'no' : 'yes';
            set_transient(self::DEPENDENCIES_MET, $dependencies_met, DAY_IN_SECONDS);
        }
        return 'yes' == $dependencies_met ? $setting : false;
    }
    protected function validate_action(ActionScheduler_Action $action)
    {
        try {
            parent::validate_action($action);
        } catch (Exception $e) {
            $message = sprintf(__('%s Support for strings longer than this will be removed in a future version.', 'woocommerce'), $e->getMessage());
            _doing_it_wrong('ActionScheduler_Action::$args', $message, '2.1.0');
        }
    }
    public function init()
    {
        add_filter('action_scheduler_migration_dependencies_met', array($this, 'migration_dependencies_met'));
        $post_type_registrar = new ActionScheduler_wpPostStore_PostTypeRegistrar();
        $post_type_registrar->register();
        $post_status_registrar = new ActionScheduler_wpPostStore_PostStatusRegistrar();
        $post_status_registrar->register();
        $taxonomy_registrar = new ActionScheduler_wpPostStore_TaxonomyRegistrar();
        $taxonomy_registrar->register();
    }
}