<?php
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );

function __autoload_my_classes( $classname )
{
  $q = explode( '\\', $classname );
  $c = array_pop( $q );
  $f = 'autoload/' . implode( '/', $q ) . '/class.' . $c . '.php';

  if ( file_exists( $f ) )
    require_once( $f );
  else
  {
    $f = 'autoload/' . implode( '/', $q ) . '/' . $c . '.php';
    if ( file_exists( $f ) )
      require_once( $f );
  }
}

spl_autoload_register( '__autoload_my_classes' );
date_default_timezone_set( 'Europe/Warsaw' );

require_once 'config.php';
require_once 'libraries/medoo/medoo.php';
require_once 'libraries/phpmailer/class.phpmailer.php';
require_once 'libraries/phpmailer/class.smtp.php';

session_start();

if ( !isset( $_SESSION[ 'check' ] ) )
{
  session_regenerate_id();
  $_SESSION[ 'check' ] = true;
  $_SESSION[ 'ip' ] = $_SERVER[ 'REMOTE_ADDR' ];
}

if ( $_SESSION[ 'ip' ] !== $_SERVER[ 'REMOTE_ADDR' ] )
{
  session_destroy();
  header( 'Location: /' );
  exit;
}

$mdb = new medoo( [
    'database_type' => 'mysql',
    'database_name' => $database[ 'name' ],
    'server'        => $database[ 'host' ],
    'username'      => $database[ 'user' ],
    'password'      => $database[ 'password' ],
    'charset'       => 'utf8'
  ] );

// =========================================================================
// Auth: cron endpoint protection
// =========================================================================

if ( php_sapi_name() !== 'cli' )
{
  $cron_key = isset( $config['cron_key'] ) ? $config['cron_key'] : '';
  $provided_key = isset( $_GET['key'] ) ? $_GET['key'] : '';

  if ( $cron_key === '' || $provided_key !== $cron_key )
  {
    http_response_code( 403 );
    exit( 'Forbidden' );
  }
}

// =========================================================================
// Helper functions (used by handlers)
// =========================================================================

function parsePaczkomatAddress($input)
{
  $pattern = '/^([\w-]+)\s+\|\s+([^,]+),\s+(\d{2}-\d{3})\s+(.+)$/';

  if (preg_match($pattern, $input, $matches)) {
      return [
          'code' => $matches[1],
          'address' => $matches[2],
          'postalCode' => $matches[3],
          'city' => $matches[4],
      ];
  } else {
      return null;
  }
}

function parseOrlenAddress( $input )
{
  $pattern = '/^([^\|]+)\s*\|\s*([^,]+),\s*(.+?)\s+(\d{2}-\d{3})$/';
  if (preg_match($pattern, $input, $matches)) {
    return [
      'code' => trim($matches[1]),
      'address' => trim($matches[2]),
      'city' => trim($matches[3]),
      'postalCode' => trim($matches[4]),
    ];
  } else {
    return null;
  }
}

function getImageUrlById($id) {
  $apiUrl = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['SERVER_NAME'] . '/api/v1/product.php';

  $ch = curl_init();

  curl_setopt($ch, CURLOPT_URL, $apiUrl . '?id=' . urlencode($id));
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, array(
      'Content-Type: application/json',
  ));

  $response = curl_exec($ch);
  if ($response === false) {
      curl_close($ch);
      return null;
  }

  curl_close($ch);

  $data = json_decode($response, true);

  return isset($data['img']) ? $data['img'] : null;
}

// =========================================================================
// Shared dependencies
// =========================================================================

$settings = ( new \Domain\Settings\SettingsRepository( $mdb ) )->allSettings();
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb );
$orderRepo = new \Domain\Order\OrderRepository( $mdb );
$cronRepo = new \Domain\CronJob\CronJobRepository( $mdb );
$orderAdminService = new \Domain\Order\OrderAdminService( $orderRepo, null, null, null, $cronRepo );

$processor = new \Domain\CronJob\CronJobProcessor( $cronRepo );

// =========================================================================
// One-time migration: JSON queue → DB
// =========================================================================

