<?php
/**
 * Class AutoCourierCreator
 *
 * @package DhlPolska\Shipment
 */

namespace DhlPolska\Shipment;

use DhlPolska\Exception\CourierBookingFailedException;
use DhlPolska\Exception\ExceptionMessageResolver;
use DhlPolskaVendor\Octolize\DhlPolska\API\WebAPI;
use DhlPolskaVendor\Octolize\DhlPolska\API\WebApi\Struct\AddressData;
use DhlPolskaVendor\Octolize\DhlPolska\API\WebApi\Struct\AuthData as WebApiAuthData;
use DhlPolskaVendor\Octolize\DhlPolska\API\WebApi\Struct\ShipmentOrderInfo;
use DhlPolska\Integration\FlexibleShipping\FlexibleShippingIntegration;
use DhlPolska\WooCommerceSettings\WooCommerceDHLPolskaSettings;
use DhlPolska\Shipment\WorkingDayDateCalculator;
use DhlPolskaVendor\Psr\Log\LoggerInterface;
use DhlPolskaVendor\WPDesk\PluginBuilder\Plugin\Hookable;
use WC_Order;

/**
 * Books DHL courier automatically on order status change.
 */
class AutoCourierCreator implements Hookable {
	use UnitsConversionTrait;

	private const ORDER_META_COURIER_BOOKING_IDS = '_dhl_polska_courier_booking_ids';

	private WooCommerceDHLPolskaSettings $settings;

	private LoggerInterface $logger;

	public function __construct(
		WooCommerceDHLPolskaSettings $settings,
		LoggerInterface $logger
	) {
		$this->settings = $settings;
		$this->logger   = $logger;
	}

	public function hooks() {
		add_action( 'woocommerce_order_status_changed', [ $this, 'maybe_book_courier' ], 20, 4 );
	}

	/**
	 * @param int            $order_id .
	 * @param string         $old_status .
	 * @param string         $new_status .
	 * @param WC_Order|false $order .
	 *
	 * @return void
	 */
	public function maybe_book_courier( $order_id, $old_status, $new_status, $order ) {
		$auto_statuses = $this->settings->get_auto_courier_statuses();
		if ( empty( $auto_statuses ) || ! $this->is_target_status( (string) $new_status, $auto_statuses ) ) {
			return;
		}

		if ( ! $order instanceof WC_Order ) {
			$order = wc_get_order( $order_id );
		}
		if ( ! $order ) {
			return;
		}

		$shipments = fs_get_order_shipments( $order->get_id(), FlexibleShippingIntegration::INTEGRATION_ID );
		if ( empty( $shipments ) ) {
			return;
		}

		$shipment_ids        = [];
		$shipment_datas      = [];
		$shipments_to_update = [];

		foreach ( $shipments as $shipment ) {
			$shipment_data = new ShipmentData( $shipment, $order, $this->settings );
			$shipment_id   = $shipment_data->get_shipment_id();
			if ( '' === $shipment_id ) {
				$this->logger->warning(
					sprintf(
						'DHL API auto courier booking skipped - missing shipment ID (order: %d, shipment: %d).',
						$order->get_id(),
						(int) $shipment->get_id()
					)
				);

				return;
			}

			$shipment_ids[]        = $shipment_id;
			$shipment_datas[]      = $shipment_data;
			$shipments_to_update[] = $shipment;
		}

		$pickup_date      = $this->get_pickup_date();
		$pickup_time_from = $this->settings->get_courier_pickup_start();
		$pickup_time_to   = $this->settings->get_courier_pickup_end();
		$order_info       = $this->build_shipment_order_info( $shipment_datas );
		try {
			$booking_ids = $this->get_webapi()->book_courier(
				$pickup_date,
				$pickup_time_from,
				$pickup_time_to,
				$shipment_ids,
				$order_info
			);
			if ( ! empty( $booking_ids ) ) {
				$booking_id = (string) reset( $booking_ids );
				$booking_id = trim( $booking_id );

				if ( '' === $booking_id ) {
					return;
				}

				foreach ( $shipments_to_update as $shipment ) {
					if ( $shipment instanceof DhlShipment ) {
						$shipment->set_courier_booking_id( $booking_id );
					}
				}

				$order->update_meta_data( self::ORDER_META_COURIER_BOOKING_IDS, $booking_ids );
				$order->add_order_note(
					sprintf(
						// Translators: courier booking identifiers.
						__( 'Zamówiono kuriera DHL. Identyfikatory zamówienia: %s', 'dhl-polska' ),
						implode( ', ', $booking_ids )
					)
				);
				$order->save();
			}
		} catch ( \Exception $exception ) {
				$this->logger->error(
					sprintf(
						'DHL API auto courier booking error (order: %d): %s',
						$order->get_id(),
						$exception->getMessage()
					)
				);
				$error_message = ExceptionMessageResolver::resolve_throwable( $exception, CourierBookingFailedException::class );
				$order->add_order_note( $error_message );
		}
	}

