<?php
/**
 * Base order model.
 *
 * @package northmill-online-shared
 */

declare(strict_types=1);

namespace Northmill_Online_Shared\Modules\Sync\Orders\Models;

use Northmill_Online_Shared\Abstract_Shared_Base;
use Northmill_Online_Shared\Abstracts\Hub_API\Hub_Accounts\Models\Hub_Account_Model;
use Northmill_Online_Shared\Abstracts\Hub_API\Hub_Accounts\Hub_Account_Setup;
use Northmill_Online_Shared\Abstracts\Hub_API\Hub_Accounts\WP_Repository;
use Northmill_Online_Shared\Data\Payment_Methods;
use Northmill_Online_Shared\Helpers\General_Helpers;
use Northmill_Online_Shared\Helpers\Hook_Helpers;
use Northmill_Online_Shared\Modules\Admin\Settings\Settings_Fields_Module;
use Northmill_Online_Shared\Modules\Loggers\Log_Level_Enum;
use Northmill_Online_Shared\Modules\Sync\Customers\Customer_Factory;
use Northmill_Online_Shared\Modules\Sync\Customers\Models\Customer_Model;
use WC_Order;
use WC_Product;
use WC_Tax;

defined( 'ABSPATH' ) || exit;

/**
 * Class Order_Base_Model
 *
 * Base class for order models.
 */
class Order_Base_Model {

	public const META_KEY_HUB_ORDER_ID       = 'mf_id';
	public const META_KEY_HUB_ORDER_NUMBER   = 'mf_order_no';
	public const META_KEY_ORDER_TYPE         = 'mf_order_type';
	public const META_KEY_SYNC_STATUS        = 'mf_sync_status';
	public const META_KEY_FULLY_PAID         = 'mf_order_has_been_fully_paid';
	public const META_KEY_HASH               = 'mf_hash';
	public const META_KEY_LAST_SYNC_RESPONSE = 'mf_last_sync_response';
	public const META_KEY_HUB_ACCOUNT_WP_ID  = 'nmf_backoffice_account_id';

	/**
	 * Default payment origin when sending order to Hub.
	 *
	 * @used Northmill Sync.
	 */
	public const DEFAULT_PAYMENT_ORIGIN = 'WooCommerce';

	/**
	 * Hub account order was made to.
	 *
	 * @var Hub_Account_Model
	 */
	private Hub_Account_Model $backoffice_account;

	/**
	 * Order payment method.
	 *
	 * @var string
	 */
	private string $payment_method;

	/**
	 * Construct order class.
	 *
	 * @param WC_Order $wc_order WooCommerce order. Parent order if this is for a refund.
	 */
	public function __construct(
		protected readonly WC_Order $wc_order
	) {}

	/**
	 * Get order WC ID.
	 *
	 * @return int
	 */
	public function get_order_id(): int {
		return $this->wc_order->get_id();
	}

	/**
	 * Get order number in Hub.
	 * Note that if this is a refund order, the order number will be the parent order number.
	 *
	 * @return string|null
	 */
	public function get_hub_order_number(): ?string {
		$order_number = get_post_meta( $this->get_order_id(), self::META_KEY_HUB_ORDER_NUMBER, true );

		return Hook_Helpers::apply_scoped_filters(
			'northmill_online_shared_get_hub_order_number',
			$order_number,
			$this->get_order_id(),
			$this->wc_order
		);
	}

	/**
	 * Get order external reference.
	 *
	 * @param string $order_number WC order number.
	 * @param string $extra_prefix Extra prefix to add to the external reference. Between any custom prefix from settings, and the order number.
	 *
	 * @return string
	 *
	 * @used Northmill Sync
	 * @used Northmill Booking
	 */
	public function get_external_reference( string $order_number, string $extra_prefix = '' ): string {
		return $this->get_external_reference_prefix() . $extra_prefix . $order_number;
	}

	/**
	 * Get order external reference prefix.
	 *
	 * @return string
	 */
	private function get_external_reference_prefix(): string {
		$external_reference_prefix = Abstract_Shared_Base::base_instance()->admin->settings->get_setting(
			Settings_Fields_Module::HUB_ORDER_PREFIX,
			''
		);

		if ( ! empty( $external_reference_prefix ) ) {
			$external_reference_prefix = General_Helpers::ensure_trailing_symbol( $external_reference_prefix, '-' );
		}

		return $external_reference_prefix;
	}

