<?php

if (!defined('ABSPATH')) {
    die('You are not allowed to call this page directly.');
}

class MeprTransactionsCtrl extends MeprBaseCtrl
{
    /**
     * Loads hooks for various AJAX actions and screen options.
     *
     * @return void
     */
    public function load_hooks()
    {
        add_action('wp_ajax_mepr_edit_status', [$this, 'edit_trans_status']);
        add_action('wp_ajax_mepr_delete_transaction', [$this, 'delete_transaction']);
        add_action('wp_ajax_mepr_refund_transaction', [$this, 'refund_transaction']);
        add_action('wp_ajax_mepr_resend_txn_email', [$this, 'resend_txn_email']);
        add_action('wp_ajax_mepr_send_welcome_email', [$this, 'send_welcome_email']);
        add_action('wp_ajax_mepr_default_expiration', [$this, 'default_expiration']);
        add_action('admin_enqueue_scripts', [$this, 'enqueue_scripts']);
        add_action('wp_ajax_mepr_transactions', [$this, 'csv']);
        add_action('mepr_control_table_footer', [$this, 'export_footer_link'], 10, 3);

        add_action('wp_ajax_mepr_refund_txn_and_cancel_sub', [$this, 'refund_txn_and_cancel_sub']);

        // Screen Options.
        $hook = 'memberpress_page_memberpress-trans';
        add_action("load-{$hook}", [$this,'add_screen_options']);
        add_filter('set_screen_option_mp_transactions_perpage', [$this,'setup_screen_options'], 10, 3);
        add_filter("manage_{$hook}_columns", [$this, 'get_columns'], 0);

        add_filter('cron_schedules', [$this,'intervals']);

        if (!defined('MEMBERPRESS_DISABLE_TXN_EXPIRE_EVENTS') || false === (bool) MEMBERPRESS_DISABLE_TXN_EXPIRE_EVENTS) {
            // Set a wp-cron.
            add_action('mepr_send_txn_expire_events', [$this,'send_expired_txn_events']);

            if (!wp_next_scheduled('mepr_send_txn_expire_events')) {
                wp_schedule_event(time() + MeprUtils::hours(1), 'mepr_send_txn_expire_events_interval', 'mepr_send_txn_expire_events');
            }
        } else {
            $timestamp = wp_next_scheduled('mepr_send_txn_expire_events');
            if ($timestamp) {
                wp_unschedule_event($timestamp, 'mepr_send_txn_expire_events');
            }
        }
        add_action('mepr_table_controls_search', [$this, 'table_search_box']);

        // Capture complete and refunded events for offline gateway.
        if (class_exists('MeprArtificialGateway')) {
            add_action('mepr_txn_store', 'MeprArtificialGateway::capture_txn_status_for_events');
        }
    }

    /**
     * Adds custom intervals for cron schedules.
     *
     * @param array $schedules The existing schedules.
     *
     * @return array The modified schedules.
     */
    public function intervals($schedules)
    {
        $schedules['mepr_send_txn_expire_events_interval'] = [
            'interval' => MeprUtils::hours(1), // Every hour.
            'display'  => 'MemberPress Send Transaction Expire Events',
        ];

        return $schedules;
    }

    /**
     * Unschedules the transaction expire events.
     *
     * @return void
     */
    public static function unschedule_events()
    {
        $timestamp = wp_next_scheduled('mepr_send_txn_expire_events');
        wp_unschedule_event($timestamp, 'mepr_send_txn_expire_events');
    }

    /**
     * Handles the listing of transactions, including new and edit actions.
     *
     * @return void
     */
    public function listing()
    {
        $action = (isset($_REQUEST['action']) && !empty($_REQUEST['action'])) ? sanitize_key(wp_unslash($_REQUEST['action'])) : false;
        if ($action === 'new') {
            $this->new_trans();
        } elseif ($action === 'edit') {
            $this->edit_trans();
        } else {
            $this->display_list();
        }
    }

    /**
     * Handles the creation of a new transaction.
     *
     * @param array $errors An array of errors, if any.
     *
     * @return void
     */
    public function new_trans($errors = [])
    {
        $mepr_options = MeprOptions::fetch();
        $txn          = new MeprTransaction();
        $user_login   = $subscr_num = '';

        if (empty($errors) && MeprUtils::is_post_request()) {
            $this->create_trans($txn);
        } else {
            if (isset($_REQUEST['user']) && !empty($_REQUEST['user'])) {
                $user_login = sanitize_user(wp_unslash($_REQUEST['user']));
            }

            if (isset($_REQUEST['subscription']) && is_numeric($_REQUEST['subscription'])) {
                $sub             = new MeprSubscription(intval(wp_unslash($_REQUEST['subscription'])));
                $usr             = $sub->user();
                $prd             = $sub->product();
                $user_login      = $usr->user_login;
                $subscr_num      = $sub->subscr_id;
                $txn->product_id = $sub->product_id;
            }

            MeprView::render('/admin/transactions/new_trans', get_defined_vars());
        }
    }

