<?php

namespace App\Services\BootstrapTable;

use App\Models\Order;
use App\Models\User;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use JetBrains\PhpStorm\ArrayShape;

class BootstrapTable
{
    /**
     * Request Instance
     *
     * @var |Request
     */
    protected $request;
    protected $model;

    /**
     * Filtering Properties
     *
     * @var
     */
    protected $offset;
    protected $limit;
    protected $sortBy;
    protected $orderBy;
    protected $filter;
    protected $searchKey;
    protected $staticSearch;
    protected $staticSearchKey;

    /**
     * Raw Query Properties
     *
     * @var |string
     */
    protected $concatColumns = "concat(first_name, ' ', last_name)";

    /**
     * Relationship Properties
     *
     * @var
     */
    protected $relationships;
    protected $relationshipType;
    protected $normalRelationInstance;
    protected $normalRelationColumn;

    /**
     * Optional Filtering Properties
     *
     * @var
     */
    protected $inputDateFormat;
    protected $dateColumns;
    protected $selectableColumns;

    /**
     * Initiate on every request
     *
     * @param Request $request
     */
    public function __construct(Request $request)
    {
        $this->request = $request;
        $this->offset = $this->request->get('offset');
        $this->limit = $this->request->get('limit');
        $this->sortBy = $this->request->get('sort');
        $this->orderBy = $this->request->get('order');
        $this->filter = $this->request->get('filter');
    }

