<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace SmilePack\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Factory;
use Joomla\Registry\Registry;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\Component\ComponentHelper;
use NRFramework\Widgets\Widget;

class PayPal extends Widget
{
	/**
	 * The component params.
	 * 
	 * @var Registry
	 */
	private $params;
	
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $widget_options = [
		/**
		 * Enable test mode.
		 * 
		 * Both the PayPal Client ID and Secret must be sandbox credentials.
		 * 
		 * This is used for creating the plan id in AJAX call.
		 */
		'testmode' => false,
		
		/**
		 * The plan ID is required for recurring subscriptions.
		 * 
		 * This is optional but if given it'll be used to create the subscription based on the provided plan_id.
		 * 
		 * Tip: You can create a plan id manually via your PayPal dashboard: https://www.paypal.com/billing/plans
		 */
		'plan_id' => '',
		
		// The type of PayPal button to load
		'label' => 'paypal',
		
		/**
		 * The button layout.
		 * 
		 * Default: vertical
		 * 
		 * Available options:
		 * 
		 * - vertical: Display the buttons vertically
		 * - horizontal: Display the buttons horizontally. Supports up to 2 buttons. Doesn't support credit card option.
		 */
		'button_layout' => 'vertical',

		/**
		 * The button color.
		 * 
		 * Available options:
		 * - gold
		 * - blue
		 * - silver
		 * - black
		 * - white
		 */
		'color' => 'gold',

		/**
		 * The button corner style.
		 * 
		 * Available options:
		 * 
		 * - sharp
		 * - rect
		 * - pill
		 */
		'corner_style' => 'rect',

		// The button max width.
		'max_width' => '',

		/**
		 * Show the PayPal tagline.
		 * 
		 * This works only for the horizontal layout.
		 */
		'tagline' => true,

		/**
		 * The product type.
		 * 
		 * Available options:
		 * - PHYSICAL: A physical product
		 * - DIGITAL: A digital product
		 * - SERVICE: A service
		 */
		'product_type' => '',

		// The product name
		'item_name' => '',

		// The billing amount
		'billing_amount' => '',

		/**
		 * The billing cycle.
		 * 
		 * Available options:
		 * - onetime
		 * - week
		 * - month
		 * - year
		 * - custom (Set the recurring interval in the recurring_interval option)
		 */
		'billing_cycle' => 'onetime',

		// The recurring interval in days
		'recurring_interval' => '',

		/**
		 * The currency code.
		 * 
		 * Currency codes: https://developer.paypal.com/reference/currency-codes/
		 */
		'currency' => 'USD',

		// The tax rate (1-100)%
		'tax_rate' => 0,

		// The shipping amount
		'shipping_amount' => 0,

		/**
		 * The locale that'll be used on the PayPal checkout page.
		 * 
		 * Locale codes: https://developer.paypal.com/reference/locale-codes/
		 */
		'locale' => '',

		/**
		 * The payment payments.
		 * 
		 * Available options:
		 * 
		 * - auto: Show all available payment methods
		 * - custom: Allows the use of include_payment_methods and exclude_payment_methods payment methods
		 */
		'payment_methods' => 'auto',

		// Includes specific payment methods.
		'include_payment_methods' => [],

		// Excludes specific payment methods.
		'exclude_payment_methods' => [],

		// Set whether to show the shipping address within the PayPal checkout form
		'show_shipping_address' => true,

		/**
		 * The success action.
		 * 
		 * Available options:
		 * - message
		 * - redirect
		 */
		'action' => 'message',

		// The success message
		'success_message' => '',

		// The success redirect URL
		'success_url' => '',

		// Set whether to hide the payment buttons after a successful payment
		'success_hide_payment_buttons' => true
	];

