<?php

declare(strict_types=1);

namespace app\models\connection;

use yii;
use yii\base\Model;

use app\models\Dellin as DellinModel;
use app\models\Product;
use app\models\Account;
use app\models\Settings;

use GuzzleHttp\Client as Guzzle;
use GuzzleHttp\Exception\ClientException as GuzzleException;

use DateTime;
use DateTimeZone;
use Exception;

class Dellin extends Model
{
    /**
     * Инстанция браузера
     */
    public static Guzzle $browser;

    /**
     * Сессия аккаунта
     */
    public static string $session;

    public function __construct($config = [])
    {
        parent::__construct($config);

        self::$browser = new Guzzle([
            'base_uri'  => 'https://api.dellin.ru/'
        ]);

        self::authorization();
    }

    // /**
    //  * Поиск городов
    //  *
    //  * @return array|null Найденные города
    //  */
    // public function searchCities(): ?array
    // {
    // $this->onReady(function () {
    //     // Запрос городов
    //     $request = $this->browser->post('/v2/public/kladr.json', [
    //         'json' => [
    //             'appkey' => yii::$app->params['dellin']['key'],

    //         ]
    //     ])
    // });
    // }
    /**
     * Рассчет доставки (расширенный)
     *
     * Рассчет нескольких товаров идет через простое перемножение результатов доставки одного товара
     * В API всегда идет рассчет для одного товара, так было решено
     *
     * @param int $from Идентификатор терминала Dellin
     * @param int $to Идентификатор терминала Dellin
     * @param int $weight Вес (кг)
     * @param int $x Ширина (cм)
     * @param int $y Высота (cм)
     * @param int $z Длинна (cм)
     * @param int $amount Количество
     * @param Account|int|null $account Аккаунт
     *
     * @return string
     *
     * @todo Загружать помимо терминалов ещё и адреса, чтобы доделать доставку малогабаритных грузов
     * Разрабраться с параметрами 0,54м * 0,39м * 0,39м (0.082134м) и 0.1 куб метр в чем разница
     */
    public static function calcDeliveryAdvanced(int $from, int $to, int $weight, int $x, int $y, int $z, int $amount = 1, bool $avia = false, Account|int|null $account = null): array
    {
        return self::handle(function () use ($from, $to, $weight, $x, $y, $z, $amount, $avia, $account) {
            // Всё готово к работе

            $account = Account::initAccount($account);

            // Инициализация
            $from = DellinModel::searchByTerminalId($from, terminal_data_only: true);
            $to = DellinModel::searchByTerminalId($to, terminal_data_only: true);

            // Значения по умолчанию, если указан 0
            if (empty($x) || $x === 0) $x = 25;
            if (empty($y) || $y === 0) $y = 40;
            if (empty($z) || $z === 0) $z = 25;
            if (empty($weight) || $weight === 0) $weight = 300;

            // Конвертация из сантиметров в метры
            $x /= 100;
            $y /= 100;
            $z /= 100;

            // Вычисление самой крупной стороны, так как ДеловыеЛинии имеют ограничения на все три поля и у длинны оно больше всех
            if ($x > $z && $x > $y) {
                // "X" больше всех

                // Инициализация
                $width = $z;
                $height = $y;
                $length = $x;
            } else if ($y > $x && $y > $z) {
                // "Y" больше всех

                // Инициализация
                $width = $x;
                $height = $z;
                $length = $y;
            } else {
                // "Z" больше всех

                // Инициализация
                $width = $x;
                $height = $y;
                $length = $z;
            }

            // Инициализация
            $query = [];

            // Рассчёт типа доставки
            if (
                !$avia
                && $weight <= 30
                && ($length <= 0.54 && $width <= 0.39 && $height <= 0.39)
                && $length * $width * $height <= 0.1
            ) {
                // Доставка категории "small"

                $query['delivery']['deliveryType']['type'] = 'small';
                $query['delivery']['derival']['variant'] = 'address';
                $query['delivery']['derival']['address']['search'] = $from->fullAddress;
                $query['delivery']['derival']['time']['worktimeStart'] = '08:00';
                $query['delivery']['derival']['time']['worktimeEnd'] = '20:00';
                $query['delivery']['arrival']['variant'] = 'address';
                $query['delivery']['arrival']['address']['search'] = $to->fullAddress;
                $query['delivery']['arrival']['time']['worktimeStart'] = '08:00';
                $query['delivery']['arrival']['time']['worktimeEnd'] = '20:00';
            } else {
                // Доставка категории "auto"

                if ($avia) {
                    // Рассчет для доставки по воздуху

                    // Ограничение на минимальный вес
                    $weight <= 0.5 and $weight = 0.5;

                    $query['delivery']['deliveryType']['type'] = 'avia';
                } else {
                    // Рассчет для доставки по земле

                    $query['delivery']['deliveryType']['type'] = 'auto';
                }

                $query['delivery']['derival']['variant'] = 'terminal';
                $query['delivery']['derival']['terminalID'] = $from->id;
                $query['delivery']['arrival']['variant'] = 'terminal';
                $query['delivery']['arrival']['terminalID'] = $to->id;
            }

            // Инициализация часового пояса
            preg_match_all('/UTC([\+\-0-9:]*)/', $account->zone ?? Settings::searchActive()['timezone_default'] ?? 'UTC+3', $timezone);
            $timezone = $timezone[1][0];

            // Инициализация
            $query = array_merge_recursive(
                $query,
                [
                    'appkey' => yii::$app->params['dellin']['key'],
                    'sessionID' => self::$session,
                    'delivery' => [
                        'derival' => [
                            'produceDate' => (new DateTime())->setTimestamp(time() + 86400 * 3)->setTimezone(new DateTimeZone($timezone))->format('Y-m-d')
                        ]
                    ],
                    'members' => [
                        'requester' => [
                            'role' => 'sender'
                        ]
                    ],
                    'cargo' => [
                        'quantity' => 1,
                        'width' => $width,
                        'height' => $height,
                        'length' => $length,
                        'totalVolume' => $width * $height * $length,
                        'totalWeight' => $weight,
                        'oversizedWeight' => $weight,
                        'oversizedVolume' => $width * $height * $length
                    ]
                ]
            );

            // Запрос
            $request = self::$browser->post('/v2/calculator.json', [
                'json' => $query
            ]);

            if ($request->getStatusCode() === 200) {
                // Запрос прошел успешно

                // Инициализация
                $response = json_decode((string) $request->getBody(), true);

                if ($response['metadata']['status'] === 200) {
                    // Со стороны ДеловыеЛинии ошибок нет

                    $response['data']['price'] = [
                        'one' => $response['data']['price'],
                        'all' => $response['data']['price'] * $amount
                    ];

                    return $response['data'];
                }

                throw new Exception('На стороне сервера ДеловыеЛинии какие-то проблемы, либо отправлен неверный запрос', 500);
            }

            throw new Exception('Не удалось запросить рассчёт доставки у ДеловыеЛинии', 500);
        });
    }

