<?php
/**
 * Packie courier adapter
 * Included by api.php — expects $db, $pm, $action, $input, $arg_* CLI vars to be set.
 * ecom_call() / ecom_graphql() are defined in api.php before this file is included.
*/

if (!class_exists('Logger')) require_once __DIR__ . '/../logger.php';

// ── Carrier-scoped helpers ────────────────────────────────────────────────────
function get_packie_headers() {
    global $pm;
    return $pm->getShipHeaders();
}

function detect_packie_status(string $street, string $suburb, string $city, string $postcode, string $country = 'NZ', string $debug_order = '', int $packie_order_id = 0): string {
    global $pm;
    return $pm->detectShipStatus($street, $suburb, $city, $postcode, $country, $debug_order, $packie_order_id);
}

function sync_packie_stocks(mysqli $db): array {
    global $pm;

    $headers = get_packie_headers();
    if (!$headers) return ['success' => false, 'message' => 'Missing Packie headers/token', 'count' => 0];

    $ch = curl_init($pm->shipUrl('stocks'));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    $stocks_res = curl_exec($ch);
    curl_close($ch);

    $stocks_data = json_decode($stocks_res, true);
    if (!isset($stocks_data['success']) || !$stocks_data['success']) {
        return ['success' => false, 'message' => 'Packie stocks endpoint failed', 'count' => 0];
    }

    $rows = $stocks_data['result'] ?? [];
    if (empty($rows)) {
        return ['success' => true, 'message' => 'No stock rows returned', 'count' => 0];
    }

    $stock_stmt = $db->prepare(
"INSERT INTO v2_packie_stocks (id, name, serviceType, height, width, length, weight, cubicWeight)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
serviceType = VALUES(serviceType),
height = VALUES(height),
width = VALUES(width),
length = VALUES(length),
weight = VALUES(weight),
cubicWeight = VALUES(cubicWeight)"
);

    $count = 0;
    foreach ($rows as $stock) {
        $id = (int)($stock['id'] ?? 0);
        if ($id <= 0) continue;

        $name = (string)($stock['name'] ?? '');
        $service_type = (int)($stock['serviceType'] ?? 0);
        $height = (float)($stock['height'] ?? 0);
        $width = (float)($stock['width'] ?? 0);
        $length = (float)($stock['length'] ?? 0);
        $weight = (float)($stock['weight'] ?? 0);
        $cubic_weight = (float)($stock['cubicWeight'] ?? 0);

        $stock_stmt->bind_param('isiddddd', $id, $name, $service_type, $height, $width, $length, $weight, $cubic_weight);
        $stock_stmt->execute();
        $count++;
    }

    return ['success' => true, 'message' => 'Stocks synced', 'count' => $count];
}

function sync_packie_orders_and_stocks(mysqli $db): array {
    global $pm;

    Logger::info('packie_sync', 'Sync started');

    $shopify_raw = function_exists('ecommerce_get_open_orders_raw') ? ecommerce_get_open_orders_raw() : [];

    $headers = get_packie_headers();
    if (!$headers) {
        Logger::error('packie_sync', 'Missing Packie headers/token');
        return ['success' => false, 'message' => 'Missing Packie headers/token'];
    }

    $ch2 = curl_init($pm->shipUrl('Orders/get-orders?status=1&search=&integrationId=0&page=1&perPage=100'));
    curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch2, CURLOPT_HTTPHEADER, $headers);
    $data = json_decode(curl_exec($ch2), true);
    curl_close($ch2);

    if (isset($data['result']['orders'])) {
        $insert_stmt = $db->prepare(
    "INSERT INTO v2_packie_cache (shopify_order_number, packie_id, packie_status, original_shopify_address)
     VALUES (?, ?, ?, ?)
     ON DUPLICATE KEY UPDATE
         packie_id     = VALUES(packie_id),
         packie_status = VALUES(packie_status),
         original_shopify_address = IF(original_shopify_address IS NULL OR original_shopify_address = '', VALUES(original_shopify_address), original_shopify_address)"
);

        foreach ($data['result']['orders'] as $order) {
            $shopify_num = (string)($order['orderNumber'] ?? '');
            if ($shopify_num === '') continue;

            $r          = $order['receiver'] ?? [];
            $p_street   = $r['street']   ?? '';
            $p_suburb   = $r['suburb']   ?? '';
            $p_city     = $r['city']     ?? '';
            $p_postcode = $r['postCode'] ?? '';
            $p_country  = $r['country']  ?? 'NZ';

            // Build a raw address string from Shopify as the source of truth
            $raw_addr_string = '';
            foreach ($shopify_raw as $so) {
                if (str_replace('#', '', $so['name'] ?? '') === $shopify_num) {
                    $sa    = $so['shipping_address'] ?? [];
                    $parts = array_filter([$sa['address1'] ?? '', $sa['address2'] ?? '', $sa['city'] ?? '', $sa['zip'] ?? '']);
                    $raw_addr_string = implode(', ', $parts);
                    break;
                }
            }
            if ($raw_addr_string === '') {
                $raw_addr_string = trim(implode(', ', array_filter([$p_street, $p_suburb, $p_city, $p_postcode])));
            }

            $packie_id = (string)($order['id'] ?? '');
            
            $status = detect_packie_status($p_street, $p_suburb, $p_city, $p_postcode, $p_country, $shopify_num, (int)$packie_id);

            Logger::info('packie_sync', 'Order synced', ['order' => $shopify_num, 'status' => $status, 'address' => "$p_street, $p_suburb, $p_city"]);

            $insert_stmt->bind_param("ssss", $shopify_num, $packie_id, $status, $raw_addr_string);
            $insert_stmt->execute();
        }
    }

    $stocks_sync = sync_packie_stocks($db);
    Logger::info('packie_sync', 'Stocks sync result', ['success' => $stocks_sync['success'], 'count' => (int)($stocks_sync['count'] ?? 0)]);

    Logger::info('packie_sync', 'Sync complete');
    return ['success' => true, 'message' => 'Sync complete', 'stocks' => $stocks_sync];
}