    /**
     * @param Builder $dataQuery
     * @param bool|null $staticSearch
     * @param array|null $staticSearchKey
     * @param array|null $relationships
     * @param array|null $relationshipType
     * @param array|null $dateColumns
     * @param string|null $inputDateFormat
     * @param string|null $replaceOutputDateFormat
     * @param array|null $selectableColumns
     * @param string|null $groupBy
     * @param string|null $defaultOrderBy
     * @param string|null $defaultOrderByColumnName
     * @return array
     */
    #[ArrayShape(['total' => "int", 'rows' => "array"])] function loadFromServer(
        Builder $dataQuery,
        ?bool   $staticSearch = false,
        ?array  $staticSearchKey = [],
        ?array  $relationships = [],
        ?array  $relationshipType = ['morph', 'morph_user', 'normal'],
        ?array  $dateColumns = [],
        ?string $inputDateFormat = 'd-m-Y',
        ?string $replaceOutputDateFormat = 'd-m-Y',
        ?array  $selectableColumns = [],
        ?string $groupBy = null,
        ?string $defaultOrderBy = 'desc',
        ?string $defaultOrderByColumnName = 'created_at'
    ): array
    {
        $this->dateColumns = $dateColumns;
        $this->inputDateFormat = $inputDateFormat;
        $this->selectableColumns = $selectableColumns;
        $this->relationships = $relationships;
        $this->staticSearch = $staticSearch;
        $this->staticSearchKey = $staticSearchKey;
        $this->model = $dataQuery->getModel();
        if (!empty($this->relationships)) {
            $this->relationshipType = $relationshipType;
        }

        $dataQuery->when($this->filter, function (Builder $query) {
            collect(json_decode($this->filter))->each(function ($item, $key) use ($query) {
                $this->searchKey = $item;

                // If filter value has 'dot (.)' it contains relationship and column name in a single string
                // this code will divide them into an array and assign each element with relevant property
                if (str_contains($key, '.')) {
                    $explode = explode('.', $key);
                    $this->normalRelationInstance = $explode[0];
                    $this->normalRelationColumn = $explode[1];
                }

                // if key matches with $dateColumn property, this condition will filter date in range format
                if (in_array($key, $this->dateColumns)) {
                    // date range must contain a 'dash (-)'
                    // this code will divide them into an array
                    $selectedDates = explode(' - ', $item);

                    // this code will format given date to 'start of the day' and 'end of the day' in 'datetime' format.
                    // i.e. from  |  2022-03-01 00:00:00  |  start of the day
                    // i.e. to    |  2022-03-26 23:59:59  |  end of the day
                    $from = Carbon::createFromFormat($this->inputDateFormat, $selectedDates[0])->startOfDay()->toDateTimeString();
                    $to = Carbon::createFromFormat($this->inputDateFormat, $selectedDates[1])->endOfDay()->toDateTimeString();

                    $query->whereDate($key, '>=', $from)->whereDate($key, '<=', $to);

                    // if key matches with $selectableColumns property, this condition will filter select input fields
                } elseif (in_array($key, $this->selectableColumns)) {
                    $query->where($key, '=', $item);

                    // if key matches with $relationships property, this condition will filter relational data
                    // if key named 'morph_user' matches with $relationshipType property, this condition will filter polymorphic user data
                    // this condition is pretty much depended on project by project you need to customize based on requirement
                } elseif (in_array(!empty($this->normalRelationInstance), $this->relationships) && in_array('morph_user', $this->relationshipType)) {
                    $query->whereHasMorph($this->normalRelationInstance, [User::class, CsrList::class, Order::class], function (Builder $morph, $type) {
                        $column = $type === User::class
                            ? 'name'
                            : DB::raw($this->concatColumns);
                        $morph->where($column, 'like', "%{$this->searchKey}%");
                    });

                    // if key matches with $relationships property, this condition will filter relational data
                    // if key named 'normal' matches with $relationshipType property, this condition will filter relational user data
                    // this condition is pretty much depended on project by project you need to customize based on requirement
                } elseif (in_array(!empty($this->normalRelationInstance), $this->relationships) && in_array('normal', $this->relationshipType)) {
                    $query->whereHas($this->normalRelationInstance, function (Builder $normal) {
                        $normal->where($this->normalRelationColumn, 'like', "{$this->searchKey}%");
                    });
                } elseif ($key === 'first_name') {
                    $query->where(DB::raw($this->concatColumns), 'like', "{$this->searchKey}%");
                } else {
                    if (!in_array($key, $this->staticSearchKey)) {
                        // this search query will run if above condition fails
                        $query->where($key, 'like', "{$item}%");
                    }
                }
            });
        })->when($this->sortBy, function (Builder $query) {
            if (str_contains($this->sortBy, '.')) {
                $explode = explode('.', $this->sortBy);
                $this->normalRelationInstance = $explode[0];
                $this->normalRelationColumn = $explode[1];
            }

            if (in_array(!empty($this->normalRelationInstance), $this->relationships) && in_array('normal', $this->relationshipType)) {
                $query->with([$this->normalRelationInstance => function ($normal) {
                    $normal->orderBy($this->normalRelationColumn, $this->orderBy);
                }]);
            } elseif ($this->sortBy === 'index') {
                $query->orderBy('id', $this->orderBy);
            } // this condition is custom
            else {
                if (!in_array($this->sortBy, $this->staticSearchKey)) {
                    if ($this->sortBy !== 'causer.name') {
                        $query->orderBy($this->sortBy, $this->orderBy);
                    }
                }
            }
        });

        // count total number of records
        $count = $dataQuery->count();

        if (!is_null($groupBy)) {
            $data = $dataQuery->groupBy($groupBy);
            $count = $data->get()->count();
        }

        // based on user input offset, limit and orderBy will work
        $data = $dataQuery->when(!in_array($this->sortBy, $this->staticSearchKey), function ($v) {
            return $v->when($this->offset, function (Builder $query) {
                $query->offset($this->offset);
            })->when($this->limit, function (Builder $query) {
                $query->limit($this->limit);
            });
        })
            ->orderBy($defaultOrderByColumnName, $defaultOrderBy)
            ->get()
            ->toArray();

        // non-database attribute search logic
        if ($this->staticSearch) {
            $data = collect($data)->when($this->orderBy, function ($v) {
                if ($this->orderBy == 'desc') {
                    return $v->sortByDesc($this->sortBy);
                } else {
                    return $v->sortBy($this->sortBy);
                }
            })->when($this->filter, function ($v) {
                return $v->filter(function ($v) {
                    return filterArray($this->searchKey, $v);
                });
            })->skip($this->offset)->take($this->limit)->values()->toArray();
        }

        // pushing an index element with prepared data,
        // to use as serial number if needed
        if ($this->sortBy === 'index' && $this->orderBy === 'desc') {
            $index = $count - $this->offset;
        } else {
            $index = $this->offset + 1;
        }

        foreach ($data as $key => $item) {
            $data[$key]['uuid'] = uniqid();
            if ($this->sortBy === 'index' && $this->orderBy === 'desc') {
                $data[$key]['index'] = $index--;
            } else {
                $data[$key]['index'] = $index++;
            }
            // $data[$key]['id'] = $item['id'];

            // TODO - CONVERT RELATIONSHIP DATA IN 2D ARRAY
            if (in_array('roles', $this->relationships) && in_array('normal', $this->relationshipType)) {
                foreach ($data[$key]['roles'] as $role) {
                    $data[$key]['roles'] = [
                        'id' => $role['id'],
                        'name' => $role['name'],
                    ];
                }
            }

            if (in_array('causer', $this->relationships) && in_array('morph_user', $this->relationshipType)) {
                $data[$key]['causer'] = [
                    'id' => $item['id'],
                    'name' => (!empty($item['causer']['name']) ? $item['causer']['name'] : 'Unknown') ?? (!empty($item['causer']['first_name']) ? $item['causer']['first_name'] : '') . ' ' . (!empty($item['causer']['last_name']) ? $item['causer']['last_name'] : ''),
                ];
            }
        }

        // this will format date output to a given date format
        // default is 'd-m-Y h:i:s a'
        $this->formatDate($data, $replaceOutputDateFormat);

        return [
            'total' => $count,
            'rows' => /*collect(*/ $data, /*)->skip($this->offset)->take($this->limit)->values()->toArray()*/
        ];
    }

    /**
     * This function will set the date output format recursively to each data
     *
     * @param $data
     * @param $replaceOutputDateFormat
     * @return bool
     */
    function formatDate(&$data, $replaceOutputDateFormat)
    {
        return array_walk_recursive($data, 'self::arrayReplacingDateTime', $replaceOutputDateFormat);
    }

    /**
     * This is pre-defined callback function with extra parameter for array_walk_recursive() in formatDate() function
     *
     * @param $item
     * @param $key
     * @param $replaceOutputDateFormat
     * @return mixed|string
     */
    function arrayReplacingDateTime(&$item, $key, $replaceOutputDateFormat)
    {
        if (!is_null($item)) {
            if (in_array($key, $this->dateColumns)) {
                try {
                    $item = Carbon::createFromFormat(config('amplivo.date_configuration.to_date_format'), $item)->format($replaceOutputDateFormat);
                } catch (Exception $exception) {
                    reportLog($exception);
                }
            }
        }
        return $item;
    }
}
