<?php

declare(strict_types=1);

namespace KupShop\SellerBundle\Utils;

use KupShop\ComponentsBundle\Entity\Thumbnail;
use KupShop\KupShopBundle\Util\StringUtil;
use KupShop\OrderingBundle\Entity\Purchase\PurchaseState;
use KupShop\SellerBundle\Translations\SellersTranslation;
use Query\Operator;
use Query\QueryBuilder;
use Query\Translation;

class SellerUtil
{
    public const AVAILABILITY_IN_STORE = 2;
    public const AVAILABILITY_PARTIALLY_IN_STORE = 1;
    public const AVAILABILITY_NOT_IN_STORE = 0;

    protected SellerMultiFetch $multiFetch;

    private ?array $sellers = null;

    public function __construct(SellerMultiFetch $multiFetch)
    {
        $this->multiFetch = $multiFetch;
    }

    public function getSeller(int $sellerId): ?array
    {
        if (!($seller = $this->getSellers()[$sellerId] ?? false)) {
            return null;
        }

        if (findModule(\Modules::COMPONENTS) && !findModule(\Modules::SELLERS, \Modules::SUB_SMARTY_SELLERS) && isset($seller['photos'])) {
            $seller['thumbnails'] = array_map(function ($photo) {
                return new Thumbnail((string) $photo['id'], $photo['description'], ''.$photo['dateUpdated']->getTimestamp());
            }, $seller['photos']);
        }

        return $seller;
    }

    public function getSellers(bool $force = false): array
    {
        if ($this->sellers && $force === false) {
            return $this->sellers;
        }

        $qb = $this->getBaseQueryBuilder();

        $sellers = [];
        foreach ($qb->execute() as $item) {
            $sellers[$item['id']] = $this->prepareSeller($item);
        }

        $this->multiFetchSellersData($sellers);

        return $this->sellers = $sellers;
    }

    public function isSellerOrderingAllowed(array $seller): bool
    {
        return ($seller['data']['ordering_disabled'] ?? 'N') === 'N';
    }

    public function sellersGroupBy(array $sellers, string $fieldPath): array
    {
        $result = [];
        foreach ($sellers as $seller) {
            $path = explode('/', $fieldPath);
            $groupByField = $seller[array_shift($path)] ?? null;
            foreach ($path as $field) {
                if (empty($groupByField[$field])) {
                    $groupByField = null;
                }

                $groupByField = &$groupByField[$field];
            }

            $result[$groupByField ?: ''][$seller['id']] = $seller;
        }

        // seradim abecedne
        uksort($result, fn ($a, $b) => StringUtil::slugify($a) <=> StringUtil::slugify($b));

        return $result;
    }

    // Find closest seller by latitude and longitude
    public function getClosestSeller(float $latitude, float $longitude): ?array
    {
        $seller = $this->getBaseQueryBuilder()
            ->addSelect('(
               6371 *
               acos(cos(radians(:latitude)) *
               cos(radians(X(position))) *
               cos(radians(Y(position)) -
               radians(:longitude)) +
               sin(radians(:latitude)) *
               sin(radians(X(position))))
            ) AS distance')
            ->andWhere('position IS NOT NULL AND position != ""')
            ->addParameters(
                [
                    'latitude' => $latitude,
                    'longitude' => $longitude,
                ]
            )
            ->orderBy('distance', 'ASC')
            ->setMaxResults(1)
            ->execute()->fetch();

        if (!$seller) {
            return null;
        }

        return $this->prepareSeller($seller);
    }

