<?php
/**
 * Zappy courier adapter.
 * Uses Zappy's browser-style form login and session cookies for internal API calls.
 */

if (!class_exists('Logger')) require_once __DIR__ . '/../logger.php';

function zappy_config(): array {
    global $pm;
    return $pm && method_exists($pm, 'getShipConfig') ? $pm->getShipConfig() : [];
}

function zappy_base_url(array $config): string {
    return rtrim((string)($config['base_url'] ?? 'https://ship.zappy.nz'), '/');
}

function zappy_request_json(string $method, string $path, array $payload = [], ?string $cookie_file = null): ?array {
    $config = zappy_config();
    $base = zappy_base_url($config);
    $url = $base . '/' . ltrim($path, '/');
    $referer_path = stripos($path, 'apiConsignment/') !== false
        ? '/Consignment/newSingleConsignment'
        : '/';

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
    curl_setopt($ch, CURLOPT_TIMEOUT, 20);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Accept: application/json, text/javascript, */*; q=0.01',
        'Accept-Language: en,en-NZ;q=0.9,en-US;q=0.8',
        'Content-Type: application/json; charset=UTF-8',
        'Origin: ' . $base,
        'Referer: ' . $base . $referer_path,
        'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome Safari/537.36',
        'X-Requested-With: XMLHttpRequest',
    ]);
    if ($cookie_file) {
        curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file);
        curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file);
    }
    if (strtoupper($method) === 'POST') {
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
    }

    $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);
    if (!is_array($decoded)) {
        Logger::warn('zappy.request', 'non_json_response', ['path' => $path, 'http' => $http, 'curl_error' => $err]);
        return null;
    }
    return $decoded;
}

function zappy_request_html(string $path, ?string $cookie_file = null): string {
    $config = zappy_config();
    $base = zappy_base_url($config);
    $url = $base . '/' . ltrim($path, '/');

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 20);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Referer: ' . $base . '/',
    ]);
    if ($cookie_file) {
        curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file);
        curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file);
    }
    $raw = curl_exec($ch);
    $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err = curl_error($ch);
    curl_close($ch);

    if ($http < 200 || $http >= 400 || !is_string($raw)) {
        Logger::warn('zappy.html', 'request_failed', ['path' => $path, 'http' => $http, 'curl_error' => $err]);
        return '';
    }
    return $raw;
}

function zappy_login(): array {
    $config = zappy_config();
    $base = zappy_base_url($config);
    $username = trim((string)($config['username'] ?? $config['UserName'] ?? $config['user_name'] ?? $config['shipping_username'] ?? $config['login'] ?? ''));
    $password = (string)($config['api_key'] ?? $config['password'] ?? $config['Password'] ?? $config['shipping_password'] ?? $config['shipping_api_key'] ?? $config['api_secret'] ?? '');

    if ($username === '' || $password === '') {
        Logger::warn('zappy.login', 'missing_credentials', ['keys' => array_keys($config), 'has_username' => $username !== '' ? '1' : '0', 'has_password' => $password !== '' ? '1' : '0']);
        return ['success' => false, 'error' => 'Zappy username/password are not configured. Save both Zappy Username and Zappy Password in courier settings.'];
    }

    $cookie_file = tempnam(sys_get_temp_dir(), 'zappy_cookie_');
    if (!$cookie_file) {
        return ['success' => false, 'error' => 'Could not create Zappy session cookie jar.'];
    }

    $login_url = $base . '/account/login?ReturnUrl=%2F';

    $get = curl_init($login_url);
    curl_setopt($get, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($get, CURLOPT_FOLLOWLOCATION, false);
    curl_setopt($get, CURLOPT_COOKIEJAR, $cookie_file);
    curl_setopt($get, CURLOPT_COOKIEFILE, $cookie_file);
    curl_setopt($get, CURLOPT_HTTPHEADER, [
        'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome Safari/537.36',
    ]);
    curl_setopt($get, CURLOPT_TIMEOUT, 15);
    $login_page = (string)curl_exec($get);
    curl_close($get);

    $verification_token = '';
    if (preg_match('/name="__RequestVerificationToken"[^>]*value="([^"]+)"/i', $login_page, $m)) {
        $verification_token = html_entity_decode($m[1], ENT_QUOTES | ENT_HTML5);
    }

    $login_fields = [
        'Kind' => '0',
        'UserName' => $username,
        'Password' => $password,
    ];
    if ($verification_token !== '') {
        $login_fields['__RequestVerificationToken'] = $verification_token;
    }

    $ch = curl_init($login_url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_fields));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HEADER, true);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
    curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file);
    curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Content-Type: application/x-www-form-urlencoded',
        'Origin: ' . $base,
        'Referer: ' . $login_url,
        'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome Safari/537.36',
    ]);
    curl_setopt($ch, CURLOPT_TIMEOUT, 20);
    $raw = curl_exec($ch);
    $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err = curl_error($ch);
    curl_close($ch);

    $cookie_contents = @file_get_contents($cookie_file) ?: '';
    $has_session = strpos($cookie_contents, 'ASP.NET_SessionId') !== false;
    $has_auth = stripos($cookie_contents, '.AspNet') !== false
        || stripos($cookie_contents, 'PayportAuthCookie') !== false
        || stripos($cookie_contents, 'Auth') !== false
        || stripos($cookie_contents, 'ApplicationCookie') !== false;

    if (($http < 200 || $http >= 400) && $http !== 302) {
        @unlink($cookie_file);
        Logger::warn('zappy.login', 'http_error', ['http' => $http, 'curl_error' => $err]);
        return ['success' => false, 'error' => 'Zappy login failed.'];
    }

    $body = (string)$raw;
    $looks_like_login_page = stripos($body, 'account/login') !== false
        || stripos($body, 'name="UserName"') !== false
        || stripos($body, 'name="Password"') !== false;
    if (!$has_auth && $looks_like_login_page) {
        @unlink($cookie_file);
        Logger::warn('zappy.login', 'not_authenticated', [
            'http' => $http,
            'has_session' => $has_session ? '1' : '0',
            'has_auth' => $has_auth ? '1' : '0',
            'has_verification_token' => $verification_token !== '' ? '1' : '0',
        ]);
        return ['success' => false, 'error' => 'Zappy rejected the login. Check the exact Zappy username and password.'];
    }

    return ['success' => true, 'cookie_file' => $cookie_file];
}

function zappy_attr_value(string $html, string $attr): string {
    if (preg_match('/\b' . preg_quote($attr, '/') . '\s*=\s*"([^"]*)"/i', $html, $m)) {
        return html_entity_decode($m[1], ENT_QUOTES | ENT_HTML5);
    }
    return '';
}

function zappy_first_match(string $html, string $pattern): string {
    if (preg_match($pattern, $html, $m)) {
        return html_entity_decode(strip_tags($m[1]), ENT_QUOTES | ENT_HTML5);
    }
    return '';
}

function zappy_first_raw(string $html, string $pattern): string {
    if (preg_match($pattern, $html, $m)) {
        return $m[1];
    }
    return '';
}

function zappy_numbers_from_html(string $html): array {
    $values = [];
    if (preg_match_all('/value\s*=\s*"(-?\d+(?:\.\d+)?)"/i', $html, $m)) {
        foreach ($m[1] as $v) $values[] = (float)$v;
    }
    if (preg_match_all('/>\s*(-?\d+(?:\.\d+)?)\s*</', $html, $m)) {
        foreach ($m[1] as $v) $values[] = (float)$v;
    }
    return $values;
}

function zappy_parse_custom_packages(string $html): array {
    $packages = [];
    if ($html === '') return $packages;

    if (!preg_match_all('/(<div class="customPackItem\b.*?)(?=<div class="customPackItem\b|<script>\s*localStorage\.removeItem\("customPackList"\))/is', $html, $blocks)) {
        return $packages;
    }

    foreach ($blocks[0] as $block) {
        $custom_id = zappy_attr_value($block, 'custom_pack_id');
        $pack_type = zappy_attr_value($block, 'pack_type');
        $name = trim(zappy_first_match($block, '/<span class="spanName"\s*>(.*?)<\/span>/is'));
        if ($name === '') $name = trim(zappy_attr_value(zappy_first_raw($block, '/(<input[^>]+class="inputName"[^>]*>)/is'), 'value'));
        if ($name === '') $name = 'Zappy package ' . $custom_id;

        $ref_tag = zappy_first_raw($block, '/(<input[^>]+class="inputRef"[^>]*>)/is');
        $ref = trim(zappy_attr_value($ref_tag, 'value'));
        $option_id = $ref !== '' ? $ref : $custom_id;
        if ($option_id === '') continue;

        $vol_html = zappy_first_raw($block, '/<div class="cp_volCalculator"[^>]*>(.*?)<div class="cp_volCalculator tube"/is');
        if ($vol_html === '') $vol_html = $block;
        $nums = zappy_numbers_from_html($vol_html);
        $length = (float)($nums[0] ?? 0);
        $width = (float)($nums[1] ?? 0);
        $height = (float)($nums[2] ?? 0);

        $weight_tag = zappy_first_raw($block, '/(<input[^>]+class="inputWeight"[^>]*>)/is');
        $weight = (float)(zappy_attr_value($weight_tag, 'value') ?: 0);

        if ($length <= 0 || $width <= 0 || $height <= 0) continue;

        $packages[] = [
            'id' => (string)$option_id,
            'label' => $name,
            'customPackageId' => $custom_id,
            'packType' => $pack_type,
            'reference' => $ref,
            'length' => $length,
            'width' => $width,
            'height' => $height,
            'weight' => $weight,
        ];
    }

    usort($packages, function ($a, $b) {
        return strcasecmp((string)$a['label'], (string)$b['label']);
    });
    return $packages;
}

function zappy_provider_stock_options(): array {
    $result = zappy_provider_stock_options_result();
    return $result['options'] ?? [];
}

function zappy_provider_stock_options_result(): array {
    $login = zappy_login();
    if (empty($login['success'])) {
        Logger::warn('zappy.packages', 'login_failed', ['error' => $login['error'] ?? 'unknown']);
        return [
            'success' => false,
            'error' => $login['error'] ?? 'Zappy login failed.',
            'options' => [],
        ];
    }
    $cookie_file = $login['cookie_file'];
    try {
        $html = zappy_request_html('CustomPackage', $cookie_file);
        if ($html === '') {
            return [
                'success' => false,
                'error' => 'Zappy CustomPackage page could not be loaded.',
                'options' => [],
            ];
        }
        $options = zappy_parse_custom_packages($html);
        if (count($options) === 0) {
            Logger::warn('zappy.packages', 'no_packages_parsed', [
                'html_length' => strlen($html),
                'looks_like_login' => stripos($html, 'account/login') !== false || stripos($html, 'name="UserName"') !== false ? '1' : '0',
                'has_custom_package_marker' => stripos($html, 'customPackItem') !== false ? '1' : '0',
            ]);
            return [
                'success' => false,
                'error' => 'Zappy logged in, but no package sizes were parsed from CustomPackage. Please open Zappy CustomPackage once in a browser, then try Force Update & Set Stocks again.',
                'options' => [],
            ];
        }
        return [
            'success' => true,
            'error' => '',
            'options' => $options,
        ];
    } finally {
        if ($cookie_file) @unlink($cookie_file);
    }
}

function zappy_package_mapping(mysqli $db, string $package_code): ?array {
    $code = strtoupper(preg_replace('/[^A-Z0-9]/', '', $package_code));
    if ($code === '') return null;

    $stmt = $db->prepare("
        SELECT provider_stock_id, provider_label, length, width, height, weight
        FROM pp_provider_package_map
        WHERE provider_slug = 'zappy' AND package_code = ? AND is_enabled = 1
        LIMIT 1
    ");
    $stmt->bind_param('s', $code);
    $stmt->execute();
    $row = $stmt->get_result()->fetch_assoc();
    return $row ?: null;
}

function zappy_find_order(array $orders, string $shopify_num): ?array {
    foreach ($orders as $order) {
        $haystack = json_encode($order);
        if ($shopify_num === '' || ($haystack !== false && strpos($haystack, $shopify_num) !== false)) {
            return is_array($order) ? $order : null;
        }
    }
    return null;
}

function zappy_order_value(array $order, array $keys, string $default = ''): string {
    foreach ($keys as $key) {
        if (isset($order[$key]) && $order[$key] !== null && $order[$key] !== '') return (string)$order[$key];
    }
    return $default;
}

function zappy_first_non_empty(...$values): string {
    foreach ($values as $value) {
        $text = trim((string)($value ?? ''));
        if ($text !== '') return $text;
    }
    return '';
}

function zappy_third_party_order_id(array $order): string {
    foreach (['third_party_orders_id', 'third_party_order_id', 'third_party_order_ids', 'third_party_id', 'id', 'order_id'] as $key) {
        if (isset($order[$key]) && trim((string)$order[$key]) !== '') return trim((string)$order[$key]);
    }
    return '';
}

function zappy_delivery_address(array $order): array {
    return [
        'address_body' => zappy_order_value($order, ['address_body', 'address_1', 'address1', 'delivery_address', 'recipient_address']),
        'address_id' => 0,
        'address_building' => '',
        'address_unit' => zappy_order_value($order, ['address_unit', 'address_2', 'address2', 'unit']),
        'address_city' => zappy_order_value($order, ['address_city', 'city', 'recipient_city']),
        'address_country' => zappy_order_value($order, ['address_country', 'country', 'recipient_country'], 'NZ'),
        'address_google_lat' => 0,
        'address_google_lng' => 0,
        'address_name' => '',
        'address_name_display' => '',
        'address_postcode' => zappy_order_value($order, ['address_postcode', 'postcode', 'post_code', 'zip', 'recipient_postcode']),
        'address_state' => zappy_order_value($order, ['address_state', 'state', 'recipient_state']),
        'address_suburb' => zappy_order_value($order, ['address_suburb', 'suburb', 'recipient_suburb', 'city']),
        'address_zone' => '',
        'company_name' => zappy_order_value($order, ['company_name', 'company']),
        'contact_name' => zappy_order_value($order, ['contact_name', 'recipient_name', 'name', 'customer_name']),
        'email' => zappy_order_value($order, ['email', 'recipient_email']),
        'instruction' => zappy_order_value($order, ['instruction', 'recipient_instruction', 'delivery_instruction']),
        'is_commercial' => '0',
        'is_residential' => '1',
        'is_rural' => '0',
        'kind' => 1,
        'phone' => zappy_order_value($order, ['phone', 'recipient_phone']),
        'valid_address' => '',
        'vat_number' => '',
        'vat_number_type' => '',
        'ConsignmentAddressType' => 'recipient',
        'NZPost_address_id' => '',
        'Town' => null,
    ];
}

function zappy_pickup_address(array $sender): array {
    $config = zappy_config();
    $street = zappy_first_non_empty($sender['streetAddress'] ?? '', $config['sender_street'] ?? '', $config['pickup_street'] ?? '', '247 Cuba Street');
    $city = zappy_first_non_empty($sender['city'] ?? '', $config['sender_city'] ?? '', $config['pickup_city'] ?? '', 'Palmerston North');
    $suburb = zappy_first_non_empty($sender['suburb'] ?? '', $config['sender_suburb'] ?? '', $config['pickup_suburb'] ?? '', $city);
    $postcode = zappy_first_non_empty($sender['postCode'] ?? '', $config['sender_postcode'] ?? '', $config['pickup_postcode'] ?? '', '4410');
    $company = zappy_first_non_empty($sender['company'] ?? '', $config['sender_company'] ?? '', $config['pickup_company'] ?? '', 'Afrihair');
    $contact = zappy_first_non_empty($sender['contactName'] ?? '', $config['sender_contact_name'] ?? '', $config['pickup_contact_name'] ?? '', $company);
    $email = zappy_first_non_empty($sender['email'] ?? '', $config['sender_email'] ?? '', $config['pickup_email'] ?? '');
    $phone = zappy_first_non_empty($sender['phone'] ?? '', $config['sender_phone'] ?? '', $config['pickup_phone'] ?? '');
    $address_id = zappy_first_non_empty($sender['addressId'] ?? '', $config['sender_address_id'] ?? '', $config['pickup_address_id'] ?? '');

    return [
        'address_body' => $street,
        'address_id' => $address_id,
        'address_building' => '',
        'address_unit' => '',
        'address_city' => $city,
        'address_country' => (string)($sender['country'] ?? 'NZ'),
        'address_google_lat' => '0.000000000000',
        'address_google_lng' => '0.000000000000',
        'address_name' => $company,
        'address_name_display' => '',
        'address_postcode' => $postcode,
        'address_state' => '',
        'address_suburb' => $suburb,
        'address_zone' => '',
        'company_name' => $company,
        'contact_name' => $contact,
        'email' => $email,
        'instruction' => '',
        'is_commercial' => '0',
        'is_residential' => '1',
        'is_rural' => '0',
        'kind' => 0,
        'phone' => $phone,
        'valid_address' => '',
        'vat_number' => '',
        'vat_number_type' => '',
        'ConsignmentAddressType' => 'sender',
        'NZPost_address_id' => '',
        'Town' => null,
    ];
}

function zappy_quote_id(array $quote, string $quote_request_id): string {
    return base64_encode(json_encode([
        'provider' => 'zappy',
        'quoteRequestId' => $quote_request_id,
        'carrier_id' => $quote['carrier_id'] ?? '',
        'carrier_method_code' => $quote['carrier_method_code'] ?? '',
        'carrier_method_id' => $quote['carrier_method_id'] ?? '',
        'carrier_account' => $quote['carrier_account'] ?? '',
    ]));
}

function zappy_decode_quote_id(string $rate_id): ?array {
    $rate_id = trim($rate_id);
    if ($rate_id === '' || $rate_id === 'none') return null;

    $decoded = base64_decode($rate_id, true);
    if ($decoded === false) return null;

    $data = json_decode($decoded, true);
    if (!is_array($data) || strtolower((string)($data['provider'] ?? '')) !== 'zappy') return null;

    return $data;
}

function zappy_quote_display_name(array $quote): string {
    $carrier_id = strtolower(trim((string)($quote['carrier_id'] ?? '')));
    $method_code = strtoupper(trim((string)($quote['carrier_method_code'] ?? $quote['carrier_method_id'] ?? '')));
    $carrier_account = trim((string)($quote['carrier_account'] ?? ''));

    if ($carrier_id === 'ft' || stripos($method_code, 'FT_') === 0 || strcasecmp($carrier_account, 'Parcelport') === 0) {
        return 'Aramex';
    }

    if ($carrier_id === 'ncp' || strpos($method_code, 'CPOL') === 0) {
        $suffix = '';
        if (preg_match('/CPOLTP([A-Z0-9]+)$/', $method_code, $m)) {
            $suffix = $m[1];
        } elseif (preg_match('/(A[0-9]|DLE|XL|LF|FS)$/', $method_code, $m)) {
            $suffix = $m[1];
        }
        return $suffix !== '' ? 'NZPost ' . $suffix : 'NZPost';
    }

    return trim((string)($quote['productDesc'] ?? $method_code ?? 'Zappy'));
}

function zappy_print_label_url(string $encoded_ref): string {
    $encoded_ref = trim($encoded_ref);
    if ($encoded_ref === '') return '';

    $config = zappy_config();
    return zappy_base_url($config) . '/Consignment/PrintLabel?REF=' . rawurlencode($encoded_ref) . '#';
}

function zappy_direct_label_pdf_url(array $selected_quote, array $refs, array $label_refs): string {
    $config = zappy_config();
    $base = zappy_base_url($config);
    $carrier_id = strtolower(trim((string)($selected_quote['carrier_id'] ?? '')));
    $method_code = strtoupper(trim((string)($selected_quote['carrier_method_code'] ?? $selected_quote['carrier_method_id'] ?? '')));
    $carrier_account = trim((string)($selected_quote['carrier_account'] ?? ''));

    if ($carrier_id === 'ft' || stripos($method_code, 'FT_') === 0 || strcasecmp($carrier_account, 'Parcelport') === 0) {
        $ref = preg_replace('/[^A-Za-z0-9_-]/', '', (string)($refs[0] ?? ''));
        return $ref !== '' ? $base . '/Files/Downloads/Fastway/' . rawurlencode($ref) . '.pdf' : '';
    }

    if ($carrier_id === 'ncp' || strpos($method_code, 'CPOL') === 0) {
        $label_ref = preg_replace('/[^A-Za-z0-9_-]/', '', (string)($label_refs[0] ?? ''));
        if ($label_ref === '') return '';
        $filename = preg_match('/\.pdf$/i', $label_ref) ? $label_ref : $label_ref . '_1.pdf';
        return $base . '/Files/Downloads/NZPost/' . rawurlencode($filename);
    }

    return '';
}

function zappy_build_consignment_quote_payload(mysqli $db, $pm, string $order_name, string $stock_id, string $cookie_file): array {
    $config = zappy_config();
    $client_id = (int)($config['client_id'] ?? $config['clientId'] ?? $config['ClientId'] ?? $config['zappy_client_id'] ?? 2879);
    if ($client_id <= 0) {
        return ['success' => false, 'error' => 'Zappy Client ID is required in courier settings.'];
    }

    $shopify_num = preg_replace('/\D+/', '', $order_name);
    $package = function_exists('afri_resolve_package_preset') ? afri_resolve_package_preset($db, $stock_id) : null;
    if (!$package) {
        return ['success' => false, 'error' => 'Selected bag size was not found.'];
    }

    $package_map = zappy_package_mapping($db, (string)($package['code'] ?? $stock_id));
    if ($package_map) {
        $package['length'] = (float)($package_map['length'] ?? $package['length']);
        $package['width'] = (float)($package_map['width'] ?? $package['width']);
        $package['height'] = (float)($package_map['height'] ?? $package['height']);
        $package['weight'] = (float)($package_map['weight'] ?? $package['weight']);
    }

    $orders_payload = [
        'sync' => '0',
        'orderType' => '0',
        'selectedECommerce' => '',
        'dateFrom' => date('d/m/Y', strtotime('-14 days')),
        'dateTo' => '',
        'keyword' => $shopify_num,
        'page' => 1,
        'pageSize' => 50,
        'recipientAddressArea' => '',
        'recipientAddressCountry' => '',
        'searchArea' => '1',
        'selectedThirdParty' => '',
        'sortBy' => '1',
    ];
    $orders_res = zappy_request_json('POST', 'internalApi/apiConsignment/GetThirdPartyOrders/', $orders_payload, $cookie_file);
    $orders = is_array($orders_res['result'] ?? null) ? $orders_res['result'] : [];
    $order = zappy_find_order($orders, $shopify_num);
    if (!$order) {
        Logger::warn('zappy.consignment', 'order_not_found', [
            'order_name' => $order_name,
            'shopify_num' => $shopify_num,
            'orders_count' => count($orders),
            'response_success' => !empty($orders_res['success']) ? '1' : '0',
        ]);
        return ['success' => false, 'error' => 'Zappy order was not found for ' . $order_name . '.'];
    }

    $third_party_order_id = zappy_third_party_order_id($order);
    if ($third_party_order_id === '') {
        return [
            'success' => false,
            'error' => 'Zappy order ID field was not found. Please capture one full GetThirdPartyOrders result row.',
            'keys' => array_keys($order),
        ];
    }

    $sender = $pm ? $pm->senderDetails() : [];
    $volume = ((float)$package['length'] * (float)$package['width'] * (float)$package['height']) / 1000000;
    $group_id = trim((string)($package_map['provider_stock_id'] ?? ''));
    if ($group_id === '') $group_id = strtolower((string)($package['code'] ?? $stock_id));
    $pack_name = trim((string)($package_map['provider_label'] ?? ''));
    if ($pack_name === '') $pack_name = (string)($package['name'] ?? $package['code'] ?? $stock_id);

    $parcel = [
        'row' => 1,
        'length' => (string)$package['length'],
        'height' => (string)$package['height'],
        'width' => (string)$package['width'],
        'volume' => number_format($volume, 3, '.', ''),
        'weight' => number_format((float)$package['weight'], 2, '.', ''),
        'kind' => '1',
        'group_id' => $group_id,
        'cust_ref' => trim($order_name) . ' ',
        'pack_name' => $pack_name,
        'insurance_required' => false,
        'insured_value_amount' => '',
        'currency' => 'NZD',
        'document_flag' => '0',
        'content_number' => '11',
        'parcel_contents' => [],
        'is_tube' => '0',
    ];

    return [
        'success' => true,
        'payload' => [
            'clientId' => $client_id,
            'consignmentVM' => [
                'Booking' => ['pickup_option' => '1', 'PickupDate' => '', 'IsDriverPickup' => true],
                'DeliveryAddress' => zappy_delivery_address($order),
                'EmailLabel' => '0',
                'IncludeAddOns' => [],
                'IsReturnLabel' => false,
                'PickupAddress' => zappy_pickup_address($sender),
                'RemovedAddOns' => [],
                'allow_return' => 0,
                'authority_to_leave' => '1',
                'batchOrderId' => 0,
                'carrier_id' => '',
                'carrier_method_code' => '',
                'carrier_method_id' => '',
                'carrier_account' => '',
                'carrierAccountType' => '',
                'channel' => 0,
                'consignment_batch_id' => 0,
                'consignment_local_id' => 0,
                'hasCourierPost' => true,
                'includeReturnAuto' => '0',
                'is_DG' => '0',
                'is_paperless' => '1',
                'is_r18' => '0',
                'is_saturday' => '0',
                'is_signature' => '0',
                'no_atl' => '0',
                'parcels' => [$parcel],
                'photo_required' => '0',
                'preferred_carrier_id' => '',
                'quoteRequestId' => null,
                'returnLabel_is_signature' => '0',
                'seeUrgentCarrier' => 0,
                'shipping_rule_result_id' => 0,
                'third_party_orders' => [$order],
                'third_party_orders_ids' => $third_party_order_id,
                'type' => 'Single',
                'valid_address' => '',
            ],
        ],
        'order' => $order,
        'parcel' => $parcel,
        'package' => $package,
    ];
}

if ($action == 'get_stock_quote') {
    global $db, $pm;
    $config = zappy_config();
    $client_id = (int)($config['client_id'] ?? $config['clientId'] ?? $config['ClientId'] ?? $config['zappy_client_id'] ?? 2879);
    if ($client_id <= 0) {
        echo json_encode(['success' => false, 'error' => 'Zappy Client ID is required in courier settings.']);
        exit;
    }

    $login = zappy_login();
    if (empty($login['success'])) {
        echo json_encode(['success' => false, 'error' => $login['error'] ?? 'Zappy login failed.']);
        exit;
    }
    $cookie_file = $login['cookie_file'];

    try {
        $order_name = $_GET['order_name'] ?? '';
        $stock_id = trim((string)($_GET['stock_id'] ?? ''));
        $shopify_num = preg_replace('/\D+/', '', (string)$order_name);

        $package = function_exists('afri_resolve_package_preset') ? afri_resolve_package_preset($db, $stock_id) : null;
        if (!$package) {
            echo json_encode(['success' => false, 'error' => 'Selected bag size was not found.']);
            exit;
        }
        $package_map = zappy_package_mapping($db, (string)($package['code'] ?? $stock_id));
        if ($package_map) {
            $package['length'] = (float)($package_map['length'] ?? $package['length']);
            $package['width'] = (float)($package_map['width'] ?? $package['width']);
            $package['height'] = (float)($package_map['height'] ?? $package['height']);
            $package['weight'] = (float)($package_map['weight'] ?? $package['weight']);
        }

        $orders_payload = [
            'sync' => '0',
            'orderType' => '0',
            'selectedECommerce' => '',
            'dateFrom' => date('d/m/Y', strtotime('-14 days')),
            'dateTo' => '',
            'keyword' => $shopify_num,
            'page' => 1,
            'pageSize' => 50,
            'recipientAddressArea' => '',
            'recipientAddressCountry' => '',
            'searchArea' => '1',
            'selectedThirdParty' => '',
            'sortBy' => '1',
        ];
        $orders_res = zappy_request_json('POST', 'internalApi/apiConsignment/GetThirdPartyOrders/', $orders_payload, $cookie_file);
        $orders = is_array($orders_res['result'] ?? null) ? $orders_res['result'] : [];
        $order = zappy_find_order($orders, $shopify_num);
        if (!$order) {
            Logger::warn('zappy.quote', 'order_not_found', [
                'order_name' => $order_name,
                'shopify_num' => $shopify_num,
                'orders_count' => count($orders),
                'response_success' => !empty($orders_res['success']) ? '1' : '0',
            ]);
            echo json_encode(['success' => false, 'error' => 'Zappy order was not found for ' . $order_name . '.']);
            exit;
        }

        $third_party_order_id = zappy_third_party_order_id($order);
        if ($third_party_order_id === '') {
            echo json_encode([
                'success' => false,
                'error' => 'Zappy order ID field was not found. Please capture one full GetThirdPartyOrders result row.',
                'keys' => array_keys($order),
            ]);
            exit;
        }

        $sender = $pm ? $pm->senderDetails() : [];
        $volume = ((float)$package['length'] * (float)$package['width'] * (float)$package['height']) / 1000000;
        $group_id = trim((string)($package_map['provider_stock_id'] ?? ''));
        if ($group_id === '') $group_id = strtolower((string)($package['code'] ?? $stock_id));
        $pack_name = trim((string)($package_map['provider_label'] ?? ''));
        if ($pack_name === '') $pack_name = (string)($package['name'] ?? $package['code'] ?? $stock_id);
        $parcel = [
            'row' => 1,
            'length' => (string)$package['length'],
            'height' => (string)$package['height'],
            'width' => (string)$package['width'],
            'volume' => number_format($volume, 3, '.', ''),
            'weight' => number_format((float)$package['weight'], 2, '.', ''),
            'kind' => '1',
            'group_id' => $group_id,
            'cust_ref' => trim((string)$order_name) . ' ',
            'pack_name' => $pack_name,
            'insurance_required' => false,
            'insured_value_amount' => '',
            'currency' => 'NZD',
            'document_flag' => '0',
            'content_number' => '11',
            'parcel_contents' => [],
            'is_tube' => '0',
        ];

        $quote_payload = [
            'clientId' => $client_id,
            'consignmentVM' => [
                'Booking' => ['pickup_option' => '1', 'PickupDate' => '', 'IsDriverPickup' => true],
                'DeliveryAddress' => zappy_delivery_address($order),
                'EmailLabel' => '0',
                'IncludeAddOns' => [],
                'IsReturnLabel' => false,
                'PickupAddress' => zappy_pickup_address($sender),
                'RemovedAddOns' => [],
                'allow_return' => 0,
                'authority_to_leave' => '1',
                'batchOrderId' => 0,
                'carrier_id' => '',
                'carrier_method_code' => '',
                'carrier_method_id' => '',
                'carrier_account' => '',
                'carrierAccountType' => '',
                'channel' => 0,
                'consignment_batch_id' => 0,
                'consignment_local_id' => 0,
                'hasCourierPost' => true,
                'includeReturnAuto' => '0',
                'is_DG' => '0',
                'is_paperless' => '1',
                'is_r18' => '0',
                'is_saturday' => '0',
                'is_signature' => '0',
                'no_atl' => '0',
                'parcels' => [$parcel],
                'photo_required' => '0',
                'preferred_carrier_id' => '',
                'quoteRequestId' => null,
                'returnLabel_is_signature' => '0',
                'seeUrgentCarrier' => 0,
                'shipping_rule_result_id' => 0,
                'third_party_orders' => [$order],
                'third_party_orders_ids' => $third_party_order_id,
                'type' => 'Single',
                'valid_address' => '',
            ],
        ];

        $quote_res = zappy_request_json('POST', 'internalApi/apiConsignment/GetQuoteRequest/', $quote_payload, $cookie_file);
        if (empty($quote_res['success'])) {
            Logger::warn('zappy.quote', 'quote_failed', [
                'order_name' => $order_name,
                'stock_id' => $stock_id,
                'message' => $quote_res['errormessage'] ?? 'unknown',
                'errors' => $quote_res['errors'] ?? null,
                'delivery' => $quote_payload['consignmentVM']['DeliveryAddress'] ?? null,
                'pickup' => $quote_payload['consignmentVM']['PickupAddress'] ?? null,
                'parcel' => $parcel,
            ]);
            echo json_encode(['success' => false, 'error' => $quote_res['errormessage'] ?? 'Zappy quote request failed.']);
            exit;
        }

        $quote_request_id = (string)($quote_res['quoteRequestId'] ?? '');
        $quotes = is_array($quote_res['quotes'] ?? null) ? $quote_res['quotes'] : [];
        $rates = [];
        foreach ($quotes as $quote) {
            if (empty($quote['success'])) continue;
            $name = zappy_quote_display_name($quote);
            $rates[] = [
                'id' => zappy_quote_id($quote, $quote_request_id),
                'name' => $name,
                'provider' => 'zappy',
                'providerDisplayName' => 'Zappy',
                'rate' => $quote['price_total'] ?? $quote['price'] ?? 0,
                'price' => $quote['price_total'] ?? $quote['price'] ?? 0,
                'raw' => $quote,
            ];
        }

        if (empty($rates)) {
            Logger::warn('zappy.quote', 'no_successful_quotes', [
                'order_name' => $order_name,
                'stock_id' => $stock_id,
                'quote_count' => count($quotes),
                'quotes' => $quotes,
            ]);
            echo json_encode(['success' => false, 'error' => 'Zappy returned no successful quotes for this parcel.']);
            exit;
        }

        echo json_encode($rates);
        exit;
    } finally {
        if ($cookie_file) @unlink($cookie_file);
    }
}

if ($action == 'get_stocks') {
    $stocks = function_exists('afri_list_package_presets') ? afri_list_package_presets($db) : [];
    echo json_encode($stocks);
    exit;
}

if ($action == 'print_label') {
    global $db, $pm, $arg_oid, $arg_staff, $arg_stock, $arg_rate, $arg_order_name;

    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 !== '' && file_exists($_ecom_file)) include $_ecom_file;
    }

    $order_id = (string)($arg_oid ?? '');
    $order_name = (string)($arg_order_name ?? '');
    $shopify_num = preg_replace('/\D+/', '', $order_name);
    $tags = '';
    if (function_exists('ecommerce_get_order_meta') && $order_id !== '') {
        $meta = ecommerce_get_order_meta($order_id);
        if (is_array($meta)) {
            $shopify_num = preg_replace('/\D+/', '', (string)($meta['name'] ?? $order_name));
            $tags = (string)($meta['tags'] ?? '');
        }
    }

    $zappy_fail = function (string $message) use ($db, $order_id, $order_name, $shopify_num, $tags) {
        if (function_exists('afri_handle_label_failure')) {
            echo json_encode(afri_handle_label_failure($db, $order_id, $order_name, $shopify_num, $tags, 'Zappy', $message)) . "\n";
        } else {
            echo json_encode(['success' => false, 'error' => $message]) . "\n";
        }
        exit;
    };

    $selected_quote = zappy_decode_quote_id((string)($arg_rate ?? ''));
    if (!$selected_quote) {
        Logger::warn('zappy.print_label', 'missing_or_invalid_quote', ['rate' => $arg_rate ?? 'none', 'order' => $arg_order_name ?? '']);
        $zappy_fail('Selected Zappy quote was missing.');
    }

    foreach (['quoteRequestId', 'carrier_id', 'carrier_method_code', 'carrier_method_id', 'carrier_account'] as $required_key) {
        if (trim((string)($selected_quote[$required_key] ?? '')) === '') {
            Logger::warn('zappy.print_label', 'quote_missing_field', ['field' => $required_key, 'quote' => $selected_quote]);
            $zappy_fail('Selected quote is missing ' . $required_key . '.');
        }
    }

    $login = zappy_login();
    if (empty($login['success'])) {
        Logger::warn('zappy.print_label', 'login_failed', ['error' => $login['error'] ?? 'unknown']);
        $zappy_fail($login['error'] ?? 'Zappy login failed.');
    }

    $cookie_file = $login['cookie_file'];
    try {
        $context = zappy_build_consignment_quote_payload(
            $db,
            $pm,
            (string)($arg_order_name ?? ''),
            (string)($arg_stock ?? 'none'),
            $cookie_file
        );
        if (empty($context['success'])) {
            Logger::warn('zappy.print_label', 'payload_build_failed', [
                'order' => $arg_order_name ?? '',
                'stock' => $arg_stock ?? 'none',
                'error' => $context['error'] ?? 'unknown',
            ]);
            $zappy_fail($context['error'] ?? 'Could not build consignment payload.');
        }

        $consignment_vm = $context['payload']['consignmentVM'];
        $consignment_vm['Booking']['pickup_option'] = '0';
        $consignment_vm['carrier_id'] = (string)$selected_quote['carrier_id'];
        $consignment_vm['carrier_method_id'] = (string)$selected_quote['carrier_method_id'];
        $consignment_vm['carrier_account'] = (string)$selected_quote['carrier_account'];
        $consignment_vm['quoteRequestId'] = (string)$selected_quote['quoteRequestId'];
        $consignment_vm['carrier_method_code'] = (string)$selected_quote['carrier_method_code'];
        $consignment_vm['allow_return'] = '0';
        $consignment_vm['is_paperless'] = null;
        $consignment_vm['customs_code'] = '';
        $consignment_vm['customs_code_type'] = 'VAT';

        $save_payload = ['consignmentVM' => $consignment_vm];
        $save_res = zappy_request_json('POST', 'internalApi/apiConsignment/SaveSingleConsignment/', $save_payload, $cookie_file);
        if (empty($save_res['success'])) {
            Logger::warn('zappy.print_label', 'save_failed', [
                'order_id' => $arg_oid ?? '',
                'order' => $arg_order_name ?? '',
                'stock' => $arg_stock ?? 'none',
                'message' => $save_res['message'] ?? $save_res['errormessage'] ?? 'unknown',
                'errors' => $save_res['errorMsgs'] ?? $save_res['errors'] ?? null,
                'quote' => $selected_quote,
                'parcel' => $context['parcel'] ?? null,
            ]);
            $zappy_fail(afri_extract_carrier_error($save_res, json_encode($save_res)));
        }

        $content = is_array($save_res['content'] ?? null) ? $save_res['content'] : [];
        $local_ids = is_array($content['consignmentLocalIDs'] ?? null) ? $content['consignmentLocalIDs'] : [];
        $refs = is_array($content['consignmentRefs'] ?? null) ? $content['consignmentRefs'] : [];
        $tracking_refs = is_array($content['consignmentTrackinglRefs'] ?? null) ? $content['consignmentTrackinglRefs'] : [];
        $label_refs = is_array($content['consignmentLabelRefs'] ?? null) ? $content['consignmentLabelRefs'] : [];
        $encoded_refs = is_array($content['consignmentRefEncode'] ?? null) ? $content['consignmentRefEncode'] : [];
        $label_link = trim((string)($content['LabelDownloadLink'] ?? ''));
        if ($label_link === '') {
            $label_link = zappy_direct_label_pdf_url($selected_quote, $refs, $label_refs);
        }
        if ($label_link === '') {
            $label_link = zappy_print_label_url((string)($encoded_refs[0] ?? ''));
        }

        Logger::info('zappy.print_label', 'save_success', [
            'order_id' => $arg_oid ?? '',
            'order' => $arg_order_name ?? '',
            'stock' => $arg_stock ?? 'none',
            'local_ids' => $local_ids,
            'refs' => $refs,
            'encoded_refs' => $encoded_refs,
            'tracking_refs' => $tracking_refs,
            'label_refs' => $label_refs,
            'total_bill' => $content['totalBill'] ?? null,
            'label_link' => $label_link,
        ]);

        if ($label_link !== '') {
            if ($shopify_num !== '') {
                $stmt = $db->prepare("UPDATE pp_packie_cache SET label_pdf_url = ? WHERE shopify_order_number = ?");
                $stmt->bind_param('ss', $label_link, $shopify_num);
                $stmt->execute();
            }
        }

        if (function_exists('ecommerce_update_order_tags') && $order_id !== '') {
            $updated_tags = $tags ? "$tags, Packed, Packed by " . (string)($arg_staff ?? 'Staff') : "Packed, Packed by " . (string)($arg_staff ?? 'Staff');
            ecommerce_update_order_tags($order_id, $updated_tags);
        }

        $tracking_text = trim((string)($tracking_refs[0] ?? ''));
        echo json_encode([
            'success' => true,
            'order_id' => $order_id,
            'order_name' => $order_name,
            'shopify_num' => $shopify_num,
            'provider' => 'Zappy',
            'tracking' => $tracking_text,
            'label_pdf_url' => $label_link,
        ]) . "\n";
    } finally {
        if ($cookie_file) @unlink($cookie_file);
    }
    exit;
}
