<?php

/**
 *
 *    Copyright (C) 2016-2019 onOffice GmbH
 *
 *    This program is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU Affero General Public License as published by
 *    the Free Software Foundation, either version 3 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

namespace onOffice\WPlugin;

use DI\ContainerBuilder;
use DI\DependencyException;
use DI\NotFoundException;
use Exception;
use onOffice\WPlugin\DataFormConfiguration\DataFormConfiguration;
use onOffice\WPlugin\DataFormConfiguration\UnknownFormException;
use onOffice\WPlugin\Field\Collection\FieldsCollectionConfiguratorForm;
use onOffice\WPlugin\Field\CompoundFieldsFilter;
use onOffice\WPlugin\Field\SearchcriteriaFields;
use onOffice\WPlugin\Field\UnknownFieldException;
use onOffice\WPlugin\Form\CaptchaHandler;
use onOffice\WPlugin\Form\CaptchaEnterpriseHandler;
use onOffice\WPlugin\Form\AltchaHandler;
use onOffice\WPlugin\Form\FormFieldValidator;
use onOffice\WPlugin\Form\FormPostConfiguration;
use onOffice\WPlugin\Types\FieldsCollection;
use onOffice\WPlugin\Types\FieldTypes;
use onOffice\WPlugin\Factory\EstateListFactory;
use onOffice\WPlugin\DataView\DataDetailViewHandler;

/**
 *
 *
 * Terminology used in this class:
 *
 * - prefix: the prefix of the input form. Must be the name of a form
 * - form No.: Every input's name consists of the prefix + form no to make multiple forms on
 *				one page possible.
 *				The Form No must be incremented at every new form output.
 *
 *
 */

abstract class FormPost
{
	/** */
	const MESSAGE_SUCCESS = 'success';

	/** */
	const MESSAGE_REQUIRED_FIELDS_MISSING = 'fieldmissing';

	/** */
	const MESSAGE_ERROR = 'error';

	/** */
	const MESSAGE_RECAPTCHA_SPAM = 'recaptchaSpam';

	/** @var int */
	private static $_formNo = 0;

	/** @var array */
	private $_formDataInstances = [];

	/** @var FormPostConfiguration */
	private $_pFormPostConfiguration = null;

	/** @var int */
	private $_absolutCountResults = 0;

	/** @var SearchcriteriaFields */
	private $_pSearchcriteriaFields = null;

	/** @var FieldsCollection */
	private $_pFieldsCollection = null;

	/** @var CompoundFieldsFilter */
	private $_pCompoundFields = null;

	/** @var FieldsCollectionConfiguratorForm */
	private $_pFieldsCollectionConfiguratorForm;

	/**
	 *
	 * @param FormPostConfiguration $pFormPostConfiguration
	 * @param SearchcriteriaFields $pSearchcriteriaFields
	 * @param FieldsCollectionConfiguratorForm $pFieldsCollectionConfiguratorForm
	 */

	public function __construct(
		FormPostConfiguration $pFormPostConfiguration,
		SearchcriteriaFields $pSearchcriteriaFields,
		FieldsCollectionConfiguratorForm $pFieldsCollectionConfiguratorForm)
	{
		$this->_pFormPostConfiguration = $pFormPostConfiguration;
		$this->_pSearchcriteriaFields = $pSearchcriteriaFields;
		$this->_pFieldsCollectionConfiguratorForm = $pFieldsCollectionConfiguratorForm;
	}

	/**
	 *
	 * @param DataFormConfiguration $pConfig
	 * @param int $formNo
	 * @return void
	 * @throws DependencyException
	 * @throws Field\UnknownFieldException
	 * @throws NotFoundException
	 */

	public function initialCheck(DataFormConfiguration $pConfig, int $formNo)
	{
		$this->updatePostHoneypot();
		$pFormData = $this->buildFormData($pConfig, $formNo);
		$pFormData->setFormSent(true);
		$this->setFormDataInstances($pFormData);

		 // CSRF Protection: Verify nonce
		$nonce = $_POST['onoffice_nonce'] ?? '';
		$formId = $pConfig->getFormName();
		if (!wp_verify_nonce($nonce, 'onoffice_form_' . $formId)) {
			$pFormData->setStatus(self::MESSAGE_ERROR);
			$this->_pFormPostConfiguration->getLogger()->logError(
				new \Exception('CSRF token verification failed for form: ' . $formId)
			);
			return;
		}

		if ($this->_pFormPostConfiguration->getPostMessage() !== "") {
			$pFormData->setStatus(self::MESSAGE_SUCCESS);
			return;
		} elseif ($pFormData->getMissingFields() === [] && !$this->checkCaptcha($pConfig)) {
			$pFormData->setStatus(self::MESSAGE_RECAPTCHA_SPAM);
			return;
		} elseif ($pFormData->getMissingFields() !== []) {
			$pFormData->setStatus(self::MESSAGE_REQUIRED_FIELDS_MISSING);
			return;
		} 

		try {
			$this->analyseFormContentByPrefix($pFormData);
			$pFormData->setStatus(self::MESSAGE_SUCCESS);
		} catch (Exception $pException) {
			$pFormData->setStatus(self::MESSAGE_ERROR);
			$this->_pFormPostConfiguration->getLogger()->logError($pException);
		}
	}