	private function get_pickup_date(): string {
		$calculator             = new WorkingDayDateCalculator();
		$shipment_date_fallback = new ShipmentDateFallback();
		$cutoff                 = $this->settings->get_courier_booking_date_cutoff_time();
		$date                   = $calculator->get_date_after_cutoff( $cutoff );

		if ( null !== $date ) {
			return $date;
		}

		return $shipment_date_fallback->today();
	}

	/**
	 * @param ShipmentData[] $shipment_datas .
	 *
	 * @return ShipmentOrderInfo
	 */
	private function build_shipment_order_info( array $shipment_datas ): ShipmentOrderInfo {
		$postal_code_normalizer = new PostalCodeNormalizer();
		$pieces_counter         = new ShipmentOrderInfoPiecesCounter();
		$pieces_counts          = $pieces_counter->count( $shipment_datas );

		$shipper = new AddressData(
			(string) $this->settings->get_sender_name(),
			(string) $this->settings->get_sender_street(),
			(string) $this->settings->get_sender_house_number(),
			(string) $this->settings->get_sender_city(),
			$postal_code_normalizer->normalize( (string) $this->settings->get_sender_postcode(), 'PL' ),
			null,
			(string) $this->settings->get_sender_name(),
			(string) $this->settings->get_sender_phone(),
			(string) $this->settings->get_sender_email()
		);

		$total_weight          = 0.0;
		$heaviest_piece_weight = null;

		foreach ( $shipment_datas as $shipment_data ) {
			foreach ( $shipment_data->get_packages() as $package ) {
				$quantity = max( 1, (int) ( $package['quantity'] ?? 1 ) );
				$weight   = $this->get_weight_in_kg( (float) ( $package['weight'] ?? 0 ) );

				if ( null !== $weight ) {
					$total_weight         += ( $weight * $quantity );
					$heaviest_piece_weight = max( $heaviest_piece_weight ?? 0.0, $weight );
				}
			}
		}

		$total_weight_int          = $total_weight > 0 ? (int) round( $total_weight ) : null;
		$heaviest_piece_weight_int = null !== $heaviest_piece_weight ? (int) round( $heaviest_piece_weight ) : null;

		return new ShipmentOrderInfo(
			$shipper,
			$pieces_counts['number_of_ex_pieces'],
			$pieces_counts['number_of_dr_pieces'],
			$total_weight_int,
			$heaviest_piece_weight_int
		);
	}

	protected function get_webapi(): WebAPI {
		return new WebAPI(
			new WebApiAuthData( $this->settings->get_webapi_login(), $this->settings->get_webapi_password() ),
			$this->settings->get_webapi_sap_number(),
			$this->settings->is_test_mode(),
			$this->logger
		);
	}

	/**
	 * @param string   $new_status .
	 * @param string[] $auto_statuses .
	 *
	 * @return bool
	 */
	private function is_target_status( string $new_status, array $auto_statuses ): bool {
		$normalized_new_status = $this->normalize_status( $new_status );

		foreach ( $auto_statuses as $status ) {
			$status = (string) $status;
			if ( $status === $new_status || $status === $normalized_new_status ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * @param string $status .
	 *
	 * @return string
	 */
	private function normalize_status( string $status ): string {
		if ( 0 === strpos( $status, 'wc-' ) ) {
			return $status;
		}

		return 'wc-' . $status;
	}
}