    public function getSellersOrderedByClosestToPosition(float $latitude, float $longitude): ?array
    {
        $sellers = $this->getSellers();

        foreach ($sellers as &$seller) {
            if ($seller['x'] && $seller['y']) {
                $theta = $longitude - $seller['y'];
                $dist = sin(deg2rad($latitude)) * sin(deg2rad($seller['x'])) + cos(deg2rad($latitude)) * cos(deg2rad($seller['x'])) * cos(deg2rad($theta));
                $dist = acos($dist);
                $dist = rad2deg($dist);
                $miles = $dist * 60 * 1.1515;
                // vzdálenost v KM
                $seller['distance'] = $miles * 1.609344;
            } else {
                $seller['distance'] = 0;
            }
        }

        usort($sellers, fn ($a, $b) => $a['distance'] - $b['distance']);

        return $sellers;
    }

    public function prepareSeller(array $seller): array
    {
        $seller['id'] = (int) $seller['id'];
        $seller['blocks'] = [];
        $seller['data'] = array_replace_recursive(
            json_decode($seller['data'] ?: '', true) ?: [],
            json_decode($seller['data_translation'] ?? '', true) ?: [],
        );
        $seller['is_ordering_disabled'] = !$this->isSellerOrderingAllowed($seller);
        $seller['opening_hours'] = $this->getOpeningHours($seller, new \DateTime());
        $seller['flags'] = array_filter(explodeFlags($seller['flags'] ?: ''), fn ($k) => !empty($k), ARRAY_FILTER_USE_KEY);

        return $seller;
    }

    /** Vratí informaci, zda je prodejna už zavřená */
    public function isSellerClosed(array $seller, int $hourReserve = 0, ?\DateTime $date = null): bool
    {
        // pripravim si datum
        $date = $date ?: new \DateTime();

        // zkontroluju, zda je vyplnena hodina zavreni pro konkretni den
        if (!($closeTime = $this->getClosingTime($seller, $date, 1))) {
            return true;
        }

        $closeTimeParts = explode(':', $closeTime);

        $hour = (int) $closeTimeParts[0];
        $minute = (int) ($closeTimeParts[1] ?? 0);

        $closeDate = (clone $date)->setTime($hour - $hourReserve, $minute);

        // zkontroluju, zda neni uz zavreno
        if ($date >= $closeDate) {
            return true;
        }

        return false;
    }

    /**
     * Vrátí nejbližší datum, kdy je prodejna otevřená
     *
     * $returnNullOnNoOpenDate - pokud se mi v celem tydnu nepodari najit zadny datum, tak vrati `null` - znamena to, ze
     * u ty prodejny pravdepodobne neni nastavena oteviraci doba
     */
    public function getClosestOpenDate(array $seller, int $hourReserve = 0, bool $returnNullOnNoOpenDate = false): ?\DateTime
    {
        $loop = 0;
        $date = new \DateTime();

        $found = false;

        do {
            if ($isClosed = $this->isSellerClosed($seller, $hourReserve, $date)) {
                $date->add(new \DateInterval('P1D'));
                $date->setTime(0, 0);
            }

            if (!$isClosed) {
                $found = true;
            }

            $loop++;
        } while ($isClosed && $loop <= 7);

        if ($returnNullOnNoOpenDate && !$found) {
            return null;
        }

        return $date;
    }

    public function loadSellersDeliveryInfoByPurchaseState(PurchaseState $purchaseState, array &$sellers): void
    {
        $productPieces = [];
        $products = [];

        foreach ($purchaseState->getProducts() as $product) {
            $products[$product->getIdProduct()] = $products[$product->getIdProduct()] ?? null;
            if ($product->getIdVariation()) {
                $products[$product->getIdProduct()][] = $product->getIdVariation();
            }

            $key = $product->getIdProduct().($product->getIdVariation() ? '/'.$product->getIdVariation() : '');
            $productPieces[$key] = ($productPieces[$key] ?? 0) + $product->getPieces();
        }

        $this->multiFetch->fetchSellersInStore($sellers, $products);
        $this->loadSellersDeliveryDate($sellers, $productPieces);
    }

