<?php
/*
 * @package    ECLCDEK shipping plugin СДЭК to RadicalMart
 * @subpackage    ECLCDEK
 * @version    1.0.1
 * @author Econsult Lab.
 * @copyright  Copyright (c) 2025 Econsult Lab. All rights reserved.
 * @license    GNU/GPL license: https://www.gnu.org/copyleft/gpl.html
 * @link       https://econsultlab.ru
 */

namespace Joomla\Plugin\RadicalMartShipping\ECLCDEK\Helper;

use CdekSDK2\BaseTypes\Location;
use CdekSDK2\BaseTypes\Package;
use Exception;
use JConfig;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\Field\ListField;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Version;
use Joomla\Component\RadicalMart\Site\Model\CheckoutModel;
use Joomla\Database\DatabaseInterface;
use Joomla\Database\ParameterType;
use Joomla\Registry\Registry;
use stdClass;

class ECLCDEKHelper
{
	/**
	 * Параметры плагина
	 *
	 * @var Registry|null
	 * @since 1.0.0
	 */
	private static ?Registry $_params = null;

	/**
	 * Определяет включение режима отладки плагина
	 *
	 * @return bool
	 *
	 * @since 1.0.0
	 */
	public static function isDebug(): bool
	{
		self::_getPluginParams();

		return (bool) self::$_params->get('debug', 0);
	}

	/**
	 * Формирует секцию товара для расчета стоимости заказа
	 *
	 * @param   array   $products  Товары заказа
	 * @param   object  $shipping  Метод доставки
	 *
	 * @return array
	 *
	 * @since 1.0.0
	 */
	private static function _getGoods(array $products, object $shipping): array
	{
		$packages = $shipping->params->get('packages', array());
		$tmp      = array();
		foreach ($packages as $package)
		{
			$val       = $package->length * $package->width * $package->height;
			$tmp[$val] = $package;
		}
		ksort($tmp);
		$value       = 0;
		$full_weight = 0.0;
		foreach ($products as $product)
		{
			$quantity         = $product->order['quantity'];
			$weight_unit      = $product->shipping->get('weight_unit', 'g');
			$dimensions_units = $product->shipping->get('dimensions_units', 'cm');
			$width            = self::_normalizeDimensions($product->shipping->get('width', 0), $dimensions_units, 'cm');
			$length           = self::_normalizeDimensions($product->shipping->get('length', 0), $dimensions_units, 'cm');
			$height           = self::_normalizeDimensions($product->shipping->get('$height', 0), $dimensions_units, 'cm');
			$weight           = self::_normalizeWeight($product->shipping->get('weight', 0), $weight_unit, 'g');
			$value            += $width * $length * $height * $quantity;
			$full_weight      += $weight * $quantity;
		}
		$out = array();

		//TODO Необходимо проработать механизм разделения заказа на упаковки
		// Реализовано просто деление на упаковки из расчета одного типа упаковки
		foreach ($tmp as $pkg_val => $package)
		{
			if ($value <= $pkg_val)
			{
				$out[] = Package::create(
					array(
						'width'  => (float) $package->width,
						'height' => (float) $package->height,
						'length' => (float) $package->length,
						'weight' => (float) $full_weight)
				);
			}
			else
			{
				$count = 1;
				while ($count * $pkg_val < $value)
				{
					$out[] = Package::create(
						array(
							'width'  => (float) $package->width,
							'height' => (float) $package->height,
							'length' => (float) $package->length,
							'weight' => (float) $full_weight)
					);
					$count++;
				}
				// Корректируем вес каждой посылки
				foreach ($out as $item)
				{
					$item->weight = $item->weight / ($count - 1);
				}
			}
		}

		return $out;
	}