$json_queue_path = __DIR__ . '/temp/apilo-sync-queue.json';
if ( file_exists( $json_queue_path ) )
{
  $json_content = file_get_contents( $json_queue_path );
  $json_queue = $json_content ? json_decode( $json_content, true ) : [];

  if ( is_array( $json_queue ) )
  {
    foreach ( $json_queue as $task )
    {
      $order_id = (int)($task['order_id'] ?? 0);
      if ( $order_id <= 0 ) continue;

      if ( !empty($task['payment']) )
      {
        $cronRepo->enqueue(
          \Domain\CronJob\CronJobType::APILO_SYNC_PAYMENT,
          ['order_id' => $order_id],
          \Domain\CronJob\CronJobType::PRIORITY_HIGH,
          50
        );
      }
      if ( isset($task['status']) && $task['status'] !== null && $task['status'] !== '' )
      {
        $cronRepo->enqueue(
          \Domain\CronJob\CronJobType::APILO_SYNC_STATUS,
          ['order_id' => $order_id, 'status' => (int)$task['status']],
          \Domain\CronJob\CronJobType::PRIORITY_HIGH,
          50
        );
      }
    }
  }

  unlink( $json_queue_path );
  echo '<p>Migracja kolejki JSON → DB zakończona</p>';
}

// =========================================================================
// Handler registration
// =========================================================================

// 1. Apilo token keepalive (priorytet: krytyczny)
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_TOKEN_KEEPALIVE, function($payload) use ($integrationsRepository) {
  $apilo_settings = $integrationsRepository->getSettings('apilo');
  if ( !(int)($apilo_settings['enabled'] ?? 0) ) return true; // skip if disabled

  $integrationsRepository->apiloKeepalive( 300 );
  echo '<p>Apilo token keepalive</p>';
  return true;
});

