<?php

namespace Automattic\WCShipping\LabelPurchase;

use Automattic\WCShipping\Utils;
use WC_Order;
use WC_Order_Item_Product;
use Automattic\WCShipping\Connect\WC_Connect_Account_Settings;
use Automattic\WCShipping\Connect\WC_Connect_Service_Schemas_Store;
use Automattic\WCShipping\Connect\WC_Connect_Utils;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class ViewService {

		/**
		 * @var array Supported countries by USPS, see: https://webpmt.usps.gov/pmt010.cfm
		 */
	private $supported_countries = array( 'US', 'AS', 'PR', 'VI', 'GU', 'MP', 'UM', 'FM', 'MH' );

	/**
	 * @var array Supported currencies
	 */
	private $supported_currencies = array( 'USD' );

	/**
	 * @var WC_Connect_Account_Settings
	 */
	private $account_settings;

	/**
	 * @var WC_Connect_Service_Schemas_Store
	 */
	private $service_schemas_store;

	/**
	 * @param WC_Connect_Account_Settings      $account_settings
	 * @param WC_Connect_Service_Schemas_Store $service_schemas_store
	 */
	function __construct( WC_Connect_Account_Settings $account_settings, WC_Connect_Service_Schemas_Store $service_schemas_store ) {
		$this->account_settings      = $account_settings;
		$this->service_schemas_store = $service_schemas_store;
	}

	/**
	 * Remove shipment information stored along the label for labels with PURCHASE_ERROR status based on the shipment id.
	 * Only removes the meta if there are no other non-error labels with the same shipment id.
	 *
	 * @param array $purchased_labels [
	 *    [ 'id' => {WCS shipment id}, 'status' => 'PURCHASED' ],
	 *    [ 'id' => {WCS shipment id}, 'status' => 'PURCHASE_ERROR' ]
	 *  ]
	 * @param array $shipment_meta [ 'shipment_0' => [...], 'shipment_1' => [...] ]
	 *
	 * @return array
	 */
	public function remove_meta_for_purchase_error( array $purchased_labels, array $shipment_meta ): array {
		foreach ( $purchased_labels as $purchased_label ) {
			$shipment_id                   = $purchased_label['id'];
			$shipment_key                  = "shipment_{$shipment_id}";
			$none_error_label_with_same_id = array_filter(
				$purchased_labels,
				function ( $label ) use ( $purchased_label ) {
					return $label['id'] === $purchased_label['id'] && $label['status'] !== 'PURCHASE_ERROR';
				}
			);

			if (
				$purchased_label['status'] === 'PURCHASE_ERROR' &&
				isset( $shipment_meta[ $shipment_key ] ) &&
				empty( $none_error_label_with_same_id )
			) {
				unset( $shipment_meta[ $shipment_key ] );
			}
		}

		return $shipment_meta;
	}

	/**
	 * Remove shipment information stored along refunded labels.
	 *
	 * The purchased label will still contain all refund related data (e.g. status), but we will no longer be using the
	 * selected rate itself, so we might as well filter it out immediately to make dependent logic simpler (e.g. React).
	 *
	 * @param array[] $purchased_labels {
	 *     @type int      $id The WCS shipment ID (read: not label ID).
	 *     @type int      $label_id The purchased label ID.
	 *     @type object   $refund A conditional object that will only exist if the label has a refund request.
	 *     ...
	 * }
	 * @param array[] $shipment_meta An array of shipment related meta-data. The key for these are "shipment_{id}".
	 * @return array
	 */
	public function remove_meta_for_refunds( array $purchased_labels, array $shipment_meta ): array {
		foreach ( $purchased_labels as $purchased_label ) {
			$shipment_key                     = sprintf( 'shipment_%d', $purchased_label['id'] );
			$none_refunded_label_with_same_id = array_filter(
				$purchased_labels,
				function ( $label ) use ( $purchased_label ) {
					return $label['id'] === $purchased_label['id'] && empty( $label['refund'] );
				}
			);

			if (
				! empty( $purchased_label['refund'] ) &&
				isset( $shipment_meta[ $shipment_key ] )
				/**
				 * Only remove the meta if there is no none refunded label with the same id (shipment id)
				 * As once a new purchase is made, the former meta is overwritten with the new one for the same id (shipment id)
				 */
				&& empty( $none_refunded_label_with_same_id )
			) {
				unset( $shipment_meta[ $shipment_key ] );
			}
		}

		return $shipment_meta;
	}

	/**
	 * Remove customs information snapshots for refunded shipments.
	 *
	 * @param array[] $purchased_labels {
	 *     @type int      $id The WCS shipment ID (read: not label ID).
	 *     @type object   $refund A conditional object that will only exist if the label has a refund request.
	 *     ...
	 * }
	 * @param array[] $customs_information An array of customs form snapshots. The key for these are "shipment_{id}".
	 * @return array
	 */
	public function remove_customs_information_for_refunds( array $purchased_labels, array $customs_information ): array {
		foreach ( $purchased_labels as $purchased_label ) {
			$shipment_key = sprintf( 'shipment_%d', $purchased_label['id'] );

			if (
				! empty( $purchased_label['refund'] ) &&
				isset( $customs_information[ $shipment_key ] )
			) {
				unset( $customs_information[ $shipment_key ] );
			}
		}

		return $customs_information;
	}

	/**
	 * This function transform the WC_Order object to a representational JSON form for the React app.
	 *
	 * This is based on WooCommerce v3's get_order API woocommerce/includes/legacy/api/v3/class-wc-api-orders.php.
	 *
	 * @param WC_Order $order The Woo Order we wish to prepare for the API.
	 * @return array
	 */
	public function get_order_data( WC_Order $order ): array {
		$decimal_point = 2;
		$order_data    = array(
			'id'                        => $order->get_id(),
			'order_number'              => $order->get_order_number(),
			'order_key'                 => $order->get_order_key(),
			'created_at'                => $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0,
			'updated_at'                => wc_format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0 ),
			'completed_at'              => wc_format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0 ),
			'status'                    => $order->get_status(),
			'currency'                  => $order->get_currency(),
			'total'                     => wc_format_decimal( $order->get_total(), $decimal_point ),
			'subtotal'                  => wc_format_decimal( $order->get_subtotal(), $decimal_point ),
			'total_line_items_quantity' => 0,
			'total_tax'                 => wc_format_decimal( $order->get_total_tax(), $decimal_point ),
			'total_shipping'            => wc_format_decimal( $order->get_shipping_total(), $decimal_point ),
			'cart_tax'                  => wc_format_decimal( $order->get_cart_tax(), $decimal_point ),
			'shipping_tax'              => wc_format_decimal( $order->get_shipping_tax(), $decimal_point ),
			'total_discount'            => wc_format_decimal( $order->get_total_discount(), $decimal_point ),
			'shipping_methods'          => html_entity_decode( $order->get_shipping_method(), ENT_QUOTES, get_bloginfo( 'charset' ) ),
			'payment_details'           => array(
				'method_id'    => $order->get_payment_method(),
				'method_title' => $order->get_payment_method_title(),
				'paid'         => ! is_null( $order->get_date_paid() ),
			),
			'billing_address'           => array(
				'first_name' => $order->get_billing_first_name(),
				'last_name'  => $order->get_billing_last_name(),
				'company'    => $order->get_billing_company(),
				'address_1'  => $order->get_billing_address_1(),
				'address_2'  => $order->get_billing_address_2(),
				'city'       => $order->get_billing_city(),
				'state'      => $order->get_billing_state(),
				'postcode'   => $order->get_billing_postcode(),
				'country'    => $order->get_billing_country(),
				'email'      => $order->get_billing_email(),
				'phone'      => $order->get_billing_phone(),
			),
			'shipping_address'          => array(
				'first_name' => $order->get_shipping_first_name(),
				'last_name'  => $order->get_shipping_last_name(),
				'company'    => $order->get_shipping_company(),
				'address_1'  => $order->get_shipping_address_1(),
				'address_2'  => $order->get_shipping_address_2(),
				'city'       => $order->get_shipping_city(),
				'state'      => $order->get_shipping_state(),
				'postcode'   => $order->get_shipping_postcode(),
				'country'    => $order->get_shipping_country(),
				'email'      => $order->get_billing_email(),
				'phone'      => $order->get_shipping_phone(),
			),
			'note'                      => $order->get_customer_note(),
			'customer_ip'               => $order->get_customer_ip_address(),
			'customer_user_agent'       => $order->get_customer_user_agent(),
			'customer_id'               => $order->get_user_id(),
			'view_order_url'            => $order->get_view_order_url(),
			'line_items'                => array(),
			'shipping_lines'            => array(),
			'tax_lines'                 => array(),
			'fee_lines'                 => array(),
			'coupon_lines'              => array(),
		);

		// Add line items.
		foreach ( $order->get_items() as $item_id => $item ) {
			/** @var WC_Order_Item_Product $item */
			$product = $item->get_product();

			if ( ! $product instanceof \WC_Product ) {
				continue;
			}

			if ( ! $product->needs_shipping() ) {
				continue;
			}

			$order_data['total_line_items_quantity'] += $item->get_quantity();
			$product_meta                             = array();

			$customs_info = Utils::get_product_customs_data( $product );
			if ( $customs_info ) {
				$product_meta['customs_info'] = $customs_info;
			}

			$variations = array_map(
				function ( $item_meta ) {
					$new_meta = new \stdClass();
					foreach ( $item_meta as $property => $value ) {
						$new_meta->{ $property } = wp_kses( $value, array() );
					}

					return $new_meta;
				},
				array_values( $item->get_all_formatted_meta_data() )
			);

			$line_item = array(
				'id'           => $item_id,
				'subtotal'     => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $decimal_point ),
				'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $decimal_point ),
				'total'        => wc_format_decimal( $order->get_line_total( $item, false, false ), $decimal_point ),
				'total_tax'    => wc_format_decimal( $item->get_total_tax(), $decimal_point ),
				'price'        => wc_format_decimal( $order->get_item_total( $item, false, false ), $decimal_point ),
				'quantity'     => $item->get_quantity(),
				'tax_class'    => $item->get_tax_class(),
				'name'         => wp_kses( $item->get_name(), array() ),
				'product_id'   => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(),
				'sku'          => is_object( $product ) ? $product->get_sku() : null,
				'meta'         => (object) $product_meta,
				'image'        => wp_get_attachment_url( $product->get_image_id() ) ?: wc_placeholder_img_src(),
				'weight'       => $product->get_weight(),
				'dimensions'   => array(
					'length' => $product->get_length(),
					'width'  => $product->get_width(),
					'height' => $product->get_height(),
				),
				'variation'    => $variations,
			);

			$order_data['line_items'][] = $line_item;
		}

		// Add shipping.
		foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) {
			$order_data['shipping_lines'][] = array(
				'id'           => $shipping_item_id,
				'method_id'    => $shipping_item->get_method_id(),
				'method_title' => $shipping_item->get_name(),
				'total'        => wc_format_decimal( $shipping_item->get_total(), $decimal_point ),
			);
		}

		// Add taxes.
		foreach ( $order->get_tax_totals() as $tax_code => $tax ) {
			$tax_line = array(
				'id'       => $tax->id,
				'rate_id'  => $tax->rate_id,
				'code'     => $tax_code,
				'title'    => $tax->label,
				'total'    => wc_format_decimal( $tax->amount, $decimal_point ),
				'compound' => (bool) $tax->is_compound,
			);

			$order_data['tax_lines'][] = $tax_line;
		}

		// Add fees.
		foreach ( $order->get_fees() as $fee_item_id => $fee_item ) {
			$order_data['fee_lines'][] = array(
				'id'        => $fee_item_id,
				'title'     => $fee_item->get_name(),
				'tax_class' => $fee_item->get_tax_class(),
				'total'     => wc_format_decimal( $order->get_line_total( $fee_item ), $decimal_point ),
				'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $decimal_point ),
			);
		}

		// Add coupons.
		foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) {
			/** @var WC_Order_Item_Coupon $coupon_item */
			$coupon_line = array(
				'id'     => $coupon_item_id,
				'code'   => $coupon_item->get_code(),
				'amount' => wc_format_decimal( $coupon_item->get_discount(), $decimal_point ),
			);

			$order_data['coupon_lines'][] = $coupon_line;
		}

		return $order_data;
	}


	/**
	 * Check whether the given country code is supported for shipping labels.
	 */
	public function is_supported_country( string $country_code ): bool {
		return in_array( $country_code, $this->supported_countries, true );
	}

	/**
	 * Check whether the given currency code is supported for shipping labels.
	 */
	public function is_supported_currency( string $currency_code ): bool {
		return in_array( $currency_code, $this->supported_currencies, true );
	}

	/**
	 * Check whether shipping label feature is enabled from WC Services setting.
	 */
	public function is_shipping_label_enabled(): bool {
		$account_settings = $this->account_settings->get( true );

		if ( isset( $account_settings['purchaseSettings']['enabled'] ) && is_bool( $account_settings['purchaseSettings']['enabled'] ) ) {
			return $account_settings['purchaseSettings']['enabled'];
		}

		return true;
	}

	/**
	 * Check whether the given order is eligible for shipping label creation - the order has at least one product that is:
	 * - Shippable.
	 * - Non-refunded.
	 *
	 * @param WC_Order $order The order to check for shipping label creation eligibility.
	 * @return bool Whether the given order is eligible for shipping label creation.
	 */
	public function is_order_eligible_for_shipping_label_creation( WC_Order $order ): bool {
		// Set up a dictionary from product ID to quantity in the order, which will be updated by refunds and existing labels later.
		$quantities_by_product_id = array();
		foreach ( $order->get_items() as $item ) {
			$product = WC_Connect_Utils::get_item_product( $order, $item );
			if ( $product && $product->needs_shipping() ) {
				$product_id                              = $product->get_id();
				$current_quantity                        = array_key_exists( $product_id, $quantities_by_product_id ) ? $quantities_by_product_id[ $product_id ] : 0;
				$quantities_by_product_id[ $product_id ] = $current_quantity + $item->get_quantity();
			}
		}

		// A shipping label cannot be created without a shippable product.
		if ( empty( $quantities_by_product_id ) ) {
			return false;
		}

		// Update the quantity for each refunded product ID in the order.
		foreach ( $order->get_refunds() as $refund ) {
			foreach ( $refund->get_items() as $refunded_item ) {
				$product = WC_Connect_Utils::get_item_product( $order, $refunded_item );
				if ( ! is_a( $product, 'WC_Product' ) ) {
					continue;
				}

				$product_id = $product->get_id();
				if ( array_key_exists( $product_id, $quantities_by_product_id ) ) {
					$current_count                           = $quantities_by_product_id[ $product_id ];
					$quantities_by_product_id[ $product_id ] = $current_count - abs( $refunded_item->get_quantity() );
				}
			}
		}

		// The order is eligible for shipping label creation when there is at least one product with positive quantity.
		foreach ( $quantities_by_product_id as $product_id => $quantity ) {
			if ( $quantity > 0 ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Check whether the store is eligible for shipping label creation:
	 * - Store currency is supported.
	 * - Store country is supported.
	 *
	 * @return bool Whether the WC store is eligible for shipping label creation.
	 */
	public function is_store_eligible_for_shipping_label_creation(): bool {
		$base_currency = get_woocommerce_currency();
		if ( ! $this->is_supported_currency( $base_currency ) ) {
			return false;
		}

		$base_location = wc_get_base_location();
		if ( ! $this->is_supported_country( $base_location['country'] ) ) {
			return false;
		}

		return true;
	}

	public function is_dhl_express_available(): bool {
		$dhl_express = $this->service_schemas_store->get_service_schema_by_id( 'dhlexpress' );

		return (bool) $dhl_express;
	}
}
