<?php
/**
 * BookiFlex Core
 *
 * @package    BookiFlex
 * @subpackage Core
 * @author     BookiFlex
 * @copyright  Copyright (c) 2025 BookiFlex
 * @license    GPL-2.0-or-later
 *
 * @link       https://bookiflex.com
 */

declare(strict_types=1);

namespace BookiFlex\Core\Model\DateUnit\Pricing;

use BookiFlex\Core\Model\DateUnit\DateUnitTypeInterface;
use BookiFlex\Core\Model\DateUnit\InheritancePriceRulesInterface;
use BookiFlex\Core\ValueObject\MoneyAmount;
use JsonSerializable;

defined( 'ABSPATH' ) || exit;

/**
 * Price collection for DateUnit with inheritance support
 * Simplified version without separate PriceItem class
 */
final class DateUnitPrice implements JsonSerializable
{
    /**
     * Prices stored as nested array
     * [placementType][variant] => MoneyAmount|null
     *
     * Example:
     * [
     *     'adultOnMainPlace' => [
     *         '__1' => MoneyAmount(10000),
     *         '__2' => MoneyAmount(15000)
     *     ],
     *     'childOnExtraPlace' => [
     *         '5x10' => MoneyAmount(3000)
     *     ]
     * ]
     *
     * @var array<string, array<string, MoneyAmount|null>>
     */
    private array $prices = [];

    private ?self $parent = null;
    private bool $inherited = false;
    private bool $overridden = false;
    private array $modifiers = [];

    /**
     * Constructor accepts data from DB or empty array
     *
     * @param array $data Format: [placementType => [variant => amount]]
     */
    public function __construct(array $data = [])
    {
        if (!empty($data)) {
            $this->hydrateFromArray($data);
        }
    }

    /**
     * Create from DB data (after json_decode)
     *
     * @param array|null $data
     * @return self|null
     */
    public static function fromArray(?array $data): ?self
    {
        if ($data === null || empty($data)) {
            return null;
        }

        return new self($data);
    }

    /**
     * Create from placement rules and price data
     * Used when updating prices via UpdateDateUnits
     *
     * @param array $placementRules Placement rules from AccommodationType
     * @param array $prices New prices
     */
    public static function fromPlacementRules(array $placementRules, array $prices): self
    {
        $instance = new self();

        foreach ($placementRules as $placementType => $variants) {
            if (!isset($prices[$placementType])) {
                continue;
            }

            foreach ($variants as $variant => $allowed) {
                if (!$allowed) {
                    continue;
                }

                if (isset($prices[$placementType][$variant])) {
                    $rawAmount = $prices[$placementType][$variant];
                    $instance->setPrice(
                        $placementType,
                        $variant,
                        new MoneyAmount((int)$rawAmount)
                    );
                }
            }
        }

        return $instance;
    }

    /**
     * Restore from DB data array
     */
    private function hydrateFromArray(array $data): void
    {
        foreach ($data as $placementType => $variants) {
            if (!is_array($variants)) {
                continue;
            }

            $this->prices[$placementType] = [];

            foreach ($variants as $variant => $amount) {
                $this->prices[$placementType][$variant] =
                    $amount !== null ? new MoneyAmount((int)$amount) : null;
            }
        }
    }

    /**
     * Set price for specific type and variant
     */
    private function setPrice(string $placementType, string $variant, ?MoneyAmount $amount): void
    {
        if (!isset($this->prices[$placementType])) {
            $this->prices[$placementType] = [];
        }

        $this->prices[$placementType][$variant] = $amount;
    }

    /**
     * Update prices considering placement rules
     * Creates new object (immutability)
     *
     * @param array $placementRules Rules from AccommodationType
     * @param array $newPrices New prices
     */
    public function update(array $placementRules, array $newPrices): self
    {
        // Create new instance
        $updated = new self();

        // Copy inheritance state
        $updated->parent = $this->parent;
        $updated->inherited = $this->inherited;
        $updated->overridden = $this->overridden;
        $updated->modifiers = $this->modifiers;

        // Copy existing prices
        foreach ($this->prices as $placementType => $variants) {
            foreach ($variants as $variant => $amount) {
                $updated->setPrice($placementType, $variant, $amount);
            }
        }

        // Update with new prices
        foreach ($placementRules as $placementType => $variants) {
            if (!isset($newPrices[$placementType])) {
                continue;
            }

            foreach ($variants as $variant => $allowed) {
                if (!$allowed) {
                    continue;
                }

                if (array_key_exists($placementType, $newPrices) && array_key_exists($variant, $newPrices[$placementType])) {
                    $rawAmount = $newPrices[$placementType][$variant];
                    $updated->setPrice(
                        $placementType,
                        $variant,
                        $rawAmount !== null ? new MoneyAmount((int)$rawAmount) : null
                    );
                }
            }
        }

        return $updated;
    }

