<?php
namespace SabaiApps\Directories\Component\PaymentGateway;

use SabaiApps\Directories\Component\AbstractComponent;
use SabaiApps\Directories\Component\Payment\IComponents;
use SabaiApps\Directories\Component\System;
use SabaiApps\Directories\Component\Entity;
use SabaiApps\Directories\Component\Field;
use SabaiApps\Directories\Component\CSV;
use SabaiApps\Directories\Context;
use SabaiApps\Directories\Exception;
use SabaiApps\Directories\Application;
use SabaiApps\Directories\Component\Payment\IOrder;

class PaymentGatewayComponent extends AbstractComponent implements
	IComponents,
	IGateways,
	System\IMainRouter,
	System\IAdminRouter,
	Entity\IBundleTypes,
	Field\ITypes,
	Field\IWidgets,
	CSV\IExporters,
	CSV\IImporters {

	const VERSION = '2.0.6', PACKAGE = 'directories-pro';

	public static function description() {
		return 'Payment gateway APIs for Payment component.';
	}

	public function paymentGetComponentNames() {
		return array( 'default' );
	}

	public function paymentGetComponent( $name ) {
		return new PaymentComponent\DefaultPaymentComponent( $this->_application, $name );
	}

	public function paymentGetGatewayNames() {
		return array( 'stripe', 'paypal', 'manual' );
	}

	public function paymentGetGateway( $name ) {
		switch ( $name ) {
			case 'stripe':
				return new Gateway\StripeGateway( $this->_application, $name );
			case 'paypal':
				return new Gateway\PaypalGateway( $this->_application, $name );
			case 'manual':
				return new Gateway\ManualPaymentGateway( $this->_application, $name );
		}
	}

	public function systemAdminRoutes() {
		return array(
			'/_drts/paymentgateway/set_plan_status'     => array(
				'controller'      => 'SetPlanStatus',
				'access_callback' => true,
				'callback_path'   => 'set_plan_status',
				'type'            => Application::ROUTE_CALLBACK,
				'method'          => 'post',
			),
			'/_drts/paymentgateway/set_discount_status' => array(
				'controller'      => 'SetDiscountStatus',
				'access_callback' => true,
				'callback_path'   => 'set_discount_status',
				'type'            => Application::ROUTE_CALLBACK,
				'method'          => 'post',
			),
		);
	}

	public function systemOnAccessAdminRoute( Context $context, $path, $accessType, array &$route ) {
		switch ( $path ) {
			case 'set_plan_status':
			case 'set_discount_status':
				return $this->isDefaultPaymentComponentEnabled();
		}
	}

	public function systemAdminRouteTitle( Context $context, $path, $titleType, array $route ) {}

	public function systemMainRoutes( $lang = null ) {
		return array(
			'/_drts/paymentgateway/webhook'              => array(
				'controller'      => 'Webhook',
				'callback_path'   => 'webhook',
				'access_callback' => true,
			),
			'/_drts/paymentgateway/verify_discount_code' => array(
				'controller'      => 'VerifyDiscountCode',
				'callback_path'   => 'verify_discount_code',
				'access_callback' => true,
			),
		);
	}

	public function systemOnAccessMainRoute( Context $context, $path, $accessType, array &$route ) {
		switch ( $path ) {
			case 'webhook':
				if ( ! $this->isDefaultPaymentComponentEnabled()
					|| ( ! $gateway = $context->getRequest()->asStr( 'gateway' ) )
					|| ( ! $this->_application->PaymentGateway_Util_gateway( $gateway, true ) )
				) {
					return false;
				}

				$context->gateway = $gateway;
				return true;
			case 'verify_discount_code':
				return $this->isDefaultPaymentComponentEnabled();
		}
	}

	public function systemMainRouteTitle( Context $context, $path, $titleType, array $route ) {}

	public function fieldGetTypeNames() {
		return array(
			'paymentgateway_plan',
			'paymentgateway_order',
			'paymentgateway_subscription',
			'paymentgateway_discount',
			'paymentgateway_plan_discount',
		);
	}

	public function fieldGetType( $name ) {
		switch ( $name ) {
			case 'paymentgateway_plan':
				return new FieldType\PlanFieldType( $this->_application, $name );
			case 'paymentgateway_order':
				return new FieldType\OrderFieldType( $this->_application, $name );
			case 'paymentgateway_subscription':
				return new FieldType\SubscriptionFieldType( $this->_application, $name );
			case 'paymentgateway_discount':
				return new FieldType\DiscountFieldType( $this->_application, $name );
			case 'paymentgateway_plan_discount':
				return new FieldType\PlanDiscountFieldType( $this->_application, $name );
		}
	}

	public function fieldGetWidgetNames() {
		return array(
			'paymentgateway_plan',
			'paymentgateway_order',
			'paymentgateway_subscription',
			'paymentgateway_discount',
			'paymentgateway_plan_discount',
		);
	}

	public function fieldGetWidget( $name ) {
		switch ( $name ) {
			case 'paymentgateway_plan':
				return new FieldWidget\PlanFieldWidget( $this->_application, $name );
			case 'paymentgateway_order':
				return new FieldWidget\OrderFieldWidget( $this->_application, $name );
			case 'paymentgateway_subscription':
				return new FieldWidget\SubscriptionFieldWidget( $this->_application, $name );
			case 'paymentgateway_discount':
				return new FieldWidget\DiscountFieldWidget( $this->_application, $name );
			case 'paymentgateway_plan_discount':
				return new FieldWidget\PlanDiscountFieldWidget( $this->_application, $name );
		}
	}

	public function isDefaultPaymentComponentEnabled() {
		return $this->_application->getComponent( 'Payment' )->getConfig( 'payment', 'component' ) === 'default';
	}

	public function onPaymentGatewayOrderStatusChanged( Entity\Type\IEntity $order, $oldStatus, $extraArgs ) {
		if ( ! $entity = $this->_application->Entity_ParentEntity( $order ) ) {
			$this->_application->logError( 'Failed fetching item for order (ID: ' . $order->getId() . ').' );
			return;
		}
		$data = $order->getSingleFieldValue( 'paymentgateway_order', 'data' );
		if ( ! empty( $data['claim_id'] ) ) {
			if ( ! $claim = $this->_application->Entity_Entity( 'post', $data['claim_id'] ) ) {
				$this->_application->logError( 'Failed fetching claim for item (ID: ' . $entity->getId() . ').' );
				return;
			}
			if ( $claim->getParentId() !== $entity->getId() ) {
				$this->_application->logError( 'Invalid claim (ID: ' . $claim->getId() . ').' );
				return;
			}
		}

		switch ( $order->getSingleFieldValue( 'paymentgateway_order', 'status' ) ) {
			case 'completed':
				if ( ! in_array( $oldStatus, array( 'cancelled', 'refunded', 'failed' ) ) ) {
					if ( ( ! $plan_id = $order->getSingleFieldValue( 'paymentgateway_order', 'plan_id' ) )
						|| ( ! $plan = $this->_application->Payment_Components_impl( 'default' )->paymentGetPlanById( $entity->getBundleName(), $plan_id ) )
					) {
						$this->_application->logError( 'Failed fetching payment plan for order (ID: ' . $order->getId() . ').' );
						break;
					}

					// Update content item
					$values = array();
					if ( $plan->paymentPlanType() !== 'addon' ) {
						if ( ! $entity->isPublished() // may already be published if renew/upgrade/downgrade
							&& ! $entity->isPending()
						) {
							// Update post status to pending
							$values['status'] = $this->_application->Entity_Status( $entity->getType(), 'pending' );
						}
					}
					if ( ! empty( $values ) ) {
						try {
							$this->_application->Entity_Save( $entity, $values, array( 'notification_ignore_post_pending' => true ) );
						} catch ( Exception\IException $e ) {
							$this->_application->logError( 'Failed updating content item after order status change. Error: ' . $e->getMessage() );
						}
					}

					// Apply features
					try {
						$this->_application->Payment_Features_apply( isset( $claim ) ? $claim : $entity, $order->getId() );
					} catch ( Exception\IException $e ) {
						$this->_application->logError( $e );
					}

					if ( $plan->paymentPlanType() === 'subscription' ) {
						try {
							$this->_application->PaymentGateway_Subscription_create( $order );
						} catch ( Exception\IException $e ) {
							$this->_application->logError( 'Failed creating subscription for order (ID: ' . $order->getId() . '). Error: ' . $e->getMessage() );
							return false;
						}
					}
				}
				break;
			case 'cancelled':
			case 'refunded':
			case 'failed':
				// Unapply features
				try {
					$this->_application->Payment_Features_unapply( isset( $claim ) ? $claim : $entity, $order->getId() );
				} catch ( Exception\IException $e ) {
					$this->_application->logError( $e );
				}
				break;
		}
	}

	public function onPaymentGatewaySubscriptionStatusChanged( Entity\Type\IEntity $subscription, $oldStatus, $extraArgs ) {
		if ( ! $entity = $this->_application->Entity_ParentEntity( $subscription ) ) {
			$this->_application->logError( 'Failed fetching item for subscription (ID: ' . $subscription->getId() . ').' );
			return;
		}

		$subscription_data = $subscription->getSingleFieldValue( 'paymentgateway_subscription' );
		switch ( $subscription_data['status'] ) {
			case 'active':
				break;
			case 'cancelled':
			case 'expired':
				if ( $subscription_data['data']['gateway_status'] !== 'cancelled' ) {
					if ( ! empty( $subscription_data['gateway'] ) ) {
						if ( $gateway = $this->_application->PaymentGateway_Util_gateway( $subscription_data['gateway'], true ) ) {
							try {
								$gateway->paymentGatewayCancelSubscription( $subscription );
								$subscription_data['data']['gateway_status'] = 'cancelled';
								$this->_application->Entity_Save( $subscription, array( 'paymentgateway_subscription' => $subscription_data ) );
							} catch ( \Exception $e ) {
								$this->_application->logError( $e );
							}
						} else {
							$this->_application->logWarning( 'Gateway ' . $subscription_data['gateway'] . ' not found when cancelling subscription (ID: ' . $subscription_data['id'] . ')' );
						}
					}
				}

				$action = $subscription_data['status'] === 'cancelled' && ! empty( $extraArgs['paymentgateway_cancel_manual'] ) ? 'draft' : null;  // mark draft only if cancelled manually
				$this->_application->Payment_Util_handleExpired( $entity->getType(), array( $entity->getId() => $entity ), $action );
				break;
		}
	}

	public function onDashboardPanelsLoaded( $panels ) {
		if ( isset( $panels['payment_payments']['links']['orders'] ) ) {
			$this->_loadPaymentSummaryCss();
		}
	}

	public function entityGetBundleTypeNames() {
		return array(
			'paymentgateway_plan',
			'paymentgateway_order',
			'paymentgateway_subscription',
			'paymentgateway_discount',
		);
	}

	public function entityGetBundleType( $name ) {
		switch ( $name ) {
			case 'paymentgateway_plan':
				return new EntityBundleType\PlanEntityBundleType( $this->_application, $name );
			case 'paymentgateway_order':
				return new EntityBundleType\OrderEntityBundleType( $this->_application, $name );
			case 'paymentgateway_subscription':
				return new EntityBundleType\SubscriptionEntityBundleType( $this->_application, $name );
			case 'paymentgateway_discount':
				return new EntityBundleType\DiscountEntityBundleType( $this->_application, $name );
		}
	}

	public function onEntityBundlesInfoFilter( &$bundles, $group ) {
		$found                  = false;
		$paymentgateway_bundles = array( 'paymentgateway_order', 'paymentgateway_subscription', 'paymentgateway_plan', 'paymentgateway_discount' );
		foreach ( array_keys( $bundles ) as $bundle_type ) {
			$info =& $bundles[ $bundle_type ];

			if ( empty( $info['payment_enable'] )
				|| ! empty( $info['is_taxonomy'] )
				|| ! empty( $info['parent'] )
			) {
				continue;
			}

			$found = true;

			// Add order/subscription bundles
			foreach ( $paymentgateway_bundles as $_bundle_type ) {
				if ( ! isset( $bundles[ $_bundle_type ] ) ) { // may already set if updating or importing
					$bundles[ $_bundle_type ] = array();
				}
				$bundles[ $_bundle_type ] += $this->entityGetBundleType( $_bundle_type )->entityBundleTypeInfo();
			}
		}

		if ( ! $found ) {
			// No bundle with payment enabled found, so make sure order/subscription bundles are not assigned
			foreach ( $paymentgateway_bundles as $_bundle_type ) {
				unset( $bundles[ $_bundle_type ] );
			}
		}
	}

	public function onEntitySaveEntityFields( $bundle, $entity, $isNew, &$fieldValues, &$updateProperties ) {
		if ( ! in_array( $bundle->type, array( 'paymentgateway_order', 'paymentgateway_subscription' ) )
			|| ! $isNew
			|| ! isset( $fieldValues[ $bundle->type ][0] )
		) {
			return;
		}

		$updateProperties['title'] = $fieldValues[ $bundle->type ][0]['number'] = $this->_application->Entity_NumberFormat_format(
			$entity,
			$bundle->info['label_singular'],
			isset( $bundle->info[ $bundle->type . '_number' ] ) ? $bundle->info[ $bundle->type . '_number' ] : array()
		);
	}

	public function onEntityBundleInfoUserKeysFilter( &$keys ) {
		$keys[] = 'paymentgateway_order_number';
		$keys[] = 'paymentgateway_subscription_number';
	}

	public function onWordpressPostTypeFilter( &$postType, $bundle ) {
		if ( in_array( $bundle->type, array( 'paymentgateway_plan', 'paymentgateway_order', 'paymentgateway_subscription', 'paymentgateway_discount' ) )
			&& ! $this->isDefaultPaymentComponentEnabled()
		) {
			$postType = false;
		}
	}

	public function onEntityUpdateEntitySuccess( $bundle, $entity, $oldEntity, $values, $extraArgs ) {
		if ( ! in_array( $bundle->type, array( 'paymentgateway_order', 'paymentgateway_subscription' ) )
			|| empty( $values[ $bundle->type ] )
		) {
			return;
		}

		$old_status = $oldEntity->getSingleFieldValue( $bundle->type, 'status' );
		if ( $values[ $bundle->type ][0]['status'] === $old_status ) {
			return;
		}

		$this->_application->Action( $bundle->type . '_status_changed', array( $entity, $old_status, $extraArgs ) );
	}

	public function onPaymentBasePlanTypesFilter( &$planTypes ) {
		if ( $this->_application->getComponent( 'Payment' )->getPaymentComponent( false, true ) !== 'default' ) {
			return;
		}

		$planTypes[] = 'subscription';
	}

	public function onEntityAdminBundleSettingsFormFilter( &$form, $bundle, $submitValues ) {
		if ( empty( $bundle->info['payment_enable'] )
			|| ! $this->isDefaultPaymentComponentEnabled()
		) {
			return;
		}

		$weight = 20;
		foreach ( array( 'order', 'subscription' ) as $type ) {
			$bundle_type = 'paymentgateway_' . $type;
			if ( $payment_bundle = $this->_application->Entity_Bundle( $bundle_type, $bundle->group ) ) {
				$form['payment'][ $bundle_type ] = array(
					'#weight' => ++$weight,
					'#tree'   => true,
				) + (array) $this->_application->Entity_BundleTypes_impl( $bundle_type )->entityBundleTypeSettingsForm( $payment_bundle->info, array( $bundle_type ), $submitValues );
			}
		}
	}

	public function onEntityAdminBundleSettingsSave( $bundle, &$values ) {
		if ( empty( $bundle->info['payment_enable'] )
			|| ! $this->isDefaultPaymentComponentEnabled()
		) {
			return;
		}

		foreach ( array( 'order', 'subscription' ) as $type ) {
			$bundle_type = 'paymentgateway_' . $type;
			$_values     = $values[ $bundle_type ];
			unset( $values[ $bundle_type ] );
			if ( $payment_bundle = $this->_application->Entity_Bundle( $bundle_type, $bundle->group ) ) {
				$payment_bundle->setInfo( $_values )->commit();
			}
		}
	}

	public function onEntityFormLoadAssets( $bundle, $assets ) {
		if ( $bundle->type !== 'claiming_claim'
			|| ! $this->isDefaultPaymentComponentEnabled()
			|| ( ! $parent_bundle = $this->_application->Entity_Bundle( $bundle->info['parent'] ) )
			|| empty( $parent_bundle->info['payment_enable'] )
			|| ( ! $component = $this->_application->Payment_Components_impl( 'default', true ) )
			|| ( ! $settings = $component->getSettings() )
			|| empty( $settings['gateways']['default'] )
		) {
			return;
		}

		foreach ( $settings['gateways']['default'] as $gateway_name ) {
			if ( ! $gateway = $this->_application->PaymentGateway_Util_gateway( $gateway_name, true ) ) {
				continue;
			}

			$gateway->paymentGatewayOnLoadAssets();
		}
		$this->_loadPaymentSummaryCss();
	}

	protected function _loadPaymentSummaryCss() {
		$this->_application->getPlatform()->addCssFile(
			'paymentgateway_payment_summary.min.css',
			'drts-paymentgateway_payment_summary',
			'drts',
			'directories-pro'
		);
	}

	public function onEntityBundleSortOrderFilter( &$sort_order ) {
		$sort_order['paymentgateway_plan']         = 90;
		$sort_order['paymentgateway_order']        = 91;
		$sort_order['paymentgateway_subscription'] = 92;
		$sort_order['paymentgateway_discount']     = 93;
	}

	public function onPaymentOrderCreated( IOrder $order ) {
		if ( ( ! $order_data = $order->getOrder()->getSingleFieldValue( 'paymentgateway_order', 'data' ) )
			|| empty( $order_data['discounts'] )
			|| ! is_array( $order_data['discounts'] )
		) {
			return;
		}

		$codes = array();
		foreach ( $order_data['discounts'] as $discount ) {
			if ( isset( $discount['code'] ) ) {
				$codes[] = $discount['code'];
			}
		}
		if ( empty( $codes ) ) {
			return;
		}

		$discounts = $this->_application->Entity_Query( 'post' )->fieldIs( 'bundle_type', 'paymentgateway_discount' )
			->fieldIsIn( 'paymentgateway_discount', $codes, 'discount_code' )
			->fetch();
		foreach ( $discounts as $d ) {
			try {
				$discount_info   = $d->getSingleFieldValue( 'paymentgateway_discount' );
				$usage_count     = $discount_info['usage_count'] ?? 0;
				$usage_history   = $discount_info['usage_history'] ?? array();
				$usage_history[] = $this->_application->PaymentGateway_Util_note(
					sprintf(
						__( 'Discount used on %1$s for %2$s.', 'directories-pro' ),
						'<em>' . $this->_application->H( $order->paymentOrderNumber() ) . '</em>',
						'<i>' . $this->_application->H( $order->paymentOrderName() ) . '</i>'
					),
					null,
					$order->getOrder()->getAuthorId(),
					$order->getOrder()->getId()
				);
				$this->_application->Entity_Save(
					$d,
					array(
						'paymentgateway_discount' => array(
							'usage_count'   => ++$usage_count,
							'usage_history' => $usage_history,
						) + $discount_info,
					)
				);
			} catch ( Exception\IException $e ) {
				$this->_application->logError( 'Failed updating usage count and history for discount (ID: ' . $d->getId() . '). Error: ' . $e->getMessage() );
			}
		}
	}

	public function csvGetExporterNames() {
		return array( 'paymentgateway_plan', 'paymentgateway_plan_discount' );
	}

	public function csvGetExporter( $name ) {
		switch ( $name ) {
			case 'paymentgateway_plan':
				return new CSVExporter\PlanCSVExporter( $this->_application, $name );
			case 'paymentgateway_plan_discount':
				return new CSVExporter\PlanDiscountCSVExporter( $this->_application, $name );
		}
	}

	public function csvGetImporterNames() {
		return array( 'paymentgateway_plan', 'paymentgateway_plan_discount' );
	}

	public function csvGetImporter( $name ) {
		switch ( $name ) {
			case 'paymentgateway_plan':
				return new CSVImporter\PlanCSVImporter( $this->_application, $name );
			case 'paymentgateway_plan_discount':
				return new CSVImporter\PlanDiscountCSVImporter( $this->_application, $name );
		}
	}
}
