<?php

declare(strict_types=1);

namespace RetailPricing;

use DomainException;
use InvalidArgumentException;

final class Money
{
    public function __construct(public readonly int $kopecks)
    {
        if ($kopecks < 0) {
            throw new InvalidArgumentException('Money value must be non-negative.');
        }
    }

    public static function rub(float $rubles): self
    {
        return new self((int) round($rubles * 100));
    }

    public function multiplyPercent(int $percent): self
    {
        if ($percent < 0) {
            throw new InvalidArgumentException('Percent must be non-negative.');
        }

        return new self((int) ceil($this->kopecks * (100 + $percent) / 100));
    }

    public function format(): string
    {
        return number_format($this->kopecks / 100, 2, '.', ' ') . ' RUB';
    }
}

final class SupplierOffer
{
    public function __construct(
        public readonly int $supplierId,
        public readonly Money $recommendedPrice,
        public readonly int $quantity,
    ) {
        if ($quantity < 0) {
            throw new InvalidArgumentException('Quantity must be non-negative.');
        }
    }
}

final class LargeSupplierOffer
{
    public function __construct(
        public readonly int $supplierId,
        public readonly Money $recommendedPrice,
        public readonly Money $calculatedMinPrice,
        public readonly int $quantity,
    ) {
        if ($quantity < 0) {
            throw new InvalidArgumentException('Quantity must be non-negative.');
        }
    }
}

final class WarehouseLot
{
    public function __construct(
        public readonly Money $purchasePrice,
        public readonly Money $salePrice,
        public readonly int $quantity,
    ) {
        if ($quantity < 0) {
            throw new InvalidArgumentException('Quantity must be non-negative.');
        }
    }
}

final class ProductPricingInput
{
    /**
     * @param SupplierOffer[] $smallSupplierOffers type a
     * @param LargeSupplierOffer[] $largeSupplierOffers types b and c
     * @param WarehouseLot[] $warehouseLots own stock lots
     */
    public function __construct(
        public readonly array $smallSupplierOffers,
        public readonly array $largeSupplierOffers,
        public readonly array $warehouseLots,
        public readonly int $minMarkupPercent,
    ) {
        if ($minMarkupPercent < 0) {
            throw new InvalidArgumentException('Minimal markup must be non-negative.');
        }
    }
}

final class RetailPriceCalculator
{
    public function calculate(ProductPricingInput $input): Money
    {
        $availableOwnLots = array_values(array_filter(
            $input->warehouseLots,
            static fn (WarehouseLot $lot): bool => $lot->quantity > 0,
        ));

        $hasSupplierData = $this->hasAvailableSupplierData($input);

        if (!$hasSupplierData) {
            return $this->validatedWarehouseSalePrice($availableOwnLots, $input->minMarkupPercent);
        }

        if ($availableOwnLots !== []) {
            $minimalOwnPrice = $this->minimalAllowedOwnStockPrice($availableOwnLots, $input->minMarkupPercent);
            $minimalRecommended = $this->minimalRecommendedSupplierPrice($input);

            return $minimalRecommended === null
                ? $minimalOwnPrice
                : new Money(max($minimalRecommended->kopecks, $minimalOwnPrice->kopecks));
        }

        $minimalCalculatedRemote = $this->minimalCalculatedLargeSupplierPrice($input->largeSupplierOffers);
        if ($minimalCalculatedRemote !== null) {
            return $minimalCalculatedRemote;
        }

        throw new DomainException('Cannot calculate retail price: no own stock and no calculated supplier prices.');
    }

    /**
     * @param WarehouseLot[] $availableOwnLots
     */
    private function validatedWarehouseSalePrice(array $availableOwnLots, int $minMarkupPercent): Money
    {
        if ($availableOwnLots === []) {
            throw new DomainException('Cannot use warehouse sale price: product is absent from own stock.');
        }

        $validSalePrices = [];

        foreach ($availableOwnLots as $lot) {
            $minimalAllowed = $lot->purchasePrice->multiplyPercent($minMarkupPercent);
            if ($lot->salePrice->kopecks < $minimalAllowed->kopecks) {
                throw new DomainException('Warehouse sale price is lower than purchase price plus minimal markup.');
            }

            $validSalePrices[] = $lot->salePrice;
        }

        return $this->minMoney($validSalePrices);
    }

    /**
     * @param WarehouseLot[] $availableOwnLots
     */
    private function minimalAllowedOwnStockPrice(array $availableOwnLots, int $minMarkupPercent): Money
    {
        return $this->minMoney(array_map(
            static fn (WarehouseLot $lot): Money => $lot->purchasePrice->multiplyPercent($minMarkupPercent),
            $availableOwnLots,
        ));
    }

