<?php
/**
 * Copyright 2017-2020 Nick Korbel
 *
 * This file is part of Booked Scheduler.
 *
 * Booked Scheduler is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Booked Scheduler is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Booked Scheduler.  If not, see <http://www.gnu.org/licenses/>.
 */

require ROOT_DIR . 'lib/external/Stripe/init.php';
require ROOT_DIR . 'lib/external/Unirest/Unirest.php';

class PaymentGateways
{
	const PAYPAL = 'PayPal';
	const STRIPE = 'Stripe';
}

class PaymentGatewaySetting
{
	/**
	 * @var string
	 */
	private $name;
	/**
	 * @var string
	 */
	private $value;

	/**
	 * @param string $name
	 * @param string $value
	 */
	public function __construct($name, $value)
	{
		$this->name = $name;
		$this->value = $value;
	}

	/**
	 * @return string
	 */
	public function Name()
	{
		return $this->name;
	}

	/**
	 * @return string
	 */
	public function Value()
	{
		return $this->value;
	}
}

interface IPaymentGateway
{
	/**
	 * @return string
	 */
	public function GetGatewayType();

	/**
	 * @return bool
	 */
	public function IsEnabled();

	/**
	 * @return PaymentGatewaySetting[]
	 */
	public function Settings();
}

interface IPaymentTransactionLogger
{
	/**
	 * @param string $userId
	 * @param string $status
	 * @param string $invoiceNumber
	 * @param int $transactionId
	 * @param float $totalAmount
	 * @param float $transactionFee
	 * @param string $currency
	 * @param string $transactionHref
	 * @param string $refundHref
	 * @param Date $dateCreated
	 * @param string $gatewayDateCreated
	 * @param string $gatewayName
	 * @param string $gatewayResponse
	 */
	public function LogPayment($userId, $status, $invoiceNumber, $transactionId, $totalAmount, $transactionFee, $currency, $transactionHref, $refundHref,
							   $dateCreated, $gatewayDateCreated, $gatewayName, $gatewayResponse);

	/**
	 * @param string $paymentTransactionLogId
	 * @param string $status
	 * @param int $transactionId
	 * @param float $totalRefundAmount
	 * @param float $paymentRefundAmount
	 * @param float $feeRefundAmount
	 * @param string $transactionHref
	 * @param Date $dateCreated
	 * @param string $gatewayDateCreated
	 * @param string $refundResponse
	 */
	public function LogRefund($paymentTransactionLogId, $status, $transactionId, $totalRefundAmount, $paymentRefundAmount, $feeRefundAmount, $transactionHref,
							  $dateCreated, $gatewayDateCreated, $refundResponse);
}

class PaymentTransactionLogger implements IPaymentTransactionLogger
{
	public function LogPayment($userId, $status, $invoiceNumber, $transactionId, $totalAmount, $transactionFee, $currency, $transactionHref, $refundHref,
							   $dateCreated, $gatewayDateCreated, $gatewayName, $gatewayResponse)
	{
		ServiceLocator::GetDatabase()->Execute(new AddPaymentTransactionLogCommand($userId, $status, $invoiceNumber, $transactionId, $totalAmount,
																				   $transactionFee, $currency, $transactionHref, $refundHref, $dateCreated,
																				   $gatewayDateCreated, $gatewayName, $gatewayResponse));
	}

	public function LogRefund($paymentTransactionLogId, $status, $transactionId, $totalRefundAmount, $paymentRefundAmount, $feeRefundAmount, $transactionHref,
							  $dateCreated, $gatewayDateCreated, $refundResponse)
	{
		ServiceLocator::GetDatabase()->Execute(new AddRefundTransactionLogCommand($paymentTransactionLogId, $status, $transactionId, $totalRefundAmount,
																				  $paymentRefundAmount, $feeRefundAmount, $transactionHref, $dateCreated,
																				  $gatewayDateCreated, $refundResponse));
	}
}

