<?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 = null;

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

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

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

    /**
     * Метки свойств
     */
    public function attributeLabels(): array
    {
        return array_merge(
            parent::attributeLabels(),
            [
                'catn' => 'Каталожный номер (catn)',
                'name' => 'Название (name)',
                'dscr' => 'Описание (dscr)',
                'prod' => 'Производитель (prod)',
                'dmns' => 'Габариты (dmns)',
                'wght' => 'Вес (wght)',
                'imgs' => 'Изображения (imgs)',
                'time' => 'Срок доставки (time)',
                'bffr' => 'Буфер',
                'stts' => 'Статус',
                'file_excel' => 'Документ (file_excel)',
                'file_image' => 'Изображение (file_image)',
                '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',
                        'name'
                    ],
                    'string',
                    'length' => [2, 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'
                ],
                [
                    'stts',
                    'string',
                    'length' => [4, 20],
                    'message' => '{attribute} должен быть строкой от 4 до 20 символов'
                ],
                [
                    'file_excel',
                    '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', 'jfif'],
                    'checkExtensionByMimeType' => true,
                    'maxFiles' => 10,
                    'maxSize' => 1024 * 1024 * 30,
                    'wrongExtension' => 'Разрешены только изображения в формате: ".jpg", ".jpeg", ".png", ".gif", ".webp"',
                    'message' => 'Проблема при загрузке изображения',
                    'on' =>  self::SCENARIO_IMPORT_IMAGE
                ]
            ]
        );
    }

    /**
     * Перед сохранением
     *
     * @todo Подождать обновление от ебаного Yii2 и добавить
     * проверку типов передаваемых параметров
     */
    public function beforeSave($create): bool
    {
        if (parent::beforeSave($create)) {
            // Пройдена родительская проверка

            if ($this->isNewRecord) {
                // Новая запись

                $this->stts = $this->stts ?? 'inactive';
            }

            return true;
        }

        return false;
    }

    /**
     * Запись пустого товара
     */
    public static function writeEmpty(string $catn, string $prod = 'Неизвестный', bool $active = false): ?self
    {
        // Инициализация
        $model = new self;

        // Настройки
        $model->catn = $catn;
        $model->prod = $prod;
        $model->stts = $active ? 'active' : 'inactive';

        // Запись
        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 $catn, int $limit = 999, 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 searchByCatnAndProd(string $catn, string $prod, int $limit = 1, array $select = []): static|array|null
    {
        if ($limit <= 1) {
            return static::findOne(['catn' => $catn, 'prod' => $prod]);
        }

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

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

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

        return $query;
    }

    /**
     * Поиск по каталожному номеру (через представления)
     *
     * Ищет продукт и возвращает его,
     * либо выполняет поиск через представление
     *
     * @todo Переделать нормально
     */
    public static function searchByPartialCatn(string $catn, string $stts = 'active', int $limit = 1, array $select = []): static|array|null
    {
        $query = self::find()
            ->for('product')
            ->in('product_search')
            ->where(['stts' => $stts])
            ->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 array|null Товар (Product)
     */
    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] ?? null;
    }

    /**
     * Подключение аналога
     *
     * @param Product $target Товар который надо подключить
     *
     * @return ProductEdgeProductGroup|null Ребро между товаром и группой, если создалось
     */
    public function connect(Product $product): ?ProductEdgeProductGroup
    {
        if (!$group = ProductGroup::searchByProduct($this)) {
            // Не найдена группа товаров

            // Запись новой группы
            $group = ProductGroup::writeEmpty(active: true);

            // Запись товара в группу
            $group->writeProduct($this);
        }

        if ($_group = ProductGroup::searchByProduct($product)) {
            // Найдена другая группа у товара который надо добавить в группу

            // Перенос всех участников (включая целевой товар)
            return $group->transfer($_group);
        } else {
            // Не найдена группа у товара который надо добавить в группу

            // Запись целевого товара в группу
            return $group->writeProduct($product);
        }

        return null;
    }

    /**
     * Отключение аналога
     *
     * @return bool Статус выполнения
     */
    public function disconnect(): bool
    {
        if ($group = ProductGroup::searchByProduct($this)) {
            // Найдена группа товаров

            // Удаление из группы
            $group->deleteProduct($this);

            return true;
        } else {
            // Не найдена группа товаров

            // Заебись

            return true;
        }

        return false;
    }

    /**
     * Проверка на уникальность
     *
     * @return bool|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;
        }

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

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

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

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

                return $model;
            }

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

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

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

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

                continue;
            }

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

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

        return $models;
    }

    /**
     * Активация
     *
     * @return bool Статус выполнения
     */
    public function activate(): bool
    {
        $this->stts = 'active';

        if ($this->update() > 0) return true;

        return false;
    }

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

    /**
     * Инициализация обложки для товара
     *
     * Ищет логотип в нужной категории (размере) для выбранного производителя
     *
     * @param string $prod Производитель
     * @param int $size Размерная группа
     *
     * @return string Относительный путь до изображения от публичной корневой папки
     */
    public static function cover(string $prod, int $size = 150): string
    {
        if ($size === 0) $size = '';
        else $size = "h$size/";

        // Инициализация пути
        $path = "/img/covers/$size" . strtolower($prod);

        // Поиск файла и возврат
        if (file_exists(YII_PATH_PUBLIC . DIRECTORY_SEPARATOR . $path . '.jpg')) return $path . '.jpg';
        else if (file_exists(YII_PATH_PUBLIC . DIRECTORY_SEPARATOR . $path . '.jpeg')) return $path . '.jpeg';
        else if (file_exists(YII_PATH_PUBLIC . DIRECTORY_SEPARATOR . $path . '.png')) return $path . '.png';

        // Возврат изображения по умолчанию
        return "/img/covers/$size" . 'product.png';
    }
}