	/**
	 * @return array
	 */
	protected function expandFieldsCollection()
	{
		return [];
	}

	/**
	 * @param DataFormConfiguration $pFormConfig
	 * @throws DependencyException
	 * @throws NotFoundException
	 */
	protected function setFieldsCollection(DataFormConfiguration $pFormConfig)
	{
		$pFieldsCollection = new FieldsCollection();
		$pFieldBuilderShort = $this->_pFormPostConfiguration->getFieldsCollectionBuilderShort();
		$pFieldBuilderShort
			->addFieldsAddressEstate($pFieldsCollection)
			->addFieldsSearchCriteria($pFieldsCollection)
			->addFieldsFormFrontend($pFieldsCollection);

		$this->_pFieldsCollection = $this->_pFieldsCollectionConfiguratorForm
			->buildForFormType($pFieldsCollection, $pFormConfig->getFormType());

		foreach ($this->expandFieldsCollection() as $pField) {
			if (!$this->_pFieldsCollection->containsFieldByModule($pField->getModule(), $pField->getName())) {
				$this->_pFieldsCollection->addField($pField);
			}
		}
	}

	/**
	 *
	 * @param DataFormConfiguration $pConfig
	 * @return bool
	 *
	 */

	private function checkCaptcha(DataFormConfiguration $pConfig): bool
    {
        if (!$pConfig->getCaptcha()) {
            return true;
        }

        $pWPOptionsWrapper = $this->_pFormPostConfiguration->getWPOptionsWrapper();
        $token = $this->_pFormPostConfiguration->getPostvarCaptchaToken();

        // Enterprise reCAPTCHA
        $enterpriseSiteKey = $pWPOptionsWrapper->getOption('onoffice-settings-captcha-enterprise-sitekey', '');
        $enterpriseProjectId = $pWPOptionsWrapper->getOption('onoffice-settings-captcha-enterprise-projectid', '');
        $enterpriseApiKey = $pWPOptionsWrapper->getOption('onoffice-settings-captcha-enterprise-apikey', '');

        if (!empty($enterpriseSiteKey) && !empty($enterpriseProjectId) && !empty($enterpriseApiKey)) {
            $pHandler = new CaptchaEnterpriseHandler($token, $enterpriseProjectId, $enterpriseSiteKey, $enterpriseApiKey);
            return $pHandler->checkCaptcha();
        }

        // TODO: Classic reCAPTCHA - Remove this later, when Enterprise reCAPTCHA is fully rolled out
        $classicSiteKey = $pWPOptionsWrapper->getOption('onoffice-settings-captcha-sitekey', '');
        $classicSecretKey = $pWPOptionsWrapper->getOption('onoffice-settings-captcha-secretkey', '');

        if (!empty($classicSiteKey) && !empty($classicSecretKey)) {
            $pHandler = new CaptchaHandler($token, $classicSecretKey);
            return $pHandler->checkCaptcha();
        }

        // ALTCHA fallback: activated when no reCAPTCHA is configured and a supported theme is active
        if (AltchaHandler::isAltchaActive()) {
            $altchaPayload = $_POST['altcha'] ?? '';
            try {
                $pAltchaHandler = new AltchaHandler($altchaPayload, AltchaHandler::getHmacKey());
                return $pAltchaHandler->checkCaptcha();
            } catch (\Exception $e) {
                return false;
            }
        }

        return true;
    }

	/**
	 *
	 * @param DataFormConfiguration $pFormConfig
	 * @param int $formNo
	 * @return FormData
	 * @throws Field\UnknownFieldException
	 * @throws DependencyException
	 * @throws NotFoundException
	 */

