<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

class Shop extends MY_Controller
{
	/**
	 * v10 introduces the concept that this controller is nothing more than an internal API.
	 * Many, if not all, of these functions would be replicated in a Javascript library included in the new shop module.
	 * ie: addToCart() in the Javascript file calls this AJAX endpoint.
	 *
	 * The proposed functions would include:
	 *
	 * addToCart($page_id, $price, $qty, $extra_data=array())
	 *  - returns the cart contents
	 *
	 * updateCart($page_id, $price, $qty)
	 *  - returns the cart contents
	 *
	 * deleteCart($page_id)
	 *  - returns the cart contents
	 *
	 * getShippingOptions()
	 *  - returns the shipping gateways and rates available for the current user's cart
	 *
	 * getPaymentOptions()
	 *  - returns the payment gateways available for the current user's cart
	 *
	 * saveCheckoutData($fields, $data)
	 *  - any fields named in the $fields array will be saved from the $data array/object.
	 *  - allows the front end to decide which fields get saved and when.
	 *  - special fields for "shipping_gateway" and "payment_gateway" would probably be required.
	 *  - the developer must ensure that any required data is collected by the end of the process.
	 */
	const HTTP_OK           = 200;
	const HTTP_CREATED      = 201;
	const HTTP_UNAUTHORIZED = 401;
	const HTTP_FORBIDDEN    = 403;
	const HTTP_NOT_FOUND    = 404;
	const HTTP_CONFLICT     = 409;

	public static $currency = "CAD";

	public function __construct()
	{
		parent::__construct();
		$this->scripts[] = '<script src="/js/min/shop.min.js"></script>';
	}

	public function cart($cart_id=null): void
	{
		if($_SERVER["REQUEST_METHOD"] == "GET")
		{
			$this->cart_get();
		}
		else if($_SERVER["REQUEST_METHOD"] == "POST")
		{
			$this->cart_post();
		}
		else if($_SERVER["REQUEST_METHOD"] == "PUT")
		{
			$this->cart_put();
		}
		else if($_SERVER["REQUEST_METHOD"] == "DELETE")
		{
			$this->cart_delete($cart_id);
		}
	}

	private function cart_get(): void
	{
		$this->load->model("shop/shop_cart");
		$cart_items = $this->shop_cart->GetItemsBySessionID(session_id());

		http_response_code(HTTP_OK);
		$this->spit_json($cart_items);
	}

	private function cart_post(): void
	{
		$this->load->model(array("page", "shop/shop_cart"));

		$page_id         = $this->input->post("page_id");
		$quantity        = $this->input->post("quantity");
		$product_options = $this->input->post("product_options");

		if(!is_array($product_options))
		{
			$product_options = array();
		}

		$page = new Page();
		$page->load($page_id);
		$price = $page->field("price");

		$added      = false;
		$cart_items = $this->shop_cart->GetItemsBySessionID(session_id());
		$cereal     = serialize($product_options);

		//add the product to the cart
		//check our existing cart to see if this product already exists
		foreach($cart_items as $item)
		{
			//product ID and product options must match, otherwise we add a new item
			if($item->page_id == $page_id && serialize($item->product_options) == $cereal)
			{
				//if it does, update the quantity
				$cart_item = new Shop_Cart();
				$cart_item->load($item->cart_id);
				$cart_item->price = $price;
				$cart_item->quantity += $quantity;
				$cart_item->save();

				$added = true;
				break;
			}
		}
		if(!$added)
		{
			$cart_item                  = new Shop_Cart();
			$cart_item->session_id      = session_id();
			$cart_item->page_id         = $page_id;
			$cart_item->price           = $price;
			$cart_item->quantity        = $quantity;
			$cart_item->product_options = serialize($product_options);
			$cart_item->save();
		}

        // echo('<em class="fas fa-fw fa-check" aria-hidden="true"></em> Added To Cart: ' . $quantity . ' x ' . $product->product_name . ($product_variant_id ? ' (' . $variant->variant . ')' : ""));
		$this->clean_application_cache();

		http_response_code(HTTP_CREATED);
		$this->spit_json($cart_item);
	}

