<?php

declare(strict_types=1);

namespace App\Filters;

use InvalidArgumentException;
use Openguru\OpenCartFramework\CriteriaBuilder\Criterion;
use Openguru\OpenCartFramework\CriteriaBuilder\Rules\BaseRule;
use Openguru\OpenCartFramework\QueryBuilder\Builder;
use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
use Openguru\OpenCartFramework\QueryBuilder\RawExpression;
use Openguru\OpenCartFramework\QueryBuilder\Table;
use Openguru\OpenCartFramework\Support\Arr;
use RuntimeException;

class ProductPrice extends BaseRule
{
    public const NAME = 'RULE_PRODUCT_PRICE';

    public static function initWithDefaults(): BaseRule
    {
        return new self(static::NAME, [
            'product_price' => new Criterion(static::CRITERIA_OPTION_NUMBER, [
                'operator' => static::CRITERIA_OPERATOR_GREATER_OR_EQUAL,
                'value' => [
                    'from' => 0,
                    'to' => null,
                ],
            ]),
            'include_discounts' => new Criterion(static::CRITERIA_OPTION_BOOLEAN, [
                'value' => true,
            ]),
            'include_specials' => new Criterion(static::CRITERIA_OPTION_BOOLEAN, [
                'value' => true,
            ]),
        ]);
    }

    /**
     * @return void
     */
    public function apply(Builder $builder, $operand)
    {
        $includeSpecials = $this->criteria['include_specials']->params['value'] ?? true;

        /** @var Criterion|null $productPriceCriterion */
        $productPriceCriterion = $this->criteria['product_price'] ?? null;

        if (! $productPriceCriterion) {
            throw new RuntimeException('Invalid product price rule format. Criterion is not found. Check filter JSON.');
        }

        if (! isset(static::$numberCompareOperators[$productPriceCriterion->params['operator']])) {
            throw new InvalidArgumentException('Invalid operator: ' . $productPriceCriterion->params['operator']);
        }

        $column = 'products.price';

        if ($includeSpecials) {
            $specialsFacetHash = md5(serialize($productPriceCriterion) . 'specials');
            $joinAlias = 'product_specials_' . $specialsFacetHash;
            if ($builder->hasJoinAlias($joinAlias)) {
                return;
            }

            $customerGroupId = config('oc_customer_group_id', 1);

            $sub2 = $builder->newQuery()
                ->select([
                    'product_id',
                    new RawExpression("MIN(CONCAT(LPAD(priority, 5, '0'), LPAD(price, 10, '0'))) AS sort_key"),
                ])
                ->from(db_table('product_special'), 'ps')
                ->where("ps.customer_group_id", '=', $customerGroupId)
                ->whereRaw(
                    "
                    (ps.date_start = '0000-00-00' OR ps.date_start < NOW())
                    AND (ps.date_end = '0000-00-00' OR ps.date_end > NOW())
                    "
                )
                ->groupBy(['product_id']);

            $sub = $builder->newQuery()
                ->select([
                    'ps1.product_id',
                    'ps1.price',
                ])
                ->from(db_table('product_special'), 'ps1')
                ->join(new Table($sub2, 'ps2'), function (JoinClause $join) {
                    $join->on('ps1.product_id', '=', 'ps2.product_id')
                        ->whereRaw("CONCAT(LPAD(ps1.priority, 5, '0'), LPAD(ps1.price, 10, '0')) = ps2.sort_key");
                });

            $builder->join(new Table($sub, $joinAlias), function (JoinClause $join) use ($joinAlias) {
                $join->on('products.product_id', '=', "$joinAlias.product_id");
            }, Builder::JOIN_TYPE_LEFT);

            $column = new RawExpression("COALESCE($joinAlias.price, products.price)");
        }

        $numberOperator = static::$numberCompareOperators[$productPriceCriterion->params['operator']];
        $value = $this->prepareValue(
            $numberOperator,
            $productPriceCriterion->params['value']
        );

        if ($numberOperator === 'BETWEEN') {
            [$min, $max] = $value; // $min = левая, $max = правая граница

            // если обе границы не указаны — фильтр игнорируем
            if ($min === null && $max === null) {
                return;
            }

            // если только правая граница — "меньше или равно"
            if ($min === null && $max !== null) {
                $builder->where($column, '<=', $max, $operand);

                return;
            }

            // если только левая граница — "больше или равно"
            if ($min !== null && $max === null) {
                $builder->where($column, '>=', $min, $operand);

                return;
            }

            // левая и правая граница равны
            if ($min !== null && $max !== null && $min === $max) {
                $builder->where($column, '=', $min, $operand);

                return;
            }

            // если обе границы есть — классический between (min ≤ x ≤ max)
            if ($min !== null && $max !== null) {
                $builder->whereBetween($column, [$min, $max], $operand);
            }
        } else {
            $builder->where($column, $numberOperator, $value[0], $operand);
        }
    }

    private function prepareValue($numberOperator, array $value): array
    {
        $from = null;
        $to = null;

        if (is_numeric($value['from'])) {
            $from = (int) $value['from'];
        }

        if (is_numeric($value['to'])) {
            $to = (int) $value['to'];
        }

        return [$from, $to];
    }
}
