<?php

declare(strict_types=1);

namespace app\models;

use yii\web\UploadedFile;
use yii\imagine\Image;

use app\models\traits\SearchByEdge;
use moonland\phpexcel\Excel;

/**
 * Продукт (в ассортименте магазина)
 *
 * Представляет собой лот состоящий из предложений от поставщиков
 *
 * @see Supply Поставки для продуктов
 */
class Product extends Document
{
    use SearchByEdge;

    /**
     * Сценарий импорта .excel документа
     *
     * Использовать для обхода правил при загрузке файла
     */
    const SCENARIO_IMPORT_EXCEL = 'import_excel';

    /**
     * Сценарий импорта изображений
     *
     * Использовать для обхода правил при загрузке файла
     */
    const SCENARIO_IMPORT_IMAGE = 'import_image';

    /**
     * Сценарий записи товара
     */
    const SCENARIO_WRITE = 'write';

    /**
     * Аккаунт
     *
     * Используется для управления администратором от лица пользователя
     */
    public Account|string|null $account = null;

    /**
     * Файл .excel для импорта товаров
     */
    public Excel|UploadedFile|string|null $file_excel_1 = null;

    /**
     * Файл .excel для импорта товаров
     */
    public Excel|UploadedFile|string|null $file_excel_2 = null;

    /**
     * Файл .excel для импорта товаров
     */
    public Excel|UploadedFile|string|null $file_excel_3 = null;

    /**
     * Изображение для импорта
     */
    public UploadedFile|string|array|null $file_image = null;

    /**
     * Группа в которой состоит товар
     */
    public ProductGroup|null $group = null;

    /**
     * Имя коллекции
     */
    public static function collectionName(): string
    {
        return 'product';
    }

    /**
     * Свойства
     */
    public function attributes(): array
    {
        return array_merge(
            parent::attributes(),
            [
                'catn',
                // В библеотеке есть баг на название DESC (неизвестно в моей или нет)
                'dscr',
                'prod',
                'dmns',
                'wght',
                'imgs',
                'time',
                'bffr',
            ]
        );
    }

    /**
     * Метки свойств
     */
    public function attributeLabels(): array
    {
        return array_merge(
            parent::attributeLabels(),
            [
                'catn' => 'Каталожный номер (catn)',
                'dscr' => 'Описание (dscr)',
                'prod' => 'Производитель (prod)',
                'dmns' => 'Габариты (dmns)',
                'wght' => 'Вес (wght)',
                'imgs' => 'Изображения (imgs)',
                'time' => 'Срок доставки (time)',
                'bffr' => 'Буфер',
                'file_excel_1' => 'Документ (file_excel_1)',
                'file_excel_2' => 'Документ (file_excel_2)',
                'file_excel_3' => 'Документ (file_excel_3)',
                'file_image' => 'Изображение (file_image)',
                'group' => 'Группа (group)',
                'account' => 'Аккаунт'
            ]
        );
    }

    /**
     * Правила
     *
     * @todo Правило для всех трёх габаритов
     */
    public function rules(): array
    {
        return array_merge(
            parent::rules(),
            [
                [
                    'catn',
                    'required',
                    'message' => 'Заполните поля: {attribute}',
                    'on' => self::SCENARIO_WRITE,
                    'except' => [self::SCENARIO_IMPORT_EXCEL, self::SCENARIO_IMPORT_IMAGE]
                ],
                [
                    'catn',
                    'string',
                    'message' => '{attribute} должен быть строкой'
                ],
                [
                    'prod',
                    'string',
                    'length' => [3, 80],
                    'message' => '{attribute} должен быть строкой от 3 до 80 символов'
                ],
                [
                    'imgs',
                    'arrayValidator',
                    'message' => '{attribute} должен быть массивом'
                ],
                [
                    'dscr',
                    'string',
                    'length' => [3, 256],
                    'message' => '{attribute} должен быть строкой от 3 до 256 символов'
                ],
                [
                    'dmns',
                    'arrayWithNumbersValidator',
                    'message' => '{attribute} должен быть массивом и хранить циферные значения'
                ],
                [
                    'wght',
                    'integer',
                    'min' => 0,
                    'max' => 30000,
                    'message' => '{attribute} должен иметь значение от 0 до 30000'
                ],
                [
                    [
                        'file_excel_1',
                        'file_excel_2',
                        'file_excel_3'
                    ],
                    'file',
                    'skipOnEmpty' => true,
                    'extensions' => 'xlsx',
                    'checkExtensionByMimeType' => false,
                    'maxSize' => 1024 * 1024 * 30,
                    'wrongExtension' => 'Разрешены только документы в формате: ".xlsx"',
                    'message' => 'Проблема при чтении документа',
                    'on' =>  self::SCENARIO_IMPORT_EXCEL
                ],
                [
                    'file_image',
                    'required',
                    'message' => 'Загрузите изображение',
                    'on' => self::SCENARIO_IMPORT_IMAGE
                ],
                [
                    'file_image',
                    'file',
                    'skipOnEmpty' => false,
                    'extensions' => ['jpg', 'jpeg', 'png', 'gif', 'webp'],
                    'checkExtensionByMimeType' => true,
                    'maxFiles' => 10,
                    'maxSize' => 1024 * 1024 * 30,
                    'wrongExtension' => 'Разрешены только изображения в формате: ".jpg", ".jpeg", ".png", ".gif", ".webp"',
                    'message' => 'Проблема при загрузке изображения',
                    'on' =>  self::SCENARIO_IMPORT_IMAGE
                ]
            ]
        );
    }