	private function cart_put(): void
	{
		$this->load->model(array("page", "shop/shop_cart"));

		mb_parse_str(file_get_contents("php://input"), $put_vars);
		$cart_id         = $put_vars["cart_id"];
		$quantity        = $put_vars["quantity"];

		$cart_item = new Shop_Cart();
		$cart_item->load($cart_id);

		// Ensure that the item being updated is from our session.
		if($cart_item->session_id != session_id())
		{
			http_response_code(HTTP_FORBIDDEN);
			return;
		}

		if($quantity == 0)
		{
			$cart_item->delete();
		}
		else
		{
			$cart_item->quantity = $quantity;
			$cart_item->save();
		}

		$this->clean_application_cache();

		$cart_items = $this->shop_cart->GetItemsBySessionID(session_id());
		http_response_code(HTTP_OK);
		$this->spit_json($cart_items);
	}

	private function cart_delete($cart_id): void
	{
		$this->load->model("shop/shop_cart");

		$cart_item = new Shop_Cart();
		$cart_item->load($cart_id);

		// Ensure that the item being deleted is from our session.
		if($cart_item->session_id != session_id())
		{
			http_response_code(HTTP_FORBIDDEN);
			return;
		}

		if($cart_item->delete())
		{
			$this->clean_application_cache();

			http_response_code(HTTP_OK);
		}
		else
		{
			http_response_code(HTTP_CONFLICT);
		}
	}