	/**
	 * Приводит измерения товаров к базовым единицам. Например, килограммы в граммы.
	 *
	 * @param   float   $value         Значение измерения в исходных единицах
	 * @param   string  $sourceUnit    Исходная единица измерения
	 * @param   string  $targetUnit    Базовая единица измерения
	 * @param   array   $allowedUnits  Перечень разрешенных единиц измерения
	 *
	 * @return float
	 *
	 * @since 1.0.0
	 */
	private static function _normalizeProductParams(float $value, string $sourceUnit, string $targetUnit, array $allowedUnits): float
	{
		if (strtolower($sourceUnit) === strtolower($targetUnit) ||
			!key_exists($targetUnit, $allowedUnits) || !key_exists($sourceUnit, $allowedUnits))
		{
			return $value;
		}

		return $value * $allowedUnits[$sourceUnit] / $allowedUnits[$targetUnit];
	}

	/**
	 * Приводит габариты товаров к базовым единицам.
	 *
	 * @param   float   $value       Значение измерения в исходных единицах
	 * @param   string  $sourceUnit  Исходная единица измерения
	 * @param   string  $targetUnit  Базовая единица измерения
	 *
	 * @return float
	 *
	 * @since 1.0.0
	 */
	private static function _normalizeDimensions(float $value, string $sourceUnit, string $targetUnit): float
	{
		$allowedUnits = array('mm' => 1, 'cm' => 10, 'm' => 100, 'in' => 25.4, 'ft' => 304.8);

		return self::_normalizeProductParams($value, $sourceUnit, $targetUnit, $allowedUnits);
	}

	/**
	 * Приводит вес товаров к базовым единицам.
	 *
	 * @param   float   $value       Значение измерения в исходных единицах
	 * @param   string  $sourceUnit  Исходная единица измерения
	 * @param   string  $targetUnit  Базовая единица измерения
	 *
	 * @return float
	 *
	 * @since 1.0.0
	 */
	private static function _normalizeWeight(float $value, string $sourceUnit, string $targetUnit): float
	{
		$allowedUnits = array('g' => 1, 'kg' => 1000, 't' => 1000000, 'lb' => 453.59, 'ozt' => 28.35);

		return self::_normalizeProductParams($value, $sourceUnit, $targetUnit, $allowedUnits);
	}

	/**
	 * Нормализация данных тарифа
	 *
	 * @param   mixed  $option  Данные тарифа
	 *
	 * @return false|array
	 *
	 * @since 1.0.0
	 */
	private static function _normalizeDeliveryTariffs($option): false|array
	{
		if ($option == '-')
			$option = [];
		else if (!is_array($option))
		{
			$option = [$option];
		}
		foreach ($option as &$item)
		{
			$item = (int) $item;
		}

		return $option;
	}

	/**
	 * Формирует объект разрешенных тарифов из настроек метода доставки
	 *
	 * @param   object  $shipping  Настройки метода доставки
	 *
	 * @return stdClass
	 *
	 * @since 1.0.0
	 */
	private static function _getTariffs(object $shipping): object
	{
		$o         = new stdClass();
		$o->office = self::_normalizeDeliveryTariffs($shipping->params->get('tariffs_office', false));
		$o->door   = self::_normalizeDeliveryTariffs($shipping->params->get('tariffs_door', false));
		$o->pickup = self::_normalizeDeliveryTariffs($shipping->params->get('tariffs_pickup', false));;

		return $o;
	}


	/**
	 * Получает параметры расчета стоимости доставки в виде массива
	 *
	 * @param   object         $shipping  Параметры метода доставки
	 * @param   object         $item      Объект корзины
	 * @param   Location|null  $to        Параметры места назначения
	 *
	 * @return array
	 *
	 * @since 1.0.0
	 */
	public static function buildCalculateParams(object $shipping, object $item, Location $to = null): array
	{
		self::_getPluginParams();
		$from            = new Location(array(
			'country_code' => $shipping->params->get('from_country_code', 'RU'),
			'city'         => '',
			'code'         => $shipping->params->get('from_city', '')
		));
		$to              = is_null($to) ? new Location(array(
				'address'      => '',
				'code'         => '',
				'country_code' => 'RU')
		) : $to;
		$goods           = self::_getGoods($item->products, $shipping);
		$currency        = $shipping->order->price["currency"];
		$tariffs         = self::_getTariffs($shipping);
		$deliveryOptions = $shipping->params->get('delivery_options', 'office');
		$shipping_base   = (object) $shipping->order->price;

		return self::_buildCalculateParams($from, $to, $goods, $currency, $tariffs, $deliveryOptions, $shipping_base);
	}