class PayPalGateway implements IPaymentGateway
{
	const CLIENT_ID = 'client_id';
	const SECRET = 'secret';
	const ENVIRONMENT = 'environment';
	const ACTION_PAYMENT = 'payment';
	const ACTION_CANCEL = 'cancel';
	const ACTION_EXECUTE = 'execute';

	/**
	 * @var bool
	 */
	private $enabled;
	/**
	 * @var string
	 */
	private $clientId;
	/**
	 * @var string
	 */
	private $secret;
	/**
	 * @var string
	 */
	private $environment;

	/**
	 * @param bool $enabled
	 * @param string $clientId
	 * @param string $secret
	 * @param string $environment
	 */
	public function __construct($enabled, $clientId, $secret, $environment)
	{
		$this->enabled = $enabled;
		if ($enabled)
		{
			$this->clientId = $clientId;
			$this->secret = $secret;
			$this->environment = $environment;
		}
	}

	/**
	 * @param string $clientId
	 * @param string $secret
	 * @param string $environment
	 * @return PayPalGateway
	 */
	public static function Create($clientId, $secret, $environment)
	{
		$enabled = (!empty($clientId) && !empty($secret) && !empty($environment));
		return new PayPalGateway($enabled, $clientId, $secret, $environment);
	}

	public function GetGatewayType()
	{
		return PaymentGateways::PAYPAL;
	}

	public function IsEnabled()
	{
		return $this->enabled;
	}

	public function Settings()
	{
		return array(
				new PaymentGatewaySetting(self::CLIENT_ID, $this->ClientId()),
				new PaymentGatewaySetting(self::SECRET, $this->Secret()),
				new PaymentGatewaySetting(self::ENVIRONMENT, $this->Environment()),
		);
	}

	/**
	 * @return string
	 */
	public function ClientId()
	{
		return $this->clientId;
	}

	/**
	 * @return string
	 */
	public function Secret()
	{
		return $this->secret;
	}

	/**
	 * @return string
	 */
	public function Environment()
	{
		return $this->environment;
	}

	private function GetAuthToken($baseUrl)
	{
		$authUrl = "$baseUrl/v1/oauth2/token";
		$headers = array('Accept' => 'application/json', 'Accept-Language' => 'en_US', 'Content-Type' => 'application/x-www-form-urlencoded');
		$data = array('grant_type' => 'client_credentials');
		$body = Unirest\Request\Body::form($data);
		Unirest\Request::auth($this->ClientId(), $this->Secret());
		Unirest\Request::verifyPeer(false);
		$response = Unirest\Request::post($authUrl, $headers, $body);

		return $response->body->access_token;
	}

	private function GetBaseUrl()
	{
		$baseUrl = "https://api.paypal.com";
		$paypalEnvironment = $this->Environment();
		if (strtolower($paypalEnvironment) == "sandbox")
		{
			$baseUrl = "https://api.sandbox.paypal.com";
		}
		return $baseUrl;
	}

	/**
	 * @param CreditCartSession $cart
	 * @param string $returnUrl
	 * @param string $cancelUrl
	 * @return object
	 */
	public function CreatePayment(CreditCartSession $cart, $returnUrl, $cancelUrl)
	{
		$resources = Resources::GetInstance();
		$baseUrl = $this->GetBaseUrl();
		$token = $this->GetAuthToken($baseUrl);

		try
		{
			Log::Debug('PayPal Checkout/Orders CartId/invoice number: %s, Total: %s', $cart->Id(), $cart->Total());
			$checkoutUrl = "$baseUrl/v2/checkout/orders";
			$headers = array('Accept' => 'application/json', 'Accept-Language' => 'en_US', 'Content-Type' => 'application/json', "Authorization" => "Bearer $token");
			$purchaseRequest = array('description' => $resources->GetString('CreditPurchase'), 'amount' => array('value' => "{$cart->Total()}", 'currency_code' => $cart->Currency));
			$data = array(
					'intent' => 'CAPTURE',
					'application_context' => array('return_url' => $returnUrl, 'cancel_url' => $cancelUrl),
					'purchase_units' => array($purchaseRequest));
			$body = Unirest\Request\Body::json($data);
			Unirest\Request::verifyPeer(false);
			$response = Unirest\Request::post($checkoutUrl, $headers, $body);

			if (Log::DebugEnabled())
			{
				Log::Debug("PayPal Checkout/Orders Url: %s, Request: %s, Response: %s", $checkoutUrl, $body, json_encode($response->body));
			}

			return $response->body;
		} catch (Exception $exception)
		{
			Log::Error('PayPal Checkout/Orders error details. Data: %s, CartId/invoice number: %s, Total: %s, Error: %s', $body, $cart->Id(),
					   $cart->Total(), $exception);
		}

		return null;
	}

