<?php

class Cart {
  protected PDO $pdo;
  protected ProductMain $product;

  protected ?int $idCustomer = null;
  protected ?int $idCurrency = null;
  protected ?string $locale = null;

  protected ?int $cartId = null;
  protected ?string $sessionId = null;

  protected int $reserveTtl = 900;
  
  private $onMutate = null;

  public function __construct(PDO $pdo, ProductMain $product) {
    $this->pdo = $pdo;
    $this->product = $product;
  }
  
  public function setMutationHook(callable $fn): void {
    $this->onMutate = $fn;
  }
  private function notifyMutate(): void {
    if ($this->onMutate) { ($this->onMutate)(); }
  }
  
  public function setContext(?int $id_customer, ?int $id_currency, ?string $locale): void {
    $this->idCustomer = $id_customer;
    $this->idCurrency = $id_currency;
    $this->locale = $locale;
  }

  public function setReserveTtl(int $seconds): void {
  	$this->reserveTtl = max(60, $seconds);
  }
  
  public function commitForOrder(?int $id_customer): bool {
	  if ($this->cartId === null) return false;
	  $data = $this->get();
	  if (!$data || empty($data['items'])) return false;
	
	  foreach ($data['items'] AS $item) {
	    $ok = $this->product->consume((int)$item['id_product'], (int)$item['quantity'], $this->cartId, $id_customer);
	    if (!$ok) {
	      return false;
	    }
	  }
	  return true;
	}
  
  public function getShortSummary(): ?array {
  	$data = $this->get();
  	if (!$data) return null;
  	$items = $data['items'] ?? [];
	  $units = 0;
	  foreach ($items AS $item) $units += (int)($item['quantity'] ?? 0);
	  return [
	    'units'      => $units,
	    'line_count' => count($items),
	    'totals'     => $data['totals'] ?? ['subtotal_net' => 0,'tax' => 0,'total_gross' => 0.0],
	  ];
  }
  
  public function getFullSummary(ProductDetails $details, int $fallback_country): ?array {
	  $data = $this->get();
	  if (!$data) return null;

	  $items = $data['items'] ?? [];
	  $units = 0;
	  

	  foreach ($items AS &$item) {
	    $quantity = max(1, (int)($item['quantity'] ?? 1));
	    $units += $quantity;
	
	    $details->setPricingContext(1, $quantity);
	    $details->setInput(['id' => (int)$item['id_product']]);
	    $display = $details->getDisplayData();
	
	    $item['title'] = $display['content']['title'] ?? ('#'.$item['id_product']);
	    $item['url'] = $display['content']['url'] ?? null;
	    $item['link'] = $display['content']['link'] ?? null;
	    $item['image'] = $display['images'][0] ?? null;
	    $item['weight'] = $display['weight'] ?? null;
	    $item['attributes'] = $display['attributes'] ?? ['tags'=>[], 'properties'=>[]];
	    $item['price'] = $display['price'] ?? null;
	    $item['stock'] = $display['stock'] ?? null;
	
	    $max_quantity = $this->product->getMaxEditableQuantity((int)$item['id_product'], $data['id']);
	
	    $reserved_by_cart = $this->product->hasReservationTable() && $max_quantity !== null
	      ? $this->product->getReservedForCart((int)$item['id_product'], $data['id'])
	      : 0;
	
	    $available_global = $max_quantity !== null ? $this->product->getAvailable((int)$item['id_product']) : null;
	
	    $item['quantity_limits'] = [
	      'max'               => $max_quantity,
	      'is_limited'        => $max_quantity !== null,
	      'reserved_by_cart'  => $reserved_by_cart,
	      'available_global'  => $available_global,
	    ];
	  }
	  unset($item);
	
	  $data['items'] = $items;
	  $data['summary'] = ['units' => $units, 'line_count' => count($items)];
	  return $data;
	}
  
  public function loadOrCreateCart(string $session_id): void {
  	$this->sessionId = $session_id;
  	$sql = "SELECT id FROM carts WHERE session_id = :session_id LIMIT 1";
  	$result = $this->pdo->prepare($sql);
  	$result->bindValue(':session_id', $session_id, PDO::PARAM_STR);
  	$result->execute();
  	$arr = $result->fetch(PDO::FETCH_ASSOC) ?: null;
  	if ($arr) {
  		$this->cartId = (int)$arr['id'];
	    $this->touch($this->cartId);
	    return;
  	}
    $this->cartId = $this->createCart($session_id, $this->idCustomer, $this->idCurrency, $this->locale);
  }
  