function packie_find_consignment_id_from_create_response($value): int {
    if (is_numeric($value) && (int)$value > 0) return (int)$value;
    if (is_string($value)) {
        if (preg_match('~/pdf-labels-download/(\d+)/~i', $value, $m)) return (int)$m[1];
        return 0;
    }
    if (!is_array($value)) return 0;

    foreach ([
        'consignmentId',
        'consignmentID',
        'consignment_id',
        'consignmentNumber',
        'consignment_number',
        'consignmentNo',
        'consignment_no',
        'consignment',
        'shipmentId',
        'shipment_id',
        'ticketId',
        'ticket_id',
        'id',
    ] as $key) {
        if (isset($value[$key]) && is_numeric($value[$key]) && (int)$value[$key] > 0) {
            return (int)$value[$key];
        }
    }

    if (isset($value['result']) && is_array($value['result'])) {
        $id = packie_find_consignment_id_from_create_response($value['result']);
        if ($id > 0) return $id;
    }
    foreach ($value as $child) {
        $id = packie_find_consignment_id_from_create_response($child);
        if ($id > 0) return $id;
    }

    return 0;
}

function packie_find_label_pdf_url_from_create_response($value): string {
    if (is_string($value)) {
        if (preg_match('~https://(?:www\.)?packie\.co\.nz/api/consignments/pdf-labels-download/\d+/parcels/(?:true|false)~i', $value, $m)) {
            return $m[0];
        }
        if (preg_match('~/api/consignments/pdf-labels-download/\d+/parcels/(?:true|false)~i', $value, $m)) {
            global $pm;
            return $pm ? $pm->shipUrl(ltrim($m[0], '/')) : 'https://www.packie.co.nz' . $m[0];
        }
        return '';
    }
    if (!is_array($value)) return '';

    foreach ([
        'labelPdfUrl',
        'label_pdf_url',
        'pdfLabelUrl',
        'pdf_label_url',
        'labelUrl',
        'label_url',
        'downloadUrl',
        'download_url',
        'url',
    ] as $key) {
        if (isset($value[$key])) {
            $url = packie_find_label_pdf_url_from_create_response($value[$key]);
            if ($url !== '') return $url;
        }
    }

    foreach ($value as $child) {
        $url = packie_find_label_pdf_url_from_create_response($child);
        if ($url !== '') return $url;
    }

    return '';
}

function packie_label_pdf_url(int $consignment_id): string {
    global $pm;
    if ($consignment_id <= 0 || !$pm) return '';
    return $pm->shipUrl('consignments/pdf-labels-download/' . $consignment_id . '/parcels/false');
}

function packie_find_consignment_id_from_order_response($value, string $parent_key = ''): int {
    if (is_string($value) && preg_match('~/pdf-labels-download/(\d+)/~i', $value, $m)) return (int)$m[1];
    if (is_numeric($value) && stripos($parent_key, 'consignment') !== false) return (int)$value;
    if (!is_array($value)) return 0;

    foreach ($value as $key => $child) {
        $key_text = is_string($key) ? $key : '';
        $key_lower = strtolower($key_text);
        if (
            is_numeric($child)
            && (
                strpos($key_lower, 'consignment') !== false
                || strpos($key_lower, 'shipment') !== false
                || strpos($key_lower, 'ticket') !== false
            )
            && (int)$child > 0
        ) {
            return (int)$child;
        }
        if ($key_lower === 'id' && stripos($parent_key, 'consignment') !== false && is_numeric($child) && (int)$child > 0) {
            return (int)$child;
        }
        $id = packie_find_consignment_id_from_order_response($child, $key_text);
        if ($id > 0) return $id;
    }

    return 0;
}

function packie_lookup_label_after_create(string $shopify_num, array $headers): array {
    global $pm;
    if ($shopify_num === '' || !$pm) return ['consignment_id' => 0, 'pdf_url' => ''];

    $today = date('Y-m-d');
    $from = date('Y-m-d', strtotime('-7 days'));
    $parcels_payload = json_encode([
        'search' => '',
        'from' => $from,
        'to' => $today,
        'status' => 'all',
    ]);
    $ch = curl_init($pm->shipUrl('Consignments/parcels/1/25'));
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $parcels_payload);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, array_merge($headers, ['Content-Type: application/json']));
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    $raw = curl_exec($ch);
    $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err = curl_error($ch);
    curl_close($ch);

    $decoded = json_decode((string)$raw, true);
    $parcels = $decoded['result']['result'] ?? [];
    $matched = null;
    if (is_array($parcels)) {
        foreach ($parcels as $parcel) {
            $order_num = preg_replace('/\D+/', '', (string)($parcel['orderNumber'] ?? ''));
            if ($order_num === $shopify_num) {
                $matched = $parcel;
                break;
            }
        }
    }

    Logger::info('print_label', 'post_create_parcels_lookup', [
        'order' => $shopify_num,
        'from' => $from,
        'to' => $today,
        'http_code' => $http,
        'curl_error' => $err !== '' ? $err : 'none',
        'parcels_count' => is_array($parcels) ? count($parcels) : 0,
        'matched' => $matched ? '1' : '0',
    ]);

    if ($matched) {
        $label_download_id = (int)($matched['id'] ?? 0);
        if ($label_download_id > 0) {
            return [
                'consignment_id' => $label_download_id,
                'packie_consignment_id' => (int)($matched['consignmentId'] ?? 0),
                'pdf_url' => packie_label_pdf_url($label_download_id),
            ];
        }
    }

    foreach ([1, 2, 3, 4, 5, 0, 99] as $st) {
        $url = $pm->shipUrl('Orders/get-orders?status=' . $st . '&search=' . rawurlencode($shopify_num) . '&integrationId=0&page=1&perPage=5');
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
        $raw = curl_exec($ch);
        $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err = curl_error($ch);
        curl_close($ch);

        $decoded = json_decode((string)$raw, true);
        $pdf_url = packie_find_label_pdf_url_from_create_response($decoded);
        $consignment_id = 0;
        if ($pdf_url !== '' && preg_match('~/pdf-labels-download/(\d+)/~i', $pdf_url, $m)) {
            $consignment_id = (int)$m[1];
        }

        Logger::info('print_label', 'post_create_order_lookup', [
            'status' => $st,
            'http_code' => $http,
            'curl_error' => $err !== '' ? $err : 'none',
            'consignment_id' => $consignment_id,
            'pdf_url' => $pdf_url,
        ]);

        if ($pdf_url !== '') {
            return ['consignment_id' => $consignment_id, 'pdf_url' => $pdf_url];
        }
    }

    return ['consignment_id' => 0, 'pdf_url' => ''];
}

// ── cron_auth — refresh bearer token ─────────────────────────────────────────
if ($action == 'cron_auth') {
    $ok = $pm->refreshShipToken();
    echo $ok ? "Token Refreshed\n" : "Token refresh failed\n";
    exit;
}