	public function process()
	{
		$this->load->model(array("shop/shop_cart", "shop/shop_tax", "shop/shop_order", "shop/shop_payment_gateway", "shop/shop_shipping_gateway", "shop/shop_currency"));

		$messages                    = array();
		$order_vars                  = $this->input->post("order_info");
		$payment_vars                = $this->input->post("payment_info");
		$shipping_vars               = $this->input->post("shipping_info");
		$custom_vars                 = $this->input->post("custom_info");
		$shipping_vars["postalcode"] = str_replace(array(" ", "-"), "", $shipping_vars["postalcode"]);

		$payment_gateway = new Shop_payment_gateway();
		$payment_gateway->load($payment_vars["payment_gateway_id"]);

		$shipping_gateway = new Shop_shipping_gateway();
		$shipping_gateway->load($shipping_vars["shipping_gateway_id"]);

		// Verify taxes, shipping, etc from what the form submitted to what we just calculated to ensure no forgeries have occurred.
		$received_subtotal = $order_vars["subtotal"];
		$received_tax      = $order_vars["tax"];
		$received_shipping = $order_vars["shipping"];
		$received_total    = $order_vars["total"];
		$expected_subtotal = 0;
		$expected_tax      = 0;
		$expected_shipping = $order_vars["shipping"];
		$expected_total    = 0;
		$cart_items        = $this->shop_cart->GetItemsBySessionID(session_id());
		$taxes             = $this->shop_tax->LoadBySiteIDForProvince(Mainframe::site()->site_id, $shipping_vars["country"], $shipping_vars["province"]);

		foreach($cart_items as $item)
		{
			$expected_subtotal += ($item->quantity * $item->price);
		}

		if(count($taxes) > 0)
		{
			$cart_total_before_taxes = $expected_subtotal + $expected_shipping;

			foreach($taxes as $tax)
			{
				$tax_amount = $tax->tax_percentage * $cart_total_before_taxes;
				$expected_tax += $tax_amount;
				//$expected_total += $tax_amount;
			}
		}

		$expected_total 	= $expected_subtotal + $expected_shipping + $expected_tax;
		$expected_subtotal 	= number_format($expected_subtotal, 2);
		$expected_tax 		= number_format($expected_tax, 		2);
		$expected_shipping 	= number_format($expected_shipping, 2);
		$expected_total 	= number_format($expected_total, 	2);
		$received_subtotal 	= number_format($received_subtotal, 2);
		$received_tax 		= number_format($received_tax, 		2);
		$received_shipping 	= number_format($received_shipping, 2);
		$received_total 	= number_format($received_total, 	2);

		// We're going to allow a difference of 5 cents to account for potential rounding errors between JavaScript and PHP.
		// Any difference in values greater than this margin will be flagged and alerted.
		$margin_of_error = 0.05;

		// If our re-calculated total doesn't match the expected total, return to the checkout screen.
		if($expected_total != $received_total)
		{
			$diff = $expected_total - $received_total;

			if($diff < 0)
			{
				$diff *= -1;
			}

			if($diff >= $margin_of_error)
			{
				http_response_code(409);
				$obj         = new StdClass();
				$obj->status = 0;
				$obj->errors = array("Order total conflict. Got $" . $received_total . ", expected $" . $expected_total . ". Please confirm the following information and try again.");
				$this->spit_json($obj);
				die();
			}

			// If there was a minor difference, use the value that the user saw to avoid confusion.
			$order_vars["subtotal"] = $received_subtotal;
			$order_vars["tax"]      = $received_tax;
			$order_vars["shipping"] = $received_shipping;
			$order_vars["total"]    = $received_total;
		}

		if($expected_tax != $received_tax)
		{
			$diff = $expected_tax - $received_tax;

			if($diff < 0)
			{
				$diff *= -1;
			}

			if($diff >= $margin_of_error)
			{
				http_response_code(409);
				$obj         = new StdClass();
				$obj->status = 0;
				$obj->errors = array("Tax total conflict. Got $" . $received_tax . ", expected $" . $expected_tax . ". Please confirm the following information and try again.");
				$this->spit_json($obj);
				die();
			}

			// If there was a minor difference, use the value that the user saw to avoid confusion.
			$order_vars["tax"] = $received_tax;
		}
		if($expected_shipping != $received_shipping)
		{
			$diff = $expected_shipping - $received_shipping;

			if($diff < 0)
			{
				$diff *= -1;
			}

			if($diff >= $margin_of_error)
			{
				http_response_code(409);
				$obj         = new StdClass();
				$obj->status = 0;
				$obj->errors = array("Shipping total conflict. Got $" . $received_shipping . ", expected $" . $expected_shipping . ". Please confirm the following information and try again.");
				$this->spit_json($obj);
				die();
			}

			// If there was a minor difference, use the value that the user saw to avoid confusion.
			$order_vars["shipping"] = $received_shipping;
		}

		$user                      = get_user();
		$user->phone               = $shipping_vars["phone"];
		$user->shipping_address    = $shipping_vars["address"];
		$user->shipping_city       = $shipping_vars["city"];
		$user->shipping_postalcode = $shipping_vars["postalcode"];
		$user->shipping_province   = $shipping_vars["province"];
		$user->shipping_country    = $shipping_vars["country"];
		$user->save();

		mt_srand();

		// Create order object - it won't be saved unless payment is approved.
		$order                           = new Shop_Order();
		$order->site_id                  = Mainframe::site()->site_id;
		$order->order_date               = date("Y-m-d H:i:s");
		$order->ship_date                = null;
		$order->order_status_id          = 1;
		$order->customer_postalcode      = str_replace(array(" ", "-"), "", $shipping_vars["postalcode"]);
		$order->payment_gateway_id       = $payment_vars["payment_gateway_id"];
		$order->shipping_gateway_id      = $shipping_vars["shipping_gateway_id"];
		$order->shipping_gateway_service = (isset($shipping_vars["shipping_gateway"]) ? $shipping_vars["shipping_gateway"] : null);
		$order->currency_id              = self::currency_id();
		$order->customer_name            = $shipping_vars["name"];
		$order->customer_phone           = $shipping_vars["phone"];
		$order->customer_email           = $shipping_vars["email"];
		$order->customer_address         = $shipping_vars["address"];
		$order->customer_city            = $shipping_vars["city"];
		$order->customer_province        = $shipping_vars["province"];
		$order->customer_postalcode      = $shipping_vars["postalcode"];
		$order->customer_country         = $shipping_vars["country"];
		$order->products                 = serialize($cart_items);
		$order->taxes                    = serialize($taxes);
		$order->shipping_total           = $order_vars["shipping"];
		$order->tax_total                = $order_vars["tax"];
		$order->order_subtotal           = $order_vars["subtotal"];
		$order->order_total              = $order_vars["total"];
		$order->custom_fields            = serialize($custom_vars);
		$order->tracking_number1         = "";
		$order->tracking_number2         = "";
		$order->tracking_number3         = "";

		// Logging in to place an order is optional.
		if(isset($user->user_id) && $user->user_id > 0)
		{
			$order->user_id = $user->user_id;
		}
		else
		{
			$order->user_id = null;
		}

		// Add order to system.
		if($order->save())
		{
			$this->clean_application_cache();

			// Process payment.
			$this->load->helper("payment");
			$this->load->model("payment/" . $payment_gateway->filename);

			$this->{$payment_gateway->filename}->init();
			$txn_id = $this->{$payment_gateway->filename}->process($order, $payment_vars, $messages);

			if($txn_id !== false)
			{
				$order->txn_id = $txn_id;

				// Update the order.
				if($order->save())
				{
					$this->clean_application_cache();

					// Reload the order with full details.
					$order->LoadWithDetails($order->order_id);

					$currency = new Shop_currency();
					$currency->load($order->currency_id);

					ob_start();
					$this->load->view("modules/shop/emails/order", array("order" => $order, "currency" => $currency));
					$message = ob_get_clean();

					send_notification($order->customer_email, "Purchase Receipt", $message);

					$admins = explode(",", Mainframe::site()->order_notifications);
			    	send_notification($admins, "Order Received", $message);
			    }
			    else
			    {
			    	http_response_code(409);
					$obj         = new StdClass();
					$obj->status = 0;
					$obj->errors = array("Your payment appears to be complete, but we could not save your order information. Please leave this screen up for reference and contact us to confirm your order.");
					$this->spit_json($obj);
					die();
			    }

				// Clear cart for this session ID.
				$this->shop_cart->EmptyBySessionID(session_id());
				$this->clean_application_cache();
			}
			else
			{
				http_response_code(409);
				$obj         = new StdClass();
				$obj->status = 0;
				$obj->errors = $messages;
				$this->spit_json($obj);
				die();
			}
		}

		http_response_code(200);
		$obj           = new StdClass();
		$obj->status   = 1;
		$obj->order    = $order;
		$obj->messages = $messages;
		$this->spit_json($obj);
	}

