<?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 packie_rate_matches_carrier(array $rate, string $carrier): bool {
    $carrier_key = strtolower(preg_replace('/[^a-z0-9]/', '', $carrier));
    if ($carrier_key === '') return true;
    $aliases = [
        'aramex' => ['aramex', 'fastway'],
        'nzpost' => ['nzpost', 'newzealandpost', 'courierpost', 'courierpack', 'courier'],
        'posthaste' => ['posthaste', 'posthastes'],
    ];
    $needles = $aliases[$carrier_key] ?? [$carrier_key];

    $haystacks = [
        $rate['name'] ?? '',
        $rate['provider'] ?? '',
        $rate['carrier'] ?? '',
        $rate['id'] ?? '',
    ];
    foreach ($haystacks as $value) {
        $normalized = strtolower(preg_replace('/[^a-z0-9]/', '', (string)$value));
        if ($normalized === '') continue;
        foreach ($needles as $needle) {
            if ($needle !== '' && (strpos($normalized, $needle) !== false || strpos($needle, $normalized) !== false)) {
                return true;
            }
        }
    }
    return false;
}

function packie_select_rate_for_stock(array $rates, string $preferred_carrier): ?array {
    if (empty($rates)) return null;
    foreach ($rates as $rate) {
        if (is_array($rate) && packie_rate_matches_carrier($rate, $preferred_carrier)) {
            return $rate;
        }
    }
    foreach ($rates as $rate) {
        if (is_array($rate)) return $rate;
    }
    return null;
}

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 pp_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++;
    }

    if (function_exists('afri_sync_packie_package_map')) {
        afri_sync_packie_package_map($db);
    }

    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 pp_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 = method_exists($pm, 'ensureShipTokenFresh') ? $pm->ensureShipTokenFresh() : $pm->refreshShipToken();
    echo $ok ? "Token OK\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 pp_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 pp_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 pp_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',
                'price' => (float)($it['price'] ?? 0),
                'weight' => max(0.001, (float)($it['weight'] ?? 0.001)),
                'hsCode' => $it['hsCode'] ?? ($it['hs_code'] ?? null),
                'countryOfOrigin' => $it['countryOfOrigin'] ?? ($it['country_of_origin'] ?? null),
            ];
        }

        $method = (string)($o['shippingMethod'] ?? $o['postService'] ?? 'Shipping');
        $name = '#' . $order_num;
        $order_created_at = (string)($o['orderDate'] ?? ($o['importDate'] ?? ($full['orderDate'] ?? ($full['importDate'] ?? ''))));
        $cached_corrected = $cache[$order_num]['corrected_address'] ?? '';
        $cached_original  = $cache[$order_num]['original_shopify_address'] ?? $orig_addr;
        $output[] = [
            'id' => $packie_id,
            'name' => $name,
            'order_created_at' => $order_created_at,
            'order_created_display' => function_exists('afri_short_order_datetime') ? afri_short_order_datetime($order_created_at) : '',
            'customer' => (string)($receiver['contactName'] ?? 'Customer'),
            'receiver_company' => (string)($receiver['company'] ?? ''),
            'receiver_building' => (string)($receiver['building'] ?? ''),
            'email' => (string)($receiver['email'] ?? ''),
            'phone' => (string)($receiver['phone'] ?? ''),
            'delivery_notes' => (string)($receiver['notes'] ?? ''),
            '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) ─
function packie_decode_international_details($encoded): ?array {
    $encoded = trim((string)$encoded);
    if ($encoded === '') return null;
    $decoded = base64_decode($encoded, true);
    if ($decoded === false || $decoded === '') return null;
    $json = json_decode($decoded, true);
    return is_array($json) ? $json : null;
}

function packie_is_international_order(array $order): bool {
    $receiver = is_array($order['receiver'] ?? null) ? $order['receiver'] : [];
    $country = strtoupper(trim((string)($receiver['country'] ?? 'NZ')));
    return $country !== '' && $country !== 'NZ';
}

function packie_default_international_items(array $order): array {
    $items = is_array($order['items'] ?? null) ? $order['items'] : [];
    $out = [];
    foreach ($items as $item) {
        if (!is_array($item)) continue;
        $qty = max(1, (int)($item['quantity'] ?? $item['qty'] ?? 1));
        $weight = (float)($item['weight'] ?? 0.001);
        $value = (float)($item['price'] ?? $item['value'] ?? 1);
        $out[] = [
            'description' => substr(trim((string)($item['name'] ?? 'Goods')), 0, 50),
            'quantity' => $qty,
            'netWeight' => $weight > 0 ? $weight : 0.001,
            'value' => $value > 0 ? $value : 1,
            'countryOfOrigin' => strtoupper(trim((string)($item['countryOfOrigin'] ?? $item['country_of_origin'] ?? 'CN'))),
            'hsTariff' => trim((string)($item['hsCode'] ?? $item['hs_code'] ?? '')),
        ];
    }
    if (!$out) {
        $out[] = [
            'description' => 'Goods',
            'quantity' => 1,
            'netWeight' => 0.001,
            'value' => 1,
            'countryOfOrigin' => 'CN',
            'hsTariff' => '',
        ];
    }
    return $out;
}

function packie_add_international_payload(array &$payload, array $order, ?array $details): void {
    if (!packie_is_international_order($order)) return;

    $details = is_array($details) ? $details : [];
    $items = is_array($details['items'] ?? null) ? $details['items'] : packie_default_international_items($order);
    $invoice_items = [];
    $consignment_details = [];
    $total_weight = 0.0;
    $total_value = 0.0;

    foreach ($items as $item) {
        if (!is_array($item)) continue;
        $qty = max(1, (int)($item['quantity'] ?? $item['qty'] ?? 1));
        $net_weight = (float)($item['netWeight'] ?? $item['net_weight'] ?? $item['weight'] ?? 0.001);
        $value = (float)($item['value'] ?? $item['price'] ?? 1);
        if ($net_weight <= 0) $net_weight = 0.001;
        if ($value <= 0) $value = 1;
        $origin = strtoupper(trim((string)($item['countryOfOrigin'] ?? $item['country_of_origin'] ?? 'CN')));
        $hs = trim((string)($item['hsTariff'] ?? $item['hsTariffNo'] ?? $item['hsCode'] ?? ''));
        $description = substr(trim((string)($item['description'] ?? $item['name'] ?? 'Goods')), 0, 50);

        $invoice_items[] = [
            'description' => $description !== '' ? $description : 'Goods',
            'qty' => $qty,
            'quantity' => $qty,
            'netWgt' => $net_weight,
            'netWeight' => $net_weight,
            'weight' => $net_weight,
            'value' => $value,
            'countryOfOrigin' => $origin !== '' ? $origin : 'CN',
            'country_of_origin' => $origin !== '' ? $origin : 'CN',
            'hsTariffNo' => $hs,
            'hsTariff' => $hs,
            'hsCode' => $hs,
        ];
        $consignment_details[] = [
            'description' => $description !== '' ? $description : 'Goods',
            'hsTarriff' => $hs,
            'hsTariff' => $hs,
            'netWeight' => (string)$net_weight,
            'originCountry' => $origin !== '' ? $origin : 'CN',
            'qty' => $qty,
            'valueCurrency' => $value,
        ];
        $total_weight += $net_weight * $qty;
        $total_value += $value * $qty;
    }

    $description_code = trim((string)($details['detailedDescription'] ?? '1'));
    $description_label = trim((string)($details['detailedDescriptionLabel'] ?? 'Sale of Goods'));
    $receiver_email = trim((string)($details['receiverEmail'] ?? $details['email'] ?? ''));
    $intl = [
        'detailedDescription' => $description_code,
        'detailedDescriptionLabel' => $description_label,
        'otherDescription' => substr(trim((string)($details['otherDescription'] ?? '')), 0, 50),
        'receiverTaxNo' => substr(trim((string)($details['receiverTaxNo'] ?? '')), 0, 50),
        'receiversTaxNo' => substr(trim((string)($details['receiverTaxNo'] ?? '')), 0, 50),
        'receiverEmail' => substr($receiver_email, 0, 120),
        'currency' => strtoupper(trim((string)($details['currency'] ?? 'NZD'))) ?: 'NZD',
        'reference' => substr(trim((string)($details['reference'] ?? ($order['orderNumber'] ?? ''))), 0, 50),
        'date' => trim((string)($details['date'] ?? date('Y-m-d'))),
        'invoiceDeclaration' => $invoice_items,
        'invoiceDeclarationItems' => $invoice_items,
        'items' => $invoice_items,
        'totalWeight' => $total_weight,
        'totalDeclaredAmount' => $total_value,
        'signatureRequired' => !empty($details['signatureRequired']),
    ];
    $intl_info = [
        'currency' => $intl['currency'],
        'descriptionType' => $description_code,
        'infoDate' => $intl['date'],
        'receiverTaxNo' => $intl['receiverTaxNo'],
        'reference' => $intl['reference'],
        'specify' => $intl['otherDescription'],
    ];

    $payload['isInternational'] = true;
    $payload['signatureRequired'] = !empty($details['signatureRequired']);
    $payload['internationalDetails'] = $intl;
    $payload['internationalConsignmentDetails'] = $consignment_details;
    $payload['internationalConsignmentInfo'] = $intl_info;
    $payload['internationalConsignmentInvoiceAndDetails'] = $intl;
    $payload['detailedDescription'] = $description_code;
    $payload['detailedDescriptionLabel'] = $description_label;
    $payload['otherDescription'] = $intl['otherDescription'];
    $payload['receiverTaxNo'] = $intl['receiverTaxNo'];
    $payload['receiversTaxNo'] = $intl['receiversTaxNo'];
    $payload['receiverEmail'] = $intl['receiverEmail'];
    $payload['currency'] = $intl['currency'];
    $payload['reference'] = $intl['reference'];
    $payload['date'] = $intl['date'];
    $payload['invoiceDeclaration'] = $invoice_items;
    $payload['invoiceDeclarationItems'] = $invoice_items;
    $payload['totalWeight'] = $total_weight;
    $payload['totalDeclaredAmount'] = $total_value;

    if (is_array($payload['consignmentParcels'] ?? null)) {
        packie_apply_international_parcel_defaults($payload);
    }
    if ($intl['receiverEmail'] !== '' && is_array($payload['receiver'] ?? null)) {
        $payload['receiver']['email'] = $intl['receiverEmail'];
    }
}

function packie_apply_international_parcel_defaults(array &$payload): void {
    if (empty($payload['isInternational']) || !is_array($payload['consignmentParcels'] ?? null)) return;
    $total_value = (float)($payload['totalDeclaredAmount'] ?? 0);
    foreach ($payload['consignmentParcels'] as &$parcel) {
        if (!is_array($parcel)) continue;
        if (!isset($parcel['internationalDeclaredValue']) || trim((string)$parcel['internationalDeclaredValue']) === '') {
            $parcel['internationalDeclaredValue'] = number_format($total_value > 0 ? $total_value : 1, 2, '.', '');
        }
        if (!isset($parcel['internationalGoodsDesc'])) {
            $parcel['internationalGoodsDesc'] = '';
        }
    }
    unset($parcel);
}

if ($action == 'print_label') {
    $order_id = $arg_oid;
    $staff    = $arg_staff;
    $stock_id = $arg_stock;
    $rate_id  = $arg_rate;
    $international_details = packie_decode_international_details($arg_international_details ?? '');

    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 pp_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]);
    }

    $print_success = false;
    $print_error = '';
    $provider_name = 'Packie';

    // 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_stock = function_exists('afri_resolve_packie_stock') ? afri_resolve_packie_stock($db, 'A4') : null;
        if ($fallback_stock) {
            $fallback_stock_id = (string)$fallback_stock['id'];
        } else {
            $fallback_q2 = $db->query("SELECT id FROM pp_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');
            $print_error = 'No courier stock/package mapping was available for label creation.';
        }
    }

    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'     => packie_is_international_order($packie_order),
                    '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,
                ];
                packie_add_international_payload($payload, $packie_order, $international_details);

                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,
                    ]];
                    packie_apply_international_parcel_defaults($payload);
                    $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;
                    $frontend_rate_id = trim((string)$rate_id);
                    $custom_rate_id   = ($frontend_rate_id !== '' && $frontend_rate_id !== 'none')
                        ? $frontend_rate_id
                        : ($_cr['id'] ?? $_cr['rateId'] ?? $_cr['rate_id'] ?? null);
                    $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]);
                    if (!$custom_rate_id && $print_error === '') {
                        $print_error = afri_extract_carrier_error($c_res_custom, (string)$c_raw_custom);
                    }
                } else {
                    $s = function_exists('afri_resolve_packie_stock') ? afri_resolve_packie_stock($db, $stock_id) : null;
                    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),
                        ]];
                        packie_apply_international_parcel_defaults($payload);
                        $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.
                        $preferred_carrier = trim((string)($s['quick_pick_carrier'] ?? ''));
                        $_rate_obj      = packie_select_rate_for_stock($c_res['result']['rates'] ?? [], $preferred_carrier);
                        $frontend_rate_id = trim((string)$rate_id);
                        $rate_id_final  = ($frontend_rate_id !== '' && $frontend_rate_id !== 'none')
                            ? $frontend_rate_id
                            : ($_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, 'resolved_stock' => $s['id'] ?? '', 'rate_id' => $rate_id_final ?? 'none', 'response' => $c_raw]);
                        if (!$rate_id_final && $print_error === '') {
                            $print_error = afri_extract_carrier_error($c_res, (string)$c_raw);
                        }
                    } else {
                        $url = null;
                        Logger::error('print_label', 'stock_not_found', ['stock' => $stock_id]);
                        $print_error = 'Selected courier package could not be found.';
                    }
                }

                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'] ?? '') : '';
                    if (($create_http >= 400 || $create_err !== '' || (is_array($create_decoded) && array_key_exists('success', $create_decoded) && empty($create_decoded['success']))) && $print_error === '') {
                        $print_error = afri_extract_carrier_error($create_decoded, (string)$create_raw, (string)$create_err);
                    }
                    $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);
                            if (($create_http_retry >= 400 || $create_err_retry !== '' || (is_array($retry_decoded) && array_key_exists('success', $retry_decoded) && empty($retry_decoded['success']))) && $print_error === '') {
                                $print_error = afri_extract_carrier_error($retry_decoded, (string)$create_raw_retry, (string)$create_err_retry);
                            }
                            $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 pp_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();
                        $print_success = true;
                        $print_error = '';
                        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]);
                        if ($print_error === '') {
                            $print_error = afri_extract_carrier_error($create_decoded, (string)$create_raw, (string)$create_err);
                        }
                    }
                } else {
                    Logger::warn('print_label', 'skipped_create empty_url', ['stock' => $stock_id, 'rate' => $rate_id]);
                    if ($print_error === '') $print_error = 'Courier did not return a usable rate for this package.';
                }
            } else {
                Logger::error('print_label', 'no_matching_packie_order', ['order' => $shopify_num]);
                $print_error = 'Order was not found in Packie.';
            }
        } else {
            Logger::error('print_label', 'missing_packie_headers');
            $print_error = 'Packie credentials are missing or invalid.';
        }
    } 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]);
        $print_error = 'No courier package or rate was selected for label creation.';
    }

    Logger::info('print_label', 'COMPLETE', ['order_id' => $order_id, 'shopify_num' => $shopify_num, 'success' => $print_success ? '1' : '0', 'error' => $print_error]);
    if ($print_success && $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);
    } elseif (!$print_success && function_exists('afri_handle_label_failure')) {
        $failure = afri_handle_label_failure($db, (string)$order_id, (string)($arg_order_name ?? ''), $shopify_num, $tags, $provider_name, $print_error);
        echo json_encode($failure) . "\n";
        exit;
    } else {
        Logger::info('print_label', 'skip_tags', ['ecom_meta' => $ecom_order_meta ? '1' : '0', 'fn_exists' => function_exists('ecommerce_update_order_tags') ? '1' : '0']);
    }
    echo json_encode([
        'success' => true,
        'order_id' => (string)$order_id,
        'order_name' => (string)($arg_order_name ?? ''),
        'shopify_num' => $shopify_num,
        'provider' => $provider_name,
    ]) . "\n";
    exit;
}

