<?php

namespace GeminiLabs\SiteReviews\Modules\Html;

use GeminiLabs\SiteReviews\Arguments;
use GeminiLabs\SiteReviews\Contracts\FieldContract;
use GeminiLabs\SiteReviews\Contracts\FormContract;
use GeminiLabs\SiteReviews\Contracts\PluginContract;
use GeminiLabs\SiteReviews\Helpers\Arr;
use GeminiLabs\SiteReviews\Helpers\Cast;
use GeminiLabs\SiteReviews\Helpers\Str;
use GeminiLabs\SiteReviews\Modules\Captcha;
use GeminiLabs\SiteReviews\Modules\Encryption;
use GeminiLabs\SiteReviews\Modules\Honeypot;
use GeminiLabs\SiteReviews\Modules\Sanitizer;
use GeminiLabs\SiteReviews\Modules\Style;

class Form extends \ArrayObject implements FormContract
{
    public Arguments $args;
    public array $config;
    protected Arguments $session;

    /**
     * The $args are expected to be sanitized before being passed to the form.
     */
    public function __construct(array $args = [], array $values = [], array $config = [])
    {
        if (empty($args['id'])) {
            $args['id'] = glsr(Sanitizer::class)->sanitizeIdUnique('');
        }
        $args = wp_parse_args($args, [
            'button_text' => __('Submit Form', 'site-reviews'),
            'button_text_loading' => __('Submitting, please wait...', 'site-reviews'),
        ]);
        $this->args = glsr()->args($args);
        $this->config = $this->mergeConfig($config);
        $this->loadSession($values);
        parent::__construct($this->fieldsAll(), \ArrayObject::STD_PROP_LIST | \ArrayObject::ARRAY_AS_PROPS);
        array_map([$this, 'normalizeConditions'], $this->fields());
        $this->app()->action("{$this->formName()}/form", $this);
        $this->signForm();
    }

    public function app(): PluginContract
    {
        return glsr();
    }

    public function args(): Arguments
    {
        return $this->args;
    }

    public function build(): string
    {
        return glsr(Template::class)->build('templates/reviews-form', [
            'args' => $this->args,
            'context' => [
                'class' => $this->classAttrForm(),
                'fields' => $this->buildFields(),
                'response' => $this->buildResponse(),
                'submit_button' => $this->buildSubmitButton(),
            ],
            'form' => $this,
        ]);
    }

    public function config(): array
    {
        return [];
    }

    public function configHidden(): array
    {
        return [];
    }

    public function field(string $name, array $args): FieldContract
    {
        $className = $this->fieldClass();
        $field = new $className(wp_parse_args($args, compact('name')));
        $this->normalizeField($field);
        return $field;
    }

    public function fieldClass(): string
    {
        return Field::class;
    }

    /**
     * @return FieldContract[]
     */
    public function fields(): array
    {
        return $this->getArrayCopy();
    }

    /**
     * @return FieldContract[]
     */
    public function fieldsFor(string $group): array
    {
        return array_filter($this->fields(), fn ($field) => $group === $field->group);
    }

    public function formName(): string
    {
        $formName = (new \ReflectionClass($this))->getShortName();
        $formName = Str::dashCase($formName);
        return $formName;
    }

    /**
     * @return FieldContract[]
     */
    public function hidden(): array
    {
        $fields = [];
        foreach ($this->fields() as $field) {
            if ('hidden' === $field->original_type) {
                $fields[] = $field;
            }
        }
        usort($fields, fn ($a, $b) => $a->original_name <=> $b->original_name);
        return $fields;
    }

    public function loadSession(array $values): void
    {
        $this->session = glsr()->args([
            'errors' => [],
            'failed' => false,
            'message' => '',
            'values' => $values,
        ]);
    }

    #[\ReturnTypeWillChange]
    public function offsetGet($key)
    {
        $iterator = $this->getIterator();
        if (is_numeric($key)) {
            return $iterator[$key] ?? null;
        }
        foreach ($iterator as $field) {
            if ($key === $field->original_name) {
                return $field;
            }
        }
        return null;
    }

    public function session(): Arguments
    {
        return $this->session;
    }

    /**
     * @return FieldContract[]
     */
    public function visible(): array
    {
        $fields = [];
        foreach ($this->fields() as $field) {
            if ('hidden' !== $field->original_type) {
                $fields[] = $field;
            }
        }
        return $fields;
    }

