<?php

class Discounts {
	public const TYPE_COUPON = 'coupon';
	public const TYPE_GIFTCARD = 'giftcard';
	public const TYPE_AUTO = 'auto';

  public const METHOD_PERCENT = 'percent';
  public const METHOD_AMOUNT = 'amount';
  public const METHOD_SHIPPING_FREE = 'shipping_free';

  public const SCOPE_ORDER = 'order';
  public const SCOPE_SHIPPING = 'shipping';
  public const SCOPE_ITEM = 'item';
  
  
  private PDO $pdo;
  private string $currency;
  private ?int $customerId;

  public function __construct(PDO $pdo, ?int $id_customer = null, string $currency = 'EUR') {
    $this->pdo = $pdo;
    $this->customerId = $id_customer;
    $this->currency = $currency;
  }
  
  public function applyCodeToCart(int $id_cart, ?string $code, array $snapshot): array {
	  if ($code === null || trim($code) === '') {
	    $applied = $this->loadCartDiscounts($id_cart);
      $totals  = $this->recalculateTotals($snapshot, $applied);
      return ['applied' => $applied, 'totals' => $totals];
	  }
	
	  $code = trim($code);
	  $discount = $this->resolveDiscountByCode($code);
	  if (!$discount) {
	    throw new \RuntimeException('CODE_INVALID');
	  }
	  $this->assertDiscountUsable($discount, $snapshot);
	
	  $calc = $this->calculateAppliedAmount($discount, $snapshot);
	  $this->upsertCartDiscount($id_cart, $discount, $calc);
	
	  $autos = $this->findAutoDiscountsForCart($snapshot, $discount);
	  foreach ($autos AS $auto) {
      $auto_calc = $this->calculateAppliedAmount($auto, $snapshot);
      $this->upsertCartDiscount($id_cart, $auto, $auto_calc);
	  }
	
	  $applied = $this->loadCartDiscounts($id_cart);
	  $totals = $this->recalculateTotals($snapshot, $applied);
	
	  return ['applied' => $applied, 'totals' => $totals];
	}

	public function refreshAutoDiscounts(int $id_cart, array $snapshot): array {
		$this->deleteCartDiscountsByType($id_cart, self::TYPE_AUTO);
		$autos = $this->findAutoDiscountsForCart($snapshot, null);
	  foreach ($autos AS $auto) {
      $auto_calc = $this->calculateAppliedAmount($auto, $snapshot);
      $this->upsertCartDiscount($id_cart, $auto, $auto_calc);
		}
		return $this->loadCartDiscounts($id_cart);
	}

	public function commitCartDiscountsToOrder(int $id_order, int $id_cart): void {
		$applied = $this->loadCartDiscounts($id_cart);
		if (!$applied) return;
		
		$sql = "SELECT 1 FROM orders_discount WHERE id_module = :id_module LIMIT 1";
		$result = $this->pdo->prepare($sql);
    $result->bindValue(':id_module', $id_order, PDO::PARAM_INT);
    $result->execute();
    if ($result->fetchColumn()) return;
    
    $sql = "INSERT INTO orders_discount SET 
    				id_module = :id_module, 
    				id_discount = :id_discount,
    				type = :type, 
    				code = :code, 
    				method = :method, 
    				scope = :scope, 
    				base_amount = :base_amount, 
    				applied_amount = :applied_amount,
				    applied_tax = :applied_tax,
				    applied_gross = :applied_gross,
    				note = :note ";
    
    foreach ($applied AS $discount) {
    	$result = $this->pdo->prepare($sql);
    	$result->bindValue(':id_module', $id_order, PDO::PARAM_INT);
    	if (!empty($discount['id_discount'])) $result->bindValue(':id_discount', (int)$discount['id_discount'], PDO::PARAM_INT);
      else $result->bindValue(':id_discount', null, PDO::PARAM_NULL);
    	$result->bindValue(':type', $discount['type'], PDO::PARAM_STR);
    	if (isset($discount['code']) && $discount['code'] !== '') $result->bindValue(':code', $discount['code'], PDO::PARAM_STR);
      else $result->bindValue(':code', null, PDO::PARAM_NULL);
    	$result->bindValue(':method', $discount['method'], PDO::PARAM_STR);
    	$result->bindValue(':scope', $discount['scope'], PDO::PARAM_STR);
    	$result->bindValue(':base_amount', $discount['base_amount']);
    	$result->bindValue(':applied_amount', $discount['applied_amount']);
	    $result->bindValue(':applied_tax', $discount['applied_tax']);
	    $result->bindValue(':applied_gross', $discount['applied_gross']);
      if (!empty($discount['note'])) $result->bindValue(':note', $discount['note']);
      else $result->bindValue(':note', null, PDO::PARAM_NULL);
    	$result->execute();			
    }
	}
	
