<?php
use PHPMailer\PHPMailer\PHPMailer;

final class MailDispatcher {
  private PDO $pdo;
  
  private string $shopName;
  private string $fromEmail;
  private ?string $fromName;
  private string $operatorEmail;
  private string $templateDir;
  
  private array $cc = [];
  private array $bcc = [];


  public function __construct(PDO $pdo, array $config) {
    $this->pdo = $pdo;

    $this->shopName = (string)($config['shop_name'] ?? 'Shop');
    $this->fromEmail = (string)($config['from_email'] ?? 'noreply@example.com');
    $this->fromName = isset($config['from_name']) ? (string)$config['from_name'] : null;
    $this->operatorEmail = (string)($config['operator_email'] ?? 'operator@example.com');

    $this->templateDir = rtrim((string)($config['template_dir'] ?? ($_SERVER['DOCUMENT_ROOT'].'/template/orders')), '/');

    if (!empty($config['cc'])) $this->cc = is_array($config['cc']) ? $config['cc'] : [(string)$config['cc']];
    if (!empty($config['bcc'])) $this->bcc = is_array($config['bcc']) ? $config['bcc'] : [(string)$config['bcc']];
  }

  public function enqueue(int $id_order, string $type, string $recipient_role, ?string $idempotency_key = null): void {
  	$sql = "INSERT INTO orders_mail_queue SET 
  					datetime = :datetime, 
  					id_module = :id_module,
  					type = :type, 
  					recipient_role = :recipient_role, 
  					status = 'queued', 
  					attempts = 0, 
  					error = NULL, 
  					idempotency_key = :idempotency_key";
    $result = $this->pdo->prepare($sql);
    $result->bindValue(':datetime', time(), PDO::PARAM_INT);
    $result->bindValue(':id_module', $id_order, PDO::PARAM_INT);
    $result->bindValue(':type', $type, PDO::PARAM_STR);
    $result->bindValue(':recipient_role', $recipient_role, PDO::PARAM_STR);
    if ($idempotency_key === null) {
		  $result->bindValue(':idempotency_key', null, PDO::PARAM_NULL);
		} else {
		  $result->bindValue(':idempotency_key', $idempotency_key, PDO::PARAM_STR);
		}
    $result->execute();
  }

  public function processQueue(int $limit = 50): void {
  	$sql = "SELECT * FROM orders_mail_queue WHERE status = 'queued' ORDER BY id ASC LIMIT ".$limit;
    $result = $this->pdo->prepare($sql);
    $result->execute();
    foreach ($result->fetchAll(PDO::FETCH_ASSOC) ?: [] AS $arr) {
      $this->deliver($arr);
    }
  }

  public function resend(int $id_order, string $type, string $recipient_role): void {
    $this->enqueue($id_order, $type, $recipient_role);
  }

  private function deliver(array $queue): void {
    $id_order = (int)$queue['id_module'];
    $type = (string)$queue['type'];
    $recipient_role = (string)$queue['recipient_role'];

    try {
      $data = $this->loadData($id_order);
      $locale = (string)($data['order']['locale'] ?? 'de');

      $recipient = $recipient_role === 'operator' ? $this->operatorEmail : trim((string)($data['billing']['email'] ?? ''));
      
      if ($recipient === '') throw new \RuntimeException('Recipient email missing for role '.$recipient_role);

      $context = $this->buildContext($type, $recipient_role, $data);
      $subject = $this->resolveLabelWithFallback('SUBJECT', $context);
      $html_tpl = $this->resolveLabelWithFallback('HTML', $context);
      $text_tpl = $this->resolveLabelWithFallback('TEXT', $context);


      $cart_html = $this->renderCart($data);

      $replacements = $this->buildReplacements($data, $cart_html);
      $subject = $this->replacePlaceholders($subject, $replacements);
      $html_tpl = $html_tpl ? $this->replacePlaceholders($html_tpl, $replacements) : null;
      $text_tpl = $text_tpl ? $this->replacePlaceholders($text_tpl, $replacements) : ($html_tpl ? $this->fallbackTextFromHtml($html_tpl, $data) : '');

      $attachments = $this->addPDF($recipient_role, $type, $id_order, $data);

      $this->sendMail($recipient, $subject, (string)$html_tpl, (string)$text_tpl, $attachments);

			$sql = "UPDATE orders_mail_queue SET status = 'sent', datetime = :datetime, error = NULL WHERE id = :id";
      $result = $this->pdo->prepare($sql);
      $result->bindValue(':datetime', time(), PDO::PARAM_INT);
      $result->bindValue(':id', $queue['id'], PDO::PARAM_INT);
      $result->execute();

    } catch (\Throwable $e) {
    	$sql = "UPDATE orders_mail_queue SET status = 'failed', attempts = attempts+1, error = :error WHERE id = :id";
      $result = $this->pdo->prepare($sql);
      $result->bindValue(':error', $e->getMessage());
      $result->bindValue(':id', $queue['id'], PDO::PARAM_INT);
      $result->execute();
    }
  }


