<?php

namespace App\Utilities;

use App\BaseModel;
use App\Http\Controllers\BaseViewController;
use Closure;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Fluent;
use Illuminate\Validation\Validator;

/**
 * A Utitlity class for drop-in support of dynamic form
 * fields models etc.
 * Form fields can be generated from saved json/array schema
 *
 * @see \Modules\Ticketsystem\Entities\Ticket
 * @see \Modules\Ticketsystem\Entities\TicketType
 *
 * @author Wali Razzaq
 */
class DynamicFormFields implements Htmlable
{
    protected ?array $fields = null;

    protected ?array $validationAttributes = null;

    protected ?array $rules = null;

    protected ?Validator $validator = null;

    /**
     * A callable which resolves to the schema array
     * with following structure
     *
     * [
     *  'key' => 'used to generate name of the input field',
     *  'name' => 'used a label for display',
     *  'type' => 'text|date|number|checkbox etc.',
     *  'help' => 'e.g help icon', #optional
     *  'required' => 'a boolean to explicitly make field nullable|required',
     *  'rules' => 'array of laravel validation rules', #optional
     *  'select_options' => 'comma separated options if type is `select`', #optional
     * ]
     */
    protected ?Closure $schema = null;

    /**
     * Callback to hook into the generated field
     * and rules to make any runtime changes
     *
     * callback could expect following params in order
     * as Fluent & Collection objects
     *
     * $field # fluent representation of the form field
     * $item  # fluent object of raw schema of the field
     * $rules # collection of rules for the field in context
     */
    protected ?Closure $onFieldGenerated = null;

    /**
     * parameter/columnn name to wrap the
     * input field names
     *
     * e.g. <input name='custom_data[zip]' .../>
     */
    protected string $name = 'custom_data';

    /**
     * Column name to extract the schema array
     * from the schemable model passed
     * to the constructor of this class
     */
    protected string $schemaColumn = 'dynamic_fields';

    /**
     * Attributes that would be added to every
     * field generated by the builder
     */
    protected array $globalAttributes = [];

    /**
     * array|Model to fill the values of the
     * form fields from
     *
     * Model expects a json colunm casted
     * as array with same name as $this->name
     */
    protected array|BaseModel $fieldable = [];

    public function __construct(?BaseModel $schemable = null)
    {
        $this->schemable($schemable);
    }

    public function schemaColumn(string $column): static
    {
        $this->flushCached();
        $this->schemaColumn = $column;

        return $this;
    }

    public function name(string $column): static
    {
        $this->flushCached();
        $this->name = $column;

        return $this;
    }

    public function schemable(?BaseModel $schemable = null): static
    {
        $this->flushCached();
        $this->schema = fn () => data_get($schemable, $this->schemaColumn, []);

        return $this;
    }

    public function fieldable(array|BaseModel $fieldable): static
    {
        $this->flushCached();
        $this->fieldable = $fieldable;

        return $this;
    }

    public function globalAttributes(array $rules = [])
    {
        $this->flushCached();
        $this->globalAttributes = $rules;

        return $this;
    }

    public function schema(array|callable $schema)
    {
        $this->flushCached();
        $this->schema = is_callable($schema) ? $schema : fn () => $schema;

        return $this;
    }

    public function validator(?Validator $validator = null): static
    {
        $this->validator = $validator;
        $this->setCustomAttributesToValidator();

        return $this;
    }

    public function fields(): array
    {
        if (is_null($this->fields)) {
            $this->build();
        }

        return $this->fields ?? [];
    }

    public function rules(): array
    {
        if (is_null($this->rules)) {
            $this->build();
        }

        return $this->rules ?? [];
    }

    public function toHtml()
    {
        if (blank($this->fields())) {
            return '';
        }

        $model = is_array($this->fieldable) ? BaseModel::make([$this->name => $this->fieldable]) : $this->fieldable;
        $fields = BaseViewController::prepare_form_fields($this->fields(), $model);
        $fields = BaseViewController::add_html_string($fields);

        return collect($fields)->pluck('html')->flatten()->map(fn (Htmlable $html) => $html->toHtml())->implode(PHP_EOL);
    }