// 2. Apilo send order (priorytet: wysoki)
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_SEND_ORDER, function($payload) use ($mdb, $integrationsRepository, $orderAdminService, $config) {
  $apilo_settings = $integrationsRepository->getSettings('apilo');
  if ( !$apilo_settings['enabled'] || !$apilo_settings['sync_orders'] || !$apilo_settings['access-token'] || $apilo_settings['sync_orders_date_start'] > date('Y-m-d H:i:s') ) return true;

  $orders = $mdb->select( 'pp_shop_orders', '*', [ 'AND' => [ 'apilo_order_id' => null, 'date_order[>=]' => $apilo_settings['sync_orders_date_start'] ], 'ORDER' => [ 'date_order' => 'ASC' ], 'LIMIT' => 1 ] );
  if ( empty($orders) ) return true;

  foreach ( $orders as $order )
  {
    $products = $mdb->select( 'pp_shop_order_products', '*', [ 'order_id' => $order['id'] ] );
    $productRepo = new \Domain\Product\ProductRepository( $mdb );
    $products_array = [];
    $order_message = '';
    foreach ( $products as $product )
    {
      $sku = $productRepo->getSkuWithFallback( (int)$product['product_id'], true );

      $products_array[] = [
        'idExternal' => $product['product_id'],
        'ean' => $productRepo->getEanWithFallback( (int)$product['product_id'], true ),
        'sku' => $sku ? $sku : md5( $product['product_id'] ),
        'originalName' => $product['name'],
        'originalPriceWithTax' => (float)$product['price_brutto_promo'] > 0 ? str_replace( ',', '.', $product['price_brutto_promo'] ) : str_replace( ',', '.', $product['price_brutto'] ),
        'originalPriceWithoutTax' => (float)$product['price_brutto_promo'] > 0 ? str_replace( ',', '.', round( $product['price_brutto_promo'] / ( 1 + $product['vat']/100 ), 2 ) ) : str_replace( ',', '.', round( $product['price_brutto'] / ( 1 + $product['vat']/100 ), 2 ) ),
        'quantity' => $product['quantity'],
        'tax' => number_format( $product['vat'], 2, '.', '' ),
        'status' => 1,
        'type' => 1,
        'media' => $product['message']
      ];

      $order_message .= '<table>';
        $order_message .= '<tr>';
          $order_message .= '<td style="width: 150px; vertical-align: middle; text-align: center;">';
            $order_message .= '<img src="' . getImageUrlById( $product['parent_product_id'] ) . '" style="width: 150px;" alt="">';
          $order_message .= '</td>';
          $order_message .= '<td style="vertical-align: middle;">';
            $order_message .= '<strong>' . $product['name'] . '</strong><br>';
            $order_message .= strip_tags( $product['attributes'] ) . '<br>';
            $order_message .= preg_replace( '/\s+/', ' ', htmlspecialchars( $product['message'] ) ) . '<br>';
            $order_message .= strip_tags( str_replace( '<br>', ' | ', preg_replace( '/\s+/', ' ', $product['custom_fields'] ) ) ) . '<br>';
          $order_message .= '</td>';
        $order_message .= '</tr>';
      $order_message .= '</table>';
      $order_message .= '<hr>';
    }

    if ( strlen( $order_message ) > 850 )
      $order_message = '<p><strong>Wiadomość do zamówienia była zbyt długa. Sprawdź szczegóły w panelu sklepu</strong></p>';

    $products_array[] = [
      'idExternal' => '',
      'ean' => null,
      'sku' => '',
      'originalName' => strip_tags( $order['transport'] ),
      'originalPriceWithTax' => str_replace( ',', '.', $order['transport_cost'] ),
      'originalPriceWithoutTax' => str_replace( ',', '.', round( $order['transport_cost'] / ( 1 + 23/100 ), 2 ) ),
      'quantity' => 1,
      'tax' => number_format( 23, 2, '.', '' ),
      'status' => 1,
      'type' => 2,
      'media' => null
    ];

    $has_priced_products = false;
    foreach ( $products_array as $pa )
    {
      if ( $pa['type'] == 1 && (float)$pa['originalPriceWithTax'] > 0 )
      {
        $has_priced_products = true;
        break;
      }
    }
    if ( !$has_priced_products )
    {
      \Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Pominięto zamówienie - wszystkie produkty mają cenę 0.00', [ 'products' => $products_array ] );
      \Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Apilo: zamówienie #' . $order['id'] . ' ma zerowe ceny produktów', 'Zamówienie #' . $order['id'] . ' nie zostało wysłane do Apilo, ponieważ wszystkie produkty mają cenę 0.00 PLN. Sprawdź zamówienie w panelu sklepu.' );
      $mdb->update( 'pp_shop_orders', [ 'apilo_order_id' => -2 ], [ 'id' => $order['id'] ] );
      echo '<p>Pominięto zamówienie #' . $order['id'] . ' - zerowe ceny produktów</p>';
      continue;
    }

    $access_token = $integrationsRepository->apiloGetAccessToken();
    $order_date = new DateTime( $order['date_order'] );
    $paczkomatData = parsePaczkomatAddress( $order['inpost_paczkomat'] );
    $orlenPointData = parseOrlenAddress( $order['orlen_point'] );

    $street = '';
    $city = '';
    $postal_code = '';

    if ( $order['client_street'] )
      $street = $order['client_street'];
    else if ( $paczkomatData )
      $street = $paczkomatData['address'];
    else if ( $orlenPointData )
      $street = $orlenPointData['address'];
    else if ( $order['transport_id'] == 3 or ( $order['transport_id'] == 9 and $order['orlen_point'] == null ) )
      $street = 'ul. Krakowska 156/104';

    if ( $order['client_city'] )
      $city = $order['client_city'];
    else if ( $paczkomatData )
      $city = $paczkomatData['city'];
    else if ( $orlenPointData )
      $city = $orlenPointData['city'];
    else if ( $order['transport_id'] == 3 or ( $order['transport_id'] == 9 and $order['orlen_point'] == null ) )
      $city = 'Rzeszów';

    if ( $order['client_postal_code'] )
      $postal_code = $order['client_postal_code'];
    else if ( $paczkomatData )
      $postal_code = $paczkomatData['postalCode'];
    else if ( $orlenPointData )
      $postal_code = $orlenPointData['postalCode'];
    else if ( $order['transport_id'] == 3 or ( $order['transport_id'] == 9 and $order['orlen_point'] == null ) )
      $postal_code = '35-506';

    if ( $order['transport_id'] == 9 and $order['orlen_point'] == null )
    {
      \Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Błąd integracji APILO - brak punktu ORLEN PACZKA', 'W zamówieniu #' . $order['id'] . ' wybrano dostawę ORLEN PACZKA, ale nie podano punktu odbioru. Proszę o uzupełnienie danych w panelu sklepu.' );
    }

    $postData = [
      'idExternal' => $order['id'],
      'isInvoice' => $order['firm_name'] ? true : false,
      'customerLogin' => $order['client_email'],
      'paymentType' => (int)( new \Domain\PaymentMethod\PaymentMethodRepository( $mdb ) )->getApiloPaymentTypeId( (int)$order['payment_method_id'] ),
      'originalCurrency' => 'PLN',
      'originalAmountTotalWithTax' => str_replace( ',', '.', $order['summary'] ),
      'orderItems' => $products_array,
      'orderedAt' => $order_date->format('Y-m-d\TH:i:s\Z'),
      'addressCustomer' => [
        'name' => $order['client_name'] . ' ' . $order['client_surname'],
        'phone' => $order['client_phone'],
        'email' => $order['client_email'],
        'streetName' => $street,
        'city' => $city,
        'zipCode' => $postal_code
      ],
      'addressDelivery' => [
        'name' => $order['client_name'] . ' ' . $order['client_surname'],
        'phone' => $order['client_phone'],
        'email' => $order['client_email'],
        'streetName' => $street,
        'city' => $city,
        'zipCode' => $postal_code
      ],
      'carrierAccount' => (int)( new \Domain\Transport\TransportRepository( $mdb ) )->getApiloCarrierAccountId( (int)$order['transport_id'] ),
      'orderNotes' => [ [
        'type' => 1,
        'comment' => 'Wiadomość do zamówienia:<br>' . $order['message'] . '<br><br>' . $order_message
      ] ],
      'status' => (int)( new \Domain\ShopStatus\ShopStatusRepository( $mdb ) )->getApiloStatusId( (int)$order['status'] ),
      'platformId' => $apilo_settings['platform-id']
    ];

    if ( $order['firm_name'] )
    {
      $postData['addressInvoice']['name'] = $order['firm_name'];
      $postData['addressInvoice']['streetName'] = $order['firm_street'];
      $postData['addressInvoice']['city'] = $order['firm_city'];
      $postData['addressInvoice']['zipCode'] = $order['firm_postal_code'];
      $postData['addressInvoice']['companyTaxNumber'] = $order['firm_nip'];
    }

    if ( $order['inpost_paczkomat'] )
    {
      $postData['addressDelivery']['parcelName'] = $order['inpost_paczkomat'] ? 'Paczkomat: ' . $order['inpost_paczkomat'] : null;
      $postData['addressDelivery']['parcelIdExternal'] = $order['inpost_paczkomat'] ? ( $paczkomat = trim( explode( '|', $order['inpost_paczkomat'] )[0] ) ) : null;

      if ( !$postData['addressDelivery']['zipCode'] or !$postData['addressDelivery']['city'] )
      {
        preg_match("/\b\d{2}-\d{3}\b/", $order['inpost_paczkomat'], $postalCodeMatches);
        $postalCode = $postalCodeMatches[0] ?? '';

        $cityPattern = "/\d{2}-\d{3}\s+(.+)/";
        preg_match($cityPattern, $order['inpost_paczkomat'], $cityMatches);
        $city = $cityMatches[1] ?? '';

        $postData['addressDelivery']['zipCode'] = $postalCode;
        $postData['addressDelivery']['city'] = $city;
      }
    }

    if ( $order['orlen_point'] )
    {
      $postData['addressDelivery']['parcelName'] = $order['orlen_point'] ? 'Automat ORLEN ' . $order['orlen_point'] : null;
      $postData['addressDelivery']['parcelIdExternal'] = $order['orlen_point'] ? ( $parcelId = trim( explode( '-', $order['orlen_point'] )[1] ) ) : null;

      if ( !$postData['addressDelivery']['zipCode'] or !$postData['addressDelivery']['city'] )
      {
        preg_match("/\b\d{2}-\d{3}\b/", $order['orlen_point'], $postalCodeMatches);
        $postalCode = $postalCodeMatches[0] ?? '';

        $cityPattern = "/\d{2}-\d{3}\s+(.+)/";
        preg_match($cityPattern, $order['orlen_point'], $cityMatches);
        $city = $cityMatches[1] ?? '';

        $postData['addressDelivery']['zipCode'] = $postalCode;
        $postData['addressDelivery']['city'] = $city;
      }
    }

    if ( $order['paid'] )
    {
      $payment_date = new DateTime( $order['date_order'] );
      $postData['orderPayments'][] = [
        'amount' => str_replace( ',', '.', $order['summary'] ),
        'paymentDate' => $payment_date->format('Y-m-d\TH:i:s\Z'),
        'type' => ( new \Domain\PaymentMethod\PaymentMethodRepository( $mdb ) )->getApiloPaymentTypeId( (int)$order['payment_method_id'] )
      ];
    }

    $ch = curl_init();
    curl_setopt( $ch, CURLOPT_URL, "https://projectpro.apilo.com/rest/api/orders/" );
    curl_setopt( $ch, CURLOPT_POST, 1 );
    curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode( $postData ) );
    curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, "POST" );
    curl_setopt( $ch, CURLOPT_HTTPHEADER, array(
      "Authorization: Bearer " . $access_token,
      "Accept: application/json",
      "Content-Type: application/json"
    ));
    curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
    $response = curl_exec( $ch );
    if (curl_errno( $ch ) ) {
      $curl_error_send = curl_error( $ch );
      \Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Błąd cURL przy wysyłaniu zamówienia: ' . $curl_error_send, [ 'curl_error' => $curl_error_send ] );
      echo 'Błąd cURL: ' . $curl_error_send;
    }
    $http_code_send = (int)curl_getinfo( $ch, CURLINFO_HTTP_CODE );
    curl_close( $ch );

    $response = json_decode( $response, true );

    if ( isset($config['debug']['apilo']) && $config['debug']['apilo'] )
    {
      file_put_contents( $_SERVER['DOCUMENT_ROOT'] . '/logs/apilo.txt', date( 'Y-m-d H:i:s' ) . " --- SEND ORDER TO APILO\n\n", FILE_APPEND );
      file_put_contents( $_SERVER['DOCUMENT_ROOT'] . '/logs/apilo.txt', print_r( $postData, true ) . "\n\n", FILE_APPEND );
      file_put_contents( $_SERVER['DOCUMENT_ROOT'] . '/logs/apilo.txt', print_r( $response, true ) . "\n\n", FILE_APPEND );
    }

    if ( isset($response['message']) && $response['message'] == 'Order already exists' )
    {
      $apilo_order_id = str_replace( 'Order id: ', '', $response['description'] );
      $mdb->update( 'pp_shop_orders', [ 'apilo_order_id' => $apilo_order_id ], [ 'id' => $order['id'] ] );
      \Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Zamówienie już istnieje w Apilo (apilo_order_id: ' . $apilo_order_id . ')', [ 'http_code' => $http_code_send, 'response' => $response ] );
      echo '<p>Zaktualizowałem id zamówienia na podstawie zamówienia apilo.com</p>';
    }
    elseif ( isset($response['message']) && $response['message'] == 'Validation error' )
    {
      $is_duplicate_idexternal = false;
      if ( isset( $response['errors'] ) && is_array( $response['errors'] ) )
      {
        foreach ( $response['errors'] as $error )
        {
          if ( isset( $error['field'] ) && $error['field'] == 'idExternal' &&
               ( strpos( $error['message'], 'już wykorzystywana' ) !== false ||
                 strpos( $error['message'], 'already' ) !== false ) )
          {
            $is_duplicate_idexternal = true;
            break;
          }
        }
      }

      if ( $is_duplicate_idexternal )
      {
        $ch_get = curl_init();
        curl_setopt( $ch_get, CURLOPT_URL, "https://projectpro.apilo.com/rest/api/orders/?idExternal=" . $order['id'] );
        curl_setopt( $ch_get, CURLOPT_RETURNTRANSFER, true );
        curl_setopt( $ch_get, CURLOPT_HTTPHEADER, array(
          "Authorization: Bearer " . $access_token,
          "Accept: application/json"
        ));
        $get_response = curl_exec( $ch_get );
        curl_close( $ch_get );

        $get_response_data = json_decode( $get_response, true );

        if ( isset( $get_response_data['list'] ) && count( $get_response_data['list'] ) > 0 )
        {
          $apilo_order_id = $get_response_data['list'][0]['id'];
          $mdb->update( 'pp_shop_orders', [ 'apilo_order_id' => $apilo_order_id ], [ 'id' => $order['id'] ] );
          \Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Duplikat idExternal - pobrano apilo_order_id: ' . $apilo_order_id, [ 'http_code' => $http_code_send, 'response' => $response, 'get_response' => $get_response_data ] );
          echo '<p>Zamówienie już istnieje w Apilo. Zaktualizowano ID zamówienia: ' . $apilo_order_id . '</p>';
        }
        else
        {
          \Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Błąd: duplikat idExternal, ale nie znaleziono zamówienia w Apilo', [ 'http_code' => $http_code_send, 'response' => $response, 'get_response' => $get_response_data ] );
          $email_data = print_r( $response, true );
          $email_data .= print_r( $postData, true );
          \Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Błąd wysyłania zamówienia do apilo.com - nie znaleziono zamówienia', $email_data );
        }
      }
      else
      {
        \Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Błąd walidacji wysyłania zamówienia do Apilo', [ 'http_code' => $http_code_send, 'response' => $response ] );
        $email_data = print_r( $response, true );
        $email_data .= print_r( $postData, true );
        \Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Błąd wysyłania zamówienia do apilo.com', $email_data );
      }
    }
    elseif ( $http_code_send >= 400 || !isset( $response['id'] ) )
    {
      $mdb->update( 'pp_shop_orders', [ 'apilo_order_id' => -1 ], [ 'id' => $order['id'] ] );
      \Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Błąd wysyłania zamówienia do Apilo (HTTP ' . $http_code_send . ')', [ 'http_code' => $http_code_send, 'response' => $response ] );
      $email_data = 'HTTP Code: ' . $http_code_send . "\n\n";
      $email_data .= print_r( $response, true );
      $email_data .= print_r( $postData, true );
      \Shared\Helpers\Helpers::send_email( 'biuro@project-pro.pl', 'Błąd wysyłania zamówienia #' . $order['id'] . ' do apilo.com (HTTP ' . $http_code_send . ')', $email_data );
      echo '<p>Błąd wysyłania zamówienia do apilo.com: ID: ' . $order['id'] . ' (HTTP ' . $http_code_send . ')</p>';
    }
    else
    {
      $mdb->update( 'pp_shop_orders', [ 'apilo_order_id' => $response['id'] ], [ 'id' => $order['id'] ] );
      \Domain\Integrations\ApiloLogger::log( $mdb, 'send_order', (int)$order['id'], 'Zamówienie wysłane do Apilo (apilo_order_id: ' . $response['id'] . ')', [ 'http_code' => $http_code_send, 'response' => $response ] );
      echo '<p>Wysłałem zamówienie do apilo.com: ID: ' . $order['id'] . ' - ' . $response['id'] . '</p>';
    }
  }

  return true;
});