	public function captureOrderRedemptions(int $id_order): void {
		$sql = "SELECT id_customer FROM orders WHERE id = :id LIMIT 1";
	  $result = $this->pdo->prepare($sql);
	  $result->bindValue(':id', $id_order, PDO::PARAM_INT);
	  $result->execute();
	  $id_customer = (int)($result->fetchColumn() ?: 0);
	
		$sql = "SELECT id_discount, type, code, applied_amount FROM orders_discount WHERE id_module = :id_module";
	  $result = $this->pdo->prepare($sql);
	  $result->bindValue(':id_module', $id_order, PDO::PARAM_INT);
	  $result->execute();
	  $list = $result->fetchAll(PDO::FETCH_ASSOC) ?: [];
	  if (!$list) return;
	
	  foreach ($list AS $discount) {
	    if (empty($discount['id_discount'])) continue;
	
			$sql = "SELECT 1 FROM discounts_redemptions WHERE id_order = :id_order AND id_discount = :id_discount LIMIT 1";
	    $result = $this->pdo->prepare($sql);
	    $result->bindValue(':id_order', $id_order, PDO::PARAM_INT);
	    $result->bindValue(':id_discount', (int)$discount['id_discount'], PDO::PARAM_INT);
	    $result->execute();
	    if ($result->fetchColumn()) continue;
	
	    $sql = "INSERT INTO discounts_redemptions SET 
	    				datetime = :datetime,
	    				id_discount = :id_discount, 
	    				id_order = :id_order,
	    				id_customer = :id_customer,
	    				code = :code, 
	    				amount = :amount";
	    $result = $this->pdo->prepare($sql);
	    $result->bindValue(':datetime', time(), PDO::PARAM_INT);
	    $result->bindValue(':id_discount', (int)$discount['id_discount'], PDO::PARAM_INT);
	    $result->bindValue(':id_order', $id_order, PDO::PARAM_INT);
	    $result->bindValue(':id_customer', $id_customer ?: $this->customerId, PDO::PARAM_INT);
	    $result->bindValue(':code', $discount['code'] ?? null);
	    $result->bindValue(':amount', $discount['applied_amount']);
	    $result->execute();
	
	    if (($discount['type'] ?? '') === self::TYPE_GIFTCARD) {
	      $this->decrementGiftcardBalance((int)$discount['id_discount'], (string)$discount['applied_amount']);
	    }
	  }
	}


	public function loadCartDiscounts(int $id_cart): array {
		$sql = "SELECT * FROM carts_discount 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();
		return $result->fetchAll(PDO::FETCH_ASSOC) ?: [];
	}

	private function resolveDiscountByCode(string $code): ?array {
		$sql = "SELECT * FROM discounts WHERE code = :code AND c_active = '1' LIMIT 1";
		$result = $this->pdo->prepare($sql);
		$result->bindValue(':code', $code, PDO::PARAM_STR);
		$result->execute();
		$arr = $result->fetch(PDO::FETCH_ASSOC);
		return $arr ?: null;
	}
	
