<?php

/**
 * Form Manager Class.
 *
 * This code is used to manage and validate Forms
 * Consists of following methods:
 * - render: display form called from a hook (if exists and published)
 * - save: executes form, form question and for question option saving
 * - saveQuestion: executes question saving when switching between questions
 * - doFormActions: procedures to do after successful form submit
 * - validateForm: check if submitted data for form are valid
 * - validateEmail: check if valid email
 * - validateNumeric: check if numeric value
 * - validatePaf: check if valid postcode
 * - validateRegEx: check if matches the regex defined
 *
 * @category Forms
 *
 * @author mtc. Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
 */
namespace Mtc\Modules\FormBuilder\Classes;

use CSRF;
use Illuminate\Support\Facades\DB;
use Mtc\Modules\FormBuilder\Classes\FormBmi;
use Twig\Environment;
use Util;

/**
 * Form Manager Class. Handles Form saving, validation and rendering process.
 *
 * @category Forms
 *
 * @author mtc. Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
 */
class FormManager
{
    /**
     * FormManager::render().
     *
     * Render provided form on Twig template.
     * Used for inline calls from CMS / tempaltes
     *
     * @access public
     * @static
     * @param  int $form_id form object
     * @param  Environment $twig
     * @return null
     */
    public static function render($form_id, Environment $twig)
    {
        $form = Form::find($form_id);
        if (
            $form->id
            && $form->published
            && (strtotime($form->display_until) <= 0 || strtotime($form->display_until) > time())
        ) {
            $form = Form::find($form_id);
            echo
                $twig->render(
                    'FormBuilder/render.form.twig',
                    [
                        'form' => $form,
                        'template' => 'empty',
                    ],
                )
            ;
        } elseif (DEV_MODE === true) {
            if ($form) {
                $message =
                    'Form '.(int) $form_id.' - '.clean_page($form->name).' cannot be rendered as it is unpublished';
            } else {
                $message = 'Form '.(int) $form_id.' is not found';
            }
            echo '<div class="error">'.$message.'</div>';
        }
    }

    /**
     * FormManager::save().
     *
     * Save form data
     *
     * @access public
     * @static
     * @param  Form $form form object
     * @param  array $request $_REQUEST data
     *
     * @return array $form_status array of save status
     */
    public static function save(Form $form, $request)
    {
        $new_form = false;
        if (! $form->id) {
            $new_form = true;
        }

        $form->fill($request);
        if (isset($request['published']) && $request['published'] != 'false') {
            $form->published = 1;
        } else {
            $form->published = 0;
        }
        if (isset($request['save_db']) && $request['save_db'] != 'false') {
            $form->save_db = 1;
        } else {
            $form->save_db = 0;
        }
        if (isset($request['send_email']) && $request['send_email'] != 'false') {
            $form->send_email = 1;
        } else {
            $form->send_email = 0;
        }
        if (isset($request['no_limit']) && $request['no_limit'] != 'false') {
            $form->no_limit = 1;
        } else {
            $form->no_limit = 0;
        }
        $form->save();

        // Process fieldset updates
        if (! empty($request['fieldsets'])) {
            if ($new_form) {
                foreach ($request['fieldsets'] as $fieldset) {
                    $id = self::saveFieldset($fieldset, $form->id);
                    $request['questions'] = self::updateQuestionFieldsets($request['questions'], $fieldset['id'], $id);
                }
            } else {
                $old_fieldset_ids = FormFieldset::where('form_id', $form->id)
                    ->pluck('id')
                    ->toArray();
                foreach ($request['fieldsets'] as $fieldset) {
                    if (in_array($fieldset['id'], $old_fieldset_ids)) {
                        unset($old_fieldset_ids[array_search($fieldset['id'], $old_fieldset_ids)]);
                    }
                    $id = self::saveFieldset($fieldset, $form->id);

                    $request['questions'] = self::updateQuestionFieldsets($request['questions'], $fieldset['id'], $id);
                }
                FormFieldset::whereIn('id', $old_fieldset_ids)
                    ->delete();
            }
        }

        // Process question updates
        if (! empty($request['questions'])) {
            if ($new_form) {
                foreach ($request['questions'] as $question) {
                    self::saveQuestion($question, $form->id);
                }
            } else {
                $old_question_ids = FormQuestion::where('form_id', $form->id)
                    ->pluck('id')
                    ->toArray();
                foreach ($request['questions'] as $question) {
                    if (in_array($question['id'], $old_question_ids)) {
                        unset($old_question_ids[array_search($question['id'], $old_question_ids)]);
                    }
                    self::saveQuestion($question, $form->id);
                }
                FormQuestion::whereIn('id', $old_question_ids)
                    ->delete();
            }
        }

        // Re-load form and its relations for ajax request
        $form = Form::where('id', $form->id)
            ->with('questions.options')
            ->with('fieldsets.questions')
            ->first();

        return [
            'status' => 'ok',
            'success' => 'Form saved successfully',
            'form' => $form,
        ];
    }