// ── cron_sync — pull open orders from Shopify & match against Packie ─────────
if ($action == 'cron_sync') {
    $res = sync_packie_orders_and_stocks($db);
    echo json_encode($res);
    exit;
}

// ── cron_sync_stocks — force refresh only stock catalogue ───────────────────
if ($action == 'cron_sync_stocks') {
    $res = sync_packie_stocks($db);
    echo json_encode($res);
    exit;
}

// ── get_orders (shipping-only fallback) ─────────────────────────────────────
if (!function_exists('shipping_get_orders')) {
function shipping_get_orders(): array {
    global $db, $pm;
    $headers = get_packie_headers();
    if (!$headers) return [];

    $notes = [];
    $res_notes = $db->query("SELECT order_id, note_text FROM v2_order_notes");
    if ($res_notes) {
        while ($row = $res_notes->fetch_assoc()) {
            $notes[(string)$row['order_id']] = (string)$row['note_text'];
        }
    }

    $cache = [];
    $res_cache = $db->query("SELECT shopify_order_number, packie_id, packie_status, original_shopify_address, corrected_address FROM v2_packie_cache");
    if ($res_cache) {
        while ($row = $res_cache->fetch_assoc()) {
            $cache[(string)$row['shopify_order_number']] = $row;
        }
    }

    $ch = curl_init($pm->shipUrl('Orders/get-orders?status=1&search=&integrationId=0&page=1&perPage=100'));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    $list_raw = curl_exec($ch);
    curl_close($ch);

    $list = json_decode($list_raw, true);
    $orders = $list['result']['orders'] ?? [];
    $output = [];

    $cache_upsert = $db->prepare("\n        INSERT INTO v2_packie_cache (shopify_order_number, packie_id, packie_status, original_shopify_address)
        VALUES (?, ?, ?, ?)
        ON DUPLICATE KEY UPDATE
            packie_id = VALUES(packie_id),
            packie_status = VALUES(packie_status),
            original_shopify_address = IF(original_shopify_address IS NULL OR original_shopify_address = '', VALUES(original_shopify_address), original_shopify_address)
    ");

    foreach ($orders as $o) {
        $packie_id = (string)($o['id'] ?? '');
        $packie_id_int = (int)$packie_id;
        $order_num = (string)($o['orderNumber'] ?? '');
        if ($packie_id === '' || $order_num === '') continue;

        $receiver = $o['receiver'] ?? [];
        $street   = trim((string)($receiver['address1'] ?? ($receiver['street'] ?? '')));
        $suburb   = trim((string)($receiver['address2'] ?? ($receiver['suburb'] ?? '')));
        $city     = trim((string)($receiver['city'] ?? ''));
        $postcode = trim((string)($receiver['postCode'] ?? ''));
        $country  = trim((string)($receiver['country'] ?? 'NZ'));
        $orig_addr = implode(', ', array_filter([$street, $suburb, $city, $postcode]));

        $status = $cache[$order_num]['packie_status'] ?? '';
        if ($status === '' || $status === 'PENDING') {
            $status = detect_packie_status($street, $suburb, $city, $postcode, $country, $order_num, (int)$packie_id);
            $cache_upsert->bind_param('siss', $order_num, $packie_id_int, $status, $orig_addr);
            $cache_upsert->execute();
        }
        if ($status === 'PRINTED') continue;

        $detail_items = [];
        $chd = curl_init($pm->shipUrl('Orders/get/' . urlencode($packie_id)));
        curl_setopt($chd, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($chd, CURLOPT_HTTPHEADER, $headers);
        $detail_raw = curl_exec($chd);
        curl_close($chd);
        $detail = json_decode($detail_raw, true);
        $full = $detail['result'][0] ?? [];

        foreach (($full['items'] ?? []) as $it) {
            $item_id = (string)($it['id'] ?? uniqid('it', true));
            $sku_label = trim((string)($it['label'] ?? ''));
            $detail_items[] = [
                'name' => (string)($it['name'] ?? 'Item'),
                'qty' => (int)($it['quantity'] ?? 1),
                'sku' => $sku_label !== '' ? $sku_label : ('No SKU #' . $item_id),
                'img' => 'https://placehold.co/100x100/png?text=No+Img',
            ];
        }

        $method = (string)($o['shippingMethod'] ?? $o['postService'] ?? 'Shipping');
        $name = '#' . $order_num;
        $cached_corrected = $cache[$order_num]['corrected_address'] ?? '';
        $cached_original  = $cache[$order_num]['original_shopify_address'] ?? $orig_addr;
        $output[] = [
            'id' => $packie_id,
            'name' => $name,
            'customer' => (string)($receiver['contactName'] ?? 'Customer'),
            'address' => $cached_corrected ?: trim($street . ', ' . $city),
            'postcode' => $postcode,
            'method' => $method,
            'country' => $country,
            'is_pickup' => (stripos($method, 'Pickup') !== false || stripos($method, '247 Cuba') !== false),
            'unpaid' => false,
            'note' => $notes[$packie_id] ?? '',
            'items' => $detail_items,
            'packie_status' => $status ?: 'PENDING',
            'original_address' => $cached_original,
            'corrected_address' => $cached_corrected,
            'is_risky' => false,
            'needs_address_check' => false,
            'source' => 'shipping',
            'scanner_allowed' => !array_filter($detail_items, fn($item) => str_starts_with(strtolower((string)($item['sku'] ?? '')), 'no sku')),
        ];
    }

    return $output;
}
}

// ── print_label — create Packie consignment (called via CLI from mark_packed) ─
if ($action == 'print_label') {
    $order_id = $arg_oid;
    $staff    = $arg_staff;
    $stock_id = $arg_stock;
    $rate_id  = $arg_rate;

    Logger::info('print_label', 'start', ['order_id' => $order_id, 'staff' => $staff, 'stock' => $stock_id, 'rate' => $rate_id, 'company' => $_boot_company_slug ?? '']);
    Logger::info('print_label', 'env', ['db_ok' => $db ? '1' : '0', 'pm_ok' => $pm ? '1' : '0', 'resolved_db' => $_resolved_client_db ?? 'null']);
    if ($pm) {
        $_pl_ship_token = $pm->getShipToken();
        $_pl_ship_headers = $pm->getShipHeaders();
        Logger::info('print_label', 'provider', ['ship_slug' => $pm->getShipSlug(), 'ecom_slug' => $pm->getEcomSlug(), 'has_token' => $_pl_ship_token ? '1' : '0', 'has_headers' => $_pl_ship_headers ? '1' : '0']);
    } else {
        Logger::error('print_label', 'CRITICAL pm=null - ProviderManager not initialized. Shipping settings may be missing from v2_settings.');
    }

    // packie.php is included before shopify.php in api.php and exits here, so the
    // ecommerce adapter is never loaded in the normal CLI flow. Include it explicitly
    // so ecommerce_get_order_meta / ecommerce_update_order_tags are available.
    if (!function_exists('ecommerce_get_order_meta')) {
        $_ecom_slug = ($pm && method_exists($pm, 'getEcomSlug')) ? $pm->getEcomSlug() : 'shopify';
        $_ecom_file = dirname(__DIR__) . '/ecommerce/' . $_ecom_slug . '.php';
        if ($_ecom_slug === '') {
            Logger::info('print_label', 'ecom_disabled', ['slug' => 'none']);
        } elseif (file_exists($_ecom_file)) {
            include $_ecom_file;
            Logger::info('print_label', 'ecom_adapter_loaded', ['slug' => $_ecom_slug]);
        } else {
            Logger::warn('print_label', 'ecom_adapter_missing', ['slug' => $_ecom_slug ?: 'none']);
        }
    }

    $shopify_num = '';
    $tags = '';
    $ecom_order_meta = null;
    if (function_exists('ecommerce_get_order_meta')) {
        $ecom_order_meta = ecommerce_get_order_meta((string)$order_id);
    }
    if ($ecom_order_meta) {
        $shopify_num = str_replace('#', '', $ecom_order_meta['name'] ?? '');
        $tags = $ecom_order_meta['tags'] ?? '';
        Logger::info('print_label', 'ecom_order', ['shopify_num' => $shopify_num, 'tags' => $tags]);
    } else {
        $shopify_num = str_replace('#', '', (string)($arg_order_name ?? $order_id));
        $slug_for_log = $pm ? $pm->getEcomSlug() : 'no_pm';
        $reason = ($slug_for_log === '') ? 'ecom_disabled' : 'ecom_unavailable';
        Logger::warn('print_label', $reason, ['fallback_order' => $shopify_num, 'ecom_slug' => $slug_for_log]);
    }

    // Some app builds can send pack-only payloads (stock=none/rate=none) even when
    // user intended to print. Fallback to a default satchel so printing still runs.
    if ($stock_id === 'none' && $rate_id === 'none') {
        $fallback_stock_id = null;
        $fallback_q = $db->query("SELECT id FROM v2_packie_stocks WHERE name = 'Satchel A4' LIMIT 1");
        if ($fallback_q && ($fr = $fallback_q->fetch_assoc())) {
            $fallback_stock_id = (string)$fr['id'];
        } else {
            $fallback_q2 = $db->query("SELECT id FROM v2_packie_stocks ORDER BY id ASC LIMIT 1");
            if ($fallback_q2 && ($fr2 = $fallback_q2->fetch_assoc())) {
                $fallback_stock_id = (string)$fr2['id'];
            }
        }

        if ($fallback_stock_id !== null) {
            $stock_id = $fallback_stock_id;
            Logger::info('print_label', 'auto_stock_fallback', ['selected_stock_id' => $stock_id]);
        } else {
            Logger::warn('print_label', 'auto_stock_fallback_failed no_packie_stocks_found');
        }
    }

    if ($stock_id !== 'none' || $rate_id !== 'none') {
        $headers = get_packie_headers();
        if ($headers) {
            $packie_order = null;
            foreach ([1, 2, 3, 4, 5, 0, 99] as $st) {
                $ch = curl_init($pm->shipUrl("Orders/get-orders?status={$st}&search={$shopify_num}&integrationId=0&page=1&perPage=5"));
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
                curl_setopt($ch, CURLOPT_TIMEOUT, 10);
                $res = json_decode(curl_exec($ch), true);
                curl_close($ch);

                if (!empty($res['result']['orders'])) {
                    foreach ($res['result']['orders'] as $po) {
                        if ($po['orderNumber'] === $shopify_num) {
                            $packie_order = $po;
                            Logger::info('print_label', 'matched_packie_order', ['status' => $st, 'packie_id' => $po['id'] ?? '']);
                            break 2;
                        }
                    }
                }
            }

            if ($packie_order) {
                $sender_raw = $pm->senderDetails();
                $sender_from_order = is_array($packie_order['sender'] ?? null) ? $packie_order['sender'] : [];
                $receiver_raw = is_array($packie_order['receiver'] ?? null) ? $packie_order['receiver'] : [];
                $sender_street = trim((string)($sender_raw['street'] ?? $sender_raw['streetAddress'] ?? $sender_from_order['street'] ?? $sender_from_order['address1'] ?? ''));
                $sender_city = trim((string)($sender_raw['city'] ?? $sender_from_order['city'] ?? ''));
                $sender_postcode = trim((string)($sender_raw['postcode'] ?? $sender_raw['postCode'] ?? $sender_from_order['postCode'] ?? $sender_from_order['postcode'] ?? ''));
                $sender_country = trim((string)($sender_raw['country'] ?? 'NZ'));
                $pickup_suburb = trim((string)($sender_raw['suburb'] ?? $sender_from_order['suburb'] ?? $sender_city ?? ''));

                if ($sender_street === '') $sender_street = '247 Cuba Street';
                if ($sender_city === '') $sender_city = 'Palmerston North';
                if ($sender_postcode === '') $sender_postcode = '4410';
                if ($pickup_suburb === null || $pickup_suburb === '') $pickup_suburb = 'Palmerston North';

                // Packie create is strict about primitive string types; normalize nulls.
                foreach ($receiver_raw as $rk => $rv) {
                    if ($rv === null) $receiver_raw[$rk] = '';
                }

                // Packie create expects pickup_address to exist with string street.
                $pickup_address = [
                    'street' => (string)$sender_street,
                    'suburb' => (string)$pickup_suburb,
                    'city' => (string)$sender_city,
                    'postcode' => (string)$sender_postcode,
                    'postCode' => (string)$sender_postcode,
                    'country' => (string)$sender_country,
                ];

                $payload = [
                    'hasDangerousGoods'   => false,
                    'integratedOrderId'   => $packie_order['id'],
                    'integrationType'     => 1,
                    'isInternational'     => false,
                    'receiver'            => $receiver_raw,
                    'sender'              => [
                        'contactName' => (string)($sender_raw['contactName'] ?? ''),
                        'company' => (string)($sender_raw['company'] ?? ''),
                        // Provide both key variants so both calculate and create accept the object.
                        'street' => $sender_street,
                        'streetAddress' => $sender_street,
                        'address1' => $sender_street,
                        'address2' => $pickup_suburb,
                        'city' => $sender_city,
                        'suburb' => $pickup_suburb,
                        'state' => $sender_city,
                        'postcode' => $sender_postcode,
                        'postCode' => $sender_postcode,
                        'country' => $sender_country,
                    ],
                    'pickup_address'      => $pickup_address,
                    'shippingDescription' => 'Fastest Delivery',
                    'shippingMethod'      => 'Fastest Delivery',
                    'signatureRequired'   => false,
                ];

                Logger::debug('print_label', 'sender_normalized', ['street' => $sender_street ?: '[empty]', 'city' => $sender_city ?: '[empty]', 'postcode' => $sender_postcode ?: '[empty]']);
                Logger::debug('print_label', 'pickup_raw_values', ['street' => $sender_street, 'suburb' => $pickup_suburb, 'city' => $sender_city, 'postcode' => $sender_postcode]);
                Logger::debug('print_label', 'pickup_normalized', ['street' => $pickup_address['street'], 'suburb' => $pickup_address['suburb'], 'city' => $pickup_address['city'], 'postcode' => $pickup_address['postcode']]);

                if ($stock_id === 'custom') {
                    // Custom box: always recalculate at print time so rate IDs are never stale.
                    $payload['shippingDescription'] = 'Custom Box';
                    $payload['consignmentParcels']  = [[
                        'id' => '0', 'height' => (string)$arg_h, 'width' => (string)$arg_w,
                        'length' => (string)$arg_l, 'weight' => (string)$arg_weight,
                        'quantity' => '1', 'serviceType' => 1,
                    ]];
                    $ch = curl_init($pm->shipUrl('Consignments/calculate'));
                    curl_setopt($ch, CURLOPT_POST, true);
                    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
                    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
                    curl_setopt($ch, CURLOPT_TIMEOUT, 15);
                    $c_raw_custom     = curl_exec($ch);
                    $c_res_custom     = json_decode($c_raw_custom, true);
                    curl_close($ch);
                    $_cr              = $c_res_custom['result']['rates'][0] ?? null;
                    $custom_rate_id   = $_cr['id'] ?? $_cr['rateId'] ?? $_cr['rate_id'] ?? null;
                    // Fall back to the frontend-supplied ID if recalculation fails.
                    if (!$custom_rate_id && $rate_id !== 'none') $custom_rate_id = $rate_id;
                    $url = $custom_rate_id
                        ? $pm->shipUrl('Consignments/create/' . str_replace(' ', '%20', $custom_rate_id))
                        : null;
                    Logger::debug('print_label', 'custom_calculate', ['rate_id' => $custom_rate_id ?? 'none', 'response' => $c_raw_custom]);
                } else {
                    $stock_id_safe             = (int)$stock_id;
                    $_ps = $db->prepare("SELECT * FROM v2_packie_stocks WHERE id = ?");
                    $_ps->bind_param('i', $stock_id_safe);
                    $_ps->execute();
                    $s = $_ps->get_result()->fetch_assoc();
                    if ($s) {
                        $payload['consignmentParcels'] = [[
                            'id'          => (string)$s['id'],
                            'height'      => (string)$s['height'],
                            'width'       => (string)$s['width'],
                            'length'      => (string)$s['length'],
                            'weight'      => (string)$s['weight'],
                            'quantity'    => '1',
                            'serviceType' => (int)($s['serviceType'] ?: 1),
                        ]];
                        $ch = curl_init($pm->shipUrl('Consignments/calculate'));
                        curl_setopt($ch, CURLOPT_POST, true);
                        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
                        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
                        curl_setopt($ch, CURLOPT_TIMEOUT, 15);
                        $c_raw          = curl_exec($ch);
                        $c_res          = json_decode($c_raw, true);
                        curl_close($ch);
                        // Support multiple field-name conventions used by Packie's API.
                        $_rate_obj      = $c_res['result']['rates'][0] ?? null;
                        $rate_id_final  = $_rate_obj['id'] ?? $_rate_obj['rateId'] ?? $_rate_obj['rate_id'] ?? null;
                        $url            = $rate_id_final
                            ? $pm->shipUrl('Consignments/create/' . str_replace(' ', '%20', $rate_id_final))
                            : null;
                        Logger::debug('print_label', 'calculate', ['stock' => $stock_id_safe, 'rate_id' => $rate_id_final ?? 'none', 'response' => $c_raw]);
                    } else {
                        $url = null;
                        Logger::error('print_label', 'stock_not_found', ['stock' => $stock_id_safe]);
                    }
                }

                if (!empty($url)) {
                    $encoded_payload = json_encode($payload);
                    if ($encoded_payload === false) {
                        Logger::error('print_label', 'json_encode_failed', ['code' => json_last_error(), 'msg' => json_last_error_msg()]);
                        $encoded_payload = '{}';
                    }
                    Logger::debug('print_label', 'payload_debug', ['pickup_suburb_value' => $payload['pickup_address']['suburb'] ?? null, 'pickup_address' => $payload['pickup_address'], 'sender_suburb' => $payload['sender']['suburb'] ?? null]);
                    Logger::debug('print_label', 'create_request', ['url' => $url, 'payload' => $encoded_payload]);
                    $ch = curl_init($url);
                    curl_setopt($ch, CURLOPT_POST, true);
                    curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded_payload);
                    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                    $headers_with_json = array_merge($headers ?: [], ['Content-Type: application/json']);
                    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers_with_json);
                    curl_setopt($ch, CURLOPT_TIMEOUT, 20);
                    $create_raw = curl_exec($ch);
                    $create_http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
                    $create_err  = curl_error($ch);
                    curl_close($ch);
                    Logger::debug('print_label', 'create_response', ['body' => $create_raw]);
                    Logger::debug('print_label', 'create_meta', ['http_code' => $create_http, 'curl_error' => $create_err !== '' ? $create_err : 'none']);

                    $create_decoded = json_decode((string)$create_raw, true);
                    $create_message = is_array($create_decoded) ? (string)($create_decoded['message'] ?? '') : '';
                    $label_consignment_id = packie_find_consignment_id_from_create_response($create_decoded);
                    $label_pdf_url = packie_find_label_pdf_url_from_create_response($create_decoded);
                    if ($label_pdf_url === '') $label_pdf_url = packie_label_pdf_url($label_consignment_id);
                    Logger::info('print_label', 'create_label_lookup', [
                        'http_code' => $create_http,
                        'consignment_id' => $label_consignment_id,
                        'pdf_url' => $label_pdf_url,
                        'response_keys' => is_array($create_decoded) ? implode(',', array_slice(array_keys($create_decoded), 0, 12)) : 'non_json',
                    ]);
                    if ($create_message !== '' && stripos($create_message, '/pickup_address/suburb') !== false) {
                        $payload_retry = $payload;
                        unset($payload_retry['pickup_address']);
                        $encoded_retry = json_encode($payload_retry);
                        if ($encoded_retry !== false) {
                            Logger::warn('print_label', 'create_retry', ['reason' => 'pickup_suburb_null_without_pickup_address']);
                            Logger::debug('print_label', 'create_retry_request', ['url' => $url, 'payload' => $encoded_retry]);
                            $ch2 = curl_init($url);
                            curl_setopt($ch2, CURLOPT_POST, true);
                            curl_setopt($ch2, CURLOPT_POSTFIELDS, $encoded_retry);
                            curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);
                            curl_setopt($ch2, CURLOPT_HTTPHEADER, $headers_with_json);
                            curl_setopt($ch2, CURLOPT_TIMEOUT, 20);
                            $create_raw_retry = curl_exec($ch2);
                            $create_http_retry = (int)curl_getinfo($ch2, CURLINFO_HTTP_CODE);
                            $create_err_retry  = curl_error($ch2);
                            curl_close($ch2);
                            Logger::debug('print_label', 'create_retry_response', ['body' => $create_raw_retry]);
                            Logger::debug('print_label', 'create_retry_meta', ['http_code' => $create_http_retry, 'curl_error' => $create_err_retry !== '' ? $create_err_retry : 'none']);
                            $retry_decoded = json_decode((string)$create_raw_retry, true);
                            $retry_consignment_id = packie_find_consignment_id_from_create_response($retry_decoded);
                            $retry_pdf_url = packie_find_label_pdf_url_from_create_response($retry_decoded);
                            if ($retry_consignment_id > 0) {
                                $label_consignment_id = $retry_consignment_id;
                            }
                            if ($retry_pdf_url !== '') {
                                $label_pdf_url = $retry_pdf_url;
                            } elseif ($label_consignment_id > 0) {
                                $label_pdf_url = packie_label_pdf_url($label_consignment_id);
                            }
                        } else {
                            Logger::warn('print_label', 'create_retry_skipped', ['reason' => 'json_encode_failed']);
                        }
                    }

                    if ($shopify_num !== '') {
                        $lookup = packie_lookup_label_after_create($shopify_num, $headers_with_json);
                        if (($lookup['consignment_id'] ?? 0) > 0) {
                            $label_consignment_id = (int)$lookup['consignment_id'];
                        }
                        if (($lookup['pdf_url'] ?? '') !== '') {
                            $label_pdf_url = (string)$lookup['pdf_url'];
                        } elseif ($label_consignment_id > 0) {
                            $label_pdf_url = packie_label_pdf_url($label_consignment_id);
                        }
                    }

                    if ($label_consignment_id > 0 && $label_pdf_url !== '' && $shopify_num !== '') {
                        $label_stmt = $db->prepare(
                            "INSERT INTO v2_packie_cache (shopify_order_number, label_consignment_id, label_pdf_url, packie_status)
                             VALUES (?, ?, ?, 'PRINTED')
                             ON DUPLICATE KEY UPDATE
                                 label_consignment_id = VALUES(label_consignment_id),
                                 label_pdf_url = VALUES(label_pdf_url),
                                 packie_status = 'PRINTED'"
                        );
                        $label_stmt->bind_param('sis', $shopify_num, $label_consignment_id, $label_pdf_url);
                        $label_stmt->execute();
                        Logger::info('print_label', 'label_pdf_url_cached', ['order' => $shopify_num, 'consignment_id' => $label_consignment_id, 'url' => $label_pdf_url]);
                    } else {
                        Logger::warn('print_label', 'label_pdf_url_not_found', ['order' => $shopify_num, 'http_code' => $create_http, 'response' => $create_raw]);
                    }
                } else {
                    Logger::warn('print_label', 'skipped_create empty_url', ['stock' => $stock_id, 'rate' => $rate_id]);
                }
            } else {
                Logger::error('print_label', 'no_matching_packie_order', ['order' => $shopify_num]);
            }
        } else {
            Logger::error('print_label', 'missing_packie_headers');
        }
    } else {
        // Still no stock/rate after fallback attempt.
        Logger::warn('print_label', 'skipped_packie_create', ['reason' => 'no_stock_and_no_rate_after_fallback', 'stock' => $stock_id, 'rate' => $rate_id]);
    }

    Logger::info('print_label', 'COMPLETE', ['order_id' => $order_id, 'shopify_num' => $shopify_num]);
    if ($ecom_order_meta && function_exists('ecommerce_update_order_tags')) {
        $updated_tags = $tags ? "$tags, Packed, Packed by $staff" : "Packed, Packed by $staff";
        Logger::info('print_label', 'update_tags', ['tags' => $updated_tags]);
        ecommerce_update_order_tags((string)$order_id, $updated_tags);
    } else {
        Logger::info('print_label', 'skip_tags', ['ecom_meta' => $ecom_order_meta ? '1' : '0', 'fn_exists' => function_exists('ecommerce_update_order_tags') ? '1' : '0']);
    }
    exit;
}