	public function get(): ?array {
		if ($this->cartId === null || $this->sessionId === null) return null;
		$items = $this->listItems($this->cartId);
		$totals = $this->computeTotals($items);
		return [
			'id'          => $this->cartId,
			'session_id'  => $this->sessionId,
			'id_customer' => $this->idCustomer,
			'id_currency' => $this->idCurrency,
			'locale'      => $this->locale,
			'items'       => $items,
			'totals'      => $totals,
		];
	}
  
	public function mergeCustomerCart(int $id_customer, bool $merge = true): void {
		if ($this->cartId === null) return;
		
		$sql = "UPDATE carts SET id_customer = :id_customer WHERE id = :id ";
		$result = $this->pdo->prepare($sql);
		$result->bindValue(':id_customer', $id_customer, PDO::PARAM_INT);
		$result->bindValue(':id', $this->cartId, PDO::PARAM_INT);
		$result->execute();
		
		$this->idCustomer = $id_customer;
		
		if ($merge) {
			$other_ids = $this->findOtherOpenCartsByCustomer($id_customer, $this->cartId);
			foreach ($other_ids AS $merge_id) {
				$this->mergeFromCartId((int)$merge_id);
			}
		}
		$this->touch($this->cartId);
		
	}
	
	
	
	public function remove(int $id): ?array {
		if ($this->cartId === null || $this->sessionId === null) return null;
			
		$sql = "SELECT id, id_product, quantity FROM carts_items WHERE id = :id AND id_module = :id_module LIMIT 1";
	  $result = $this->pdo->prepare($sql);
	  $result->bindValue(':id', $id, PDO::PARAM_INT);
	  $result->bindValue(':id_module', $this->cartId, PDO::PARAM_INT);
	  $result->execute();
	  $arr = $result->fetch(PDO::FETCH_ASSOC);
	  if (!$arr) return $this->get();
	
	  $sql = "DELETE FROM carts_items WHERE id = :id";
	  $result = $this->pdo->prepare($sql);
	  $result->bindValue(':id', $id, PDO::PARAM_INT);
	  $result->execute();
	
	  $this->product->releasePartial((int)$arr['id_product'], (int)$arr['quantity'], $this->cartId);
	
	  $this->touch($this->cartId);
	  $this->notifyMutate();
	  return $this->get();
	}
	
	public function clear(): void {
		if ($this->cartId !== null && $this->sessionId !== null) {
			$sql = "DELETE FROM carts_items WHERE id_module = :id_module";
			$result = $this->pdo->prepare($sql);
			$result->bindValue(':id_module', $this->cartId, PDO::PARAM_INT);
			$result->execute();
			$this->product->releaseByCart($this->cartId);
			$this->touch($this->cartId);
		}
		$this->notifyMutate();
	}
	
	public function setQuantity(int $id, int $quantity): ?array {
		if ($this->cartId === null || $this->sessionId === null) return null;
		$quantity = max(0, $quantity);
		
		$sql = "SELECT id, id_product, quantity FROM carts_items WHERE id = :id AND id_module = :id_module LIMIT 1";
	  $result = $this->pdo->prepare($sql);
	  $result->bindValue(':id', $id, PDO::PARAM_INT);
	  $result->bindValue(':id_module', $this->cartId, PDO::PARAM_INT);
	  $result->execute();
	  $arr = $result->fetch(PDO::FETCH_ASSOC);
	  if (!$arr) return $this->get();
	
	  $delta = $quantity - (int)$arr['quantity'];
	  
	  
		if ($quantity === 0) {
			$sql_del = "DELETE FROM carts_items WHERE id = :id";
			$result_del = $this->pdo->prepare($sql_del);
			$result_del->bindValue(':id', $id, PDO::PARAM_INT);
			$result_del->execute();
			$this->product->releasePartial((int)$arr['id_product'], (int)$arr['quantity'], $this->cartId);
		} else if ($delta > 0) {
			$can = $this->product->canAddToCart((int)$arr['id_product'], $delta);
			if (($can['allowed'] ?? false) !== true) {
				$reason = $can['reason'] ?? 'denied';
				return NULL;
			}
			$this->updateItemQuantity($id, $quantity);
			$this->product->reserve((int)$arr['id_product'], $delta, $this->cartId, $this->idCustomer, $this->reserveTtl);
		} elseif ($delta < 0) {
			$this->updateItemQuantity($id, $quantity);
			$this->product->releasePartial((int)$arr['id_product'], -$delta, $this->cartId);
		}
			
		$this->touch($this->cartId);
		$this->notifyMutate();
		return $this->get();
	}
	