// 3. Apilo sync payment (event-driven — enqueued by OrderAdminService)
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_SYNC_PAYMENT, function($payload) use ($mdb, $orderRepo, $orderAdminService) {
  $order_id = (int)($payload['order_id'] ?? 0);
  if ( $order_id <= 0 ) return true;

  $order = $orderRepo->findRawById( $order_id );
  if ( !$order ) return true;

  if ( empty($order['apilo_order_id']) ) return false; // retry — awaiting apilo_order_id

  if ( (int)$order['paid'] !== 1 ) return true; // not paid — nothing to sync

  return $orderAdminService->syncApiloPayment( $order );
});

// 4. Apilo sync status (event-driven — enqueued by OrderAdminService)
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_SYNC_STATUS, function($payload) use ($mdb, $orderRepo, $orderAdminService) {
  $order_id = (int)($payload['order_id'] ?? 0);
  $status = isset($payload['status']) ? (int)$payload['status'] : null;
  if ( $order_id <= 0 || $status === null ) return true;

  $order = $orderRepo->findRawById( $order_id );
  if ( !$order ) return true;

  if ( empty($order['apilo_order_id']) ) return false; // retry — awaiting apilo_order_id

  return $orderAdminService->syncApiloStatus( $order, $status );
});

// 5. Apilo product sync
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_PRODUCT_SYNC, function($payload) use ($mdb, $integrationsRepository) {
  $apilo_settings = $integrationsRepository->getSettings('apilo');
  if ( !$apilo_settings['enabled'] || !$apilo_settings['sync_products'] || !$apilo_settings['access-token'] ) return true;

  $stmt = $mdb->query( 'SELECT id, apilo_product_id, apilo_get_data_date, apilo_product_name FROM pp_shop_products WHERE apilo_product_id IS NOT NULL AND apilo_product_id != 0 AND ( apilo_get_data_date IS NULL OR apilo_get_data_date <= \'' . date( 'Y-m-d H:i:s', strtotime( '-10 minutes', time() ) ) . '\' ) ORDER BY apilo_get_data_date ASC LIMIT 1' );
  $result = $stmt ? $stmt->fetch( \PDO::FETCH_ASSOC ) : null;
  if ( !$result ) return true;

  $access_token = $integrationsRepository->apiloGetAccessToken();
  $url = 'https://projectpro.apilo.com/rest/api/warehouse/product/' . $result['apilo_product_id'] . '/';
  $curl = curl_init( $url );
  curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
  curl_setopt( $curl, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer " . $access_token,
    "Accept: application/json"
  ] );

  $response = curl_exec( $curl );
  if ( $response === false ) return false;

  $responseData = json_decode( $response, true );
  if ( !is_array( $responseData ) || !isset( $responseData['quantity'] ) ) return false;

  $mdb->update( 'pp_shop_products', [ 'quantity' => $responseData['quantity'] ], [ 'apilo_product_id' => $result['apilo_product_id'] ] );
  $mdb->update( 'pp_shop_products', [ 'price_netto' => \Shared\Helpers\Helpers::normalize_decimal( $responseData['priceWithoutTax'], 2 ), 'price_brutto' => \Shared\Helpers\Helpers::normalize_decimal( $responseData['priceWithTax'], 2 ) ], [ 'apilo_product_id' => $result['apilo_product_id'] ] );
  $mdb->update( 'pp_shop_products', [ 'apilo_get_data_date' => date( 'Y-m-d H:i:s' ) ], [ 'apilo_product_id' => $result['apilo_product_id'] ] );
  \Shared\Helpers\Helpers::clear_product_cache( (int)$result['id'] );

  echo '<p>Zaktualizowałem dane produktu (APILO) <b>' . $result['apilo_product_name'] . ' #' . $result['id'] . '</b></p>';
  return true;
});

