<?php
declare(strict_types=1);

require_once __DIR__ . '/bootstrap.php';

function shopify_shop_domain(): string {
    $shop = defined('SHOPIFY_SHOP') ? (string)SHOPIFY_SHOP : (pd_env('SHOPIFY_SHOP', '') ?? '');
    return trim($shop);
}

function shopify_access_token(): string {
    $tok = defined('SHOPIFY_ACCESS_TOKEN') ? (string)SHOPIFY_ACCESS_TOKEN : (pd_env('SHOPIFY_ACCESS_TOKEN', '') ?? '');
    return trim($tok);
}

function shopify_api_version(): string {
    // Prefer newest; fall back to existing config.
    $v = defined('SHOPIFY_API_VERSION') ? (string)SHOPIFY_API_VERSION : (pd_env('SHOPIFY_API_VERSION', '') ?? '');
    $v = trim($v);
    return $v !== '' ? $v : '2026-01';
}

function shopify_webhook_secret(): string {
    $s = pd_env('SHOPIFY_WEBHOOK_SECRET', '');
    if ($s === '' && defined('SHOPIFY_WEBHOOK_SECRET')) {
        $s = (string) SHOPIFY_WEBHOOK_SECRET;
    }
    return trim($s);
}

/**
 * Write a line to the Shopify webhook log. Creates the log directory if needed.
 * Use this to confirm the webhook endpoint is hit and to trace HMAC checks.
 */
function shopify_webhook_log(string $message, array $context = []): void {
    $logDir = dirname(__DIR__, 1) . '/log';
    $logFile = $logDir . '/shopify_webhook.log';
    if (!is_dir($logDir)) {
        @mkdir($logDir, 0777, true);
    }
    $line = '[' . date('c') . '] ' . $message;
    if (!empty($context)) {
        $json = @json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if (is_string($json) && $json !== '') {
            $line .= ' ' . $json;
        }
    }
    @file_put_contents($logFile, $line . "\n", FILE_APPEND);
}

function shopify_webhook_ignore(string $reason, array $context = []): void {
    shopify_webhook_log('IGNORED: ' . $reason, $context);
    pd_json(200, array_merge(['success' => true, 'ignored' => true, 'reason' => $reason], $context));
}

function shopify_verify_webhook(string $rawBody): void {
    $skipHmac = (getenv('SKIP_SHOPIFY_WEBHOOK_HMAC') ?: (defined('SKIP_SHOPIFY_WEBHOOK_HMAC') ? (string) SKIP_SHOPIFY_WEBHOOK_HMAC : '')) !== '';
    $skipHmac = $skipHmac && in_array(strtolower(trim($skipHmac)), ['1', 'true', 'yes'], true);

    $secret = shopify_webhook_secret();
    $secretLen = strlen($secret);
    $fromEnv = (getenv('SHOPIFY_WEBHOOK_SECRET') !== false && getenv('SHOPIFY_WEBHOOK_SECRET') !== '');
    $contentEncoding = (string)($_SERVER['HTTP_CONTENT_ENCODING'] ?? '');
    shopify_webhook_log(
        'HMAC input: secret_length=' . $secretLen . ', body_length=' . strlen($rawBody) .
        ', secret_from=' . ($fromEnv ? 'env' : (defined('SHOPIFY_WEBHOOK_SECRET') ? 'config' : 'none')) .
        ', secret_prefix=' . substr($secret, 0, 6) . '...' .
        ($contentEncoding !== '' ? ', Content-Encoding=' . $contentEncoding : '')
    );
    if ($secret === '') {
        shopify_webhook_log('REJECT: Shopify webhook secret missing (set SHOPIFY_WEBHOOK_SECRET in php/config.php or env)');
        pd_json(500, ['success' => false, 'error' => 'Set SHOPIFY_WEBHOOK_SECRET env var']);
    }
    $hmacHeader = (string)($_SERVER['HTTP_X_SHOPIFY_HMAC_SHA256'] ?? '');
    if ($hmacHeader === '') {
        shopify_webhook_log('REJECT: Missing X-Shopify-Hmac-Sha256 header');
        pd_json(401, ['success' => false, 'error' => 'Missing X-Shopify-Hmac-Sha256 header']);
    }
    $computed = base64_encode(hash_hmac('sha256', $rawBody, $secret, true));
    $checkResult = hash_equals($computed, $hmacHeader);
    shopify_webhook_log(
        'HMAC check: result=' . ($checkResult ? 'OK' : 'FAIL') . ', computed_len=' . strlen($computed) . ', header_len=' . strlen($hmacHeader)
    );
    if ($checkResult) {
        return;
    }
    if ($skipHmac) {
        shopify_webhook_log('WARNING: HMAC verification SKIPPED (SKIP_SHOPIFY_WEBHOOK_HMAC is set). Remove it in production.');
        return;
    }
    shopify_webhook_log('REJECT: HMAC mismatch (computed does not equal header)');
    pd_log('Shopify webhook HMAC mismatch', ['computed' => $computed, 'header' => $hmacHeader]);
    pd_json(401, ['success' => false, 'error' => 'Invalid webhook HMAC']);
}