	/**
	 * Renders the widget with the given layout
	 * 
	 * @return  void
	 */
	public function render()
	{
		$this->params = $this->getParams();

		if (!$this->isReady())
		{
			return;
		}

		$this->prepare();
		
		$this->styles();

		$this->loadMedia();

		$defaultPath  = implode(DIRECTORY_SEPARATOR, [JPATH_ADMINISTRATOR, 'components', 'com_smilepack', 'layouts']);

		$layout = new FileLayout('widgets.' . $this->getName() . '.' . $this->options['layout'], null, ['debug' => false]);
		$layout->addIncludePaths($defaultPath);

		return $layout->render($this->options);
	}

	/**
	 * Checks if the widget is ready to be rendered.
	 * 
	 * @return  boolean
	 */
	private function isReady()
	{
		if (is_bool($this->params))
		{
			echo Text::_('COM_SMILEPACK_CANNOT_FIND_PAYPAL_PARAMS');
			return false;
		}

		\NRFramework\Functions::loadLanguage('com_smilepack');

		$prefix = $this->options['testmode'] ? 'sandbox' : 'live';
		$mode_label = Text::_('COM_SMILEPACK_' . strtoupper($prefix));

		$url = Uri::base() . 'administrator/index.php?option=com_config&view=component&component=com_smilepack';

		$keys = $this->getKeys($this->options['testmode']);

		$error = false;
		
		// Abort if the client ID is missing
		if (empty($keys['client_id']))
		{
			echo '<p>' . Text::sprintf('COM_SMILEPACK_PAYPAL_CLIENT_ID_KEY_MISSING', $mode_label, $mode_label, $url) . '</p>';
			$error = true;
		}
		
		
		// Abort if the client secret is missing
		if ($this->options['pro'] && !empty($this->options['billing_cycle']) && $this->options['billing_cycle'] !== 'onetime' && empty($keys['client_secret']))
		{
			echo '<p> ' .Text::sprintf('COM_SMILEPACK_PAYPAL_CLIENT_SECRET_KEY_MISSING', $mode_label, $mode_label, $url) . '</p>';
			$error = true;
		}
		

		return !$error;
	}

	/**
	 * Prepare the widget.
	 * 
	 * @return  void
	 */
	private function prepare()
	{
		
		if ($this->isSubscription())
		{
			$this->options['token'] = Session::getFormToken();
			$this->options['root_url'] = Uri::base();
		}
		

		$this->replaceLocalSmartTags();
	}

	private function replaceLocalSmartTags()
	{
		if (!$this->options['pro'])
		{
			return;
		}

		$replacements = [
			'{this.product_name}' => $this->options['item_name'],
			'{this.billing_amount}' => $this->getTotalAmount(),
			'{this.currency}' => $this->options['currency']
		];

		$replacement_keys = [
			'item_name'
		];

		// Replace success message
		if ($this->options['action'] === 'message')
		{
			$replacement_keys[] = 'success_message';
		}
		
		
		// Replace success URL
		if ($this->options['action'] === 'redirect')
		{
			$replacement_keys[] = 'success_url';
		}
		
		
		foreach ($replacement_keys as $key)
		{
			$this->options[$key] = str_replace(array_keys($replacements), array_values($replacements), $this->options[$key]);
		}
	}

	/**
	 * Add custom styles.
	 * 
	 * @return  void
	 */
	public function styles()
	{
		if (!$this->options['load_css_vars'])
		{
			return;
		}
	
		$controls = [
            [
                'property' => '--max-width',
                'value' => $this->options['max_width']
            ],
		];

		$selector = '.sp-paypal-button.' . $this->options['id'];
		
		$controlsInstance = new \NRFramework\Controls\Controls(null, $selector);

        if (!$controlsCSS = $controlsInstance->generateCSS($controls))
        {
            return;
        }

        Factory::getDocument()->addStyleDeclaration($controlsCSS);
	}

