<?php
/**
 * Teleship Order Sync
 *
 * Handles synchronization of WooCommerce orders to Teleship API.
 * Orders are pushed to Teleship when they enter "processing" or "on-hold" status.
 * Mirrors functionality from Magento's OrderSync with mapping table, retry,
 * loop prevention, and international order filtering.
 *
 * Status handling matches the backend woocommerce.service.ts:
 * - processing/on-hold: Sync as UNFULFILLED
 * - completed: Update existing to FULFILLED (don't create new, don't delete)
 * - cancelled: Delete from Teleship (void shipments first if fulfilled)
 * - refunded: Do nothing (leave order as-is in Teleship)
 * - failed: Delete from Teleship
 * - pending/draft/checkout-draft: Skip (not valid placed orders)
 *
 * @package Teleship
 * @since 2.0.0
 */

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

/**
 * Class Teleship_Order_Sync
 *
 * Syncs WooCommerce orders to Teleship
 */
class Teleship_Order_Sync {

    /**
     * Order statuses that trigger sync to Teleship
     * Matches backend WOOCOMMERCE_STATUS_MAP where value is UNFULFILLED
     */
    const SYNC_STATUSES = array('processing', 'on-hold');

    /**
     * Order statuses that should remove (delete) order from Teleship
     * Shipments are voided first if the order was fulfilled
     */
    const REMOVE_STATUSES = array('cancelled', 'failed');

    /**
     * Order statuses that update existing orders to FULFILLED
     * Completed orders are only updated if they already exist, never created
     */
    const COMPLETED_STATUSES = array('completed');

    /**
     * Meta key for tracking Teleship order ID
     */
    const META_TELESHIP_ORDER_ID = '_teleship_order_id';

    /**
     * Meta key for tracking if order was fulfilled by Teleship
     */
    const META_TELESHIP_FULFILLED = '_teleship_fulfilled';

    /**
     * Meta key for tracking number
     */
    const META_TRACKING_NUMBER = '_teleship_tracking_number';

    /**
     * Meta key for tracking URL
     */
    const META_TRACKING_URL = '_teleship_tracking_url';

    /**
     * Meta key for carrier (consistent with webhook receiver)
     */
    const META_CARRIER = '_teleship_tracking_carrier';

    /**
     * Meta key for shipment ID
     */
    const META_SHIPMENT_ID = '_teleship_shipment_id';

    /**
     * Meta key for webhook processing flag (loop prevention)
     */
    const META_WEBHOOK_PROCESSING = '_teleship_webhook_processing';

    /**
     * Mapping table name (without prefix)
     */
    const MAPPING_TABLE = 'teleship_order_mapping';

    /**
     * Sync status constants
     */
    const SYNC_STATUS_PENDING  = 'pending';
    const SYNC_STATUS_SYNCED   = 'synced';
    const SYNC_STATUS_FAILED   = 'failed';
    const SYNC_STATUS_REMOVED  = 'removed';

    /**
     * Singleton instance
     *
     * @var Teleship_Order_Sync
     */
    private static $instance = null;

    /**
     * Pending orders to sync (batched on shutdown)
     *
     * @var array
     */
    private $pending_orders = array();