	/**
	 * Get the order Hub account.
	 *
	 * @return Hub_Account_Model
	 */
	public function get_backoffice_account(): Hub_Account_Model {
		if ( ! empty( $this->backoffice_account ) ) {
			return $this->backoffice_account;
		}

		$backoffice_account_id = $this->wc_order->get_meta( self::META_KEY_HUB_ACCOUNT_WP_ID );
		$backoffice_account    = WP_Repository::get_account_by_post_id( $backoffice_account_id );

		if ( $backoffice_account ) {
			$this->backoffice_account = $backoffice_account;
			return $this->backoffice_account;
		}

		$backoffice_account = WP_Repository::get_current_main_account();

		if ( $backoffice_account ) {
			if ( Hub_Account_Setup::is_multiple_stocks_enabled() ) {
				Abstract_Shared_Base::base_instance()->logger->log(
					'sync',
					'Order has no Hub account. Using main account.',
					array(
						'order_number' => $this->wc_order->get_order_number(),
					),
					Log_Level_Enum::WARNING
				);
			}

			$this->backoffice_account = $backoffice_account;
			return $this->backoffice_account;
		}

		Abstract_Shared_Base::base_instance()->logger->log(
			'sync',
			'Order has no Hub account. No main account found.',
			array(
				'order_number' => $this->wc_order->get_order_number(),
			),
			Log_Level_Enum::CRITICAL
		);

		wp_die( 'No Hub account found.' );
	}

	/**
	 * Get the payment method for use in API.
	 *
	 * @return string
	 */
	protected function get_hub_order_pay_method(): string {
		if ( Payment_Methods::WC_COD === $this->get_pay_method() ) {
			return 'Cashier';
		} elseif ( Payment_Methods::NMC_MF_INVOICE === $this->get_pay_method() ) {
			return 'Invoice';
		} else {
			return 'Web';
		}
	}

	/**
	 * Get WC payment method for order.
	 * For parent order if this is a refund.
	 *
	 * @return string
	 */
	protected function get_pay_method(): string {
		if ( ! isset( $this->payment_method ) ) {
			$this->payment_method = $this->wc_order->get_payment_method();
		}
		return $this->payment_method;
	}

	/**
	 * Get payment status for use in API.
	 *
	 * @return string
	 */
	protected function get_order_pay_status(): string {
		// If the order has been fully paid, it shouldn't revert when set to on-hold (it needs to be credited).
		$order_has_been_fully_paid = get_post_meta( $this->wc_order->get_id(), self::META_KEY_FULLY_PAID, true );
		if ( $order_has_been_fully_paid ) {
			return 'FullyPaid';
		}

		$order_status = $this->wc_order->get_status();
		switch ( $order_status ) {
			case 'pending':
			case 'on-hold':
				$mf_pay_status = 'NotPaid';
				break;
			case 'processing':
				// Orders with the payment method 'cod' are set to processing when created (default WC behavior), but aren't paid.
				if ( $this->get_pay_method() === Payment_Methods::WC_COD ) {
					$mf_pay_status = 'NotPaid';
				} elseif ( $this->get_pay_method() === Payment_Methods::NMC_MF_INVOICE ) {
					$mf_pay_status = 'NotPaid';
				} else {
					$mf_pay_status = 'FullyPaid';
					update_post_meta( $this->wc_order->get_id(), self::META_KEY_FULLY_PAID, true );
				}
				break;
			case 'completed':
				$mf_pay_status = 'FullyPaid';
				update_post_meta( $this->wc_order->get_id(), self::META_KEY_FULLY_PAID, true );
				break;
			case 'cancelled':
			case 'refunded':
			case 'failed':
			default:
				$mf_pay_status = 'NotSet';
				break;
		}
		return $mf_pay_status;
	}