	/**
	 * Получает параметры расчета стоимости доставки в виде строки JSON
	 *
	 * @param   object         $shipping  Параметры метода доставки
	 * @param   object         $item      Объект корзины
	 * @param   Location|null  $to        Параметры места назначения
	 *
	 * @return string
	 *
	 * @since 1.0.0
	 */
	public static function buildCalculateParamsJSON(object $shipping, object $item, Location $to = null): string
	{
		self::_getPluginParams();
		$from            = new Location(array(
			'country_code' => $shipping->params->get('from_country_code', 'RU'),
			'city'         => '',
			'code'         => $shipping->params->get('from_city', '')
		));
		$to              = is_null($to) ? new Location(array(
				'address'      => '',
				'code'         => '',
				'country_code' => 'RU')
		) : $to;
		$goods           = self::_getGoods($item->products, $shipping);
		$currency        = $shipping->order->price["currency"];
		$tariffs         = self::_getTariffs($shipping);
		$deliveryOptions = $shipping->params->get('delivery_options', 'office');
		$shipping_base   = (object) $shipping->order->price;

		return json_encode(self::_buildCalculateParams($from, $to, $goods, $currency, $tariffs, $deliveryOptions, $shipping_base), JSON_UNESCAPED_UNICODE);
	}

	/**
	 * Формирует структуру параметров расчета стоимости доставки
	 *
	 * @param   Location  $from             Локация отправления
	 * @param   Location  $to               Локация получения
	 * @param   array     $goods            Товары
	 * @param   string    $currency         Валюта
	 * @param   object    $tariffs          Разрешенные тарифы
	 * @param   string    $deliveryOptions  Тип доставки
	 * @param   object    $shipping_base    Базовая стоимость заказа
	 *
	 * @return array
	 *
	 * @since 1.0.0
	 */

	private static function _buildCalculateParams(Location $from, Location $to, array $goods, string $currency, object $tariffs, string $deliveryOptions, object $shipping_base): array
	{
		return array(
			'from'            => $from,
			'to'              => $to,
			'goods'           => $goods,
			'currency'        => $currency,
			'tariffs'         => $tariffs,
			'deliveryOptions' => $deliveryOptions,
			'shipping_base'   => $shipping_base
		);
	}

	/**
	 * Формирует параметры виджета доставки
	 *
	 * @param   object  $shipping  Параметры метода доставки
	 * @param   object  $item      Параметры корзины
	 *
	 * @return string
	 *
	 * @since 1.0.0
	 */
	public static function buildWidgetParams(object $shipping, object $item): string
	{
		self::_getPluginParams();
		$tmp = array(
			'from'                => new Location(
				array(
					'country_code' => $shipping->params->get('from_country_code', 'RU'),
					'code'         => $shipping->params->get('from_city', ''),
					'postal_code'  => $shipping->params->get('from_postal_code', ''),
					'address'      => $shipping->params->get('from_address', ''),
					'city'         => ''
				)),
			'root'                => 'cdek-map-' . $shipping->id,
			'popup'               => true,
			'apiKey'              => self::$_params->get('yandex_api_key', ''),
			'canChoose'           => true,
			'servicePath'         => '/plugins/radicalmart_shipping/eclcdek/src/CDEK/widget/scripts/service.php',
			'hideFilters'         => (object) array(
				'have_cashless'    => !(bool) $shipping->params->get('filters_have_cashless', false),
				'have_cash'        => !(bool) $shipping->params->get('filters_have_cash', false),
				'is_dressing_room' => !(bool) $shipping->params->get('filters_is_dressing_room', false),
				'type'             => !(bool) $shipping->params->get('filters_type', false),
			),
			'hideDeliveryOptions' => (object) array(
				'office' => $shipping->params->get('delivery_options', '') === 'door',
				'door'   => $shipping->params->get('delivery_options', '') == 'office'
			),
			'debug'               => self::isDebug(),
			'defaultLocation'     => '',
			'lang'                => 'rus',
		);

		return json_encode($tmp, JSON_UNESCAPED_UNICODE);
	}