    public function loadSellersDeliveryDate(array &$sellers, array $products = []): void
    {
        // pokus o nejaky obecny nacteni datumu doruceni pro prodejny
        foreach ($sellers as &$seller) {
            $deliveryDate = new \DateTime();
            $availability = self::AVAILABILITY_IN_STORE;

            $deliveryDateIncrement = 0;
            foreach ($seller['products'] ?? [] as $key => $item) {
                $pieces = $products[$key] ?? 1;

                // pokud nejaky produkt neni v dostatecnem mnozstvi na prodejne, ale je skladem jinde
                if ($item['in_store'] < $pieces) {
                    $missingPieces = $pieces - $item['in_store'];
                    // pokud je v dostatecnem mnozstvi skladem jinde
                    if (($item['in_store_main'] + $item['in_store_other']) >= $missingPieces) {
                        // mam dostatek kusu skladem, ale nejsou na aktualni prodejne, takze je budu muset zavest
                        $deliveryDateIncrement = 2;
                        $availability = min($availability, self::AVAILABILITY_PARTIALLY_IN_STORE);
                    } else {
                        // nemam dostatek kusu skladem
                        $availability = self::AVAILABILITY_NOT_IN_STORE;
                    }
                }
            }

            // pokud je datum vyzvednuti dnes, ale prodejna za chvili zavira, tak bych mel dat datum az na dalsi den
            if (!$deliveryDateIncrement) {
                if ($this->isSellerClosed($seller, 1)) {
                    $deliveryDate = new \DateTime('tomorrow');
                }
            }

            $seller['deliveryDate'] = $deliveryDate->add(new \DateInterval('P'.$deliveryDateIncrement.'D'));
            $seller['availability'] = $availability;
        }
    }

    /** Default fetches for sellers */
    protected function multiFetchSellersData(array &$sellers): void
    {
        $this->multiFetch->fetchSellersBlocks($sellers);
        $this->multiFetch->fetchSellersPhotos($sellers);
    }

    protected function getBaseQueryBuilder(): QueryBuilder
    {
        return sqlQueryBuilder()
            ->select('se.*, X(se.position) as x, Y(se.position) as y')
            ->from('sellers', 'se')
            ->andWhere(Operator::equals(['se.figure' => 'Y']))
            ->andWhere(Translation::coalesceTranslatedFields(
                translationClass: SellersTranslation::class,
                columns: function ($columns) {
                    $columns['data'] = 'data_translation';

                    return $columns;
                }));
    }

    /**
     * Fetches the specific times (opening hours, closing hours, break start, break end) of a seller for a particular date.
     *
     * @param int $valueIndex represents the type of time interval to retrieve (0 for opening time, 1 for closing time, 2 for break starting time, 3 for break ending time)
     */
    public function getClosingTime(array $seller, \DateTime $date, int $valueIndex): ?string
    {
        $openingHours = $this->getOpeningHours($seller, $date);

        return $openingHours[$date->format('N')][$valueIndex] ?? null;
    }

    /**
     * Gets the opening hours of the seller, evaluates extra opening hours for a particular date.
     */
    public function getOpeningHours(array $seller, \DateTime $date): array
    {
        if (($seller['data']['extra_opening_hours']['toggle'] ?? 'N') === 'N') {
            return $seller['data']['opening_hours'] ?? [];
        }

        $startDateString = empty($seller['data']['extra_opening_hours']['date_from']) ? '01.01.1970 00:00:00' : $seller['data']['extra_opening_hours']['date_from'];
        $endDateString = empty($seller['data']['extra_opening_hours']['date_to']) ? '01.01.2070 00:00:00' : $seller['data']['extra_opening_hours']['date_to'];

        $startDate = new \DateTime($startDateString);
        $endDate = new \DateTime($endDateString);

        if ($date >= $startDate && $date <= $endDate) {
            $openingHours = $seller['data']['extra_opening_hours'];
        } else {
            $openingHours = $seller['data']['opening_hours'];
        }

        return $openingHours ?? [];
    }
}
