<?php
/**
 * Shopify App Proxy endpoint (PHP).
 *
 * Purpose:
 * - Serve `pricing_rule_screen_storm` metaobject entries to the storefront without exposing Admin credentials.
 *
 * Requirements (set as environment variables on your server):
 * - SHOPIFY_STORE                 e.g. "g1hj10-vk.myshopify.com" (no https://)
 * - SHOPIFY_ADMIN_ACCESS_TOKEN    Admin API access token (from Custom App)
 * - API_VERSION                   optional, default "2026-01"
 *
 * Shopify App Proxy setup:
 * - App proxy prefix: /apps
 * - Subpath: /door-config
 * - Proxy URL: https://YOUR_DOMAIN/shopify-app-proxy/pricing-rule-screen-storm.php
 *
 * Storefront request URL:
 * - /apps/door-config/pricing-rule-screen-storm
 */
declare(strict_types=1);

// CORS (Shopify storefront -> this domain)
$origin = isset($_SERVER['HTTP_ORIGIN']) ? (string)$_SERVER['HTTP_ORIGIN'] : '*';
header('Access-Control-Allow-Origin: ' . ($origin !== '' ? $origin : '*'));
header('Vary: Origin');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Max-Age: 600');

if (isset($_SERVER['REQUEST_METHOD']) && strtoupper((string)$_SERVER['REQUEST_METHOD']) === 'OPTIONS') {
  http_response_code(204);
  exit;
}

header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: public, max-age=60'); // short cache; adjust as needed

function env(string $k, string $default = ''): string {
  $v = getenv($k);
  if ($v === false || $v === null) return $default;
  return (string)$v;
}

function norm_shop_domain(string $s): string {
  $s = trim($s);
  $s = preg_replace('#^https?://#', '', $s);
  return rtrim($s, '/');
}

function json_out(int $status, array $payload): void {
  http_response_code($status);
  echo json_encode($payload, JSON_UNESCAPED_SLASHES);
  exit;
}

function shopify_admin_graphql(string $shopDomain, string $adminToken, string $apiVersion, string $query, array $variables): array {
  $url = "https://{$shopDomain}/admin/api/{$apiVersion}/graphql.json";
  $body = json_encode(['query' => $query, 'variables' => $variables], JSON_UNESCAPED_SLASHES);

  $ch = curl_init($url);
  curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $body,
    CURLOPT_HTTPHEADER => [
      'Content-Type: application/json',
      'X-Shopify-Access-Token: ' . $adminToken,
    ],
    CURLOPT_TIMEOUT => 20,
  ]);

  $raw = curl_exec($ch);
  $err = curl_error($ch);
  $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
  curl_close($ch);

  if ($raw === false) {
    return ['ok' => false, 'status' => 0, 'error' => $err ?: 'curl_exec failed'];
  }

  $json = json_decode($raw, true);
  if (!is_array($json)) {
    return ['ok' => false, 'status' => $code, 'error' => 'Invalid JSON from Shopify', 'raw' => $raw];
  }

  if ($code < 200 || $code >= 300) {
    return ['ok' => false, 'status' => $code, 'error' => 'Non-2xx from Shopify', 'response' => $json];
  }

  return ['ok' => true, 'status' => $code, 'response' => $json];
}

function graphql_errors(?array $json): array {
  if (!is_array($json)) return [];
  $errs = $json['errors'] ?? null;
  return is_array($errs) ? $errs : [];
}

function edges_to_records(array $edges): array {
  $records = [];
  if (!is_array($edges)) return $records;
  foreach ($edges as $edge) {
    $node = $edge['node'] ?? null;
    if (!is_array($node)) continue;
    $out = ['handle' => $node['handle'] ?? ''];
    $fields = $node['fields'] ?? [];
    if (is_array($fields)) {
      foreach ($fields as $f) {
        if (!is_array($f)) continue;
        $k = $f['key'] ?? null;
        if (!is_string($k) || $k === '') continue;
        $out[$k] = $f['value'] ?? null;
      }
    }
    $records[] = $out;
  }
  return $records;
}