// 6. Apilo pricelist sync
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_PRICELIST_SYNC, function($payload) use ($mdb, $integrationsRepository) {
  $apilo_settings = $integrationsRepository->getSettings('apilo');
  if ( !$apilo_settings['enabled'] || !$apilo_settings['access-token'] ) return true;

  $access_token = $integrationsRepository->apiloGetAccessToken();
  $url = 'https://projectpro.apilo.com/rest/api/warehouse/price-calculated/?price=' . $apilo_settings['pricelist_id'];

  $curl = curl_init( $url );
  curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
  curl_setopt( $curl, CURLOPT_CUSTOMREQUEST, "GET" );
  curl_setopt( $curl, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer " . $access_token,
    "Accept: application/json",
    "Content-Type: application/json"
  ] );

  $response = curl_exec( $curl );
  if ( $response === false ) return false;

  $responseData = json_decode( $response, true );
  if ( !is_array( $responseData ) ) return false;

  if ( isset($responseData['list']) && $responseData['list'] )
  {
    foreach ( $responseData['list'] as $product_price )
    {
      if ( $product_price['customPriceWithTax'] )
      {
        $price_brutto = $product_price['customPriceWithTax'];
        $vat = $mdb->get( 'pp_shop_products', 'vat', [ 'apilo_product_id' => $product_price['product'] ] );
        $price_netto = $price_brutto / ( ( 100 + $vat ) / 100 );

        $mdb->update( 'pp_shop_products', [ 'price_netto' => \Shared\Helpers\Helpers::normalize_decimal( $price_netto, 2 ), 'price_brutto' => \Shared\Helpers\Helpers::normalize_decimal( $price_brutto, 2 ) ], [ 'apilo_product_id' => $product_price['product'] ] );
        $product_id = $mdb->get( 'pp_shop_products', 'id', [ 'apilo_product_id' => $product_price['product'] ] );

        ( new \Domain\Product\ProductRepository( $mdb ) )->updateCombinationPricesFromBase( (int)$product_id, $price_brutto, $vat, null );
        \Shared\Helpers\Helpers::clear_product_cache( (int)$product_id );
      }
    }
  }
  $integrationsRepository->saveSetting( 'apilo', 'pricelist_update_date', date( 'Y-m-d H:i:s' ) );
  echo '<p>Zaktualizowałem ceny produktów (APILO)</p>';
  return true;
});

