<?php

// Include everything.
require dirname(__FILE__) . '/wcp-include-all.php';

// ===========================================================================
// '$hardcron' is affected by DISABLE_WP_CRON constant. If it is false
// then the user has setup wp_cron via a real cron job and we can run as if
// it is a $hardcron
abstract class CryptoCronJob
{

	public $wcp_settings;
	private $hardcron;
	private $max_unused_addresses_buffer = null;

	public function __construct()
	{
		$cron = esc_attr(get_option(WCP_GENERAL_SETTINGS)['enable_soft_cron']);
		if (defined('DISABLE_WP_CRON') && constant('DISABLE_WP_CRON') && !$cron) {
			$this->hardcron = true;
		} else {
			$this->hardcron = false;
		}
		$this->wcp_settings = wcp__get_settings();
	}

	public function run()
	{
		$this->process_payments();
		$this->pre_generate_addresses();
	}

	abstract protected function get_gateway();

	abstract protected function get_settings_name();

	abstract protected function get_table_name();

	private function get_electrum_mpk()
	{
		return $this->get_gateway()->get_next_available_mpk();
	}

	protected function get_electrum_util()
	{
		return $this->get_gateway()->get_electrum_util();
	}

	// unlikely to change and we call this in a loop
	// so cache it.

	private function get_max_unused_addresses_buffer()
	{

		if ($this->max_unused_addresses_buffer === null) {
			$this->max_unused_addresses_buffer = $this->get_gateway()->get_max_unused_addresses_buffer();
			file_put_contents('_array.txt', print_r($this->max_unused_addresses_buffer, TRUE));
		}
		return $this->max_unused_addresses_buffer;
	}


	abstract protected function process_payments();

	private function pre_generate_addresses()
	{
		global $wpdb;

		// only pre-generate if we are doing a hardcron
		// otherwise we risk slowing down our users a lot
		// while we generate addresses
		if (!$this->hardcron) {
			return;
		}

		if (!$this->get_electrum_mpk()) {
			return;
		}
		// Calculate number of unused addresses belonging to currently active electrum wallet
		$crypto_addresses_table_name = $this->get_table_name();
		$origin_id                = $this->get_electrum_mpk();
		file_put_contents('_array.txt', print_r($origin_id, TRUE));

		$current_time                     = time();
		$assigned_address_expires_in_secs = $this->wcp_settings['assigned_address_expires_in_mins'] * 60;
		$funds_received_value_expires_in_secs = 0; // $this->wcp_settings['funds_received_value_expires_in_mins'] * 60;

		$addresses_reuse = $this->wcp_settings['reuse_expired_addresses'];

		if ($addresses_reuse) {
			$reuse_expired_addresses_query_part =
				"OR (`status`='assigned'
							 AND (('$current_time' - `assigned_at`) > '$assigned_address_expires_in_secs')
							 AND (('$current_time' - `received_funds_checked_at`) > '$funds_received_value_expires_in_secs')
							 )";
		} else {
			$reuse_expired_addresses_query_part = '';
		}


		// Calculate total number of currently unused addresses in a system. Make sure there aren't too many.
		// NULL == not found
		// Retrieve:
		// 'unused'   - with fresh zero balances
		// 'assigned' - expired, with fresh zero balances (if 'reuse_expired_addresses' is true)
		//
		// Hence - any returned address will be clean to use.
		$query                  =
			"SELECT COUNT(*) as `total_unused_addresses` FROM `$crypto_addresses_table_name`
               WHERE `origin_id`='$origin_id'
               AND `total_received_funds`='0'
               AND (`status`='unused' $reuse_expired_addresses_query_part)
               ";
		$total_unused_addresses = $wpdb->get_var($query);
		for (; $total_unused_addresses < $this->get_max_unused_addresses_buffer(); $total_unused_addresses++) {
			$address_data = $this->get_electrum_util()->generate_new_crypto_address_for_electrum_wallet();
		}
	}
}