    /**
     * An array of field names that can be overridden in the hiddenConfig array
     * by fields with the same name in the config array.
     * 
     * @return string[]
     */
    protected function allowedHiddenFieldOverrides(): array
    {
        return [];
    }

    protected function buildFields(): string
    {
        $fields = [];
        foreach ($this->hidden() as $field) {
            $fields[] = $field->build();
        }
        $fields[] = glsr(Honeypot::class)->build($this->args->id);
        foreach ($this->visible() as $field) {
            $fields[] = $field->build();
        }
        $rendered = implode("\n", $fields);
        $rendered = $this->app()->filterString("{$this->formName()}/build/fields", $rendered, $this);
        return $rendered;
    }

    protected function buildResponse(): string
    {
        $rendered = glsr(Template::class)->build('templates/form/response', [
            'context' => [
                'class' => $this->classAttrResponse(),
                'message' => wpautop($this->session->message),
            ],
            'has_errors' => !empty($this->session->errors),
        ]);
        $rendered = $this->app()->filterString("{$this->formName()}/build/response", $rendered, $this);
        return $rendered;
    }

    protected function buildSubmitButton(): string
    {
        $captcha = glsr(Captcha::class)->container();
        $rendered = glsr(Template::class)->build('templates/form/submit-button', [
            'context' => [
                'class' => $this->classAttrSubmitButton(),
                'loading_text' => $this->args->button_text_loading,
                'text' => $this->args->button_text,
            ],
        ]);
        $rendered = $this->app()->filterString("{$this->formName()}/build/submit_button", $rendered, $this);
        if ('above' === glsr(Captcha::class)->position()) {
            $rendered = $captcha.$rendered;
        } else {
            $rendered = $rendered.$captcha;
        }
        return $rendered;
    }

    protected function classAttrForm(): string
    {
        $classes = [
            $this->args->class,
            glsr(Style::class)->classes('form'),
        ];
        if (!empty($this->session->errors)) {
            $classes[] = glsr(Style::class)->validation('form_error');
        }
        if (glsr_get_option('settings.forms.session_storage', false, 'bool')) {
            $classes[] = 'glsr-persist-data';
        }
        $classes = implode(' ', $classes);
        $classes = glsr(Sanitizer::class)->sanitizeAttrClass($classes);
        return $classes;
    }

    protected function classAttrResponse(): string
    {
        $classes = [
            glsr(Style::class)->validation('form_message'),
        ];
        if (!empty($this->session->errors)) {
            $classes[] = glsr(Style::class)->validation('form_message_failed');
        }
        $classes = implode(' ', $classes);
        $classes = glsr(Sanitizer::class)->sanitizeAttrClass($classes);
        return $classes;
    }

    protected function classAttrSubmitButton(): string
    {
        $classes = glsr(Style::class)->classes('button');
        $classes = glsr(Sanitizer::class)->sanitizeAttrClass($classes);
        return $classes;
    }

    /**
     * @return FieldContract[]
     */
    protected function fieldsAll(): array
    {
        $fields = array_values(array_merge($this->fieldsHidden(), $this->fieldsVisible()));
        $fields = $this->app()->filterArray("{$this->formName()}/fields/all", $fields, $this);
        return $fields;
    }

    /**
     * @return FieldContract[]
     */
    protected function fieldsHidden(): array
    {
        $fields = [];
        foreach ($this->config as $name => $args) {
            if ('hidden' !== $args['type']) {
                continue;
            }
            $field = $this->field($name, $args);
            if ($field->isValid()) {
                $fields[$name] = $field;
            }
        }
        $fields = $this->app()->filterArray("{$this->formName()}/fields/hidden", $fields, $this);
        return $fields;
    }

    /**
     * @return FieldContract[]
     */
    protected function fieldsVisible(): array
    {
        $fields = [];
        foreach ($this->config as $name => $args) {
            if ('hidden' === $args['type']) {
                continue;
            }
            $field = $this->field($name, $args);
            if ($field->isValid()) {
                $fields[$name] = $field;
            }
        }
        $fields = $this->app()->filterArray("{$this->formName()}/fields/visible", $fields, $this);
        return $fields;
    }