    protected function build(): static
    {
        // Prefer request('custom_data') for pre-filling, fallback to fieldable/model data
        $requestCustomData = request()->input($this->name, []);
        $data = is_array($this->fieldable) ? $this->fieldable : data_get($this->fieldable, $this->name, []);
        $this->fields = collect(value($this->schema))
            ->mapInto(Fluent::class)
            ->reject(fn ($item) => empty($item->key)) // remove any item having no key
            ->map(function (Fluent $item) use ($data, $requestCustomData) {
                $rules = collect([$item->required ? 'required' : 'nullable'])
                    ->when($item->type == 'checkbox')->push('boolean')
                    ->merge($item->rules ?? []);

                $name = str("$this->name.$item->key");
                // Prefer request value, fallback to model/array value
                $value = array_key_exists($item->key, $requestCustomData)
                    ? $requestCustomData[$item->key]
                    : data_get($data, $item->key);

                // Handle database type by converting to select with database values
                $formType = $item->type;
                $selectOptions = [];

                if ($item->type === 'database') {
                    $formType = 'select';
                    $selectOptions = $this->getDatabaseSelectValues($item);
                }

                $options = Arr::pull($this->globalAttributes, 'options', []);
                $values = [
                    'value' => $selectOptions,
                    'selected' => $value,
                ];

                if ($item->type == 'select') {
                    $selectOptions = str($item->select_options)
                        ->explode(',')
                        ->unshift('') // Add empty option
                        ->mapWithKeys(fn ($o) => [trim($o) => trim($o)]);

                    $values = [
                        'value' => $selectOptions,
                        'selected' => $values['value'],
                    ];
                }

                if ($formType == 'select') {
                    // FIXME: Remove `nms-select2` class once the livewire/select2 issue is fixed
                    $options['class'] = 'nms-select2 '.($options['class'] ?? '');
                }

                $field = fluent([
                    ...$this->globalAttributes,
                    'name' => (string) $name->bracketify(),
                    'form_type' => $formType,
                    'description' => $item->name,
                    'help' => $item->help,
                    'options' => $options,
                    ...$values,
                ]);

                value($this->onFieldGenerated, $field, $item, $rules);

                $this->rules[(string) $name] = $rules->all();
                $this->validationAttributes[(string) $name] = $item->name;

                return $field->toArray();
            })->all();

        $this->setCustomAttributesToValidator();

        return $this;
    }

    /**
     * Get select values from database for database type fields
     */
    protected function getDatabaseSelectValues($item): array
    {
        try {
            $tableName = $item->tablename ?? '';
            $keyValue = $item->key_value ?? '';
            $displayFields = $item->display_fields ?? '';

            if (empty($tableName) || empty($keyValue) || empty($displayFields)) {
                return [];
            }

            // Parse display fields from list format
            $concatParts = $this->parseDisplayFields($displayFields);

            if (empty($concatParts)) {
                return [];
            }

            // Build the SQL query using CONCAT for display fields
            $concatExpression = implode(', ', $concatParts);
            $query = DB::table($tableName)
                ->selectRaw("$keyValue as id, CONCAT($concatExpression) as display");

            // Apply SQL filter if provided
            $sqlFilter = $item->sql_filter ?? '';
            if (! empty($sqlFilter)) {
                $query->whereRaw($sqlFilter);
            }

            $query->orderBy('display');

            $results = $query->get();

            // Convert to key-value array for select
            $selectValues = [];
            foreach ($results as $result) {
                $selectValues[$result->id] = $result->display;
            }

            return $selectValues;
        } catch (\Exception $e) {
            // Log error and return empty array to prevent form breaking
            \Log::error("Error loading database select values for field {$item->key}: ".$e->getMessage());

            return [];
        }
    }

    /**
     * Parse display fields from comma-separated format to SQL CONCAT parts
     */
    protected function parseDisplayFields(string $displayFields): array
    {
        $parts = [];
        $current = '';
        $inQuotes = false;
        $length = strlen($displayFields);

        for ($i = 0; $i < $length; $i++) {
            $char = $displayFields[$i];

            if ($char === "'" && ($i === 0 || $displayFields[$i - 1] !== '\\')) {
                $inQuotes = ! $inQuotes;
                $current .= $char;
            } elseif ($char === ',' && ! $inQuotes) {
                $trimmed = trim($current);
                if (! empty($trimmed)) {
                    $parts[] = $trimmed;
                }
                $current = '';
            } else {
                $current .= $char;
            }
        }

        // Add the last part
        $trimmed = trim($current);
        if (! empty($trimmed)) {
            $parts[] = $trimmed;
        }

        // Process parts to handle quoted literals
        $result = [];
        foreach ($parts as $part) {
            $part = trim($part);
            if (empty($part)) {
                continue;
            }

            // If part is quoted (starts and ends with single quote), treat as literal
            if (preg_match('/^\'([^\']*)\'$/', $part, $matches)) {
                $result[] = "'".addslashes($matches[1])."'";
            } else {
                // Otherwise treat as field name
                $result[] = $part;
            }
        }

        return $result;
    }

    protected function setCustomAttributesToValidator()
    {
        if (is_null($this->validator) || blank($this->validationAttributes)) {
            return;
        }

        $this->validator->addCustomAttributes($this->validationAttributes);
    }

    protected function flushCached()
    {
        $this->fields = $this->rules = null;
    }
}