	public function add(array $payload): ?array {
		if ($this->cartId === null || $this->sessionId === null) return null;
		
		$id_product = (int)($payload['id_product'] ?? 0);
		$quantity = max(1, (int)($payload['quantity'] ?? 1));
		if ($id_product <= 0) return NULL;
		
	  $addons_in  = $payload['addons'] ?? ($payload['addons_json'] ?? null);
		$addons_arr = is_array($addons_in) ? $this->normalizeAddons($addons_in) : null;
		$addons_json = $this->canonicalJson($addons_arr);
	  $addons_note = $payload['addons_note'] ?? null;
	  
		$note = $payload['note'] ?? null;
		
				
		$id_country = $this->product->getPricing()[0] ?? null;
		$id_country_fallback = $this->product->getPricing()[2] ?? null;
		
		
		$price = $this->product->getPriceSingle($id_product, $id_country, $quantity, $id_country_fallback);
		if (($price['unavailable'] ?? false) === true) {
			return NULL;
		}
	
		$can = $this->product->canAddToCart($id_product, $quantity);
		if (($can['allowed'] ?? false) !== true) {
			$reason = $can['reason'] ?? 'denied';
			$avail = $can['available'] ?? null;
			return NULL;
		}
	
		$sku = trim((string)($payload['sku'] ?? ''));
		if ($sku === '') {
			$sku = $this->product->getSku($id_product);
		}
		
		$tax_rate = (float)($price['vat_rate'] ?? 0.0);
		$gross = (float)$price['value'];
		$net = $this->grossToNet($gross, $tax_rate);
		
		$addons_net = 0.0;
		
		if (!$this->validateAddonsAgainstProduct($id_product, $addons_arr)) {
		  return null;
		}
		
		$sql = "SELECT * FROM carts_items 
						WHERE id_module = :id_module 
						AND id_product = :id_product 
						AND IFNULL(addons_json,'') = IFNULL(:addons_json,'')
	          AND IFNULL(addons_note,'') = IFNULL(:addons_note,'') 
	          LIMIT 1";
		$result = $this->pdo->prepare($sql);
		$result->bindValue(':id_module', $this->cartId, PDO::PARAM_INT);
		$result->bindValue(':id_product', $id_product, PDO::PARAM_INT);
		$result->bindValue(':addons_json', $addons_json, $addons_json === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
		$result->bindValue(':addons_note', $addons_note, $addons_note === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
		$result->execute();
		if ($result->rowCount()){
			$arr = $result->fetch();
			$new_quantity = (int)$arr['quantity'] + $quantity;
			$delta = $new_quantity - (int)$arr['quantity'];
			$can = $this->product->canAddToCart($id_product, $delta);
	    if (($can['allowed'] ?? false) !== true) {
	      return null;
	    }
	    $item_id = (int)$arr['id'];
			$this->updateItemQuantity($item_id, $new_quantity);
			$this->product->reserve($id_product, $delta, $this->cartId, $this->idCustomer, $this->reserveTtl);
		} else {
			$sql = "INSERT INTO carts_items SET 
							id_module = :id_module, 
							id_product = :id_product,
							quantity = :quantity,
							price_net = :price_net,
							addons_net = :addons_net,
							tax_rate = :tax_rate,
							sku = :sku, 
							addons_json = :addons_json,
							note = :note, 
							addons_note = :addons_note";
			$result = $this->pdo->prepare($sql);
			$result->bindValue(':id_module', $this->cartId, PDO::PARAM_INT);
			$result->bindValue(':id_product', $id_product, PDO::PARAM_INT);
			$result->bindValue(':quantity', $quantity, PDO::PARAM_INT);
			$result->bindValue(':price_net', $net, PDO::PARAM_STR);
			$result->bindValue(':addons_net', $addons_net, PDO::PARAM_STR);
			$result->bindValue(':tax_rate', $tax_rate, PDO::PARAM_STR);
			$result->bindValue(':sku', $sku, PDO::PARAM_STR);
			
			$result->bindValue(':addons_json', $addons_json, $addons_json === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
			if ($note === NULL) $result->bindValue(':note', NULL, PDO::PARAM_NULL);
			else $result->bindValue(':note', $note, PDO::PARAM_STR);
			if ($addons_note === NULL) $result->bindValue(':addons_note', NULL, PDO::PARAM_NULL);
			else $result->bindValue(':addons_note', $addons_note, PDO::PARAM_STR);
			$result->execute();
			$item_id = (int)$this->pdo->lastInsertId();
			$this->product->reserve($id_product, $quantity, $this->cartId, $this->idCustomer, $this->reserveTtl);
		}
	
		
		$this->touch($this->cartId);
		
		$out = $this->get();
		if (is_array($out)) {
		  foreach ($out['items'] as &$it) {
		    if ((int)$it['id'] === $item_id) { $it['_added_now'] = true; break; }
		  }
		}
		$this->notifyMutate();
		return $out;
	}
  	
	protected function createCart(string $session_id, ?int $id_customer, ?int $id_currency, ?string $locale): int {
		$datetime = time();
		$sql = "INSERT INTO carts SET 
						datetime = :datetime, 
						updated = :updated, 
						expires = :expires,
						id_customer = :id_customer,
						id_currency = :id_currency,
						locale = :locale,
						session_id = :session_id ";
		$result = $this->pdo->prepare($sql);
		$result->bindValue(':datetime', $datetime, PDO::PARAM_INT);
		$result->bindValue(':updated', $datetime, PDO::PARAM_INT);
		$result->bindValue(':expires', $datetime + 14 * 24 * 3600, PDO::PARAM_INT);
		
		if ($id_customer === NULL) $result->bindValue(':id_customer', null, PDO::PARAM_NULL);
    else $result->bindValue(':id_customer', $id_customer, PDO::PARAM_INT);
    if ($id_currency === null) $result->bindValue(':id_currency', null, PDO::PARAM_NULL);
    else $result->bindValue(':id_currency', $id_currency, PDO::PARAM_INT);
    if ($locale === null) $result->bindValue(':locale', null, PDO::PARAM_NULL);
    else $result->bindValue(':locale', $locale, PDO::PARAM_STR);
    
		$result->bindValue(':session_id', $session_id, PDO::PARAM_STR);
		$result->execute();
		return (int)$this->pdo->lastInsertId();
	}
	
	protected function listItems(int $id_cart): array {
		$sql = "SELECT * FROM carts_items WHERE id_module = :id_module ORDER BY id ASC";
		$result = $this->pdo->prepare($sql);
		$result->bindValue(':id_module', $id_cart, PDO::PARAM_INT);
		$result->execute();
		$arr = $result->fetchAll(PDO::FETCH_ASSOC);
		foreach ($arr AS &$row) {
			$net = ((float)$row['price_net'] + (float)($row['addons_net'] ?? 0.0)) * (int)$row['quantity'];
			$tax = $net * ((float) $row['tax_rate'] / 100.0);
			$row['line_total_net'] = $net;
			$row['line_tax'] = $tax;
			$row['line_total_gross'] = $net + $tax;
			$row['addons'] = isset($row['addons_json']) && $row['addons_json'] !== null ? json_decode($row['addons_json'], true) : null;
			$unit_weight = $this->product->getWeight($row['id_product']);
			$row['weight'] = $unit_weight;
			$row['line_weight'] = $unit_weight * (int)$row['quantity'];
		}
		unset($row);
		return $arr;
	}
	
	protected function updateItemQuantity(int $id, int $quantity): void {
		$sql = "UPDATE carts_items SET quantity = :quantity WHERE id = :id";
		$result = $this->pdo->prepare($sql);
		$result->bindValue(':id', $id, PDO::PARAM_INT);
		$result->bindValue(':quantity', $quantity, PDO::PARAM_INT);
		$result->execute();
	}
  
	protected function touch(int $id): void {
		$sql = "UPDATE carts SET updated = :updated WHERE id = :id";
		$result = $this->pdo->prepare($sql);
		$result->bindValue(':updated', time(), PDO::PARAM_INT);
		$result->bindValue(':id', $id, PDO::PARAM_INT);
		$result->execute();
		
		$sql = "UPDATE stock_reservation SET reserved_until = :new_reserved_until WHERE id_cart = :id_cart AND status = 'active' AND reserved_until >= :reserved_until ";
		$result = $this->pdo->prepare($sql);
		$result->bindValue(':new_reserved_until', time() + $this->reserveTtl, PDO::PARAM_INT);
		$result->bindValue(':id_cart', $id, PDO::PARAM_INT);
		$result->bindValue(':reserved_until', time(), PDO::PARAM_INT);
	  $result->execute();
	}
	
	protected function computeTotals(array $items): array {
		$sub = 0.0; 
		$tax = 0.0; 
		$gross = 0.0;
		foreach ($items AS $item) {
			$line_net   = ((float)$item['price_net'] + (float)($item['addons_net'] ?? 0.0)) * (int)$item['quantity'];
			$line_tax   = $line_net * ((float)$item['tax_rate'] / 100.0);
			$line_gross = $line_net + $line_tax;
			$sub   += $line_net;
			$tax   += $line_tax;
			$gross += $line_gross;
		}
		return [
			'subtotal_net'  => $sub,
			'tax'           => $tax,
			'total_gross'   => $gross,
		];
	}
	
	protected function grossToNet(float $gross, float $tax): float {
		$factor = 1.0 + ($tax / 100.0);
		return $factor > 0 ? $gross / $factor : $gross;
	}
  
  protected function canonicalJson(?array $data): ?string {
	  if ($data === null) return null;
	  $normalize = function (&$v) use (&$normalize) {
	    if (is_array($v)) {
	      $isAssoc = array_keys($v) !== range(0, count($v) - 1);
	      if ($isAssoc) {
	        ksort($v);
	        foreach ($v as &$vv) $normalize($vv);
	      } else {
	        foreach ($v as &$vv) $normalize($vv);
	        usort($v, function($a, $b){
	          $ga = $a['group'] ?? ''; 
	          $gb = $b['group'] ?? '';
	          if ($ga === $gb) return strcmp($a['value'] ?? '', $b['value'] ?? '');
	          return strcmp($ga, $gb);
	        });
	      }
	    }
	  };
	  $copy = $data;
	  $normalize($copy);
	  return json_encode($copy, JSON_UNESCAPED_UNICODE);
	}
	
  protected function normalizeAddons(?array $addons): ?array {
	  if ($addons === null) return null;
	  $out = [];
	  foreach ($addons AS $addon) {
	    if (!is_array($addon)) continue;
	    $group = isset($addon['group']) ? (string)$addon['group'] : null;
	    $value = isset($addon['value']) ? (string)$addon['value'] : null;
	    if ($group && $value) $out[] = ['group' => $group, 'value' => $value];
	  }
	  return $out ?: null;
	}
	
	protected function validateAddonsAgainstProduct(int $id_product, ?array $addons): bool {
	  if (!$addons) return true;
	  
	  $id_target = $id_product;
	  $sql = "SELECT 1 FROM products_product_properties_links WHERE id_module = :id LIMIT 1";
    $result = $this->pdo->prepare($sql);
    $result->bindValue(':id', $id_product, PDO::PARAM_INT);
    $result->execute();
    if (!$result->rowCount()) {
      $sql = "SELECT id_module FROM products WHERE id = :id LIMIT 1";
      $result = $this->pdo->prepare($sql);
      $result->bindValue(':id', $id_product, PDO::PARAM_INT);
      $result->execute();
      $id_parent = (int)($result->fetchColumn() ?: 0);
      if ($id_parent > 0) $id_target = $id_parent;
    }
	  
	  $sql = "SELECT CONCAT(guidv4_group,'|',guidv4) AS k FROM products_product_properties_links WHERE id_module = :id_module";
	  $result = $this->pdo->prepare($sql);
	  $result->bindValue(':id_module', $id_target, PDO::PARAM_INT);
	  $result->execute();
  	$valid = array_flip($result->fetchAll(PDO::FETCH_COLUMN) ?: []);
	  foreach ($addons AS $addon) {
	    $k = ($addon['group'] ?? '').'|'.($addon['value'] ?? '');
	    if (!isset($valid[$k])) return false;
	  }
	  return true;
	}

	public function updateAddonsByItemId(int $id, ?array $addons, ?string $addons_note, bool $reprice = true): ?array {
	  if ($this->cartId === null || $this->sessionId === null) return null;
	  
	  $sql = "SELECT id, id_product, quantity, addons_json, addons_note FROM carts_items WHERE id = :id AND id_module = :id_module LIMIT 1";
    $result = $this->pdo->prepare($sql);
    $result->bindValue(':id', $id, PDO::PARAM_INT);
    $result->bindValue(':id_module', $this->cartId, PDO::PARAM_INT);
    $result->execute();
    $arr = $result->fetch(PDO::FETCH_ASSOC);
    if (!$arr) { return $this->get(); }

    $addons_arr = is_array($addons) ? $this->normalizeAddons($addons) : null;
    if (!$this->validateAddonsAgainstProduct((int)$arr['id_product'], $addons_arr)) {
      return null;
    }
    $addons_json = $this->canonicalJson($addons_arr);

		$sql = "SELECT id, quantity
					  FROM carts_items
					  WHERE id_module = :id_module
				    AND id_product = :id_product
				    AND IFNULL(addons_json,'') = IFNULL(:addons_json,'')
				    AND IFNULL(addons_note,'') = IFNULL(:addons_note,'')
				    AND id <> :id
					  LIMIT 1";

    $result = $this->pdo->prepare($sql);
    $result->bindValue(':id_module', $this->cartId, PDO::PARAM_INT);
    $result->bindValue(':id_product', (int)$arr['id_product'], PDO::PARAM_INT);
    $result->bindValue(':addons_json', $addons_json, $addons_json === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
    $result->bindValue(':addons_note', $addons_note, $addons_note === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
    $result->bindValue(':id', $id, PDO::PARAM_INT);
    $result->execute();
    $arr_sub = $result->fetch(PDO::FETCH_ASSOC);

    if ($arr_sub) {
      $new_quantity = (int)$arr_sub['quantity'] + (int)$arr['quantity'];
      $sql_upd = "UPDATE carts_items SET quantity = :quantity WHERE id = :id";
      $result_upd = $this->pdo->prepare($sql_upd);
      $result_upd->bindValue(':quantity', $new_quantity, PDO::PARAM_INT);
      $result_upd->bindValue(':id', (int)$arr_sub['id'], PDO::PARAM_INT);
      $result_upd->execute();

			$sql_del = "DELETE FROM carts_items WHERE id = :id AND id_module = :id_module";
      $result_del = $this->pdo->prepare($sql_del);
      $result_del->bindValue(':id', $id, PDO::PARAM_INT);
      $result_del->bindValue(':id_module', $this->cartId, PDO::PARAM_INT);
      $result_del->execute();
    } else {
    	$sql_upd = "UPDATE carts_items SET addons_json = :addons_json, addons_note = :addons_note WHERE id = :id AND id_module = :id_module";
      $result_upd = $this->pdo->prepare($sql_upd);
      $result_upd->bindValue(':id', $id, PDO::PARAM_INT);
      $result_upd->bindValue(':id_module', $this->cartId, PDO::PARAM_INT);
      $result_upd->bindValue(':addons_json', $addons_json, $addons_json === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
      if ($addons_note === null) $result_upd->bindValue(':addons_note', null, PDO::PARAM_NULL);
      else $result_upd->bindValue(':addons_note', $addons_note, PDO::PARAM_STR);
      $result_upd->execute();

      if ($reprice) { $this->repriceLineByItemId($id, false); }
    }

    $this->touch($this->cartId);
    $this->notifyMutate();
    return $this->get();
	  
	}

	public function replaceItemProduct(int $id, int $new_id_product, ?array $addons = null, ?string $note = null, ?string $addons_note = null): ?array {
	  if ($this->cartId === null || $this->sessionId === null) return null;
		
		$sql = "SELECT id, id_product, quantity FROM carts_items WHERE id = :id AND id_module = :id_module LIMIT 1";
    $result = $this->pdo->prepare($sql);
    $result->bindValue(':id', $id, PDO::PARAM_INT);
    $result->bindValue(':id_module', $this->cartId, PDO::PARAM_INT);
    $result->execute();
    $arr = $result->fetch(PDO::FETCH_ASSOC);
    if (!$arr) { return $this->get(); }

	
    $addons_arr  = is_array($addons) ? $this->normalizeAddons($addons) : null;
    if (!$this->validateAddonsAgainstProduct($new_id_product, $addons_arr)) {
      return null;
    }
    $addons_json = $this->canonicalJson($addons_arr);

    [$id_country, , $id_country_fallback] = $this->product->getPricing() + [null, null, null];
    $price = $this->product->getPriceSingle($new_id_product, $id_country, (int)$arr['quantity'], $id_country_fallback);
    if (($price['unavailable'] ?? false) === true) { return null; }

    $can = $this->product->canAddToCart($new_id_product, (int)$arr['quantity']);
    if (($can['allowed'] ?? false) !== true) { return null; }

    $sku = $this->product->getSku($new_id_product);
    $tax_rate = (float)($price['vat_rate'] ?? 0.0);
    $gross = (float)$price['value'];
    $price_net = $this->grossToNet($gross, $tax_rate);
    $addons_net = $this->computeAddonsNet($new_id_product, $addons_arr, (int)$arr['quantity']);

    $sql = "SELECT id, quantity
			      FROM carts_items
			      WHERE id_module = :id_module
			      AND id_product = :id_product
			      AND IFNULL(addons_json,'') = IFNULL(:addons_json,'')
			      AND IFNULL(addons_note,'') = IFNULL(:addons_note,'')
			      LIMIT 1";
    $result = $this->pdo->prepare($sql);
    $result->bindValue(':id_module', $this->cartId, PDO::PARAM_INT);
    $result->bindValue(':id_product', $new_id_product, PDO::PARAM_INT);
    $result->bindValue(':addons_json', $addons_json, $addons_json === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
    $result->bindValue(':addons_note', $addons_note, $addons_note === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
    $result->execute();
    $arr_sub = $result->fetch(PDO::FETCH_ASSOC);

    $this->product->releasePartial((int)$arr['id_product'], (int)$arr['quantity'], $this->cartId);
    $this->product->reserve($new_id_product, (int)$arr['quantity'], $this->cartId, $this->idCustomer, $this->reserveTtl);

    if ($arr_sub && (int)$arr_sub['id'] !== (int)$id) {
      $new_quantity = (int)$arr_sub['quantity'] + (int)$arr['quantity'];
      $sql_upd = "UPDATE carts_items SET quantity = :quantity WHERE id = :id";
      $result_upd = $this->pdo->prepare($sql_upd);
      $result_upd->bindValue(':quantity', $new_quantity, PDO::PARAM_INT);
      $result_upd->bindValue(':id', (int)$arr_sub['id'], PDO::PARAM_INT);
      $result_upd->execute();

			$sql_del = "DELETE FROM carts_items WHERE id = :id AND id_module = :id_module";
      $result_del = $this->pdo->prepare($sql_del);
      $result_del->bindValue(':id', $id, PDO::PARAM_INT);
      $result_del->bindValue(':id_module', $this->cartId, PDO::PARAM_INT);
      $result_del->execute();
    } else {
      $sql_upd = "UPDATE carts_items
					        SET id_product = :id_product,
			            sku = :sku,
			            price_net = :price_net,
			            addons_net = :addons_net,
			            tax_rate = :tax_rate,
			            addons_json = :addons_json,
			            addons_note = :addons_note,
			            note = :note
					        WHERE id = :id AND id_module = :id_module";
      $result_upd = $this->pdo->prepare($sql_upd);
      $result_upd->bindValue(':id_product', $new_id_product, PDO::PARAM_INT);
      $result_upd->bindValue(':sku', $sku, PDO::PARAM_STR);
      $result_upd->bindValue(':price_net', $price_net, PDO::PARAM_STR);
      $result_upd->bindValue(':addons_net', $addons_net, PDO::PARAM_STR);
      $result_upd->bindValue(':tax_rate', $tax_rate, PDO::PARAM_STR);
      $result_upd->bindValue(':addons_json', $addons_json, $addons_json === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
      if ($addons_note === null) $result_upd->bindValue(':addons_note', null, PDO::PARAM_NULL);
      else $result_upd->bindValue(':addons_note', $addons_note, PDO::PARAM_STR);
      if ($note === null) $result_upd->bindValue(':note', null, PDO::PARAM_NULL);
      else $result_upd->bindValue(':note', $note, PDO::PARAM_STR);
      $result_upd->bindValue(':id', $id, PDO::PARAM_INT);
      $result_upd->bindValue(':id_module', $this->cartId, PDO::PARAM_INT);
      $result_upd->execute();
    }

    $this->touch($this->cartId);
    $this->notifyMutate();
    return $this->get();
	}
	
	public function repriceLineByItemId(int $id, bool $doTouchAndReturn = true): ?array {
	  if ($this->cartId === null || $this->sessionId === null) return null;
	
		$sql = "SELECT id, id_product, quantity, addons_json FROM carts_items WHERE id = :id AND id_module = :id_module LIMIT 1";
	  $result = $this->pdo->prepare($sql);
	  $result->bindValue(':id', $id, PDO::PARAM_INT);
	  $result->bindValue(':id_module', $this->cartId, PDO::PARAM_INT);
	  $result->execute();
	  $arr = $result->fetch(PDO::FETCH_ASSOC);
	  if (!$arr) return $this->get();
	
	  $addons_arr  = isset($arr['addons_json']) && $arr['addons_json'] !== null ? json_decode($arr['addons_json'], true) : null;
	
	  [$id_country, , $id_country_fallback] = $this->product->getPricing() + [null, null, null];
	  $price = $this->product->getPriceSingle((int)$arr['id_product'], $id_country, (int)$arr['quantity'], $id_country_fallback);
	  if (($price['unavailable'] ?? false) === true) {
	    return $this->get();
	  }
	
	  $tax_rate = (float)($price['vat_rate'] ?? 0.0);
	  $gross = (float)$price['value'];
	  $price_net = $this->grossToNet($gross, $tax_rate);
	  $addons_net = $this->computeAddonsNet((int)$arr['id_product'], $addons_arr, (int)$arr['quantity']);
	
		$sql = "UPDATE carts_items SET price_net = :price_net, addons_net = :addons_net, tax_rate = :tax_rate WHERE id = :id AND id_module = :id_module";
	  $result = $this->pdo->prepare($sql);
	  $result->bindValue(':price_net', $price_net, PDO::PARAM_STR);
	  $result->bindValue(':addons_net', $addons_net, PDO::PARAM_STR);
	  $result->bindValue(':tax_rate', $tax_rate, PDO::PARAM_STR);
	  $result->bindValue(':id', $id, PDO::PARAM_INT);
	  $result->bindValue(':id_module', $this->cartId, PDO::PARAM_INT);
	  $result->execute();
	
	  if ($doTouchAndReturn) {
	    $this->touch($this->cartId);
	    return $this->get();
	  }
	  return null;
	}
	
	protected function computeAddonsNet(int $id_product, ?array $addons_arr, int $quantity): float {
	  if (!$addons_arr) return 0.0;
	  $sumPerUnit = 0.0;
	  return $sumPerUnit;
	}
		
	protected function findOtherOpenCartsByCustomer(int $id_customer, int $merge_id): array {
	  $sql = "SELECT id FROM carts WHERE id_customer = :id_customer AND id <> :id AND expires >= :expires ORDER BY updated DESC";
	  $result = $this->pdo->prepare($sql);
	  $result->bindValue(':id_customer', $id_customer, PDO::PARAM_INT);
	  $result->bindValue(':id',  $merge_id, PDO::PARAM_INT);
	  $result->bindValue(':expires', time(), PDO::PARAM_INT);
	  $result->execute();
	  return $result->fetchAll(PDO::FETCH_COLUMN) ?: [];
	}
	
	protected function mergeFromCartId(int $id_cart): void {
	  if ($this->cartId === null || $id_cart === $this->cartId) return;
	
		$sql = "SELECT id_product, quantity, addons_json, addons_note, note, sku FROM carts_items WHERE id_module = :id_module ORDER BY id ASC";
	  $result = $this->pdo->prepare($sql);
	  $result->bindValue(':id_module', $id_cart, PDO::PARAM_INT);
	  $result->execute();
	  
	  if ($result->rowCount()){
	  	
	  	$this->product->releaseByCart($id_cart);
	  	
	  	while ($arr = $result->fetch()){
		    $payload = [
		      'id_product'  => (int)$arr['id_product'],
		      'quantity'    => (int)$arr['quantity'],
		      'sku'         => (string)($arr['sku'] ?? ''),
		      'addons_json' => $arr['addons_json'] ? json_decode($arr['addons_json'], true) : null,
		      'addons_note' => $arr['addons_note'] ?? null,
		      'note'        => $arr['note'] ?? null,
		    ];
		    $this->add($payload);
	  	}
	  }
	
	  $sql = "DELETE FROM carts_items WHERE id_module = :id_module";
	  $result = $this->pdo->prepare($sql);
	  $result->bindValue(':id_module', $id_cart, PDO::PARAM_INT);
	  $result->execute();
	  
	  
	  $sql = "UPDATE carts SET expires = :expires WHERE id = :id";
	  $result = $this->pdo->prepare($sql);
	  $result->bindValue(':expires', time()-1, PDO::PARAM_INT);
	  $result->bindValue(':id', $id_cart, PDO::PARAM_INT);
	  $result->execute();
	  
	}

}
