<?php

namespace Automattic\WCShipping\Shipments;

use Automattic\WCShipping\Connect\WC_Connect_Service_Settings_Store;
use Automattic\WCShipping\Shipment\ShipmentFromLabelGenerator;
use Automattic\WCShipping\Shipments\Models\Shipments;
use Exception;
use InvalidArgumentException;
use WC_Order;
use Automattic\WCShipping\Exceptions\RESTRequestException;
use Automattic\WCShipping\Utils;

class ShipmentsService {

	const META_KEY = '_wcshipping-shipments';

	/**
	 * @var WC_Connect_Service_Settings_Store
	 */
	protected $settings_store;

	/**
	 * @var ShipmentDataValidator
	 */
	protected $validator;

	/**
	 * Constructor.
	 *
	 * @param WC_Connect_Service_Settings_Store $settings_store Settings store instance.
	 * @param ShipmentDataValidator|null        $validator      Optional validator instance for testing.
	 */
	public function __construct( WC_Connect_Service_Settings_Store $settings_store, ?ShipmentDataValidator $validator = null ) {
		$this->settings_store = $settings_store;
		$this->validator      = $validator ?? new ShipmentDataValidator();
	}

	/**
	 * Update shipments on an order.
	 *
	 * @param int   $order_id The WC order ID.
	 * @param array $raw_shipments The shipments sent with the api call.
	 * @param array $shipment_ids_to_update The shipment ID map to update.
	 * @return RESTRequestException|int
	 *
	 * @throws RESTRequestException Will throw an error if the order is not found.
	 */
	public function update_order_shipments( int $order_id, array $raw_shipments, array $shipment_ids_to_update = array() ) {
		/**
		 * @var WC_Order $order
		 */
		$order = wc_get_order( $order_id );
		if ( ! $order instanceof WC_Order ) {
			/*
			 * We have to escape an exceptions output in case it's not caught internally.
			 *
			 * @link https://github.com/WordPress/WordPress-Coding-Standards/issues/884
			 */
			throw new RESTRequestException( esc_html__( 'Order not found when updating order shipments ', 'woocommerce-shipping' ) );
		}

		$shipments = $this->create_shipments_model( $raw_shipments, $order );

		$order->update_meta_data( self::META_KEY, $shipments );

		if ( ! empty( $shipment_ids_to_update ) ) {
			$order_labels = $this->settings_store->get_label_order_meta_data( $order_id );
			foreach ( $order_labels as &$label ) {
				if ( isset( $shipment_ids_to_update[ $label['id'] ] ) ) {
					$label['id'] = $shipment_ids_to_update[ $label['id'] ];
				}
			}
			$order->update_meta_data( 'wcshipping_labels', $order_labels );
		}

		return $order->save();
	}

	/**
	 * Get the order shipments JSON.
	 *
	 * If shipments are already stored as order meta, returns a JSON encoded object of those shipments.
	 * Otherwise, it generates shipments (and flag whether fallback was needed) and then returns a JSON encoded object.
	 *
	 * @param int $order_id The WC order ID.
	 * @return array {
	 *     @type array $shipments The shipments.
	 *     @type array $autogenerated_from_labels The autogenerated from labels.
	 * }
	 */
	public function get_order_shipments_data( $order_id ): array {
		/**
		 * @var WC_Order $order
		 */
		$order = wc_get_order( $order_id );
		if ( ! $order instanceof WC_Order ) {
			return array(
				'shipments'                 => array(),
				'autogenerated_from_labels' => array(),
			);
		}

		$raw_shipments = $order->get_meta( self::META_KEY );

		if ( ! empty( $raw_shipments ) ) {
			$shipments = $this->create_shipments_model( $raw_shipments, $order );

			return array(
				'shipments'                 => $shipments,
				'autogenerated_from_labels' => array(),
			);
		}

		$labels = $this->settings_store->get_label_order_meta_data( $order->get_id() );

		// Filter labels that are refunded or ones that errored while purchase
		// when auto-generating shipments to prevent
		// creation of extra shipments from refunded labels.
		$valid_labels = array_filter(
			$labels,
			function ( $label ) {
				return empty( $label['refund'] ) && $label['status'] !== 'PURCHASE_ERROR';
			}
		);

		/**
		 * We normally end up generating shipments from labels when we have only once shipment,
		 * that's where shipment split flow is never used and historically no shipment meta is set.
		 * So we return the shipment built from order items and the autogenerated from labels array
		 * with the index of the label that was used to generate the shipment.
		 */
		if ( count( $valid_labels ) === 1 ) {
			return array(
				'shipments'                 => array( self::build_shipment_from_order_items( $order ) ),
				'autogenerated_from_labels' => array( 0 ),
			);
		}

		$generator = new ShipmentFromLabelGenerator( $order );

		return $generator->generate_shipments( $valid_labels );
	}

	/**
	 * Create a Shipments model from raw data.
	 *
	 * First attempts to create the model directly. If an InvalidArgumentException
	 * is thrown (indicating corrupted data like null item IDs), runs the repair
	 * logic and retries. This avoids the overhead of validation for healthy orders.
	 *
	 * @param array    $raw_shipments Raw shipments data.
	 * @param WC_Order $order         The WooCommerce order object.
	 * @return array The shipments data as an array.
	 *
	 * @throws RESTRequestException If model creation fails even after repair.
	 */
	protected function create_shipments_model( array $raw_shipments, WC_Order $order ): array {
		try {
			$shipments_model = new Shipments( $raw_shipments );
			return $shipments_model->to_array();
		} catch ( InvalidArgumentException $e ) {
			// Data is corrupted (e.g., null item IDs). Attempt repair and retry.
			$repaired_shipments = $this->validator->validate_and_repair( $raw_shipments, $order );

			try {
				$shipments_model = new Shipments( $repaired_shipments );
				return $shipments_model->to_array();
			} catch ( Exception $retry_exception ) {
				throw new RESTRequestException(
					sprintf(
						/* translators: %s: Reason for the failure in creating the data model */
						esc_html__( 'Shipment model creation failed after repair attempt: %s', 'woocommerce-shipping' ),
						esc_html( $retry_exception->getMessage() )
					)
				);
			}
		} catch ( Exception $e ) {
			throw new RESTRequestException(
				sprintf(
					/* translators: %s: Reason for the failure in creating the data model */
					esc_html__( 'Shipment model creation failed with error: %s', 'woocommerce-shipping' ),
					esc_html( $e->getMessage() )
				)
			);
		}
	}

	/**
	 * Build a shipment from order items.
	 *
	 * @param WC_Order $order Order object.
	 * @return array
	 */
	public static function build_shipment_from_order_items( $order ) {
		$order_products = array();
		foreach ( $order->get_items() as $item_id => $item ) {
			$product = $item->get_product();

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

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

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

			$subItems = array();
			if ( $item->get_quantity() > 1 ) {
				foreach ( array_fill( 0, $item->get_quantity(), null ) as $index => $_ ) {
					$subItems[] = $item_id . '-sub-' . $index;
				}
			}
			$line_item = array(
				'id'       => $item_id,
				'subItems' => $subItems,
			);

			$order_products[] = $line_item;
		}

		return $order_products;
	}
}