	/**
	 * Get order status for use in API.
	 *
	 * @param string $order_type WC order type.
	 *
	 * @return string
	 */
	protected function get_order_status_for_hub( string $order_type ): string {

		$order_status = $this->wc_order->get_status();

		$hub_order_status = match ( $order_status ) {
			'on-hold' => 'Started',
			'processing', 'completed' => 'Closed',
			'cancelled', 'refunded', 'failed', 'kco_incomplete' => 'Cancelled',
			default => 'Created',
		};

		$payment_completes_status = array( 'completed', 'processing' );
		if (
			in_array( $order_status, $payment_completes_status, true ) &&
			empty( $this->wc_order->get_transaction_id() )
		) {
			$hub_order_status = 'ReadyForHandOut';
		}

		// Order v3 integration.
		if (
			'NORMAL_ORDER' === $order_type &&
			in_array( $order_status, $payment_completes_status, true ) &&
			$this->get_pay_method() !== Payment_Methods::WC_COD &&
			$this->get_pay_method() !== Payment_Methods::NMC_MF_INVOICE
		) {
			if ( $this->is_shipping_local_pickup() ) {
				$hub_order_status = 'ReadyForHandOut';
			} else {
				$hub_order_status = 'Delivered';
			}
		}

		return $hub_order_status;
	}

	/**
	 * Create customer data from this order.
	 *
	 * @return Customer_Model|null
	 */
	public function get_customer(): ?Customer_Model {
		return Customer_Factory::create_customer_from_order( $this->wc_order );
	}

	/**
	 * Get the billing address for this order.
	 *
	 * @return array|null Array of billing address data for API, or null if name in billing address is set.
	 */
	protected function get_billing_address(): ?array {

		$address = array(
			'Name'     => trim( $this->wc_order->get_billing_first_name() . ' ' . $this->wc_order->get_billing_last_name() ),
			'Address1' => $this->wc_order->get_billing_address_1(),
			'Address2' => $this->wc_order->get_billing_address_2(),
			'PostCode' => $this->wc_order->get_billing_postcode(),
			'City'     => $this->wc_order->get_billing_city(),
			'Country'  => $this->wc_order->get_billing_country(),
			'Email'    => $this->wc_order->get_billing_email(),
			'Mobile'   => $this->wc_order->get_billing_phone(),
		);

		foreach ( $address as $key => $v ) {
			if ( empty( $v ) ) {
				unset( $address[ $key ] );
			}
		}

		return ( isset( $address['Name'] ) ? $address : null );
	}

	/**
	 * Get the shipping address for this order.
	 *
	 * @return array|null Array of shipping address data for API, or null if name in shipping address is set.
	 */
	protected function get_shipping_address(): ?array {

		$address = array(
			'Name'     => trim( $this->wc_order->get_shipping_first_name() . ' ' . $this->wc_order->get_billing_last_name() ),
			'Address1' => $this->wc_order->get_shipping_address_1(),
			'Address2' => $this->wc_order->get_shipping_address_2(),
			'PostCode' => $this->wc_order->get_shipping_postcode(),
			'City'     => $this->wc_order->get_shipping_city(),
			'Country'  => $this->wc_order->get_shipping_country(),
		);

		foreach ( $address as $key => $v ) {
			if ( empty( $v ) ) {
				unset( $address[ $key ] );
			}
		}

		return ( isset( $address['Name'] ) ? $address : null );
	}

	/**
	 * Get tax for order row
	 *
	 * @param WC_Product $product Product to get tax for.
	 *
	 * @return float
	 */
	protected function get_order_row_tax( WC_Product $product ): float {
		$tax_percentage = 0;
		$wc_tax         = new WC_Tax();

		$tax_class = $product->get_tax_class();

		$tax_data = $wc_tax->get_rates( $tax_class );

		foreach ( $tax_data as $tax_rate ) {
			$tax_percentage = $tax_rate['rate'];
		}

		return is_numeric( $tax_percentage ) ? (float) $tax_percentage : 0;
	}


	/**
	 * Retrieve the message indicating that the order synchronization has failed.
	 *
	 * @return string
	 */
	protected function get_order_sync_failed_message(): string {
		return __( 'Hub order sync failed. Please retry by saving the order.', 'northmill-online-shared' );
	}