// ── check_status — read v2_packie_cache ─────────────────────────────────────────
if ($action == 'check_status') {
    $order_name = $_GET['order_name'] ?? $input['order_name'] ?? '';
    $clean_num  = str_replace('#', '', $order_name);

    $stmt = $db->prepare("SELECT packie_status FROM v2_packie_cache WHERE shopify_order_number = ?");
    $stmt->bind_param("s", $clean_num);
    $stmt->execute();
    $res = $stmt->get_result()->fetch_assoc();

    echo json_encode(['packie_status' => $res ? $res['packie_status'] : 'PENDING']);
    exit;
}

// ── get_stocks — list box sizes ───────────────────────────────────────────────
if ($action == 'get_stocks') {
    $res = $db->query("SELECT * FROM v2_packie_stocks");

    // If stocks are empty (or initial query failed), try a live sync before returning.
    if (!$res || $res->num_rows === 0) {
        if (function_exists('sync_packie_stocks')) {
            @sync_packie_stocks($db);
            $res = $db->query("SELECT * FROM v2_packie_stocks");
        }
    }

    $stocks = [];
    if ($res) {
        while ($row = $res->fetch_assoc()) {
            $stocks[] = [
                'id'          => (int)$row['id'],
                'name'        => $row['name'],
                'serviceType' => (int)$row['serviceType'],
                'height'      => (float)$row['height'],
                'width'       => (float)$row['width'],
                'length'      => (float)$row['length'],
                'weight'      => (float)$row['weight'],
            ];
        }
    }
    echo json_encode($stocks);
    exit;
}