    /**
     * Handles the editing of an existing transaction.
     *
     * @return void
     */
    public function edit_trans()
    {
        $mepr_options = MeprOptions::fetch();
        $subscr_num   = '';

        if (isset($_REQUEST['id'])) {
            $txn        = new MeprTransaction(intval(wp_unslash($_REQUEST['id'])));
            $usr        = $txn->user();
            $user_login = $usr->user_login;

            $sub = $txn->subscription();
            if ($sub) {
                $subscr_num = $sub->subscr_id;
            }

            if (MeprUtils::is_post_request()) {
                $this->update_trans($txn);
            } else {
                MeprView::render('/admin/transactions/edit_trans', get_defined_vars());
            }
        } else {
            $this->new_trans();
        }
    }

    /**
     * Creates a new transaction.
     *
     * @param MeprTransaction $txn The transaction object.
     *
     * @return void
     */
    public function create_trans($txn)
    {
        check_admin_referer('mepr_create_or_update_transaction', 'mepr_transactions_nonce');

        $mepr_options = MeprOptions::fetch();

        $errors = $this->validate_trans();

        $usr = new MeprUser();
        $usr->load_user_data_by_login(sanitize_user(wp_unslash($_POST['user_login'] ?? '')));
        $user_login = $usr->user_login;
        $subscr_num = '';

        $txn->trans_num  = (isset($_POST['trans_num']) && !empty($_POST['trans_num'])) ? sanitize_file_name(wp_unslash($_POST['trans_num'])) : uniqid();
        $txn->user_id    = $usr->ID;
        $txn->product_id = intval(wp_unslash($_POST['product_id'] ?? 0));
        // $txn->set_subtotal($_POST['amount']); //Don't do this, it doesn't work right on existing txns
        $txn->amount     = MeprUtils::format_currency_us_float(sanitize_text_field(wp_unslash($_POST['amount'] ?? '')));
        $txn->tax_amount = MeprUtils::format_currency_us_float(sanitize_text_field(wp_unslash($_POST['tax_amount'] ?? '')));
        $txn->total      = $txn->amount + $txn->tax_amount;
        $txn->tax_rate   = MeprUtils::format_currency_us_float(sanitize_text_field(wp_unslash($_POST['tax_rate'] ?? '')));
        $txn->status     = sanitize_text_field(wp_unslash($_POST['status'] ?? ''));
        $txn->gateway    = sanitize_text_field(wp_unslash($_POST['gateway'] ?? ''));

        if (isset($_POST['subscr_num']) && !empty($_POST['subscr_num'])) {
            $sub = MeprSubscription::get_one_by_subscr_id(sanitize_text_field(wp_unslash($_POST['subscr_num'])));
            if ($sub) {
                $txn->subscription_id = $sub->id;
                $subscr_num           = $sub->subscr_id;
                $sub->store();
            }
        }

        if (isset($_POST['created_at']) && ($_POST['created_at'] === '' || is_null($_POST['created_at']))) {
            $txn->created_at = MeprUtils::ts_to_mysql_date(time()); // This crap is due to mysql craziness.
        } else {
            $txn->created_at = MeprUtils::ts_to_mysql_date(strtotime(sanitize_text_field(wp_unslash($_POST['created_at']))));
        }

        if (isset($_POST['expires_at']) && ($_POST['expires_at'] === '' || is_null($_POST['expires_at']))) {
            $txn->expires_at = MeprUtils::db_lifetime(); // This crap is due to mysql craziness.
        } else {
            $txn->expires_at = MeprUtils::ts_to_mysql_date(strtotime(sanitize_text_field(wp_unslash($_POST['expires_at']))), 'Y-m-d 23:59:59');
        }

        $this->set_refunded_at_from_post($txn);

        // Only save to the database if there aren't any errors.
        if (empty($errors)) {
            $txn->store();

            if ($txn->status === MeprTransaction::$complete_str) {
                MeprEvent::record('transaction-completed', $txn);

                // This is a recurring payment.
                $sub = $txn->subscription();
                if ($sub && $sub->txn_count > 1) {
                    MeprEvent::record('recurring-transaction-completed', $txn);
                } elseif (!$sub) {
                    MeprEvent::record('non-recurring-transaction-completed', $txn);
                }
            }

            if (empty($txn->subscription_id)) {
                // If the transaction is not part of a subscription (stand-alone transaction).
                MeprHooks::do_action('mepr_signup', $txn);
            } elseif (
                isset($sub) &&
                count($sub->transactions()) <= 1 &&
                $txn->status === MeprTransaction::$complete_str
            ) {
                // If the transaction is the only completed transaction of a recurring subscription.
                MeprHooks::do_action('mepr_signup', $txn);
            }

            $message = __('A transaction was created successfully.', 'memberpress');

            $_REQUEST['action'] = 'edit';
            $txn                = new MeprTransaction($txn->id); // Refresh the txn obj to get all generated fields.
            MeprView::render('/admin/transactions/edit_trans', get_defined_vars());
        } else {
            $this->new_trans($errors);
        }
    }