    /**
     * Get price for specific placement type and variant
     * Considering inheritance and modifiers
     *
     * @param string $placementType Placement type (adultOnMainPlace, etc.)
     * @param string $variant Variant (__2, 5x10, etc.)
     */
    public function getAmount(string $placementType, string $variant): ?MoneyAmount
    {
        // Check own prices
        if (isset($this->prices[$placementType][$variant])) {
            $amount = $this->prices[$placementType][$variant];

            // If price and modifiers exist - apply
            if (isset($this->modifiers[$placementType])) {
                $amount = $this->applyModifier($amount, $this->modifiers[$placementType]);
            }

            return $amount;
        }

        // If no own price and not overridden - get from parent
        if ($this->parent !== null && !$this->overridden) {
            return $this->parent->getAmount($placementType, $variant);
        }

        return null;
    }

    /**
     * Get all prices for placement type
     *
     * @param string $placementType
     * @return array<string, MoneyAmount|null>
     */
    public function getAmountsForPlacementType(string $placementType): array
    {
        $result = [];

        // Collect own prices
        if (isset($this->prices[$placementType])) {
            foreach ($this->prices[$placementType] as $variant => $amount) {
                $result[$variant] = $amount;

                // Apply modifiers
                if ($amount !== null && isset($this->modifiers[$placementType])) {
                    $result[$variant] = $this->applyModifier($amount, $this->modifiers[$placementType]);
                }
            }
        }

        // Add from parent what we don't have
        if ($this->parent !== null && !$this->overridden) {
            $parentAmounts = $this->parent->getAmountsForPlacementType($placementType);
            foreach ($parentAmounts as $variant => $amount) {
                if (!isset($result[$variant])) {
                    $result[$variant] = $amount;
                }
            }
        }

        return $result;
    }

    /**
     * Apply modifier to price
     */
    private function applyModifier(MoneyAmount $amount, array $modifier): MoneyAmount
    {
        $type = $modifier['type'] ?? '';
        $adjustment = $modifier['adjustment'] ?? '';
        $value = $modifier['value'] ?? 0;

        switch ($type) {
            case DateUnitTypeInterface::PERCENTAGE_CALCULATOR:
                $isDiscount = $adjustment === DateUnitTypeInterface::DISCOUNT_ADJUSTMENT_TYPE;
                return $amount->applyPercentage((int)$value, $isDiscount);

            case DateUnitTypeInterface::FIXED_CALCULATOR:
                $isDiscount = $adjustment === DateUnitTypeInterface::DISCOUNT_ADJUSTMENT_TYPE;
                return $amount->applyFixed((int)$value, $isDiscount);

            case DateUnitTypeInterface::FORMULA_CALCULATOR:
                // TODO: Implement formula calculation if needed
                return $amount;

            default:
                return $amount;
        }
    }

    /**
     * Set parent price for inheritance
     */
    public function setParent(?self $parent): void
    {
        $this->parent = $parent;
        if ($parent !== null) {
            $this->inherited = true;
        }
    }

    public function getParent(): ?self
    {
        return $this->parent;
    }

    public function isInherited(): bool
    {
        return $this->inherited;
    }

    public function setInherited(bool $value): void
    {
        $this->inherited = $value;
    }

    public function isOverridden(): bool
    {
        return $this->overridden;
    }

    public function setOverridden(bool $value): void
    {
        $this->overridden = $value;
    }

    /**
     * Register price modifiers from InheritancePriceRules
     */
    public function registerModifiers(InheritancePriceRulesInterface $rules): void
    {
        $this->modifiers = $rules->getModifiers();
    }