	public static function currency()
	{
		$CI =& get_instance();

		$CI->load->model(array("shop/shop_currency"));

		$c = new Shop_Currency();
		$c->Load(Mainframe::site()->currency_id);

		return $c;
	}

	public static function currency_id()
	{
		return self::currency()->currency_id;
	}

	public static function currency_exchange()
	{
		return self::currency()->exchange_rate;
	}

	public static function get_currencies()
	{
		if(isset(self::$currencies))
		{
			return self::$currencies;
		}

		$CI =& get_instance();

		$CI->load->model("shop/shop_currency");

		$currencies = $CI->shop_currency->LoadAll();

		foreach($currencies as $c)
		{
			self::$currencies[$c->currency_code] = $c;
		}

		return self::$currencies;
	}

	public static function convert_currency($value, $from, $to)
	{
		$CI =& get_instance();
		$CI->load->model("shop/shop_currency");

		$cfrom = new Shop_Currency();
		$cfrom->LoadByCode($from);

		$cto = new Shop_Currency();
		$cto->LoadByCode($to);

		if($cfrom->exchange_rate > $cto->exchange_rate)
		{
			$diff = $cfrom->exchange_rate - $cto->exchange_rate + 1;
			return $value * $diff;
		}
		else if($cfrom->exchange_rate < $cto->exchange_rate)
		{
			$diff = $cto->exchange_rate - $cfrom->exchange_rate + 1;
			return $value * $diff;
		}
		else if($cfrom->exchange_rate == $cto->exchange_rate)
		{
			return $value;
		}

		//absolute failsafe - all conditions are already covered above
		return $value;
	}