function shopify_graphql(string $query, array $variables = []): array {
    $shop = shopify_shop_domain();
    $token = shopify_access_token();
    if ($shop === '' || $token === '') {
        pd_json(500, ['success' => false, 'error' => 'Set SHOPIFY_SHOP and SHOPIFY_ACCESS_TOKEN']);
    }

    $url = 'https://' . $shop . '/admin/api/' . shopify_api_version() . '/graphql.json';
    $payload = json_encode(['query' => $query, 'variables' => $variables], JSON_UNESCAPED_SLASHES);
    $headers = [
        'Content-Type: application/json',
        'X-Shopify-Access-Token: ' . $token,
    ];

    $attempts = 4;
    for ($i = 0; $i < $attempts; $i++) {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $payload,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_TIMEOUT => 60,
        ]);
        $resp = curl_exec($ch);
        $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err = curl_error($ch);
        curl_close($ch);

        if ($err) {
            if ($i < ($attempts - 1)) {
                usleep(pd_backoff_us($i));
                continue;
            }
            pd_json(500, ['success' => false, 'error' => 'Shopify GraphQL request failed: ' . $err]);
        }

        $data = json_decode((string)$resp, true);
        if (!is_array($data)) {
            if ($i < ($attempts - 1)) {
                usleep(pd_backoff_us($i));
                continue;
            }
            pd_json(500, ['success' => false, 'error' => 'Invalid Shopify GraphQL response', 'raw' => substr((string)$resp, 0, 500)]);
        }

        $retryable = in_array($code, [429, 500, 502, 503, 504], true);
        if (($code < 200 || $code >= 300) && $retryable && $i < ($attempts - 1)) {
            usleep(pd_backoff_us($i));
            continue;
        }

        if ($code < 200 || $code >= 300) {
            if (function_exists('shopify_webhook_log')) {
                shopify_webhook_log('Shopify GraphQL HTTP error', ['http' => $code, 'response' => $data]);
            }
            pd_json($code, ['success' => false, 'error' => 'Shopify GraphQL HTTP ' . $code, 'response' => $data]);
        }
        if (!empty($data['errors'])) {
            if (function_exists('shopify_webhook_log')) {
                shopify_webhook_log('Shopify GraphQL errors', ['errors' => $data['errors']]);
            }
            pd_log('Shopify GraphQL errors', ['errors' => $data['errors']]);
            pd_json(500, ['success' => false, 'error' => 'Shopify GraphQL errors', 'errors' => $data['errors']]);
        }
        return $data['data'] ?? [];
    }

    pd_json(500, ['success' => false, 'error' => 'Unexpected retry loop exit']);
}

function shopify_metafields_set_on_owner(string $ownerGid, array $metafields, bool $throwOnError = false): void {
    // $metafields: [ ['namespace'=>'custom','key'=>'document_status','type'=>'single_line_text_field','value'=>'sent'], ... ]
    $inputs = [];
    foreach ($metafields as $mf) {
        if (!is_array($mf)) continue;
        $ns = (string)($mf['namespace'] ?? 'custom');
        $key = (string)($mf['key'] ?? '');
        $type = (string)($mf['type'] ?? '');
        $value = $mf['value'] ?? null;
        if ($key === '' || $type === '') continue;
        $inputs[] = [
            'ownerId' => $ownerGid,
            'namespace' => $ns,
            'key' => $key,
            'type' => $type,
            'value' => is_bool($value) ? ($value ? 'true' : 'false') : (string)$value,
        ];
    }
    if (empty($inputs)) return;

    $mutation = <<<'GQL'
mutation MetafieldsSet($metafields: [MetafieldsSetInput!]!) {
  metafieldsSet(metafields: $metafields) {
    metafields { namespace key value }
    userErrors { field message code }
  }
}
GQL;
    if ($throwOnError) {
        $out = shopify_graphql_throw($mutation, ['metafields' => $inputs]);
    } else {
        $out = shopify_graphql($mutation, ['metafields' => $inputs]);
    }
    $errs = $out['metafieldsSet']['userErrors'] ?? [];
    if (is_array($errs) && count($errs) > 0) {
        pd_log('Shopify metafieldsSet userErrors', ['errors' => $errs, 'owner_gid' => $ownerGid]);
        if ($throwOnError) {
            throw new ShopifyApiException('Failed to set metafields: ' . json_encode($errs), 500);
        }
        pd_json(500, ['success' => false, 'error' => 'Failed to set metafields', 'userErrors' => $errs]);
    }
}