// 7. Apilo status poll
$processor->registerHandler( \Domain\CronJob\CronJobType::APILO_STATUS_POLL, function($payload) use ($mdb, $integrationsRepository, $orderRepo, $orderAdminService) {
  $apilo_settings = $integrationsRepository->getSettings('apilo');
  if ( !$apilo_settings['enabled'] || !$apilo_settings['sync_orders'] || !$apilo_settings['access-token'] ) return true;

  $stmt = $mdb->query( 'SELECT id, apilo_order_id, apilo_order_status_date, number FROM pp_shop_orders WHERE apilo_order_id IS NOT NULL AND ( status != 6 AND status != 8 AND status != 9 ) AND ( apilo_order_status_date IS NULL OR apilo_order_status_date <= \'' . date( 'Y-m-d H:i:s', strtotime( '-10 minutes', time() ) ) . '\' ) ORDER BY apilo_order_status_date ASC LIMIT 5' );
  $orders = $stmt ? $stmt->fetchAll( \PDO::FETCH_ASSOC ) : [];

  foreach ( $orders as $order )
  {
    if ( $order['apilo_order_id'] )
    {
      $access_token = $integrationsRepository->apiloGetAccessToken();
      $url = 'https://projectpro.apilo.com/rest/api/orders/' . $order['apilo_order_id'] . '/';

      $ch = curl_init( $url );
      curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
      curl_setopt( $ch, CURLOPT_HTTPHEADER, [
        "Authorization: Bearer " . $access_token,
        "Accept: application/json"
      ] );

      $response = curl_exec( $ch );
      $http_code_poll = (int)curl_getinfo( $ch, CURLINFO_HTTP_CODE );
      $responseData = json_decode( $response, true );

      if ( isset($responseData['id']) && $responseData['id'] && isset($responseData['status']) && $responseData['status'] )
      {
        $shop_status_id = ( new \Domain\ShopStatus\ShopStatusRepository( $mdb ) )->getByIntegrationStatusId( 'apilo', (int)$responseData['status'] );

        if ( $shop_status_id )
          $orderAdminService->changeStatus( (int)$order['id'], $shop_status_id, false );

        \Domain\Integrations\ApiloLogger::log( $mdb, 'status_poll', (int)$order['id'], 'Status pobrany z Apilo (apilo_status: ' . $responseData['status'] . ', shop_status: ' . ($shop_status_id ? $shop_status_id : 'brak mapowania') . ')', [ 'apilo_order_id' => $order['apilo_order_id'], 'http_code' => $http_code_poll, 'response' => $responseData ] );

        $orderRepo->updateApiloStatusDate( (int)$order['id'], date( 'Y-m-d H:i:s' ) );
        echo '<p>Zaktualizowałem status zamówienia <b>' . $order['number'] . '</b></p>';
      }
    }
  }
  return true;
});