	private function assertDiscountUsable(array $discount, array $cart): void {
		$now = time();
		if (!empty($discount['valid_from']) && (int)$discount['valid_from'] > $now) {
			throw new \RuntimeException('CODE_NOT_YET_VALID');
		}
		if (!empty($discount['valid_to']) && (int)$discount['valid_to'] < $now) {
			throw new \RuntimeException('CODE_EXPIRED');
		}
		
		if (!empty($discount['global_redemption_limit'])) {
			$used = $this->countGlobalRedemptions((int)$discount['id']);
			if ($used >= (int)$discount['global_redemption_limit']) {
				throw new \RuntimeException('CODE_LIMIT_REACHED_GLOBAL');
			}
		}
		if (!empty($discount['per_customer_limit']) && $this->customerId) {
			$used = $this->countCustomerRedemptions((int)$discount['id'], (int)$this->customerId);
			if ($used >= (int)$discount['per_customer_limit']) {
				throw new \RuntimeException('CODE_LIMIT_REACHED_CUSTOMER');
			}
		}
		if (($discount['type'] ?? '') === self::TYPE_GIFTCARD) {
			if ((float)$discount['remaining_balance'] <= 0.0) {
				throw new \RuntimeException('GIFTCARD_EMPTY');
			}
		}
		
		$subtotal = (float)($cart['totals_subtotal_net'] ?? $cart['totals_subtotal'] ?? 0);
		if (!empty($discount['min_subtotal']) && $subtotal < (float)$discount['min_subtotal']) {
			throw new \RuntimeException('MIN_SUBTOTAL_NOT_REACHED');
		}
		
		$this->checkIncludeExclude($discount, $cart);
	}
	
	private function checkIncludeExclude(array $discount, array $cart): void {
		$include = $this->jsonDecodeSafe($discount['include_json'] ?? null);
		$exclude = $this->jsonDecodeSafe($discount['exclude_json'] ?? null);
		if (!$include && !$exclude) return;
		
		$ids = array_map(fn($item) => (int)$item['id_product'], $cart['items'] ?? []);
		
		if (!empty($include['product_ids'])) {
			$ok = array_intersect($ids, array_map('intval', (array)$include['product_ids']));
			if (empty($ok)) throw new \RuntimeException('INCLUDE_CONDITION_NOT_MET');
		}
		if (!empty($exclude['product_ids'])) {
			$bad = array_intersect($ids, array_map('intval', (array)$exclude['product_ids']));
			if (!empty($bad)) throw new \RuntimeException('EXCLUDED_BY_RULE');
		}
	}
	
	private function calculateAppliedAmount(array $discount, array $cart): array {
		$scope  = $discount['scope'];
		$method = $discount['method'];
				
		$items_net = 0.0;
		$items_tax = 0.0;
		foreach (($cart['items'] ?? []) AS $item) {
	    $net = (float)($item['line_subtotal_net'] ?? 0.0);
	    $rate = (float)($item['tax_rate'] ?? 0.0);
	    $items_net += $net;
	    $items_tax += $net * ($rate / 100.0);
		}
		$avg_items_rate = $items_net > 0 ? ($items_tax / $items_net) : 0.0;
		
		$shipping_rate = isset($cart['shipping_tax_rate']) ? ((float)$cart['shipping_tax_rate']/100.0) : 0.0;
		
		
		$base = 0.0;
		if ($scope === self::SCOPE_ORDER) {
			$base = (float)($cart['totals_subtotal_net'] ?? $cart['totals_subtotal'] ?? 0);
		} elseif ($scope === self::SCOPE_SHIPPING) {
			$base = (float)($cart['shipping_price_net'] ?? $cart['shipping_price'] ?? 0);
		} elseif ($scope === self::SCOPE_ITEM) {
			foreach ($cart['items'] ?? [] AS $item) {
			if (!empty($item['discount_exempt'])) continue;
				$base += (float)$item['line_subtotal_net'];
			}
		}
		
		$applied = 0.0;
		$val = (float)($discount['value'] ?? 0.0);

		if (!empty($discount['c_gross'])) {
	    if ($scope === self::SCOPE_ORDER || $scope === self::SCOPE_ITEM) {
	    	$val = $val / (1.0 + $avg_items_rate);
	    } elseif ($scope === self::SCOPE_SHIPPING) {
	    	$val = $val / (1.0 + $shipping_rate);
	    }
		}
		
		if ($method === self::METHOD_PERCENT) {
			$applied = round($base * ((float)$discount['value'] / 100.0), 5);
		} elseif ($method === self::METHOD_AMOUNT) {
			$applied = min(round($val, 5), $base);   
		} elseif ($method === self::METHOD_SHIPPING_FREE) {
			$applied = round($base, 5);
		}
		
		if (($discount['type'] ?? '') === self::TYPE_GIFTCARD) {
			$balance = (float)$discount['remaining_balance'];
			$applied = min($applied > 0 ? $applied : $base, $balance);
		}
		
		if (!empty($discount['max_discount'])) {
			$applied = min($applied, (float)$discount['max_discount']);
		}
		
		$tx = $this->computeAppliedTaxAndGrossForScope($applied, $scope, $cart);
		
		return [
			'base_amount'    => $this->toDec($base),
			'applied_amount' => $this->toDec($applied),
		  'applied_tax'    => $tx['tax'],
		  'applied_gross'  => $tx['gross'],
		];
	}
	