$shopDomain = norm_shop_domain(env('SHOPIFY_STORE', env('SHOPIFY_SHOP_DOMAIN','g1hj10-vk.myshopify.com')));
$adminToken = env('SHOPIFY_ADMIN_ACCESS_TOKEN','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');
$apiVersion = env('API_VERSION', env('SHOPIFY_API_VERSION', '2024-10'));

if ($shopDomain === '' || $adminToken === '') {
  json_out(500, ['error' => 'SERVER_NOT_CONFIGURED']);
}

$type = 'pricing_rule_screen_storm';
$first = 10;

$debug = isset($_GET['debug']) && (string)$_GET['debug'] === '1';
// Optional filter: /apps/door-config/pricing-rule-screen-storm?table_key=thickness_36_36_82_82
$wantTableKey = isset($_GET['table_key']) ? trim((string)$_GET['table_key']) : '';
// Dynamic range match:
//   /pricing-rule-screen-storm.php?prefix=screen_storm_slab_only_oversized&width=38&height=84
$wantPrefix = isset($_GET['prefix']) ? trim((string)$_GET['prefix']) : '';
$wantWidth = isset($_GET['width']) ? (float)$_GET['width'] : null;
$wantHeight = isset($_GET['height']) ? (float)$_GET['height'] : null;
// Optional door thickness (decimal inches), e.g. 2.25 for 2 1/4:
//   ...&prefix=exterior_slab_oversized&width=30&height=98&thickness=2.25
$wantThickness = null;
if (isset($_GET['thickness']) && trim((string)$_GET['thickness']) !== '') {
  $wantThickness = (float)$_GET['thickness'];
}

function extract_price_value($priceField) {
  // `price` is often stored as a JSON string: {"amount":"220.00","currency_code":"USD"}
  if ($priceField == null) return null;
  if (is_string($priceField)) {
    $s = trim($priceField);
    if ($s === '') return null;
    $decoded = json_decode($s, true);
    if (is_array($decoded) && isset($decoded['amount'])) return $decoded['amount'];
    return $s;
  }
  if (is_array($priceField) && isset($priceField['amount'])) return $priceField['amount'];
  return $priceField;
}

function norm_key(string $s): string {
  $s = strtolower(trim($s));
  // treat underscores and spaces as dashes for comparisons
  $s = preg_replace('/[\\s_]+/', '-', $s);
  // collapse multiple dashes
  $s = preg_replace('/-+/', '-', $s);
  return trim($s, '-');
}

function to_float($v): ?float {
  if ($v === null) return null;
  if (is_numeric($v)) return (float)$v;
  if (is_string($v)) {
    $s = trim($v);
    if ($s === '') return null;
    $s = preg_replace('/[^0-9.\\-]+/', '', $s);
    if ($s === '' || $s === '-' || $s === '.' || $s === '-.') return null;
    return is_numeric($s) ? (float)$s : null;
  }
  return null;
}

function record_matches_range(array $r, string $prefix, float $w, float $h): bool {
  $tk = (string)($r['table_key'] ?? '');
  if ($tk === '') return false;
  // prefix match (normalize underscore/dash)
  if (norm_key($tk) !== norm_key($prefix) && strpos(norm_key($tk), norm_key($prefix) . '-') !== 0) {
    return false;
  }
  $wMin = to_float($r['width_min'] ?? null);
  $wMax = to_float($r['width_max'] ?? null);
  $hMin = to_float($r['height_min'] ?? null);
  $hMax = to_float($r['height_max'] ?? null);
  if ($wMin === null || $wMax === null || $hMin === null || $hMax === null) return false;
  return ($w >= $wMin && $w <= $wMax && $h >= $hMin && $h <= $hMax);
}

/**
 * Parse thickness from metaobject string/number (e.g. "2.25", "2 1/4", "2-1/4").
 */
function parse_thickness_value($v): ?float {
  if ($v === null) return null;
  if (is_int($v) || is_float($v)) {
    $x = (float)$v;
    return is_finite($x) ? $x : null;
  }
  $s = trim((string)$v);
  if ($s === '') return null;
  // "2 1/4" (whole + fraction)
  if (preg_match('/^(\d+)\s+(\d+)\s*\/\s*(\d+)$/u', $s, $m)) {
    $whole = (float)$m[1];
    $num = (float)$m[2];
    $den = (float)$m[3];
    if ($den === 0.0) return null;
    return $whole + ($num / $den);
  }
  // "2-1/4"
  if (preg_match('/^(\d+)\s*-\s*(\d+)\s*\/\s*(\d+)$/u', $s, $m)) {
    $whole = (float)$m[1];
    $num = (float)$m[2];
    $den = (float)$m[3];
    if ($den === 0.0) return null;
    return $whole + ($num / $den);
  }
  return to_float($s);
}

