<?php
/**
 * GGM Commerce Shipments Class
 *
 * Handles shipment management for orders including split shipping,
 * carrier integration, and tracking.
 *
 * Shipment statuses:
 * - pending: Shipment created but not yet dispatched
 * - label_created: Shipping label generated (carrier API)
 * - shipped: Handed to carrier / in transit
 * - in_transit: In transit (from carrier tracking)
 * - out_for_delivery: Out for delivery
 * - delivered: Successfully delivered
 * - failed: Delivery failed
 * - returned: Returned to sender
 * - cancelled: Shipment cancelled
 *
 * @package GGM_Commerce
 */

// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter

/**
 * Class GGM_Commerce_Shipments
 */
class GGM_Commerce_Shipments {

	/**
	 * Shipment status constants.
	 */
	const STATUS_PENDING          = 'pending';
	const STATUS_LABEL_CREATED    = 'label_created';
	const STATUS_SHIPPED          = 'shipped';
	const STATUS_IN_TRANSIT       = 'in_transit';
	const STATUS_OUT_FOR_DELIVERY = 'out_for_delivery';
	const STATUS_DELIVERED        = 'delivered';
	const STATUS_FAILED           = 'failed';
	const STATUS_RETURNED         = 'returned';
	const STATUS_CANCELLED        = 'cancelled';

	/**
	 * Carrier constants.
	 */
	const CARRIER_ROYAL_MAIL  = 'royal_mail';
	const CARRIER_DPD         = 'dpd';
	const CARRIER_DHL         = 'dhl';
	const CARRIER_UPS         = 'ups';
	const CARRIER_FEDEX       = 'fedex';
	const CARRIER_EVRI        = 'evri';
	const CARRIER_YODEL       = 'yodel';
	const CARRIER_PARCELFORCE = 'parcelforce';
	const CARRIER_USPS        = 'usps';
	const CARRIER_OTHER       = 'other';

	/**
	 * Get all available carriers.
	 *
	 * @return array Carrier slug => name pairs.
	 */
	public static function get_carriers() {
		return array(
			self::CARRIER_ROYAL_MAIL  => 'Royal Mail',
			self::CARRIER_DPD         => 'DPD',
			self::CARRIER_DHL         => 'DHL',
			self::CARRIER_UPS         => 'UPS',
			self::CARRIER_FEDEX       => 'FedEx',
			self::CARRIER_EVRI        => 'Evri (Hermes)',
			self::CARRIER_YODEL       => 'Yodel',
			self::CARRIER_PARCELFORCE => 'Parcelforce',
			self::CARRIER_USPS        => 'USPS',
			self::CARRIER_OTHER       => 'Other',
		);
	}

	/**
	 * Get carrier name from slug.
	 *
	 * @param string $carrier_slug The carrier slug.
	 * @return string The carrier name.
	 */
	public static function get_carrier_name( $carrier_slug ) {
		$carriers = self::get_carriers();
		return isset( $carriers[ $carrier_slug ] ) ? $carriers[ $carrier_slug ] : $carrier_slug;
	}

	/**
	 * Get all shipment statuses.
	 *
	 * @return array Status slug => name pairs.
	 */
	public static function get_statuses() {
		return array(
			self::STATUS_PENDING          => 'Pending',
			self::STATUS_LABEL_CREATED    => 'Label Created',
			self::STATUS_SHIPPED          => 'Shipped',
			self::STATUS_IN_TRANSIT       => 'In Transit',
			self::STATUS_OUT_FOR_DELIVERY => 'Out for Delivery',
			self::STATUS_DELIVERED        => 'Delivered',
			self::STATUS_FAILED           => 'Delivery Failed',
			self::STATUS_RETURNED         => 'Returned',
			self::STATUS_CANCELLED        => 'Cancelled',
		);
	}

	/**
	 * Get status label.
	 *
	 * @param string $status The status slug.
	 * @return string The status label.
	 */
	public static function get_status_label( $status ) {
		$statuses = self::get_statuses();
		return isset( $statuses[ $status ] ) ? $statuses[ $status ] : ucfirst( $status );
	}