// 8. Price history
$processor->registerHandler( \Domain\CronJob\CronJobType::PRICE_HISTORY, function($payload) use ($mdb) {
  $results = $mdb->select( 'pp_shop_products', [ 'id', 'price_brutto', 'price_brutto_promo' ], [ 'OR' => [ 'price_history_date[!]' => date( 'Y-m-d' ), 'price_history_date' => null ], 'ORDER' => [ 'price_history_date' => 'ASC' ], 'LIMIT' => 100 ] );

  foreach ( $results as $row )
  {
    $price = $row['price_brutto_promo'] > 0 ? $row['price_brutto_promo'] : $row['price_brutto'];
    if ( $price )
    {
      $mdb->insert( 'pp_shop_product_price_history', [
        'id_product' => $row['id'],
        'price' => $price,
        'date' => date( 'Y-m-d' )
      ] );
    }

    $mdb->update( 'pp_shop_products', [ 'price_history_date' => date( 'Y-m-d' ) ], [ 'id' => $row['id'] ] );
    $mdb->delete( 'pp_shop_product_price_history', [ 'date[<=]' => date( 'Y-m-d', strtotime( '-31 days', time() ) ) ] );
    echo '<p>Zapisuję historyczną cenę dla produktu <b>#' . $row['id'] . '</b></p>';
  }

  return true;
});