/**
 * When $wantThickness is set: records with a thickness constraint must match;
 * records with no thickness fields still match (backward compatible).
 */
function record_matches_thickness(array $r, ?float $wantThickness, float $eps = 0.02): bool {
  if ($wantThickness === null) return true;

  $tMin = parse_thickness_value($r['thickness_min'] ?? null);
  if ($tMin === null) $tMin = parse_thickness_value($r['thickness_in_min'] ?? null);
  $tMax = parse_thickness_value($r['thickness_max'] ?? null);
  if ($tMax === null) $tMax = parse_thickness_value($r['thickness_in_max'] ?? null);

  if ($tMin !== null && $tMax !== null) {
    return ($wantThickness >= $tMin - $eps && $wantThickness <= $tMax + $eps);
  }

  $single = parse_thickness_value($r['thickness'] ?? null);
  if ($single === null) $single = parse_thickness_value($r['door_thickness'] ?? null);
  if ($single === null) $single = parse_thickness_value($r['thickness_in'] ?? null);
  if ($single === null) $single = parse_thickness_value($r['thickness_inches'] ?? null);

  // No thickness on this rule row => still eligible (width/height only).
  if ($single === null) return true;

  return abs($single - $wantThickness) <= $eps;
}

/** True if the metaobject row defines any thickness-related field. */
function record_has_thickness_field(array $r): bool {
  foreach (['thickness', 'door_thickness', 'thickness_in', 'thickness_inches', 'thickness_min', 'thickness_max', 'thickness_in_min', 'thickness_in_max'] as $k) {
    $v = $r[$k] ?? null;
    if ($v === null) continue;
    if (trim((string)$v) !== '') return true;
  }
  return false;
}

function record_matches_range_and_thickness(array $r, string $prefix, float $w, float $h, ?float $wantThickness): bool {
  if (!record_matches_range($r, $prefix, $w, $h)) return false;
  return record_matches_thickness($r, $wantThickness);
}

function record_range_score(array $r): float {
  // Smaller ranges are preferred (more specific match).
  $wMin = to_float($r['width_min'] ?? null);
  $wMax = to_float($r['width_max'] ?? null);
  $hMin = to_float($r['height_min'] ?? null);
  $hMax = to_float($r['height_max'] ?? null);
  if ($wMin === null || $wMax === null || $hMin === null || $hMax === null) return 1e18;
  return abs($wMax - $wMin) + abs($hMax - $hMin);
}

// Primary: read via definition (recommended). Requires read_metaobject_definitions + read_metaobjects.
$queryByDef = <<<'GQL'
query MetaobjectDefinitionEntries($type: String!, $first: Int!) {
  metaobjectDefinitionByType(type: $type) {
    type
    name
    metaobjectsCount
    metaobjects(first: $first) {
      edges {
        node {
          handle
          fields { key value }
        }
      }
    }
  }
}
GQL;

$res = shopify_admin_graphql($shopDomain, $adminToken, $apiVersion, $queryByDef, [
  'type' => $type,
  'first' => $first,
]);

if (!$res['ok']) {
  json_out(502, [
    'error' => 'SHOPIFY_ERROR',
    'status' => $res['status'],
    'detail' => $res['error'] ?? 'Unknown',
    // Helpful for debugging auth/scope issues (remove once stable).
    'shopify_response' => $res['response'] ?? null,
  ]);
}

$resp = $res['response'] ?? [];
$def = $resp['data']['metaobjectDefinitionByType'] ?? null;
$edges = [];
$resolvedType = $type;
$metaobjectsCount = null;
$definitionName = null;
$gqlErrors = graphql_errors(is_array($resp) ? $resp : null);