  private function loadData(int $id_order): array {
    $order = $this->fetchOrderRow($id_order);
    if (!$order) throw new \RuntimeException('Order not found: '.$id_order);

    $sql = "SELECT * FROM orders_addresses WHERE id_module = :id_module ORDER BY FIELD(type,'billing','shipping')";
    $result = $this->pdo->prepare($sql);
    $result->bindValue(':id_module', $id_order, PDO::PARAM_INT);
    $result->execute();
    $billing = $shipping = null;
    foreach ($result->fetchAll(PDO::FETCH_ASSOC) ?: [] AS $arr) {
      if (($arr['type'] ?? '') === 'billing') $billing  = $arr;
      if (($arr['type'] ?? '') === 'shipping') $shipping = $arr;
    }
    if (!$shipping) $shipping = $billing;

		$sql = "SELECT sku, title, quantity, unit_total_gross AS unit_price_gross, line_total_gross AS line_total_gross FROM orders_items WHERE id_module = :id_module";
    $result = $this->pdo->prepare($sql);
    $result->bindValue(':id_module', $id_order, PDO::PARAM_INT);
    $result->execute();
    $items = [];
    foreach ($result->fetchAll(PDO::FETCH_ASSOC) ?: [] AS $arr) {
      $quantity = (int)($arr['quantity'] ?? 1);
      $unit = (float)($arr['unit_price_gross'] ?? 0.0);
      $items[] = [
        'sku'        => $arr['sku'] ?? null,
        'name'       => $arr['title'] ?? '',
        'quantity'   => $quantity,
        'unit_price' => $unit,
        'total'      => (float)($arr['line_total_gross'] ?? ($quantity*$unit)),
      ];
    }

    $totals = [
      'subtotal_net'   => (float)($order['totals_subtotal_net'] ?? 0.0),
      'tax'            => (float)($order['totals_tax'] ?? 0.0),
      'total_gross'    => (float)($order['totals_total_gross'] ?? 0.0),
      'subtotal_gross' => (float)($order['totals_subtotal_net'] ?? 0.0) + (float)($order['totals_tax'] ?? 0.0),
      'shipping'       => (float)($order['totals_shipping_gross'] ?? 0.0),
    ];

    $payment = $this->fetchPayment($id_order);

    return [
      'id_order'     => $id_order,
      'order_number' => $order['order_number'] ?? null,
      'order'        => $order,
      'items'        => $items,
      'totals'       => $totals,
      'billing'      => $billing,
      'shipping'     => $shipping,
      'payment'      => $payment,
      'shop'         => ['name' => $this->shopName],
    ];
  }

  private function fetchOrderRow(int $id_order): array {
    $sql = "SELECT * FROM orders WHERE id = :id LIMIT 1";
    $result = $this->pdo->prepare($sql);
    $result->bindValue(':id', $id_order, PDO::PARAM_INT);
    $result->execute();
    return $result->fetch(PDO::FETCH_ASSOC) ?: [];
  }

  private function fetchPayment(int $id_order): array {
    $sql = "SELECT provider, method, method_brand FROM orders_payment WHERE id_module = :id_module LIMIT 1";
    $result = $this->pdo->prepare($sql);
    $result->bindValue(':id_module', $id_order, PDO::PARAM_INT);
    $result->execute();
    $arr = $result->fetch(PDO::FETCH_ASSOC) ?: [];
    return [
      'provider' => strtoupper((string)($arr['provider'] ?? '')),
      'method'   => strtoupper((string)($arr['method'] ?? '')),
      'brand'    => strtoupper((string)($arr['method_brand'] ?? '')),
    ];
  }

  private function buildContext(string $type, string $recipient_role, array $data): array {
    $payment = $data['payment'] ?? [];
    return [
      'TYPE' 			=> strtoupper($type),
      'ROLE'    	=> strtoupper($recipient_role),
      'PROVIDER'	=> strtoupper($payment['provider'] ?? ''),
      'METHOD'  	=> strtoupper($payment['method'] ?? ''),
      'BRAND'   	=> strtoupper($payment['brand'] ?? ''),
    ];
  }
      
  private function resolveLabelWithFallback(string $kind, array $context): ?string {
    $keys = $this->labelKeys($kind, $context);
    foreach ($keys AS $key) {
      if (defined($key)) return constant($key);
    }
    return $kind.'_'.$context['TYPE'];
  }