// ── get_custom_quote — calculate rates for a custom box ──────────────────────
if ($action == 'get_custom_quote') {
    $order_name  = $_GET['order_name'] ?? '';
    $w           = $_GET['w']          ?? 0;
    $l           = $_GET['l']          ?? 0;
    $h           = $_GET['h']          ?? 0;
    $weight      = $_GET['weight']     ?? 0;
    $shopify_num = str_replace('#', '', $order_name);
    $headers     = get_packie_headers();

    if (!$headers) { echo json_encode([]); exit; }

    $ch_search = curl_init($pm->shipUrl("Orders/get-orders?status=1&search={$shopify_num}&integrationId=0&page=1&perPage=5"));
    curl_setopt($ch_search, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch_search, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch_search, CURLOPT_TIMEOUT, 10);
    $search_res = json_decode(curl_exec($ch_search), true);
    curl_close($ch_search);

    // Verify the returned order matches the requested order number.
    $po = null;
    foreach ($search_res['result']['orders'] ?? [] as $_candidate) {
        if ((string)($_candidate['orderNumber'] ?? '') === $shopify_num) {
            $po = $_candidate;
            break;
        }
    }
    if ($po !== null) {
        $payload = [
            'consignmentParcels' => [[
                'id' => '0', 'height' => (string)$h, 'width' => (string)$w,
                'length' => (string)$l, 'weight' => (string)$weight,
                'quantity' => '1', 'serviceType' => 1,
            ]],
            'hasDangerousGoods'  => false,
            'integratedOrderId'  => $po['id'],
            'integrationType'    => 1,
            'isInternational'    => false,
            'receiver'           => $po['receiver'],
            'sender'             => $po['sender'],
        ];
        $ch = curl_init($pm->shipUrl('Consignments/calculate'));
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_TIMEOUT, 15);
        $res = json_decode(curl_exec($ch), true);
        curl_close($ch);
        echo json_encode($res['result']['rates'] ?? []);
    } else {
        echo json_encode([]);
    }
    exit;
}