	/**
	 * Retrieve the message indicating that the order synchronization has succeeded.
	 * Should return a string with three placeholders:
	 * %1$s: Hub order number
	 * %2$s: action taken on order (added/updated)
	 * %3$s: Hub account name.
	 *
	 * @return string
	 */
	protected function get_order_sync_succeeded_message(): string {
		/*
		 * translators:
		 * %1$s: Hub order number
		 * %2$s: action taken on order (added/updated)
		 * %3$s: Hub account name.
		 */
		return __( 'Order #%1$s %2$s in Hub account %3$s.', 'northmill-online-shared' );
	}

	/**
	 * Handle response from Hub when syncing order.
	 *
	 * @param object|null $hub_response      Hub response object with order data. Null if error.
	 * @param string      $action            Executed API action.
	 * @param string      $order_key         Key (property) in response object for order data.
	 * @param string      $order_number_key  Key (property) in response object for order number.
	 * @param string      $order_id_key      Key (property) in response object for order ID.
	 *
	 * @return bool
	 *
	 * @used Northmill Sync
	 * @used Northmill Booking
	 */
	public function handle_hub_order_sync_response( ?object $hub_response, string $action = 'add', string $order_key = 'Order', string $order_number_key = 'Number', string $order_id_key = 'Id' ): bool {
		if ( $hub_response ) {
			$order_id = $this->get_order_id();

			if ( ! empty( $hub_response->list[0]->Success ) ) {
				if ( ! empty( $hub_response->list[0]->{$order_key}->{$order_number_key} ) ) {
					update_post_meta( $order_id, self::META_KEY_HUB_ORDER_ID, $hub_response->list[0]->{$order_key}->{$order_id_key} );
					update_post_meta( $order_id, self::META_KEY_HUB_ORDER_NUMBER, $hub_response->list[0]->{$order_key}->{$order_number_key} );
					update_post_meta( $order_id, self::META_KEY_ORDER_TYPE, $hub_response->list[0]->{$order_key}->{$order_number_key} );
					update_post_meta( $order_id, self::META_KEY_LAST_SYNC_RESPONSE, $hub_response );
					update_post_meta( $order_id, self::META_KEY_SYNC_STATUS, __( 'Synced', 'northmill-online-shared' ) );
				}

				$action_text = 'update' === $action ?
					__( 'updated', 'northmill-online-shared' ) :
					__( 'added', 'northmill-online-shared' );

				$this->add_order_note(
					sprintf(
						$this->get_order_sync_succeeded_message(),
						$hub_response->list[0]->{$order_key}->{$order_number_key} ?? 'NULL',
						$action_text,
						$this->get_backoffice_account()->get_display_name(),
					)
				);
				return true;
			}

			$this->add_order_note( $hub_response );
		}

		$this->add_order_note( $this->get_order_sync_failed_message() );
		return false;
	}

	/**
	 * Adds note to order.
	 *
	 * @param mixed $note Note to add. Will be run through print_r if not a string.
	 *
	 * @return void
	 */
	public function add_order_note( mixed $note ): void {
		if ( ! is_string( $note ) ) {
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
			$note = print_r( $note, true );
		}

		$this->wc_order->add_order_note( $note );
	}

	/**
	 * Check if the shipping method of this order is local pickup.
	 *
	 * @return bool
	 */
	public function is_shipping_local_pickup(): bool {
		$shipping_methods = $this->wc_order->get_shipping_methods();
		$local_pickup     = false;

		foreach ( $shipping_methods as $item ) {
			$shipping_method_id = $item->get_method_id();
			if ( 'local_pickup' === $shipping_method_id ) {
				$local_pickup = true;
			}
		}

		return $local_pickup;
	}

	/**
	 * Get order total of this order.
	 *
	 * @return float
	 *
	 * @used Northmill Sync
	 */
	public function get_order_total(): float {
		/**
		 * Method wc_order->get_total() actually returs a string.
		 *
		 * @noinspection PhpCastIsUnnecessaryInspection
		 */
		return floatval( $this->wc_order->get_total() );
	}

	/**
	 * Get WC order of this model.
	 *
	 * @return WC_Order
	 */
	public function get_wc_order(): WC_Order {
		return $this->wc_order;
	}
}