	/**
	 * Loads media files
	 * 
	 * @return  void
	 */
	public function loadMedia()
	{
		$queryStrings = [];

		// Add currency
		$queryStrings[] = 'currency=' . $this->options['currency'];

		// Add locale
		if (!empty($this->options['locale']))
		{
			$queryStrings[] = 'locale=' . $this->options['locale'];
		}

		
		// Add subscription required data
		if ($this->isSubscription())
		{
			$queryStrings[] = 'vault=true';
			$queryStrings[] = 'intent=subscription';
		}

		// Add enabled funding sources
		if ($this->options['payment_methods'] === 'custom')
		{
			if (count($this->options['include_payment_methods']))
			{
				$queryStrings[] = 'enable-funding=' . implode(',', $this->options['include_payment_methods']);
			}
	
			// Add disabled funding sources
			if (count($this->options['exclude_payment_methods']))
			{
				$queryStrings[] = 'disable-funding=' . implode(',', $this->options['exclude_payment_methods']);
			}
		}
		

		$keys = $this->getKeys($this->options['testmode']);
		
		HTMLHelper::script('https://www.paypal.com/sdk/js?client-id=' . $keys['client_id'] . '&' . implode('&', $queryStrings));
		HTMLHelper::script('com_smilepack/widgets/paypal-button.js', ['version' => 'auto', 'relative' => true]);
		HTMLHelper::stylesheet('com_smilepack/widgets/paypal-button.css', ['version' => 'auto', 'relative' => true]);
	}

	
	/**
	 * Create and return the plan ID.
	 * 
	 * @return  void
	 */
	protected function ajax_get_plan_id()
	{
		$input = Factory::getApplication()->input;

		$paypal_data = json_decode($input->get('data', '', 'string'), true);
		$paypal_data = new Registry($paypal_data);

		$this->params = $this->getParams();

		// Create product
		$product_id = $this->createProduct($paypal_data);

		if (!$product_id)
		{
			echo json_encode([
				'error' => true,
				'message' => 'Cannot create product'
			]);
			die();
		}

		// Create plan
		$plan_id = $this->createPlan($product_id, $paypal_data);
		
		if (!isset($plan_id))
		{
			echo json_encode([
				'error' => true,
				'message' => 'Cannot create plan'
			]);
			die();
		}

		// Return plan id
		echo json_encode([
			'error' => false,
			'message' => '',
			'plan_id' => $plan_id
		]);
		die();
	}

	/**
	 * Save the product for future use.
	 * 
	 * @param   string    $product_id   The product ID
	 * @param   Registry  $paypal_data  The PayPal data
	 * 
	 * @return  void
	 */
	private function saveProduct($product_id, $paypal_data)
	{
		// Get required product related data
		$name = $paypal_data->get('item_name', '');
		$type = $paypal_data->get('product_type', '');
		
		// Hash key
		$key = $this->getProductHash($name, $type);
		
		// Get existing PayPal Products List
		if ($saved_products = json_decode($this->params->get('paypal_products', ''), true))
		{
			// Add to list if missing
			if (isset($saved_products[$key]))
			{
				return;
			}
		}

		$saved_products[$key] = $product_id;

		$input = Factory::getApplication()->input;

		// Update PayPal Products List
		$this->params->set('paypal_products', json_encode($saved_products));
		
		// Update extension params
		\NRFramework\Extension::updateExtensionParams('com_smilepack', 'component', 'com_smilepack', $this->params->toArray());
	}

	/**
	 * Save the plan for future use.
	 * 
	 * @param   string  $key      The plan hash name
	 * @param   string  $plan_id  The plan ID
	 * 
	 * @return  void
	 */
	private function savePlan($key, $plan_id)
	{
		// Get existing PayPal Plans List
		if ($saved_plans = json_decode($this->params->get('paypal_plans', ''), true))
		{
			// Add to list if missing
			if (isset($saved_plans[$key]))
			{
				return;
			}
		}

		$saved_plans[$key] = $plan_id;

		$input = Factory::getApplication()->input;

		// Update PayPal Plans List
		$this->params->set('paypal_plans', json_encode($saved_plans));
		
		// Update extension params
		\NRFramework\Extension::updateExtensionParams('com_smilepack', 'component', 'com_smilepack', $this->params->toArray());
	}