	private function computeAppliedTaxAndGrossForScope(float $applied_net, string $scope, array $cart): array {
	  if ($applied_net <= 0) return ['tax' => $this->toDec(0.0), 'gross' => $this->toDec(0.0)];
	
	  $items_net = 0.0;
	  $items_tax = 0.0;
	  foreach (($cart['items'] ?? []) AS $item) {
	    $net  = (float)($item['line_subtotal_net'] ?? $item['line_total_net'] ?? 0.0);
	    $rate = (float)($item['tax_rate'] ?? 0.0) / 100.0;
	    $items_net += $net;
	    $items_tax += $net * $rate;
	  }
	  $avg_items_rate = $items_net > 0 ? ($items_tax / $items_net) : 0.0;
	
	  $shipping_rate = isset($cart['shipping_tax_rate']) ? ((float)$cart['shipping_tax_rate'] / 100.0) : 0.0;
	
	  $rate = ($scope === self::SCOPE_SHIPPING) ? $shipping_rate : $avg_items_rate;
	
	  $tax = $applied_net * $rate;
	  $gross = $applied_net + $tax;
	  return ['tax' => $this->toDec($tax), 'gross' => $this->toDec($gross)];
	}

	
	private function fetchDiscountById(int $id): ?array {
	  $sql = "SELECT * FROM discounts WHERE id = :id LIMIT 1";
	  $result = $this->pdo->prepare($sql);
	  $result->bindValue(':id', $id, PDO::PARAM_INT);
	  $result->execute();
	  $arr = $result->fetch(PDO::FETCH_ASSOC);
	  return $arr ?: null;
	}
	