    /**
     * Updates an existing transaction.
     *
     * @param MeprTransaction $txn The transaction object.
     *
     * @return void
     */
    public function update_trans($txn)
    {
        check_admin_referer('mepr_create_or_update_transaction', 'mepr_transactions_nonce');

        $mepr_options = MeprOptions::fetch();

        $errors = $this->validate_trans();

        $usr = new MeprUser();
        $usr->load_user_data_by_login(sanitize_user(wp_unslash($_POST['user_login'] ?? '')));
        $user_login      = $usr->user_login;
        $subscr_num      = '';
        $txn->trans_num  = sanitize_file_name(wp_unslash($_POST['trans_num'] ?? ''));
        $txn->user_id    = $usr->ID;
        $txn->product_id = intval(wp_unslash($_POST['product_id'] ?? 0));
        // $txn->set_subtotal($_POST['amount']); //Don't do this, it doesn't work right on existing txns
        $txn->amount     = MeprUtils::format_currency_us_float(sanitize_text_field(wp_unslash($_POST['amount'] ?? '')));
        $txn->tax_amount = MeprUtils::format_currency_us_float(sanitize_text_field(wp_unslash($_POST['tax_amount'] ?? '')));
        $txn->total      = $txn->amount + $txn->tax_amount;
        $txn->tax_rate   = MeprUtils::format_currency_us_float(sanitize_text_field(wp_unslash($_POST['tax_rate'] ?? '')));
        $txn->status     = sanitize_text_field(wp_unslash($_POST['status'] ?? ''));
        $txn->gateway    = sanitize_text_field(wp_unslash($_POST['gateway'] ?? ''));

        if (isset($_POST['subscr_num']) && !empty($_POST['subscr_num'])) {
            $sub = MeprSubscription::get_one_by_subscr_id(sanitize_text_field(wp_unslash($_POST['subscr_num'])));
            if ($sub) {
                $txn->subscription_id = $sub->id;
                $subscr_num           = $sub->subscr_id;
                $sub->store();
            }
        }

        if (isset($_POST['created_at']) && ($_POST['created_at'] === '' || is_null($_POST['created_at']))) {
            $txn->created_at = MeprUtils::ts_to_mysql_date(time()); // This crap is due to mysql craziness.
        } else {
            $txn->created_at = MeprUtils::ts_to_mysql_date(strtotime(sanitize_text_field(wp_unslash($_POST['created_at']))));
        }

        if (isset($_POST['expires_at']) && ($_POST['expires_at'] === '' || is_null($_POST['expires_at']))) {
            $txn->expires_at = MeprUtils::db_lifetime(); // This crap is due to mysql craziness.
        } else {
            $txn->expires_at = MeprUtils::ts_to_mysql_date(strtotime(sanitize_text_field(wp_unslash($_POST['expires_at']))));
        }

        $this->set_refunded_at_from_post($txn);

        // Only save to the database if there aren't any errors.
        if (empty($errors)) {
            $txn->store();
            $message = __('The transaction was successfully updated.', 'memberpress');
        }

        MeprView::render('/admin/transactions/edit_trans', get_defined_vars());
    }