    /**
     * FormManager::updateQuestionFieldsets().
     *
     * Function parses all questions as array and updates their fieldsets
     * Function is used as to ensure that fieldset passed from site equals
     * to fieldset stored in database (as Vue uses dummy IDs)
     *
     * @access public
     * @static
     * @param  array $questions List of questions coming from $_REQUEST
     * @param  int $from Old fieldset id
     * @param  int $to New fieldset id
     * @return array $questions List of questions with updated fieldset ids
     */
    public static function updateQuestionFieldsets($questions, $from, $to)
    {
        if (is_array($questions)) {
            foreach ($questions as $key => $question) {
                if ($question['fieldset_id'] == $from) {
                    $questions[$key]['fieldset_id'] = $to;
                }
            }
        }

        return $questions;
    }

    /**
     * FormManager::saveQuestion().
     *
     * Save form question and its options
     *
     * @access public
     * @static
     * @param  array $question_data $_REQUEST data
     * @param  int $form_id current form id
     * @return null
     */
    public static function saveQuestion($question_data, $form_id)
    {
        $question = FormQuestion::where('id', $question_data['id'])
            ->where('form_id', $form_id)
            ->first();
        if (! $question) {
            $question = new FormQuestion;
            $question->form_id = $form_id;
        }
        $question->fill($question_data);

        if (isset($question_data['slug'])) {
            $question->slug = Util::remove_accents($question_data['slug']);
        }

        if (isset($question_data['required']) && $question_data['required'] != 'false') {
            $question->required = 1;
        } else {
            $question->required = 0;
        }

        $question->save();

        if (isset($question_data['options']) && count($question_data['options']) > 0) {
            $old_option_ids = FormQuestionOption::where('question_id', $question->id)
                ->pluck('id')
                ->toArray();
            foreach ($question_data['options'] as $option_data) {
                if (in_array($option_data['id'], $old_option_ids)) {
                    unset($old_option_ids[array_search($option_data['id'], $old_option_ids)]);
                }
                $option = FormQuestionOption::where('id', $option_data['id'])
                    ->where('question_id', $question->id)
                    ->first();
                if (! $option) {
                    $option = new FormQuestionOption;
                    $option->question_id = $question->id;
                }
                $option->value = $option_data['value'];
                $option->order = $option_data['order'];
                $option->correct = $option_data['correct'];
                $option->save();
            }
            FormQuestionOption::whereIn('id', $old_option_ids)
                ->delete();
        }
    }

    /**
     * FormManager::saveFieldset().
     *
     * Save form question and its options
     *
     * @access public
     * @static
     * @param  array $fieldset_data $_REQUEST data
     * @param  int $form_id ID of current form
     * @return int Fieldset ID
     */
    public static function saveFieldset($fieldset_data, $form_id)
    {
        $fieldset = FormFieldset::where('id', $fieldset_data['id'])
            ->where('form_id', $form_id)
            ->first();
        if (! $fieldset) {
            $fieldset = new FormFieldset;
            $fieldset->form_id = $form_id;
        }
        $fieldset->fill($fieldset_data);
        if (! empty($fieldset_data['show_title']) && $fieldset_data['show_title'] != 'false') {
            $fieldset->show_title = 1;
        } else {
            $fieldset->show_title = 0;
        }

        $fieldset->save();

        return $fieldset->id;
    }