	private function buildFormData(DataFormConfiguration $pFormConfig, $formNo): FormData
	{
		$pFormFieldValidator = new FormFieldValidator(new RequestVariablesSanitizer, $this->_pSearchcriteriaFields);

		$this->setFieldsCollection($pFormConfig);
		$this->_pCompoundFields = $this->_pFormPostConfiguration->getCompoundFields();
		$requiredFields = $this->_pCompoundFields->mergeFields($this->_pFieldsCollection, $pFormConfig->getRequiredFields());
		$inputs = $this->_pCompoundFields->mergeAssocFields($this->_pFieldsCollection, $pFormConfig->getInputs());
		$pFormConfig->setInputs($inputs);
		if (!empty($pFormConfig->getHiddenFields())) {
			$this->updateValueForHiddenFields($pFormConfig->getHiddenFields());
		}

		$formFields = $this->getAllowedPostVars($pFormConfig);
		$formData = $pFormFieldValidator->getValidatedValues($formFields, $this->_pFieldsCollection);
		$requiredFields = $this->getAllowedRequiredFields($requiredFields);
		$requiredFieldsAreAllowed = $this->getAllowedRequiredFieldsIsRangeField($requiredFields);
		$pFormData = new FormData($pFormConfig, $formNo);
		$pFormData->setRequiredFields($requiredFieldsAreAllowed);
		$pFormData->setFormtype($pFormConfig->getFormType());
		$pFormData->setValues($formData);

		return $pFormData;
	}

	/**
	 * @param array $hiddenFields
	 *
	 * @return void
	 * @throws DependencyException
	 * @throws NotFoundException
	 * @throws UnknownFieldException
	 */

	private function updateValueForHiddenFields(array $hiddenFields)
	{
		$fields = [];
		foreach ($hiddenFields as $field) {
			$fields[] = $this->_pFieldsCollection->getFieldByKeyUnsafe($field);
		}

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Called within form processing pipeline after nonce/reCAPTCHA verification in initialCheck().
        foreach ($fields as $field) {
            if ($field->getType() === FieldTypes::FIELD_TYPE_MULTISELECT && isset($_POST[$field->getName()]) && !empty($_POST[$field->getName()])
                && is_string($_POST[$field->getName()])) {
                $fieldName = $field->getName();
                // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Already validated with isset() and is_string() checks above, sanitized with array_map() and wp_unslash() below.
                $rawValue = wp_unslash($_POST[$fieldName]);
                $_POST[$fieldName] = array_map('sanitize_text_field', explode(', ', $rawValue));
            }
        }
        // phpcs:enable WordPress.Security.NonceVerification.Missing
	}

	/**
	 * @return FieldsCollection
	 */
	protected function getFieldsCollection(): FieldsCollection
	{
		return $this->_pFieldsCollection;
	}

	/**
	 *
	 * @param string $name
	 * @return string
	 */

	private function getModule(string $name): string
	{
		try {
			return $this->_pFieldsCollection->getFieldByKeyUnsafe($name)->getModule();
		} catch (UnknownFieldException $pEx) {
			return '';
		}
	}
 
	/**
	 *
	 * @param $requiredFields
	 * @return string[]
	 *
	 */

	protected function getAllowedRequiredFields($requiredFields): array
	{
		$activeRequiredFields = [];

		foreach ($requiredFields as $name) {
			$module = $this->getModule($name);
			if ($this->_pFieldsCollection->containsFieldByModule($module, $name)) {
				$activeRequiredFields[] = $name;
			}
		}
		return $activeRequiredFields;
	}
 
	/**
	 *
	 * @param array $requiredFields
	 * @return string[]
	 *
	 */

	private function getAllowedRequiredFieldsIsRangeField(array $requiredFields): array
	{
		foreach ($requiredFields as $key => $value) {
			$isRangeField = $this->_pFieldsCollection->getFieldByKeyUnsafe($value)->getIsRangeField();
			if ($isRangeField) {
				array_push($requiredFields, $value.'__von');
				array_push($requiredFields, $value.'__bis');
				unset($requiredFields[$key]);
			}
		}

		return $requiredFields;
	}

	/**
	 * @param string $subject
	 * @param array $values
	 * @param int|null $estateId
	 * @param array $inputs
	 * @return string
	*/
	public function generateCustomEmailSubject(string $subject, array $values, int $estateId = null, array $inputs): string
	{
		preg_match_all('/%%([^%]+)%%/', $subject, $matches);
		$fields = $matches[1];

		foreach ($fields as $field) {
			$replacement = $values[$field] ?? '';
			if ($field === 'estateid' && $estateId !== null) {
				$replacement = $estateId;
			} else if($field === 'immonr' && $estateId !== null) {
				$estateListFactory = new EstateListFactory(new DataDetailViewHandler());
				$pEstateDetail = $estateListFactory->createEstateDetail($estateId);
				$pEstateDetail->loadEstates();
				$replacement = $pEstateDetail->estateIterator()['objektnr_extern'] ?? '';
			} else if (array_key_exists($field, $inputs)) {
				$fieldObject = $this->_pFieldsCollection->getFieldByKeyUnsafe($field);
				if ($fieldObject->getIsRangeField()) {
					$replacement = trim(($values[$field . '__von'] ?? '') . ' - ' . ($values[$field . '__bis'] ?? ''), ' - ');
				} elseif (is_array($replacement)) {
					$replacement = implode(', ', $replacement);
				}
			}

			$subject = str_replace("%%$field%%", $replacement, $subject);
		}

		return $subject;
	}