    /**
     * Запись пустого продукта
     */
    public static function writeEmpty(string $catn): ?self
    {
        // Инициализация
        $model = new self;

        // Настройки
        $model->catn = $catn;

        // Запись
        return $model->save() ? $model : null;
    }

    /**
     * Импорт изображений
     *
     * @return int Количество сохранённых изображений
     */
    public function importImages(): int
    {
        // Инициализация
        $amount = 0;

        if ($this->validate()) {
            // Проверка пройдена

            foreach ($this->file_image as $file) {
                // Перебор обрабатываемых изображений

                if (!file_exists(YII_PATH_PUBLIC . $catalog = '/img/products/' . $this->_key)) {
                    // Директория для изображений продукта не найдена

                    if (!mkdir(YII_PATH_PUBLIC . $catalog, 0775, true)) {
                        // Не удалось записать директорию

                        return false;
                    };
                }


                if (!file_exists(YII_PATH_PUBLIC . $catalog_h150 = '/img/products/' . $this->_key . '/h150')) {
                    // Директория для обложек изображений продукта не найдена

                    if (!mkdir(YII_PATH_PUBLIC . $catalog_h150, 0775, true)) {
                        // Не удалось записать директорию

                        return false;
                    };
                }

                // Запись на сервер
                $file->saveAs(YII_PATH_PUBLIC . $catalog . '/' . $file->baseName . '.' . $file->extension . '.original');

                // Конвертация изображения для сохранения полного изображения
                Image::resize(YII_PATH_PUBLIC . $catalog . '/' . $file->baseName . '.' . $file->extension . '.original', 800, 800)
                    ->save(YII_PATH_PUBLIC . $catalog . '/' . $file->baseName . '.' . $file->extension, ['quality' => 80]);

                // Конвертация изображения для сохранения обложки (150px)
                Image::resize(YII_PATH_PUBLIC . $catalog . '/' . $file->baseName . '.' . $file->extension, 150, 150)
                    ->save(YII_PATH_PUBLIC . $catalog_h150 . '/' . $file->baseName . '.' . $file->extension, ['quality' => 80]);

                // Инициализация
                $this->imgs ?? $this->imgs = [];

                // Запись в базу данных
                $this->imgs = array_merge(
                    $this->imgs,
                    [[
                        'covr' => count($this->imgs) === 0 ? true : false,
                        'orig' => $catalog . '/' . $file->baseName . '.' . $file->extension,
                        'h150' => $catalog_h150 . '/' . $file->baseName . '.' . $file->extension
                    ]]
                );

                $this->scenario = self::SCENARIO_WRITE;

                if ($this->save()) {
                    // Изменения сохранены в базе данных

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

        if ($this->hasErrors()) {
            // Получены ошибки

            foreach ($this->getErrors() as $attribute    => $errors) {
                // Перебор атрибутов

                foreach ($errors as $error) {
                    // Перебор ошибок атрибутов

                    $label = $this->getAttributeLabel($attribute);

                    Notification::_write("$label: $error", type: Notification::TYPE_ERROR);
                }
            }
        }

        return $amount;
    }

    /**
     * Поиск по каталожному номеру
     *
     * Ищет продукт и возвращает его,
     * либо выполняет поиск через представление
     *
     * @todo Переделать нормально
     */
    public static function searchByCatn(string|null $catn, int $limit = 1, array $select = []): static|array|null
    {
        if ($limit <= 1) {
            return static::findOne(['catn' => $catn]);
        }

        $query = self::find()
            ->where(['catn' => $catn])
            ->limit($limit)
            ->select($select)
            ->createCommand()
            ->execute()
            ->getAll();

        foreach ($query as &$attribute) {
            // Приведение всех свойств в массив и очистка от лишних данных

            $attribute = $attribute->getAll();
        }

        return $query;
    }

    /**
     * Поиск по каталожному номеру (через представления)
     *
     * Ищет продукт и возвращает его,
     * либо выполняет поиск через представление
     *
     * @todo Переделать нормально
     */
    public static function searchByPartialCatn(string $catn, int $limit = 1, array $select = []): static|array|null
    {
        $query = self::find()
            ->for('product')
            ->in('product_search')
            ->filter(['catn' => $catn], 'START_SENSETIVE')
            ->limit($limit)
            ->orderBy(['catn' => 'ASC'])
            ->select($select)
            ->createCommand()
            ->execute()
            ->getAll();

        foreach ($query as &$attribute) {
            // Приведение всех свойств в массив и очистка от лишних данных

            $attribute = $attribute->getAll();
        }

        return $query;
    }

    /**
     * Найти по идентификатору поставки
     *
     * @param string|null $_id Идентификатор поставки
     *
     * @return self|null Привязанный продукт
     */
    public static function searchBySupplyId(string $_id): ?array
    {
        return static::searchByEdge(
            from: 'supply',
            to: 'product',
            edge: 'supply_edge_product',
            direction: 'INBOUND',
            subquery_where: [
                [
                    'supply_edge_product._from == "' . $_id . '"'
                ],
                [
                    'supply_edge_product._to == product._id'
                ]
            ],
            subquery_select: 'product',
            where: 'supply_edge_product[0]._id != null',
            limit: 1,
            select: 'supply_edge_product[0]'
        )[0];
    }

    /**
     * Найти все аналоги
     *
     * @param string $catn Идентификатор товара
     * @param int $limit Ограничение по количеству
     *
     * @return array|null Найденные аналоги
     */
    public static function searchAnalogs(string $catn, int $limit = 30): ?array
    {
        // Инициализация буфера возврата
        $return = [];

        // Поиск ключей аналогов
        $analogs = ProductEdgeProduct::searchConnections(self::searchByCatn($catn)->_key, $limit);

        foreach ($analogs as $analog) {
            // Перебор найденных ключей (_key) аналогов

            if ($analog = Product::searchById(self::collectionName() . "/$analog")) {
                // Найден товар

                // Запись в буфер вывода
                $return[] = $analog;
            } else {
                // Не найден товар
            }
        }

        return $return;
    }

    /**
     * Синхронизация аналогов
     *
     * Связывает с товаром и связанными с ним товарами в одну группу
     *
     * @param self $to Цель
     *
     * @return array Созданные рёбра
     */
    public function synchronization(self $to): array
    {
        // Инициализация буфера сохранённых рёбер
        $edges = [];

        // Инициализация списка товаров в группе и буфера для связей "всех со всеми"
        $products = $_products =  array_unique(array_merge($this->connections(), $to->connections(), [Product::collectionName() . "/$to->_key"]));

        foreach ($products as $product) {
            // Перебор связей для создания связей во всех товарах

            // Удаление обрабатываемого товара из буферного списка товаров
            unset($_products[array_search($product, $_products)]);

            foreach ($_products as $_product) {
                // Перебор товаров из буфера

                if ($from = self::searchById($product)) {
                    // Товар найден
                } else {
                    if ($from = Product::writeEmpty($product)) {
                        // Удалось записать товар
                    } else {
                        // Не удалось записать товар

                        continue;
                    }
                }

                if ($to = self::searchById($_product)) {
                    // Товар найден
                } else {
                    if ($to = Product::writeEmpty($_product)) {
                        // Удалось записать товар
                    } else {
                        // Не удалось записать товар

                        continue;
                    }
                }

                if ($edge = $from->connect($to)) {
                    // Ребро создано

                    // Запись в буфер
                    $edges[] = $edge;
                }
            }
        }

        return $edges;
    }

    /**
     * Подключение аналога
     *
     * @param self $to Цель
     *
     * @return ProductEdgeProduct|null Ребро между товарами, если создалось
     */
    public function connect(self $to): ?ProductEdgeProduct
    {
        if (ProductEdgeProduct::searchByVertex(Product::collectionName() . "/$this->_key", Product::collectionName() . "/$to->_key", type: 'analogue')) {
            // Найдено ребро
        } else if (ProductEdgeProduct::searchByVertex(Product::collectionName() . "/$to->_key", Product::collectionName() . "/$this->_key", type: 'analogue')) {
            // Найдено ребро (наоборот)

            // !!! Вероятно эта проверка здесь не нужна, так как мы знаем входные данные
        } else {
            // Не найдены ребра

            if ($edge = ProductEdgeProduct::write(Product::collectionName() . "/$this->_key", Product::collectionName() . "/$to->_key", data: ['type' => 'analogue'])) {
                // Ребро сохранено

                // Запись в журнал о соединении
                $this->journal('connect analogue', [
                    'to' => Product::collectionName() . "/$to->_key"
                ]);

                // Запись в журнал о соединении
                $to->journal('connect analogue', [
                    'from' => Product::collectionName() . "/$this->_key"
                ]);

                return $edge;
            }
        }

        return null;
    }

    /**
     * Отключение аналога
     *
     * @param self|null $to Цель (если null, то целью являются все подключенные аналоги)
     * @param bool $all Удалить соединения со всеми членами группы
     */
    public function disconnect(self|null $to = null, bool $all = true): bool
    {
        if (isset($to)) {
            // Передана цель для удаления (из её группы)

            if ($edge = @ProductEdgeProduct::searchByVertex(Product::collectionName() . "/$this->_key", Product::collectionName() . "/$to->_key", type: 'analogue')[0]) {
                // Найдено ребро
            } else if ($edge = @ProductEdgeProduct::searchByVertex(Product::collectionName() . "/$to->_key", Product::collectionName() . "/$this->_key", type: 'analogue')[0]) {
                // Найдено ребро (наоборот)
            } else {
                // Не найдены ребра

                return false;
            }
        } else {
            // Не передана цель для удаления (из её группы)

            foreach ($this->connections() as $edge) {
                // Перебор всех рёбер

                if (Product::collectionName() . "/$this->_key" !== $edge && $to = Product::searchById($edge)) {
                    // Найден товар (проверен на то, что не является самим собой)

                    // Разъединение
                    $this->disconnect($to, all: false);
                }
            }

            return true;
        }

        if ($edge->delete() > 0) {
            // Удалось удалить ребро (связь)

            if ($all) {
                // Запрошено удаление соединений со всеми членами группы

                foreach ($to->connections() as $edge) {
                    // Перебор рёбер (найденных соединений с группой в которой находилась цель)

                    if (Product::collectionName() . "/$this->_key" !== $edge && $to = Product::searchById($edge)) {
                        // Найден товар (проверен на то, что не является самим собой)

                        // Разъединение
                        $this->disconnect($to, all: false);
                    }
                }
            }

            // Запись в журнал о разъединении
            $this->journal('disconnect analogue', [
                'to' => Product::collectionName() . "/$to->_key"
            ]);

            // Запись в журнал о соединении
            $to->journal('disconnect analogue', [
                'from' => Product::collectionName() . "/$this->_key"
            ]);

            return true;
        }

        return false;
    }

    /**
     * Найти все связанные товары
     *
     * @param int $limit Ограничение по максимальному значению
     */
    public function connections(int $limit = 100): array
    {
        // Инициализация буфера связанных товаров
        $products = [];

        foreach (ProductEdgeProduct::searchByDirection(self::collectionName() . "/$this->_key", direction: 'ANY', type: 'analogue', limit: $limit) as $edge) {
            // Перебор связей для создания списка товаров (вершин)

            // Добавление товаров (вершин) в буфер (подразумевается, что без дубликатов)
            if (!in_array($edge->_from, $products, true)) $products[] = $edge->_from;
            if (!in_array($edge->_to, $products, true)) $products[] = $edge->_to;
        }

        return $products;
    }

    /**
     * Проверка на уникальность
     *
     * @return bool|static true если создать новую запись, static если найден дубликат
     *
     * @todo
     * 1. Обработка дубликатов
     */
    public function validateForUniqueness(): bool|static
    {
        if ($supplies = self::search(['catn' => $this->catn, 'prod' => $this->prod], limit: 100)) {
            // Найдены поставки с таким же артикулом (catn) и производителем (prod)

            // if (count($supplies) > 1) throw new exception ('В базе данных имеется более чем один дубликат', 500);
            if (count($supplies) > 1) return false;

            // Запись обрабатываемой поставки
            $supply = $supplies[0];

            // Возврат (найден дубликат в базе данных)
            return $supply;
        } else return true;

        // Возврат (подразумевается ошибка)
        return false;
    }

    /**
     * Инициализация продукта
     *
     * @param string $catn Артикул, каталожный номер
     */
    public static function initEmpty(string $catn): Supply|array
    {
        $oemn = self::searchOemn($catn);

        if (count($oemn) === 1) {
            // Передан только один артикул

            if ($model = Product::searchByCatn($catn)) {
                // Продукт уже существует

                return $model;
            }

            // Запись пустого продукта
            return Product::writeEmpty($catn);
        }

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

        foreach ($oemn as $catn) {
            // Перебор всех найденных артикулов

            if ($model = Product::searchByCatn($catn)) {
                // Продукт уже существует

                continue;
            }

            // Запись
            if ($model = Product::writeEmpty($catn)) {
                // Записано

                // Запись в массив сохранённых моделей
                $models[] = $model;
            }
        }

        return $models;
    }
}