    /**
     * FormManager::doFormActions().
     *
     * Do actions required upon valid form submission
     *
     * @access public
     * @static
     * @param  array $request $_REQUEST data
     * @param  Environment $twig Twig Environment
     *
     * @return string|null
     */
    public static function doFormActions($request, Environment $twig): ?string
    {
        $form = Form::find($request['form_id']);

        if ($form->save_db) {
            $response = new FormResponse;
            $response->date = date('Y-m-d H:i:s');
            $response->form_id = $request['form_id'];
            $response->ip = $_SERVER['REMOTE_ADDR'];
            $response->save();
            $types_to_process = ['select', 'checkbox', 'radio', 'textarea', 'input'];
            $choice_types = ['select', 'checkbox', 'radio'];
            foreach ($form->questions as $question) {
                if (in_array($question->type, $types_to_process)) {
                    $answer = new FormAnswer;
                    $answer->response_id = $response->id;
                    $answer->question_id = $question->id;
                    if (in_array($question->type, $choice_types)) {
                        if (isset($request['question_'.$question->id])) {
                            $option_id = $request['question_'.$question->id];
                            $opt = FormQuestionOption::find($option_id);
                            $answer->value = $opt->value;
                            $answer->value_id = $request['question_'.$question->id];
                        }
                    } else {
                        $answer->value = $request['question_'.$question->id];
                    }
                    $answer->save();
                }
            }
        }

        if ($form->send_email && filter_var($form->email_receiver, FILTER_VALIDATE_EMAIL)) {
            $subject = 'New '.$form->name.' form submission';
            $email = $twig->render(
                'FormBuilder/email.twig',
                [
                    'form_data' => $request,
                    'form_name' => $form->name,
                    'questions' => $form->questions,
                ],
            );

            $from = '-f '.config('site.from_email');

            $headers = 'MIME-Version: 1.0'."\r\n";
            $headers .= "Content-type: text/html; charset=iso-8859-1\r\n";
            $headers .= 'FROM: "'.config('app.name').'" <'.config('site.from_email').'>'."\r\n";

            mail($form->email_receiver, $subject, $email, $headers, $from);
            mail(DEV_EMAIL, 'DEV COPY: '.$subject, $email, $headers, $from);
        }
        if ($form->callback == 1) {
            $processed_form_data = [];
            foreach ($form->questions as $question) {
                if (! empty($question->slug)) {
                    $processed_form_data[$question->slug] = $request['question_'.$question->id];
                }
            }
            if ($form->callback_class == 'FormBmi' && $form->callback_action == 'calculate') {
                return FormBmi::calculate($processed_form_data);
            } else {
                // fill this with array data
                if (
                    method_exists($form->callback_class, $form->callback_action)
                    && is_callable(array($form->callback_class, $form->callback_action))
                ) {
                    call_user_func(
                        array($form->callback_class, $form->callback_action),
                        $processed_form_data,
                    );
                }
            }
        }

        return null;
    }

    /**
     * FormManager::processSaveRequest()
     *
     * Process Save request from admin edit page
     *
     * @access public
     * @static
     * @param  array $request $_REQUEST data
     * @return array result of save request - conains status and errors
     */
    public static function processSaveRequest($request)
    {
        if (empty($request['id']) && empty($request['name'])) {
            return [
                'status' => 'Failed',
                'errors' => [
                    'name' => 'Form needs to have name defined when saving!',
                ],
            ];
        } else {
            $form = Form::find($request['id']);
            if (! $form) {
                $form = new Form;
            }
            if ($validation_errors = self::validateAdmin($request)) {
                return [
                    'status' => 'Failed',
                    'errors' => $validation_errors,
                ];
            } else {
                return self::save($form, $request);
            }
        }
    }

