<?php

namespace Kirki\App\Services;

use function Kirki\Framework\app;
use function Kirki\Framework\user;

defined('ABSPATH') || exit;

use Exception;
use Kirki\App\Constants\Form\FormFieldTypes;
use Kirki\App\DTO\Form\FormConfigDTO;
use Kirki\App\FormActions\FormActionDispatcher;
use Kirki\App\Models\Form;
use Kirki\App\Models\FormData;
use Kirki\App\Supports\ActionHooks;
use Kirki\App\Supports\Form\FormFieldRulesBuilder;
use Kirki\App\Supports\Recaptcha;
use Kirki\App\Supports\Session;
use Kirki\Framework\Exceptions\ValidationException;
use Kirki\Framework\Http\Response;
use Kirki\Framework\Validation\Validator;

/**
 * Orchestrates a front-end form submission.
 */
class FormSubmissionService
{
	/**
	 * @var FormActionDispatcher
	 */
	protected $actions;

	public function __construct()
	{
		$this->actions = app(FormActionDispatcher::class); // @todo: resolve using DI once the router starts using container
	}

	/**
	 * Handle a form submission.
	 *
	 * @param array $params The full request payload.
	 * @return bool Whether every configured action (email/webhook/...) succeeded.
	 *
	 * @throws ValidationException When field validation fails.
	 * @throws Exception           When the form metadata/configuration is invalid, a
	 *                             submission limit has been reached, or the submission
	 *                             could not be saved.
	 */
	public function handle(array $params)
	{
		
		['form_id' => $form_id, 'post_id' => $post_id] = $this->parse_form_metadata($params['_kirki_form'] ?? '');

		Recaptcha::verify($params, $form_id);

		$form_config = $this->load_form_config($form_id, $post_id);
		
		$form_data = $this->extract_form_data($params, $form_config->fields);
		

		$form_data = $this->validate_fields($form_data, $form_config->fields);

		$form = $this->save_form($form_id, $post_id, $form_config);
		$session_id = Session::get_session_id(); // todo: need to fix this for user ip

		$this->enforce_submission_limits($form->id, $session_id, $form_config);

		$this->save_submission($form->id, $form_data, $form_config, $session_id);

		$actions_succeeded = $this->actions->dispatch($form_data, $form_config);

		if (!$actions_succeeded) {
			throw new Exception(esc_html__('One or more form actions failed. Please try again.', 'kirki'), (int) Response::INTERNAL_SERVER_ERROR);
		}

		ActionHooks::kirki_form_submitted($form_data, $form_config);

		return true;
	}

	/**
	 * Extract only the valid form field data from the request payload.
	 *
	 * Filters out internal submission metadata keys, ignores file-type fields
	 * (since file uploads are no longer supported), and strips any unconfigured keys.
	 *
	 * @param array $params The full request payload.
	 * @param array $fields Form field configuration.
	 * @return array The cleaned form data.
	 */
	protected function extract_form_data(array $params, array $fields = [])
	{
		$raw_data = $params;

		if (empty($fields)) {
			return $raw_data;
		}

		$form_data = [];

		foreach ($fields as $name => $field) {
			if (($field['type'] ?? null) === FormFieldTypes::FILE) {
				continue;
			}

			if (array_key_exists($name, $raw_data)) {
				$form_data[$name] = $raw_data[$name];
			}
		}

		return $form_data;
	}

	/**
	 * Parse and verify the base64 form metadata token.
	 *
	 * The token is signed with `wp_hash()` at render time, so an attacker
	 * cannot mint tokens for arbitrary form/post combinations.
	 *
	 * @param mixed $form_meta_data_base64 Base64 encoded form metadata.
	 * @return array{form_id: string|null, post_id: string|null}
	 */
	protected function parse_form_metadata($form_meta_data_base64)
	{
		if (!is_string($form_meta_data_base64)) {
			return ['form_id' => null, 'post_id' => null];
		}

		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
		$form_meta_data = explode('|', base64_decode(base64_decode($form_meta_data_base64)));

		if (count($form_meta_data) < 3) {
			return ['form_id' => null, 'post_id' => null];
		}

		$form_id = $form_meta_data[0];
		$post_id = $form_meta_data[1];
		$signature = $form_meta_data[2];

		$expected = wp_hash($form_id . '|' . $post_id);

		if (!hash_equals($expected, (string) $signature)) {
			return ['form_id' => null, 'post_id' => null];
		}

		return [
			'form_id' => $form_id ?: null,
			'post_id' => $post_id ?: null,
		];
	}

	/**
	 * Resolve the stored form configuration for a submission.
	 *
	 * @param string|null $form_id The form element id.
	 * @param string|null $post_id The id of the post the form is rendered on.
	 * @return FormConfigDTO The form configuration.
	 *
	 * @throws Exception When the metadata or configuration is invalid.
	 */
	protected function load_form_config($form_id, $post_id)
	{
		if (!isset($form_id, $post_id)) {
			throw new Exception(esc_html__('Form data is invalid!', 'kirki'), (int) Response::BAD_REQUEST);
		}

		$form_config = Session::get($form_id);

		if (!is_array($form_config)) {
			throw new Exception(esc_html__('Form config not found', 'kirki'), (int) Response::BAD_REQUEST);
		}

		return FormConfigDTO::from_array($form_config);
	}

