<?php

/**
 * Vvveb
 *
 * Copyright (C) 2022  Ziadin Givan
 *
 * 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 <https://www.gnu.org/licenses/>.
 *
 */

namespace Vvveb\Plugins\ContactForm\Component;

use function Vvveb\__;
use function Vvveb\email;
use function Vvveb\humanReadable;
use function Vvveb\session as sess;
use function Vvveb\siteSettings;
use Vvveb\Sql\Plugins\ContactForm\MessageSQL;
use Vvveb\System\CacheManager;
use Vvveb\System\Component\ComponentBase;
use Vvveb\System\Core\Request;
use Vvveb\System\Notifications;
use Vvveb\System\Core\View;
use Vvveb\System\Event;
use Vvveb\System\Traits\Spam;

if (! defined('V_VERSION')) {
	die('Invalid request!');
}

class Form extends ComponentBase {
	use Spam;

	public static array $defaultOptions = [
		'save'    	     => true,
		'email'         => true,
		'sendto'        => null,
		'confirm-email' => null,
		'name'          => '', //unique form identifier
		'success'       => null, //optional success message
		'rate-limit'    => 5, //max submissions per hour per visitor (session based)
	];

	public int $cacheExpire = 0;

	function arrayToText($message, &$html, &$txt) {
		$html .= '<table>';

		foreach ($message as $name => $value) {
			$name = humanReadable(htmlspecialchars($name));
			$value = htmlspecialchars($value);

			$html .= "<tr><th>$name</th><td>$value</td></tr>";
			$txt .= "$name : $value\n";
		}
		$html .= '</table>';

		return [$html, $txt];
	}

	/**
	 * Session based throttling: max N submissions per hour.
	 *
	 * @return bool True when the visitor may still submit.
	 */
	protected function rateLimitReached(): bool {
		$limit = (int) ($this->options['rate-limit'] ?? 5);

		if ($limit <= 0) {
			return true; //disabled
		}

		$now       = time();
		$key       = 'contact-form-submits';
		$submitted = array_filter(
			(array) (sess($key) ?? []),
			fn ($t) => ($now - (int) $t) < 3600
		);

		return count($submitted) < $limit;
	}

	/** Record a submission for the hourly rate limit. */
	protected function recordSubmission(): void {
		$now       = time();
		$key       = 'contact-form-submits';
		$submitted = array_values(array_filter(
			(array) (sess($key) ?? []),
			fn ($t) => ($now - (int) $t) < 3600
		));
		$submitted[] = $now;
		\Vvveb\session([$key => $submitted]);
	}

	/** Show an error for the current form (keyed by form name when present). */
	protected function formError(string $error): void {
		$view     = View::getInstance();
		$formName = $post['form-name'] ?? ($this->options['name'] ?? null);

		if ($formName) {
			$view->errors[$formName] = $error;
		} else {
			$view->errors[] = $error;
		}
	}

	function request(&$results, $index = 0) {
		$request = Request::getInstance();

		if (isset($request->post['contact-form'])) {
			$post = $request->post;

			list($results, $post) = Event :: trigger(__CLASS__,__FUNCTION__, $results, $post);

			//if $post still has data, some filter above set by a spam plugin might remove the message
			if ($post) {
				$view       = View::getInstance();
				$name       = $this->options['name'];
				$formName   = $post['form-name'] ?? null; //data-v-type
				$formName = $formName ?? $name;

				//throttle abusive visitors before touching the database or sending mail
				if (! $this->rateLimitReached()) {
					$this->formError(__('Too many messages, please try again later.'), $formName);
					return;
				}

				if (! $this->isSpam($post)) {
					$post       = $this->removeSpamCatchFields($post);
					$meta       = $request->server;
					$metaFields = ['HTTP_USER_AGENT', 'REMOTE_ADDR', 'REQUEST_TIME', 'REQUEST_URI'];
					$meta       = array_intersect_key($request->server, array_flip($metaFields));
					$msg        = ['message' => ['data' => json_encode($post), 'meta' => json_encode($meta), 'type' => $formName]];
					//success message comes from options only, never from POST data
					$success    = $this->options['success'] ?? null; //data-v-success

					if ($formName && ($formName != $name)) {
						return $results;
					}


					if ($this->options['save'] == true) {
						$message    = new MessageSQL();

						if ($message->add($msg)) {
							CacheManager :: clearObjectCache('component', 'notifications');
							$success = $success ?: __('Message was sent!');
							$this->recordSubmission();
							$results['success'] = $success;

							if ($formName) {
								$view->success[$formName] = $success;
							} else {
								$view->success[] = $success;
							}
						} else {
							$error = __('Error sending message!');

							if ($formName) {
								$view->errors[$formName] = $error;
							} else {
								$view->errors[] = $error;
							}
						}
					}

					$formName = humanReadable($formName);
					$html     = '<h2>' . htmlspecialchars($formName) . '</h2>';
					$txt      =  "$formName\n\n";

					list($html, $txt) = $this->arrayToText($post, $html, $txt);

					$html .= '<h3>' . __('Meta') . '</h3>';
					$txt .= __('Meta') . "\n\n";

					list($html, $txt) = $this->arrayToText($meta, $html, $txt);

					if ($this->options['email'] == true) {
						$site    = siteSettings(SITE_ID, sess('language_id') ?? 1);
						$subject = ($site['description']['title'] ?? '') . ' - ' . $formName . (isset($post['subject']) ? ' - ' . str_replace(["\r", "\n"], '', $post['subject']) : '');
						$to      = $this->options['sendto'] ?? $site['contact-email'] ?? false;

						try {
							// Explicit "to" preserves the plugin's send-to configuration;
							// without it the event's configured recipients apply
							Notifications::trigger('contact-form.message', [
								'subject' => $subject,
								'message' => $txt,
								'html'    => $html,
							] + ($to ? ['to' => $to] : []) + [
								'form' => $post,
								'meta' => $meta,
							]);
						} catch (\Throwable $e) {
							$error = __('Error sending mail!') . "\n" . $e->getMessage();
							$view->errors[] = $error;
						}

						//record rate limit and show feedback when saving is disabled
						//when saving is disabled this is the only success signal;
						//rate limit is recorded once per request in the save branch
						if (! isset($error) && $this->options['save'] != true) {
							$this->recordSubmission();

							$success = $success ?: __('Message was sent!');
							$results['success'] = $success;

							if ($formName) {
								$view->success[$formName] = $success;
							} else {
								$view->success[] = $success;
							}
						}
					}
				} else {
					$error =  __('Sending message failed!');
					if ($formName) {
						$view->errors[$formName] = $error;
					} else {
						$view->errors[] = $error;
					}
				}
			}
		}
	}

	function results(): mixed {
		$results       = [];
		list($results) = Event :: trigger(__CLASS__,__FUNCTION__, $results);

		return $results;
	}
}