    /**
     * FormManager::validateAdmin()
     *
     * ValidateSave request from admin edit page
     *
     * @access public
     * @static
     * @param  array $request $_REQUEST data
     * @return array result of validation - list of errros
     */
    public static function validateAdmin($request)
    {
        $errors = [];
        if (empty($request['name'])) {
            $errors['name'] = 'Form Info - Form Name is required';
        }

        if (empty($request['submit_text'])) {
            $errors['info'] = 'Form Info - Submit button text is required';
        }

        if (
            empty($request['save_db'])
            && empty($request['callback'])
            && empty($request['redirect'])
            && empty($request['send_email'])
        ) {
            $errors['settings'] = 'Form Settings - At least one method of data processing is required';
        }

        if (is_array($request['fieldsets'])) {
            foreach ($request['fieldsets'] as $fieldset) {
                if (empty($fieldset['name'])) {
                    $errors['fieldsets'] = 'All fieldsets require a name';
                }
            }
        }
        if (is_array($request['questions'])) {
            foreach ($request['questions'] as $question) {
                if (empty($question['name'])) {
                    $errors['questions'] = 'All form elements require a name';
                }
                if (empty($question['type'])) {
                    $errors['questions'] = 'All form elements require a type';
                }
            }
        }

        return $errors;
    }

    /**
     * FormManager::validateForm()
     *
     * Validate form Postback
     *
     * @access public
     * @static
     * @param  array $request $_REQUEST data
     * @return array $forms list of all site forms
     */
    public static function validateForm($request)
    {
        $errors = [];

        if (! empty($request['form_id'])) {
            $form = Form::find($request['form_id']);
            if (! CSRF::checkToken('do_form_'.$form->id)) {
                $errors['csrf'] = 'Security check failed, please try again';
            }

            foreach ($form->questions as $question) {
                if (
                    $question->required
                    && (
                        ! isset($request['question_'.$question->id])
                        || $request['question_'.$question->id] == ''
                        || $question->type == 'select'
                        && $request['question_'.$question->id] == 0
                    )
                ) {
                    $errors[$question->id] = $question->name.' is required';
                }

                if (! empty($request['question_'.$question->id])) {
                    if ($question->validate == 'email' && ! self::validateEmail($request['question_'.$question->id])) {
                        $errors[$question->id] =
                            $question->name.' must be a valid email address - '.$request['question_'.$question->id];
                    }

                    if ($question->validate == 'numeric' && ! is_numeric($request['question_'.$question->id])) {
                        $errors[$question->id] = $question->name.' must be a numeric value';
                    }

                    if (
                        $question->validate == 'postcode'
                        && ! self::validatePostcode($request['question_'.$question->id])
                    ) {
                        $errors[$question->id] = $question->name.' must be a postcode value';
                    }

                    if (
                        $question->validate == 'regex'
                        && ! self::validateRegEx($request['question_'.$question->id], $question->regex)
                    ) {
                        $errors[$question->id] = $question->name.' is not valid';
                    }
                }
            }
        } else {
            $errors[] = 'Failed saving form data';
        }

        return $errors;
    }

    /**
     * FormManager::validateEmail().
     *
     * Check if value provided is a valid email
     *
     * @access public
     * @static
     * @param  string $eval string to test
     * @return bool $result whether string is a valid email
     */
    public static function validateEmail($eval)
    {
        return filter_var($eval, FILTER_VALIDATE_EMAIL);
    }

    /**
     * FormManager::validatePostcode().
     *
     * Check if value provided is a valid UK postcode
     *
     * @access public
     * @static
     * @param  string $eval string to test
     * @return bool $result whether string is a valid postcode
     */
    public static function validatePostcode($eval): bool
    {
        return DB::connection('paf')
            ->table('postcode_geo')
            ->where('postcode', '=', strtoupper(trim(str_replace(' ', '', $eval))))
            ->count() > 0;
    }

    /**
     * FormManager::validateRegEx().
     *
     * Check if value provided is a valid regular expression
     *
     * @access public
     * @static
     * @param  string $eval string to test
     * @param  string $regex regex string to match against
     * @return bool $result whether string is a valid regular expression
     */
    public static function validateRegEx($eval, $regex)
    {
        return preg_match('/'.$regex.'/', trim($eval));
    }
}