class BitcoinBTCCronJob extends CryptoCronJob
{

	private $gateway;

	public function __construct()
	{
		parent::__construct();
		$this->gateway = new WCP_Bitcoin_BTC();
	}

	protected function get_settings_name()
	{
		return WCP_BTC_SETTINGS;
	}

	protected function get_gateway()
	{
		return $this->gateway;
	}

	protected function process_payments()
	{
		global $wpdb;

		$crypto_addresses_table_name = $this->get_table_name();
		$confs = esc_attr(get_option(WCP_BTC_SETTINGS)['confs_num']);

		$funds_received_value_expires_in_secs = 0; // $this->wcp_settings['funds_received_value_expires_in_mins'] * 60;
		$assigned_address_expires_in_secs     = $this->wcp_settings['assigned_address_expires_in_mins'] * 60;
		$confirmations_required               = $confs;

		$clean_address = null;
		$current_time  = time();

		// Search for completed orders (addresses that received full payments for their orders) ...
		// NULL == not found
		// Retrieve:
		// 'assigned'   - unexpired, with old balances (due for revalidation. Fresh balances and still 'assigned' means no [full] payment received yet)
		// 'revalidate' - all
		// order results by most recently assigned
		$query                  =
			"SELECT * FROM `$crypto_addresses_table_name`
					 WHERE (
							 (`status`='assigned' AND (('$current_time' - `assigned_at`) < '$assigned_address_expires_in_secs'))
							 OR
							 (`status`='revalidate')
					 )
					 AND (('$current_time' - `received_funds_checked_at`) > '$funds_received_value_expires_in_secs')
					 ORDER BY `received_funds_checked_at` ASC;"; // Check the ones that haven't been checked for longest time
		$rows_for_balance_check = $wpdb->get_results($query, ARRAY_A);

		if (is_array($rows_for_balance_check)) {
			$ran_cycles = 0;
			foreach ($rows_for_balance_check as $row_for_balance_check) {
				$ran_cycles++;  // To limit number of cycles per soft cron job.

				$address_request_array = array();
				// Retrieve current balance at address considering required confirmations number and api_timemout value.
				$address_request_array['btc_address'] = $row_for_balance_check['btc_address'];
				$balance_info_array                   = $this->get_electrum_util()->getreceivedbyaddress_info($address_request_array, true);

				// Prepare 'address_meta' for use.
				$address_meta    = WCP_unserialize_address_meta(@$row_for_balance_check['address_meta']);
				$last_order_info = @$address_meta['orders'][0];
				$row_id          = $row_for_balance_check['id'];

				if ($balance_info_array['result'] == 'success') {
					/*
				$balance_info_array = array (
						'result'                      => 'success',
						'message'                     => "",
						'host_reply_raw'              => "",
						'balance'                     => $funds_received,
						);
				*/

					// Refresh 'received_funds_checked_at' field
					$current_time = time();
					$query        =
						"UPDATE `$crypto_addresses_table_name`
											 SET `total_received_funds` = '{$balance_info_array['balance']}',
													 `received_funds_checked_at`='$current_time'
											 WHERE `id`='$row_id';";
					$ret_code     = $wpdb->query($query);

					if ($balance_info_array['balance'] > 0) {
						if ($row_for_balance_check['status'] == 'revalidate') {
							// Address with suddenly appeared balance. Check if that is matching to previously-placed [likely expired] order
							if (!$last_order_info || !@$last_order_info['order_id'] || !@$balance_info_array['balance'] || !@$last_order_info['order_total']) {
								// No proper metadata present. Mark this address as 'xused' (used by unknown entity outside of this application) and be done with it forever.
								$query    =
									"UPDATE `$crypto_addresses_table_name`
																	 SET `status` = 'xused'
																	 WHERE `id`='$row_id';";
								$ret_code = $wpdb->query($query);
								continue;
							} else {
								// Metadata for this address is present. Mark this address as 'assigned' and treat it like that further down...
								$query    =
									"UPDATE `$crypto_addresses_table_name`
																	 SET `status` = 'assigned'
																	 WHERE `id`='$row_id';";
								$ret_code = $wpdb->query($query);
							}
						}

						WCP__log_event(__FILE__, __LINE__, "Cron job: NOTE: Detected non-zero balance at address: '{$row_for_balance_check['btc_address']}, order ID = '{$last_order_info['order_id']}'. Detected balance ='{$balance_info_array['balance']}'.");

						if ($balance_info_array['balance'] < $last_order_info['order_total']) {
							WCP__log_event(__FILE__, __LINE__, "Cron job: NOTE: balance at address: '{$row_for_balance_check['btc_address']}' (BTC '{$balance_info_array['balance']}') is not yet sufficient to complete it's order (order ID = '{$last_order_info['order_id']}'). Total required: '{$last_order_info['order_total']}'. Will wait for more funds to arrive...");
						}
					} else {
					}

					// Note: to be perfectly safe against late-paid orders, we need to:
					// Scan '$address_meta['orders']' for first UNPAID order that is exactly matching amount at address.
					if ($balance_info_array['balance'] >= $last_order_info['order_total']) {
						// Process full payment event
						/*
					$address_meta =
						 array (
							'orders' =>
							 array (
								// All orders placed on this address in reverse chronological order
								array (
									 'order_id'     => $order_id,
									 'order_total'  => $order_total_in_btc,
									 'order_datetime'  => date('Y-m-d H:i:s T'),
									 'requested_by_ip' => @$_SERVER['REMOTE_ADDR'],
								),
								array (
									 ...
								),
							 ),
							'other_meta_info' => array (...)
						 );
					*/

						// Last order was fully paid! Complete it...
						WCP__log_event(__FILE__, __LINE__, "Cron job: NOTE: Full payment for order ID '{$last_order_info['order_id']}' detected at address: '{$row_for_balance_check['btc_address']}' (BTC '{$balance_info_array['balance']}'). Total was required for this order: '{$last_order_info['order_total']}'. Processing order ...");

						// Update order' meta info
						$address_meta['orders'][0]['paid'] = true;

						// Process and complete the order within WooCommerce (send confirmation emails, etc...)
						WCP__process_payment_completed_for_order("bitcoin_btc", $last_order_info['order_id'], $balance_info_array['balance']);

						// Update address' record
						$address_meta_serialized = WCP_serialize_address_meta($address_meta);

						// Update DB - mark address as 'used'.
						//
						$current_time = time();

						// Note: `total_received_funds` and `received_funds_checked_at` are already updated above.
						//
						$query    =
							"UPDATE `$crypto_addresses_table_name`
													 SET
															`status`='used',
															`address_meta`='$address_meta_serialized'
													 WHERE `id`='$row_id';";
						$ret_code = $wpdb->query($query);
						WCP__log_event(__FILE__, __LINE__, "Cron job: SUCCESS: Order ID '{$last_order_info['order_id']}' successfully completed.");

						// This is not needed here. Let it process as many orders as are paid for in the same loop.
						// Maybe to be moved there --> //..// (to avoid soft-cron checking of balance of hundreds of addresses in a same loop)
						//
						// Return here to avoid overloading too many processing needs to one random visitor.
						// Then it means no more than one order can be processed per 2.5 minutes (or whatever soft cron schedule is).
						// Hard cron is immune to this limitation.
						// if (!$hardcron && $ran_cycles >= $wcp_settings['soft_cron_max_loops_per_run'])
						// {
						// return;
						// }
					}
				} else {
					WCP__log_event(__FILE__, __LINE__, "Cron job: Warning: Cannot retrieve balance for address: '{$row_for_balance_check['btc_address']}: " . $balance_info_array['message']);
				}
				// ..//
			}
		}
	}

