<?php
/**
 * OpenCart CSV Export: explode by option combinations (NO PREFIX).
 * Совместимо со старыми PHP (без стрелочных функций и строгих типов).
 *
 * Фичи:
 * - Без префикса таблиц; таблицы экранируем бектиками (важно для `option`).
 * - Категории: в одной ячейке, каждая строка — путь, уровни через "|", с префиксами.
 * - stock_status_id — отдельная колонка.
 * - SKU и URL нумеруются _1, _2, ... если есть комбинации опций.
 * - Если есть related: в UPC пишем model первого related по возрастанию.
 * - additional_images: все доп. картинки через запятую без пробелов.
 * - attributes: "Группа|Атрибут|Значение" построчно; в конце добавляем "Специфікація|Опції|..." если есть комбо.
 * - Автодетект отсутствия pov.sort_order и product_image.sort_order.
 */

ini_set('memory_limit', '1024M');
ini_set('display_errors', 1);
error_reporting(E_ALL);

$options = getopt('', array(
  'db_host::', 'db_user::', 'db_pass::', 'db_name::', 'db_port::',
  'lang::', 'store::', 'outfile::'
));

$DB_HOST   = isset($options['db_host']) ? $options['db_host'] : '127.0.0.1';
$DB_USER   = isset($options['db_user']) ? $options['db_user'] : 'root';
$DB_PASS   = isset($options['db_pass']) ? $options['db_pass'] : '';
$DB_NAME   = isset($options['db_name']) ? $options['db_name'] : 'opencart';
$DB_PORT   = isset($options['db_port']) ? (int)$options['db_port'] : 3306;

$LANG_ID   = isset($options['lang'])  ? (int)$options['lang']  : 1;
$STORE_ID  = isset($options['store']) ? (int)$options['store'] : 0;
$OUTFILE   = isset($options['outfile']) ? $options['outfile'] : ('export_' . date('Ymd_His') . '.csv');

$OPTION_TYPES = array('select','radio','image','color');

$NAME_SUFFIX_FMT      = '[%s]';
$NAME_PAIR_SEP        = '; ';
$NAME_KV_SEP          = ': ';
$CATEGORY_LEVEL_SEP   = '|';   // между уровнями
$CATEGORIES_ROW_SEP   = "\n";  // каждая категория — с новой строки
$ATTR_ROW_SEP         = "\n";
$ATTR_GROUP_FOR_OPTS  = 'Специфікація';
$ATTR_NAME_FOR_OPTS   = 'Опції';

$mysqli = new mysqli($DB_HOST, $DB_USER, $DB_PASS, $DB_NAME, $DB_PORT);
if ($mysqli->connect_errno) { fwrite(STDERR, "DB connect error: " . $mysqli->connect_error . PHP_EOL); exit(1); }
$mysqli->set_charset('utf8mb4');

function q($sql){
  global $mysqli;
  $res = $mysqli->query($sql);
  if (!$res) {
    throw new RuntimeException("SQL error: {$mysqli->error}\nSQL: $sql");
  }
  return $res;
}
function esc($v){
  global $mysqli;
  return "'" . $mysqli->real_escape_string($v) . "'";
}
function tn($t){ return '`' . $t . '`'; }
function tableExists($t){ return q("SHOW TABLES LIKE " . esc($t))->num_rows > 0; }
function columnExists($t,$c){ return q("SHOW COLUMNS FROM " . tn($t) . " LIKE " . esc($c))->num_rows > 0; }

/* ---------- Справочники ---------- */
$brandById = array();
if (tableExists('manufacturer')) {
  $res = q("SELECT manufacturer_id, name FROM " . tn('manufacturer'));
  while ($row = $res->fetch_assoc()) { $brandById[(int)$row['manufacturer_id']] = $row['name']; }
  $res->free();
}