// 9. Order analysis
$processor->registerHandler( \Domain\CronJob\CronJobType::ORDER_ANALYSIS, function($payload) use ($mdb) {
  $orders = $mdb->select( 'pp_shop_orders', 'id', [ 'parsed' => 0, 'LIMIT' => 1 ] );
  foreach ( $orders as $order )
  {
    $products = $mdb->select( 'pp_shop_order_products', 'product_id', [ 'order_id' => $order ] );
    foreach ( $products as $product1 )
    {
      if ( $parent_id = $mdb->get( 'pp_shop_products', 'parent_id', [ 'id' => $product1 ] ) )
        $product1 = $parent_id;

      foreach ( $products as $product2 )
      {
        if ( $parent_id = $mdb->get( 'pp_shop_products', 'parent_id', [ 'id' => $product2 ] ) )
          $product2 = $parent_id;

        if ( $product1 != $product2 )
        {
          $stmt = $mdb->query( 'SELECT * FROM pp_shop_orders_products_intersection WHERE product_1_id = :product_1_id AND product_2_id = :product_2_id OR product_1_id = :product_2_id AND product_2_id = :product_1_id', [ 'product_1_id' => (int)$product1, 'product_2_id' => (int)$product2 ] );
          $intersection_id = $stmt ? $stmt->fetch( \PDO::FETCH_ASSOC ) : null;
          if ( $intersection_id )
          {
            $mdb->update( 'pp_shop_orders_products_intersection', [ 'count' => $intersection_id['count'] + 1 ], [ 'id' => $intersection_id['id'] ] );
          }
          else
          {
            $mdb->insert( 'pp_shop_orders_products_intersection', [ 'product_1_id' => (int)$product1, 'product_2_id' => (int)$product2, 'count' => 1 ] );
          }
        }
      }
    }
    $mdb->update( 'pp_shop_orders', [ 'parsed' => 1 ], [ 'id' => $order ] );
    echo '<p>Parsuję zamówienie <b>#' . $order . '</b></p>';
  }
  return true;
});

// 10. Google XML feed
$processor->registerHandler( \Domain\CronJob\CronJobType::GOOGLE_XML_FEED, function($payload) use ($mdb) {
  ( new \Domain\Product\ProductRepository( $mdb ) )->generateGoogleFeedXml();
  echo '<p>Wygenerowano Google XML Feed</p>';
  return true;
});

// 11. TrustMate invitation — handled by separate cron-turstmate.php (requires browser context)
$processor->registerHandler( \Domain\CronJob\CronJobType::TRUSTMATE_INVITATION, function($payload) use ($config) {
  if ( !isset($config['trustmate']['enabled']) || !$config['trustmate']['enabled'] ) return true;
  // TrustMate requires browser context (JavaScript). Handled by cron-turstmate.php.
  return true;
});

// =========================================================================
// Run processor
// =========================================================================

$result = $processor->run( 20 );

echo '<hr>';
echo '<p><small>CronJob stats: scheduled=' . $result['scheduled'] . ', processed=' . $result['processed'] . ', succeeded=' . $result['succeeded'] . ', failed=' . $result['failed'] . ', skipped=' . $result['skipped'] . '</small></p>';