	/**
	 * @param CreditCartSession $cart
	 * @param string $paymentId
	 * @param string $payerId
	 * @param IPaymentTransactionLogger $logger
	 * @return object
	 */
	public function ExecutePayment(CreditCartSession $cart, $paymentId, $payerId, IPaymentTransactionLogger $logger)
	{
		$baseUrl = $this->GetBaseUrl();
		$token = $this->GetAuthToken($baseUrl);
		try
		{
			Log::Debug('PayPal Capture CartId/invoice number: %s, Total: %s', $cart->Id(), $cart->Total());
			$checkoutUrl = "$baseUrl/v2/checkout/orders/$paymentId/capture";
			$headers = array('Accept' => 'application/json', 'Accept-Language' => 'en_US', 'Content-Type' => 'application/json', "Authorization" => "Bearer $token");
			Unirest\Request::verifyPeer(false);
			$response = Unirest\Request::post($checkoutUrl, $headers);

			$sale = $response->body->purchase_units[0]->payments->captures[0];
			$self = "";
			$refund = "";
			foreach ($sale->links as $link)
			{
				if ($link->rel == "self")
				{
					$self = $link->href;
				}
				if ($link->rel == "refund")
				{
					$refund = $link->href;
				}
			}

			$logger->LogPayment($cart->UserId,
								$response->body->status,
								$sale->id,
								$response->body->id,
								$sale->amount->value,
								$sale->seller_receivable_breakdown->paypal_fee->value,
								$sale->amount->currency_code,
								$self,
								$refund,
								Date::Now(),
								$sale->create_time,
								$this->GetGatewayType(),
								json_encode($response->body));
			if (Log::DebugEnabled())
			{
				Log::Debug("PayPal Capture Url: %s, Response: %s", $checkoutUrl, json_encode($response->body));
			}

			return $response->body;
		} catch (Exception $exception)
		{
			Log::Error('PayPal Capture error details. CartId/invoice number: %s, Total: %s, Error: %s', $cart->Id(), $cart->Total(), $exception);
		}
		return null;
	}