    /**
     * Validates transaction data.
     *
     * @return array An array of validation errors.
     */
    public function validate_trans()
    {
        $errors = [];
        $usr    = new MeprUser();

        if (!isset($_POST['user_login']) || empty($_POST['user_login'])) {
            $errors[] = __('The username must be set.', 'memberpress');
        } elseif (is_email(sanitize_email(wp_unslash($_POST['user_login']))) && !username_exists(sanitize_email(wp_unslash($_POST['user_login'])))) {
            $usr->load_user_data_by_email(sanitize_email(wp_unslash($_POST['user_login'])));

            if (!$usr->ID) {
                $errors[] = __('You must enter a valid username or email address', 'memberpress');
            } else { // For use downstream in create and update transaction methods.
                $_POST['user_login'] = $usr->user_login;
            }
        } else {
            $usr->load_user_data_by_login(sanitize_user(wp_unslash($_POST['user_login'])));

            if (!$usr->ID) {
                $errors[] = __('You must enter a valid username or email address', 'memberpress');
            }
        }

        // Simple validation here.
        if (!isset($_POST['amount']) || empty($_POST['amount'])) {
            $errors[] = __('The amount must be set.', 'memberpress');
        }

        if (preg_match('/[^0-9., ]/', sanitize_text_field(wp_unslash($_POST['amount'])))) {
            $errors[] = __('The amount must be a number.', 'memberpress');
        }

        if (isset($_POST['subscr_num']) && !empty($_POST['subscr_num'])) {
            $sub = MeprSubscription::get_one_by_subscr_id(sanitize_text_field(wp_unslash($_POST['subscr_num'])));
            if ($sub) {
                $product_id = intval(wp_unslash($_POST['product_id'] ?? 0));
                if ((int) $sub->product_id !== $product_id) {
                    $prd      = new MeprProduct($product_id);
                    $sub_prd  = $sub->product();
                    $errors[] = sprintf(
                        // Translators: %1$s: expected membership name, %2$s: actual membership name.
                        __('This is not a subscription for membership "%1$s" but for "%2$s"', 'memberpress'),
                        $prd->post_title,
                        $sub_prd->post_title
                    );
                }

                /*
                 ** $usr object is already set above
                 ** $usr = new MeprUser();
                 ** $usr->load_user_data_by_login($_POST['user_login']);
                 */
                $sub_usr = $sub->user();

                if ($usr->ID !== $sub_usr->ID) {
                    $errors[] = sprintf(
                        // Translators: %1$s: expected username, %2$s: actual username.
                        __('This is not a subscription for user "%1$s" but for "%2$s"', 'memberpress'),
                        $usr->user_login,
                        $sub_usr->user_login
                    );
                }

                /*
                 * Gateway validation is temporarily disabled.
                    if($sub->gateway != $_POST['gateway']) {
                        if( $sub->gateway == MeprTransaction::$free_gateway_str ||
                            $sub->gateway == MeprTransaction::$manual_gateway_str ) {
                            $sub_gateway = $sub->gateway;
                        }
                        else {
                            $pm = $sub->payment_method();
                            $sub_gateway = sprintf( __( '%s (%s)' ), $pm->label, $pm->name );
                        }

                        $errors[] = sprintf( __( "This subscription is using a different payment gateway: %s" ), $sub_gateway );
                    }
                 */
            } else {
                $errors[] = __('This subscription was not found.', 'memberpress');
            }
        }

        if (empty($_POST['trans_num']) || preg_match('#[^a-zA-z0-9_\-]#', sanitize_text_field(wp_unslash($_POST['trans_num'])))) {
            $errors[] = __('The Transaction Number is required, and must contain only letters, numbers, underscores and hyphens.', 'memberpress');
        }

        return MeprHooks::apply_filters('mepr_admin_transaction_validation_errors', $errors);
    }

