<?php
/**
 * Shopify Admin webhook: orders/create
 *
 * If payment method is ACH Bank Deposit (configured), generate a Stripe Checkout Session URL
 * and store it on the HubSpot Order object for later emailing to the customer.
 */

require __DIR__ . '/hubspot-config.php';
require __DIR__ . '/shopify-config.php';
require __DIR__ . '/stripe-config.php';
require __DIR__ . '/includes/shopify-api.php';
require __DIR__ . '/includes/hubspot-api.php';
require __DIR__ . '/includes/stripe-api.php';

header('Content-Type: text/plain; charset=utf-8');

// Allow long enough for HubSpot Shopify order sync (see SHOPIFY_HUBSPOT_ORDER_SYNC_MAX_WAIT_SECONDS).
$__hubspotWait = defined('SHOPIFY_HUBSPOT_ORDER_SYNC_MAX_WAIT_SECONDS')
    ? (int) SHOPIFY_HUBSPOT_ORDER_SYNC_MAX_WAIT_SECONDS
    : 600;
@set_time_limit(max(300, $__hubspotWait + 120));
@ignore_user_abort(true);

function log_message(string $level, string $message, array $context = []): void
{
    $primaryDir = __DIR__ . '/../logs';
    $logDir = $primaryDir;
    if (!is_dir($logDir)) {
        @mkdir($logDir, 0775, true);
    }
    if (!is_dir($logDir) || !is_writable($logDir)) {
        $fallback = rtrim((string) sys_get_temp_dir(), "\\/") . DIRECTORY_SEPARATOR . 'hubspot-shopify-logs';
        $logDir = $fallback;
        if (!is_dir($logDir)) {
            @mkdir($logDir, 0775, true);
        }
    }

    $logFile = rtrim($logDir, "\\/") . DIRECTORY_SEPARATOR . 'shopify-order-created.log';
    $ts = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z');
    $ctx = '';
    if (!empty($context)) {
        $ctx = ' ' . json_encode($context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    }
    $line = $ts . ' [' . $level . '] ' . $message . $ctx . PHP_EOL;
    @file_put_contents($logFile, $line, FILE_APPEND | LOCK_EX);
    @error_log('[shopify-order-created] ' . trim($line));
}

log_message('info', 'PHP runtime limits', [
    'max_execution_time' => ini_get('max_execution_time'),
    'memory_limit' => ini_get('memory_limit'),
]);

/**
 * Send a quick 200 OK response to Shopify, then continue work.
 * This helps on shared hosting where long-running webhooks are aborted/time out.
 */
function ack_shopify_and_continue(): void
{
    if (headers_sent()) {
        return;
    }
    http_response_code(200);
    echo "OK\n";
    // Best-effort flush.
    @ob_flush();
    @flush();
    if (function_exists('fastcgi_finish_request')) {
        @fastcgi_finish_request();
    }
}

function order_has_payment_gateway(array $order, string $gatewayName): bool
{
    $needle = strtolower(trim($gatewayName));
    if ($needle === '') {
        return false;
    }

    $names = $order['payment_gateway_names'] ?? null;
    if (is_array($names)) {
        foreach ($names as $n) {
            if (strtolower(trim((string) $n)) === $needle) {
                return true;
            }
        }
    }

    $gateway = $order['gateway'] ?? null;
    if (is_string($gateway) && strtolower(trim($gateway)) === $needle) {
        return true;
    }

    return false;
}

/**
 * @param array<string, mixed> $existingProps
 * @return array{session_id: string, url: string, has_any: bool, has_both: bool}
 */
function hubspot_existing_stripe_checkout_state(array $existingProps, string $sessionProp, string $urlProp): array
{
    $sessionId = trim((string) ($existingProps[$sessionProp] ?? ''));
    $url = trim((string) ($existingProps[$urlProp] ?? ''));
    $validSession = $sessionId !== '' && strpos($sessionId, 'cs_') === 0;
    $validUrl = $url !== '' && preg_match('#^https?://#i', $url) === 1;

    return [
        'session_id' => $sessionId,
        'url' => $url,
        'has_any' => $validSession || $validUrl,
        'has_both' => $validSession && $validUrl,
    ];
}

$rawBody = file_get_contents('php://input');
if (!is_string($rawBody)) {
    $rawBody = '';
}



$order = json_decode($rawBody, true);
if (!is_array($order)) {
    log_message('error', 'Invalid JSON payload');
    http_response_code(400);
    exit;
}

$topic = isset($_SERVER['HTTP_X_SHOPIFY_TOPIC']) ? (string) $_SERVER['HTTP_X_SHOPIFY_TOPIC'] : '';
if ($topic !== '' && $topic !== 'orders/create') {
    log_message('info', 'Ignored webhook topic', ['topic' => $topic]);
    http_response_code(200);
    exit;
}

$shopifyOrderId = isset($order['id']) ? (string) $order['id'] : '';
if ($shopifyOrderId === '') {
    log_message('error', 'Missing order id');
    http_response_code(400);
    exit;
}

// Prevent overlapping runs for the same order id (Shopify can retry webhooks).
$lockDir = rtrim((string) sys_get_temp_dir(), "\\/") . DIRECTORY_SEPARATOR . 'hubspot-shopify-locks';
if (!is_dir($lockDir)) {
    @mkdir($lockDir, 0775, true);
}
$lockFile = rtrim($lockDir, "\\/") . DIRECTORY_SEPARATOR . 'order_' . preg_replace('/[^0-9A-Za-z_-]/', '_', $shopifyOrderId) . '.lock';
$lockHandle = @fopen($lockFile, 'c');
if ($lockHandle !== false) {
    if (!@flock($lockHandle, LOCK_EX | LOCK_NB)) {
        log_message('info', 'Another worker is processing this order; skipping', ['shopifyOrderId' => $shopifyOrderId]);
        http_response_code(200);
        echo "OK\n";
        exit;
    }
}

log_message('info', 'Webhook received', [
    'shopifyOrderId' => $shopifyOrderId,
    'payment_gateway_names' => is_array($order['payment_gateway_names'] ?? null) ? $order['payment_gateway_names'] : null,
    'gateway' => $order['gateway'] ?? null,
    'achGatewayName' => SHOPIFY_ACH_PAYMENT_GATEWAY_NAME,
    'financial_status' => $order['financial_status'] ?? null,
]);

// Only act for ACH payment method orders.
if (!order_has_payment_gateway($order, SHOPIFY_ACH_PAYMENT_GATEWAY_NAME)) {
    http_response_code(200);
    echo "Not ACH; nothing to do.\n";
    exit;
}

// Create the HubSpot deal immediately for ACH orders while Shopify payment is still pending.
$existingDealId = hubspot_find_deal_id_by_shopify_order($shopifyOrderId);
if ($existingDealId === null) {
    $createdDeal = hubspot_create_deal_from_shopify_order($order, HUBSPOT_DEALSTAGE_HARDWARE_ACH_ID);
    if (!$createdDeal['ok']) {
        log_message('error', 'HubSpot deal create failed for ACH order', [
            'shopifyOrderId' => $shopifyOrderId,
            'status' => $createdDeal['status'] ?? null,
            'error' => $createdDeal['error'] ?? null,
        ]);
    } else {
        log_message('info', 'HubSpot deal created for ACH order', [
            'shopifyOrderId' => $shopifyOrderId,
            'dealId' => $createdDeal['deal_id'] ?? null,
            'dealStageId' => HUBSPOT_DEALSTAGE_HARDWARE_ACH_ID,
        ]);
    }
} else {
    log_message('info', 'HubSpot deal already exists for ACH order', [
        'shopifyOrderId' => $shopifyOrderId,
        'dealId' => $existingDealId,
    ]);
}

// Acknowledge Shopify quickly; the remaining work can take time.
ack_shopify_and_continue();

// Retry until HubSpot Order exists (race with HubSpot Shopify integration).
$propsToFetch = [
    defined('HUBSPOT_ORDER_PROPERTY_STRIPE_CHECKOUT_URL') ? HUBSPOT_ORDER_PROPERTY_STRIPE_CHECKOUT_URL : '',
    defined('HUBSPOT_ORDER_PROPERTY_STRIPE_CHECKOUT_SESSION_ID') ? HUBSPOT_ORDER_PROPERTY_STRIPE_CHECKOUT_SESSION_ID : '',
    defined('HUBSPOT_ORDER_PROPERTY_PAYMENT_STATUS') ? HUBSPOT_ORDER_PROPERTY_PAYMENT_STATUS : '',
];

$found = null;
$sleepSeconds = defined('SHOPIFY_HUBSPOT_ORDER_SYNC_SLEEP_SECONDS')
    ? (int) SHOPIFY_HUBSPOT_ORDER_SYNC_SLEEP_SECONDS
    : 2;
$maxWaitSeconds = defined('SHOPIFY_HUBSPOT_ORDER_SYNC_MAX_WAIT_SECONDS')
    ? (int) SHOPIFY_HUBSPOT_ORDER_SYNC_MAX_WAIT_SECONDS
    : 600;
$maxTries = (int) ceil($maxWaitSeconds / max(1, $sleepSeconds));
for ($i = 1; $i <= $maxTries; $i++) {
    $found = hubspot_find_order_by_shopify_order($shopifyOrderId, $propsToFetch);
    if ($found !== null) {
        break;
    }
    // Log a few attempts for debugging without spamming logs.
    if ($i === 1 || $i === 5 || $i === 15 || $i % 30 === 0 || $i === $maxTries) {
        // Do a verbose HubSpot search to see if this is "0 results" vs auth/error.
        $verbose = hubspot_json_request('POST', '/crm/v3/objects/orders/search', [
            'filterGroups' => [
                [
                    'filters' => [
                        [
                            'propertyName' => HUBSPOT_ORDER_PROPERTY_SHOPIFY_ORDER_ID,
                            'operator' => 'EQ',
                            'value' => $shopifyOrderId,
                        ],
                    ],
                ],
            ],
            'properties' => ['hs_order_name', HUBSPOT_ORDER_PROPERTY_SHOPIFY_ORDER_ID],
            'limit' => 1,
        ]);
        $resultCount = null;
        if (is_array($verbose['body'])) {
            $results = $verbose['body']['results'] ?? null;
            if (is_array($results)) {
                $resultCount = count($results);
            }
        }
        log_message('info', 'Waiting for HubSpot Order sync', [
            'shopifyOrderId' => $shopifyOrderId,
            'attempt' => $i,
            'maxTries' => $maxTries,
            'sleepSeconds' => $sleepSeconds,
            'hubspotSearchOk' => $verbose['ok'],
            'hubspotSearchStatus' => $verbose['status'],
            'hubspotResultCount' => $resultCount,
        ]);
    }
    if ($i < $maxTries) {
        sleep($sleepSeconds);
    }
}

if ($found === null) {
    log_message('error', 'HubSpot Order not found after retries', [
        'shopifyOrderId' => $shopifyOrderId,
        'maxTries' => $maxTries,
        'sleepSeconds' => $sleepSeconds,
        'waitSeconds' => $maxTries * $sleepSeconds,
        'hint' => 'Increase SHOPIFY_HUBSPOT_ORDER_SYNC_MAX_WAIT_SECONDS (max 900) if HubSpot sync is slower. Confirm hs_external_order_id matches Shopify order.id',
    ]);
    exit;
}

$hubspotOrderId = $found['id'];
log_message('info', 'HubSpot Order found', [
    'shopifyOrderId' => $shopifyOrderId,
    'hubspotOrderId' => $hubspotOrderId,
]);

// Fresh read right before acting (handles concurrent webhooks and "PATCH failed then Shopify retried").
$refreshed = hubspot_find_order_by_shopify_order($shopifyOrderId, $propsToFetch);
if ($refreshed !== null) {
    $found = $refreshed;
    $hubspotOrderId = $found['id'];
}

$existingProps = $found['properties'] ?? [];
$state = hubspot_existing_stripe_checkout_state(
    $existingProps,
    HUBSPOT_ORDER_PROPERTY_STRIPE_CHECKOUT_SESSION_ID,
    HUBSPOT_ORDER_PROPERTY_STRIPE_CHECKOUT_URL
);

// Never create a second Checkout Session for the same order if we already stored session id and/or URL.
if ($state['has_any']) {
    if ($state['has_both']) {
        log_message('info', 'Stripe checkout already set; skipping create', [
            'shopifyOrderId' => $shopifyOrderId,
            'hubspotOrderId' => $hubspotOrderId,
            'sessionId' => $state['session_id'],
        ]);
        http_response_code(200);
        echo "Already has checkout link.\n";
        exit;
    }

    // Session id without URL (e.g. PATCH partially failed): hydrate URL from Stripe, no new session.
    if ($state['session_id'] !== '' && $state['url'] === '') {
        $got = stripe_get_checkout_session($state['session_id']);
        if ($got['ok'] && isset($got['session']) && is_array($got['session'])) {
            $hydratedUrl = trim((string) ($got['session']['url'] ?? ''));
            if ($hydratedUrl !== '') {
                $fix = hubspot_update_order_properties($hubspotOrderId, [
                    HUBSPOT_ORDER_PROPERTY_STRIPE_CHECKOUT_URL => $hydratedUrl,
                ]);
                log_message('info', 'Hydrated Stripe checkout URL from existing session', [
                    'shopifyOrderId' => $shopifyOrderId,
                    'hubspotOrderId' => $hubspotOrderId,
                    'sessionId' => $state['session_id'],
                    'hubspotPatchOk' => $fix['ok'] ?? null,
                ]);
            }
        }
        http_response_code(200);
        echo "Already has checkout session; URL refreshed if possible.\n";
        exit;
    }

    // URL without session id (unusual): still do not create a duplicate session.
    log_message('info', 'Stripe checkout URL present without session id; skipping create', [
        'shopifyOrderId' => $shopifyOrderId,
        'hubspotOrderId' => $hubspotOrderId,
    ]);
    http_response_code(200);
    echo "Already has checkout URL.\n";
    exit;
}

log_message('info', 'Calling Stripe to create checkout session', [
    'shopifyOrderId' => $shopifyOrderId,
    'hubspotOrderId' => $hubspotOrderId,
]);

$created = stripe_create_checkout_session_for_shopify_order($order);
if (!$created['ok']) {
    log_message('error', 'Stripe session create failed', [
        'shopifyOrderId' => $shopifyOrderId,
        'hubspotOrderId' => $hubspotOrderId,
        'status' => $created['status'] ?? null,
        'error' => $created['error'] ?? null,
    ]);
    http_response_code(502);
    exit;
}

$checkoutUrl = (string) ($created['url'] ?? '');
$sessionId = (string) ($created['id'] ?? '');

$patch = [
    HUBSPOT_ORDER_PROPERTY_STRIPE_CHECKOUT_URL => $checkoutUrl,
    HUBSPOT_ORDER_PROPERTY_STRIPE_CHECKOUT_SESSION_ID => $sessionId,
    HUBSPOT_ORDER_PROPERTY_PAYMENT_STATUS => 'link_created',
];

$patched = hubspot_update_order_properties($hubspotOrderId, $patch);
if (!$patched['ok']) {
    log_message('error', 'HubSpot Order update failed', [
        'shopifyOrderId' => $shopifyOrderId,
        'hubspotOrderId' => $hubspotOrderId,
        'status' => $patched['status'] ?? null,
        'error' => $patched['error'] ?? null,
    ]);
    http_response_code(502);
    echo "HubSpot update failed.\n";
    exit;
}

log_message('info', 'Stripe checkout saved to HubSpot Order', [
    'shopifyOrderId' => $shopifyOrderId,
    'hubspotOrderId' => $hubspotOrderId,
    'sessionId' => $sessionId,
]);

// Release per-order lock if held.
if (is_resource($lockHandle)) {
    @flock($lockHandle, LOCK_UN);
    @fclose($lockHandle);
}