	/**
	 * Take a postal code and look up city/country info.
	 */
	public function geocode($postalcode=null, $return=false)
	{
		if($postalcode == null)
		{
			$postalcode = $this->input->get_post("postalcode");
		}

		$postalcode = str_replace(array(" ", "-"), "", $postalcode);

		$this->load->model("shop/shop_postalcode");

		$pc = new Shop_Postalcode();
		$pc->LoadByCode($postalcode);

		if(isset($pc->postalcode_id) && $pc->postalcode_id > 0)
		{
			if($return)
			{
				return $pc;
			}
			else
			{
				$this->spit_json($pc);
			}
		}

		return null;
	}

	public function taxes()
	{
		$country  = $this->input->get_post("country");
		$province = $this->input->get_post("province");

		$this->load->helper("mainframe");
		$this->load->model("shop/shop_tax");
		Mainframe::init();

		$taxes = $this->shop_tax->LoadBySiteIDForProvince(Mainframe::site()->site_id, $country, $province);

		$this->spit_json($taxes);
	}

	public function shipping_rates()
	{
		$shipping_gateway_id = $this->input->get_post("shipping_gateway_id");

		$this->load->helper(array("mainframe", "shipping"));
		$this->load->model(array("shop/shop_cart", "shop/shop_shipping_gateway"));

		$gateway = new Shop_Shipping_Gateway();
		$gateway->load($shipping_gateway_id);

		$cart = new Shop_Cart();
		$cart_items = $cart->GetItemsBySessionID(session_id());

		$gateway_data               = array();
		$gateway_data["cart_items"] = $cart_items;
		$gateway_data["my_gateway"] = $gateway;

		$this->load->model("shipping/" . $gateway->filename);
		$gfn = $gateway->filename;
		$rates = $this->$gfn->get_rates($gateway_data);

		$gateway_data["rates"] = $rates;
		?>
		<div class="shipping_gateway" id="shipping_gateway<?php echo($gateway->shipping_gateway_id); ?>form">
			<?php $this->load->view("shop/shipping/" . $gateway->filename, $gateway_data); ?>
		</div>
		<?php
	}

	public function update_exchange_rates()
	{
		$this->load->helper("mainframe");
		$this->load->model("shop/shop_currency");
		Mainframe::init();

		$currencies = $this->shop_currency->get();

		$url = "http://openexchangerates.org/api/latest.json?app_id=f05be6ad81164e9e8ffe777bd2aca8f0";

		$ch = curl_init($url);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
		$rates = curl_exec($ch);
		curl_close($ch);

		$rates = json_decode($rates);
		$rates = $rates->rates;

		if(!is_object($rates))
		{
			//something has gone wrong, probably a server outage
			return;
		}

		$base = "";
		$base_value = 0;
		$base_diff = 0;

		//find our base currency, probably CAD
		foreach($currencies as $currency)
		{
			if($currency->exchange_rate == 1)
			{
				$base = $currency->currency_code;
				$base_value = 1/$rates->$base;
				$base_diff = 1-$base_diff;
			}
		}

		//because the base currency returned by OpenExchangeRates is always USD its easiest to convert OUR base currency to USD, then convert to each currency based on the new value, which represents OUR base currency as a percent of all others.
		foreach($currencies as $currency)
		{
			//NEVER update our base currency. If base currency gets messed up, all values eventually turn to zero and prices reflect zero.
			if($currency->exchange_rate == 1)
			{
				continue;
			}

			$currency_code = $currency->currency_code;
			$exchange_rate = $rates->$currency_code * $base_value;

			$c = new Shop_Currency();
			$c->LoadByCode($currency->currency_code);
			$c->exchange_rate = $exchange_rate;
			$c->save();
		}
	}
}