	public function validateAndNormalizeCartDiscounts(int $id_cart, array $snapshot): array {
	  $changed = false;
	  $applied = $this->loadCartDiscounts($id_cart);
	
	  foreach ($applied AS $discount) {
	    $master = null;
	    if (!empty($discount['id_discount'])) {
	      $master = $this->fetchDiscountById((int)$discount['id_discount']);
	    } elseif (!empty($discount['code'])) {
	      $master = $this->resolveDiscountByCode((string)$discount['code']);
	    }
	    if (!$master) {
	      $this->removeDiscountLinkById($id_cart, (int)$discount['id']);
	      $changed = true;
	      continue;
	    }
	
	    try {
	      $this->assertDiscountUsable($master, $snapshot);
	    } catch (\RuntimeException $e) {
	      $this->removeDiscountLinkById($id_cart, (int)$discount['id']);
	      $changed = true;
	      continue;
	    }
	
	    $calc = $this->calculateAppliedAmount($master, $snapshot);
	    $this->upsertCartDiscount($id_cart, $master, $calc);
	    $changed = true;
	  }
	
	  $this->refreshAutoDiscounts($id_cart, $snapshot);
	
	  return $this->loadCartDiscounts($id_cart);
	}

	
	private function upsertCartDiscount(int $id_cart, array $discount, array $calc): void {
		$sql = "DELETE FROM carts_discount WHERE id_module = :id_module AND (code <=> :code OR id_discount <=> :id_discount) ";
		$result = $this->pdo->prepare($sql);
		$result->bindValue(':id_module', $id_cart, PDO::PARAM_INT);
		$result->bindValue(':code', $discount['code'] ?? null);
  	$result->bindValue(':id_discount', $discount['id'] ?? null);
		$result->execute();
		
		
    $sql = "INSERT INTO carts_discount SET 
    				id_module = :id_module, 
    				id_discount = :id_discount,
    				type = :type, 
    				code = :code, 
    				method = :method, 
    				scope = :scope, 
    				base_amount = :base_amount, 
    				applied_amount = :applied_amount,
					  applied_tax = :applied_tax,
					  applied_gross = :applied_gross,
    				note = :note ";
    $result = $this->pdo->prepare($sql);
    				
  	$result->bindValue(':id_module', $id_cart, PDO::PARAM_INT);
  	$result->bindValue(':id_discount', $discount['id'] ?? null);
  	$result->bindValue(':type', $discount['type'], PDO::PARAM_STR);
  	$result->bindValue(':code', $discount['code'] ?? null);
  	$result->bindValue(':method', $discount['method'], PDO::PARAM_STR);
  	$result->bindValue(':scope', $discount['scope'], PDO::PARAM_STR);
  	$result->bindValue(':base_amount', $calc['base_amount']);
  	$result->bindValue(':applied_amount', $calc['applied_amount']);
		$result->bindValue(':applied_tax', $calc['applied_tax']);
		$result->bindValue(':applied_gross', $calc['applied_gross']);
  	$result->bindValue(':note', $discount['label'] ?? null);
  	$result->execute();
	}
	
	private function findAutoDiscountsForCart(array $cart, ?array $added): array {
		$sql = "SELECT * FROM discounts 
						WHERE c_active = '1' 
						AND type = :type
						AND (valid_from IS NULL OR valid_from <= :now)
						AND (valid_to IS NULL OR valid_to >= :now)";
		$result = $this->pdo->prepare($sql);
		$result->bindValue(':type', self::TYPE_AUTO, PDO::PARAM_STR);
		$result->bindValue(':now', time(), PDO::PARAM_INT);
		$result->execute();
		$list = $result->fetchAll(PDO::FETCH_ASSOC) ?: [];
		if (!$list) return [];
		
		$exclusive = array_filter($list, fn($discount) => ($discount['stacking'] ?? 'stackable') === 'exclusive');
		if ($exclusive) {
			$best = null;
			$best_val = -1.0;
			foreach ($exclusive AS $discount) {
				$calc = $this->calculateAppliedAmount($discount, $cart);
				$val = (float)$calc['applied_amount'];
				if ($val > $best_val) {
					$best_val = $val; 
					$best = $discount;
				}
			}
			return $best ? [$best] : [];
		}
		
		return $list;
	}
	
	private function countGlobalRedemptions(int $id_discount): int {
		$sql = "SELECT COUNT(*) FROM discounts_redemptions WHERE id_discount = :id_discount";
		$result = $this->pdo->prepare($sql);
		$result->bindValue(':id_discount', $id_discount, PDO::PARAM_INT);
		$result->execute();
		return (int)$result->fetchColumn();
	}
	
	private function countCustomerRedemptions(int $id_discount, int $id_customer): int {
		$sql = "SELECT COUNT(*) FROM discounts_redemptions WHERE id_discount = :id_discount AND id_customer = :id_customer";
		$result = $this->pdo->prepare($sql);
		$result->bindValue(':id_discount', $id_discount, PDO::PARAM_INT);
		$result->bindValue(':id_customer', $id_customer, PDO::PARAM_INT);
		$result->execute();
		return (int)$result->fetchColumn();
	}
	
