<?php

namespace TEC\Tickets\Commerce;

use WP_Post;
use WP_Error;
use TEC\Tickets\Commerce;
use Tribe__Utils__Array as Arr;
use TEC\Tickets\Commerce\Communication\Email as Email_Communication;
use TEC\Tickets\Commerce\Reports\Attendees as Attendees_Reports;

/**
 * Class Tickets Provider class for Tickets Commerce
 *
 * @since 5.1.9
 *
 * @package TEC\Tickets\Commerce
 */
class Module extends \Tribe__Tickets__Tickets {

	/**
	 * Constructor.
	 *
	 * @since 5.1.9
	 * @since 5.23.0 Moved to do_init() to avoid issues with translations.
	 */
	public function __construct() {
		// This needs to happen before parent construct.
		$this->plugin_name = 'Tickets Commerce'; // Intentionally not translated.

		parent::__construct();

		$this->attendee_object = Attendee::POSTTYPE;

		$this->attendee_ticket_sent = '_tribe_tpp_attendee_ticket_sent';

		$this->attendee_optout_key = Attendee::$optout_meta_key;

		$this->ticket_object = Ticket::POSTTYPE;

		$this->event_key = Attendee::$event_relation_meta_key;

		$this->attendee_event_key = Attendee::$event_relation_meta_key;

		$this->checkin_key = Attendee::$checked_in_meta_key;

		$this->order_key = Attendee::$order_relation_meta_key;

		$this->refund_order_key = '_tribe_tpp_refund_order';

		$this->security_code = Attendee::$security_code_meta_key;

		$this->full_name = '_tribe_tpp_full_name';

		$this->email = Attendee::$purchaser_email_meta_key;
	}

	/**
	 * {@inheritdoc}
	 */
	public $orm_provider = \TEC\Tickets\Commerce::PROVIDER;

	/**
	 * Name of the CPT that holds Attendees (tickets holders).
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	const ATTENDEE_OBJECT = Attendee::POSTTYPE;

	/**
	 * Name of the CPT that holds Orders
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	const ORDER_OBJECT = Order::POSTTYPE;

	/**
	 * Meta key that relates Attendees and Events.
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	const ATTENDEE_EVENT_KEY = '_tec_tickets_commerce_event';

	/**
	 * Meta key that relates Attendees and Products.
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	const ATTENDEE_PRODUCT_KEY = '_tec_tickets_commerce_ticket';

	/**
	 * Meta key that relates Attendees and Orders.
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	const ATTENDEE_ORDER_KEY = '_tec_tickets_commerce_order';

	/**
	 * Indicates if a ticket for this attendee was sent out via email.
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	public $attendee_ticket_sent;

	/**
	 * Meta key that if this attendee wants to show on the attendee list
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	public $attendee_optout_key;

	/**
	 * Meta key that if this attendee PayPal status
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	public $attendee_tpp_key;

	/**
	 * Name of the CPT that holds Tickets
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	public $ticket_object;

	/**
	 * Meta key that relates Products and Events
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	public $event_key;

	/**
	 * Meta key that stores if an attendee has checked in to an event
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	public $checkin_key;

	/**
	 * Meta key that ties attendees together by order
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	public $order_key;

	/**
	 * Meta key that ties attendees together by refunded order
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	public $refund_order_key;

	/**
	 * Meta key that holds the security code that's printed in the tickets
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	public $security_code;

	/**
	 * Meta key that holds the full name of the tickets PayPal "buyer"
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	public $full_name;

	/**
	 * Meta key that holds the email of the tickets PayPal "buyer"
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	public $email;

	/**
	 * Meta key that holds the name of a ticket to be used in reports if the Product is deleted
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	public $deleted_product = '_tribe_deleted_product_name';

	/**
	 * A variable holder if PayPal is loaded
	 *
	 * @since 5.1.9
	 *
	 * @var boolean
	 */
	protected $is_loaded = false;

	/**
	 * This method is required for the module to properly load.
	 *
	 * @since 5.1.9
	 * @return static
	 */
	public static function get_instance() {
		return tribe( static::class );
	}

	/**
	 * Hooks the module, happens on the `wp` hook.
	 *
	 * @since 5.22.0
	 *
	 * @return void
	 */
	public function hook() {
		parent::hook();

		// Overwrite with the translated name.
		$this->plugin_name = __( 'Tickets Commerce', 'event-tickets' );
	}

	/**
	 * Registers all actions/filters
	 *
	 * @since 5.1.9
	 */
	public function hooks() {
		// if the hooks have already been bound, don't do it again
		if ( $this->is_loaded ) {
			return false;
		}
	}