// ── address_autocomplete — proxy Packie domestic address search ───────────────
if ($action == 'address_autocomplete') {
    $term = $_GET['term'] ?? '';
    if (strlen($term) < 3) { echo json_encode(['result' => []]); exit; }

    $headers = get_packie_headers();
    if (!$headers) { echo json_encode(['result' => []]); exit; }

    $ch = curl_init($pm->shipUrl('Address/search-domestic-address?term=' . urlencode($term)));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $headers_with_json = array_merge($headers ?: [], ['Content-Type: application/json']);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers_with_json);
    echo curl_exec($ch);
    curl_close($ch);
    exit;
}

// ── update_address — update ecommerce + Packie receiver, refresh rural status ──
if ($action == 'update_address') {
    $caller = get_current_user_from_request($db);
    if (!$caller) {
        Logger::warn('update_address', 'auth_failed');
        http_response_code(401);
        echo json_encode(['success' => false, 'error' => 'Unauthorized']);
        exit;
    }

    $order_id    = trim((string)($input['order_id'] ?? ''));
    $addr_unit   = trim((string)($input['unit']     ?? ''));
    $addr_street = trim((string)($input['address1'] ?? ''));
    $addr_suburb = trim((string)($input['address2'] ?? ''));
    $addr_city   = trim((string)($input['city']     ?? ''));
    $addr_zip    = trim((string)($input['zip']      ?? ''));
    $addr_notes  = trim((string)($input['notes']    ?? ''));
    // order_name sent by the app (e.g. '#17160') as a fallback when ecom adapter is unavailable
    $input_order_name = str_replace('#', '', trim((string)($input['order_name'] ?? '')));
    $full_street = $addr_unit !== '' ? "{$addr_unit} {$addr_street}" : $addr_street;

    Logger::info('update_address', 'start', ['order_id' => $order_id, 'order_name' => $input_order_name, 'street' => $full_street, 'suburb' => $addr_suburb, 'city' => $addr_city, 'zip' => $addr_zip]);

    // Explicitly load the ecommerce adapter if not already loaded.
    // packie.php is included before shopify.php in api.php, so without this
    // ecommerce_get_order_address / ecommerce_update_order_address won't exist yet.
    if (!function_exists('ecommerce_get_order_address')) {
        $_ecom_slug_ua = ($pm && method_exists($pm, 'getEcomSlug')) ? $pm->getEcomSlug() : 'shopify';
        $_ecom_file_ua = dirname(__DIR__) . '/ecommerce/' . $_ecom_slug_ua . '.php';
        if ($_ecom_slug_ua !== '' && file_exists($_ecom_file_ua)) {
            include $_ecom_file_ua;
            Logger::info('update_address', 'ecom_adapter_loaded', ['slug' => $_ecom_slug_ua]);
        } else {
            Logger::warn('update_address', 'ecom_adapter_missing', ['slug' => $_ecom_slug_ua]);
        }
    }

    $headers = get_packie_headers();
    if (!$headers) {
        Logger::error('update_address', 'missing_packie_headers');
        echo json_encode(['success' => false, 'error' => 'Packie not configured']);
        exit;
    }

    // Step 1: Try ecommerce adapter (only loaded in non-CLI web context)
    $ecom_name  = '';
    $ecom_email = '';
    $ecom_phone = '';
    $order_name = '';   // Shopify order number like '17160'

    if (function_exists('ecommerce_get_order_address')) {
        $ecom_order = ecommerce_get_order_address($order_id);
        if ($ecom_order) {
            $order_name = str_replace('#', '', $ecom_order['name'] ?? '');
            $ship       = $ecom_order['shipping_address'] ?? [];
            $ecom_email = $ecom_order['email'] ?? '';
            $ecom_phone = $ship['phone'] ?? '';
            $ecom_name  = $ship['name']  ?? '';
            Logger::info('update_address', 'ecom_order_found', ['order_name' => $order_name, 'contact' => $ecom_name]);
            if (function_exists('ecommerce_update_order_address')) {
                $ok = ecommerce_update_order_address($order_id, $ecom_order['tags'] ?? '', [
                    'address1' => $full_street,
                    'address2' => $addr_suburb,
                    'city'     => $addr_city,
                    'zip'      => $addr_zip,
                ]);
                Logger::info('update_address', 'ecom_update ' . ($ok ? 'success' : 'failed'));
            }
        } else {
            Logger::warn('update_address', 'ecom_order_not_found', ['order_id' => $order_id]);
            // Fall back to the order name sent directly by the app
            $order_name = $input_order_name;
        }
    } else {
        Logger::warn('update_address', 'ecom_adapter_not_loaded');
        $order_name = $input_order_name;
    }

    // Step 2: Resolve Packie order ID
    // Primary: when ecom is enabled, search Packie by Shopify order name
    // Fallback: treat order_id directly as the Packie order ID (ecom-disabled flow)
    $packie_order_id      = null;
    $shopify_order_number = $order_name;   // may be '' if ecom disabled

    if ($order_name !== '') {
        foreach ([1, 2, 3, 4, 5, 0, 99] as $stat) {
            $ch = curl_init($pm->shipUrl('Orders/get-orders?status=' . $stat . '&search=' . urlencode($order_name) . '&integrationId=0&page=1&perPage=5'));
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
            $res = json_decode(curl_exec($ch), true);
            curl_close($ch);
            if (!empty($res['result']['orders'])) {
                foreach ($res['result']['orders'] as $po) {
                    if ($po['orderNumber'] === $order_name) {
                        $packie_order_id = $po['id'];
                        break 2;
                    }
                }
            }
        }
        Logger::info('update_address', 'packie_search_by_name', ['name' => $order_name, 'found' => $packie_order_id ?? 'none']);
    }

    if (!$packie_order_id && is_numeric($order_id)) {
        // ecom disabled: app sends Packie order ID directly
        $packie_order_id = (int)$order_id;
        Logger::info('update_address', 'packie_id_direct', ['packie_id' => $packie_order_id]);
    }

    // Resolve Shopify order number from DB if not already known
    if ($shopify_order_number === '' && $packie_order_id) {
        $_pc_id = (int)$packie_order_id;
        $_pc = $db->prepare("SELECT shopify_order_number FROM v2_packie_cache WHERE packie_id = ?");
        $_pc->bind_param('i', $_pc_id);
        $_pc->execute();
        $row_pc = $_pc->get_result()->fetch_assoc();
        if ($row_pc) {
            $shopify_order_number = (string)$row_pc['shopify_order_number'];
            Logger::info('update_address', 'resolved_shopify_num', ['shopify_num' => $shopify_order_number]);
        } else {
            Logger::warn('update_address', 'packie_cache_miss', ['packie_id' => $packie_order_id]);
        }
    }

    // Fetch contact details from Packie if ecom didn't supply them
    if ($packie_order_id && ($ecom_name === '' || $ecom_email === '')) {
        $ch = curl_init($pm->shipUrl('Orders/get-order/' . (int)$packie_order_id));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
        $po_raw  = curl_exec($ch);
        curl_close($ch);
        $po_data = json_decode((string)$po_raw, true);
        $po_recv = $po_data['result']['receiver'] ?? [];
        $ecom_name  = $ecom_name  ?: (string)($po_recv['contactName'] ?? '');
        $ecom_email = $ecom_email ?: (string)($po_recv['email']       ?? '');
        $ecom_phone = $ecom_phone ?: (string)($po_recv['phone']       ?? '');
        Logger::info('update_address', 'packie_order_fetched', ['contact' => $ecom_name, 'email' => $ecom_email]);
    }

    // Step 3: Update Packie receiver
    if ($packie_order_id) {
        $p_payload   = [
            'contactName' => $ecom_name  ?: 'Customer',
            'email'       => $ecom_email ?: '',
            'phone'       => $ecom_phone ?: '',
            'unit'        => $addr_unit,
            'address1'    => $addr_street,
            'street'      => $full_street,
            'suburb'      => $addr_suburb,
            'city'        => $addr_city,
            'postcode'    => $addr_zip,
            'postCode'    => $addr_zip,
            'country'     => 'NZ',
            'notes'       => $addr_notes,
        ];
        $p_encoded    = json_encode($p_payload);
        $headers_json = array_merge($headers ?: [], ['Content-Type: application/json']);
        $ch = curl_init($pm->shipUrl('Orders/EditReceiver/' . (int)$packie_order_id));
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
        curl_setopt($ch, CURLOPT_POSTFIELDS, $p_encoded);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers_json);
        curl_setopt($ch, CURLOPT_TIMEOUT, 15);
        $edit_raw  = curl_exec($ch);
        $edit_http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        Logger::debug('update_address', 'editreceiver_request', ['packie_id' => $packie_order_id, 'payload' => $p_encoded]);
        Logger::debug('update_address', 'editreceiver_response', ['http' => $edit_http, 'body' => $edit_raw]);
    } else {
        Logger::error('update_address', 'no_packie_order_id');
    }

    // Step 4: Re-detect shipping status and update v2_packie_cache
    $new_status = detect_packie_status($full_street, $addr_suburb, $addr_city, $addr_zip, 'NZ', $shopify_order_number);
    Logger::info('update_address', 'new_status', ['status' => $new_status, 'shopify_num' => $shopify_order_number]);

    $corrected_address = implode(', ', array_filter([$full_street, $addr_suburb, $addr_city, $addr_zip]));

    if ($shopify_order_number !== '') {
        $stmt = $db->prepare('UPDATE v2_packie_cache SET packie_status = ?, corrected_address = ? WHERE shopify_order_number = ?');
        $stmt->bind_param('sss', $new_status, $corrected_address, $shopify_order_number);
        $stmt->execute();
        Logger::info('update_address', 'cache_updated', ['affected' => $stmt->affected_rows, 'corrected' => $corrected_address]);
    } else {
        Logger::warn('update_address', 'cache_not_updated no_shopify_num');
    }

    echo json_encode(['success' => true, 'new_status' => $new_status, 'corrected_address' => $corrected_address]);
    exit;
}

