<?php

declare(strict_types=1);

namespace CpCreation\VitiCore\Versioning;

use CpCreation\VitiCore\Product\Model\Product;
use CpCreation\VitiCore\Product\Model\ProductPrice;
use CpCreation\VitiCore\Product\Model\ProductPriceGroup;
use CpCreation\VitiCore\Product\Model\ProductType;
use CpCreation\VitiCore\Product\Model\ProductVariant;
use CpCreation\VitiCore\Shop\Model\DeliveryMethod;
use CpCreation\VitiCore\Shop\Model\ShippingPrice;
use CpCreation\VitiCore\Shop\Model\ShippingZone;
use CpCreation\VitiCore\Versioning\Model\BusinessVersion;
use CpCreation\VitiCore\Versioning\Repository\BusinessVersionRepository;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface;

final readonly class EcommerceVersionRecorder
{
    public const SCOPE_PRODUCT_PRICE = 'ecommerce.product_price';
    public const SCOPE_PRODUCT_VARIANT_AVAILABILITY = 'ecommerce.product_variant_availability';
    public const SCOPE_SHIPPING_PRICE = 'ecommerce.shipping_price';

    public function __construct(
        private BusinessVersionManager $businessVersionManager,
        private BusinessVersionRepository $businessVersionRepository,
        private EntityManagerInterface $entityManager,
    ) {
    }

    /**
     * @return array<string, array<string, mixed>>
     */
    public function captureProductPriceSnapshots(Product $product): array
    {
        $snapshots = [];

        foreach ($product->getVariants() as $variant) {
            if (!$variant instanceof ProductVariant) {
                continue;
            }

            foreach ($variant->getPrices() as $price) {
                if (!$price instanceof ProductPrice) {
                    continue;
                }

                $snapshots[$price->getId()->toString()] = $this->snapshotProductPrice($price);
            }
        }

        return $snapshots;
    }

    /**
     * @return array<string, mixed>
     */
    public function captureShippingPriceSnapshot(ShippingPrice $price): array
    {
        return $this->snapshotShippingPrice($price);
    }

    /**
     * @param array<string, array<string, mixed>> $previousSnapshots
     */
    public function recordProductPriceChanges(Product $product, array $previousSnapshots, ?UserInterface $user): int
    {
        $createdVersions = 0;

        foreach ($product->getVariants() as $variant) {
            if (!$variant instanceof ProductVariant) {
                continue;
            }

            foreach ($variant->getPrices() as $price) {
                if (!$price instanceof ProductPrice) {
                    continue;
                }

                $snapshot = $this->snapshotProductPrice($price);
                $previousSnapshot = $previousSnapshots[$price->getId()->toString()] ?? null;

                if ($previousSnapshot === $snapshot) {
                    continue;
                }

                if ($this->recordPublishedVersion($price, $snapshot, self::SCOPE_PRODUCT_PRICE, $user, [
                    'action' => null === $previousSnapshot ? 'create' : 'update',
                    'source' => 'admin.product',
                ])) {
                    ++$createdVersions;
                }
            }
        }

        if ($createdVersions > 0) {
            $this->entityManager->flush();
        }

        return $createdVersions;
    }

    /**
     * @param array<string, mixed>|null $previousSnapshot
     */
    public function recordShippingPriceChange(ShippingPrice $price, ?array $previousSnapshot, ?UserInterface $user): bool
    {
        $snapshot = $this->snapshotShippingPrice($price);

        if ($previousSnapshot === $snapshot) {
            return false;
        }

        $created = $this->recordPublishedVersion($price, $snapshot, self::SCOPE_SHIPPING_PRICE, $user, [
            'action' => null === $previousSnapshot ? 'create' : 'update',
            'source' => 'admin.shipping_price',
        ]);

        if ($created) {
            $this->entityManager->flush();
        }

        return $created;
    }

    public function recordShippingPriceDeletion(ShippingPrice $price, ?UserInterface $user): void
    {
        $this->recordPublishedVersion($price, $this->snapshotShippingPrice($price), self::SCOPE_SHIPPING_PRICE, $user, [
            'action' => 'delete',
            'source' => 'admin.shipping_price',
        ], true);

        $this->entityManager->flush();
    }

    public function scheduleProductPriceChange(
        ProductPrice $price,
        int $scheduledPrice,
        ?int $scheduledPriceDiscount,
        \DateTimeImmutable $validFrom,
        ?UserInterface $user,
    ): bool {
        if (null !== $this->businessVersionRepository->findPendingDraft(ProductPrice::class, $price->getId()->toString(), self::SCOPE_PRODUCT_PRICE)) {
            return false;
        }

        $snapshot = $this->snapshotProductPrice($price);
        $snapshot['price'] = $scheduledPrice;
        $snapshot['priceDiscount'] = $scheduledPriceDiscount;
        $snapshot['finalPrice'] = $this->resolveFinalPriceValues($scheduledPrice, $scheduledPriceDiscount);

        $created = $this->recordDraftVersion($price, $snapshot, self::SCOPE_PRODUCT_PRICE, $user, $validFrom, [
            'action' => 'schedule',
            'source' => 'admin.product',
        ]);

        if ($created) {
            $this->entityManager->flush();
        }

        return $created;
    }

    public function scheduleProductVariantAvailabilityChange(
        ProductVariant $variant,
        ?bool $scheduledUnPublished,
        ?bool $scheduledSoldOut,
        \DateTimeImmutable $validFrom,
        ?UserInterface $user,
    ): bool {
        if (null !== $this->businessVersionRepository->findPendingDraft(ProductVariant::class, $variant->getId()->toString(), self::SCOPE_PRODUCT_VARIANT_AVAILABILITY)) {
            return false;
        }

        $snapshot = $this->snapshotProductVariantAvailability($variant);
        $snapshot['unPublished'] = $scheduledUnPublished ?? $variant->isUnPublished();
        $snapshot['soldOut'] = $scheduledSoldOut ?? $variant->isSoldOut();

        if ($snapshot['unPublished'] === $variant->isUnPublished() && $snapshot['soldOut'] === $variant->isSoldOut()) {
            return false;
        }

        $created = $this->recordDraftVersion($variant, $snapshot, self::SCOPE_PRODUCT_VARIANT_AVAILABILITY, $user, $validFrom, [
            'action' => 'schedule',
            'source' => 'admin.product',
        ]);

        if ($created) {
            $this->entityManager->flush();
        }

        return $created;
    }

    public function publishDueScheduledChanges(\DateTimeImmutable $now, int $limit = 100, bool $dryRun = false): int
    {
        return $this->publishDueScheduledProductPrices($now, $limit, $dryRun)
            + $this->publishDueScheduledProductVariantAvailability($now, $limit, $dryRun);
    }

    public function publishDueScheduledProductPrices(\DateTimeImmutable $now, int $limit = 100, bool $dryRun = false): int
    {
        $publishedVersions = 0;

        foreach ($this->businessVersionRepository->findDueDrafts(self::SCOPE_PRODUCT_PRICE, $now, $limit) as $version) {
            if (ProductPrice::class !== $version->getObjectClass()) {
                continue;
            }

            $price = $this->entityManager->find(ProductPrice::class, $version->getObjectId());
            if (!$price instanceof ProductPrice) {
                continue;
            }

            $snapshot = $version->getSnapshot();
            if (!\is_int($snapshot['price'] ?? null) && !ctype_digit((string) ($snapshot['price'] ?? ''))) {
                continue;
            }

            ++$publishedVersions;

            if ($dryRun) {
                continue;
            }

            $price->setPrice((int) $snapshot['price']);
            $price->setPriceDiscount(null !== ($snapshot['priceDiscount'] ?? null) ? (int) $snapshot['priceDiscount'] : null);

            $metadata = $version->getMetadata();
            $metadata['publishedBy'] = 'viti-core:business-version:publish-due';
            $metadata['publishedAt'] = $now->format(\DateTimeInterface::ATOM);
            $version->setMetadata($metadata);

            $this->businessVersionManager->publish($version);
        }

        if (!$dryRun && $publishedVersions > 0) {
            $this->entityManager->flush();
        }

        return $publishedVersions;
    }

    public function publishDueScheduledProductVariantAvailability(\DateTimeImmutable $now, int $limit = 100, bool $dryRun = false): int
    {
        $publishedVersions = 0;

        foreach ($this->businessVersionRepository->findDueDrafts(self::SCOPE_PRODUCT_VARIANT_AVAILABILITY, $now, $limit) as $version) {
            if (ProductVariant::class !== $version->getObjectClass()) {
                continue;
            }

            $variant = $this->entityManager->find(ProductVariant::class, $version->getObjectId());
            if (!$variant instanceof ProductVariant) {
                continue;
            }

            $snapshot = $version->getSnapshot();
            if (!\is_bool($snapshot['unPublished'] ?? null) || !\is_bool($snapshot['soldOut'] ?? null)) {
                continue;
            }

            ++$publishedVersions;

            if ($dryRun) {
                continue;
            }

            $variant->setUnPublished($snapshot['unPublished']);
            $variant->setSoldOut($snapshot['soldOut']);

            $metadata = $version->getMetadata();
            $metadata['publishedBy'] = 'viti-core:business-version:publish-due';
            $metadata['publishedAt'] = $now->format(\DateTimeInterface::ATOM);
            $version->setMetadata($metadata);

            $this->businessVersionManager->publish($version);
        }

        if (!$dryRun && $publishedVersions > 0) {
            $this->entityManager->flush();
        }

        return $publishedVersions;
    }

    /**
     * @param array<string, mixed> $snapshot
     * @param array<string, mixed> $metadata
     */
    private function recordPublishedVersion(
        object $subject,
        array $snapshot,
        string $scope,
        ?UserInterface $user,
        array $metadata,
        bool $force = false,
    ): bool {
        $objectClass = $subject::class;
        $objectId = (string) $subject->getId();

        if (!$force) {
            $latest = $this->businessVersionRepository->findLatest($objectClass, $objectId, $scope);
            if (null !== $latest && $latest->getSnapshot() === $snapshot) {
                return false;
            }
        }

        $version = $this->businessVersionManager->create(
            subject: $subject,
            snapshot: $snapshot,
            scope: $scope,
            createdBy: $user?->getUserIdentifier(),
            status: BusinessVersion::STATUS_DRAFT,
            validFrom: new \DateTimeImmutable(),
            metadata: $metadata,
        );
        $this->businessVersionManager->publish($version);

        return true;
    }

    /**
     * @param array<string, mixed> $snapshot
     * @param array<string, mixed> $metadata
     */
    private function recordDraftVersion(
        object $subject,
        array $snapshot,
        string $scope,
        ?UserInterface $user,
        \DateTimeImmutable $validFrom,
        array $metadata,
    ): bool {
        $objectClass = $subject::class;
        $objectId = (string) $subject->getId();
        $latest = $this->businessVersionRepository->findLatest($objectClass, $objectId, $scope);

        if (
            null !== $latest
            && BusinessVersion::STATUS_DRAFT === $latest->getStatus()
            && $latest->getValidFrom()?->getTimestamp() === $validFrom->getTimestamp()
            && $latest->getSnapshot() === $snapshot
        ) {
            return false;
        }

        $this->businessVersionManager->create(
            subject: $subject,
            snapshot: $snapshot,
            scope: $scope,
            createdBy: $user?->getUserIdentifier(),
            status: BusinessVersion::STATUS_DRAFT,
            validFrom: $validFrom,
            metadata: $metadata,
        );

        return true;
    }

    /**
     * @return array<string, mixed>
     */
    private function snapshotProductPrice(ProductPrice $price): array
    {
        $variant = $price->getVariant();
        $product = $variant?->getProduct();

        return [
            'price' => $price->getPrice(),
            'priceDiscount' => $price->getPriceDiscount(),
            'finalPrice' => $this->resolveFinalPrice($price),
            'group' => $this->priceGroupReference($price->getGroup()),
            'variant' => $this->variantReference($variant),
            'product' => $this->productReference($product),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function snapshotProductVariantAvailability(ProductVariant $variant): array
    {
        return [
            'unPublished' => $variant->isUnPublished(),
            'soldOut' => $variant->isSoldOut(),
            'variant' => $this->variantReference($variant),
            'product' => $this->productReference($variant->getProduct()),
        ];
    }

    private function resolveFinalPrice(ProductPrice $price): ?int
    {
        return $this->resolveFinalPriceValues($price->getPrice(), $price->getPriceDiscount());
    }

    private function resolveFinalPriceValues(?int $basePrice, ?int $discountPrice): ?int
    {
        if (null === $basePrice) {
            return null;
        }

        if (null !== $discountPrice && $discountPrice > 0 && $discountPrice < $basePrice) {
            return $discountPrice;
        }

        return $basePrice;
    }

    /**
     * @return array<string, mixed>
     */
    private function snapshotShippingPrice(ShippingPrice $price): array
    {
        return [
            'qtyOf' => $price->getQtyOf(),
            'qtyAt' => $price->getQtyAt(),
            'packagePrice' => $price->getPackagePrice(),
            'bottlePrice' => $price->getBottlePrice(),
            'configGlobal' => $price->isConfigGlobal(),
            'privateMargePercent' => $price->getPrivateMargePercent(),
            'privateMarge' => $price->getPrivateMarge(),
            'nameCarrier' => $price->getNameCarrier(),
            'deliveryMethod' => $this->deliveryMethodReference($price->getDeliveryMethod()),
            'zones' => $this->collectionReferences($price->getZones(), fn (ShippingZone $zone): array => $this->shippingZoneReference($zone)),
            'types' => $this->collectionReferences($price->getTypes(), fn (ProductType $type): array => $this->productTypeReference($type)),
            'groups' => $this->collectionReferences($price->getGroups(), fn (ProductPriceGroup $group): array => $this->priceGroupReference($group)),
        ];
    }

    /**
     * @template T of object
     *
     * @param Collection<int, T> $items
     * @param callable(T): array{id: string, label: string} $formatter
     *
     * @return list<array{id: string, label: string}>
     */
    private function collectionReferences(Collection $items, callable $formatter): array
    {
        $references = [];

        foreach ($items as $item) {
            $references[] = $formatter($item);
        }

        usort($references, static fn (array $left, array $right): int => $left['id'] <=> $right['id']);

        return $references;
    }

    /**
     * @return array{id: string, label: string}|null
     */
    private function productReference(?Product $product): ?array
    {
        if (null === $product) {
            return null;
        }

        return [
            'id' => $product->getId()->toString(),
            'label' => $this->translatedLabel($product),
        ];
    }

    /**
     * @return array{id: string, label: string}|null
     */
    private function variantReference(?ProductVariant $variant): ?array
    {
        if (null === $variant) {
            return null;
        }

        return [
            'id' => $variant->getId()->toString(),
            'label' => $this->normalizeLabel($variant->getFullName()),
        ];
    }

    /**
     * @return array{id: string, label: string}|null
     */
    private function priceGroupReference(?ProductPriceGroup $group): ?array
    {
        if (null === $group) {
            return null;
        }

        return [
            'id' => $group->getId()->toString(),
            'label' => $this->normalizeLabel($group->getName()),
        ];
    }

    /**
     * @return array{id: string, label: string}
     */
    private function productTypeReference(ProductType $type): array
    {
        return [
            'id' => $type->getId()->toString(),
            'label' => $this->translatedLabel($type),
        ];
    }

    /**
     * @return array{id: string, label: string}
     */
    private function shippingZoneReference(ShippingZone $zone): array
    {
        return [
            'id' => $zone->getId()->toString(),
            'label' => $this->normalizeLabel($zone->getName()),
        ];
    }

    /**
     * @return array{id: string, label: string}|null
     */
    private function deliveryMethodReference(?DeliveryMethod $deliveryMethod): ?array
    {
        if (null === $deliveryMethod) {
            return null;
        }

        return [
            'id' => $deliveryMethod->getId()->toString(),
            'label' => $this->translatedLabel($deliveryMethod),
        ];
    }

    private function translatedLabel(object $subject): string
    {
        if (is_callable([$subject, 'getName'])) {
            $label = $subject->{'getName'}();

            if (\is_string($label) || null === $label) {
                return $this->normalizeLabel($label);
            }
        }

        return 'Sans nom';
    }

    private function normalizeLabel(?string $label): string
    {
        $label = trim(strip_tags((string) $label));

        return '' !== $label ? $label : 'Sans nom';
    }
}