	/**
	 * Generate a unique shipment number.
	 *
	 * @param int $order_id The order ID.
	 * @return string The shipment number.
	 */
	public static function generate_shipment_number( $order_id ) {
		global $wpdb;

		// Get existing shipment count for this order.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$count = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$wpdb->prefix}ggmc_shipments WHERE order_id = %d",
				$order_id
			)
		);

		$suffix = $count > 0 ? '-' . ( $count + 1 ) : '';

		// Get order number.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$order_number = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT order_number FROM {$wpdb->prefix}ggmc_orders WHERE id = %d",
				$order_id
			)
		);

		return 'SHP-' . ( $order_number ? $order_number : $order_id ) . $suffix;
	}

	/**
	 * Create a new shipment for an order.
	 *
	 * @param int   $order_id The order ID.
	 * @param array $data     Shipment data.
	 * @return int|false The shipment ID on success, false on failure.
	 */
	public static function create_shipment( $order_id, $data = array() ) {
		global $wpdb;

		$defaults = array(
			'carrier'            => '',
			'carrier_service'    => '',
			'tracking_number'    => '',
			'tracking_url'       => '',
			'status'             => self::STATUS_PENDING,
			'weight'             => null,
			'weight_unit'        => 'kg',
			'dimensions_length'  => null,
			'dimensions_width'   => null,
			'dimensions_height'  => null,
			'dimensions_unit'    => 'cm',
			'shipping_cost'      => 0.00,
			'insurance_amount'   => 0.00,
		);

		$data = wp_parse_args( $data, $defaults );

		$shipment_number = self::generate_shipment_number( $order_id );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$result = $wpdb->insert(
			"{$wpdb->prefix}ggmc_shipments",
			array(
				'order_id'           => $order_id,
				'shipment_number'    => $shipment_number,
				'carrier'            => $data['carrier'],
				'carrier_service'    => $data['carrier_service'],
				'tracking_number'    => $data['tracking_number'],
				'tracking_url'       => $data['tracking_url'],
				'status'             => $data['status'],
				'weight'             => $data['weight'],
				'weight_unit'        => $data['weight_unit'],
				'dimensions_length'  => $data['dimensions_length'],
				'dimensions_width'   => $data['dimensions_width'],
				'dimensions_height'  => $data['dimensions_height'],
				'dimensions_unit'    => $data['dimensions_unit'],
				'shipping_cost'      => $data['shipping_cost'],
				'insurance_amount'   => $data['insurance_amount'],
				'created_by'         => get_current_user_id(),
				'created_at'         => current_time( 'mysql' ),
			),
			array( '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%f', '%s', '%f', '%f', '%f', '%s', '%f', '%f', '%d', '%s' )
		);

		if ( $result ) {
			$shipment_id = $wpdb->insert_id;

			// Log as system note.
			if ( class_exists( 'GGM_Commerce_Order_Notes' ) ) {
				$carrier_name = self::get_carrier_name( $data['carrier'] );
				GGM_Commerce_Order_Notes::add_system_note(
					$order_id,
					sprintf( 'Shipment %s created via %s', $shipment_number, $carrier_name )
				);
			}

			return $shipment_id;
		}

		return false;
	}

	/**
	 * Get a shipment by ID.
	 *
	 * @param int $shipment_id The shipment ID.
	 * @return object|null The shipment object or null.
	 */
	public static function get_shipment( $shipment_id ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}ggmc_shipments WHERE id = %d",
				$shipment_id
			)
		);
	}

	/**
	 * Get all shipments for an order.
	 *
	 * @param int $order_id The order ID.
	 * @return array Array of shipment objects.
	 */
	public static function get_order_shipments( $order_id ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$shipments = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}ggmc_shipments WHERE order_id = %d ORDER BY created_at DESC",
				$order_id
			)
		);

		return $shipments ? $shipments : array();
	}

	/**
	 * Update a shipment.
	 *
	 * @param int   $shipment_id The shipment ID.
	 * @param array $data        Data to update.
	 * @return bool True on success, false on failure.
	 */
	public static function update_shipment( $shipment_id, $data ) {
		global $wpdb;

		$shipment = self::get_shipment( $shipment_id );
		if ( ! $shipment ) {
			return false;
		}

		$allowed_fields = array(
			'carrier', 'carrier_service', 'tracking_number', 'tracking_url',
			'status', 'label_url', 'label_data', 'weight', 'weight_unit',
			'dimensions_length', 'dimensions_width', 'dimensions_height', 'dimensions_unit',
			'shipping_cost', 'insurance_amount', 'carrier_response',
			'shipped_at', 'estimated_delivery', 'delivered_at',
		);

		$update_data = array();
		$format      = array();

		foreach ( $data as $key => $value ) {
			if ( in_array( $key, $allowed_fields, true ) ) {
				$update_data[ $key ] = $value;

				// Determine format.
				if ( in_array( $key, array( 'weight', 'dimensions_length', 'dimensions_width', 'dimensions_height', 'shipping_cost', 'insurance_amount' ), true ) ) {
					$format[] = '%f';
				} elseif ( in_array( $key, array( 'shipped_at', 'estimated_delivery', 'delivered_at' ), true ) ) {
					$format[] = '%s';
				} else {
					$format[] = '%s';
				}
			}
		}

		if ( empty( $update_data ) ) {
			return false;
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$result = $wpdb->update(
			"{$wpdb->prefix}ggmc_shipments",
			$update_data,
			array( 'id' => $shipment_id ),
			$format,
			array( '%d' )
		);

		// Log status changes and fire hooks for email notifications.
		if ( isset( $data['status'] ) && $data['status'] !== $shipment->status ) {
			if ( class_exists( 'GGM_Commerce_Order_Notes' ) ) {
				GGM_Commerce_Order_Notes::add_system_note(
					$shipment->order_id,
					sprintf(
						'Shipment %s status: %s → %s',
						$shipment->shipment_number,
						self::get_status_label( $shipment->status ),
						self::get_status_label( $data['status'] )
					)
				);
			}

			// Fire hooks for email notifications.
			do_action( 'ggmc_shipment_status_changed', $shipment_id, $data['status'], $shipment->status );
			do_action( 'ggmc_shipment_status_' . $data['status'], $shipment_id, $shipment->order_id );
		}

		return $result !== false;
	}

	/**
	 * Mark a shipment as shipped.
	 *
	 * @param int    $shipment_id     The shipment ID.
	 * @param string $tracking_number Optional tracking number.
	 * @param string $tracking_url    Optional tracking URL.
	 * @return bool True on success, false on failure.
	 */
	public static function mark_shipped( $shipment_id, $tracking_number = '', $tracking_url = '' ) {
		$data = array(
			'status'     => self::STATUS_SHIPPED,
			'shipped_at' => current_time( 'mysql' ),
		);

		if ( ! empty( $tracking_number ) ) {
			$data['tracking_number'] = $tracking_number;
		}

		if ( ! empty( $tracking_url ) ) {
			$data['tracking_url'] = $tracking_url;
		}

		return self::update_shipment( $shipment_id, $data );
	}

	/**
	 * Mark a shipment as delivered.
	 *
	 * @param int $shipment_id The shipment ID.
	 * @return bool True on success, false on failure.
	 */
	public static function mark_delivered( $shipment_id ) {
		return self::update_shipment( $shipment_id, array(
			'status'       => self::STATUS_DELIVERED,
			'delivered_at' => current_time( 'mysql' ),
		) );
	}

	/**
	 * Delete a shipment.
	 *
	 * @param int $shipment_id The shipment ID.
	 * @return bool True on success, false on failure.
	 */
	public static function delete_shipment( $shipment_id ) {
		global $wpdb;

		$shipment = self::get_shipment( $shipment_id );
		if ( ! $shipment ) {
			return false;
		}

		// Delete shipment items first.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->delete(
			"{$wpdb->prefix}ggmc_shipment_items",
			array( 'shipment_id' => $shipment_id ),
			array( '%d' )
		);

		// Delete shipment events.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->delete(
			"{$wpdb->prefix}ggmc_shipment_events",
			array( 'shipment_id' => $shipment_id ),
			array( '%d' )
		);

		// Delete shipment.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$result = $wpdb->delete(
			"{$wpdb->prefix}ggmc_shipments",
			array( 'id' => $shipment_id ),
			array( '%d' )
		);

		if ( $result && class_exists( 'GGM_Commerce_Order_Notes' ) ) {
			GGM_Commerce_Order_Notes::add_system_note(
				$shipment->order_id,
				sprintf( 'Shipment %s deleted', $shipment->shipment_number )
			);
		}

		return $result !== false;
	}

	/**
	 * Add items to a shipment.
	 *
	 * @param int   $shipment_id The shipment ID.
	 * @param array $items       Array of [ 'order_item_id' => quantity ] pairs.
	 * @return bool True on success, false on failure.
	 */
	public static function add_shipment_items( $shipment_id, $items ) {
		global $wpdb;

		$success = true;

		foreach ( $items as $order_item_id => $quantity ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
			$result = $wpdb->replace(
				"{$wpdb->prefix}ggmc_shipment_items",
				array(
					'shipment_id'   => $shipment_id,
					'order_item_id' => $order_item_id,
					'quantity'      => $quantity,
				),
				array( '%d', '%d', '%d' )
			);

			if ( ! $result ) {
				$success = false;
			}
		}

		return $success;
	}

	/**
	 * Get items in a shipment.
	 *
	 * @param int $shipment_id The shipment ID.
	 * @return array Array of shipment item objects with order item details.
	 */
	public static function get_shipment_items( $shipment_id ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$items = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT si.*, oi.product_id, oi.product_name, oi.product_sku, oi.quantity as order_quantity, oi.price
				FROM {$wpdb->prefix}ggmc_shipment_items si
				JOIN {$wpdb->prefix}ggmc_order_items oi ON si.order_item_id = oi.id
				WHERE si.shipment_id = %d",
				$shipment_id
			)
		);

		return $items ? $items : array();
	}

	/**
	 * Get unshipped items for an order.
	 *
	 * @param int $order_id The order ID.
	 * @return array Array of order items with remaining unshipped quantities.
	 */
	public static function get_unshipped_items( $order_id ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$items = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT oi.*,
					COALESCE(SUM(si.quantity), 0) as shipped_quantity,
					(oi.quantity - COALESCE(SUM(si.quantity), 0)) as remaining_quantity
				FROM {$wpdb->prefix}ggmc_order_items oi
				LEFT JOIN {$wpdb->prefix}ggmc_shipment_items si ON oi.id = si.order_item_id
				WHERE oi.order_id = %d
				GROUP BY oi.id
				HAVING remaining_quantity > 0",
				$order_id
			)
		);

		return $items ? $items : array();
	}

	/**
	 * Check if an order is fully shipped.
	 *
	 * @param int $order_id The order ID.
	 * @return bool True if fully shipped, false otherwise.
	 */
	public static function is_order_fully_shipped( $order_id ) {
		$unshipped = self::get_unshipped_items( $order_id );
		return empty( $unshipped );
	}

	/**
	 * Add a tracking event to a shipment.
	 *
	 * @param int    $shipment_id       The shipment ID.
	 * @param string $event_description The event description.
	 * @param array  $data              Additional event data.
	 * @return int|false The event ID on success, false on failure.
	 */
	public static function add_tracking_event( $shipment_id, $event_description, $data = array() ) {
		global $wpdb;

		$defaults = array(
			'event_code' => '',
			'location'   => '',
			'event_time' => current_time( 'mysql' ),
			'raw_data'   => null,
		);

		$data = wp_parse_args( $data, $defaults );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$result = $wpdb->insert(
			"{$wpdb->prefix}ggmc_shipment_events",
			array(
				'shipment_id'       => $shipment_id,
				'event_code'        => $data['event_code'],
				'event_description' => $event_description,
				'location'          => $data['location'],
				'event_time'        => $data['event_time'],
				'raw_data'          => $data['raw_data'] ? wp_json_encode( $data['raw_data'] ) : null,
			),
			array( '%d', '%s', '%s', '%s', '%s', '%s' )
		);

		return $result ? $wpdb->insert_id : false;
	}

	/**
	 * Get tracking events for a shipment.
	 *
	 * @param int $shipment_id The shipment ID.
	 * @return array Array of event objects.
	 */
	public static function get_tracking_events( $shipment_id ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$events = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}ggmc_shipment_events
				WHERE shipment_id = %d
				ORDER BY event_time DESC",
				$shipment_id
			)
		);

		return $events ? $events : array();
	}

	/**
	 * Get status badge HTML.
	 *
	 * @param string $status The status slug.
	 * @return string HTML badge.
	 */
	public static function get_status_badge( $status ) {
		$colors = array(
			self::STATUS_PENDING          => array( 'bg' => '#f0f0f1', 'color' => '#50575e' ),
			self::STATUS_LABEL_CREATED    => array( 'bg' => '#e7f3ff', 'color' => '#0073aa' ),
			self::STATUS_SHIPPED          => array( 'bg' => '#fff8e5', 'color' => '#996800' ),
			self::STATUS_IN_TRANSIT       => array( 'bg' => '#e0f0ff', 'color' => '#0066cc' ),
			self::STATUS_OUT_FOR_DELIVERY => array( 'bg' => '#fef3cd', 'color' => '#856404' ),
			self::STATUS_DELIVERED        => array( 'bg' => '#d4edda', 'color' => '#155724' ),
			self::STATUS_FAILED           => array( 'bg' => '#f8d7da', 'color' => '#721c24' ),
			self::STATUS_RETURNED         => array( 'bg' => '#fff3cd', 'color' => '#856404' ),
			self::STATUS_CANCELLED        => array( 'bg' => '#f0f0f1', 'color' => '#50575e' ),
		);

		$style = isset( $colors[ $status ] ) ? $colors[ $status ] : $colors[ self::STATUS_PENDING ];
		$label = self::get_status_label( $status );

		return sprintf(
			'<span style="display: inline-block; padding: 3px 8px; border-radius: 3px; font-size: 11px; font-weight: 500; background: %s; color: %s;">%s</span>',
			esc_attr( $style['bg'] ),
			esc_attr( $style['color'] ),
			esc_html( $label )
		);
	}

	/**
	 * Migrate legacy shipping data from orders table to shipments table.
	 *
	 * @return int Number of orders migrated.
	 */
	public static function migrate_legacy_shipping() {
		global $wpdb;

		// Find orders with shipping data in the old columns.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$orders = $wpdb->get_results(
			"SELECT id, order_number, shipping_carrier, shipping_tracking_number, shipping_tracking_url, shipped_at, delivered_at
			FROM {$wpdb->prefix}ggmc_orders
			WHERE shipping_carrier IS NOT NULL AND shipping_carrier != ''"
		);

		if ( empty( $orders ) ) {
			return 0;
		}

		$migrated = 0;

		foreach ( $orders as $order ) {
			// Check if already migrated.
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$existing = $wpdb->get_var(
				$wpdb->prepare(
					"SELECT COUNT(*) FROM {$wpdb->prefix}ggmc_shipments WHERE order_id = %d",
					$order->id
				)
			);

			if ( $existing > 0 ) {
				continue;
			}

			// Create shipment from legacy data.
			$status = self::STATUS_PENDING;
			if ( ! empty( $order->delivered_at ) ) {
				$status = self::STATUS_DELIVERED;
			} elseif ( ! empty( $order->shipped_at ) ) {
				$status = self::STATUS_SHIPPED;
			}

			$shipment_number = 'SHP-' . ( $order->order_number ? $order->order_number : $order->id );

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
			$result = $wpdb->insert(
				"{$wpdb->prefix}ggmc_shipments",
				array(
					'order_id'        => $order->id,
					'shipment_number' => $shipment_number,
					'carrier'         => $order->shipping_carrier,
					'tracking_number' => $order->shipping_tracking_number,
					'tracking_url'    => $order->shipping_tracking_url,
					'status'          => $status,
					'shipped_at'      => $order->shipped_at,
					'delivered_at'    => $order->delivered_at,
					'created_at'      => current_time( 'mysql' ),
				),
				array( '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
			);

			if ( $result ) {
				$migrated++;
			}
		}

		return $migrated;
	}
}

/**
 * Helper function to get the shipments instance.
 *
 * @return GGM_Commerce_Shipments
 */
function ggm_commerce_shipments() {
	return new GGM_Commerce_Shipments();
}