    /**
     * Get singleton instance
     *
     * @return Teleship_Order_Sync
     */
    public static function instance() {
        if (null === self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Constructor - registers hooks
     */
    private function __construct() {
        // Only initialize if connected to Teleship
        if (!teleship_oauth()->is_connected()) {
            return;
        }

        // Hook into order status changes
        add_action('woocommerce_order_status_changed', array($this, 'on_order_status_changed'), 10, 4);

        // Hook into new order creation
        add_action('woocommerce_new_order', array($this, 'on_new_order'), 10, 2);

        // Hook for processing status specifically
        add_action('woocommerce_order_status_processing', array($this, 'on_order_processing'), 10, 2);

        // Batch processing on shutdown
        add_action('shutdown', array($this, 'process_pending_orders'));

        teleship_log('[Order Sync] Hooks registered');
    }

    /**
     * Initialize the order sync
     *
     * @return void
     */
    public static function init() {
        self::instance();
    }

    /**
     * Create the mapping table
     *
     * Called on plugin activation.
     */
    public static function create_mapping_table() {
        global $wpdb;

        $table_name = $wpdb->prefix . self::MAPPING_TABLE;
        $charset_collate = $wpdb->get_charset_collate();

        $sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
            mapping_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            wc_order_id bigint(20) unsigned NOT NULL,
            wc_order_number varchar(100) NOT NULL DEFAULT '',
            teleship_order_uuid varchar(100) DEFAULT NULL,
            sync_status varchar(20) NOT NULL DEFAULT 'pending',
            sync_attempts int(11) NOT NULL DEFAULT 0,
            error_message text DEFAULT NULL,
            last_synced_at datetime DEFAULT NULL,
            created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (mapping_id),
            UNIQUE KEY idx_wc_order_id (wc_order_id),
            KEY idx_sync_status (sync_status),
            KEY idx_teleship_uuid (teleship_order_uuid)
        ) {$charset_collate};";

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta($sql);

        teleship_log('[Order Sync] Mapping table created/verified');
    }

    /**
     * Drop the mapping table
     *
     * Called on plugin uninstall.
     */
    public static function drop_mapping_table() {
        global $wpdb;
        $table_name = $wpdb->prefix . self::MAPPING_TABLE;
        $wpdb->query("DROP TABLE IF EXISTS {$table_name}"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- Uninstall cleanup of custom table
    }

    /**
     * Get or create a mapping entry for an order
     *
     * @param WC_Order $order Order object.
     * @return object|null Mapping row or null on failure.
     */
    private function get_or_create_mapping($order) {
        global $wpdb;
        $table_name = $wpdb->prefix . self::MAPPING_TABLE;

        $order_id = $order->get_id();

        // Try to find existing
        $mapping = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom mapping table lookup
            $wpdb->prepare("SELECT * FROM {$table_name} WHERE wc_order_id = %d", $order_id) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        );

        if ($mapping) {
            return $mapping;
        }

        // Create new
        $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom mapping table insert
            $table_name,
            array(
                'wc_order_id'     => $order_id,
                'wc_order_number' => $order->get_order_number(),
                'sync_status'     => self::SYNC_STATUS_PENDING,
                'sync_attempts'   => 0,
                'created_at'      => current_time('mysql'),
            ),
            array('%d', '%s', '%s', '%d', '%s')
        );

        return $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Re-read after insert
            $wpdb->prepare("SELECT * FROM {$table_name} WHERE wc_order_id = %d", $order_id) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        );
    }

    /**
     * Update a mapping entry
     *
     * @param int   $order_id WooCommerce order ID.
     * @param array $data     Data to update.
     */
    private function update_mapping($order_id, $data) {
        global $wpdb;
        $table_name = $wpdb->prefix . self::MAPPING_TABLE;

        $data['last_synced_at'] = current_time('mysql');

        $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom mapping table update
            $table_name,
            $data,
            array('wc_order_id' => $order_id),
            null,
            array('%d')
        );
    }

    /**
     * Get mapping by WooCommerce order ID
     *
     * @param int $order_id WooCommerce order ID.
     * @return object|null Mapping row.
     */
    public function get_mapping_by_order_id($order_id) {
        global $wpdb;
        $table_name = $wpdb->prefix . self::MAPPING_TABLE;

        return $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom mapping table lookup
            $wpdb->prepare("SELECT * FROM {$table_name} WHERE wc_order_id = %d", $order_id) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        );
    }

    /**
     * Handle new order creation
     *
     * @param int      $order_id Order ID.
     * @param WC_Order $order    Order object.
     */
    public function on_new_order($order_id, $order = null) {
        if (!$order) {
            $order = wc_get_order($order_id);
        }

        if (!$order) {
            return;
        }

        // Check if order status qualifies for sync
        $status = $order->get_status();
        if (in_array($status, self::SYNC_STATUSES, true)) {
            // Check international-only guard
            if (!$this->should_sync_order($order)) {
                return;
            }
            $this->queue_order_for_sync($order);
        }
    }

    /**
     * Handle order status processing
     *
     * @param int      $order_id Order ID.
     * @param WC_Order $order    Order object.
     */
    public function on_order_processing($order_id, $order = null) {
        if (!$order) {
            $order = wc_get_order($order_id);
        }

        if ($order && $this->should_sync_order($order)) {
            $this->queue_order_for_sync($order);
        }
    }

    /**
     * Handle order status change
     *
     * @param int      $order_id   Order ID.
     * @param string   $old_status Previous status.
     * @param string   $new_status New status.
     * @param WC_Order $order      Order object.
     */
    public function on_order_status_changed($order_id, $old_status, $new_status, $order) {
        teleship_log("[Order Sync] Order {$order_id} status changed: {$old_status} -> {$new_status}");

        // Loop prevention: skip if webhook is currently processing this order
        if ($order->get_meta(self::META_WEBHOOK_PROCESSING) === 'yes') {
            teleship_log("[Order Sync] Order {$order_id} skipped - webhook processing in progress");
            return;
        }

        // Sync to Teleship when order enters syncable status
        if (in_array($new_status, self::SYNC_STATUSES, true)) {
            if ($this->should_sync_order($order)) {
                // If order was previously fulfilled by Teleship and is being reverted to processing,
                // clear stale fulfillment data so sync sends correct UNFULFILLED status
                if ($this->was_fulfilled_by_teleship($order)) {
                    $this->clear_fulfillment_data($order);
                }
                $this->queue_order_for_sync($order);
            }
            return;
        }

        // Handle completed status: update to FULFILLED (not delete) unless Teleship fulfilled it
        if (in_array($new_status, self::COMPLETED_STATUSES, true)) {
            if (!Teleship_Order_Normalizer::is_supported_destination($order)) {
                return;
            }
            if ($this->was_fulfilled_by_teleship($order)) {
                teleship_log("[Order Sync] Order {$order_id} completed by Teleship fulfillment, skipping update");
                return;
            }
            $this->update_order_status_in_teleship($order, 'FULFILLED');
            return;
        }

        // Handle cancelled/failed: void shipments if fulfilled, then delete from Teleship
        if (in_array($new_status, self::REMOVE_STATUSES, true)) {
            $this->delete_order_from_teleship($order);
            return;
        }

        // Note: refunded orders are intentionally ignored — leave as-is in Teleship
    }

    /**
     * Check if an order should be synced (international filter + validity)
     *
     * @param WC_Order $order Order object.
     * @return bool True if order should be synced.
     */
    private function should_sync_order($order) {
        if (!Teleship_Order_Normalizer::is_supported_destination($order)) {
            $shipping_country = $order->get_shipping_country() ?: $order->get_billing_country();
            teleship_log('[Order Sync] Order ' . $order->get_id() . ' skipped - destination country ' . $shipping_country . ' not supported');
            return false;
        }

        return true;
    }

    /**
     * Queue order for batch sync
     *
     * @param WC_Order $order Order object.
     */
    private function queue_order_for_sync($order) {
        $order_id = $order->get_id();

        // Prevent duplicate queue entries
        if (!isset($this->pending_orders[$order_id])) {
            $this->pending_orders[$order_id] = $order;
            teleship_log("[Order Sync] Queued order {$order_id} for sync");
        }
    }

    /**
     * Process pending orders on shutdown
     */
    public function process_pending_orders() {
        if (empty($this->pending_orders)) {
            return;
        }

        teleship_log('[Order Sync] Processing ' . count($this->pending_orders) . ' pending orders');

        foreach ($this->pending_orders as $order_id => $order) {
            $this->sync_order($order);
        }

        $this->pending_orders = array();
    }

    /**
     * Sync order to Teleship using normalizer and mapping table
     *
     * @param WC_Order $order Order object.
     * @return bool Success status
     */
    public function sync_order($order, $force_post = false, $order_data = null) {
        $order_id = $order->get_id();

        try {
            teleship_log("[Order Sync] Syncing order {$order_id} to Teleship");

            $oauth = teleship_oauth();
            $auth_header = $oauth->get_authorization_header();

            if (empty($auth_header)) {
                teleship_log("[Order Sync] No auth token available for order {$order_id}");
                return false;
            }

            // Get or create mapping
            $mapping = $this->get_or_create_mapping($order);

            // Normalize order data (skip if pre-normalized data provided)
            if (null === $order_data) {
                $order_data = Teleship_Order_Normalizer::normalize($order);
            }

            if (null === $order_data) {
                teleship_log("[Order Sync] Order {$order_id} skipped by normalizer");
                if ($mapping) {
                    $this->update_mapping($order_id, array(
                        'sync_status'   => self::SYNC_STATUS_FAILED,
                        'error_message' => 'Normalizer returned null (missing address or line items)',
                        'sync_attempts' => ($mapping->sync_attempts ?? 0) + 1,
                    ));
                }
                return false;
            }

            $api_url = teleship_get_api_url() . '/api/orders';

            // During bulk sync (force_post), always POST — backend upserts on orderId+source+accountId
            $teleship_order_id = null;
            $method = 'POST';
            if (!$force_post) {
                // Check if order already exists in Teleship (from mapping or meta)
                if ($mapping && !empty($mapping->teleship_order_uuid)) {
                    $teleship_order_id = $mapping->teleship_order_uuid;
                } else {
                    $teleship_order_id = $order->get_meta(self::META_TELESHIP_ORDER_ID);
                }

                if (!empty($teleship_order_id)) {
                    $api_url .= '/' . $teleship_order_id;
                    $method = 'PUT';
                }
            }

            $request_args = array(
                'headers' => array(
                    'Authorization' => $auth_header,
                ),
                'timeout' => 30,
            );

            if ($method === 'PUT') {
                $response = Teleship_HTTP_Client::put($api_url, $order_data, $request_args);
            } else {
                $response = Teleship_HTTP_Client::post($api_url, $order_data, $request_args);
            }

            if (is_wp_error($response)) {
                $error_msg = $response->get_error_message();
                teleship_log("[Order Sync] Failed to sync order {$order_id}: {$error_msg}");
                if ($mapping) {
                    $this->update_mapping($order_id, array(
                        'sync_status'   => self::SYNC_STATUS_FAILED,
                        'error_message' => $error_msg,
                        'sync_attempts' => ($mapping->sync_attempts ?? 0) + 1,
                    ));
                }
                return false;
            }

            $http_code = wp_remote_retrieve_response_code($response);
            $body = json_decode(wp_remote_retrieve_body($response), true);

            // Handle 404 on PUT — stale UUID, clear it and fall back to POST
            if ($http_code === 404 && $method === 'PUT') {
                teleship_log("[Order Sync] Order {$order_id} not found at UUID {$teleship_order_id}, clearing stale reference and retrying as POST");

                // Clear stale UUID from order meta
                $order->delete_meta_data(self::META_TELESHIP_ORDER_ID);
                $order->save();

                // Clear stale UUID from mapping table
                if ($mapping) {
                    $this->update_mapping($order_id, array(
                        'teleship_order_uuid' => null,
                        'sync_status'         => self::SYNC_STATUS_PENDING,
                        'error_message'       => null,
                    ));
                }

                // Retry as POST
                $post_url = teleship_get_api_url() . '/api/orders';
                $response = Teleship_HTTP_Client::post($post_url, $order_data, $request_args);

                if (is_wp_error($response)) {
                    $error_msg = $response->get_error_message();
                    teleship_log("[Order Sync] POST fallback failed for order {$order_id}: {$error_msg}");
                    if ($mapping) {
                        $this->update_mapping($order_id, array(
                            'sync_status'   => self::SYNC_STATUS_FAILED,
                            'error_message' => $error_msg,
                            'sync_attempts' => ($mapping->sync_attempts ?? 0) + 1,
                        ));
                    }
                    return false;
                }

                $http_code = wp_remote_retrieve_response_code($response);
                $body = json_decode(wp_remote_retrieve_body($response), true);
            }

            if ($http_code !== 200 && $http_code !== 201) {
                $error_msg = "HTTP {$http_code}";
                if (isset($body['message'])) {
                    $error_msg .= ': ' . (is_array($body['message']) ? wp_json_encode($body['message']) : $body['message']);
                }
                teleship_log("[Order Sync] Failed to sync order {$order_id}: {$error_msg}");
                if ($mapping) {
                    $this->update_mapping($order_id, array(
                        'sync_status'   => self::SYNC_STATUS_FAILED,
                        'error_message' => $error_msg,
                        'sync_attempts' => ($mapping->sync_attempts ?? 0) + 1,
                    ));
                }
                return false;
            }

            // Save Teleship order ID
            $new_teleship_id = isset($body['id']) ? $body['id'] : $teleship_order_id;
            if (!empty($new_teleship_id)) {
                $order->update_meta_data(self::META_TELESHIP_ORDER_ID, $new_teleship_id);
                $order->save();

                if ($mapping) {
                    $this->update_mapping($order_id, array(
                        'teleship_order_uuid' => $new_teleship_id,
                        'sync_status'         => self::SYNC_STATUS_SYNCED,
                        'error_message'       => null,
                        'sync_attempts'       => ($mapping->sync_attempts ?? 0) + 1,
                    ));
                }
            }

            teleship_log("[Order Sync] Successfully synced order {$order_id}");
            return true;

        } catch (Exception $e) {
            teleship_log("[Order Sync] Error syncing order {$order_id}: " . $e->getMessage());
            $mapping = $this->get_mapping_by_order_id($order_id);
            if ($mapping) {
                $this->update_mapping($order_id, array(
                    'sync_status'   => self::SYNC_STATUS_FAILED,
                    'error_message' => $e->getMessage(),
                    'sync_attempts' => ($mapping->sync_attempts ?? 0) + 1,
                ));
            }
            return false;
        }
    }

    /**
     * Update order status in Teleship (without deleting)
     *
     * @param WC_Order $order  Order object.
     * @param string   $status Teleship status (FULFILLED, CANCELLED, etc.).
     * @return bool Success status.
     */
    private function update_order_status_in_teleship($order, $status) {
        $order_id = $order->get_id();
        $teleship_order_id = $order->get_meta(self::META_TELESHIP_ORDER_ID);

        if (empty($teleship_order_id)) {
            // Check mapping table
            $mapping = $this->get_mapping_by_order_id($order_id);
            if ($mapping && !empty($mapping->teleship_order_uuid)) {
                $teleship_order_id = $mapping->teleship_order_uuid;
            }
        }

        if (empty($teleship_order_id)) {
            teleship_log("[Order Sync] Order {$order_id} has no Teleship ID, nothing to update");
            return true;
        }

        try {
            $oauth = teleship_oauth();
            $auth_header = $oauth->get_authorization_header();

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

            $api_url = teleship_get_api_url() . '/api/orders/' . $teleship_order_id;

            $response = Teleship_HTTP_Client::put(
                $api_url,
                array('status' => $status),
                array(
                    'headers' => array(
                        'Authorization' => $auth_header,
                    ),
                    'timeout' => 30,
                )
            );

            if (is_wp_error($response)) {
                teleship_log("[Order Sync] Failed to update order {$order_id} status: " . $response->get_error_message());
                return false;
            }

            $http_code = wp_remote_retrieve_response_code($response);
            if ($http_code !== 200) {
                teleship_log("[Order Sync] Failed to update order {$order_id} status: HTTP {$http_code}");
                return false;
            }

            teleship_log("[Order Sync] Updated order {$order_id} status to {$status} in Teleship");
            return true;

        } catch (Exception $e) {
            teleship_log("[Order Sync] Error updating order {$order_id} status: " . $e->getMessage());
            return false;
        }
    }

    /**
     * Delete order from Teleship
     *
     * @param WC_Order $order Order object.
     * @return bool Success status
     */
    public function delete_order_from_teleship($order) {
        $order_id = $order->get_id();
        $teleship_order_id = $order->get_meta(self::META_TELESHIP_ORDER_ID);

        if (empty($teleship_order_id)) {
            // Check mapping table
            $mapping = $this->get_mapping_by_order_id($order_id);
            if ($mapping && !empty($mapping->teleship_order_uuid)) {
                $teleship_order_id = $mapping->teleship_order_uuid;
            }
        }

        if (empty($teleship_order_id)) {
            teleship_log("[Order Sync] Order {$order_id} has no Teleship ID, nothing to delete");
            return true;
        }

        try {
            $oauth = teleship_oauth();
            $auth_header = $oauth->get_authorization_header();

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

            // If order was fulfilled, void active shipments first
            if ($this->was_fulfilled_by_teleship($order)) {
                teleship_log("[Order Sync] Order {$order_id} was fulfilled, voiding shipments before removal");
                $this->void_order_shipments($teleship_order_id, $auth_header);
            }

            teleship_log("[Order Sync] Deleting order {$order_id} from Teleship");

            $api_url = teleship_get_api_url() . '/api/orders/' . $teleship_order_id;

            $response = Teleship_HTTP_Client::delete(
                $api_url,
                array(
                    'headers' => array(
                        'Authorization' => $auth_header,
                    ),
                    'timeout' => 30,
                )
            );

            if (is_wp_error($response)) {
                teleship_log("[Order Sync] Failed to delete order {$order_id}: " . $response->get_error_message());
                return false;
            }

            // Clear Teleship meta
            $order->delete_meta_data(self::META_TELESHIP_ORDER_ID);
            $order->delete_meta_data(self::META_TELESHIP_FULFILLED);
            $order->save();

            // Update mapping
            $this->update_mapping($order_id, array(
                'sync_status' => self::SYNC_STATUS_REMOVED,
            ));

            teleship_log("[Order Sync] Successfully deleted order {$order_id} from Teleship");
            return true;

        } catch (Exception $e) {
            teleship_log("[Order Sync] Error deleting order {$order_id}: " . $e->getMessage());
            return false;
        }
    }

    /**
     * Void all active shipments for a Teleship order
     *
     * Fetches the order's shipments from the API and voids any that are not
     * already cancelled/voided.
     *
     * @param string $teleship_order_id Teleship order UUID.
     * @param string $auth_header       Authorization header value.
     * @return void
     */
    private function void_order_shipments($teleship_order_id, $auth_header) {
        $api_url = teleship_get_api_url() . '/api/orders/' . $teleship_order_id;

        // Fetch the order to get its shipments
        $response = Teleship_HTTP_Client::get(
            $api_url,
            array(
                'headers' => array(
                    'Authorization' => $auth_header,
                ),
                'timeout' => 30,
            )
        );

        if (is_wp_error($response)) {
            teleship_log("[Order Sync] Failed to fetch order for voiding shipments: " . $response->get_error_message());
            return;
        }

        $body = json_decode(wp_remote_retrieve_body($response), true);
        $shipments = isset($body['shipments']) ? $body['shipments'] : array();

        if (empty($shipments)) {
            teleship_log("[Order Sync] No shipments to void for order {$teleship_order_id}");
            return;
        }

        foreach ($shipments as $shipment) {
            $status = isset($shipment['status']) ? strtolower($shipment['status']) : '';
            if (in_array($status, array('cancelled', 'voided'), true)) {
                continue;
            }

            $shipment_id = $shipment['id'];
            teleship_log("[Order Sync] Voiding shipment {$shipment_id} for order {$teleship_order_id}");

            $void_url = teleship_get_api_url() . '/api/shipments/' . $shipment_id . '/void';
            $void_response = Teleship_HTTP_Client::post(
                $void_url,
                array(),
                array(
                    'headers' => array(
                        'Authorization' => $auth_header,
                    ),
                    'timeout' => 30,
                )
            );

            if (is_wp_error($void_response)) {
                teleship_log("[Order Sync] Failed to void shipment {$shipment_id}: " . $void_response->get_error_message());
            } else {
                $void_code = wp_remote_retrieve_response_code($void_response);
                teleship_log("[Order Sync] Void shipment {$shipment_id} response: {$void_code}");
            }
        }
    }

    /**
     * Retry failed order syncs
     *
     * @param int $max_attempts Maximum attempts before giving up.
     * @return array Result summary.
     */
    public function retry_failed($max_attempts = 5) {
        global $wpdb;
        $table_name = $wpdb->prefix . self::MAPPING_TABLE;

        $failed_mappings = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Retry query needs fresh data
            $wpdb->prepare(
                "SELECT * FROM {$table_name} WHERE sync_status = %s AND sync_attempts < %d ORDER BY last_synced_at ASC LIMIT 50", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                self::SYNC_STATUS_FAILED,
                $max_attempts
            )
        );

        $retried = 0;
        $succeeded = 0;

        foreach ($failed_mappings as $mapping) {
            $order = wc_get_order($mapping->wc_order_id);
            if (!$order) {
                $this->update_mapping($mapping->wc_order_id, array(
                    'sync_status'   => self::SYNC_STATUS_FAILED,
                    'error_message' => 'Order no longer exists',
                ));
                continue;
            }

            // Only retry if order is still in a syncable status
            if (!in_array($order->get_status(), self::SYNC_STATUSES, true)) {
                continue;
            }

            // Skip orders with unsupported destinations
            if (!$this->should_sync_order($order)) {
                continue;
            }

            $retried++;
            if ($this->sync_order($order)) {
                $succeeded++;
            }
        }

        return array(
            'retried'   => $retried,
            'succeeded' => $succeeded,
            'failed'    => $retried - $succeeded,
        );
    }

    /**
     * Check if order was fulfilled by Teleship
     *
     * @param WC_Order $order Order object.
     * @return bool
     */
    public function was_fulfilled_by_teleship($order) {
        return 'yes' === $order->get_meta(self::META_TELESHIP_FULFILLED);
    }

    /**
     * Clear fulfillment data from a WooCommerce order.
     *
     * Called when a shipment is cancelled or order is manually reverted to processing.
     * Resets per-item shipped quantities and clears Teleship fulfillment metadata
     * so the order can be re-synced as UNFULFILLED.
     *
     * @param WC_Order $order The order to clear fulfillment data from.
     */
    public function clear_fulfillment_data($order) {
        // Reset per-item shipped quantities
        foreach ($order->get_items() as $item_id => $item) {
            Teleship_Fulfillment_Calculator::update_shipped_quantity($item_id, 0);
        }

        // Clear fulfillment metadata
        $order->delete_meta_data(self::META_TELESHIP_FULFILLED);
        $order->delete_meta_data(self::META_SHIPMENT_ID);
        $order->delete_meta_data(self::META_TRACKING_NUMBER);
        $order->delete_meta_data(self::META_TRACKING_URL);
        $order->delete_meta_data(self::META_CARRIER);
        $order->delete_meta_data('_teleship_shipped_date');
        $order->delete_meta_data('_teleship_firstmile_carrier');
        $order->delete_meta_data('_teleship_firstmile_tracking');
        $order->delete_meta_data('_teleship_firstmile_tracking_url');
        $order->save();

        teleship_log('Cleared fulfillment data for order ' . $order->get_id());
    }

    /**
     * Mark order as fulfilled by Teleship
     *
     * @param WC_Order $order          Order object.
     * @param string   $tracking_number Tracking number.
     * @param string   $tracking_url    Tracking URL.
     * @param string   $carrier         Carrier name.
     * @param string   $shipment_id     Teleship shipment ID.
     */
    public function mark_as_fulfilled($order, $tracking_number, $tracking_url = '', $carrier = '', $shipment_id = '') {
        $order->update_meta_data(self::META_TELESHIP_FULFILLED, 'yes');
        $order->update_meta_data(self::META_TRACKING_NUMBER, $tracking_number);

        if (!empty($tracking_url)) {
            $order->update_meta_data(self::META_TRACKING_URL, $tracking_url);
        }

        if (!empty($carrier)) {
            $order->update_meta_data(self::META_CARRIER, $carrier);
        }

        if (!empty($shipment_id)) {
            $order->update_meta_data(self::META_SHIPMENT_ID, $shipment_id);
        }

        // Add order note
        $note = sprintf(
            /* translators: 1: Carrier name, 2: Tracking number */
            __('Teleship shipment created. Carrier: %1$s, Tracking: %2$s', 'teleship-global-shipping'),
            $carrier ?: 'Unknown',
            $tracking_number
        );

        if (!empty($tracking_url)) {
            $note .= sprintf(' <a href="%s" target="_blank">%s</a>', esc_url($tracking_url), __('Track', 'teleship-global-shipping'));
        }

        $order->add_order_note($note, false, true);
        $order->save();

        teleship_log("[Order Sync] Order {$order->get_id()} marked as fulfilled by Teleship");
    }

    /**
     * Get tracking info for an order
     *
     * @param WC_Order $order Order object.
     * @return array|null Tracking info or null
     */
    public function get_tracking_info($order) {
        $tracking_number = $order->get_meta(self::META_TRACKING_NUMBER);

        if (empty($tracking_number)) {
            return null;
        }

        return array(
            'tracking_number' => $tracking_number,
            'tracking_url'    => $order->get_meta(self::META_TRACKING_URL),
            'carrier'         => $order->get_meta(self::META_CARRIER),
            'shipment_id'     => $order->get_meta(self::META_SHIPMENT_ID),
        );
    }

    /**
     * Manually sync a single order
     *
     * @param int $order_id Order ID.
     * @return array Result array
     */
    public function manual_sync($order_id) {
        $order = wc_get_order($order_id);

        if (!$order) {
            return array(
                'success' => false,
                'message' => 'Order not found',
            );
        }

        if (!$this->should_sync_order($order)) {
            $shipping_country = $order->get_shipping_country() ?: $order->get_billing_country();
            return array(
                'success' => false,
                'message' => 'Order destination country ' . $shipping_country . ' is not supported for sync',
            );
        }

        $result = $this->sync_order($order);

        return array(
            'success' => $result,
            'message' => $result ? 'Order synced successfully' : 'Failed to sync order',
        );
    }

    /**
     * Bulk sync orders
     *
     * @param array $args Query arguments for orders.
     * @return array Result array
     */
    public function bulk_sync($args = array()) {
        $default_args = array(
            'status'  => self::SYNC_STATUSES,
            'limit'   => 100,
            'orderby' => 'date',
            'order'   => 'DESC',
        );

        $args = wp_parse_args($args, $default_args);
        $orders = wc_get_orders($args);

        $synced   = 0;
        $filtered = 0;
        $skipped  = 0;
        $failed   = 0;

        // Phase 1: Filter and normalize all orders upfront
        $orders_to_sync = array();

        foreach ($orders as $order) {
            $order_id = $order->get_id();

            // Check international-only filter
            if (!$this->should_sync_order($order)) {
                $filtered++;
                continue;
            }

            // Pre-normalize order data
            $order_data = Teleship_Order_Normalizer::normalize($order);

            if (null === $order_data) {
                teleship_log("[Order Sync] Order {$order_id} skipped by normalizer");
                $mapping = $this->get_or_create_mapping($order);
                if ($mapping) {
                    $this->update_mapping($order_id, array(
                        'sync_status'   => self::SYNC_STATUS_FAILED,
                        'error_message' => 'Normalizer returned null (missing address or line items)',
                        'sync_attempts' => ($mapping->sync_attempts ?? 0) + 1,
                    ));
                }
                $skipped++;
                continue;
            }

            $orders_to_sync[] = array(
                'order' => $order,
                'data'  => $order_data,
            );
        }

        teleship_log(sprintf(
            '[Order Sync] Bulk sync: %d total, %d filtered, %d skipped by normalizer, %d to sync',
            count($orders), $filtered, $skipped, count($orders_to_sync)
        ));

        // Phase 2: Sync valid orders — always POST, backend upserts on orderId+source+accountId
        foreach ($orders_to_sync as $item) {
            if ($this->sync_order($item['order'], true, $item['data'])) {
                $synced++;
            } else {
                $failed++;
            }
        }

        return array(
            'success'  => $failed === 0,
            'message'  => sprintf('Synced %d, filtered %d, skipped %d, failed %d (of %d total)', $synced, $filtered, $skipped, $failed, count($orders)),
            'synced'   => $synced,
            'filtered' => $filtered,
            'skipped'  => $skipped,
            'failed'   => $failed,
            'total'    => count($orders),
        );
    }
}

/**
 * Get Teleship Order Sync instance
 *
 * @return Teleship_Order_Sync
 */
function teleship_order_sync() {
    return Teleship_Order_Sync::instance();
}

// Initialize on plugins_loaded
add_action('plugins_loaded', array('Teleship_Order_Sync', 'init'), 20);