/* Категории: имена и части пути (по language_id) */
$catNameById = array();
$catPathPartsById = array(); // category_id => [Root, Sub, Leaf]
if (tableExists('category') && tableExists('category_description')) {
  $res = q("SELECT c.category_id, cd.name
            FROM " . tn('category') . " c
            JOIN " . tn('category_description') . " cd
              ON cd.category_id=c.category_id AND cd.language_id={$GLOBALS['LANG_ID']}");
  while ($row = $res->fetch_assoc()) { $catNameById[(int)$row['category_id']] = $row['name']; }
  $res->free();

  if (tableExists('category_path')) {
    $res = q("SELECT cp.category_id, cp.path_id, cp.level
              FROM " . tn('category_path') . " cp
              ORDER BY cp.category_id, cp.level");
    while ($row = $res->fetch_assoc()) {
      $cid = (int)$row['category_id'];
      $pid = (int)$row['path_id'];
      $name = isset($catNameById[$pid]) ? $catNameById[$pid] : null;
      if ($name) { $catPathPartsById[$cid][] = $name; }
    }
    $res->free();
  }
}

/* SEO */
$seoMap = array(); // "product_id=123" => keyword
$hasSeoUrl   = tableExists('seo_url');
$hasUrlAlias = tableExists('url_alias');
if ($hasSeoUrl) {
  $res = q("SELECT query, keyword FROM " . tn('seo_url') . " WHERE store_id={$STORE_ID} AND language_id={$LANG_ID}");
  while ($row = $res->fetch_assoc()) { $seoMap[$row['query']] = $row['keyword']; }
  $res->free();
} elseif ($hasUrlAlias) {
  $res = q("SELECT query, keyword FROM " . tn('url_alias'));
  while ($row = $res->fetch_assoc()) { $seoMap[$row['query']] = $row['keyword']; }
  $res->free();
}
function buildProductUrl($product_id){
  global $seoMap;
  $key = "product_id=".(int)$product_id;
  if (!empty($seoMap[$key])) {
    $kw = ltrim($seoMap[$key], '/');
    return '/'.$kw;
  }
  return '/index.php?route=product/product&product_id='.(int)$product_id;
}

/* Опции — имена */
$optionName = array();
$optionValueName = array();
if (tableExists('option_description')) {
  $res = q("SELECT option_id, name FROM " . tn('option_description') . " WHERE language_id={$LANG_ID}");
  while ($row = $res->fetch_assoc()) { $optionName[(int)$row['option_id']] = $row['name']; }
  $res->free();
}
if (tableExists('option_value_description')) {
  $res = q("SELECT option_value_id, name FROM " . tn('option_value_description') . " WHERE language_id={$LANG_ID}");
  while ($row = $res->fetch_assoc()) { $optionValueName[(int)$row['option_value_id']] = $row['name']; }
  $res->free();
}

/* Атрибуты — имена групп и атрибутов */
$attrGroupName = array();     // attribute_group_id => name
$attrName = array();          // attribute_id => name
if (tableExists('attribute_group_description')) {
  $res = q("SELECT attribute_group_id, name FROM " . tn('attribute_group_description') . " WHERE language_id={$LANG_ID}");
  while ($row = $res->fetch_assoc()) { $attrGroupName[(int)$row['attribute_group_id']] = $row['name']; }
  $res->free();
}
if (tableExists('attribute_description')) {
  $res = q("SELECT attribute_id, name FROM " . tn('attribute_description') . " WHERE language_id={$LANG_ID}");
  while ($row = $res->fetch_assoc()) { $attrName[(int)$row['attribute_id']] = $row['name']; }
  $res->free();
}
$attrToGroup = array(); // attribute_id => attribute_group_id
if (tableExists('attribute')) {
  $res = q("SELECT attribute_id, attribute_group_id FROM " . tn('attribute'));
  while ($row = $res->fetch_assoc()) { $attrToGroup[(int)$row['attribute_id']] = (int)$row['attribute_group_id']; }
  $res->free();
}

/* ---------- Хелперы ---------- */
function loadProductVariantOptionBuckets($product_id){
  global $OPTION_TYPES, $optionName, $optionValueName;
  $res = q("SELECT po.product_option_id, po.option_id, o.type
            FROM " . tn('product_option') . " po
            JOIN " . tn('option') . " o ON o.option_id=po.option_id
            WHERE po.product_id=".(int)$product_id);
  $po = array();
  while ($row = $res->fetch_assoc()) {
    if (in_array($row['type'], $OPTION_TYPES, true)) {
      $po[] = array(
        'product_option_id' => (int)$row['product_option_id'],
        'option_id'         => (int)$row['option_id'],
        'type'              => $row['type']
      );
    }
  }
  $res->free();
  if (!$po) return array();

  $hasPovSort = columnExists('product_option_value','sort_order');
  $orderClause = $hasPovSort ? 'pov.sort_order, pov.product_option_value_id' : 'pov.product_option_value_id';

  $buckets = array();
  foreach ($po as $entry) {
    $pid = $entry['product_option_id'];
    $oid = $entry['option_id'];
    $optName = isset($optionName[$oid]) ? $optionName[$oid] : ('Option#'.$oid);

    $sql = "SELECT pov.product_option_value_id, pov.option_value_id, pov.quantity, pov.subtract,
                   pov.price, pov.price_prefix, pov.points, pov.points_prefix, pov.weight, pov.weight_prefix
            FROM " . tn('product_option_value') . " pov
            WHERE pov.product_option_id={$pid}
            ORDER BY {$orderClause}";
    $res2 = q($sql);
    $vals = array();
    while ($r = $res2->fetch_assoc()) {
      $ovId = (int)$r['option_value_id'];
      $vals[] = array(
        'option_id'         => $oid,
        'option_name'       => $optName,
        'option_value_id'   => $ovId,
        'option_value_name' => isset($optionValueName[$ovId]) ? $optionValueName[$ovId] : ('Value#'.$ovId),
        'price'             => (float)$r['price'],
        'price_prefix'      => $r['price_prefix'] ? $r['price_prefix'] : '+',
        'weight'            => (float)$r['weight'],
        'weight_prefix'     => $r['weight_prefix'] ? $r['weight_prefix'] : '+',
        'points'            => (int)$r['points'],
        'points_prefix'     => $r['points_prefix'] ? $r['points_prefix'] : '+',
      );
    }
    $res2->free();
    if ($vals) $buckets[] = $vals;
  }
  return $buckets;
}

function cartesianProduct($arrays){
  $result = array(array());
  foreach ($arrays as $propertyValues) {
    $tmp = array();
    foreach ($result as $product) {
      foreach ($propertyValues as $value) {
        $tmp[] = array_merge($product, array($value));
      }
    }
    $result = $tmp;
  }
  return $result;
}

function applyOptionCombination($base, $combo){
  if ($combo) {
    $pairs = array();
    foreach ($combo as $v) { $pairs[] = $v['option_name'] . $GLOBALS['NAME_KV_SEP'] . $v['option_value_name']; }
    $suffix = sprintf($GLOBALS['NAME_SUFFIX_FMT'], implode($GLOBALS['NAME_PAIR_SEP'], $pairs));
    $base['name_with_options'] = trim($base['name'].' '.$suffix);
  } else {
    $base['name_with_options'] = $base['name'];
  }
  $price = (float)$base['price'];
  foreach ($combo as $v) {
    if ((float)$v['price'] != 0.0) {
      if ($v['price_prefix'] === '+') $price += (float)$v['price'];
      elseif ($v['price_prefix'] === '-') $price -= (float)$v['price'];
    }
  }
  $base['price_with_options'] = $price;

  if ($combo) {
    $ov = array();
    foreach ($combo as $v) { $ov[] = $v['option_name'] . $GLOBALS['NAME_KV_SEP'] . $v['option_value_name']; }
    $base['option_combo'] = implode($GLOBALS['NAME_PAIR_SEP'], $ov);
  } else {
    $base['option_combo'] = '';
  }
  return $base;
}

function loadEffectiveSpecial($product_id, $customer_group_id = 1){
  $today = date('Y-m-d');
  $sql = "SELECT ps.price
          FROM " . tn('product_special') . " ps
          WHERE ps.product_id=".(int)$product_id."
            AND ps.customer_group_id=".(int)$customer_group_id."
            AND (ps.date_start='0000-00-00' OR ps.date_start IS NULL OR ps.date_start<='{$today}')
            AND (ps.date_end='0000-00-00' OR ps.date_end IS NULL OR ps.date_end>='{$today}')
          ORDER BY ps.priority ASC, ps.price ASC
          LIMIT 1";
  $res = q($sql); $row = $res->fetch_assoc(); $res->free();
  return $row ? (float)$row['price'] : null;
}

/* Категории: каждая строка — путь, уровни через |, с префиксами */
function loadProductCategoryLines($product_id){
  global $catPathPartsById, $CATEGORY_LEVEL_SEP, $CATEGORIES_ROW_SEP;
  if (!$catPathPartsById) return '';
  $res = q("SELECT category_id FROM " . tn('product_to_category') . " WHERE product_id=".(int)$product_id);
  $lines = array();
  while ($row = $res->fetch_assoc()) {
    $cid = (int)$row['category_id'];
    if (!empty($catPathPartsById[$cid])) {
      $parts = $catPathPartsById[$cid];
      $n = count($parts);
      for ($k=1; $k<=$n; $k++) {
        $lines[] = implode($CATEGORY_LEVEL_SEP, array_slice($parts, 0, $k));
      }
    }
  }
  $res->free();
  $lines = array_values(array_unique($lines));
  return implode($CATEGORIES_ROW_SEP, $lines);
}

/* Related */
function loadRelatedIdsArr($product_id){
  if (!tableExists('product_related')) return array();
  $res = q("SELECT related_id FROM " . tn('product_related') . " WHERE product_id=".(int)$product_id." ORDER BY related_id");
  $ids = array(); while ($row = $res->fetch_assoc()) { $ids[] = (int)$row['related_id']; }
  $res->free();
  return $ids;
}
function relatedIdsCsv($ids){ return $ids ? implode(',', $ids) : ''; }
function getModelByProductId($pid){
  $res = q("SELECT model FROM " . tn('product') . " WHERE product_id=".(int)$pid." LIMIT 1");
  $row = $res->fetch_assoc(); $res->free();
  return $row ? (string)$row['model'] : '';
}

/* Доп. изображения */
function loadAdditionalImagesCsv($product_id){
  if (!tableExists('product_image')) return '';
  $order = columnExists('product_image', 'sort_order') ? 'sort_order, product_image_id' : 'product_image_id';
  $res = q("SELECT image FROM " . tn('product_image') . " WHERE product_id=".(int)$product_id." ORDER BY {$order}");
  $imgs = array();
  while ($row = $res->fetch_assoc()) {
    $img = trim((string)$row['image']);
    if ($img !== '') { $imgs[] = $img; }
  }
  $res->free();
  return implode(',', $imgs);
}

/* Атрибуты товара (без опций) — "Группа|Аттрибут|Значение" построчно */
function loadProductAttributesBlock($product_id, $lang_id){
  if (!tableExists('product_attribute')) return '';
  $res = q("SELECT attribute_id, text FROM " . tn('product_attribute') . " WHERE product_id=".(int)$product_id." AND language_id=".(int)$lang_id);
  $rows = array();
  global $attrToGroup, $attrGroupName, $attrName, $ATTR_ROW_SEP;
  while ($r = $res->fetch_assoc()) {
    $aid = (int)$r['attribute_id'];
    $txt = trim((string)$r['text']);
    $gId = isset($attrToGroup[$aid]) ? $attrToGroup[$aid] : null;
    $gName = ($gId !== null && isset($attrGroupName[$gId])) ? $attrGroupName[$gId] : '';
    $aName = isset($attrName[$aid]) ? $attrName[$aid] : ('attr#'.$aid);
    if ($aName !== '' && $txt !== '') {
      $rows[] = $gName.'|'.$aName.'|'.$txt;
    }
  }
  $res->free();
  sort($rows, SORT_NATURAL | SORT_FLAG_CASE);
  return implode($ATTR_ROW_SEP, $rows);
}

/* Добавить строку-опции в блок атрибутов (в конец), если есть комбо */
function appendOptionsToAttributes($attributesBlock, $optionCombo){
  global $ATTR_ROW_SEP, $ATTR_GROUP_FOR_OPTS, $ATTR_NAME_FOR_OPTS;
  if ($optionCombo === '' || $optionCombo === null) return $attributesBlock;
  $optLine = $ATTR_GROUP_FOR_OPTS.'|'.$ATTR_NAME_FOR_OPTS.'|'.$optionCombo;
  if ($attributesBlock === '') return $optLine;
  return $attributesBlock . $ATTR_ROW_SEP . $optLine;
}

/* ---------- Проверка обязательных таблиц ---------- */
$must = array('product','product_description','product_to_store');
for ($i=0; $i<count($must); $i++) {
  $t = $must[$i];
  if (!tableExists($t)) { fwrite(STDERR,"Fatal: required table '{$t}' not found.\n"); exit(1); }
}

/* ---------- Главный SELECT ---------- */
$sqlProducts = "
SELECT
  p.product_id, p.model, p.sku, p.upc, p.ean, p.jan, p.isbn, p.mpn,
  p.location, p.quantity, p.stock_status_id, p.image, p.manufacturer_id,
  p.price, p.tax_class_id, p.weight, p.weight_class_id, p.length, p.width, p.height, p.length_class_id,
  p.status, p.date_added, p.date_modified, p.shipping,
  pd.name, pd.description, pd.meta_title, pd.meta_description, pd.meta_keyword
FROM " . tn('product') . " p
JOIN " . tn('product_description') . " pd
  ON pd.product_id=p.product_id AND pd.language_id={$LANG_ID}
JOIN " . tn('product_to_store') . " pts
  ON pts.product_id=p.product_id AND pts.store_id={$STORE_ID}
ORDER BY p.product_id ASC";
$resP = q($sqlProducts);

/* ---------- CSV ---------- */
$fp = fopen($OUTFILE, 'w');
if (!$fp) { fwrite(STDERR,"Cannot open outfile: {$OUTFILE}\n"); exit(1); }

$headers = array(
  'product_id','model','sku','upc','ean','jan','isbn','mpn','location',
  'manufacturer','categories','related_ids',
  'name','name_with_options','option_combo',
  'price','price_with_options','special_price',
  'quantity','stock_status_id','status','shipping',
  'image','additional_images','url',
  'meta_title','meta_description','meta_keyword',
  'attributes',
  'description'
);
fputcsv($fp, $headers);

/* ---------- Потоковая выгрузка ---------- */
$rows = 0;
while ($p = $resP->fetch_assoc()) {
  $pid = (int)$p['product_id'];

  /* related */
  $relIds = loadRelatedIdsArr($pid);
  $relCsv = relatedIdsCsv($relIds);
  $groupUPC = '';
  if ($relIds) {
    $firstRel = $relIds[0];
    $groupUPC = getModelByProductId($firstRel);
  }

  /* Доп. изображения и атрибуты (базовые, без опций) */
  $additionalImages = loadAdditionalImagesCsv($pid);
  $attributesBase = loadProductAttributesBlock($pid, $LANG_ID);

  $base = array(
    'product_id'        => $pid,
    'model'             => $p['model'],
    'sku'               => $p['sku'],
    'upc'               => ($groupUPC !== '' ? $groupUPC : $p['upc']),
    'ean'               => $p['ean'],
    'jan'               => $p['jan'],
    'isbn'              => $p['isbn'],
    'mpn'               => $p['mpn'],
    'location'          => $p['location'],
    'manufacturer'      => isset($brandById[(int)$p['manufacturer_id']]) ? $brandById[(int)$p['manufacturer_id']] : '',
    'categories'        => loadProductCategoryLines($pid),
    'related_ids'       => $relCsv,
    'name'              => $p['name'],
    'price'             => (float)$p['price'],
    'quantity'          => (int)$p['quantity'],
    'stock_status_id'   => (int)$p['stock_status_id'],
    'status'            => (int)$p['status'],
    'shipping'          => (int)$p['shipping'],
    'image'             => $p['image'],
    'additional_images' => $additionalImages,
    'url'               => buildProductUrl($pid),
    'meta_title'        => $p['meta_title'],
    'meta_description'  => $p['meta_description'],
    'meta_keyword'      => $p['meta_keyword'],
    'attributes_base'   => $attributesBase,
    'description'       => html_entity_decode(strip_tags($p['description']), ENT_QUOTES, 'UTF-8'),
  );

  $base['special_price'] = loadEffectiveSpecial($pid, 1);

  $buckets = loadProductVariantOptionBuckets($pid);

  if (!$buckets) {
    /* без опций — SKU/URL не трогаем; атрибуты как есть */
    $with = applyOptionCombination($base, array());
    $attrsOut = appendOptionsToAttributes($with['attributes_base'], $with['option_combo']);
    $row = array(
      $with['product_id'],$with['model'],$with['sku'],$with['upc'],$with['ean'],$with['jan'],$with['isbn'],$with['mpn'],$with['location'],
      $with['manufacturer'],$with['categories'],$with['related_ids'],
      $with['name'],$with['name_with_options'],$with['option_combo'],
      $with['price'],$with['price_with_options'],$with['special_price'],
      $with['quantity'],$with['stock_status_id'],$with['status'],$with['shipping'],
      $with['image'],$with['additional_images'],$with['url'],
      $with['meta_title'],$with['meta_description'],$with['meta_keyword'],
      $attrsOut,
      $with['description']
    );
    fputcsv($fp, $row); $rows++; continue;
  }

  /* есть опции — нумеруем SKU и URL; в атрибуты добавляем строку с опциями */
  $combos = cartesianProduct($buckets);
  $idx = 1;
  foreach ($combos as $combo) {
    $with = applyOptionCombination($base, $combo);

    /* Базовый SKU (fallback) */
    $baseSku = trim((string)$with['sku']);
    if ($baseSku === '') { $baseSku = trim((string)$with['model']); }
    if ($baseSku === '') { $baseSku = 'PID'.$with['product_id']; }
    $skuOut = $baseSku . '_' . $idx;

    /* URL с суффиксом */
    $urlOut = $with['url'] . '_' . $idx;

    /* Атрибуты + последняя строка с опциями */
    $attrsOut = appendOptionsToAttributes($with['attributes_base'], $with['option_combo']);

    $row = array(
      $with['product_id'],$with['model'],$skuOut,$with['upc'],$with['ean'],$with['jan'],$with['isbn'],$with['mpn'],$with['location'],
      $with['manufacturer'],$with['categories'],$with['related_ids'],
      $with['name'],$with['name_with_options'],$with['option_combo'],
      $with['price'],$with['price_with_options'],$with['special_price'],
      $with['quantity'],$with['stock_status_id'],$with['status'],$with['shipping'],
      $with['image'],$with['additional_images'],$urlOut,
      $with['meta_title'],$with['meta_description'],$with['meta_keyword'],
      $attrsOut,
      $with['description']
    );
    fputcsv($fp, $row); $rows++; $idx++;
  }
}

$resP->free();
fclose($fp);
echo "Готово! Записано строк: {$rows}\nCSV: {$OUTFILE}\n";