	protected function get_table_name()
	{
		return TableBTC::get_table_name();
	}
}

class FaircoinFAIRCronJob extends CryptoCronJob
{

	private $gateway;

	public function __construct()
	{
		parent::__construct();
		$this->gateway = new WCP_FairCoin_FAIR();
	}

	protected function get_settings_name()
	{
		return WCP_FAIR_SETTINGS;
	}

	protected function process_payments()
	{
		global $wpdb;
		$crypto_addresses_table_name = $this->get_table_name();

		$funds_received_value_expires_in_secs = 0; // $this->wcp_settings['funds_received_value_expires_in_mins'] * 60;
		$assigned_address_expires_in_secs     = $this->wcp_settings['assigned_address_expires_in_mins'] * 60;

		$clean_address = null;
		$current_time  = time();

		// Search for completed orders (addresses that received full payments for their orders) ...
		// NULL == not found
		// Retrieve:
		// 'assigned'   - unexpired, with old balances (due for revalidation. Fresh balances and still 'assigned' means no [full] payment received yet)
		// 'revalidate' - all
		// order results by most recently assigned
		$query                  =
			"SELECT * FROM `$crypto_addresses_table_name`
					 WHERE (
							 (`status`='assigned' AND (('$current_time' - `assigned_at`) < '$assigned_address_expires_in_secs'))
							 OR
							 (`status`='revalidate')
					 )
					 AND (('$current_time' - `received_funds_checked_at`) > '$funds_received_value_expires_in_secs')
					 ORDER BY `received_funds_checked_at` ASC;"; // Check the ones that haven't been checked for longest time
		$rows_for_balance_check = $wpdb->get_results($query, ARRAY_A);

		if (is_array($rows_for_balance_check)) {
			$ran_cycles = 0;
			foreach ($rows_for_balance_check as $row_for_balance_check) {
				$ran_cycles++;  // To limit number of cycles per soft cron job.

				$address_request_array = array();
				// Retrieve current balance at address considering required confirmations number and api_timemout value.
				$address_request_array['fair_address'] = $row_for_balance_check['fair_address'];
				$balance_info_array                   = $this->get_electrum_util()->getreceivedbyaddress_info($address_request_array, true);

				// Prepare 'address_meta' for use.
				$address_meta    = WCP_unserialize_address_meta(@$row_for_balance_check['address_meta']);
				$last_order_info = @$address_meta['orders'][0];
				$row_id          = $row_for_balance_check['id'];

				if ($balance_info_array['result'] == 'success') {
					/*
				$balance_info_array = array (
						'result'                      => 'success',
						'message'                     => "",
						'host_reply_raw'              => "",
						'balance'                     => $funds_received,
						);
				*/

					// Refresh 'received_funds_checked_at' field
					$current_time = time();
					$query        =
						"UPDATE `$crypto_addresses_table_name`
											 SET `total_received_funds` = '{$balance_info_array['balance']}',
													 `received_funds_checked_at`='$current_time'
											 WHERE `id`='$row_id';";
					$ret_code     = $wpdb->query($query);

					if ($balance_info_array['balance'] > 0) {
						if ($row_for_balance_check['status'] == 'revalidate') {
							// Address with suddenly appeared balance. Check if that is matching to previously-placed [likely expired] order
							if (!$last_order_info || !@$last_order_info['order_id'] || !@$balance_info_array['balance'] || !@$last_order_info['order_total']) {
								// No proper metadata present. Mark this address as 'xused' (used by unknown entity outside of this application) and be done with it forever.
								$query    =
									"UPDATE `$crypto_addresses_table_name`
																	 SET `status` = 'xused'
																	 WHERE `id`='$row_id';";
								$ret_code = $wpdb->query($query);
								continue;
							} else {
								// Metadata for this address is present. Mark this address as 'assigned' and treat it like that further down...
								$query    =
									"UPDATE `$crypto_addresses_table_name`
																	 SET `status` = 'assigned'
																	 WHERE `id`='$row_id';";
								$ret_code = $wpdb->query($query);
							}
						}

						WCP__log_event(__FILE__, __LINE__, "Cron job: NOTE: Detected non-zero balance at address: '{$row_for_balance_check['fair_address']}, order ID = '{$last_order_info['order_id']}'. Detected balance ='{$balance_info_array['balance']}'.");

						if ($balance_info_array['balance'] < $last_order_info['order_total']) {
							WCP__log_event(__FILE__, __LINE__, "Cron job: NOTE: balance at address: '{$row_for_balance_check['fair_address']}' (FAIR '{$balance_info_array['balance']}') is not yet sufficient to complete it's order (order ID = '{$last_order_info['order_id']}'). Total required: '{$last_order_info['order_total']}'. Will wait for more funds to arrive...");
						}
					} else {
					}

					// Note: to be perfectly safe against late-paid orders, we need to:
					// Scan '$address_meta['orders']' for first UNPAID order that is exactly matching amount at address.
					if ($balance_info_array['balance'] >= $last_order_info['order_total']) {
						// Process full payment event
						/*
					$address_meta =
						 array (
							'orders' =>
							 array (
								// All orders placed on this address in reverse chronological order
								array (
									 'order_id'     => $order_id,
									 'order_total'  => $order_total_in_btc,
									 'order_datetime'  => date('Y-m-d H:i:s T'),
									 'requested_by_ip' => @$_SERVER['REMOTE_ADDR'],
								),
								array (
									 ...
								),
							 ),
							'other_meta_info' => array (...)
						 );
					*/

						// Last order was fully paid! Complete it...
						WCP__log_event(__FILE__, __LINE__, "Cron job: NOTE: Full payment for order ID '{$last_order_info['order_id']}' detected at address: '{$row_for_balance_check['fair_address']}' (FAIR '{$balance_info_array['balance']}'). Total was required for this order: '{$last_order_info['order_total']}'. Processing order ...");

						// Update order' meta info
						$address_meta['orders'][0]['paid'] = true;

						// Process and complete the order within WooCommerce (send confirmation emails, etc...)
						WCP__process_payment_completed_for_order("faircoin_fair", $last_order_info['order_id'], $balance_info_array['balance']);

						// Update address' record
						$address_meta_serialized = WCP_serialize_address_meta($address_meta);

						// Update DB - mark address as 'used'.
						//
						$current_time = time();

						// Note: `total_received_funds` and `received_funds_checked_at` are already updated above.
						//
						$query    =
							"UPDATE `$crypto_addresses_table_name`
													 SET
															`status`='used',
															`address_meta`='$address_meta_serialized'
													 WHERE `id`='$row_id';";
						$ret_code = $wpdb->query($query);
						WCP__log_event(__FILE__, __LINE__, "Cron job: SUCCESS: Order ID '{$last_order_info['order_id']}' successfully completed.");

						// This is not needed here. Let it process as many orders as are paid for in the same loop.
						// Maybe to be moved there --> //..// (to avoid soft-cron checking of balance of hundreds of addresses in a same loop)
						//
						// Return here to avoid overloading too many processing needs to one random visitor.
						// Then it means no more than one order can be processed per 2.5 minutes (or whatever soft cron schedule is).
						// Hard cron is immune to this limitation.
						// if (!$hardcron && $ran_cycles >= $wcp_settings['soft_cron_max_loops_per_run'])
						// {
						// return;
						// }
					}
				} else {
					WCP__log_event(__FILE__, __LINE__, "Cron job: Warning: Cannot retrieve balance for address: '{$row_for_balance_check['btc_address']}: " . $balance_info_array['message']);
				}
				// ..//
			}
		}
	}

	protected function get_gateway()
	{
		return $this->gateway;
	}

	protected function get_table_name()
	{
		return TableFAIR::get_table_name();
	}
}

function WCP_cron_job_worker()
{
	global $wpdb;

	$cron_classes = array('FaircoinFAIRCronJob', 'BitcoinBTCCronJob');

	foreach ($cron_classes as $job_class) {
		$job = new $job_class();
		$job->run();
	}
}