<?php

namespace App\Models\Objects;

use App\Services\Filters\FiltersService;
use App\Support\UuidScopeTrait;
use App\Transformers\Objects\OptionTransformer;
use Faker\Generator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;

class Field extends Model {
    use UuidScopeTrait, SoftDeletes;

    protected $dates = [
    ];

    protected $fillable = [
        'uuid',
        'type',
        'name',
        'title',
        'required',
        'multiple',
        'filterable',
        'hidden',
        'params'
    ];

    protected $hidden = [
        'id'
    ];

    protected $casts = [
        'params' => 'array'
    ];


    public function groups(): BelongsToMany {
        return $this->belongsToMany(FieldsGroup::class, 'fields_group_fields', 'field_id', 'group_id');
    }

    public function values(): HasMany {
        return $this->hasMany(FieldType::CLASSES[$this->type]);
    }

    public function objectValues($objectId): HasMany {
        return $this->values()->where(['object_id' => $objectId])->orderBy('ord');
    }

    public function objectsValues(array $ids): HasMany {
        return $this->values()->whereIn('object_id', $ids);
    }

    public function objectTypeValues($objectTypeId): HasMany {
        return $this->values()->whereHas('object', function($query) use($objectTypeId) {
            $query->where(['type_id' => $objectTypeId]);
        });
    }



    public function getOptionsAttribute(): ?Collection {
        if (($this->type === FieldType::RELATION) && !empty($this->params['options']['show'])) {
            if ($model = $this->params['related'] ?? null) {
                $query = $model::query();
                collect($this->params['options']['whereHas'] ?? [])->each(function($val, $prop) use($query) {
                    $query->whereHas($prop, function($query) use($val) {
                        foreach ($val as $attr => $v) {
                            $query->where([$attr => $v]);
                        }
                    });
                });
                if ($where = $this->params['options']['where'] ?? null) $query->where($where);
                return $query->get();
            }
        }
        return null;
    }

    public function getTransformerAttribute() {
        $transformer = $this->params['transformer'] ?? OptionTransformer::class;
        return new $transformer;
    }



    public function getRepresented($filters = [], ?FiltersService $service = null): ?Collection {
        if ($model = $this->params['related'] ?? null) {
            $query = $model::whereHas('relationValues', function($query) use($filters, $service) {
                $query->where(['field_id' => $this->id])->whereHas('object', function($query) use($filters, $service) {
                    self::applyFilters($query, collect($filters)->except($this->name), $service);
                });
            });
            return $query->get();
        }
        return null;
    }

    public function getRange($filters = [], ?FiltersService $service = null): ?array {
        if (in_array($this->type, [FieldType::INTEGER, FieldType::FLOAT, FieldType::DATE, FieldType::DATETIME])) {
            $query = $this->values()->whereHas('object', function($query) use($filters, $service) {
                self::applyFilters($query, collect($filters)->except($this->name), $service);
            });
            return ['min' => $query->min('value') ?? 0, 'max' => $query->max('value') ?? 0];
        }
        return null;
    }

    public function getValue($objectId) {
        return $this->objectValues($objectId)->get()->map(function($value) {
            return $value->get();
        });
    }

    public function setValue($objectId, $value) {
        if ($this->multiple) $this->objectValues($objectId)->delete();
        if (!($value instanceof Collection)) $value = collect((is_array($value) && !Arr::isAssoc($value)) ? $value : [$value]);
        $value->each(function($value, $ord) use($objectId) {
            if ($this->multiple) $this->values()->create(['object_id' => $objectId, 'ord' => $ord])->set($value);
            else $this->values()->firstOrCreate(['object_id' => $objectId])->set($value);
        });
        return $this->getValue($objectId);
    }

    public function addValue($objectId, $value) {
        if ($this->multiple) {
            $max = $this->objectValues($objectId)->max('ord');
            if (is_null($max)) $max = -1;
            if (!($value instanceof Collection)) $value = collect((is_array($value) && !Arr::isAssoc($value)) ? $value : [$value]);
            $value->each(function($value, $ord) use($objectId, $max) {
                $this->values()->create(['object_id' => $objectId, 'ord' => $max + $ord + 1])->set($value);
            });
        }
        return $this->getValue($objectId);
    }


    public static function applyFilters(Builder $query, Collection $filters, ?FiltersService $service = null) {
        if ($types = $filters->get('types')) {
            $types = is_array($types) ? $types : [$types];
            $filters->forget('types');
            $query->whereHas('type', function($query) use($types) {
                $query->whereIn('uuid', $types)->orWhereIn('name', $types);
            });
            $filters->filter(function($value) {return $value;})->each(function($value, $prop) use($query, $types, $service) {
                $field = Field::query()->where(['name' => $prop])->whereHas('groups.objectType', function($query) use($types) {
                    $query->whereIn('uuid', $types)->orWhereIn('name', $types);
                })->first();
                if ($field) $field->applyFilter($query, $value);
            });
            if ($service && $service->objectRelationName) $query->whereHas($service->objectRelationName, function($query) use($service, $filters) {
                if (method_exists($service, 'applyNativeFilters')) $service->applyNativeFilters($query, $filters);
                if (method_exists($service, 'applyPermissionsFilters')) $service->applyPermissionsFilters($query);
            });
        }
    }

    public function applyFilter($query, $value) {
        if ($value = $this->prepareFilterValue($value)) $query->whereHas("{$this->type}Values", function($query) use($value) {
            $query->where(['field_id' => $this->id]);
            $class = FieldType::CLASSES[$this->type];
            $class::applyFilter($query, $value);
        });
    }

    public function prepareFilterValue($value) {
        return is_array($value) ? collect($value)->filter(function($val) {
            return !is_null($val);
        })->all() : $value;
    }


    public function applyOrder(Builder $query, $dir) {
        if ($table = FieldType::TABLES[$this->type] ?? null) {
            $query->leftJoin("{$table} as prop-{$this->name}", function(JoinClause $join) use($table) {
                $join->on('objects.id', '=', "prop-{$this->name}.object_id");
            })->where(["prop-{$this->name}.field_id" => $this->id])->orderBy("prop-{$this->name}.value", $dir);
        }
    }




    public function fakeValue($objectId) {
        $faker = new Generator();
        if ($this->type === FieldType::STRING) $value = $faker->sentence(4);
        elseif ($this->type === FieldType::TEXT) $value = $faker->realText(rand(50, 240));
        elseif ($this->type === FieldType::INTEGER) $value = rand(0, 500);
        elseif ($this->type === FieldType::FLOAT) $value = $faker->randomFloat(1, 0, 100);
        elseif ($this->type === FieldType::BOOLEAN) $value = boolval(rand(0,1));
        elseif ($this->type === FieldType::DATE) $value = $faker->dateTimeBetween('-5 years', '+ 3 years');
        elseif ($this->type === FieldType::DATETIME) $value = $faker->dateTimeBetween('-5 years', '+ 3 years');
        elseif ($this->type === FieldType::RELATION) $value = $this->options->random(($this->multiple) ? rand(1, 3) : 1);
        $this->setValue($objectId, $value ?? null);
    }

}