function shopify_set_order_metafields(int $orderId, array $metafields): void {
    shopify_metafields_set_on_owner('gid://shopify/Order/' . $orderId, $metafields);
}

function shopify_set_draft_order_metafields(int $draftOrderId, array $metafields): void {
    shopify_metafields_set_on_owner('gid://shopify/DraftOrder/' . $draftOrderId, $metafields);
}

/** Thrown when Shopify API is used with throw=true so caller can save error to DB instead of exiting. */
class ShopifyApiException extends RuntimeException {
    public function __construct(string $message, int $code = 0, ?\Throwable $previous = null) {
        parent::__construct($message, $code, $previous);
    }
}

/** Like shopify_graphql but throws instead of pd_json so orchestrator can catch and save to DB. */
function shopify_graphql_throw(string $query, array $variables = []): array {
    $shop = shopify_shop_domain();
    $token = shopify_access_token();
    if ($shop === '' || $token === '') {
        throw new ShopifyApiException('Set SHOPIFY_SHOP and SHOPIFY_ACCESS_TOKEN', 500);
    }
    $url = 'https://' . $shop . '/admin/api/' . shopify_api_version() . '/graphql.json';
    $payload = json_encode(['query' => $query, 'variables' => $variables], JSON_UNESCAPED_SLASHES);
    $headers = ['Content-Type: application/json', 'X-Shopify-Access-Token: ' . $token];
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $payload,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT => 60,
    ]);
    $resp = curl_exec($ch);
    $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err = curl_error($ch);
    curl_close($ch);
    if ($err) {
        throw new ShopifyApiException('Shopify GraphQL request failed: ' . $err, 500);
    }
    $data = json_decode((string)$resp, true);
    if (!is_array($data)) {
        throw new ShopifyApiException('Invalid Shopify GraphQL response: ' . substr((string)$resp, 0, 300), 500);
    }
    if ($code < 200 || $code >= 300) {
        throw new ShopifyApiException('Shopify GraphQL HTTP ' . $code . ': ' . json_encode($data), $code);
    }
    if (!empty($data['errors'])) {
        throw new ShopifyApiException('Shopify GraphQL errors: ' . json_encode($data['errors']), 500);
    }
    return $data['data'] ?? [];
}

/** Same as shopify_get_order_minimal but throws instead of pd_json so orchestrator can save error to DB. */
function shopify_get_order_minimal_or_throw(int $orderId): array {
    $query = <<<'GQL'
query Order($id: ID!) {
  order(id: $id) {
    id
    name
    email
    phone
    note
    createdAt
    customer { firstName lastName email phone }
    shippingAddress { name firstName lastName address1 address2 city province zip country phone }
    billingAddress { name firstName lastName address1 address2 city province zip country phone }
    subtotalPriceSet { shopMoney { amount currencyCode } }
    totalShippingPriceSet { shopMoney { amount currencyCode } }
    totalTaxSet { shopMoney { amount currencyCode } }
    totalPriceSet { shopMoney { amount currencyCode } }
    totalWeight
    lineItems(first: 50) {
      nodes {
        title
        quantity
        originalUnitPriceSet { shopMoney { amount currencyCode } }
        sku
        customAttributes { key value }
      }
    }
    metafields(first: 20, namespace: "custom") {
      nodes { key type value }
    }
  }
}
GQL;
    $gid = 'gid://shopify/Order/' . $orderId;
    $out = shopify_graphql_throw($query, ['id' => $gid]);
    $order = $out['order'] ?? null;
    if (!is_array($order)) {
        throw new ShopifyApiException('Order not found: ' . $gid, 404);
    }
    return $order;
}