    protected function mergeConfig(array $config): array
    {
        $config = array_merge($config, $this->config());
        $config = $this->app()->filterArray("{$this->formName()}/config", $config, $this);
        if (!wp_is_numeric_array($config)) { // allow custom filtered field order
            $order = array_keys($config);
            $order = $this->app()->filterArray("{$this->formName()}/fields/order", $order, $this);
            $ordered = array_intersect_key(array_merge(array_flip($order), $config), $config);
            $config = $ordered;
        }
        array_walk($config, function (&$field, $key) {
            if (!is_numeric($key)) {
                // ensure all fields include a name value to allow the field check below.
                $field['name'] = $key;
            }
        });
        foreach ($this->configHidden() as $name => $value) {
            if (in_array($name, $this->allowedHiddenFieldOverrides())) {
                $search = Arr::searchByKey($name, $config, 'name');
                if (false !== $search) {
                    continue; // skip this hidden field
                }
            }
            $config[$name] = [
                'name' => $name,
                'type' => 'hidden',
                'value' => $value,
            ];
        }
        $config = array_filter($config, fn ($args) => !empty($args['type']));
        return $config;
    }

    /**
     * @todo Should this be done manually? This shouldn't be run when getting field rules for validation
     */
    protected function normalizeConditions(FieldContract $field): void
    {
        if (!$conditions = $field->conditions()) {
            return;
        }
        $results = [];
        foreach ($conditions['conditions'] as $args) {
            if (!$triggerField = $this->offsetGet($args['name'])) {
                $results[] = true; // ignore if the condition's field does not exist
                continue;
            }
            $results[] = (new FieldCondition($args, $triggerField))->isValid();
        }
        $results = array_filter($results);
        if (count($results) === count($conditions['conditions'])) {
            return; // all conditions valid
        }
        if ('any' === $conditions['criteria'] && !empty($results)) {
            return; // some conditions valid
        }
        $field->is_hidden = true;
        // reset value
    }

    /**
     * Normalize the field with the form's session data.
     * Any normalization that is not specific to the form or session data
     * should be done in the field itself.
     */
    protected function normalizeField(FieldContract $field): void
    {
        $this->normalizeFieldChecked($field);
        $this->normalizeFieldErrors($field);
        $this->normalizeFieldId($field);
        $this->normalizeFieldValue($field);
    }

    /**
     * Set the checked attribute of the field from the session.
     */
    protected function normalizeFieldChecked(FieldContract $field): void
    {
        if (!$field->isChoiceField()) {
            return;
        }
        if (!is_scalar($field->value)) {
            return;
        }
        $value = Cast::toString($this->session->values[$field->original_name] ?? '');
        if (empty($value)) {
            return;
        }
        $field->checked = (string) $field->value === $value;
    }

    /**
     * Set the field errors from the session.
     */
    protected function normalizeFieldErrors(FieldContract $field): void
    {
        $errors = $this->session->errors[$field->original_name] ?? [];
        $field->errors = Arr::consolidate($errors);
    }

    /**
     * Prefix the field id with the form id.
     */
    protected function normalizeFieldId(FieldContract $field): void
    {
        if (empty($this->args->id)) {
            return;
        }
        if (empty($field->id)) {
            return;
        }
        if ($field->is_raw) {
            return;
        }
        $fieldId = Str::removePrefix($field->id, $field->namePrefix());
        $fieldId = Str::prefix($fieldId, $this->args->id);
        $field->id = $fieldId;
    }

    /**
     * Set the value attribute of the field from the session.
     */
    protected function normalizeFieldValue(FieldContract $field): void
    {
        if ($field->isChoiceField()) {
            $value = Cast::toArray($this->session->values[$field->original_name] ?? []);
        } else {
            $value = Cast::toString($this->session->values[$field->original_name] ?? '');
        }
        if (empty($value)) {
            return;
        }
        $field->value = $value;
    }

    protected function signForm(): void
    {
        $hidden = $this->hidden();
        $values = [];
        foreach ($hidden as $field) {
            $values[$field->original_name] = $field->value;
        }
        $values = $this->app()->filterArray("{$this->formName()}/signature/values", $values, $this);
        $signatureField = $this->field('form_signature', [
            'type' => 'hidden',
            'value' => glsr(Encryption::class)->encrypt(maybe_serialize($values)),
        ]);
        $fields = array_values(array_merge($hidden, [$signatureField], $this->visible()));
        $this->exchangeArray($fields);
    }
}
