<?php

namespace PortalPeople\delivery\components;

class StockAdjustmentCalculator
{
    private const ADJUSTMENT_TYPE_DECREASE = 'decrease';

    private const ADJUSTMENT_TYPE_INCREASE = 'increase';

    public function decreaseStockValue(
        $source,
        $cartlineQty,
        $product = null,
        $haulier = null,
        array $productKeys = ['erpid', 'stock_code', 'sku', 'id'],
        array $haulierKeys = ['haulierid', 'haulier_id', 'id'],
        ?array &$adjustmentLedger = null,
        ?string $adjustmentKey = null
    ): int {
        $currentStock = $this->readNumericValue($source, 'stock_level');

        if (
            ! $this->isAdjustmentEligible(
                self::ADJUSTMENT_TYPE_DECREASE,
                $source,
                $product,
                $haulier,
                $productKeys,
                $haulierKeys,
                $adjustmentLedger,
                $adjustmentKey
            )
        ) {
            return $currentStock;
        }

        $qty = max(0, (int) $cartlineQty);
        if ($qty === 0) {
            return $currentStock;
        }

        return max(0, $currentStock - $qty);
    }

    public function increaseStockValue(
        $source,
        $cartlineQty,
        $product = null,
        $haulier = null,
        array $productKeys = ['erpid', 'stock_code', 'sku', 'id'],
        array $haulierKeys = ['haulierid', 'haulier_id', 'id'],
        ?array &$adjustmentLedger = null,
        ?string $adjustmentKey = null
    ): int {
        $currentStock = $this->readNumericValue($source, 'stock_level');

        if (
            ! $this->isAdjustmentEligible(
                self::ADJUSTMENT_TYPE_INCREASE,
                $source,
                $product,
                $haulier,
                $productKeys,
                $haulierKeys,
                $adjustmentLedger,
                $adjustmentKey
            )
        ) {
            return $currentStock;
        }

        $qty = max(0, (int) $cartlineQty);
        if ($qty === 0) {
            return $currentStock;
        }

        return $currentStock + $qty;
    }

    public function matchesProductAndHaulier(
        $source,
        $candidateProduct,
        $candidateHaulier,
        array $productKeys,
        array $haulierKeys
    ): bool {
        if (
            ! $this->hasOverlap(
                $this->extractCandidateValues($source, $productKeys),
                $this->extractCandidateValues($candidateProduct, $productKeys)
            )
        ) {
            return false;
        }

        if ($candidateHaulier === null || $candidateHaulier === '') {
            return true;
        }

        return $this->hasOverlap(
            $this->extractCandidateValues($source, $haulierKeys),
            $this->extractCandidateValues($candidateHaulier, $haulierKeys)
        );
    }

    public function buildAdjustmentKey(
        string $type,
        $source,
        $product = null,
        $haulier = null,
        array $productKeys = ['erpid', 'stock_code', 'sku', 'id'],
        array $haulierKeys = ['haulierid', 'haulier_id', 'id']
    ): string {
        return hash(
            'sha256',
            implode('|', [
                $type,
                $this->serializeCandidateValues($source, $productKeys),
                $this->serializeCandidateValues($product, $productKeys),
                $this->serializeCandidateValues($source, $haulierKeys),
                $this->serializeCandidateValues($haulier, $haulierKeys),
            ])
        );
    }

    public function hasAppliedAdjustment(?array $adjustmentLedger, string $adjustmentKey): bool
    {
        if ($adjustmentLedger === null || $adjustmentLedger === []) {
            return false;
        }

        return !empty($adjustmentLedger[$adjustmentKey]);
    }

    public function markAdjustmentApplied(array &$adjustmentLedger, string $adjustmentKey): void
    {
        $adjustmentLedger[$adjustmentKey] = true;
    }

    private function isAdjustmentEligible(
        string $type,
        $source,
        $product,
        $haulier,
        array $productKeys,
        array $haulierKeys,
        ?array &$adjustmentLedger,
        ?string $adjustmentKey
    ): bool {
        if (!$this->matchesProductAndHaulier($source, $product, $haulier, $productKeys, $haulierKeys)) {
            return false;
        }

        if ($adjustmentLedger === null) {
            return true;
        }

        $ledgerKey = $adjustmentKey ?: $this->buildAdjustmentKey(
            $type,
            $source,
            $product,
            $haulier,
            $productKeys,
            $haulierKeys
        );

        if ($this->hasAppliedAdjustment($adjustmentLedger, $ledgerKey)) {
            return false;
        }

        $this->markAdjustmentApplied($adjustmentLedger, $ledgerKey);

        return true;
    }

    private function readNumericValue($source, string $attributeName): int
    {
        if (is_object($source) && isset($source->$attributeName)) {
            return max(0, (int) $source->$attributeName);
        }

        if (is_array($source) && array_key_exists($attributeName, $source)) {
            return max(0, (int) $source[$attributeName]);
        }

        return max(0, (int) $source);
    }

    private function hasOverlap(array $sourceValues, array $candidateValues): bool
    {
        if ($sourceValues === []) {
            return true;
        }

        if ($candidateValues === []) {
            return false;
        }

        return (bool) array_intersect($sourceValues, $candidateValues);
    }

    private function extractCandidateValues($candidate, array $attributeNames): array
    {
        $values = [];

        if (is_array($candidate)) {
            foreach ($attributeNames as $attributeName) {
                if (array_key_exists($attributeName, $candidate) && $candidate[$attributeName] !== '') {
                    $values[] = (string) $candidate[$attributeName];
                }
            }

            return $values;
        }

        if (is_object($candidate)) {
            foreach ($attributeNames as $attributeName) {
                if (isset($candidate->$attributeName) && $candidate->$attributeName !== '') {
                    $values[] = (string) $candidate->$attributeName;
                }
            }

            return $values;
        }

        if ($candidate !== null && $candidate !== '') {
            $values[] = (string) $candidate;
        }

        return $values;
    }

    private function serializeCandidateValues($candidate, array $attributeNames): string
    {
        return implode(',', $this->extractCandidateValues($candidate, $attributeNames));
    }
}
