<?php
/**
 * WooCommerce Shipping Fulfillment class.
 *
 * Extends the WooCommerce Fulfillment class to add shipping-specific functionality.
 *
 * @package WCShipping\Fulfillments
 * @since   1.9.0
 */

declare( strict_types=1 );

namespace Automattic\WCShipping\Fulfillments;

use Automattic\WCShipping\Utils;
use Automattic\WCShipping\Fulfillments\FulfillmentNotificationType;

defined( 'ABSPATH' ) || exit;

// Only extend the parent class if it exists (requires WooCommerce 10.1+)
if ( class_exists( '\Automattic\WooCommerce\Internal\Fulfillments\Fulfillment' ) ) {
	/**
	 * Shipping Fulfillment Class
	 *
	 * Extends the core Fulfillment class to provide shipping-specific functionality
	 * such as tracking information, carrier details, and label management.
	 *
	 * @since 1.9.0
	 */
	class ShippingFulfillment extends \Automattic\WooCommerce\Internal\Fulfillments\Fulfillment {

		/**
		 * The type of notification to send to the customer.
		 *
		 * @var string One of FulfillmentNotificationType constants, or FulfillmentNotificationType::NONE for no notification.
		 */
		private $notify_customer = FulfillmentNotificationType::NONE;

		/**
		 * Get the tracking number for this shipping fulfillment.
		 * Returns the tracking number from the purchased label, or null if no purchased label exists.
		 *
		 * @return string|null The tracking number or null if not set.
		 */
		public function get_tracking_number(): ?string {
			$purchased_label = $this->get_purchased_label();

			if ( $purchased_label && isset( $purchased_label['tracking'] ) ) {
				return $purchased_label['tracking'];
			}

			return null;
		}

		/**
		 * Get the carrier name for this shipping fulfillment.
		 * Returns the carrier name from the purchased label.
		 *
		 * @return string|null The carrier name or null if not set.
		 */
		public function get_carrier_name(): ?string {
			$purchased_label = $this->get_purchased_label();

			if ( $purchased_label && isset( $purchased_label['carrier_name'] ) ) {
				return $purchased_label['carrier_name'];
			}

			return null;
		}

		/**
		 * Get the carrier ID for this shipping fulfillment.
		 * Returns the carrier ID from the purchased label.
		 *
		 * @return string|null The carrier ID or null if not set.
		 */
		public function get_carrier_id(): ?string {
			$purchased_label = $this->get_purchased_label();

			if ( $purchased_label && isset( $purchased_label['carrier_id'] ) ) {
				return $purchased_label['carrier_id'];
			}

			return null;
		}

		/**
		 * Get the shipping service name for this fulfillment.
		 * Returns the service name from the purchased label.
		 *
		 * @return string|null The shipping service name or null if not set.
		 */
		public function get_service_name(): ?string {
			$purchased_label = $this->get_purchased_label();

			if ( $purchased_label && isset( $purchased_label['service_name'] ) ) {
				return $purchased_label['service_name'];
			}

			return null;
		}

		/**
		 * Get the shipping service ID for this fulfillment.
		 * Returns the service ID from the purchased label.
		 *
		 * @return string|null The shipping service ID or null if not set.
		 */
		public function get_service_id(): ?string {
			$purchased_label = $this->get_purchased_label();

			if ( $purchased_label && isset( $purchased_label['service_id'] ) ) {
				return $purchased_label['service_id'];
			}

			return null;
		}

		/**
		 * Get the shipping labels for this fulfillment.
		 *
		 * @return array The labels array or empty array if not set.
		 */
		public function get_labels(): array {
			$labels = $this->get_meta( '_shipping_labels' );
			return is_array( $labels ) ? array_map(
				function ( $label ) {
					return array_merge( $label, array( 'fulfillment_id' => $this->get_id() ) );
				},
				$labels
			) : array();
		}

		/**
		 * Set the shipping labels for this fulfillment.
		 * This method appends new labels to existing ones to preserve refunded labels.
		 *
		 * @param array $labels The labels array to add.
		 * @return void
		 */
		public function set_labels( array $labels ): void {
			$existing_labels = $this->get_labels();
			$all_labels      = array_merge( $existing_labels, $labels );
			$this->replace_all_labels( $all_labels );
		}

		/**
		 * Replace all shipping labels for this fulfillment.
		 * Use this method when you want to completely overwrite existing labels.
		 *
		 * @param array $labels The labels array to set (replaces all existing labels).
		 * @return void
		 */
		public function replace_all_labels( array $labels ): void {
			// Clean up existing individual label_id entries
			$this->cleanup_individual_label_entries();

			// Store the main labels array
			$this->update_meta_data( '_shipping_labels', $labels );

			// Store individual label_id entries for fast lookup and set tracking metadata
			foreach ( $labels as $label ) {
				if ( isset( $label['label_id'] ) ) {
					$this->add_meta_data( '_shipping_label_id', $label['label_id'] );

					// Set tracking metadata for purchased labels
					if ( isset( $label['status'] ) && 'PURCHASED' === $label['status'] ) {
						$this->set_tracking_metadata( $label );
					}
				}
			}
		}

		/**
		 * Add a single label to the fulfillment.
		 *
		 * @param array $label The label data to add.
		 * @return void
		 */
		public function add_label( array $label ): void {
			$this->set_labels( array( $label ) );
		}

		/**
		 * Remove a label by its ID.
		 *
		 * @param string $label_id The ID of the label to remove.
		 * @return bool True if label was removed, false if not found.
		 */
		public function remove_label( string $label_id ): bool {
			$labels         = $this->get_labels();
			$original_count = count( $labels );

			$labels = array_filter(
				$labels,
				function ( $label ) use ( $label_id ) {
					return ! isset( $label['label_id'] ) || $label['label_id'] != $label_id;
				}
			);

			if ( count( $labels ) !== $original_count ) {
				// Update the main labels array (this will also sync all individual entries)
				$this->replace_all_labels( array_values( $labels ) ); // Re-index array
				return true;
			}

			return false;
		}

		/**
		 * Get a specific label by its ID.
		 *
		 * @param string $label_id The ID of the label to retrieve.
		 * @return array|null The label data or null if not found.
		 */
		public function get_label_by_id( string $label_id ): ?array {
			$labels = $this->get_labels();

			foreach ( $labels as $label ) {
				if ( isset( $label['label_id'] ) && $label['label_id'] == $label_id ) {
					return $label;
				}
			}

			return null;
		}

		/**
		 * Get the purchased label for this fulfillment.
		 *
		 * @return array|null The purchased label data or null if not found.
		 */
		public function get_purchased_label(): ?array {
			$labels = $this->get_labels();

			foreach ( $labels as $label ) {
				if ( isset( $label['status'] ) && 'PURCHASED' === $label['status'] ) {
					return $label;
				}
			}

			return null;
		}

		/**
		 * Check if this fulfillment has shipping labels.
		 *
		 * @return bool True if labels exist, false otherwise.
		 */
		public function has_label(): bool {
			$labels = $this->get_labels();
			return ! empty( $labels );
		}

		/**
		 * Get the count of labels for this fulfillment.
		 *
		 * @return int The number of labels.
		 */
		public function get_label_count(): int {
			return count( $this->get_labels() );
		}

		/**
		 * Check if this fulfillment has tracking information.
		 * Returns true if there's a purchased label with tracking information.
		 *
		 * @return bool True if tracking information exists, false otherwise.
		 */
		public function has_tracking(): bool {
			return ! empty( $this->get_tracking_number() );
		}

		/**
		 * Get the shipping label rate information.
		 *
		 * @return array|null The shipping rate data or null if not set.
		 */
		public function get_shipping_label_rate(): ?array {
			$rate = $this->get_meta( '_shipping_label_rate' );
			return is_array( $rate ) ? $rate : null;
		}

		/**
		 * Set the shipping label rate information.
		 *
		 * @param array|null $rate The shipping rate data.
		 * @return void
		 */
		public function set_shipping_label_rate( ?array $rate ): void {
			if ( null === $rate ) {
				$this->delete_meta_data( '_shipping_label_rate' );
			} else {
				$this->update_meta_data( '_shipping_label_rate', $rate );
			}
		}

		/**
		 * Get the hazmat configuration for this fulfillment.
		 *
		 * @return array|null The hazmat configuration or null if not set.
		 */
		public function get_shipping_label_hazmat(): ?array {
			$hazmat = $this->get_meta( '_shipping_label_hazmat' );
			return is_array( $hazmat ) ? $hazmat : null;
		}

		/**
		 * Set the hazmat configuration for this fulfillment.
		 *
		 * @param array|null $hazmat The hazmat configuration.
		 * @return void
		 */
		public function set_shipping_label_hazmat( ?array $hazmat ): void {
			if ( null === $hazmat ) {
				$this->delete_meta_data( '_shipping_label_hazmat' );
			} else {
				$this->update_meta_data( '_shipping_label_hazmat', $hazmat );
			}
		}

		/**
		 * Get the selected origin address for this fulfillment.
		 *
		 * @return array|null The origin address or null if not set.
		 */
		public function get_selected_origin(): ?array {
			$origin = $this->get_meta( '_shipping_label_origin' );
			return is_array( $origin ) ? $origin : null;
		}

		/**
		 * Set the selected origin address for this fulfillment.
		 *
		 * @param array|null $origin The origin address.
		 * @return void
		 */
		public function set_selected_origin( ?array $origin ): void {
			if ( null === $origin ) {
				$this->delete_meta_data( '_shipping_label_origin' );
			} else {
				$this->update_meta_data( '_shipping_label_origin', $origin );
			}
		}

		/**
		 * Get the shipping label destination address.
		 *
		 * @return array|null The destination address or null if not set.
		 */
		public function get_shipping_label_destination(): ?array {
			$destination = $this->get_meta( '_shipping_label_destination' );
			return is_array( $destination ) ? $destination : null;
		}

		/**
		 * Set the shipping label destination address.
		 *
		 * @param array|null $destination The destination address.
		 * @return void
		 */
		public function set_shipping_label_destination( ?array $destination ): void {
			if ( null === $destination ) {
				$this->delete_meta_data( '_shipping_label_destination' );
			} else {
				$this->update_meta_data( '_shipping_label_destination', $destination );
			}
		}

		/**
		 * Get the shipping label customs information.
		 *
		 * @return array|null The customs information or null if not set.
		 */
		public function get_shipping_label_customs(): ?array {
			$customs = $this->get_meta( '_shipping_label_customs' );
			return is_array( $customs ) ? $customs : null;
		}

		/**
		 * Set the shipping label customs information.
		 *
		 * @param array|null $customs The customs information.
		 * @return void
		 */
		public function set_shipping_label_customs( ?array $customs ): void {
			if ( null === $customs ) {
				$this->delete_meta_data( '_shipping_label_customs' );
			} else {
				$this->update_meta_data( '_shipping_label_customs', $customs );
			}
		}

		/**
		 * Get the shipping label dates information.
		 *
		 * @return array|null The shipment dates or null if not set.
		 */
		public function get_shipping_label_dates(): ?array {
			$dates = $this->get_meta( '_shipping_label_dates' );
			return is_array( $dates ) ? $dates : null;
		}

		/**
		 * Set the shipping label dates information.
		 *
		 * @param array|null $dates The shipment dates.
		 * @return void
		 */
		public function set_shipping_label_dates( ?array $dates ): void {
			if ( null === $dates ) {
				$this->delete_meta_data( '_shipping_label_dates' );
			} else {
				$this->update_meta_data( '_shipping_label_dates', $dates );
			}
		}

		/**
		 * Update a specific label by its ID with new data.
		 *
		 * @param string $label_id The ID of the label to update.
		 * @param array  $data The data to update for the label.
		 * @return bool True if label was found and updated, false otherwise.
		 */
		public function update_label( string $label_id, array $data ): bool {
			$labels  = $this->get_labels();
			$updated = false;

			foreach ( $labels as &$label ) {
				if ( isset( $label['label_id'] ) && $label['label_id'] == $label_id ) {
					$label   = array_merge( $label, $data );
					$updated = true;
					break;
				}
			}

			if ( $updated ) {
				$this->replace_all_labels( $labels );
			}

			return $updated;
		}

		/**
		 * Set tracking metadata for a label.
		 * This includes tracking number, tracking URL, and shipment provider.
		 *
		 * @param array $label_data The label data containing tracking information.
		 * @return void
		 */
		public function set_tracking_metadata( array $label_data ): void {
			// Set tracking number if available
			if ( isset( $label_data['tracking'] ) ) {
				$this->add_meta_data( '_tracking_number', $label_data['tracking'] );

				// Calculate and set tracking URL if we have carrier_id
				if ( isset( $label_data['carrier_id'] ) && ! empty( $label_data['carrier_id'] ) ) {
					$full_tracking_url = Utils::get_tracking_url( $label_data['carrier_id'], $label_data['tracking'] );
					if ( ! empty( $full_tracking_url ) ) {
						$this->add_meta_data( '_tracking_url', $full_tracking_url );
					}
				}
			}

			$shipment_provider = '';
			if ( isset( $label_data['carrier_id'] ) && ! empty( $label_data['carrier_id'] ) ) {
				$shipment_provider = $label_data['carrier_id'] === 'upsdap' ? 'ups' : $label_data['carrier_id'];
			}

			if ( ! empty( $shipment_provider ) ) {
				$this->add_meta_data( '_shipment_provider', $shipment_provider );
			}
		}

		/**
		 * Check if a label's status will change to a specific target status.
		 *
		 * @param string $label_id The ID of the label to check.
		 * @param array  $new_label_data The new label data being applied.
		 * @param string $target_status The status to check for (default: 'PURCHASED').
		 * @return bool True if status will change to the target status, false otherwise.
		 */
		public function will_status_change( string $label_id, array $new_label_data, string $target_status = 'PURCHASED' ): bool {
			$old_label  = $this->get_label_by_id( $label_id );
			$old_status = isset( $old_label['status'] ) ? $old_label['status'] : '';
			$new_status = isset( $new_label_data['status'] ) ? $new_label_data['status'] : '';

			// Check if status is transitioning TO the target status
			return $old_status !== $target_status && $new_status === $target_status;
		}

		/**
		 * Clean up individual label_id meta entries.
		 * Removes all meta entries with '_shipping_label_id' key.
		 *
		 * @return void
		 */
		private function cleanup_individual_label_entries(): void {
			$this->delete_meta_data( '_shipping_label_id' );
		}

		/**
		 * Get the notification type to send to the customer.
		 *
		 * @return string One of FulfillmentNotificationType constants, or FulfillmentNotificationType::NONE for no notification.
		 */
		public function get_notify_customer(): string {
			return $this->notify_customer ?? FulfillmentNotificationType::NONE;
		}

		/**
		 * Set the notification type to send to the customer.
		 *
		 * @param string|null $notification_type One of FulfillmentNotificationType constants, or null for no notification (converted to NONE).
		 * @return void
		 * @throws \InvalidArgumentException If the notification type is invalid.
		 */
		public function set_notify_customer( ?string $notification_type ): void {
			if ( null !== $notification_type && ! FulfillmentNotificationType::is_valid( $notification_type ) ) {
				throw new \InvalidArgumentException(
					sprintf(
						'Invalid notification type: %s. Must be one of: %s',
						esc_html( $notification_type ),
						esc_html( implode( ', ', FulfillmentNotificationType::get_all() ) )
					)
				);
			}
			$this->notify_customer = $notification_type ?? FulfillmentNotificationType::NONE;
		}

		/**
		 * Save the fulfillment and trigger notifications if needed.
		 *
		 * @return int The ID of the saved fulfillment.
		 */
		public function save() {
			$result          = parent::save();
			$notify_customer = $this->get_notify_customer();

			if ( $this->get_is_fulfilled() && FulfillmentNotificationType::NONE !== $notify_customer ) {
				$order_id = (int) $this->get_entity_id();
				$order    = wc_get_order( $order_id );

				if ( FulfillmentNotificationType::CREATED === $notify_customer ) {
					/**
					 * Trigger the fulfillment created notification.
					 */
					do_action( 'woocommerce_fulfillment_created_notification', $order_id, $this, $order );
				} elseif ( FulfillmentNotificationType::UPDATED === $notify_customer ) {
					/**
					 * Trigger the fulfillment updated notification.
					 */
					do_action( 'woocommerce_fulfillment_updated_notification', $order_id, $this, $order );
				}

				// This prevents consecutive notifications. It leaves the control to the invoking code.
				$this->set_notify_customer( FulfillmentNotificationType::NONE );
			}

			return $result;
		}

		/**
		 * Delete the fulfillment and trigger notifications if needed.
		 *
		 * @param bool $force_delete Whether to force delete (bypass trash).
		 * @return bool Result of the delete operation.
		 */
		public function delete( $force_delete = false ) {
			$notify_customer = $this->get_notify_customer();
			$order_id        = (int) $this->get_entity_id();
			$is_fulfilled    = $this->get_is_fulfilled();

			$result = parent::delete( $force_delete );

			if ( $result && $is_fulfilled && FulfillmentNotificationType::DELETED === $notify_customer ) {
				/**
				 * Trigger the fulfillment deleted notification on deleting a fulfilled fulfillment.
				 */
				do_action( 'woocommerce_fulfillment_deleted_notification', $order_id, $this, wc_get_order( $order_id ) );
			}

			return $result;
		}

		/**
		 * Get shipping-specific data as an array.
		 *
		 * @return array The shipping data.
		 */
		public function get_shipping_data(): array {
			return array(
				'labels'               => $this->get_labels(),
				'selected_rates'       => $this->get_shipping_label_rate(),
				'selected_hazmat'      => $this->get_shipping_label_hazmat(),
				'selected_origin'      => $this->get_selected_origin(),
				'selected_destination' => $this->get_shipping_label_destination(),
				'customs_information'  => $this->get_shipping_label_customs(),
				'shipment_dates'       => $this->get_shipping_label_dates(),
			);
		}
	}
} else {
	/**
	 * Fallback Shipping Fulfillment Class
	 *
	 * Provides a class placeholder when the parent Fulfillment class
	 * is not available (WooCommerce < 10.1.0).
	 *
	 * @since 1.9.0
	 */
	class ShippingFulfillment extends \WC_Data {
	}
}