	/**
	 * @param TransactionLogView $log
	 * @param float $amount
	 * @param IPaymentTransactionLogger $logger
	 * @return object
	 */
	public function Refund(TransactionLogView $log, $amount, IPaymentTransactionLogger $logger)
	{
		$baseUrl = $this->GetBaseUrl();
		$token = $this->GetAuthToken($baseUrl);

		try
		{
			Log::Debug('PayPal Refund. TransactionId: %s, InvoiceNumber: %s, Total: %s', $log->TransactionId, $log->InvoiceNumber, $amount);
			$refundUrl = "$baseUrl/v2/payments/captures/{$log->InvoiceNumber}/refund";
//			$refundUrl = "$baseUrl/v2/payments/captures/{$log->TransactionId}/refund";
			$headers = array('Accept' => 'application/json', 'Accept-Language' => 'en_US', 'Content-Type' => 'application/json', "Authorization" => "Bearer $token");
			$data = array('amount' => array('value' => "{$amount}", 'currency_code' => $log->Currency));
			$body = Unirest\Request\Body::json($data);
			Unirest\Request::verifyPeer(false);
			$response = Unirest\Request::post($refundUrl, $headers, $body);

			if (Log::DebugEnabled())
			{
				Log::Debug("PayPal Refund Url: %s, Request: %s, Response: %s", $refundUrl, $body, json_encode($response->body));
			}

			$self = "";
			foreach ($response->body->links as $link)
			{
				if ($link->rel == "self")
				{
					$self = $link->href;
				}
			}

			$breakdown = $response->body->seller_payable_breakdown;

			$logger->LogRefund($log->Id,
							   $response->body->status,
							   $response->body->id,
							   $breakdown ? $breakdown->total_refunded_amount->value : $amount,
							   $breakdown ? $breakdown->gross_amount->value : 0,
							   $breakdown ? $breakdown->paypal_fee->value : 0,
							   $self,
							   Date::Now(),
							   $response->body->create_time ? $response->body->create_time : "",
							   json_encode($response->body));

			return $response->body;
		} catch (Exception $exception)
		{
			Log::Error('Error refunding PayPal payment. TransactionId %s, Total %s, Error %s', $log->TransactionId, $amount, $exception);
		}

		return null;
	}
}

class StripeGateway implements IPaymentGateway
{
	const PUBLISHABLE_KEY = 'publishable_key';
	const SECRET_KEY = 'secret_key';

	/**
	 * @var bool
	 */
	private $enabled;
	/**
	 * @var string
	 */
	private $publishableKey;
	/**
	 * @var string
	 */
	private $secretKey;
	/**
	 * @var TransactionLogView
	 */
	public $_LastTransactionView;
	/**
	 * @var bool
	 */
	public $_Refunded;
	/**
	 * @var float
	 */
	public $_LastRefundAmount;

	/**
	 * @param bool $enabled
	 * @param string $publishableKey
	 * @param string $secretKey
	 */
	public function __construct($enabled, $publishableKey, $secretKey)
	{
		$this->enabled = $enabled;
		if ($enabled)
		{
			$this->publishableKey = $publishableKey;
			$this->secretKey = $secretKey;
		}
	}

	/**
	 * @param string $publishableKey
	 * @param string $secretKey
	 * @return StripeGateway
	 */
	public static function Create($publishableKey, $secretKey)
	{
		$enabled = (!empty($publishableKey) && !empty($secretKey));
		return new StripeGateway($enabled, $publishableKey, $secretKey);
	}

	public function GetGatewayType()
	{
		return PaymentGateways::STRIPE;
	}

	public function IsEnabled()
	{
		return $this->enabled;
	}

	public function Settings()
	{
		return array(
				new PaymentGatewaySetting(self::PUBLISHABLE_KEY, $this->PublishableKey()),
				new PaymentGatewaySetting(self::SECRET_KEY, $this->SecretKey()),
		);
	}

	/**
	 * @return string
	 */
	public function PublishableKey()
	{
		return $this->publishableKey;
	}

	/**
	 * @return string
	 */
	public function SecretKey()
	{
		return $this->secretKey;
	}