    /**
     * Enqueues scripts for the transactions admin page.
     *
     * @param string $hook The current admin page hook.
     *
     * @return void
     */
    public function enqueue_scripts($hook)
    {
        if ($hook === 'memberpress_page_memberpress-trans' || $hook === 'memberpress_page_memberpress-new-trans') {
            $l10n = [
                'del_txn_title'                     => __('Delete Transaction', 'memberpress'),
                'del_txn'                           => __('Deleting transactions could cause the associated member to lose access to protected content. Are you sure you want to delete this transaction?', 'memberpress'),
                'del_txn_error'                     => __('The transaction could not be deleted. Please try again later.', 'memberpress'),
                'refund_txn_title'                  => __('Refund Transaction', 'memberpress'),
                'refund_txn'                        => __('This will refund the transaction at the gateway level. This action is not reversable. Are you sure you want to refund this transaction?', 'memberpress'),
                'refund_txn_and_cancel_sub_title'   => __('Refund Transaction and Cancel Subscription', 'memberpress'),
                'refund_txn_and_cancel_sub'         => __('This will refund the transaction and cancel the subscription associated with this transaction at the gateway level. This action is not reversable. Are you sure you want to refund this transaction and cancel it\'s subscription?', 'memberpress'),
                'refunded_text'                     => __('Refunded', 'memberpress'),
                'refund_txn_error'                  => __('The transaction could not be refunded. Please issue the refund by logging into your gateway\'s virtual terminal', 'memberpress'),
                'refund_txn_and_cancel_sub_error'   => __('The transaction could not be refunded and/or subscription could not be cancelled. Please issue the refund by logging into your gateway\'s virtual terminal', 'memberpress'),
                'refund_txn_success'                => __('The transaction was successfully refunded.', 'memberpress'),
                'refund_txn_and_cancel_sub_success' => __('The transaction was successfully refunded and the subscription was cancelled.', 'memberpress'),
                'delete_transaction_nonce'          => wp_create_nonce('delete_transaction'),
                'edit_txn_status_nonce'             => wp_create_nonce('edit_txn_status'),
                'refund_txn_nonce'                  => wp_create_nonce('refund_txn'),
                'refund_txn_cancel_sub_nonce'       => wp_create_nonce('refund_txn_cancel_sub'),
                'click_to_copy'                     => __('Click to copy', 'memberpress'),
                'site_timezone'                     => wp_timezone_string(),
                'confirm_text'                      => __('Confirm', 'memberpress'),
                'cancel_text'                       => __('Cancel', 'memberpress'),
            ];

            wp_register_style('mepr-jquery-ui-smoothness', MEPR_CSS_URL . '/vendor/jquery-ui/smoothness.min.css', [], '1.13.3');
            wp_register_style('jquery-ui-timepicker-addon', MEPR_CSS_URL . '/vendor/jquery-ui-timepicker-addon.css', ['mepr-jquery-ui-smoothness'], MEPR_VERSION);
            wp_register_style('mepr-clipboardtip', MEPR_CSS_URL . '/vendor/tooltipster.bundle.min.css', [], MEPR_VERSION);
            wp_register_style('mepr-clipboardtip-borderless', MEPR_CSS_URL . '/vendor/tooltipster-sideTip-borderless.min.css', ['mepr-clipboardtip'], MEPR_VERSION);
            wp_enqueue_style('mepr-transactions-css', MEPR_CSS_URL . '/admin-transactions.css', ['jquery-ui-timepicker-addon', 'mepr-clipboardtip', 'mepr-clipboardtip-borderless'], MEPR_VERSION);

            wp_register_script('mepr-table-controls-js', MEPR_JS_URL . '/table_controls.js', ['jquery'], MEPR_VERSION);
            wp_register_script('mepr-timepicker-js', MEPR_JS_URL . '/vendor/jquery-ui-timepicker-addon.js', ['jquery-ui-datepicker'], MEPR_VERSION);
            wp_register_script('mepr-date-picker-js', MEPR_JS_URL . '/date_picker.js', ['mepr-timepicker-js'], MEPR_VERSION);
            wp_register_script('mphelpers', MEPR_JS_URL . '/mphelpers.js', ['suggest'], MEPR_VERSION);
            wp_register_script('mepr-tooltipster', MEPR_JS_URL . '/vendor/tooltipster.bundle.min.js', ['jquery'], MEPR_VERSION);

            wp_enqueue_script(
                'mepr-components',
                MEPR_JS_URL . '/build/components.js',
                [
                    'wp-element',
                    'wp-components',
                    'wp-hooks',
                ],
                MEPR_VERSION,
                true
            );
            wp_enqueue_style('wp-components');
            wp_enqueue_script(
                'mepr-transactions-js',
                MEPR_JS_URL . '/admin_transactions.js',
                [
                    'jquery',
                    'mphelpers',
                    'mepr-table-controls-js',
                    'mepr-date-picker-js',
                    'clipboard',
                    'mepr-tooltipster',
                    'mepr-components',
                ],
                MEPR_VERSION
            );
            wp_localize_script('mepr-transactions-js', 'MeprTxn', $l10n);
            // Localize table controls script with date validation messages.
            $table_controls_l10n = [
                'invalid_date_format'   => esc_html__('Please enter valid dates in YYYY-MM-DD format', 'memberpress'),
                'end_date_before_start' => esc_html__('End date must be greater than or equal to start date', 'memberpress'),
                'date_field_required'   => esc_html__('Set both from and to date', 'memberpress'),
            ];
            wp_localize_script('mepr-table-controls-js', 'MeprTableControls', $table_controls_l10n);
        }
    }