	/**
	 * Create a product.
	 * 
	 * @param   Registry  $paypal_data  The PayPal data
	 * 
	 * @return  array
	 */
	private function createProduct($paypal_data)
	{
		$item_name = $paypal_data->get('item_name', '');
		$product_type = $paypal_data->get('product_type', '');

		// Hash key
		$key = $this->getProductHash($item_name, $product_type);

		// Get existing PayPal Products List
		if ($saved_products = json_decode($this->params->get('paypal_products', ''), true))
		{
			// Check if the product already exists and return it
			if (array_key_exists($key, $saved_products))
			{
				return $saved_products[$key];
			}
		}

		$testmode = $paypal_data->get('testmode', false);

		$keys = $this->getKeys($testmode);
		
		$options = new Registry();
        $options->set('headers.Content-Type', 'application/json');
        $options->set('headers.Authorization', 'Basic ' . base64_encode($keys['client_id'] . ':' . $keys['client_secret']));

        $http = \Joomla\CMS\Http\HttpFactory::getHttp($options);

		$url = $this->getPayPalAPIURL($testmode) . 'catalogs/products';

		$payload = [
			'name' => $item_name,
			'type' => $product_type
		];

		$response = $http->post($url, json_encode($payload));

		$data = json_decode($response->body, true);

		if (!isset($data['id']))
		{
			return;
		}
		
		// Save the product_id
		$this->saveProduct($data['id'], $paypal_data);

		return $data['id'];
	}

	/**
	 * Create a plan.
	 * 
	 * @param   string    $product_id   The product ID
	 * @param   Registry  $paypal_data  The PayPal data
	 * 
	 * @return  array
	 */
	private function createPlan($product_id, $paypal_data)
	{
		$plan_hash_name = $this->getPlanHashName($product_id, $paypal_data);

		// Get existing PayPal Plans List
		if ($saved_plans = json_decode($this->params->get('paypal_plans', ''), true))
		{
			// Check if the plan already exists and return it
			if (array_key_exists($plan_hash_name, $saved_plans))
			{
				return $saved_plans[$plan_hash_name];
			}
		}

		$testmode = $paypal_data->get('testmode', false);

		$keys = $this->getKeys($testmode);

		$options = new Registry();
        $options->set('headers.Content-Type', 'application/json');
        $options->set('headers.Authorization', 'Basic ' . base64_encode($keys['client_id'] . ':' . $keys['client_secret']));

        $http = \Joomla\CMS\Http\HttpFactory::getHttp($options);

		$url = $this->getPayPalAPIURL($testmode) . 'billing/plans';

		$amount = (float) $paypal_data->get('billing_amount', 0);

		$tax_rate = (float) $paypal_data->get('tax_rate', 0);

		$shipping_amount = (float) $paypal_data->get('shipping_amount', 0);
		
		$amount = $amount + $shipping_amount + ($amount * $tax_rate / 100);

		$payload = [
			'product_id' => $product_id,
			'name' => $paypal_data->get('item_name', ''),
			'status' => 'ACTIVE',
			'billing_cycles' => [
				[
					'tenure_type' => 'REGULAR',
					'sequence' => 1,
					'pricing_scheme' => [
						'fixed_price' => [
							'currency_code' => strtoupper($paypal_data->get('currency', 'USD')),
							'value'         => (string) $amount
						]
					],
					'total_cycles' => 0,
					'frequency' => [
						'interval_unit' => $paypal_data->get('billing_cycle', 'onetime') === 'custom' ? 'DAY' : $paypal_data->get('billing_cycle', 'onetime'),
						'interval_count' => $paypal_data->get('billing_cycle', 'onetime') === 'custom' ? $paypal_data->get('recurring_interval', '') : 1
					]
				]
			],
			'payment_preferences' => [
				'auto_bill_outstanding'     => true,
				'setup_fee_failure_action'  => 'CANCEL',
				'payment_failure_threshold' => 3
			],
			'taxes' => [
				'inclusive' => true,
				'percentage' => (string) $tax_rate
			]
		];

		$response = $http->post($url, json_encode($payload));

		$data = json_decode($response->body, true);

		if (!isset($data['id']))
		{
			return;
		}
		
		// Save the plan_id
		$this->savePlan($plan_hash_name, $data['id']);

		return $data['id'];
	}