    private function minimalRecommendedSupplierPrice(ProductPricingInput $input): ?Money
    {
        $prices = [];

        foreach ($input->smallSupplierOffers as $offer) {
            if ($offer->quantity > 0) {
                $prices[] = $offer->recommendedPrice;
            }
        }

        foreach ($input->largeSupplierOffers as $offer) {
            if ($offer->quantity > 0) {
                $prices[] = $offer->recommendedPrice;
            }
        }

        return $prices === [] ? null : $this->minMoney($prices);
    }

    /**
     * @param LargeSupplierOffer[] $offers
     */
    private function minimalCalculatedLargeSupplierPrice(array $offers): ?Money
    {
        $prices = [];

        foreach ($offers as $offer) {
            if ($offer->quantity > 0) {
                $prices[] = $offer->calculatedMinPrice;
            }
        }

        return $prices === [] ? null : $this->minMoney($prices);
    }

    private function hasAvailableSupplierData(ProductPricingInput $input): bool
    {
        foreach ($input->smallSupplierOffers as $offer) {
            if ($offer->quantity > 0) {
                return true;
            }
        }

        foreach ($input->largeSupplierOffers as $offer) {
            if ($offer->quantity > 0) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param Money[] $prices
     */
    private function minMoney(array $prices): Money
    {
        if ($prices === []) {
            throw new InvalidArgumentException('Cannot get minimal price from an empty list.');
        }

        return array_reduce(
            $prices,
            static fn (?Money $carry, Money $price): Money => $carry === null || $price->kopecks < $carry->kopecks ? $price : $carry,
        );
    }
}

final class SupplierMarkupRange
{
    public function __construct(
        public readonly Money $fromInclusive,
        public readonly ?Money $toInclusive,
        public readonly int $markupPercent,
    ) {
        if ($markupPercent < 0) {
            throw new InvalidArgumentException('Markup must be non-negative.');
        }
    }

    public function matches(Money $purchasePrice): bool
    {
        if ($purchasePrice->kopecks < $this->fromInclusive->kopecks) {
            return false;
        }

        return $this->toInclusive === null || $purchasePrice->kopecks <= $this->toInclusive->kopecks;
    }
}

final class SupplierMarkupPolicy
{
    /**
     * @param SupplierMarkupRange[] $ranges
     */
    public function __construct(
        public readonly int $supplierId,
        public readonly array $ranges,
        public readonly ?int $discountlessMarkupPercent = null,
    ) {
    }

    public function markupFor(Money $purchasePrice, bool $discountless): int
    {
        if ($discountless && $this->discountlessMarkupPercent !== null) {
            return $this->discountlessMarkupPercent;
        }

        foreach ($this->ranges as $range) {
            if ($range->matches($purchasePrice)) {
                return $range->markupPercent;
            }
        }

        return 25;
    }
}

final class SupplierPurchaseOffer
{
    public function __construct(
        public readonly int $supplierId,
        public readonly Money $purchasePrice,
        public readonly int $quantity,
    ) {
        if ($quantity < 0) {
            throw new InvalidArgumentException('Quantity must be non-negative.');
        }
    }
}

final class SupplierCalculatedSalePrice
{
    public function __construct(
        public readonly int $supplierId,
        public readonly Money $purchasePrice,
        public readonly Money $salePrice,
        public readonly int $markupPercent,
        public readonly int $quantity,
    ) {
    }
}

final class SupplierSalePriceCalculator
{
    /**
     * @param SupplierPurchaseOffer[] $purchaseOffers
     * @param array<int, SupplierMarkupPolicy> $policiesBySupplierId
     * @return SupplierCalculatedSalePrice[]
     */
    public function calculate(array $purchaseOffers, array $policiesBySupplierId, bool $discountless): array
    {
        $result = [];

        foreach ($purchaseOffers as $offer) {
            if ($offer->quantity <= 0) {
                continue;
            }

            $policy = $policiesBySupplierId[$offer->supplierId] ?? new SupplierMarkupPolicy($offer->supplierId, []);
            $markupPercent = $policy->markupFor($offer->purchasePrice, $discountless);

            $result[] = new SupplierCalculatedSalePrice(
                supplierId: $offer->supplierId,
                purchasePrice: $offer->purchasePrice,
                salePrice: $offer->purchasePrice->multiplyPercent($markupPercent),
                markupPercent: $markupPercent,
                quantity: $offer->quantity,
            );
        }

        return $result;
    }
}