	/**
	 * Получает параметры интерграции с СДЭК
	 *
	 * @param   string  $login   Логин
	 * @param   string  $secret  Пароль
	 *
	 *
	 * @since 1.0.0
	 */
	public static function getCDEKIntegrationParams(string &$login, string &$secret): void
	{
		self::_getPluginParams();

		$login  = self::$_params->get('cdek_login', "");
		$secret = self::$_params->get('cdek_pass', "");
	}

	/**
	 * Получает настройки плагина
	 *
	 * @since 1.0.0
	 */
	private static function _getPluginParams(): void
	{
		if (!self::$_params instanceof Registry)
		{
			$plugin        = PluginHelper::getPlugin('radicalmart_shipping', 'eclcdek');
			self::$_params = new  Registry($plugin->params);
		}
	}

	/**
	 * Сохранение отладочной информации в лог
	 *
	 * @param   string  $source         Наименование метода источника записи
	 * @param   string  $variable_name  Наименование переменной
	 * @param   mixed   $data           Переменная для записи в лог
	 *
	 * @return void
	 *
	 * @since 1.0.0
	 */
	public static function addLog(string $source, string $variable_name, $data): void
	{

		static $path;
		if ($data && self::isDebug())
		{
			if (empty($path))
			{
				$config = new JConfig();
				$path   = $config->log_path . '/' . 'eclcdek.php';
			}
			$line   = "";
			$is_new = !file_exists($path);
			if ($is_new)
			{
				// Файла нет - инициализируем создание
				$head    = array();
				$head[]  = '#';
				$head[]  = '#<?php die(\'Forbidden.\'); ?>';
				$head[]  = '#Date: ' . gmdate('Y-m-d H:i:s') . ' UTC';
				$version = new Version();
				$head[]  = '#Software: ' . $version->getLongVersion();
				$head[]  = '';

				// Prepare the fields string
				$head[] = '#Fields: Date/Time Source Variable Value';
				$head[] = '';
				$line   = implode("\n", $head);
			}

			$line   .= Factory::getDate()->format('Y-m-d H:i:s') . " ";
			$line   .= $source . " ";
			$line   .= $variable_name . " ";
			$line   .= json_encode($data, JSON_UNESCAPED_UNICODE) . "\n";
			$result = file_put_contents($path, $line, $is_new ? 0 : FILE_APPEND);
			if ($result === false)
			{
				throw new \RuntimeException('Cannot write to log file.');
			}
		}
	}

	/**
	 * Заполняет список опций поля ListField и устанавливает значение по-умолчанию на основании значения текста.
	 *
	 * @param   Form       $form           Форма, где расположено поле
	 * @param   ListField  $fld            Поле списка
	 * @param   object[]   $options        Массив значений опций
	 * @param   string     $valueKey       Наименование поля value
	 * @param   string     $textKey        Наименование поля Text
	 * @param   string     $selectedValue  Значение для поля текст, которое должно быть выбрано
	 *
	 * @since 1.0.0
	 */
	public static function addListFieldOptions(Form &$form, ListField &$fld, array $options, string $valueKey, string $textKey, string $selectedValue = ""): void
	{


		$fld->addOption(Text::_("JSELECT"), ["value" => ""]);

		foreach ($options as $option)
		{
			if (property_exists($option, $valueKey) && property_exists($option, $textKey))
			{
				if (isset($selectedValue) && $selectedValue === $option->$textKey)
				{
					$form->setFieldAttribute($fld->__get("fieldname"), 'default', $option->$valueKey, 'shipping');
				}
				$fld->addOption($option->$textKey, ['value' => $option->$valueKey]);
			}
		}
	}