	/**
	 * Send tickets email for attendees.
	 *
	 * @since 5.1.9
	 *
	 * @param array       $attendees   List of attendees.
	 * @param array       $args        {
	 *                                 The list of arguments to use for sending ticket emails.
	 *
	 * @type string       $subject     The email subject.
	 * @type string       $content     The email content.
	 * @type string       $from_name   The name to send tickets from.
	 * @type string       $from_email  The email to send tickets from.
	 * @type array|string $headers     The list of headers to send.
	 * @type array        $attachments The list of attachments to send.
	 * @type string       $provider    The provider slug (rsvp, tpp, woo, edd).
	 * @type int          $post_id     The post/event ID to send the emails for.
	 * @type string|int   $order_id    The order ID to send the emails for.
	 * }
	 *
	 * @return int The number of emails sent successfully.
	 */
	public function send_tickets_email_for_attendees( $attendees, $args = [] ) {
		$args = array_merge(
			[
				'subject'    => tribe_get_option( Settings::$option_confirmation_email_subject, false ),
				'from_name'  => tribe_get_option( Settings::$option_confirmation_email_sender_name, false ),
				'from_email' => tribe_get_option( Settings::$option_confirmation_email_sender_email, false ),
				'provider'   => Commerce::ABBR,
			],
			$args
		);

		return parent::send_tickets_email_for_attendees( $attendees, $args );
	}

	/**
	 * Shows the tickets form in the front end
	 *
	 * @since 5.1.9
	 *
	 * @param string $unused_content Unused content.
	 *
	 * @return void
	 */
	public function front_end_tickets_form( $unused_content ) {

		$post    = $GLOBALS['post'];
		$tickets = $this->get_tickets( $post->ID );

		foreach ( $tickets as $index => $ticket ) {
			if ( __CLASS__ !== $ticket->provider_class ) {
				unset( $tickets[ $index ] );
			}
		}

		if ( empty( $tickets ) ) {
			return;
		}

		tribe( Tickets_View::class )->get_tickets_block( $post->ID );
	}

	/**
	 * Indicates if we currently require users to be logged in before they can obtain
	 * tickets.
	 *
	 * @since 5.1.9
	 *
	 * @return bool
	 */
	public function login_required() {
		$requirements = (array) tribe_get_option( 'ticket-authentication-requirements', array() );

		return in_array( 'event-tickets_all', $requirements, true );
	}

	/**
	 * Get attendees by id and associated post type
	 * or default to using $post_id
	 *
	 * @since 5.1.9
	 *
	 * @param int|string  $post_id   The post ID.
	 * @param string|null $post_type The post type.
	 *
	 * @return array|mixed
	 */
	public function get_attendees_by_id( $post_id, $post_type = null ) {
		if ( ! $post_type ) {
			$post_type = get_post_type( $post_id );
		}

		switch ( $post_type ) {
			case $this->attendee_object:
				return $this->get_attendees_by_attendee_id( $post_id );
			case 'tpp_order_hash':
				return $this->get_attendees_by_order_id( $post_id );
			case $this->ticket_object:
				return tribe( Attendee::class )->get_attendees_by_ticket_id( $post_id, $this->orm_provider );
			default:
				return $this->get_attendees_by_post_id( $post_id );
		}

	}

	/**
	 * Get attendees for a ticket by order ID, optionally by ticket ID.
	 *
	 * This overrides the parent method because Tickets Commerce stores the order ID in the post_parent.
	 *
	 * @since 5.2.0
	 *
	 * @param int|string $order_id  Order ID.
	 * @param null|int   $ticket_id (optional) Ticket ID.
	 *
	 * @return array List of attendees.
	 */
	public function get_attendees_by_order_id( $order_id ) {
		$ticket_id = null;

		// Support an optional second argument while not causing warnings from other ticket provider classes.
		if ( 1 < func_num_args() ) {
			$ticket_id = func_get_arg( 1 );
		}

		/** @var \Tribe__Tickets__Attendee_Repository $repository */
		$repository = tribe_attendees( $this->orm_provider );

		$repository->by( 'parent', $order_id );

		if ( $ticket_id ) {
			$repository->by( 'ticket', $ticket_id );
		}

		return $this->get_attendees_from_module( $repository->all() );
	}