	/**
	 * @param string $formType
	 * @param bool $newsletterAccepted
	 * @return string
	 */
	public function generateDefaultEmailSubject(string $formType, bool $newsletterAccepted): string
	{
		if ($newsletterAccepted) {
			return __('New newsletter registration', 'onoffice-for-wp-websites');
		}
		switch ($formType) {
			case Form::TYPE_OWNER:
				return __('Message from the owner form of your website', 'onoffice-for-wp-websites');
			case Form::TYPE_INTEREST:
				return __('Message from the interest form on your website', 'onoffice-for-wp-websites');
			case Form::TYPE_CONTACT:
				return __('Message from the contact form of your website', 'onoffice-for-wp-websites');
			default:
				return '';
		}
	}

	/**
	 *
	 * @param DataFormConfiguration $pFormConfig
	 * @return string[]
	 *
	 */

	protected function getAllowedPostVars(DataFormConfiguration $pFormConfig): array
	{
		$inputs = $pFormConfig->getInputs();
		$fields = $this->_pCompoundFields->mergeAssocFields($this->_pFieldsCollection, $inputs);
		$activeFields = [];

		foreach ($fields as $name => $module) {
			if ($this->_pFieldsCollection->containsFieldByModule($module, $name)) {
				$activeFields[$name] = $module;
			}
		}
		return $activeFields;
	}


	/**
	 *
	 * @param FormData $pFormData
	 *
	 */

	abstract protected function analyseFormContentByPrefix(FormData $pFormData);


	/**
	 *
	 * @param string $prefix
	 * @param int $formNo
	 * @return FormData
	 * @throws UnknownFormException
	 *
	 */

	public function getFormDataInstance(string $prefix, $formNo): FormData
	{
		$pInstance = $this->_formDataInstances[$prefix][$formNo] ?? null;

		if ($pInstance !== null) {
			return $pInstance;
		}

		throw new UnknownFormException;
	}


	/**
	 *
	 * @param FormData $pFormData
	 *
	 */

	public function setFormDataInstances(FormData $pFormData)
	{
		$formNo = $pFormData->getFormNo();
		$prefix = $pFormData->getDataFormConfiguration()->getFormName();
		$this->_formDataInstances[$prefix][$formNo] = $pFormData;
	}


	/**
	 *
	 */

	public static function incrementFormNo()
	{
		self::$_formNo++;
	}


	/**
	 * Updates the honeypot field in the $_POST array.
	 */
	private function updatePostHoneypot()
	{
		if ( ! get_option( 'onoffice-settings-honeypot' ) ) {
			return;
		}
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Internal form processing, called within form submission pipeline with reCAPTCHA verification.
        $honeypotValue = $this->_pFormPostConfiguration->getPostHoneypot();
        if ( $honeypotValue !== '' ) {
            if ( ! empty( $this->_pFormPostConfiguration->getPostMessage() ) ) {
                $_POST['message'] = isset($_POST['tmpField']) ? sanitize_textarea_field(wp_unslash($_POST['tmpField'])) : '';
            } else {
                unset( $_POST['message'] );
            }
            $_POST['tmpField'] = sanitize_text_field($honeypotValue);
        } else {
            if ( ! empty( $this->_pFormPostConfiguration->getPostMessage() ) ) {
                $_POST['message'] = isset($_POST['tmpField']) ? sanitize_textarea_field(wp_unslash($_POST['tmpField'])) : '';
                unset( $_POST['tmpField'] );
            }
        }
        // phpcs:enable WordPress.Security.NonceVerification.Missing
	}
	
	/**
	 *
	 * @return int
	 *
	 */

	public function getFormNo()
	{
		return self::$_formNo;
	}


	/** @param int $absolutCountResults */
	protected function setAbsolutCountResults(int $absolutCountResults)
		{ $this->_absolutCountResults = $absolutCountResults; }

	/** @return int */
	public function getAbsolutCountResults(): int
		{ return $this->_absolutCountResults; }
}