	/**
	 * Получает отформатированный адрес
	 *
	 * @param   array               $address       Адрес
	 * @param   string              $deliveryType  Тип доставки
	 * @param   ECLTariffList|null  $tariff        Тариф доставки
	 * @param   string              $client        Клиент (site, admin)
	 * @param   string              $task          Задача, для которой получаем адрес
	 *
	 * @return string
	 *
	 * @throws Exception
	 *
	 * @since 1.0.0
	 */
	public static function getFormatedAddress(array $address, string $deliveryType, $tariff, string $client = 'site', string $task = 'checkout'): string
	{
		if (!in_array($client, ['site', 'admin']))
			$client = 'site';

		return match ($deliveryType)
		{
			'office', 'door' => LayoutHelper::render('plugins.radicalmart_shipping.eclcdek.radicalmart.' . $client . '.' . $task . '-results', array('address' => $address, 'deliveryType' => $deliveryType, 'tariff' => $tariff), JPATH_ROOT),
			default => throw new Exception("Unknown delivery option"),
		};
	}

	/**
	 * Получает форматированное сообщение для вывода пользователю
	 *
	 * @param   string  $type          Тип сообщения
	 * @param   string  $text          Текст сообщения
	 * @param   string  $deliveryType  Тип доставки
	 * @param   string  $client        Клиент ,для которого выводится сообщение (site, admin)
	 *
	 * @return string
	 *
	 * @throws Exception
	 *
	 * @since 1.0.0
	 */
	public static function getFormattedMessage(string $type, string $text, string $deliveryType, $client = 'site'): string
	{
		if (!in_array($client, ['site', 'admin']))
			$client = 'site';

		return match ($deliveryType)
		{
			'office', 'door' => LayoutHelper::render('plugins.radicalmart_shipping.eclcdek.radicalmart.' . $client . '.message', array('msg_type' => $type, 'msg_text' => $text), JPATH_ROOT),
			default => throw new Exception("Unknown delivery option"),
		};
	}

	/**
	 * Получает данные по умолчанию текущего пользователя.
	 * @see CheckoutModel getCustomerData
	 *
	 * @return array|false
	 *
	 * @throws Exception
	 * @since 1.0.1
	 */
	public static function getCurrentCustomerData(): false|array
	{
		$currentUser = Factory::getApplication()->getIdentity() ?: Factory::getUser();
		$currentCustomerData = [];
		if (!$currentUser->guest)
		{
			$pk = (int) $currentUser->id;

			if (empty($pk))
			{
				return false;
			}

			$db    = Factory::getContainer()->get(DatabaseInterface::class);

			$query = $db->getQuery(true)
				->select(['c.id', 'c.contacts', 'c.shipping', 'c.payment', 'c.plugins'])
				->from($db->quoteName('#__radicalmart_customers', 'c'))
				->where($db->quoteName('c.id') . ' = :id')
				->bind(':id', $pk, ParameterType::INTEGER);
			if (!$data = $db->setQuery($query, 0, 1)->loadAssoc())
			{
				return false;
			}

			// Set contacts
			$currentCustomerData['contacts'] = (new Registry($data['contacts']))->toArray();

			// Set shipping
			$currentCustomerData['shipping'] = (new Registry($data['shipping']))->toArray();

			// Set payment
			$currentCustomerData['payment'] = (new Registry($data['payment']))->toArray();

			// Set plugins
			$currentCustomerData['plugins'] = (new Registry($data['plugins']))->toArray();

		}

		return $currentCustomerData;
	}

	/**
	 * Method to recursive merge customer form data.
	 * @see CheckoutModel mergeCustomerFormData
	 *
	 * @param   array  $source  Source checkout data.
	 * @param   array  $new     Customer data.
	 *
	 * @return array Merging customer data.
	 *
	 * @since  1.1.0
	 */
	public static function mergeCustomerFormData(array $source = [], array $new = []): array
	{
		$result = $source;

		foreach ($new as $key => $value)
		{
			if (empty($value))
			{
				continue;
			}

			if (is_array($value))
			{
				$value = self::mergeCustomerFormData((!empty($source[$key])) ? $source[$key] : [], $value);
			}

			if (empty($source[$key]))
			{
				$result[$key] = $value;
			}
		}

		return $result;
	}
}