	/**
	 * Returns the value of a key defined by the class.
	 *
	 * @since 5.1.9
	 *
	 * @param string $key The key.
	 *
	 * @return string The key value or an empty string if not defined.
	 */
	public static function get_key( $key ) {
		$instance = self::get_instance();
		$key      = strtolower( $key );

		$constant_map = [
			'attendee_event_key'   => $instance->attendee_event_key,
			'attendee_product_key' => $instance->attendee_product_key,
			'attendee_order_key'   => $instance->order_key,
			'attendee_optout_key'  => $instance->attendee_optout_key,
			'event_key'            => $instance->get_event_key(),
			'checkin_key'          => $instance->checkin_key,
			'order_key'            => $instance->order_key,
		];

		return \Tribe__Utils__Array::get( $constant_map, $key, '' );
	}

	/**
	 * Indicates if global stock support is enabled for this provider.
	 *
	 * @since 5.1.9
	 *
	 * @return bool
	 */
	public function supports_global_stock() {
		/**
		 * Allows the declaration of global stock support for Tribe Commerce tickets
		 * to be overridden.
		 *
		 * @param bool $enable_global_stock_support
		 */
		return (bool) apply_filters( 'tec_tickets_commerce_enable_global_stock', true );
	}

	/**
	 * All the methods below here were created merely as a backwards compatibility piece for our old Code that
	 * depends so much on the concept of a Main class handling all kinds of integration pieces.
	 *
	 * ! DO NOT INTRODUCE MORE LOGIC OR COMPLEXITY ON THESE METHODS !
	 *
	 * The methods are all focused on routing functionality to their correct handlers.
	 */

	/**
	 * Returns the ticket price html template.
	 *
	 * @since 5.1.9
	 *
	 * @param int|object    $product  The product.
	 * @param array|boolean $attendee The attendee.
	 *
	 * @return string
	 */
	public function get_price_html( $product, $attendee = false ) {
		return tribe( Ticket::class )->get_price_html( $product );
	}

	/**
	 * Gets the product price value
	 *
	 * @since 5.1.9
	 *
	 * @param int|WP_post $product The product.
	 *
	 * @return string
	 */
	public function get_price_value( $product ) {
		return tribe( Ticket::class )->get_price_value( $product );
	}

	/**
	 * Whether a specific attendee is valid toward inventory decrease or not.
	 *
	 * By default only attendees generated as part of a Completed order will count toward
	 * an inventory decrease but, if the option to reserve stock for Pending Orders is activated,
	 * then those attendees generated as part of a Pending Order will, for a limited time after the
	 * order creation, cause the inventory to be decreased.
	 *
	 * @since 5.1.9
	 *
	 * @param array $attendee The attendee.
	 *
	 * @return bool
	 */
	public function attendee_decreases_inventory( array $attendee ) {
		return tribe( Attendee::class )->decreases_inventory( $attendee );
	}

	/**
	 * {@inheritdoc}
	 */
	public function get_attendee( $attendee, $post_id = 0 ) {
		return tec_tc_get_attendee( $attendee, ARRAY_A );
	}

	/**
	 * Event Tickets Plus Admin Reports page will use this data from this method.
	 *
	 * @since 5.1.9
	 *
	 *
	 * @param string|int $order_id
	 *
	 * @return array
	 */
	public function get_order_data( $order_id ) {
		return tec_tc_get_order( $order_id, ARRAY_A );
	}

	/**
	 * Renders the advanced fields in the new/edit ticket form.
	 * Using the method, providers can add as many fields as
	 * they want, specific to their implementation.
	 *
	 * @since 5.1.9
	 *
	 * @param int $post_id   The post ID.
	 * @param int $ticket_id The ticket ID.
	 */
	public function do_metabox_capacity_options( $post_id, $ticket_id ) {
		tribe( Editor\Metabox::class )->do_metabox_capacity_options( $post_id, $ticket_id );
	}

	/**
	 * Maps to the Cart Class method to get the cart.
	 *
	 * @since 5.1.9
	 *
	 * @return string The cart URL.
	 */
	public function get_cart_url() {
		return tribe( Cart::class )->get_url();
	}

	/**
	 * Maps to the Checkout Class method to get the checkout.
	 *
	 * @since 5.2.0
	 *
	 * @return string The checkout URL.
	 */
	public function get_checkout_url() {
		return tribe( Checkout::class )->get_url();
	}

	/**
	 * Generate and store all the attendees information for a new order.
	 *
	 * @since 5.1.9
	 * @deprecated 5.2.0
	 *
	 * @param string $payment_status The tickets payment status, defaults to completed.
	 * @param bool   $redirect       Whether the client should be redirected or not.
	 */
	public function generate_tickets( $payment_status = 'completed', $redirect = true ) {
		_deprecated_function( __METHOD__, '5.2.0' );
	}