    /**
     * Edits the status of a transaction via AJAX.
     *
     * @return void
     */
    public function edit_trans_status()
    {
        global $wpdb;
        check_ajax_referer('edit_txn_status', 'edit_txn_status_nonce');

        if (!MeprUtils::is_mepr_admin()) {
            wp_die(esc_html__('You do not have access.', 'memberpress'));
        }

        if (!isset($_POST['id']) || empty($_POST['id']) || !isset($_POST['value']) || empty($_POST['value'])) {
            wp_die(esc_html__('Save Failed', 'memberpress'));
        }

        $id    = sanitize_key($_POST['id']);
        $value = sanitize_key($_POST['value']);
        $tdata = MeprTransaction::get_one($id, ARRAY_A);

        if (!empty($tdata)) {
            $txn = new MeprTransaction();
            $txn->load_data($tdata);
            $txn->status = esc_sql($value); // Escape the input this way since $wpdb->escape() is deprecated.
            $txn->store();
            wp_die(esc_html($txn->status));
        } else {
            wp_die(esc_html__('Save Failed', 'memberpress'));
        }
    }

    /**
     * Refunds a transaction via AJAX.
     *
     * @return void
     */
    public function refund_transaction()
    {
        if (!check_ajax_referer('refund_txn', 'refund_txn_nonce', false)) {
            wp_die(esc_html__('Security check failed.', 'memberpress'), '', ['response' => 403]);
        }

        if (!MeprUtils::is_mepr_admin()) {
            wp_die(esc_html__('You do not have access.', 'memberpress'), '', ['response' => 403]);
        }

        if (!isset($_POST['id']) || empty($_POST['id']) || !is_numeric($_POST['id'])) {
            wp_die(esc_html__('Could not refund transaction', 'memberpress'), '', ['response' => 400]);
        }

        $txn = new MeprTransaction(intval(wp_unslash($_POST['id'])));

        try {
            $txn->refund();
            MeprHooks::do_action('mepr_txn_refunded', $txn);
        } catch (Exception $e) {
            wp_die($e->getMessage(), '', ['response' => 500]); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        }

        die('true'); // Don't localize this string.
    }

    /**
     * Refunds a transaction and cancels the associated subscription via AJAX.
     *
     * @return void
     */
    public function refund_txn_and_cancel_sub()
    {
        if (!check_ajax_referer('refund_txn_cancel_sub', 'refund_txn_cancel_sub_nonce', false)) {
            wp_die(esc_html__('Security check failed.', 'memberpress'), '', ['response' => 403]);
        }

        if (!MeprUtils::is_mepr_admin()) {
            wp_die(esc_html__('You do not have access.', 'memberpress'), '', ['response' => 403]);
        }

        if (!isset($_POST['id']) || empty($_POST['id']) || !is_numeric($_POST['id'])) {
            wp_die(esc_html__('Could not refund transaction', 'memberpress'), '', ['response' => 400]);
        }

        $txn = new MeprTransaction(intval(wp_unslash($_POST['id'])));

        try {
            $txn->refund();
            MeprHooks::do_action('mepr_txn_refunded_and_canceled', $txn);
            $sub = $txn->subscription();
            if ($sub) {
                $sub->cancel();
            }
        } catch (Exception $e) {
            wp_die($e->getMessage(), '', ['response' => 500]); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        }

        die('true'); // Don't localize this string.
    }

    /**
     * Deletes a transaction via AJAX.
     *
     * @return void
     */
    public function delete_transaction()
    {
        check_ajax_referer('delete_transaction', 'mepr_transactions_nonce');

        if (!MeprUtils::is_mepr_admin()) {
            wp_die(esc_html__('You do not have access.', 'memberpress'));
        }

        if (!isset($_POST['id']) || empty($_POST['id']) || !is_numeric($_POST['id'])) {
            wp_die(esc_html__('Could not delete transaction', 'memberpress'));
        }

        $txn = new MeprTransaction(intval(wp_unslash($_POST['id'])));
        $txn->destroy();

        die('true'); // Don't localize this string.
    }

    /**
     * Gets the columns for the transactions table.
     *
     * @return array The columns.
     */
    public function get_columns()
    {
        $cols = [
            'col_id'             => __('Id', 'memberpress'),
            'col_trans_num'      => __('Transaction', 'memberpress'),
            'col_subscr_id'      => __('Subscription', 'memberpress'),
            'col_status'         => __('Status', 'memberpress'),
            'col_product'        => __('Membership', 'memberpress'),
            'col_net'            => __('Net', 'memberpress'),
            'col_tax'            => __('Tax', 'memberpress'),
            'col_total'          => __('Total', 'memberpress'),
            'col_propername'     => __('Name', 'memberpress'),
            'col_user_login'     => __('User', 'memberpress'),
            'col_payment_system' => __('Gateway', 'memberpress'),
            'col_created_at'     => __('Created On', 'memberpress'),
            'col_expires_at'     => __('Expires On', 'memberpress'),
        ];

        return MeprHooks::apply_filters('mepr_admin_transactions_cols', $cols);
    }