  private function labelKeys(string $kind, array $context): array {
    $base = $kind.'_'.$context['TYPE'];
    $provider = $context['PROVIDER'] ? '_'.$context['PROVIDER'] : '';
    $method = $context['METHOD'] ? '_'.$context['METHOD'] : '';
    $brand = $context['BRAND'] ? '_'.$context['BRAND'] : '';
    $role = $context['ROLE'] ? '_'.$context['ROLE'] : '';

    return array_values(array_unique(array_filter([
      $base.$method.$provider.$brand.$role,
      $base.$method.$provider.$brand,
      $base.$method.$provider.$role,
      $base.$method.$provider,
      $base.$method.$role,
      $base.$method
    ])));
  }

  private function buildReplacements(array $data, string $cart_html): array {
    return [
      '[SHOP_NAME]'      		=> $this->shopName,
      '[ORDER_NUMBER]'    	=> (string)($data['order_number'] ?? '#'.$data['id_order']),
      '[ID_ORDER]'       		=> (string)$data['id_order'],
      '[TOTAL_GROSS]'    		=> input_out($data['totals']['total_gross'] ?? 0, 'float_de'),
      '[SUBTOTAL_GROSS]' 		=> input_out($data['totals']['subtotal_gross'] ?? 0, 'float_de'),
      '[TAX]'            		=> input_out($data['totals']['tax'] ?? 0, 'float_de'),
      '[SHIPPING]'       		=> input_out($data['totals']['shipping'] ?? 0, 'float_de'),
      '[PAYMENT_METHOD]' 		=> $data['payment']['method']  ?? '',
      '[PAYMENT_PROVIDER]'	=> $data['payment']['provider'] ?? '',
      '[PAYMENT_BRAND]'  		=> $data['payment']['brand'] ?? '',
      '[CART]'           		=> $cart_html,
    ];
  }

  private function replacePlaceholders(string $template, array $replacements): string {
    return strtr($template, $replacements);
  }

  private function renderCart(array $data): string {
    $file = $this->templateDir.'/order_cart.php';
    if (!is_file($file)) throw new \RuntimeException('Missing cart template: '.$file);
    $order = $data;
    ob_start();
    $return = include $file;
    $buffer = ob_get_clean();
    return is_string($return) && $return !== '' ? $return : $buffer;
  }

  private function fallbackTextFromHtml(string $html, array $data): string {
  	$label_order = defined('LABEL_ORDER') ? constant('LABEL_ORDER') : 'LABEL_ORDER';
		$label_total = defined('LABEL_TOTAL') ? constant('LABEL_TOTAL') : 'LABEL_TOTAL';
    $lines = [];
    $order_number = $data['order_number'] ?? ('#'.$data['id_order']);
    $lines[] = $label_order." ".$order_number;
    foreach ($data['items'] AS $item) {
      $lines[] = "- ".$item['name']." x".(int)$item['quantity']." á ".(float)$item['unit_price']." EUR = ".(float)$item['total']." EUR";
    }
    $lines[] = $label_total.": ".(float)($data['totals']['total_gross'] ?? 0)." EUR";
    return implode("\n", $lines);
  }


  private function addPDF(string $recipient_role, string $type, int $id_order, array $data): array {
    $template = $this->templateDir."/".$type."_".$recipient_role."_pdf.php";
    if (!is_file($template)) return [];

    if (!class_exists(\TCPDF::class)) {
      throw new \RuntimeException('TCPDF not available');
    }

    $order = $data;
    ob_start();
    $return = include $template;
    $buffer = ob_get_clean();
    $html = is_string($return) && $return !== '' ? $return : $buffer;

    $pdf = new \TCPDF();
    $pdf->SetCreator($this->shopName);
    $pdf->SetTitle("Order ".$id_order);
    $pdf->AddPage();
    $pdf->writeHTML($html, true, false, true, false, '');
    $outfile = sys_get_temp_dir()."/".$type."_".$recipient_role."_".$id_order.".pdf";
    $pdf->Output($outfile, 'F');

    return [$outfile];
  }

  private function sendMail(string $recipient, string $subject, string $html, string $text, array $attachments): void {
    $mail = new PHPMailer(true);
    $fromName = $this->fromName ?? $this->shopName;

    $mail->setFrom($this->fromEmail, $fromName);
    $mail->addAddress($recipient);
    foreach ($this->cc AS $cc) if ($cc) $mail->addCC($cc);
    foreach ($this->bcc AS $bc) if ($bc) $mail->addBCC($bc);

    $mail->Subject = $subject;
    if ($html !== '') {
      $mail->isHTML(true);
      $mail->Body = $html;
      $mail->AltBody = $text ?: strip_tags($html);
    } else {
      $mail->isHTML(false);
      $mail->Body = $text;
    }

    foreach ($attachments AS $path) if (is_file($path)) $mail->addAttachment($path);

    $mail->send();
  }
}