	/**
	 * Gets an individual ticket.
	 *
	 * @since 5.1.9
	 * @since 5.6.7 Set some provider-invariant ticket properties.
	 *
	 * @param int|WP_post $post_id
	 * @param int|WP_post $ticket_id
	 *
	 * @return null|\Tribe__Tickets__Ticket_Object
	 */
	public function get_ticket( $post_id, $ticket_id ) {
		$ticket = tribe( Ticket::class )->get_ticket( $ticket_id );

		if ( ! $ticket instanceof \Tribe__Tickets__Ticket_Object ) {
			return null;
		}

		// Set provider-invariant ticket properties.
		$ticket_type  = get_post_meta( $ticket_id, '_type', true ) ?: 'default';
		$ticket->type = $ticket_type;

		return $ticket;
	}

	/**
	 * Saves a Tickets Commerce ticket.
	 *
	 * @since 5.1.9
	 *
	 * @param int                            $post_id  Post ID.
	 * @param \Tribe__Tickets__Ticket_Object $ticket   Ticket object.
	 * @param array                          $raw_data Ticket data.
	 *
	 * @return int|false The updated/created ticket post ID or false if no ticket ID.
	 */
	public function save_ticket( $post_id, $ticket, $raw_data = [] ) {
		// Run anything we might need on parent method.
		parent::save_ticket( $post_id, $ticket, $raw_data );

		/**
		 * Important, do not add anything above this method.
		 * Our goal is to reduce the amount of load on the `Module`, relegate these behaviors to the correct models.
		 */
		return tribe( Ticket::class )->save( $post_id, $ticket, $raw_data );
	}

	/**
	 * Deletes a ticket or an attendee post.
	 *
	 * @since 5.1.9
	 *
	 * @since 5.5.10 Adjust the method to handle both Ticket and Attendee post type deletion separately.
	 *
	 * @param int|WP_Post|null $event_id  The event ID.
	 * @param int|WP_Post|null $ticket_id The ticket ID.
	 *
	 * @return bool
	 */
	public function delete_ticket( $event_id, $ticket_id ) {
		/**
		 * Important, do not add anything above this method.
		 * Our goal is to reduce the amount of load on the `Module`, relegate these behaviors to the correct models.
		 */

		$ticket_post = get_post( $ticket_id );

		if ( ! $ticket_post instanceof WP_Post ) {
			return false;
		}

		$deleted = false;
		// We are handling both Ticket and Attendee post type deletion using this same method.
		if ( Attendee::POSTTYPE === $ticket_post->post_type ) {
			$deleted = tribe( Attendee::class )->delete( $ticket_id );
		}

		if ( Ticket::POSTTYPE === $ticket_post->post_type ) {
			$deleted = tribe( Ticket::class )->delete( $event_id, $ticket_id );
		}

		if ( ! $deleted ) {
			return false;
		}

		// Run anything we might need on parent method.
		parent::delete_ticket( $event_id, $ticket_id );
		return true;
	}

	/**
	 * Return whether we're currently on the checkout page for Tickets Commerce.
	 *
	 * @since 5.1.9
	 *
	 * @return bool
	 */
	public function is_checkout_page() {
		return tribe( Checkout::class )->is_current_page();
	}

	/**
	 * Links to sales report for all tickets for this event.
	 *
	 * @since 5.1.9
	 *
	 * @param int  $event_id  The event ID.
	 * @param bool $url_only  Whether to return the URL only.
	 *
	 * @return string The event reports link.
	 */
	public function get_event_reports_link( $event_id, $url_only = false ) {
		return tribe( Commerce\Reports\Orders::class )->get_event_link( $event_id, $url_only );
	}

	/**
	 * Links to the sales report for this product.
	 *
	 * @since 5.1.9
	 *
	 * @param int|WP_Post|null $event_id  The event ID.
	 * @param int|WP_Post|null $ticket_id The ticket ID.
	 *
	 * @return string The ticket reports link.
	 */
	public function get_ticket_reports_link( $event_id, $ticket_id ) {
		return tribe( Commerce\Reports\Orders::class )->get_ticket_link( $event_id, $ticket_id );
	}

