<?php
/**
 * REST API controller for Forms.
 *
 * All data stored locally in WordPress DB — no GoValid API dependency.
 *
 * @package GoValid_QR
 */

defined( 'ABSPATH' ) || exit;

class GoValid_Form_Rest_Controller {

	private const NAMESPACE = 'govalid-qr/v1';

	/**
	 * Register hooks.
	 */
	public function register(): void {
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
	}

	/**
	 * Register REST routes.
	 */
	public function register_routes(): void {

		/* ---- Forms ---- */

		register_rest_route( self::NAMESPACE, '/forms', array(
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'list_forms' ),
				'permission_callback' => array( $this, 'check_edit_permission' ),
				'args'                => array(
					'page'     => array( 'type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint' ),
					'per_page' => array( 'type' => 'integer', 'default' => 20, 'sanitize_callback' => 'absint' ),
					'status'   => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_key' ),
				),
			),
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'create_form' ),
				'permission_callback' => array( $this, 'check_edit_permission' ),
			),
		) );

		register_rest_route( self::NAMESPACE, '/forms/(?P<id>\d+)', array(
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'get_form' ),
				'permission_callback' => array( $this, 'check_edit_permission' ),
			),
			array(
				'methods'             => 'PUT',
				'callback'            => array( $this, 'update_form' ),
				'permission_callback' => array( $this, 'check_edit_permission' ),
			),
			array(
				'methods'             => 'DELETE',
				'callback'            => array( $this, 'delete_form' ),
				'permission_callback' => array( $this, 'check_edit_permission' ),
			),
		) );

		/* ---- Duplicate ---- */

		register_rest_route( self::NAMESPACE, '/forms/(?P<id>\d+)/duplicate', array(
			'methods'             => 'POST',
			'callback'            => array( $this, 'duplicate_form' ),
			'permission_callback' => array( $this, 'check_edit_permission' ),
		) );

		/* ---- Fields ---- */

		register_rest_route( self::NAMESPACE, '/forms/(?P<id>\d+)/fields', array(
			'methods'             => 'POST',
			'callback'            => array( $this, 'save_fields' ),
			'permission_callback' => array( $this, 'check_edit_permission' ),
		) );

		/* ---- Statistics ---- */

		register_rest_route( self::NAMESPACE, '/forms/(?P<id>\d+)/statistics', array(
			'methods'             => 'GET',
			'callback'            => array( $this, 'get_statistics' ),
			'permission_callback' => array( $this, 'check_edit_permission' ),
		) );

		/* ---- Submissions ---- */

		register_rest_route( self::NAMESPACE, '/forms/(?P<id>\d+)/submissions', array(
			'methods'             => 'GET',
			'callback'            => array( $this, 'list_submissions' ),
			'permission_callback' => array( $this, 'check_edit_permission' ),
			'args'                => array(
				'page'     => array( 'type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint' ),
				'per_page' => array( 'type' => 'integer', 'default' => 20, 'sanitize_callback' => 'absint' ),
			),
		) );

		register_rest_route( self::NAMESPACE, '/submissions/(?P<id>\d+)', array(
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'get_submission' ),
				'permission_callback' => array( $this, 'check_edit_permission' ),
			),
			array(
				'methods'             => 'DELETE',
				'callback'            => array( $this, 'delete_submission' ),
				'permission_callback' => array( $this, 'check_edit_permission' ),
			),
		) );

		/* ---- Export ---- */

		register_rest_route( self::NAMESPACE, '/forms/(?P<id>\d+)/export', array(
			'methods'             => 'POST',
			'callback'            => array( $this, 'export_csv' ),
			'permission_callback' => array( $this, 'check_edit_permission' ),
		) );

		/* ---- Generate Custom QR from Submissions ---- */

		register_rest_route( self::NAMESPACE, '/generate-custom-qr', array(
			'methods'             => 'POST',
			'callback'            => array( $this, 'generate_custom_qr' ),
			'permission_callback' => array( $this, 'check_edit_permission' ),
		) );

		/* ---- Public submission (no auth) ---- */

		register_rest_route( self::NAMESPACE, '/public/forms/(?P<id>\d+)/submit', array(
			'methods'             => 'POST',
			'callback'            => array( $this, 'public_submit' ),
			'permission_callback' => '__return_true',
		) );
	}

	/* ------------------------------------------------------------------
	 * Forms
	 * ----------------------------------------------------------------*/

	public function list_forms( WP_REST_Request $request ) {
		$result = GoValid_Form_Model::list_forms(
			$request->get_param( 'page' ),
			$request->get_param( 'per_page' ),
			$request->get_param( 'status' )
		);

		// Decode settings JSON for each item.
		foreach ( $result['items'] as &$item ) {
			$item->settings = json_decode( $item->settings ?? '{}' );
		}

		return rest_ensure_response( $result );
	}

	public function create_form( WP_REST_Request $request ) {
		$body = $request->get_json_params();

		if ( empty( $body['title'] ) ) {
			return new WP_Error( 'missing_title', __( 'Form title is required.', 'govalid-qr' ), array( 'status' => 400 ) );
		}

		$limit_error = $this->check_form_limit();
		if ( is_wp_error( $limit_error ) ) {
			return $limit_error;
		}

		$form_id = GoValid_Form_Model::create_form( $body );
		$form    = GoValid_Form_Model::get_form( $form_id );
		$form->settings = json_decode( $form->settings ?? '{}' );

		return rest_ensure_response( $form );
	}

	public function get_form( WP_REST_Request $request ) {
		$id   = absint( $request->get_param( 'id' ) );
		$form = GoValid_Form_Model::get_form( $id );

		if ( ! $form ) {
			return new WP_Error( 'not_found', __( 'Form not found.', 'govalid-qr' ), array( 'status' => 404 ) );
		}

		$form->settings = json_decode( $form->settings ?? '{}' );
		$form->fields   = GoValid_Form_Model::get_fields( $id );

		// Decode JSON columns on fields.
		foreach ( $form->fields as &$field ) {
			$field->options           = json_decode( $field->options ?? 'null' );
			$field->validation        = json_decode( $field->validation ?? 'null' );
			$field->conditional_logic = json_decode( $field->conditional_logic ?? 'null' );
		}

		return rest_ensure_response( $form );
	}

	public function update_form( WP_REST_Request $request ) {
		$id   = absint( $request->get_param( 'id' ) );
		$form = GoValid_Form_Model::get_form( $id );

		if ( ! $form ) {
			return new WP_Error( 'not_found', __( 'Form not found.', 'govalid-qr' ), array( 'status' => 404 ) );
		}

		$body = $request->get_json_params();
		GoValid_Form_Model::update_form( $id, $body );

		// Also save fields if included.
		if ( isset( $body['fields'] ) && is_array( $body['fields'] ) ) {
			GoValid_Form_Model::save_fields( $id, $body['fields'] );
		}

		$form = GoValid_Form_Model::get_form( $id );
		$form->settings = json_decode( $form->settings ?? '{}' );
		$form->fields   = GoValid_Form_Model::get_fields( $id );

		foreach ( $form->fields as &$field ) {
			$field->options           = json_decode( $field->options ?? 'null' );
			$field->validation        = json_decode( $field->validation ?? 'null' );
			$field->conditional_logic = json_decode( $field->conditional_logic ?? 'null' );
		}

		return rest_ensure_response( $form );
	}

	public function delete_form( WP_REST_Request $request ) {
		$id = absint( $request->get_param( 'id' ) );

		if ( ! GoValid_Form_Model::get_form( $id ) ) {
			return new WP_Error( 'not_found', __( 'Form not found.', 'govalid-qr' ), array( 'status' => 404 ) );
		}

		GoValid_Form_Model::delete_form( $id );

		return rest_ensure_response( array( 'deleted' => true ) );
	}

	/**
	 * Duplicate a form and all its fields.
	 */
	public function duplicate_form( WP_REST_Request $request ) {
		$id   = absint( $request->get_param( 'id' ) );
		$form = GoValid_Form_Model::get_form( $id );

		if ( ! $form ) {
			return new WP_Error( 'not_found', __( 'Form not found.', 'govalid-qr' ), array( 'status' => 404 ) );
		}

		$limit_error = $this->check_form_limit();
		if ( is_wp_error( $limit_error ) ) {
			return $limit_error;
		}

		$new_id = GoValid_Form_Model::duplicate_form( $id );

		if ( ! $new_id ) {
			return new WP_Error( 'duplicate_failed', __( 'Failed to duplicate form.', 'govalid-qr' ), array( 'status' => 500 ) );
		}

		$new_form = GoValid_Form_Model::get_form( $new_id );
		$new_form->settings = json_decode( $new_form->settings ?? '{}' );

		return rest_ensure_response( $new_form );
	}

	/* ------------------------------------------------------------------
	 * Fields
	 * ----------------------------------------------------------------*/

	public function save_fields( WP_REST_Request $request ) {
		$form_id = absint( $request->get_param( 'id' ) );

		if ( ! GoValid_Form_Model::get_form( $form_id ) ) {
			return new WP_Error( 'not_found', __( 'Form not found.', 'govalid-qr' ), array( 'status' => 404 ) );
		}

		$body   = $request->get_json_params();
		$fields = $body['fields'] ?? array();

		GoValid_Form_Model::save_fields( $form_id, $fields );

		$saved = GoValid_Form_Model::get_fields( $form_id );
		foreach ( $saved as &$field ) {
			$field->options           = json_decode( $field->options ?? 'null' );
			$field->validation        = json_decode( $field->validation ?? 'null' );
			$field->conditional_logic = json_decode( $field->conditional_logic ?? 'null' );
		}

		return rest_ensure_response( array( 'fields' => $saved ) );
	}

	/* ------------------------------------------------------------------
	 * Statistics
	 * ----------------------------------------------------------------*/

	public function get_statistics( WP_REST_Request $request ) {
		$id = absint( $request->get_param( 'id' ) );

		if ( ! GoValid_Form_Model::get_form( $id ) ) {
			return new WP_Error( 'not_found', __( 'Form not found.', 'govalid-qr' ), array( 'status' => 404 ) );
		}

		$stats = GoValid_Form_Model::get_statistics( $id );

		return rest_ensure_response( $stats );
	}

	/* ------------------------------------------------------------------
	 * Submissions
	 * ----------------------------------------------------------------*/

	public function list_submissions( WP_REST_Request $request ) {
		$form_id = absint( $request->get_param( 'id' ) );

		if ( ! GoValid_Form_Model::get_form( $form_id ) ) {
			return new WP_Error( 'not_found', __( 'Form not found.', 'govalid-qr' ), array( 'status' => 404 ) );
		}

		$result = GoValid_Form_Model::list_submissions(
			$form_id,
			$request->get_param( 'page' ),
			$request->get_param( 'per_page' )
		);

		// Attach field values to each submission.
		foreach ( $result['items'] as &$sub ) {
			$full = GoValid_Form_Model::get_submission( (int) $sub->id );
			$sub->values = $full ? $full->values : array();
		}

		return rest_ensure_response( $result );
	}

	public function get_submission( WP_REST_Request $request ) {
		$id         = absint( $request->get_param( 'id' ) );
		$submission = GoValid_Form_Model::get_submission( $id );

		if ( ! $submission ) {
			return new WP_Error( 'not_found', __( 'Submission not found.', 'govalid-qr' ), array( 'status' => 404 ) );
		}

		return rest_ensure_response( $submission );
	}

	public function delete_submission( WP_REST_Request $request ) {
		$id = absint( $request->get_param( 'id' ) );

		if ( ! GoValid_Form_Model::get_submission( $id ) ) {
			return new WP_Error( 'not_found', __( 'Submission not found.', 'govalid-qr' ), array( 'status' => 404 ) );
		}

		GoValid_Form_Model::delete_submission( $id );

		return rest_ensure_response( array( 'deleted' => true ) );
	}

	/* ------------------------------------------------------------------
	 * Export
	 * ----------------------------------------------------------------*/

	public function export_csv( WP_REST_Request $request ) {
		$form_id = absint( $request->get_param( 'id' ) );

		if ( ! GoValid_Form_Model::get_form( $form_id ) ) {
			return new WP_Error( 'not_found', __( 'Form not found.', 'govalid-qr' ), array( 'status' => 404 ) );
		}

		$rows = GoValid_Form_Model::export_submissions( $form_id );

		return rest_ensure_response( array( 'rows' => $rows ) );
	}

	/* ------------------------------------------------------------------
	 * Public submission
	 * ----------------------------------------------------------------*/

	public function public_submit( WP_REST_Request $request ) {
		$form_id = absint( $request->get_param( 'id' ) );
		$form    = GoValid_Form_Model::get_form( $form_id );

		if ( ! $form || 'published' !== $form->status ) {
			return new WP_Error( 'not_found', __( 'Form not found or not published.', 'govalid-qr' ), array( 'status' => 404 ) );
		}

		// Detect multipart (file upload) vs JSON submission.
		$is_multipart = ! empty( $_FILES ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing
		if ( $is_multipart ) {
			// phpcs:ignore WordPress.Security.NonceVerification
			$values_raw  = isset( $_POST['values'] ) ? sanitize_text_field( wp_unslash( $_POST['values'] ) ) : '[]';
			$values      = json_decode( $values_raw, true );
			// phpcs:ignore WordPress.Security.NonceVerification
			$honeypot    = isset( $_POST['_govalid_hp'] ) ? sanitize_text_field( wp_unslash( $_POST['_govalid_hp'] ) ) : '';
		} else {
			$body    = $request->get_json_params();
			$values  = $body['values'] ?? array();
			$honeypot = $body['_govalid_hp'] ?? '';
		}

		// Honeypot check.
		if ( ! empty( $honeypot ) ) {
			// Silently accept (don't reveal it's a spam check).
			return rest_ensure_response( array( 'success' => true ) );
		}

		// Rate limiting: max 5 submissions per 5 minutes per IP per form.
		$ip            = $this->get_client_ip();
		$transient_key = 'govalid_form_submit_' . $form_id . '_' . md5( $ip );
		$submit_count  = (int) get_transient( $transient_key );
		if ( $submit_count >= 5 ) {
			return new WP_Error(
				'rate_limited',
				__( 'Too many submissions. Please wait a few minutes before trying again.', 'govalid-qr' ),
				array( 'status' => 429 )
			);
		}
		set_transient( $transient_key, $submit_count + 1, 5 * MINUTE_IN_SECONDS );

		// Validate required fields.
		$fields = GoValid_Form_Model::get_fields( $form_id );

		$values_to_save = array();
		foreach ( $fields as $field ) {
			// Skip non-input fields.
			if ( in_array( $field->type, array( 'heading', 'separator', 'image_banner', 'page_break' ), true ) ) {
				continue;
			}

			$submitted_value = '';
			foreach ( $values as $v ) {
				if ( (int) ( $v['field_id'] ?? 0 ) === (int) $field->id ) {
					$submitted_value = $v['value'] ?? '';
					break;
				}
			}

			// Handle file upload for file AND camera fields.
			if ( in_array( $field->type, array( 'file', 'camera', 'signature' ), true ) && $is_multipart ) {
				$file_key = 'file_' . $field->id;
				// phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
				if ( ! empty( $_FILES[ $file_key ]['tmp_name'] ) ) {
					$uploaded_url = $this->handle_form_file_upload( $file_key );
					if ( is_wp_error( $uploaded_url ) ) {
						return new WP_Error(
							'validation_error',
							/* translators: 1: field label, 2: error message */
							sprintf( __( '%1$s: %2$s', 'govalid-qr' ), $field->label, $uploaded_url->get_error_message() ),
							array( 'status' => 422, 'field_id' => $field->id )
						);
					}
					$submitted_value = $uploaded_url;
				}
			}

			// Check required — for file fields, the uploaded URL counts as a value.
			if ( $field->required && '' === trim( $submitted_value ) ) {
				return new WP_Error(
					'validation_error',
					/* translators: %s: field label */
					sprintf( __( '%s is required.', 'govalid-qr' ), $field->label ),
					array( 'status' => 422, 'field_id' => $field->id )
				);
			}

			// Per-type validation & sanitization.
			if ( '' !== $submitted_value && ! in_array( $field->type, array( 'file', 'camera' ), true ) ) {
				switch ( $field->type ) {
					case 'email':
						if ( ! is_email( $submitted_value ) ) {
							return new WP_Error(
								'validation_error',
								/* translators: %s: field label */
								sprintf( __( '%s must be a valid email address.', 'govalid-qr' ), $field->label ),
								array( 'status' => 422, 'field_id' => $field->id )
							);
						}
						$submitted_value = sanitize_email( $submitted_value );
						break;

					case 'number':
						if ( ! is_numeric( $submitted_value ) ) {
							return new WP_Error(
								'validation_error',
								/* translators: %s: field label */
								sprintf( __( '%s must be a number.', 'govalid-qr' ), $field->label ),
								array( 'status' => 422, 'field_id' => $field->id )
							);
						}
						$submitted_value = sanitize_text_field( $submitted_value );
						break;

					case 'url':
						$submitted_value = esc_url_raw( $submitted_value );
						if ( empty( $submitted_value ) ) {
							return new WP_Error(
								'validation_error',
								/* translators: %s: field label */
								sprintf( __( '%s must be a valid URL.', 'govalid-qr' ), $field->label ),
								array( 'status' => 422, 'field_id' => $field->id )
							);
						}
						break;

					case 'phone':
						// Allow digits, +, -, (, ), spaces only.
						$submitted_value = preg_replace( '/[^\d+\-() ]/', '', $submitted_value );
						break;

					case 'textarea':
						// Strip tags but keep newlines. Limit to 5000 chars.
						$submitted_value = sanitize_textarea_field( substr( $submitted_value, 0, 5000 ) );
						break;

					case 'date':
					case 'time':
						// Only allow valid date/time characters.
						$submitted_value = sanitize_text_field( $submitted_value );
						break;

					default:
						// text, name, radio, gender, checkbox, select, etc.
						// Strip all HTML, limit to 1000 chars.
						$submitted_value = sanitize_text_field( substr( $submitted_value, 0, 1000 ) );
						break;
				}
			}

			$values_to_save[] = array(
				'field_id'    => (int) $field->id,
				'field_label' => $field->label,
				'field_value' => $submitted_value,
			);
		}

		$submission_id = GoValid_Form_Model::create_submission( $form_id, $values_to_save, array(
			'user_id'    => get_current_user_id() ?: null,
			'ip_address' => $ip,
			'user_agent' => sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ),
		) );

		// Email notification.
		$settings = json_decode( $form->settings ?? '{}', true );
		if ( ! empty( $settings['email_notification'] ) && ! empty( $settings['notification_email'] ) ) {
			$this->send_notification( $form, $values_to_save, $settings );
		}

		return rest_ensure_response( array(
			'success'       => true,
			'submission_id' => $submission_id,
		) );
	}

	/* ------------------------------------------------------------------
	 * Generate Custom QR from Submissions
	 * ----------------------------------------------------------------*/

	/**
	 * Generate custom QR codes from selected form submissions.
	 *
	 * Creates one QR code per submission with selected field data as metadata.
	 *
	 * @param WP_REST_Request $request
	 * @return WP_REST_Response|WP_Error
	 */
	public function generate_custom_qr( WP_REST_Request $request ) {
		// Enforce QR generation limit for free users.
		$limit_error = $this->check_qr_limit();
		if ( is_wp_error( $limit_error ) ) {
			return $limit_error;
		}

		$body            = $request->get_json_params();
		$submission_ids  = array_map( 'absint', $body['submission_ids'] ?? array() );
		$selected_fields = array_map( 'sanitize_text_field', $body['selected_fields'] ?? array() );
		$security_level  = sanitize_text_field( $body['security_level'] ?? 'SMART' );
		$qr_name         = sanitize_text_field( $body['qr_name'] ?? 'Form Submission' );

		// Password protection.
		$is_password_protected = ! empty( $body['is_password_protected'] );
		$password_type         = sanitize_text_field( $body['password_type'] ?? 'master' );
		$password              = sanitize_text_field( $body['password'] ?? '' );

		if ( empty( $submission_ids ) ) {
			return new WP_Error(
				'no_submissions',
				__( 'No submissions selected.', 'govalid-qr' ),
				array( 'status' => 400 )
			);
		}

		if ( empty( $selected_fields ) ) {
			return new WP_Error(
				'no_fields',
				__( 'No fields selected.', 'govalid-qr' ),
				array( 'status' => 400 )
			);
		}

		// Validate security level.
		$valid_levels = array( 'SMART', 'SECURE', 'ENTERPRISE' );
		if ( ! in_array( $security_level, $valid_levels, true ) ) {
			$security_level = 'SMART';
		}

		$api     = new GoValid_QR_API();
		$created = 0;
		$errors  = array();

		foreach ( $submission_ids as $sub_id ) {
			$submission = GoValid_Form_Model::get_submission( $sub_id );
			if ( ! $submission ) {
				$errors[] = sprintf( 'Submission #%d not found.', $sub_id );
				continue;
			}

			// Build metadata from only the selected fields.
			// Detect the first file URL to include as QR attachment.
			$metadata        = array();
			$attachment_url   = '';
			$attachment_name  = '';
			$file_url_pattern = '/^https?:\/\/.+\.(jpe?g|png|webp|pdf)$/i';

			foreach ( $submission->values as $val ) {
				if ( ! in_array( $val->field_label, $selected_fields, true ) ) {
					continue;
				}
				// If value is a file URL, capture as attachment (first one only).
				if ( preg_match( $file_url_pattern, $val->field_value ) ) {
					if ( empty( $attachment_url ) ) {
						$attachment_url  = $val->field_value;
						$attachment_name = basename( $val->field_value );
					}
					// Don't include raw file URL in text metadata.
					continue;
				}
				$metadata[ $val->field_label ] = $val->field_value;
			}

			// Add password protection to metadata (Django API extracts these).
			if ( $is_password_protected ) {
				$metadata['is_password_protected'] = true;
				$metadata['password_type']         = $password_type;
				if ( 'unique' === $password_type && $password ) {
					$metadata['password'] = $password;
				}
			}

			// Generate a unique QR name if multiple submissions.
			$iter_name = count( $submission_ids ) > 1
				? $qr_name . ' #' . $sub_id
				: $qr_name;

			$data = array(
				'security_level' => $security_level,
				'metadata'       => $metadata,
			);

			// Download file to a temp path if we have an attachment URL.
			$temp_file = '';
			if ( $attachment_url ) {
				require_once ABSPATH . 'wp-admin/includes/file.php';
				$temp_file = download_url( $attachment_url, 30 );
				if ( ! is_wp_error( $temp_file ) ) {
					$data['attachment_path'] = $temp_file;
					$data['attachment_name'] = $attachment_name;
				} else {
					$temp_file = ''; // Download failed — proceed without attachment.
				}
			}

			$result = $api->create_qr_code( 'custom', $iter_name, $data );

			// Clean up temp file.
			if ( $temp_file && file_exists( $temp_file ) ) {
				wp_delete_file( $temp_file );
			}

			if ( is_wp_error( $result ) ) {
				$errors[] = sprintf(
					'Submission #%d: %s',
					$sub_id,
					$result->get_error_message()
				);
			} else {
				$created++;
			}
		}

		// Flush QR list cache.
		GoValid_Cache::get_instance()->flush_prefix( 'qr_list_' );

		return rest_ensure_response( array(
			'created' => $created,
			'total'   => count( $submission_ids ),
			'errors'  => $errors,
		) );
	}

	/* ------------------------------------------------------------------
	 * Helpers
	 * ----------------------------------------------------------------*/

	public function check_edit_permission(): bool {
		return current_user_can( 'edit_posts' );
	}

	/**
	 * Check if user can create more forms.
	 *
	 * Free users: 1 form max.
	 * Subscribed users (any paid plan or institution member): unlimited.
	 *
	 * @return true|WP_Error True if allowed, WP_Error if limit reached.
	 */
	private function check_form_limit() {
		// Skip check if not connected — allow 1 form.
		if ( ! GoValid_QR::is_connected() ) {
			$count = GoValid_Form_Model::count_forms();
			if ( $count >= 1 ) {
				return new WP_Error(
					'form_limit_reached',
					__( 'Connect to GoValid to create more forms. Free accounts are limited to 1 form.', 'govalid-qr' ),
					array( 'status' => 403 )
				);
			}
			return true;
		}

		// Fetch subscription data (cached for 5 min).
		$api    = new GoValid_QR_API();
		$result = $api->get_subscription();

		$has_subscription = false;
		if ( ! is_wp_error( $result ) ) {
			$data = $result['data'] ?? $result;
			$sub  = $data['subscription'] ?? array();
			$plan = $sub['plan'] ?? array();
			$tier = $plan['tier'] ?? 'FREE';

			$has_subscription = ( 'FREE' !== $tier );
		}

		if ( ! $has_subscription ) {
			$count = GoValid_Form_Model::count_forms();
			if ( $count >= 1 ) {
				return new WP_Error(
					'form_limit_reached',
					__( 'Upgrade your subscription to create more forms. Free accounts are limited to 1 form.', 'govalid-qr' ),
					array( 'status' => 403 )
				);
			}
		}

		return true;
	}

	/**
	 * Check if QR generation limit is reached for free users.
	 *
	 * Free users can generate up to 5 QR codes.
	 *
	 * @return true|WP_Error True if allowed, WP_Error if limit reached.
	 */
	private function check_qr_limit() {
		if ( ! GoValid_QR::is_connected() ) {
			return true;
		}

		$api    = new GoValid_QR_API();
		$result = $api->get_subscription();

		$has_subscription = false;
		if ( ! is_wp_error( $result ) ) {
			$data = $result['data'] ?? $result;
			$sub  = $data['subscription'] ?? array();
			$plan = $sub['plan'] ?? array();
			$tier = $plan['tier'] ?? 'FREE';

			$has_subscription = ( 'FREE' !== $tier );
		}

		if ( ! $has_subscription ) {
			$qr_result = $api->list_qr_codes( 1, 1 );

			$qr_count = 0;
			if ( ! is_wp_error( $qr_result ) ) {
				$qr_data    = $qr_result['data'] ?? $qr_result;
				$pagination = $qr_data['pagination'] ?? array();
				$qr_count   = (int) ( $pagination['total'] ?? 0 );
			}

			if ( $qr_count >= 5 ) {
				return new WP_Error(
					'qr_limit_reached',
					__( 'Free accounts can generate up to 5 QR codes. Upgrade your subscription to generate more.', 'govalid-qr' ),
					array( 'status' => 403 )
				);
			}
		}

		return true;
	}

	private function get_client_ip(): string {
		$headers = array( 'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR' );
		foreach ( $headers as $header ) {
			if ( ! empty( $_SERVER[ $header ] ) ) {
				$ip = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );
				// X-Forwarded-For may contain multiple IPs.
				if ( strpos( $ip, ',' ) !== false ) {
					$ip = trim( explode( ',', $ip )[0] );
				}
				return $ip;
			}
		}
		return '127.0.0.1';
	}

	/**
	 * Handle a single file upload from a public form submission.
	 *
	 * Validates type (images + PDF) and size (5 MB), moves file to
	 * wp-content/uploads/govalid-forms/ and returns the public URL.
	 *
	 * @param string $file_key Key in $_FILES.
	 * @return string|WP_Error File URL on success, WP_Error on failure.
	 */
	private function handle_form_file_upload( string $file_key ) {
		// phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		if ( empty( $_FILES[ $file_key ]['tmp_name'] ) ) {
			return new WP_Error( 'no_file', 'No file uploaded.' );
		}

		// phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$file = $_FILES[ $file_key ];

		// 1. Check for upload errors.
		if ( ! empty( $file['error'] ) && UPLOAD_ERR_OK !== $file['error'] ) {
			return new WP_Error( 'upload_error', 'File upload error (code ' . $file['error'] . ').' );
		}

		// 2. Validate file extension.
		$allowed_types = array( 'image/jpeg', 'image/png', 'image/webp', 'application/pdf' );
		$file_type     = wp_check_filetype( $file['name'] );
		if ( empty( $file_type['type'] ) || ! in_array( $file_type['type'], $allowed_types, true ) ) {
			return new WP_Error( 'invalid_type', 'File type not allowed. Accepted: JPG, PNG, WebP, PDF.' );
		}

		// 3. Validate actual file content MIME (magic bytes) — prevents disguised uploads.
		if ( function_exists( 'finfo_open' ) ) {
			$finfo     = finfo_open( FILEINFO_MIME_TYPE );
			$real_mime = finfo_file( $finfo, $file['tmp_name'] );
			finfo_close( $finfo );

			if ( ! in_array( $real_mime, $allowed_types, true ) ) {
				return new WP_Error( 'invalid_content', 'File content does not match its extension. Upload rejected.' );
			}
		}

		// 4. For images, verify with getimagesize() — catches embedded PHP in image files.
		if ( 0 === strpos( $file_type['type'], 'image/' ) ) {
			$image_info = @getimagesize( $file['tmp_name'] ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
			if ( false === $image_info ) {
				return new WP_Error( 'invalid_image', 'File is not a valid image.' );
			}
		}

		// 5. Scan file content for PHP/script tags (belts and suspenders).
		$file_content = file_get_contents( $file['tmp_name'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions
		if ( false !== $file_content ) {
			$dangerous_patterns = array( '<?php', '<?=', '<script', '<%', 'eval(', 'base64_decode(', 'system(', 'exec(', 'passthru(' );
			$content_lower      = strtolower( $file_content );
			foreach ( $dangerous_patterns as $pattern ) {
				if ( false !== strpos( $content_lower, $pattern ) ) {
					return new WP_Error( 'malicious_file', 'File contains potentially dangerous content.' );
				}
			}
		}

		// 6. Validate size (max 5 MB).
		if ( $file['size'] > 5 * MB_IN_BYTES ) {
			return new WP_Error( 'too_large', 'File exceeds the 5 MB limit.' );
		}

		// 7. Sanitize the file name — remove anything suspicious.
		$file['name'] = sanitize_file_name( $file['name'] );

		// Use WP upload directory with a custom subfolder.
		require_once ABSPATH . 'wp-admin/includes/file.php';

		// Temporarily override upload dir to a govalid-forms subfolder.
		$override_dir = function ( $uploads ) {
			$subdir                = '/govalid-forms' . $uploads['subdir'];
			$uploads['subdir']     = $subdir;
			$uploads['path']       = $uploads['basedir'] . $subdir;
			$uploads['url']        = $uploads['baseurl'] . $subdir;
			return $uploads;
		};
		add_filter( 'upload_dir', $override_dir );

		$overrides = array(
			'test_form' => false,
			'test_type' => true, // Let WP also verify the MIME.
			'mimes'     => array(
				'jpg|jpeg' => 'image/jpeg',
				'png'      => 'image/png',
				'webp'     => 'image/webp',
				'pdf'      => 'application/pdf',
			),
		);

		$result = wp_handle_upload( $file, $overrides );

		remove_filter( 'upload_dir', $override_dir );

		if ( ! empty( $result['error'] ) ) {
			return new WP_Error( 'upload_failed', $result['error'] );
		}

		return $result['url'];
	}

	/**
	 * Send email notification for new submission.
	 */
	private function send_notification( object $form, array $values, array $settings ): void {
		$to      = sanitize_email( $settings['notification_email'] );
		$subject = sprintf(
			/* translators: %s: form title */
			__( 'New submission: %s', 'govalid-qr' ),
			$form->title
		);

		/* translators: %s: form title */
		$body = sprintf( __( 'A new submission has been received for "%s".', 'govalid-qr' ), $form->title ) . "\n\n";

		foreach ( $values as $val ) {
			$body .= $val['field_label'] . ': ' . $val['field_value'] . "\n";
		}

		$body .= "\n" . __( 'View submissions in your WordPress admin.', 'govalid-qr' );

		wp_mail( $to, $subject, $body );
	}
}
