<?php

declare(strict_types=1);

namespace KupShop\OrderingBundle\Util\Cart;

use Box\Spout\Common\Type;
use Box\Spout\Reader\Common\Creator\ReaderFactory;
use Box\Spout\Reader\ReaderInterface;
use KupShop\CatalogBundle\ProductList\ProductList;
use KupShop\KupShopBundle\Util\StringUtil;
use KupShop\OrderingBundle\Cart;
use Query\Operator;

readonly class CartImporter
{
    public function __construct(
        private Cart $cart,
        private ProductList $productList,
    ) {
    }

    /**
     * @return array{0: int, 1: string[]}
     */
    public function importFromString(string $data): array
    {
        $fp = fopen('php://temp', 'r+');
        fputs($fp, $data);
        rewind($fp);

        $result = $this->importFromStream($fp);

        fclose($fp);

        return $result;
    }

    /**
     * @return array{0: int, 1: string[]}
     */
    public function importFromFile(string $file, ?string $filename = null): array
    {
        if ($extension = $filename ? pathinfo($filename, PATHINFO_EXTENSION) : null) {
            $method = 'importFile_'.$extension;
            // if method exists use extension specific importer
            if (method_exists($this, $method)) {
                return $this->$method($file);
            }
        }

        // fallback to default importer
        $fp = fopen($file, 'r');
        $result = $this->importFromStream($fp);
        fclose($fp);

        return $result;
    }

    /**
     * Possible header keys (0: code column name, 1: pieces column name).
     *
     * Should be defined per e-shop when needed.
     *
     * @return iterable{0: string, 1: string}
     */
    protected function getPossibleHeaderKeys(): iterable
    {
        return [];
    }

    /**
     * Importer for XLS files.
     *
     * @return array{0: int, 1: string[]}
     */
    private function importFile_xls(string $file): array
    {
        return $this->genericFileImport($file, Type::XLSX);
    }

    /**
     * Importer for XLSX files.
     *
     * @return array{0: int, 1: string[]}
     */
    private function importFile_xlsx(string $file): array
    {
        return $this->genericFileImport($file, Type::XLSX);
    }

    /**
     * Importer for CSV files.
     *
     * @return array{0: int, 1: string[]}
     */
    private function importFile_csv(string $file): array
    {
        return $this->genericFileImport($file, Type::CSV);
    }

    /**
     * String/text/csv version of import used when textarea is filled instead of file upload.
     *
     * @param resource $stream
     *
     * @return array{0: int, 1: string[]}
     */
    private function importFromStream($stream): array
    {
        $count = 0;
        $errors = [];

        while ($line = fgets($stream)) {
            if (!($line = StringUtil::unicode_trim($line))) {
                continue;
            }

            if (!preg_match('/^([\s\S]+)\D(\d+)[^\d]*$/', $line, $matches)) {
                // invalid line, add error
                $errors[] = sprintf(translate('importLineError', 'ordering'), htmlspecialchars($line));
                continue;
            }

            $code = trim($matches[1]);

            if (!$this->importProduct($code, intval($matches[2]))) {
                // product was not found so add error
                $errors[] = sprintf(translate('importProductError', 'ordering'), $code);
                continue;
            }

            $count++;
        }

        return [$count, $errors];
    }

    /**
     * Method finds product and if found it will be added to cart.
     */
    private function importProduct(string $code, int $pieces): bool
    {
        $productList = $this->getProductList();

        $operands = [
            Operator::equals([
                Operator::coalesce(findModule(\Modules::PRODUCTS_VARIATIONS, \Modules::SUB_CODE) ? 'pv.code' : null, 'p.code') => $code,
            ]),
        ];

        if (preg_match('/^\d+$/', $code)) {
            $operands[] = Operator::equals([
                Operator::coalesce(findModule(\Modules::PRODUCTS_VARIATIONS) ? 'pv.ean' : null, 'p.ean') => $code,
            ]);
        }

        $productList->andSpec(Operator::orX($operands));

        if (!($product = $productList->getProducts()->current())) {
            return false;
        }

        $params = [
            'id_product' => $product->id,
            'id_variation' => $productList->getVariationsAsResult() ? $product->variationId ?? null : null,
            'pieces' => $pieces,
            'note' => '',
        ];

        $this->cart->addItem($params);

        return true;
    }

    /**
     * Generic method for file import using Spout library.
     */
    private function genericFileImport(string $file, string $type): array
    {
        $count = 0;
        $errors = [];

        foreach ($this->readFile($file, $type) as [$code, $pieces]) {
            if (!$this->importProduct((string) $code, (int) $pieces)) {
                $errors[] = sprintf(translate('importProductError', 'ordering'), $code);
                continue;
            }

            $count++;
        }

        return [$count, $errors];
    }

    private function readFile(string $file, string $type): iterable
    {
        $reader = $this->createReader($type);
        $reader->open($file);

        foreach ($reader->getSheetIterator() as $sheet) {
            // default code and pieces keys are 0,1
            $codeFieldKey = 0;
            $piecesFieldKey = 1;
            foreach ($sheet->getRowIterator() as $rowIndex => $row) {
                $data = $row->toArray();

                // check for header and try to find code and pieces column if a header is set
                if ($rowIndex === 1) {
                    $header = array_map(fn ($x) => StringUtil::slugify($x), $data);
                    foreach ($this->getPossibleHeaderKeys() as [$codeField, $piecesField]) {
                        if (($codeIndex = array_search($codeField, $header)) !== false && ($piecesIndex = array_search($piecesField, $header)) !== false) {
                            $codeFieldKey = $codeIndex;
                            $piecesFieldKey = $piecesIndex;
                            // skip to next row when header exists and fields are found
                            continue 2;
                        }
                    }
                }

                // ignore non-numeric pieces fields (invalid data)
                if (!is_numeric($data[$piecesFieldKey] ?? null)) {
                    continue;
                }

                $code = (string) ($data[$codeFieldKey] ?? null);
                $pieces = (int) ($data[$piecesFieldKey] ?? null);

                // skip row when there is 0 pieces to avoid unnecessary db queries
                if (array_key_exists($piecesFieldKey, $data) && $data[$piecesFieldKey] <= 0) {
                    continue;
                }

                yield [$code, $pieces];
            }
        }
    }

    private function createReader(string $type): ReaderInterface
    {
        $reader = ReaderFactory::createFromType($type);

        $configureMethod = "configureReader_{$type}";
        if (method_exists($this, $configureMethod)) {
            $this->{$configureMethod}($reader);
        }

        return $reader;
    }

    protected function getProductList(): ProductList
    {
        $productList = clone $this->productList;
        $productList->setVariationsAsResult(true);
        $productList->applyDefaultFilterParams();

        return $productList;
    }

    protected function configureReader_csv(ReaderInterface $reader): void
    {
        $reader->setFieldDelimiter(';');
    }
}