	/**
	 * Create an attendee for the Commerce provider from a ticket.
	 *
	 * @since 5.1.0
	 *
	 * @param \Tribe__Tickets__Ticket_Object|int $ticket        Ticket object or ID to create the attendee for.
	 * @param array                              $attendee_data Attendee data to create from.
	 *
	 * @return WP_post|WP_Error|false The new post object or false if we can't resolve to a Ticket object. WP_Error if modifying status fails.
	 */
	public function create_attendee( $ticket, $attendee_data ) {
		// Get the ticket object from the ID.
		if ( is_numeric( $ticket ) ) {
			$ticket = $this->get_ticket( 0, (int) $ticket );
		}

		// If the ticket is not valid, stop creating the attendee.
		if ( ! $ticket instanceof \Tribe__Tickets__Ticket_Object ) {
			return false;
		}

		$extra              = [];
		$extra['attendees'] = [
			1 => [
				'meta' => Arr::get( $attendee_data, 'attendee_meta', [] )
			]
		];
		$extra['optout']    = ! Arr::get( $attendee_data, 'send_ticket_email', true );
		$extra['iac']       = false;

		// The Manual Order takes the same format as the cart items.
		$items = [
			$ticket->ID => [
				'ticket_id' => $ticket->ID,
				'quantity'  => 1,
				'extra'     => $extra,
			]
		];

		$purchaser = [
			'full_name' => $attendee_data['full_name'],
			'email'     => $attendee_data['email'],

			// By default user ID is zero here.
			'user_id'   => 0,
		];

		$order = tribe( Gateways\Manual\Order::class )->create( $items, $purchaser );

		/**
		 * For now we need to make sure we move to pending before completed.
		 *
		 * @todo @backend when an order is moved into completed they need to update to pending first.
		 *       likely we should have this be developed by implementing a dependent status.
		 */
		$updated = tribe( Order::class )->modify_status( $order->ID, 'pending' );
		if ( is_wp_error( $updated ) ) {
			return $updated;
		}

		$updated = tribe( Order::class )->modify_status( $order->ID, 'completed' );
		if ( is_wp_error( $updated ) ) {
			return $updated;
		}

		$attendee = tec_tc_attendees()->by( 'order_id', $order->ID )->first();

		return $attendee;
	}

	/**
	 * Update an attendee for the Commerce provider.
	 *
	 * @since 5.2.0
	 *
	 * @todo TribeLegacyCommerce We need to move this into the Attendee class.
	 *
	 * @param array|int $attendee      The attendee data or ID for the attendee to update.
	 * @param array     $attendee_data The attendee data to update to.
	 *
	 * @return WP_post|false The updated post object or false if unsuccessful.
	 */
	public function update_attendee( $attendee, $attendee_data ) {
		if ( is_numeric( $attendee ) ) {
			$attendee_id = (int) $attendee;
		} elseif ( is_array( $attendee ) && isset( $attendee['attendee_id'] ) ) {
			$attendee_id = (int) $attendee['attendee_id'];
		} else {
			return false;
		}

		$attendee = tec_tc_attendees( $this->orm_provider )
			->where( 'ID', $attendee_id );

		try {
			if ( ! empty( $attendee_data['attendee_meta'] ) ) {
				$attendee->set( 'fields', $attendee_data['attendee_meta'] );
			}

			if ( ! empty( $attendee_data['full_name'] ) ) {
				$attendee->set( 'full_name', $attendee_data['full_name'] );
			}

			if ( ! empty( $attendee_data['email'] ) && filter_var( $attendee_data['email'], FILTER_VALIDATE_EMAIL ) ) {
				$attendee->set( 'email', $attendee_data['email'] );
			}

			if ( isset( $attendee_data['check_in'] ) ) {
				$attendee->set( 'checked_in', $attendee_data['check_in'] );

				if ( $attendee_data['check_in'] ) {
					parent::checkin( $attendee_id );
				} else {
					parent::uncheckin( $attendee_id );
				}
			}

			$attendee->save();

			// Send attendee email.
			$send_ticket_email      = (bool) Arr::get( $attendee_data, 'send_ticket_email', false );
			$send_ticket_email_args = (array) Arr::get( $attendee_data, 'send_ticket_email_args', [] );

			// Check if we need to send the ticket email.
			if ( $send_ticket_email ) {
				$attendee_tickets = [
					$attendee_id,
				];

				// Maybe send the attendee email.
				$this->send_tickets_email_for_attendees( $attendee_tickets, $send_ticket_email_args );
			}
		} catch ( \Tribe__Repository__Usage_Error $e ) {
			do_action( 'tribe_log', 'error', __CLASS__, [ 'message' => $e->getMessage() ] );

			return false;
		}

		return $attendee;
	}

	/**
	 * Update the email sent counter for attendee by increasing it +1.
	 *
	 * @since 5.8.0
	 *
	 * @param int $attendee_id The attendee ID.
	 */
	public function update_ticket_sent_counter( $attendee_id ) {
		tribe( Email_Communication::class )->update_ticket_sent_counter( $attendee_id );
	}
}