    /**
     * Check if empty (all prices are null or no prices at all)
     */
    public function isEmpty(): bool
    {
        if (empty($this->prices)) {
            return true;
        }

        foreach ($this->prices as $variants) {
            foreach ($variants as $amount) {
                if ($amount !== null) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * Check if price exists for specific type and variant
     * (without considering inheritance)
     */
    public function hasOwnPrice(string $placementType, string $variant): bool
    {
        return isset($this->prices[$placementType][$variant]);
    }

    /**
     * Clone with preserving all properties
     */
    public function clone(): self
    {
        $cloned = new self();

        // Copy prices (MoneyAmount are immutable, can simply assign)
        foreach ($this->prices as $placementType => $variants) {
            foreach ($variants as $variant => $amount) {
                $cloned->setPrice($placementType, $variant, $amount);
            }
        }

        // Copy state
        $cloned->parent = $this->parent;
        $cloned->inherited = $this->inherited;
        $cloned->overridden = $this->overridden;
        $cloned->modifiers = $this->modifiers;

        return $cloned;
    }

    /**
     * JSON serialization
     * Always returns only own prices for saving to DB
     */
    public function jsonSerialize(): mixed
    {
        return $this->toArray(false);
    }

    /**
     * Convert to array
     *
     * @param bool $withInheritance If true, returns effective prices considering inheritance and modifiers
     *                              If false (default), returns only own prices
     * @return array Price array in format [placementType][variant] => int|null
     */
    public function toArray(bool $withInheritance = false): array
    {
        if (!$withInheritance) {
            // Return only own prices (for saving to DB)
            $result = [];

            foreach ($this->prices as $placementType => $variants) {
                $result[$placementType] = [];

                foreach ($variants as $variant => $amount) {
                    $result[$placementType][$variant] =
                        $amount !== null ? $amount->getAmount() : null;
                }
            }

            return $result;
        }

        // Return effective prices considering inheritance
        $result = [];
        $processedKeys = [];

        // First add own prices with modifiers
        foreach ($this->prices as $placementType => $variants) {
            if (!isset($result[$placementType])) {
                $result[$placementType] = [];
            }

            foreach ($variants as $variant => $amount) {
                $key = $placementType . '_' . $variant;
                $processedKeys[$key] = true;

                if ($amount !== null && isset($this->modifiers[$placementType])) {
                    // Apply modifiers to own prices
                    $modifiedAmount = $this->applyModifier($amount, $this->modifiers[$placementType]);
                    $result[$placementType][$variant] = $modifiedAmount->getAmount();
                } else {
                    $result[$placementType][$variant] =
                        $amount !== null ? $amount->getAmount() : null;
                }
            }
        }

        // Then add from parent what we don't have
        if ($this->parent !== null && !$this->overridden) {
            // Get parent's effective prices (recursively)
            $parentPrices = $this->parent->toArray(true);

            foreach ($parentPrices as $placementType => $variants) {
                if (!isset($result[$placementType])) {
                    $result[$placementType] = [];
                }

                foreach ($variants as $variant => $parentAmount) {
                    $key = $placementType . '_' . $variant;

                    // Add only if we don't have own price for this variant
                    if (!isset($processedKeys[$key])) {
                        if ($parentAmount !== null && isset($this->modifiers[$placementType])) {
                            // Apply our modifiers to parent price
                            $amount = new MoneyAmount((int)$parentAmount);
                            $modifiedAmount = $this->applyModifier($amount, $this->modifiers[$placementType]);
                            $result[$placementType][$variant] = $modifiedAmount->getAmount();
                        } else {
                            $result[$placementType][$variant] = $parentAmount;
                        }
                    }
                }
            }
        }

        return $result;
    }

    /**
     * Get effective prices as array of MoneyAmount objects
     * Convenient for calculations
     *
     * @return array<string, array<string, MoneyAmount|null>>
     */
    public function getEffectivePrices(): array
    {
        $result = [];
        $processedKeys = [];

        // Collect own prices with modifiers
        foreach ($this->prices as $placementType => $variants) {
            if (!isset($result[$placementType])) {
                $result[$placementType] = [];
            }

            foreach ($variants as $variant => $amount) {
                $key = $placementType . '_' . $variant;
                $processedKeys[$key] = true;

                if ($amount !== null && isset($this->modifiers[$placementType])) {
                    $result[$placementType][$variant] = $this->applyModifier($amount, $this->modifiers[$placementType]);
                } else {
                    $result[$placementType][$variant] = $amount;
                }
            }
        }

        // Add from parent
        if ($this->parent !== null && !$this->overridden) {
            $parentPrices = $this->parent->getEffectivePrices();

            foreach ($parentPrices as $placementType => $variants) {
                if (!isset($result[$placementType])) {
                    $result[$placementType] = [];
                }

                foreach ($variants as $variant => $parentAmount) {
                    $key = $placementType . '_' . $variant;

                    if (!isset($processedKeys[$key]) && $parentAmount !== null) {
                        if (isset($this->modifiers[$placementType])) {
                            $result[$placementType][$variant] = $this->applyModifier($parentAmount, $this->modifiers[$placementType]);
                        } else {
                            $result[$placementType][$variant] = $parentAmount;
                        }
                    }
                }
            }
        }

        return $result;
    }

    /**
     * Create string representation for debugging
     */
    public function __toString(): string
    {
        $parts = [];

        foreach ($this->prices as $placementType => $variants) {
            foreach ($variants as $variant => $amount) {
                if ($amount !== null) {
                    $parts[] = "{$placementType}[{$variant}]=" . $amount->format();
                }
            }
        }

        $flags = [];
        if ($this->inherited) $flags[] = 'inherited';
        if ($this->overridden) $flags[] = 'overridden';
        if ($this->parent !== null) $flags[] = 'has_parent';

        $flagsStr = !empty($flags) ? ' (' . implode(', ', $flags) . ')' : '';

        return 'DateUnitPrice: ' . (!empty($parts) ? implode(', ', $parts) : 'empty') . $flagsStr;
    }
}