// ── check_status — read pp_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 pp_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') {
    if (function_exists('sync_packie_stocks')) {
        $res = $db->query("SELECT id FROM pp_packie_stocks LIMIT 1");
        if (!$res || $res->num_rows === 0) @sync_packie_stocks($db);
    }
    $stocks = function_exists('afri_list_packie_quick_stocks') ? afri_list_packie_quick_stocks($db) : (function_exists('afri_list_package_presets') ? afri_list_package_presets($db) : []);
    echo json_encode($stocks);
    exit;
}

// ── get_live_stocks — list full Packie stock catalogue for live pricing ───────
if ($action == 'get_live_stocks') {
    if (function_exists('sync_packie_stocks')) {
        $res = $db->query("SELECT id FROM pp_packie_stocks LIMIT 1");
        if (!$res || $res->num_rows === 0) @sync_packie_stocks($db);
    }
    $stocks = function_exists('afri_provider_stock_options') ? afri_provider_stock_options($db, 'packie') : [];
    echo json_encode($stocks);
    exit;
}

// ── get_custom_quote — calculate rates for a custom box ──────────────────────
function packie_quote_failure($decoded, string $raw = '', string $curl_error = '', string $fallback = 'Packie could not fetch live pricing.'): array {
    $message = function_exists('afri_extract_carrier_error')
        ? afri_extract_carrier_error($decoded, $raw, $curl_error)
        : '';
    $message = trim((string)$message);
    if ($message === '' || $message === 'Courier label creation failed.') {
        $message = $fallback;
    }
    return [
        'success' => false,
        'error' => $message,
        'message' => $message,
        'rates' => [],
    ];
}

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;
    $international_details = is_array($input['international_details'] ?? null) ? $input['international_details'] : null;
    $shopify_num = str_replace('#', '', $order_name);
    $headers     = get_packie_headers();

    if (!$headers) {
        echo json_encode(packie_quote_failure(null, '', '', 'Packie credentials are not configured.'));
        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'    => packie_is_international_order($po),
            'receiver'           => $po['receiver'],
            'sender'             => $po['sender'],
        ];
        packie_add_international_payload($payload, $po, $international_details);
        $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);
        $raw = curl_exec($ch);
        $curl_error = curl_error($ch);
        $http_code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $res = json_decode((string)$raw, true);
        curl_close($ch);
        if ($curl_error !== '' || $http_code >= 400 || !is_array($res) || ($res['success'] ?? true) === false) {
            echo json_encode(packie_quote_failure($res, (string)$raw, (string)$curl_error, 'Packie rejected the custom box quote.'));
            exit;
        }
        $rates = $res['result']['rates'] ?? [];
        if (is_array($rates)) {
            foreach ($rates as &$rate) {
                if (is_array($rate)) {
                    $rate['provider'] = 'packie';
                    $rate['providerDisplayName'] = 'Packie';
                }
            }
            unset($rate);
        }
        if (!is_array($rates) || count($rates) === 0) {
            echo json_encode(packie_quote_failure($res, (string)$raw, '', 'Packie returned no rates for this custom box.'));
            exit;
        }
        echo json_encode($rates);
    } else {
        echo json_encode(packie_quote_failure(null, '', '', "Packie order {$shopify_num} was not found for quoting."));
    }
    exit;
}