    /**
     * Рассчет доставки
     *
     * @param string $from Идентификатор терминала Dellin
     * @param string $to Идентификатор терминала Dellin
     *
     * @return string
     *
     * @deprecated
     */
    public static function calcDelivery(string $from, string $to): array
    {
        return self::handle(function () use ($from, $to) {
            // Всё готово к работе

            // Запрос
            $request = self::$browser->post('/v1/micro_calc.json', [
                'json' => [
                    'appkey' => yii::$app->params['dellin']['key'],
                    'sessionID' => self::$session,
                    'derival' => [
                        'city' => $from
                    ],
                    'arrival' => [
                        'city' => $to
                    ]
                ]
            ]);


            if ($request->getStatusCode() === 200) {
                // Запрос прошел успешно

                // Инициализация
                $response = json_decode((string) $request->getBody(), true);

                if ($response['metadata']['status'] === 200) {
                    // Со стороны ДеловыеЛинии ошибок нет

                    return $response['data'];
                }

                throw new Exception('На стороне сервера ДеловыеЛинии какие-то проблемы, либо отправлен неверный запрос', 500);
            }

            throw new Exception('Не удалось запросить рассчёт доставки у ДеловыеЛинии', 500);
        });
    }

    /**
     * Импорт терминалов
     *
     * @return array|null Сохранённые терминалы
     */
    public static function importTerminals(Account|int|null $account = null): ?int
    {
        return self::handle(function () use ($account) {
            // Всё готово к работе

            if (is_null($account)) {
                // Данные аккаунта не переданы

                if (isset(yii::$app->user)) {
                    if (yii::$app->user->isGuest) {
                        // Аккаунт не аутентифицирован

                        return 0;
                    } else {
                        // Аккаунт аутентифицирован

                        // Инициализация
                        $account = yii::$app->user->identity;
                    }
                } else {
                    return 0;
                }
            } else {
                if (is_int($account)) {
                    // Передан идентификатор (_key) аккаунта (подразумевается)

                    // Инициализация (поиск в базе данных)
                    if (!$account = Account::searchById(Account::collectionName() . "/$account")) {
                        // Не удалось инициализировать аккаунт

                        return 0;
                    }
                }
            }

            // Запрос ссылки на файл с городами, возвращает ['hash' => string, 'url' => string]
            $request = self::$browser->post('/v3/public/terminals.json', [
                'json' => [
                    'appkey' => yii::$app->params['dellin']['key'],
                ]
            ]);

            if ($request->getStatusCode() === 200) {
                // Запрос прошел успешно

                // Инициализация часового пояса
                preg_match_all('/UTC([\+\-0-9:]*)/', $account->zone ?? Settings::searchActive()['timezone_default'] ?? 'UTC+3', $timezone);
                $timezone = $timezone[1][0];

                // Инициализация параметров
                $response = json_decode((string) $request->getBody(), true);
                $dir = YII_PATH_PUBLIC . '/../assets/import/' . (new DateTime('now', new DateTimeZone($timezone)))->format('Y-m-d') . '/dellin/terminals/' . (yii::$app->user->identity->_key ?? 'system') . '/';
                $amount = 0;

                if (!file_exists($dir)) {
                    // Директории не существует

                    mkdir($dir, 0775, true);
                }

                $request = self::$browser->get($response['url'], [
                    'sink' => $file = $dir . time() . '.json'
                ]);

                // Инициализация
                $terminals = json_decode(fread(fopen($file, "r"), filesize($file)), true);

                foreach ($terminals['city'] as $terminal) {
                    // Перебор городов

                    if ($model = DellinModel::searchByCityId($terminal['id'])) {
                        // Удалось найти город в базе данных

                        $after_import_log = function () use ($model): void {
                            // Запись в журнал
                            $model->journal('update');

                            if (yii::$app->getRequest()->isConsoleRequest) {
                                // Вызов из терминала

                                echo 'Удалось перезаписать терминалы города: ' . ($model->data['name'] ?? 'Неизвестно') . PHP_EOL;
                            }
                        };
                    } else {
                        // Не удалось найти город в базе данных

                        $model = new DellinModel();

                        $after_import_log = function () use ($model): void {
                            if (yii::$app->getRequest()->isConsoleRequest) {
                                // Вызов из терминала

                                echo 'Удалось записать терминалы города: ' . ($model->data['name'] ?? 'Неизвестно') . PHP_EOL;
                            }
                        };
                    }

                    // Запись
                    $model->data = $terminal;

                    // Отправка в базу данных
                    if ($model->save()) {
                        // Удалось сохранить в базе данных

                        // Запись в журнал
                        $after_import_log();

                        // Постинкрементация счётчика
                        $amount++;

                        continue;
                    } else {
                        // Не удалось сохранить в базе данных

                        throw new Exception('Не удалось сохранить терминалы города "' . ($model->data['name'] ?? 'Неизвестно') . '" в базу данных', 500);
                    }
                }

                return $amount;
            }

            throw new Exception('Не удалось синхронизировать данные городов с ДеловыеЛинии', 500);
        });
    }