    /**
     * Displays the list of transactions.
     *
     * @return void
     */
    public function display_list()
    {
        $screen     = get_current_screen();
        $list_table = new MeprTransactionsTable($screen, $this->get_columns());

        $list_table->prepare_items();

        MeprView::render('/admin/transactions/list', get_defined_vars());
    }

    /**
     * Resends a transaction email via AJAX.
     *
     * @return void
     */
    public function resend_txn_email()
    {
        $mepr_options = MeprOptions::fetch();

        if (!MeprUtils::is_mepr_admin()) {
            wp_die(esc_html__('You do not have access.', 'memberpress'));
        }

        if (!isset($_POST['id']) || empty($_POST['id']) || !is_numeric($_POST['id'])) {
            wp_die(esc_html__('Could not send email. Please try again later.', 'memberpress'));
        }

        $txn = new MeprTransaction(intval(wp_unslash($_POST['id'])));

        MeprUtils::send_notices(
            $txn,
            'MeprUserReceiptEmail',
            'MeprAdminReceiptEmail'
        );

        wp_die(esc_html__('Email sent', 'memberpress'));
    }

    /**
     * Sends a welcome email for a transaction via AJAX.
     *
     * @return void
     */
    public function send_welcome_email()
    {
        $mepr_options = MeprOptions::fetch();

        if (!MeprUtils::is_mepr_admin()) {
            wp_die(esc_html__('You do not have access.', 'memberpress'));
        }

        if (!isset($_POST['id']) || empty($_POST['id']) || !is_numeric($_POST['id'])) {
            wp_die(esc_html__('Could not send email. Please try again later.', 'memberpress'));
        }

        $txn = new MeprTransaction(intval(wp_unslash($_POST['id'])));
        $usr = $txn->user();

        MeprUtils::maybe_send_product_welcome_notices($txn, $usr);

        wp_die(esc_html__('Welcome Email sent', 'memberpress'));
    }

    /**
     * Calculates the default expiration date for a product.
     *
     * @return void
     */
    public function default_expiration()
    {
        if (isset($_REQUEST['product_id']) && isset($_REQUEST['created_at'])) {
            $prd = MeprProduct::get_one(intval(wp_unslash($_REQUEST['product_id'])));
            if (
                $prd &&
                // !$prd->is_one_time_payment() && //Breaking one-offs which have an expiration date
                (preg_match('/\d\d\d\d-\d\d-\d\d/', sanitize_text_field(wp_unslash($_REQUEST['created_at']))) ||
                preg_match('/\d\d\d\d-\d\d-\d\d \d\d-\d\d-\d\d/', sanitize_text_field(wp_unslash($_REQUEST['created_at']))) ||
                empty($_REQUEST['created_at']))
            ) {
                $expires_at_ts = $prd->get_expires_at(strtotime(sanitize_text_field(wp_unslash($_REQUEST['created_at']))), false);

                if (!is_null($expires_at_ts)) {
                    echo esc_html(gmdate('Y-m-d H:i:s', (int)$expires_at_ts));
                }
            }
        }

        die;
    }

    /**
     * Exports transactions to a CSV file.
     *
     * @return void
     */
    public function csv()
    {
        check_ajax_referer('export_transactions', 'mepr_transactions_nonce');

        $filename = 'transactions-' . time();

        // Since we're running WP_List_Table headless we need to do this.
        // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
        $GLOBALS['hook_suffix'] = false;

        $screen = get_current_screen();
        $tab    = new MeprTransactionsTable($screen, $this->get_columns());

        if (isset($_REQUEST['all']) && !empty($_REQUEST['all'])) {
            $search       = isset($_REQUEST['search']) && !empty($_REQUEST['search']) ? esc_sql(sanitize_text_field(wp_unslash($_REQUEST['search'])))  : '';
            $search_field = isset($_REQUEST['search-field']) && !empty($_REQUEST['search-field'])  ? esc_sql(sanitize_key(wp_unslash($_REQUEST['search-field'])))  : 'any';
            $search_field = isset($tab->db_search_cols[$search_field]) ? $tab->db_search_cols[$search_field] : 'any';

            $all = MeprTransaction::list_table(
                'created_at',
                'ASC',
                '',
                $search,
                $search_field,
                '',
                $_REQUEST
            );

            MeprUtils::render_csv($all['results'], $filename);
        } else {
            $tab->prepare_items();
            MeprUtils::render_csv($tab->get_items(), $filename);
        }
    }