	/**
	 * Checks if the button is for a subscription.
	 * 
	 * @return  boolean
	 */
	private function isSubscription()
	{
		if ($this->options['billing_cycle'] === 'onetime')
		{
			return;
		}

		return true;
	}

	/**
	 * Returns the product hash.
	 * 
	 * @param   string  $item_name     The item name
	 * @param   string  $product_type  The product type
	 * 
	 * @return  string
	 */
    public function getProductHash($item_name = '', $product_type = '')
    {
		// Hash key
		return base64_encode($item_name . ':' . $product_type);
    }

	/**
	 * Returns the product ID from the hash.
	 * 
	 * @param   string  $hash  The hash
	 * 
	 * @return  string
	 */
    public function getProductIdFromHash($hash = '')
    {
        $params = $this->getParams();
        
        $saved_products = json_decode($params->get('paypal_products', ''), true);

        // Ensure we have saved products
        if (!$saved_products)
		{
            return;
        }

        // Check if the product already exists and return it
        if (!array_key_exists($hash, $saved_products))
        {
            return;
        }
        
        return $saved_products[$hash];
    }
    
	/**
	 * Returns the plan hash name.
	 * 
	 * @param   string    $product_id   The product ID
	 * @param   Registry  $paypal_data  The PayPal data
	 * 
	 * @return  string
	 
	 */
	public function getPlanHashName($product_id, $paypal_data)
	{
		$billing_cycle = $paypal_data->get('billing_cycle', 'onetime') === 'custom' ? $paypal_data->get('recurring_interval', '') : $paypal_data->get('billing_cycle', 'onetime');
		$tax_rate = (string) $paypal_data->get('tax_rate', 0);
		$shipping_amount = (string) $paypal_data->get('shipping_amount', 0);
		
		$plan_name = sprintf(
			'%1$s-%2$s-%3$s-%4$s-%5$s-currency-%6$s',
			$product_id,
			$paypal_data->get('billing_amount', ''),
			$billing_cycle,
			$tax_rate,
			$shipping_amount,
			$paypal_data->get('currency', 'USD')
		);

		if ($paypal_data->get('testmode', '0') === '1')
		{
			$plan_name .= '-sandbox';
		}
		
		return $plan_name;
	}
	

	/**
	 * Returns the total amount.
	 * 
	 * This includes the billing amount, shipping amount and tax rate.
	 * 
	 * @return  float
	 */
	private function getTotalAmount()
	{
		$amount = $this->options['billing_amount'];

		// Prepare the amount, because it might be a Smart Tag
		$amount = (float) HTMLHelper::_('content.prepare', $amount);

		$shipping_amount = (float) $this->options['shipping_amount'];

		$tax_rate = (float) $this->options['tax_rate'];

		$amount = $amount + $shipping_amount + ($amount * $tax_rate / 100);

		return $amount;
	}

	/**
	 * Returns the PayPal API URL.
	 * 
	 * @param   boolean  $testmode  Whether to use the sandbox API URL
	 * 
	 * @return  string
	 */
	private function getPayPalAPIURL($testmode = false)
	{
		return 'https://api-m' . ($testmode ? '.sandbox' : '') . '.paypal.com/v1/';
	}

	/**
	 * Returns the PayPal keys.
	 * 
	 * @param   boolean  $testmode  Whether to use the sandbox keys
	 * 
	 * @return  array
	 */
	public function getKeys($testmode = false)
	{
		$prefix = $testmode ? 'sandbox' : 'live';
		
		return [
			'client_id' => $this->params->get($prefix . '_paypal_client_id', ''),
			'client_secret' => $this->params->get($prefix . '_paypal_client_secret', '')
		];
	}
	
	/**
	 * Returns the component params.
	 * 
	 * @return  Registry|boolean
	 */
	private function getParams()
	{
		$params = ComponentHelper::getComponent('com_smilepack', true);

		if (!$params)
		{
			return false;
		}

		if (!$params->enabled)
		{
			return false;
		}

		return $params->params;
	}
}