    /**
     * Аутентификация и авторизация
     */
    protected static function authorization(): bool
    {
        // Аутентификация и авторизация
        $request = self::browser()->post('/v3/auth/login.json', [
            'json' => [
                'appkey' => yii::$app->params['dellin']['key'],
                'login' => yii::$app->params['dellin']['nickname'],
                'password' => yii::$app->params['dellin']['password']
            ]
        ]);

        if ($request->getStatusCode() === 200) {
            // Запрос прошел успешно

            // Инициализация
            $response = json_decode((string) $request->getBody(), true);

            if ($response['metadata']['status'] === 200) {
                // Аутентификация и авторизация пройдены успешно

                // Запись сессии
                self::$session = $response['data']['sessionID'];

                return true;
            }

            throw new Exception('Не удалось авторизироваться в ДеловыеЛинии', $response['metadata']['status']);
        }

        throw new Exception('Не удалось авторизироваться в ДеловыеЛинии', $request->getStatusCode);
    }

    /**
     * Инициализация и выполнение
     *
     * @param callable|null $function Код к выполнению
     * @param [mixed] ...$vars Параметры к нему
     *
     * @return mixed Возврат из функции
     */
    protected static function handle(callable $function = null, mixed ...$vars): mixed
    {
        try {
            if (self::browser() instanceof Guzzle) {
                // Браузер инициализирован

                if (self::authorization() && isset(self::$session)) {
                    // Аутентифицирован и авторизован

                    return $function(...$vars);
                } else {
                    throw new Exception('Аккаунт не авторизирован', 401);
                }
            } else {
                throw new Exception('Браузер не инициализирован', 500);
            }
        } catch (GuzzleException $e) {
            throw new Exception($e->getResponse()->getBody()->getContents() ?? 'Не удалось инициализировать инстанцию для работы с API ДеловыеЛинии', 500, $e->getPrevious());
        } catch (Exception $e) {
            throw new Exception($e->getMessage() ?? 'Не удалось инициализировать инстанцию для работы с API ДеловыеЛинии', 500, $e->getPrevious());
        }
    }

    /**
     * Чтение или инициализация браузера
     *
     * @return Guzzle Инстанция
     */
    protected static function browser(): Guzzle
    {
        return self::$browser ?? self::$browser = new Guzzle([
            'base_uri'  => 'https://api.dellin.ru/'
        ]);
    }
}