function shopify_draft_order_graphql_fragment(): string {
    return <<<'GQL'
    id
    name
    email
    phone
    note2
    createdAt
    customer { firstName lastName email phone }
    shippingAddress { name firstName lastName address1 address2 city province zip country phone }
    billingAddress { name firstName lastName address1 address2 city province zip country phone }
    subtotalPriceSet { shopMoney { amount currencyCode } }
    totalTaxSet { shopMoney { amount currencyCode } }
    totalPriceSet { shopMoney { amount currencyCode } }
    totalShippingPriceSet { shopMoney { amount currencyCode } }
    shippingLine {
      originalPriceSet { shopMoney { amount currencyCode } }
    }
    lineItems(first: 50) {
      nodes {
        title
        quantity
        originalUnitPriceSet { shopMoney { amount currencyCode } }
        sku
        customAttributes { key value }
      }
    }
    metafields(first: 30, namespace: "custom") {
      nodes { key type value }
    }
    order { id legacyResourceId }
GQL;
}

/** Order ID created from this draft, or null if draft is not completed yet. */
function shopify_draft_order_linked_order_id(int $draftOrderId): ?int {
    $query = <<<'GQL'
query DraftOrderLinkedOrder($id: ID!) {
  draftOrder(id: $id) {
    order { legacyResourceId }
  }
}
GQL;
    $gid = 'gid://shopify/DraftOrder/' . $draftOrderId;
    try {
        $out = shopify_graphql($query, ['id' => $gid]);
    } catch (Throwable $e) {
        pd_log('shopify_draft_order_linked_order_id failed', ['draft_order_id' => $draftOrderId, 'error' => $e->getMessage()]);
        return null;
    }
    $legacy = $out['draftOrder']['order']['legacyResourceId'] ?? null;
    if ($legacy === null || $legacy === '') {
        return null;
    }
    return (int)$legacy;
}

function shopify_get_draft_order_minimal_or_throw(int $draftOrderId): array {
    $fragment = shopify_draft_order_graphql_fragment();
    $query = "query DraftOrder(\$id: ID!) {\n  draftOrder(id: \$id) {\n{$fragment}\n  }\n}";
    $gid = 'gid://shopify/DraftOrder/' . $draftOrderId;
    $out = shopify_graphql_throw($query, ['id' => $gid]);
    $draft = $out['draftOrder'] ?? null;
    if (!is_array($draft)) {
        throw new ShopifyApiException('Draft order not found: ' . $gid, 404);
    }
    return $draft;
}

function shopify_get_draft_order_minimal(int $draftOrderId): array {
    try {
        return shopify_get_draft_order_minimal_or_throw($draftOrderId);
    } catch (Throwable $e) {
        shopify_webhook_log('ERROR: draft order GraphQL fetch failed', [
            'draft_order_id' => $draftOrderId,
            'error' => $e->getMessage(),
        ]);
        pd_log('shopify_get_draft_order_minimal failed', ['draft_order_id' => $draftOrderId, 'error' => $e->getMessage()]);
        pd_json(500, ['success' => false, 'error' => 'Draft order fetch failed', 'details' => $e->getMessage()]);
    }
}

function shopify_get_order_minimal(int $orderId): array {
    $query = <<<'GQL'
query Order($id: ID!) {
  order(id: $id) {
    id
    name
    email
    phone
    note
    createdAt
    customer { firstName lastName email phone }
    shippingAddress { name firstName lastName address1 address2 city province zip country phone }
    billingAddress { name firstName lastName address1 address2 city province zip country phone }
    subtotalPriceSet { shopMoney { amount currencyCode } }
    totalShippingPriceSet { shopMoney { amount currencyCode } }
    totalTaxSet { shopMoney { amount currencyCode } }
    totalPriceSet { shopMoney { amount currencyCode } }
    totalWeight
    lineItems(first: 50) {
      nodes {
        title
        quantity
        originalUnitPriceSet { shopMoney { amount currencyCode } }
        sku
        customAttributes { key value }
      }
    }
    metafields(first: 20, namespace: "custom") {
      nodes { key type value }
    }
  }
}
GQL;
    $gid = 'gid://shopify/Order/' . $orderId;
    $out = shopify_graphql($query, ['id' => $gid]);
    $order = $out['order'] ?? null;
    if (!is_array($order)) {
        pd_json(404, ['success' => false, 'error' => 'Order not found']);
    }
    return $order;
}