if (is_array($def)) {
  $resolvedType = (string)($def['type'] ?? $type);
  $definitionName = $def['name'] ?? null;
  $metaobjectsCount = $def['metaobjectsCount'] ?? null;
  $mo = $def['metaobjects'] ?? null;
  if (is_array($mo)) {
    $edges = $mo['edges'] ?? [];
  }
}

$records = edges_to_records(is_array($edges) ? $edges : []);

// Fallback: legacy metaobjects(type:...) query (same as older scripts).
if (count($records) === 0) {
  $queryLegacy = <<<'GQL'
query MetaobjectsLegacy($type: String!, $first: Int!) {
  metaobjects(type: $type, first: $first) {
    edges {
      node {
        handle
        fields { key value }
      }
    }
  }
}
GQL;
  $res2 = shopify_admin_graphql($shopDomain, $adminToken, $apiVersion, $queryLegacy, [
    'type' => $type,
    'first' => $first,
  ]);
  if ($res2['ok']) {
    $resp2 = $res2['response'] ?? [];
    $errs2 = graphql_errors(is_array($resp2) ? $resp2 : null);
    if ($errs2) {
      $gqlErrors = array_merge($gqlErrors, $errs2);
    }
    $edges2 = $res2['response']['data']['metaobjects']['edges'] ?? [];
    $records = edges_to_records(is_array($edges2) ? $edges2 : []);
  }
}

$matched = null;
if ($wantTableKey !== '') {
  // When searching for a specific key, page through results until found.
  // We use legacy pagination query to avoid needing metaobject definition scopes.
  $queryPage = <<<'GQL'
query MetaobjectsPage($type: String!, $first: Int!, $after: String) {
  metaobjects(type: $type, first: $first, after: $after) {
    pageInfo { hasNextPage endCursor }
    edges {
      node { handle fields { key value } }
    }
  }
}
GQL;

  $after = null;
  $found = null;
  $scanned = 0;
  $pageSize = 250;
  $maxPages = 50; // safety cap (up to 12,500 records)

  for ($p = 0; $p < $maxPages; $p++) {
    $resP = shopify_admin_graphql($shopDomain, $adminToken, $apiVersion, $queryPage, [
      'type' => $type,
      'first' => $pageSize,
      'after' => $after,
    ]);
    if (!$resP['ok']) break;
    $respP = $resP['response'] ?? [];
    $errsP = graphql_errors(is_array($respP) ? $respP : null);
    if ($errsP) $gqlErrors = array_merge($gqlErrors, $errsP);

    $conn = $respP['data']['metaobjects'] ?? null;
    $edgesP = (is_array($conn) ? ($conn['edges'] ?? []) : []);
    $batch = edges_to_records(is_array($edgesP) ? $edgesP : []);
    $wantNorm = norm_key($wantTableKey);
    foreach ($batch as $r) {
      $scanned++;
      if (!is_array($r)) continue;
      $rk = (string)($r['table_key'] ?? '');
      $rh = (string)($r['handle'] ?? '');
      if ($rk === $wantTableKey || $rh === $wantTableKey || norm_key($rk) === $wantNorm || norm_key($rh) === $wantNorm) {
        $found = $r;
        break 2;
      }
    }

    $pi = is_array($conn) ? ($conn['pageInfo'] ?? []) : [];
    $hasNext = (bool)(is_array($pi) ? ($pi['hasNextPage'] ?? false) : false);
    $after = is_array($pi) ? ($pi['endCursor'] ?? null) : null;
    if (!$hasNext || !$after) break;
  }

  $records = $found ? [$found] : [];
  $matched = (bool)$found;
}