    /**
     * Adds an export link to the footer of the transactions table.
     *
     * @param string  $action     The current action.
     * @param integer $totalitems The total number of items.
     * @param integer $itemcount  The current item count.
     *
     * @return void
     */
    public function export_footer_link($action, $totalitems, $itemcount)
    {
        if ($action === 'mepr_transactions') {
            MeprAppHelper::export_table_link($action, 'export_transactions', 'mepr_transactions_nonce', $itemcount);
            ?> | <?php
      MeprAppHelper::export_table_link($action, 'export_transactions', 'mepr_transactions_nonce', $totalitems, true);
        }
    }

    /**
     * Adds screen options for the transactions page.
     *
     * @return void
     */
    public function add_screen_options()
    {
        add_screen_option('layout_columns');

        $option = 'per_page';

        $args = [
            'label'   => __('Transactions', 'memberpress'),
            'default' => 10,
            'option'  => 'mp_transactions_perpage',
        ];

        add_screen_option($option, $args);
    }

    /**
     * Sets up screen options for the transactions page.
     *
     * @param mixed  $status The current status.
     * @param string $option The option name.
     * @param mixed  $value  The option value.
     *
     * @return mixed The updated status or value.
     */
    public function setup_screen_options($status, $option, $value)
    {
        if ('mp_transactions_perpage' === $option) {
            return $value;
        }
        return $status;
    }

    /**
     * Sends events for expired transactions.
     *
     * @return void
     */
    public function send_expired_txn_events()
    {
        $start_time = time();
        $max_time   = MeprUtils::minutes(10);

        $res = MeprTransaction::get_expired_txns();

        foreach ($res as $row) {
            $run_time = time() - $start_time;
            if ($run_time >= $max_time) {
                return;
            }
            $txn = new MeprTransaction($row->id);
            MeprEvent::record('transaction-expired', $txn);

            if ($txn->subscription()) {
                MeprEvent::record('recurring-transaction-expired', $txn);
            } else {
                MeprEvent::record('non-recurring-transaction-expired', $txn);
            }

            MeprHooks::do_action('mepr_txn_expired', $txn, $row->sub_status); // DEPRECATED.
            MeprHooks::do_action('mepr_transaction_expired', $txn, $row->sub_status);
        }
    }

    /**
     * Displays the search box for the transactions table.
     *
     * @return void
     */
    public function table_search_box()
    {
        if (isset($_REQUEST['page']) && $_REQUEST['page'] === 'memberpress-trans') {
            $mepr_options = MeprOptions::fetch();

            $membership = (isset($_REQUEST['membership']) ? sanitize_text_field(wp_unslash($_REQUEST['membership'])) : false);
            $status     = (isset($_REQUEST['status']) ? sanitize_key(wp_unslash($_REQUEST['status'])) : 'all');
            $gateway    = (isset($_REQUEST['gateway']) ? sanitize_key(wp_unslash($_REQUEST['gateway'])) : 'all');

            // Date range filter parameters.
            $date_range_filter         = (isset($_REQUEST['date_range_filter']) ? sanitize_text_field(wp_unslash($_REQUEST['date_range_filter'])) : 'all');
            $date_start                = (isset($_REQUEST['date_start']) ? sanitize_text_field(wp_unslash($_REQUEST['date_start'])) : '');
            $date_end                  = (isset($_REQUEST['date_end']) ? sanitize_text_field(wp_unslash($_REQUEST['date_end'])) : '');
            $date_field                = (isset($_REQUEST['date_field']) ? sanitize_text_field(wp_unslash($_REQUEST['date_field'])) : 'created_at');
            $date_fields               = MeprTransactionsHelper::get_date_filter_fields();
            $date_range_filter_options = MeprTransactionsHelper::get_date_range_filter_options();
            $args                      = [
                'orderby' => 'title',
                'order'   => 'ASC',
            ];
            $prds                      = MeprCptModel::all('MeprProduct', false, $args);
            $gateways                  = $mepr_options->payment_methods();

            MeprView::render('/admin/transactions/search_box', compact('membership', 'status', 'prds', 'gateways', 'gateway', 'date_range_filter', 'date_start', 'date_end', 'date_field', 'date_fields', 'date_range_filter_options'));
        }
    }

    /**
     * Sets the refunded_at property on a transaction object.
     *
     * @param  MeprTransaction $txn The transaction object.
     * @return void
     */
    private function set_refunded_at_from_post($txn)
    {
        if (empty($_POST['refunded_at'])) {
            $txn->refunded_at = null;
        } else {
            $txn->refunded_at = MeprUtils::ts_to_mysql_date(strtotime(sanitize_text_field(wp_unslash($_POST['refunded_at']))));
        }
    }
}