	/**
	 * @param CreditCartSession $cart
	 * @param string $email
	 * @param string $token
	 * @param IPaymentTransactionLogger $logger
	 * @return bool
	 */
	public function Charge(CreditCartSession $cart, $email, $token, IPaymentTransactionLogger $logger)
	{
		try
		{
			\Stripe\Stripe::setApiKey($this->SecretKey());

			$customer = \Stripe\Customer::create(array(
														 'email' => $email,
														 'source' => $token
												 ));

			$currency = new \Booked\Currency($cart->Currency);

			$charge = \Stripe\Charge::create(array(
													 'customer' => $customer->id,
													 'amount' => $currency->ToStripe($cart->Total()),
													 'currency' => strtolower($cart->Currency),
													 'description' => Resources::GetInstance()->GetString('Credits'),
													 'expand' => array('balance_transaction')
											 ));

			if (Log::DebugEnabled())
			{
				Log::Debug('Stripe charge response %s', json_encode($charge));
			}

			$logger->LogPayment($cart->UserId, $charge->status, $charge->invoice, $charge->id, $currency->FromStripe($charge->amount),
								$currency->FromStripe($charge->balance_transaction->fee), $cart->Currency, null, null, Date::Now(), $charge->created,
								$this->GetGatewayType(), json_encode($charge));
			return $charge->status == 'succeeded';
		} catch (\Stripe\Error\Card $ex)
		{
			// Declined
			$body = $ex->getJsonBody();
			$err = $body['error'];
			Log::Debug('Stripe charge failed. http status %s, type %s, code %s, param %s, message %s', $ex->getHttpStatus(), $err['type'], $err['code'],
					   $err['param'], $err['message']);
		} catch (\Stripe\Error\RateLimit $ex)
		{
			Log::Error('Stripe - too many requests. %s', $ex);
		} catch (\Stripe\Error\InvalidRequest $ex)
		{
			Log::Error('Stripe - invalid request. %s', $ex);
		} catch (\Stripe\Error\Authentication $ex)
		{
			Log::Error('Stripe - authentication error. %s', $ex);
		} catch (\Stripe\Error\ApiConnection $ex)
		{
			Log::Error('Stripe - connection failure. %s', $ex);
		} catch (\Stripe\Error\Base $ex)
		{
			Log::Error('Stripe - error. %s', $ex);
		} catch (Exception $ex)
		{
			Log::Error('Stripe - internal error. %s', $ex);
		}

		return false;
	}

	/**
	 * @param TransactionLogView $log
	 * @param float $amount
	 * @param IPaymentTransactionLogger $logger
	 * @return bool
	 */
	public function Refund(TransactionLogView $log, $amount, IPaymentTransactionLogger $logger)
	{
		try
		{
			$currency = new \Booked\Currency($log->Currency);

			\Stripe\Stripe::setApiKey($this->SecretKey());
			$refund = \Stripe\Refund::create(array(
													 'charge' => $log->TransactionId,
													 'amount' => $currency->ToStripe($amount),
													 'expand' => array('balance_transaction')
											 ));

			if (Log::DebugEnabled())
			{
				Log::Debug('Stripe refund response %s', json_encode($refund));
			}

			$logger->LogRefund($log->Id, $refund->status, $refund->id, $currency->FromStripe($refund->amount), $currency->FromStripe($refund->amount),
							   $currency->FromStripe($refund->balance_transaction->fee), null, Date::Now(), $refund->created, json_encode($refund));

			return $refund->status == 'succeeded';
		} catch (\Stripe\Error\Card $ex)
		{
			// Declined
			$body = $ex->getJsonBody();
			$err = $body['error'];
			Log::Debug('Stripe refund  failed. http status %s, type %s, code %s, param %s, message %s', $ex->getHttpStatus(), $err['type'], $err['code'],
					   $err['param'], $err['message']);
		} catch (\Stripe\Error\RateLimit $ex)
		{
			Log::Error('Stripe refund - too many requests. %s', $ex);
		} catch (\Stripe\Error\InvalidRequest $ex)
		{
			Log::Error('Stripe refund - invalid request. %s', $ex);
		} catch (\Stripe\Error\Authentication $ex)
		{
			Log::Error('Stripe refund - authentication error. %s', $ex);
		} catch (\Stripe\Error\ApiConnection $ex)
		{
			Log::Error('Stripe refund - connection failure. %s', $ex);
		} catch (\Stripe\Error\Base $ex)
		{
			Log::Error('Stripe - error. %s', $ex);
		} catch (Exception $ex)
		{
			Log::Error('Stripe refund - internal error. %s', $ex);
		}

		return false;
	}
}