$rangeMatchedCount = null;
if ($wantPrefix !== '' && $wantWidth !== null && $wantHeight !== null) {
  // Page through and collect all matching range entries.
  $queryPage = <<<'GQL'
query MetaobjectsPage($type: String!, $first: Int!, $after: String) {
  metaobjects(type: $type, first: $first, after: $after) {
    pageInfo { hasNextPage endCursor }
    edges {
      node { handle fields { key value } }
    }
  }
}
GQL;

  $after = null;
  $pageSize = 250;
  $maxPages = 50;
  $matches = [];
  $best = null;
  $bestScore = 1e18;
  $scanned = 0;

  for ($p = 0; $p < $maxPages; $p++) {
    $resP = shopify_admin_graphql($shopDomain, $adminToken, $apiVersion, $queryPage, [
      'type' => $type,
      'first' => $pageSize,
      'after' => $after,
    ]);
    if (!$resP['ok']) break;
    $respP = $resP['response'] ?? [];
    $errsP = graphql_errors(is_array($respP) ? $respP : null);
    if ($errsP) $gqlErrors = array_merge($gqlErrors, $errsP);

    $conn = $respP['data']['metaobjects'] ?? null;
    $edgesP = (is_array($conn) ? ($conn['edges'] ?? []) : []);
    $batch = edges_to_records(is_array($edgesP) ? $edgesP : []);

    foreach ($batch as $r) {
      $scanned++;
      if (!is_array($r)) continue;
      // Match ONLY by prefix + width + height (backward compatible behavior).
      // `thickness` query param is for returning thickness info, not filtering matches.
      if (record_matches_range($r, $wantPrefix, (float)$wantWidth, (float)$wantHeight)) {
        $r['price_extracted'] = extract_price_value($r['price'] ?? null);
        $matches[] = $r;
        $score = record_range_score($r);
        if ($score < $bestScore) {
          $bestScore = $score;
          $best = $r;
        }
      }
    }

    $pi = is_array($conn) ? ($conn['pageInfo'] ?? []) : [];
    $hasNext = (bool)(is_array($pi) ? ($pi['hasNextPage'] ?? false) : false);
    $after = is_array($pi) ? ($pi['endCursor'] ?? null) : null;
    if (!$hasNext || !$after) break;
  }

  $rangeMatchedCount = count($matches);
  // Return only the single best match in `records`.
  $records = $best ? [$best] : [];
}

$out = [
  'type' => $type,
  'resolved_type' => $resolvedType,
  'definition_name' => $definitionName,
  'metaobjects_count' => $metaobjectsCount,
  'requested_first' => $first,
  'count' => count($records),
  'records' => $records,
];

if ($wantTableKey !== '') {
  $out['table_key'] = $wantTableKey;
  $out['matched'] = $matched;
  $r0 = ($matched && isset($records[0]) && is_array($records[0])) ? $records[0] : null;
  $out['price'] = $r0 ? extract_price_value($r0['price'] ?? null) : null;
  $out['price_prehung'] = $r0 ? extract_price_value($r0['price_prehung'] ?? null) : null;
  if ($r0) {
    $raw =
      $r0['thickness'] ?? ($r0['door_thickness'] ?? ($r0['thickness_in'] ?? ($r0['thickness_inches'] ?? null)));
    if ($raw === null || trim((string)$raw) === '') {
      $raw = $r0['thickness_min'] ?? ($r0['thickness_in_min'] ?? null);
    }
    $out['matched_thickness_raw'] = $raw;
    $out['matched_thickness'] = $raw !== null ? parse_thickness_value($raw) : null;
  }
}

if ($wantPrefix !== '' && $wantWidth !== null && $wantHeight !== null) {
  $out['prefix'] = $wantPrefix;
  $out['width'] = $wantWidth;
  $out['height'] = $wantHeight;
  if ($wantThickness !== null) {
    // Echo what was requested (avoid confusing it with the matched entry's thickness field).
    $out['requested_thickness'] = $wantThickness;
  }
  $out['matched_count'] = $rangeMatchedCount;
  $out['matched'] = count($records) > 0;
  $out['price'] = (count($records) > 0) ? extract_price_value($records[0]['price'] ?? null) : null;

  // Return thickness from the matched entry (if present on the metaobject row).
  if (count($records) > 0) {
    $r0 = $records[0];
    $raw =
      $r0['thickness'] ?? ($r0['door_thickness'] ?? ($r0['thickness_in'] ?? ($r0['thickness_inches'] ?? null)));
    if ($raw === null || trim((string)$raw) === '') {
      // Try range-style fields as a fallback.
      $raw = $r0['thickness_min'] ?? ($r0['thickness_in_min'] ?? null);
    }
    $out['matched_thickness_raw'] = $raw;
    $out['matched_thickness'] = $raw !== null ? parse_thickness_value($raw) : null;
  }
}

if ($debug || $gqlErrors) {
  $out['graphql_errors'] = $gqlErrors;
}

json_out(200, $out);