// ── address_autocomplete — proxy Packie domestic address search ───────────────
// Live rates for a saved Packie stock/bag. Used when Quick Pack is off.
if ($action == 'get_stock_quote') {
    $order_name = $_GET['order_name'] ?? '';
    $stock_id   = trim((string)($_GET['stock_id'] ?? ''));
    $mode       = strtolower(trim((string)($_GET['mode'] ?? 'preferred')));
    $provider   = strtolower(trim((string)($_GET['provider_slug'] ?? 'packie')));
    $international_details = is_array($input['international_details'] ?? null) ? $input['international_details'] : null;

    if ($mode !== 'all_enabled' && $provider !== '' && $provider !== 'packie') {
        echo json_encode([
            'success' => false,
            'error' => ucfirst($provider) . ' live pricing is not implemented yet. Packie live pricing is available now.',
        ]);
        exit;
    }

    $shopify_num = str_replace('#', '', $order_name);
    $headers     = get_packie_headers();
    if (!$headers || $stock_id === '') {
        echo json_encode(packie_quote_failure(null, '', '', $stock_id === '' ? 'Please select a stock first.' : 'Packie credentials are not configured.'));
        exit;
    }

    $stock = function_exists('afri_resolve_packie_stock') ? afri_resolve_packie_stock($db, $stock_id) : null;
    if (!$stock) {
        echo json_encode(packie_quote_failure(null, '', '', 'Selected Packie stock could not be found.'));
        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);

    $po = null;
    foreach ($search_res['result']['orders'] ?? [] as $_candidate) {
        if ((string)($_candidate['orderNumber'] ?? '') === $shopify_num) {
            $po = $_candidate;
            break;
        }
    }
    if ($po === null) {
        echo json_encode(packie_quote_failure(null, '', '', "Packie order {$shopify_num} was not found for quoting."));
        exit;
    }

    $payload = [
        'consignmentParcels' => [[
            'id'          => (string)$stock['id'],
            'height'      => (string)$stock['height'],
            'width'       => (string)$stock['width'],
            'length'      => (string)$stock['length'],
            'weight'      => (string)$stock['weight'],
            'quantity'    => '1',
            'serviceType' => (int)($stock['serviceType'] ?: 1),
        ]],
        'hasDangerousGoods'  => false,
        'integratedOrderId'  => $po['id'],
        'integrationType'    => 1,
        'isInternational'    => packie_is_international_order($po),
        'receiver'           => $po['receiver'],
        'sender'             => $po['sender'],
    ];
    packie_add_international_payload($payload, $po, $international_details);

    $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);
    $raw = curl_exec($ch);
    $curl_error = curl_error($ch);
    $http_code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $res = json_decode((string)$raw, true);
    curl_close($ch);
    if ($curl_error !== '' || $http_code >= 400 || !is_array($res) || ($res['success'] ?? true) === false) {
        echo json_encode(packie_quote_failure($res, (string)$raw, (string)$curl_error, 'Packie rejected the stock quote.'));
        exit;
    }

    $rates = $res['result']['rates'] ?? [];
    if (is_array($rates)) {
        foreach ($rates as &$rate) {
            if (is_array($rate)) {
                $rate['provider'] = 'packie';
                $rate['providerDisplayName'] = 'Packie';
                $rate['stockId'] = $stock_id;
            }
        }
        unset($rate);
    }

    if (!is_array($rates) || count($rates) === 0) {
        echo json_encode(packie_quote_failure($res, (string)$raw, '', 'Packie returned no rates for this stock.'));
        exit;
    }

    echo json_encode($rates);
    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']    ?? ''));
    $addr_contact = trim((string)($input['contactName'] ?? $input['contact_name'] ?? ''));
    $addr_company = trim((string)($input['company']  ?? ''));
    $addr_building = trim((string)($input['building'] ?? ''));
    $addr_email = trim((string)($input['email'] ?? ''));
    $addr_phone = trim((string)($input['phone'] ?? ''));
    // 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'] ?? '', [
                    'name'     => $addr_contact,
                    'company'  => $addr_company,
                    'address1' => $full_street,
                    'address2' => $addr_suburb,
                    'city'     => $addr_city,
                    'zip'      => $addr_zip,
                    'phone'    => $addr_phone,
                    'email'    => $addr_email,
                ]);
                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 pp_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]);
    }

    if ($addr_contact !== '') $ecom_name = $addr_contact;
    if (array_key_exists('email', $input)) $ecom_email = $addr_email;
    if (array_key_exists('phone', $input)) $ecom_phone = $addr_phone;

    // Step 3: Update Packie receiver
    if ($packie_order_id) {
        $p_payload   = [
            'contactName' => $ecom_name  ?: 'Customer',
            'company'     => $addr_company,
            'building'    => $addr_building,
            '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 pp_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 pp_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;
}