	/**
	 * Validate the submission against the configured fields.
	 *
	 * @param array $form_data The submission data.
	 * @param array $fields    The field configuration.
	 * @return array The validated form data.
	 *
	 * @throws ValidationException When validation fails.
	 */
	protected function validate_fields(array $form_data, array $fields)
	{
		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		$data = FormFieldRulesBuilder::data_for_validation($form_data, $fields);

		Validator::make($data, FormFieldRulesBuilder::rules($fields))->validate(); //@todo: not all data are coming

		return $data;
	}

	/**
	 * Find or create the stored form record, keeping its name in sync.
	 *
	 * @param string        $form_id     The form element id.
	 * @param string        $post_id     The id of the post the form is rendered on.
	 * @param FormConfigDTO $form_config The form configuration.
	 * @return Form
	 */
	protected function save_form($form_id, $post_id, FormConfigDTO $form_config)
	{
		$form_name = $form_config->name;

		$saved = Form::update_or_create([
			'post_id' => (int) $post_id,
			'form_ele_id' => $form_id,
		], [
			'name' => $form_name,
		]);

		return $saved;
	}

	/**
	 * Reject the submission if it hits a configured entry or response limit.
	 *
	 * @param int           $form_id     The stored form id.
	 * @param string        $session_id  The Kirki session id (cookie-identified, see Session::get_session_id()).
	 * @param FormConfigDTO $form_config The form configuration.
	 * @return void
	 *
	 * @throws Exception When a limit has been reached.
	 */
	protected function enforce_submission_limits($form_id, $session_id, FormConfigDTO $form_config)
	{
		$max_entry = $form_config->maxEntry;
		$entry_limit = !empty($max_entry['restricted']) ? (int) $max_entry['value'] : null;

		if ($this->entry_limit_reached($form_id, $session_id, $entry_limit)) {
			throw new Exception(esc_html__('You have reached the maximum number of submissions allowed.', 'kirki'), (int) Response::TOO_MANY_REQUESTS);
		}

		$response_limit = $form_config->responseLimit;
		$response_limit = !empty($response_limit['restricted']) ? (int) $response_limit['value'] : null;

		if ($this->response_limit_reached($form_id, $response_limit)) {
			throw new Exception(esc_html__('This form is no longer accepting submissions.', 'kirki'), (int) Response::TOO_MANY_REQUESTS);
		}
	}

	/**
	 * Whether the current session has reached the per-session entry limit.
	 *
	 * @param int      $form_id    The stored form id.
	 * @param string   $session_id The Kirki session id (cookie-identified, see Session::get_session_id()).
	 * @param int|null $limit      The entry limit.
	 * @return bool
	 */
	protected function entry_limit_reached($form_id, $session_id, $limit)
	{
		if ($limit === null) {
			return false;
		}

		$count = FormData::where('form_id', $form_id)
			->where('session_id', $session_id)
			->distinct()
			->count('timestamp');

		return $count >= intval($limit);
	}

	/**
	 * Whether the form has reached its total response limit.
	 *
	 * @param int      $form_id The stored form id.
	 * @param int|null $limit   The response limit.
	 * @return bool
	 */
	protected function response_limit_reached($form_id, $limit)
	{
		if ($limit === null) {
			return false;
		}

		$count = FormData::where('form_id', $form_id)
			->distinct()
			->count('timestamp');

		return $count >= intval($limit);
	}



	/**
	 * Persist the submission, honouring the form's saveData preference.
	 *
	 * @param int           $form_id     Stored form id.
	 * @param array         $form_data   Form data.
	 * @param FormConfigDTO $form_config Form configuration.
	 * @param string        $session_id  Kirki session id.
	 * @return void
	 *
	 * @throws Exception When saveData is enabled but the submission could not be stored.
	 */
	protected function save_submission($form_id, $form_data, FormConfigDTO $form_config, $session_id)
	{
		if (!$form_config->saveData) {
			return;
		}

		if (!$this->insert_form_data($form_data, $form_id, $form_config->fields, $session_id)) {
			throw new Exception(esc_html__('Failed to save your submission. Please try again.', 'kirki'), (int) Response::INTERNAL_SERVER_ERROR);
		}
	}

	/**
	 * Insert submission data — one row per field, grouped by timestamp + session.
	 *
	 * @param array  $form_data       Form data.
	 * @param int    $form_id         Stored form id.
	 * @param array  $form_data_types Field configuration (for input_type).
	 * @param string $session_id      Kirki session id.
	 * @return bool
	 */
	protected function insert_form_data($form_data, $form_id, $form_data_types, $session_id)
	{
		if (empty($form_data)) {
			return false;
		}

		$timestamp = time();
		$user_id = user()->get_id();

		$rows = [];

		foreach ($form_data as $name => $value) {
			$type = isset($form_data_types[$name]['type']) ? $form_data_types[$name]['type'] : 'text';

			if (is_array($value)) {
				$value = maybe_serialize($value);
			}

			$rows[] = [
				'form_id' => $form_id,
				'user_id' => $user_id,
				'session_id' => $session_id,
				'timestamp' => $timestamp,
				'input_key' => (string) $name,
				'input_value' => $value,
				'input_type' => (string) $type,
			];
		}

		return FormData::insert($rows);
	}
}
