<?php
require_once __DIR__ . '/config.php';

$limit = 10;

$mysqli = db();
$shopifyAccessToken = null;
$results = array();

$sql = "
SELECT
    om.id AS order_match_id,
    om.shopify_order_row_id,
    om.share_link,
    so.shopify_order_id,
    so.shopify_name,
    lo.lalamove_order_id
FROM order_matches om
INNER JOIN shopify_orders so ON so.id = om.shopify_order_row_id
INNER JOIN lalamove_orders lo ON lo.id = om.lalamove_order_row_id
WHERE om.match_decision IN ('auto_matched', 'manual_matched')
  AND om.tracking_sent = 0
  AND om.share_link IS NOT NULL
  AND om.share_link <> ''
ORDER BY om.matched_at ASC
LIMIT ?
";

$stmt = $mysqli->prepare($sql);
if (!$stmt) {
    json_response(array(
        'ok' => false,
        'error' => 'Failed to prepare database query.'
    ), 500);
}

$stmt->bind_param('i', $limit);
$stmt->execute();
$result = $stmt->get_result();

while ($row = $result->fetch_assoc()) {
    $shopifyOrderId = trim((string)(isset($row['shopify_order_id']) ? $row['shopify_order_id'] : ''));
    $shopifyName = trim((string)(isset($row['shopify_name']) ? $row['shopify_name'] : ''));
    $trackingUrl = trim((string)(isset($row['share_link']) ? $row['share_link'] : ''));
    $trackingNumber = trim((string)(isset($row['lalamove_order_id']) ? $row['lalamove_order_id'] : ''));

    if ($trackingUrl === '' || $trackingNumber === '') {
        save_tracking_attempt($row, 'failed', null, null, null, 'Missing tracking URL or tracking number');
        $results[] = array(
            'shopify_order_id' => $shopifyOrderId,
            'shopify_name' => $shopifyName,
            'ok' => false,
            'step' => 'validation'
        );
        continue;
    }

    if ($shopifyAccessToken === null) {
        $tokenResp = shopify_get_access_token(false);
        if (empty($tokenResp['ok'])) {
            save_tracking_attempt(
                $row,
                'failed',
                isset($tokenResp['http_status']) ? $tokenResp['http_status'] : null,
                null,
                isset($tokenResp['raw']) ? $tokenResp['raw'] : null,
                'Failed to obtain Shopify access token'
            );
            $results[] = array(
                'shopify_order_id' => $shopifyOrderId,
                'shopify_name' => $shopifyName,
                'ok' => false,
                'step' => 'get_access_token',
                'error' => isset($tokenResp['error']) ? $tokenResp['error'] : 'Unable to get Shopify access token'
            );
            continue;
        }

        $shopifyAccessToken = $tokenResp['access_token'];
    }

    $lookupResult = shopify_find_order_for_tracking_v3($shopifyAccessToken, $shopifyName, $shopifyOrderId);

    if (empty($lookupResult['ok']) || empty($lookupResult['order'])) {
        save_tracking_attempt(
            $row,
            'failed',
            isset($lookupResult['http_status']) ? $lookupResult['http_status'] : null,
            isset($lookupResult['request_payload']) ? $lookupResult['request_payload'] : null,
            isset($lookupResult['response_payload']) ? $lookupResult['response_payload'] : null,
            isset($lookupResult['error']) ? $lookupResult['error'] : 'Failed to find Shopify order'
        );
        $results[] = array(
            'shopify_order_id' => $shopifyOrderId,
            'shopify_name' => $shopifyName,
            'ok' => false,
            'step' => 'find_order',
            'lookup_method' => isset($lookupResult['lookup_method']) ? $lookupResult['lookup_method'] : null,
            'error' => isset($lookupResult['error']) ? $lookupResult['error'] : 'Order lookup failed'
        );
        continue;
    }

    $order = $lookupResult['order'];
    $fulfillmentNodes = extract_fulfillment_nodes($order);
    $fulfillment = pick_fulfillment_for_tracking($fulfillmentNodes);

    if (empty($fulfillment['id'])) {
        $errorMessage = 'Order found but no existing fulfillment is available yet. Ask the bar crew to mark the order fulfilled in Shopify first, then rerun this script.';
        save_tracking_attempt(
            $row,
            'failed',
            isset($lookupResult['http_status']) ? $lookupResult['http_status'] : 200,
            isset($lookupResult['request_payload']) ? $lookupResult['request_payload'] : null,
            isset($lookupResult['response_payload']) ? $lookupResult['response_payload'] : null,
            $errorMessage
        );
        $results[] = array(
            'shopify_order_id' => $shopifyOrderId,
            'shopify_name' => $shopifyName,
            'ok' => false,
            'step' => 'no_fulfillment_yet',
            'lookup_method' => isset($lookupResult['lookup_method']) ? $lookupResult['lookup_method'] : null,
            'order_gid' => isset($order['id']) ? $order['id'] : null,
            'order_name' => isset($order['name']) ? $order['name'] : null,
            'display_fulfillment_status' => isset($order['displayFulfillmentStatus']) ? $order['displayFulfillmentStatus'] : null,
            'fulfillment_count' => count($fulfillmentNodes),
            'error' => $errorMessage
        );
        continue;
    }

    $mutation = "mutation FulfillmentTrackingInfoUpdate(\$fulfillmentId: ID!, \$trackingInfoInput: FulfillmentTrackingInput!, \$notifyCustomer: Boolean) {\n"
        . "  fulfillmentTrackingInfoUpdate(fulfillmentId: \$fulfillmentId, trackingInfoInput: \$trackingInfoInput, notifyCustomer: \$notifyCustomer) {\n"
        . "    fulfillment {\n"
        . "      id\n"
        . "      status\n"
        . "      trackingInfo {\n"
        . "        company\n"
        . "        number\n"
        . "        url\n"
        . "      }\n"
        . "    }\n"
        . "    userErrors {\n"
        . "      field\n"
        . "      message\n"
        . "    }\n"
        . "  }\n"
        . "}";

    $variables = array(
        'fulfillmentId' => $fulfillment['id'],
        'trackingInfoInput' => array(
            'company' => 'Lalamove',
            'number' => $trackingNumber,
            'url' => $trackingUrl,
        ),
        'notifyCustomer' => true,
    );

    $sendResp = shopify_graphql_with_token($shopifyAccessToken, $mutation, $variables);

    if ((isset($sendResp['http_status']) && ($sendResp['http_status'] === 401 || $sendResp['http_status'] === 403))
        && shopify_response_looks_like_auth_issue(isset($sendResp['raw']) ? $sendResp['raw'] : array())) {
        $refreshResp = shopify_get_access_token(true);
        if (!empty($refreshResp['ok'])) {
            $shopifyAccessToken = $refreshResp['access_token'];
            $sendResp = shopify_graphql_with_token($shopifyAccessToken, $mutation, $variables);
        }
    }

    $userErrors = array();
    if (isset($sendResp['raw']['data']['fulfillmentTrackingInfoUpdate']['userErrors'])
        && is_array($sendResp['raw']['data']['fulfillmentTrackingInfoUpdate']['userErrors'])) {
        $userErrors = $sendResp['raw']['data']['fulfillmentTrackingInfoUpdate']['userErrors'];
    }

    $hasFulfillmentId = !empty($sendResp['raw']['data']['fulfillmentTrackingInfoUpdate']['fulfillment']['id']);
    $success = !empty($sendResp['ok']) && empty($userErrors) && $hasFulfillmentId;

    save_tracking_attempt(
        $row,
        $success ? 'success' : 'failed',
        isset($sendResp['http_status']) ? $sendResp['http_status'] : null,
        array(
            'lookup_method' => isset($lookupResult['lookup_method']) ? $lookupResult['lookup_method'] : null,
            'order' => array(
                'id' => isset($order['id']) ? $order['id'] : null,
                'name' => isset($order['name']) ? $order['name'] : null,
                'legacyResourceId' => isset($order['legacyResourceId']) ? $order['legacyResourceId'] : null,
            ),
            'mutation' => $variables,
        ),
        isset($sendResp['raw']) ? $sendResp['raw'] : null,
        $success ? null : json_encode($userErrors, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
    );

    if ($success) {
        $upd1 = $mysqli->prepare("UPDATE order_matches SET tracking_sent = 1, tracking_sent_at = NOW() WHERE id = ?");
        if ($upd1) {
            $orderMatchId = (int)$row['order_match_id'];
            $upd1->bind_param('i', $orderMatchId);
            $upd1->execute();
            $upd1->close();
        }

        $upd2 = $mysqli->prepare("UPDATE shopify_orders SET match_status = 'tracking_sent' WHERE id = ?");
        if ($upd2) {
            $shopifyOrderRowId = (int)$row['shopify_order_row_id'];
            $upd2->bind_param('i', $shopifyOrderRowId);
            $upd2->execute();
            $upd2->close();
        }
    }

    $results[] = array(
        'shopify_order_id' => $shopifyOrderId,
        'shopify_name' => $shopifyName,
        'ok' => $success,
        'lookup_method' => isset($lookupResult['lookup_method']) ? $lookupResult['lookup_method'] : null,
        'order_gid' => isset($order['id']) ? $order['id'] : null,
        'order_name' => isset($order['name']) ? $order['name'] : null,
        'display_fulfillment_status' => isset($order['displayFulfillmentStatus']) ? $order['displayFulfillmentStatus'] : null,
        'fulfillment_id' => isset($fulfillment['id']) ? $fulfillment['id'] : null,
        'user_errors' => $userErrors,
        'raw' => isset($sendResp['raw']) ? $sendResp['raw'] : null,
    );
}

$stmt->close();

json_response(array(
    'ok' => true,
    'count' => count($results),
    'results' => $results,
));

function shopify_find_order_for_tracking_v3($accessToken, $shopifyName, $shopifyOrderId)
{
    $queries = build_shopify_lookup_candidates($shopifyName, $shopifyOrderId);
    $lastResponse = null;

    foreach ($queries as $candidate) {
        $resp = shopify_graphql_with_token($accessToken, $candidate['query'], $candidate['variables']);
        $lastResponse = $resp;

        if (empty($resp['ok'])) {
            continue;
        }

        $order = extract_order_from_lookup_response($candidate['type'], isset($resp['raw']) ? $resp['raw'] : array());
        if (!empty($order) && !empty($order['id'])) {
            return array(
                'ok' => true,
                'http_status' => isset($resp['http_status']) ? $resp['http_status'] : null,
                'order' => $order,
                'request_payload' => array(
                    'query' => $candidate['query'],
                    'variables' => $candidate['variables'],
                ),
                'response_payload' => isset($resp['raw']) ? $resp['raw'] : null,
                'lookup_method' => $candidate['method'],
            );
        }
    }

    $attemptedMethods = array();
    foreach ($queries as $candidate) {
        $attemptedMethods[] = $candidate['method'];
    }

    return array(
        'ok' => false,
        'http_status' => isset($lastResponse['http_status']) ? $lastResponse['http_status'] : null,
        'order' => null,
        'request_payload' => !empty($attemptedMethods) ? array('attempted_methods' => $attemptedMethods) : null,
        'response_payload' => isset($lastResponse['raw']) ? $lastResponse['raw'] : null,
        'lookup_method' => !empty($attemptedMethods) ? implode(',', $attemptedMethods) : null,
        'error' => 'Shopify order lookup failed. Check whether shopify_name matches the real Shopify order name and whether shopify_order_id is the true Shopify order resource ID.',
    );
}

function build_shopify_lookup_candidates($shopifyName, $shopifyOrderId)
{
    $orderFields = "id\n"
        . "legacyResourceId\n"
        . "name\n"
        . "displayFulfillmentStatus\n"
        . "fulfillments(first: 20) {\n"
        . "  id\n"
        . "  status\n"
        . "  trackingInfo {\n"
        . "    company\n"
        . "    number\n"
        . "    url\n"
        . "  }\n"
        . "}";

    $queries = array();

    if ($shopifyOrderId !== '' && ctype_digit($shopifyOrderId)) {
        $queries[] = array(
            'type' => 'order',
            'method' => 'order:id',
            'query' => "query FindOrderById(\$id: ID!) {\n"
                . "  order(id: \$id) {\n"
                . add_block_indent($orderFields, 4)
                . "\n  }\n"
                . "}",
            'variables' => array(
                'id' => shopify_gid_local('Order', $shopifyOrderId),
            ),
        );
    }

    if ($shopifyName !== '') {
        $queries[] = array(
            'type' => 'orderByIdentifier',
            'method' => 'orderByIdentifier:name',
            'query' => "query FindOrderByIdentifier(\$identifier: OrderIdentifierInput!) {\n"
                . "  orderByIdentifier(identifier: \$identifier) {\n"
                . add_block_indent($orderFields, 4)
                . "\n  }\n"
                . "}",
            'variables' => array(
                'identifier' => array(
                    'name' => $shopifyName,
                ),
            ),
        );

        $normalizedName = ltrim($shopifyName, '#');
        $searchTerms = array();
        $searchTerms[] = 'name:"' . addslashes_for_shopify_search($shopifyName) . '"';
        if ($normalizedName !== '' && $normalizedName !== $shopifyName) {
            $searchTerms[] = 'name:"' . addslashes_for_shopify_search($normalizedName) . '"';
        }

        foreach ($searchTerms as $searchTerm) {
            $queries[] = array(
                'type' => 'orders',
                'method' => 'orders:query=' . $searchTerm,
                'query' => "query FindOrdersByQuery(\$query: String!) {\n"
                    . "  orders(first: 5, query: \$query, reverse: true) {\n"
                    . "    nodes {\n"
                    . add_block_indent($orderFields, 6)
                    . "\n    }\n"
                    . "  }\n"
                    . "}",
                'variables' => array(
                    'query' => $searchTerm,
                ),
            );
        }
    }

    return $queries;
}

function extract_order_from_lookup_response($type, $raw)
{
    if (!is_array($raw)) {
        return null;
    }

    if ($type === 'orderByIdentifier') {
        return isset($raw['data']['orderByIdentifier']) ? $raw['data']['orderByIdentifier'] : null;
    }

    if ($type === 'orders') {
        if (!empty($raw['data']['orders']['nodes'][0]) && is_array($raw['data']['orders']['nodes'][0])) {
            return $raw['data']['orders']['nodes'][0];
        }
        return null;
    }

    return isset($raw['data']['order']) ? $raw['data']['order'] : null;
}

function extract_fulfillment_nodes($order)
{
    if (!is_array($order)) {
        return array();
    }

    if (isset($order['fulfillments']) && is_array($order['fulfillments'])) {
        return $order['fulfillments'];
    }

    return array();
}

function pick_fulfillment_for_tracking($fulfillments)
{
    if (empty($fulfillments) || !is_array($fulfillments)) {
        return array();
    }

    foreach ($fulfillments as $fulfillment) {
        if (empty($fulfillment['id'])) {
            continue;
        }

        $trackingInfo = isset($fulfillment['trackingInfo']) && is_array($fulfillment['trackingInfo']) ? $fulfillment['trackingInfo'] : array();
        if (empty($trackingInfo)) {
            return $fulfillment;
        }

        $hasAnyTracking = false;
        foreach ($trackingInfo as $info) {
            if (!empty($info['number']) || !empty($info['url'])) {
                $hasAnyTracking = true;
                break;
            }
        }

        if (!$hasAnyTracking) {
            return $fulfillment;
        }
    }

    return isset($fulfillments[0]) ? $fulfillments[0] : array();
}

function shopify_get_access_token($forceRefresh)
{
    $cacheFile = __DIR__ . '/cache/shopify_access_token_' . md5((string)SHOPIFY_STORE_DOMAIN) . '.json';

    if (!$forceRefresh) {
        $cached = read_shopify_token_cache($cacheFile);
        if ($cached !== null) {
            return array(
                'ok' => true,
                'http_status' => 200,
                'access_token' => $cached['access_token'],
                'raw' => array('cached' => true),
            );
        }
    }

    $url = 'https://' . SHOPIFY_STORE_DOMAIN . '/admin/oauth/access_token';
    $payload = array(
        'client_id' => SHOPIFY_CLIENT_ID,
        'client_secret' => SHOPIFY_CLIENT_SECRET,
        'grant_type' => 'client_credentials',
    );

    $ch = curl_init($url);
    curl_setopt_array($ch, array(
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_HTTPHEADER => array(
            'Content-Type: application/json',
            'Accept: application/json',
        ),
        CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
    ));

    $body = curl_exec($ch);
    $curlErr = curl_error($ch);
    $httpStatus = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $decoded = json_decode((string)$body, true);
    if (!is_array($decoded)) {
        $decoded = array('raw_body' => $body);
    }

    if ($curlErr !== '') {
        return array(
            'ok' => false,
            'http_status' => $httpStatus,
            'raw' => $decoded,
            'error' => $curlErr,
        );
    }

    $token = isset($decoded['access_token']) ? (string)$decoded['access_token'] : '';
    if ($httpStatus < 200 || $httpStatus >= 300 || $token === '') {
        return array(
            'ok' => false,
            'http_status' => $httpStatus,
            'raw' => $decoded,
            'error' => 'Shopify did not return a valid access token.',
        );
    }

    $expiresIn = 86399;
    if (isset($decoded['expires_in'])) {
        $expiresIn = max(60, (int)$decoded['expires_in']);
    }

    write_shopify_token_cache($cacheFile, array(
        'access_token' => $token,
        'expires_at' => time() + $expiresIn - 300,
    ));

    return array(
        'ok' => true,
        'http_status' => $httpStatus,
        'access_token' => $token,
        'raw' => $decoded,
    );
}

function shopify_graphql_with_token($accessToken, $query, $variables)
{
    $url = 'https://' . SHOPIFY_STORE_DOMAIN . '/admin/api/' . SHOPIFY_API_VERSION . '/graphql.json';
    $payload = array(
        'query' => $query,
        'variables' => $variables,
    );

    $ch = curl_init($url);
    curl_setopt_array($ch, array(
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_HTTPHEADER => array(
            'Content-Type: application/json',
            'Accept: application/json',
            'X-Shopify-Access-Token: ' . $accessToken,
        ),
        CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
    ));

    $body = curl_exec($ch);
    $curlErr = curl_error($ch);
    $httpStatus = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $decoded = json_decode((string)$body, true);
    if (!is_array($decoded)) {
        $decoded = array('raw_body' => $body);
    }

    if ($curlErr !== '') {
        return array(
            'ok' => false,
            'http_status' => $httpStatus,
            'raw' => $decoded,
            'curl_error' => $curlErr,
        );
    }

    $hasTopLevelErrors = !empty($decoded['errors']);

    return array(
        'ok' => $httpStatus >= 200 && $httpStatus < 300 && !$hasTopLevelErrors,
        'http_status' => $httpStatus,
        'raw' => $decoded,
    );
}

function shopify_response_looks_like_auth_issue($raw)
{
    $json = json_encode($raw, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    if (!is_string($json) || $json === '') {
        return false;
    }

    $json = strtolower($json);
    return strpos($json, 'invalid token') !== false
        || strpos($json, 'expired') !== false
        || strpos($json, 'unauthorized') !== false
        || strpos($json, 'forbidden') !== false;
}

function read_shopify_token_cache($cacheFile)
{
    if (!is_file($cacheFile)) {
        return null;
    }

    $raw = @file_get_contents($cacheFile);
    if ($raw === false) {
        return null;
    }

    $decoded = json_decode($raw, true);
    if (!is_array($decoded)) {
        return null;
    }

    $token = (string)(isset($decoded['access_token']) ? $decoded['access_token'] : '');
    $expiresAt = (int)(isset($decoded['expires_at']) ? $decoded['expires_at'] : 0);

    if ($token === '' || $expiresAt <= time()) {
        return null;
    }

    return array(
        'access_token' => $token,
        'expires_at' => $expiresAt,
    );
}

function write_shopify_token_cache($cacheFile, $payload)
{
    $dir = dirname($cacheFile);
    if (!is_dir($dir)) {
        @mkdir($dir, 0755, true);
    }

    @file_put_contents($cacheFile, json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}

function shopify_gid_local($type, $id)
{
    return 'gid://shopify/' . $type . '/' . $id;
}

function save_tracking_attempt($row, $status, $httpStatus, $requestPayload, $responsePayload, $errorMessage)
{
    $mysqli = db();
    $stmt = $mysqli->prepare(
        "INSERT INTO shopify_tracking_updates
        (shopify_order_row_id, order_match_row_id, tracking_company, tracking_number, tracking_url,
         notify_customer, api_status, http_status, request_payload, response_payload, error_message, attempted_at)
        VALUES (?, ?, 'Lalamove', ?, ?, 0, ?, ?, ?, ?, ?, NOW())"
    );

    if (!$stmt) {
        return;
    }

    $req = is_string($requestPayload) ? $requestPayload : json_encode($requestPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $res = is_string($responsePayload) ? $responsePayload : json_encode($responsePayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $trackingNumber = (string)(isset($row['lalamove_order_id']) ? $row['lalamove_order_id'] : '');
    $trackingUrl = (string)(isset($row['share_link']) ? $row['share_link'] : '');
    $shopifyOrderRowId = (int)(isset($row['shopify_order_row_id']) ? $row['shopify_order_row_id'] : 0);
    $orderMatchId = (int)(isset($row['order_match_id']) ? $row['order_match_id'] : 0);

    $stmt->bind_param(
        'iisssisss',
        $shopifyOrderRowId,
        $orderMatchId,
        $trackingNumber,
        $trackingUrl,
        $status,
        $httpStatus,
        $req,
        $res,
        $errorMessage
    );
    $stmt->execute();
    $stmt->close();
}

function add_block_indent($text, $spaces)
{
    $indent = str_repeat(' ', (int)$spaces);
    $lines = explode("\n", $text);
    foreach ($lines as $index => $line) {
        $lines[$index] = $indent . $line;
    }
    return implode("\n", $lines);
}

function addslashes_for_shopify_search($value)
{
    $value = str_replace('\\', '\\\\', $value);
    $value = str_replace('"', '\\"', $value);
    return $value;
}
