<?php

namespace App\Traits;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
use Mtc\MercuryDataModels\VehicleAttribute;

trait AppliesQueryConditions
{
    /**
     * Apply query conditions (nested)
     *
     * @param Builder $query
     * @param $conditions
     * @return void
     */
    protected function applyQueryConditions(Builder $query, $conditions)
    {
        $conditions = collect($conditions)
            ->map(fn ($condition) => $condition['value']);

        foreach ($conditions as $condition) {
            if ($this->isAttributeCondition($condition)) {
                $this->handleAttributeCondition($query, $condition);
                continue;
            }
            if ($this->isRelationshipCondition($condition)) {
                $this->handleRelationshipCondition($query, $condition);
                continue;
            }
            if (Str::endsWith($condition['column'], '_since')) {
                $condition['column'] = str_replace('_since', '', $condition['column']);
                $condition['value'] = Carbon::now()->subDays($condition['value']);
                $condition['operator'] = $this->invertComparisonOperator($condition['operator']);
            }

            match ($condition['type']) {
                'and' => $query->where(function ($query) use ($condition) {
                    $query->when(
                        $condition['operator'] === 'in',
                        fn ($query) => $query->whereIn($condition['column'], $condition['value']),
                        fn ($query) => $query->where($condition['column'], $condition['operator'], $condition['value'])
                    )->when(
                        isset($condition['value']) && empty($condition['value']),
                        fn ($query) => $query->orWhereNull($condition['column'])
                    );
                }),
                'or' => $query->orWhere($condition['column'], $condition['operator'], $condition['value']),
                'has' => $query->whereHas($condition['column']),
                'doesnt-have' => $query->whereDoesntHave($condition['column']),
                'null' => $query->whereNull($condition['column']),
                'or-null' => $query->orWhereNull($condition['column']),
                'not-null' => $query->whereNotNull($condition['column']),
                'or-not-null' => $query->orWhereNotNull($condition['column']),
                'where-sub' => $query->where(
                    fn ($query) => $this->applyQueryConditions($query, $conditions['conditions'])
                ),
                'or-where-sub' => $query->orWhere(
                    fn ($query) => $this->applyQueryConditions($query, $conditions['conditions'])
                ),
            };
        }
    }

    /**
     * Check if the condition targets a custom vehicle attribute value.
     * Attribute conditions are prefixed with "attribute." (e.g. attribute.pull-to-at-connect).
     *
     * @param array $condition
     * @return bool
     */
    protected function isAttributeCondition(array $condition): bool
    {
        return str_starts_with((string) $condition['column'], 'attribute.');
    }

    /**
     * Constrain the query to records whose custom attribute value matches the condition.
     *
     * @param Builder $query
     * @param array $condition
     * @return void
     */
    protected function handleAttributeCondition(Builder $query, array $condition): void
    {
        $slug = substr((string) $condition['column'], strlen('attribute.'));
        $column = $this->attributeValueColumn($query, $slug);
        $operator = $condition['operator'];
        $value = $this->castAttributeConditionValue($condition['value'], $column);

        $query->whereHas('attributeValues', function (Builder $related) use ($slug, $column, $operator, $value) {
            $related->where('slug', $slug)
                ->when(
                    $operator === 'in',
                    fn (Builder $related) => $related->whereIn($column, (array) $value),
                    fn (Builder $related) => $related->where($column, $operator, $value)
                );
        });
    }

    /**
     * Resolve the typed value column for the attribute, matching how values are stored on write.
     *
     * @param Builder $query
     * @param string $slug
     * @return string
     */
    protected function attributeValueColumn(Builder $query, string $slug): string
    {
        $type = VehicleAttribute::query()
            ->where('model', $query->getModel()->getMorphClass())
            ->where('slug', $slug)
            ->value('type');

        return match ($type) {
            'text', 'textarea', 'editor', 'select' => 'value_text',
            'number' => 'value_float',
            'integer', 'boolean' => 'value_integer',
            'datetime' => 'value_datetime',
            default => 'value',
        };
    }

    /**
     * Normalise the comparison value so boolean-style input matches the integer-backed storage.
     *
     * @param mixed $value
     * @param string $column
     * @return mixed
     */
    protected function castAttributeConditionValue(mixed $value, string $column): mixed
    {
        if ($column !== 'value_integer' || is_array($value)) {
            return $value;
        }

        if (is_bool($value)) {
            return (int) $value;
        }

        if (is_string($value) && in_array(strtolower(trim($value)), ['yes', 'true', 'on', 'y'], true)) {
            return 1;
        }

        if (is_string($value) && in_array(strtolower(trim($value)), ['no', 'false', 'off', 'n'], true)) {
            return 0;
        }

        return $value;
    }

    /**
     * Check if the condition is for relationship object
     * Related objects are joined via . (dot)
     *
     * @param $condition
     * @return bool
     */
    protected function isRelationshipCondition(array $condition): bool
    {
        return strpos((string) $condition['column'], '.') > 0;
    }

    protected function handleRelationshipCondition(Builder $query, array $condition)
    {
        $split = explode('.', (string) $condition['column']);

        $last = array_pop($split);
        $relationship = implode('.', $split);

        if ($last === 'count') {
            $relationshipName = strtolower((string) preg_replace('/(?<!^)[A-Z]/', '_$0', $relationship)) . '_count';
            $query->withCount($relationship)
                ->having($relationshipName, $condition['operator'], $condition['value']);
        } else {
            if ($condition['operator'] == 'in') {
                $query->whereHas($relationship, fn ($related) => $related->whereIn(
                    $last,
                    $condition['value']
                ));
            } else {
                $query->whereHas($relationship, fn ($related) => $related->where(
                    $last,
                    $condition['operator'],
                    $condition['value']
                ));
            }
        }
    }

    private function invertComparisonOperator(string $operator): string
    {
        return match ($operator) {
            '>=' => '<=',
            '>' => '<',
            '<=' => '>=',
            '<' => '>',
            default => $operator,
        };
    }
}