	private function insertRedemption(int $id_discount, int $id_order, ?string $code, string $amount): void {
		$sql = "INSERT INTO discounts_redemptions SET 
						datetime = :datetime, 
						id_discount = :id_discount, 
						id_order = :id_order, 
						id_customer = :id_customer, 
						code = :code, 
						amount = :amount ";
		$result = $this->pdo->prepare($sql);
		$result->bindValue(':datetime', time(), PDO::PARAM_INT);
		$result->bindValue(':id_discount', $id_discount, PDO::PARAM_INT);
		$result->bindValue(':id_order', $id_order, PDO::PARAM_INT);
		$result->bindValue(':id_customer', $this->customerId, PDO::PARAM_INT);
		$result->bindValue(':code', $code, PDO::PARAM_STR);
		$result->bindValue(':amount', $amount);
		$result->execute();
	}
	
	private function decrementGiftcardBalance(int $id_discount, string $amount): void {
		$sql = "UPDATE discounts SET remaining_balance = GREATEST(0, IFNULL(remaining_balance,0) - :amount) WHERE id = :id";
		$result = $this->pdo->prepare($sql);
		$result->bindValue(':amount', $amount);
		$result->bindValue(':id', $id_discount, PDO::PARAM_INT);
		$result->execute();
	}
	
	private function removeAllCartDiscounts(int $id_cart): void {
		$sql = "DELETE FROM carts_discount WHERE id_module = :id_module";
		$result = $this->pdo->prepare($sql);
		$result->bindValue(':id_module', $id_cart, PDO::PARAM_INT);
		$result->execute();
	}
	
	private function deleteCartDiscountsByType(int $id_cart, string $type): void {
		$sql = "DELETE FROM carts_discount WHERE id_module = :id_module AND type = :type";
		$result = $this->pdo->prepare($sql);
		$result->bindValue(':id_module', $id_cart, PDO::PARAM_INT);
		$result->bindValue(':type', $type, PDO::PARAM_STR);
		$result->execute();
	}
	
	public function removeCodeFromCart(int $id_cart, string $code): void {
    $sql = "DELETE FROM carts_discount WHERE id_module = :id_cart AND LOWER(code) = LOWER(:code)";
    $result = $this->pdo->prepare($sql);
    $result->bindValue(':id_cart', $id_cart, PDO::PARAM_INT);
    $result->bindValue(':code', $code, PDO::PARAM_STR);
    $result->execute();
	}
	
	public function removeDiscountLinkById(int $id_cart, int $id): void {
    $sql = "DELETE FROM carts_discount WHERE id_module = :id_cart AND id = :id";
    $result = $this->pdo->prepare($sql);
    $result->bindValue(':id_cart', $id_cart, PDO::PARAM_INT);
    $result->bindValue(':id', $id, PDO::PARAM_INT);
    $result->execute();
	}

	
	public function recalculateTotals(array $cart, array $applied): array {
		$order_discount = 0.0;
		$shipping_discount = 0.0;
		foreach ($applied AS $discount) {
			if ($discount['scope'] === self::SCOPE_ORDER || $discount['scope'] === self::SCOPE_ITEM) $order_discount += (float)$discount['applied_amount'];
			if ($discount['scope'] === self::SCOPE_SHIPPING) $shipping_discount += (float)$discount['applied_amount'];
		}
		
		$subtotal = (float)($cart['totals_subtotal_net'] ?? 0);
		$shipping = (float)($cart['shipping_price_net'] ?? 0);
		$total = max(0.0, $subtotal - $order_discount) + max(0.0, $shipping - $shipping_discount);
		
		return [
			'discount_order_net' 				=> $this->toDec($order_discount),
			'discount_shipping_net' 		=> $this->toDec($shipping_discount),
			'total_net_after_discounts' => $this->toDec($total),
		];
	}
	
	
	private function toDec(float $value): string {
	  return number_format($value, 5, '.', '');
	}
	
	
	private function jsonDecodeSafe($str): ?array {
		if (!$str) return null;
		$arr = json_decode((string)$str, true);
		return is_array($arr) ? $arr : null;
	}
}
