<?php
/** Safe Contact Form 7 configuration, validation, concurrency and rollback. */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

const WPAGENT_CF7_MAX_TITLE_BYTES   = 400;
const WPAGENT_CF7_MAX_FORM_BYTES    = 200000;
const WPAGENT_CF7_MAX_MAILBOX_BYTES = 2000;
const WPAGENT_CF7_MAX_SUBJECT_BYTES = 2000;
const WPAGENT_CF7_MAX_BODY_BYTES    = 100000;
const WPAGENT_CF7_MAX_MESSAGE_BYTES = 4000;

/** @return array<string,mixed> */
function wpagent_cf7_mail_input_schema( bool $secondary ): array {
	$properties = array(
		'subject' => array( 'type' => 'string', 'maxLength' => WPAGENT_CF7_MAX_SUBJECT_BYTES ),
		'sender' => array( 'type' => 'string', 'maxLength' => WPAGENT_CF7_MAX_MAILBOX_BYTES ),
		'recipient' => array( 'type' => 'string', 'maxLength' => WPAGENT_CF7_MAX_MAILBOX_BYTES ),
		'body' => array( 'type' => 'string', 'maxLength' => WPAGENT_CF7_MAX_BODY_BYTES ),
		'reply_to' => array( 'type' => 'string', 'maxLength' => WPAGENT_CF7_MAX_MAILBOX_BYTES ),
		'exclude_blank' => array( 'type' => 'boolean' ),
		'format' => array( 'type' => 'string', 'enum' => array( 'plain' ) ),
	);
	if ( $secondary ) {
		$properties['active'] = array( 'type' => 'boolean' );
	}
	return array( 'type' => 'object', 'properties' => $properties, 'additionalProperties' => false );
}

/**
 * CF7 6.1's built-in message identifiers, including its bundled field modules.
 *
 * This remains a fixed allowlist rather than trusting the filterable
 * wpcf7_messages() map. Third-party extensions therefore cannot extend the
 * public response or the writable schema with arbitrary configuration.
 *
 * @return string[]
 */
function wpagent_cf7_message_keys(): array {
	return array(
		'mail_sent_ok', 'mail_sent_ng', 'validation_error', 'spam', 'accept_terms', 'invalid_required', 'invalid_too_long', 'invalid_too_short',
		'quiz_answer_not_correct',
		'invalid_email', 'invalid_url', 'invalid_tel',
		'invalid_date', 'date_too_early', 'date_too_late',
		'captcha_not_match',
		'invalid_number', 'number_too_small', 'number_too_large',
		'upload_failed', 'upload_file_type_invalid', 'upload_file_too_large', 'upload_failed_php_error',
	);
}

/** @return array<string,mixed> */
function wpagent_cf7_messages_input_schema(): array {
	$properties = array();
	foreach ( wpagent_cf7_message_keys() as $key ) {
		$properties[ $key ] = array( 'type' => 'string', 'maxLength' => WPAGENT_CF7_MAX_MESSAGE_BYTES );
	}
	return array( 'type' => 'object', 'properties' => $properties, 'additionalProperties' => false );
}

/** @return array<string,mixed> */
function wpagent_cf7_write_properties_schema(): array {
	return array(
		'title' => array( 'type' => 'string', 'minLength' => 1, 'maxLength' => WPAGENT_CF7_MAX_TITLE_BYTES ),
		'locale' => array( 'type' => 'string', 'minLength' => 2, 'maxLength' => 32 ),
		'form' => array( 'type' => 'string', 'minLength' => 1, 'maxLength' => WPAGENT_CF7_MAX_FORM_BYTES ),
		'mail' => wpagent_cf7_mail_input_schema( false ),
		'mail_2' => wpagent_cf7_mail_input_schema( true ),
		'messages' => wpagent_cf7_messages_input_schema(),
	);
}

/** @return true|WP_Error */
function wpagent_cf7_validate_text( $value, string $field, int $max, bool $multiline = false, bool $plain = false ) {
	if ( ! is_string( $value ) || '' === trim( $value ) || strlen( $value ) > $max || wp_check_invalid_utf8( $value, true ) !== $value ) {
		/* translators: %s: Contact Form 7 field name. */
		return new WP_Error( 'wpagent_cf7_invalid_text', sprintf( __( '%s muss ein nicht leerer gültiger Text innerhalb des Größenlimits sein.', 'wp-agent-companion' ), $field ), array( 'status' => 400 ) );
	}
	if ( preg_match( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $value ) || ( ! $multiline && preg_match( '/[\r\n]/', $value ) ) ) {
		/* translators: %s: Contact Form 7 field name. */
		return new WP_Error( 'wpagent_cf7_control_character', sprintf( __( '%s enthält unerlaubte Steuer- oder Zeilenumbruchzeichen.', 'wp-agent-companion' ), $field ), array( 'status' => 400 ) );
	}
	if ( $plain && preg_match( '/<\/?[A-Za-z][^>]*>/', $value ) ) {
		/* translators: %s: Contact Form 7 field name. */
		return new WP_Error( 'wpagent_cf7_html_forbidden', sprintf( __( '%s darf kein HTML enthalten.', 'wp-agent-companion' ), $field ), array( 'status' => 400 ) );
	}
	return true;
}

/** @return string|WP_Error */
function wpagent_cf7_safe_form_template( $value ) {
	$valid = wpagent_cf7_validate_text( $value, 'form', WPAGENT_CF7_MAX_FORM_BYTES, true );
	if ( is_wp_error( $valid ) ) {
		return $valid;
	}
	$value   = trim( str_replace( array( "\r\n", "\r" ), "\n", $value ) );
	$allowed = array(
		'p' => array( 'class' => true ), 'div' => array( 'class' => true ), 'span' => array( 'class' => true ),
		'label' => array( 'for' => true, 'class' => true ), 'small' => array( 'class' => true ),
		'fieldset' => array( 'class' => true ), 'legend' => array( 'class' => true ), 'br' => array(),
	);
	if ( wp_kses( $value, $allowed ) !== $value ) {
		return new WP_Error( 'wpagent_cf7_unsafe_form_markup', __( 'form enthält HTML-Tags oder Attribute außerhalb der engen CF7-Struktur-Allowlist.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return $value;
}

/** @return true|WP_Error */
function wpagent_cf7_validate_mail_patch( $value, bool $secondary ) {
	if ( ! is_array( $value ) || array_is_list( $value ) ) {
		return new WP_Error( 'wpagent_cf7_mail_object', __( 'Mail-Einstellungen müssen ein Objekt sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$allowed = array( 'subject', 'sender', 'recipient', 'body', 'reply_to', 'exclude_blank', 'format' );
	if ( $secondary ) {
		$allowed[] = 'active';
	}
	foreach ( array_keys( $value ) as $key ) {
		if ( ! is_string( $key ) || ! in_array( $key, $allowed, true ) ) {
			return new WP_Error( 'wpagent_cf7_mail_property', __( 'Die Mail-Konfiguration enthält eine nicht erlaubte Eigenschaft.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
	}
	foreach ( array( 'exclude_blank', 'active' ) as $key ) {
		if ( array_key_exists( $key, $value ) && ! is_bool( $value[ $key ] ) ) {
			return new WP_Error( 'wpagent_cf7_mail_boolean', sprintf( __( '%s muss ein boolescher Wert sein.', 'wp-agent-companion' ), $key ), array( 'status' => 400 ) );
		}
	}
	if ( array_key_exists( 'format', $value ) && 'plain' !== $value['format'] ) {
		return new WP_Error( 'wpagent_cf7_mail_format', __( 'Über den sicheren Agent-Pfad kann das Mailformat nur auf plain gesetzt werden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$limits = array( 'subject' => WPAGENT_CF7_MAX_SUBJECT_BYTES, 'sender' => WPAGENT_CF7_MAX_MAILBOX_BYTES, 'recipient' => WPAGENT_CF7_MAX_MAILBOX_BYTES, 'reply_to' => WPAGENT_CF7_MAX_MAILBOX_BYTES, 'body' => WPAGENT_CF7_MAX_BODY_BYTES );
	foreach ( $limits as $key => $limit ) {
		if ( ! array_key_exists( $key, $value ) ) {
			continue;
		}
		if ( 'reply_to' === $key && '' === $value[ $key ] ) {
			continue;
		}
		$valid = wpagent_cf7_validate_text( $value[ $key ], $key, $limit, 'body' === $key, 'body' === $key );
		if ( is_wp_error( $valid ) ) {
			return $valid;
		}
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_cf7_validate_write_input( array $input, bool $creating = false ) {
	$allowed = array_merge( array_keys( wpagent_cf7_write_properties_schema() ), $creating ? array() : array( 'form_id', 'expected_hash' ) );
	foreach ( array_keys( $input ) as $key ) {
		if ( ! is_string( $key ) || ! in_array( $key, $allowed, true ) ) {
			return new WP_Error( 'wpagent_cf7_property', __( 'Die Anfrage enthält eine nicht erlaubte CF7-Eigenschaft.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
	}
	if ( $creating && ! array_key_exists( 'title', $input ) ) {
		return new WP_Error( 'wpagent_cf7_title_required', __( 'Ein nicht leerer Titel ist erforderlich.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( array_key_exists( 'title', $input ) ) {
		$valid = wpagent_cf7_validate_text( $input['title'], 'title', WPAGENT_CF7_MAX_TITLE_BYTES, false, true );
		if ( is_wp_error( $valid ) || trim( $input['title'] ) !== $input['title'] ) {
			return is_wp_error( $valid ) ? $valid : new WP_Error( 'wpagent_cf7_title_whitespace', __( 'Der Titel darf keine führenden oder abschließenden Leerzeichen enthalten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
	}
	if ( array_key_exists( 'locale', $input ) && ( ! is_string( $input['locale'] ) || ! wpcf7_is_valid_locale( $input['locale'] ) ) ) {
		return new WP_Error( 'wpagent_cf7_bad_locale', __( 'Ungültige Formular-Sprache.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( array_key_exists( 'form', $input ) ) {
		$form = wpagent_cf7_safe_form_template( $input['form'] );
		if ( is_wp_error( $form ) ) {
			return $form;
		}
	}
	foreach ( array( 'mail' => false, 'mail_2' => true ) as $key => $secondary ) {
		if ( array_key_exists( $key, $input ) ) {
			$valid = wpagent_cf7_validate_mail_patch( $input[ $key ], $secondary );
			if ( is_wp_error( $valid ) ) {
				return $valid;
			}
		}
	}
	if ( array_key_exists( 'messages', $input ) ) {
		if ( ! is_array( $input['messages'] ) || array_is_list( $input['messages'] ) ) {
			return new WP_Error( 'wpagent_cf7_messages_object', __( 'messages muss ein Objekt sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$allowed_messages = array_keys( wpagent_cf7_messages_input_schema()['properties'] );
		foreach ( $input['messages'] as $key => $message ) {
			if ( ! in_array( $key, $allowed_messages, true ) ) {
				return new WP_Error( 'wpagent_cf7_message_key', __( 'messages enthält einen nicht unterstützten Meldungsschlüssel.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			$valid = wpagent_cf7_validate_text( $message, 'messages.' . $key, WPAGENT_CF7_MAX_MESSAGE_BYTES, false, true );
			if ( is_wp_error( $valid ) ) {
				return $valid;
			}
		}
	}
	return true;
}

/** @return string|WP_Error */
function wpagent_cf7_expected_hash( $value ) {
	return is_string( $value ) && preg_match( '/^[a-f0-9]{64}$/', $value )
		? $value
		: new WP_Error( 'wpagent_cf7_hash', __( 'expected_hash muss ein SHA-256-Hash sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
}

/** @param array<string,mixed> $mail @param array<string,mixed> $patch @return array<string,mixed> */
function wpagent_cf7_apply_mail_patch( array $mail, array $patch, bool $primary ): array {
	foreach ( array( 'subject', 'sender', 'recipient', 'body' ) as $key ) {
		if ( array_key_exists( $key, $patch ) ) {
			$mail[ $key ] = trim( str_replace( array( "\r\n", "\r" ), "\n", $patch[ $key ] ) );
		}
	}
	if ( array_key_exists( 'reply_to', $patch ) ) {
		$headers = array();
		foreach ( preg_split( '/\r\n|\r|\n/', (string) ( $mail['additional_headers'] ?? '' ) ) ?: array() as $line ) {
			if ( '' !== trim( $line ) && ! preg_match( '/^Reply-To\s*:/i', trim( $line ) ) ) {
				$headers[] = trim( $line );
			}
		}
		if ( '' !== trim( $patch['reply_to'] ) ) {
			array_unshift( $headers, 'Reply-To: ' . trim( $patch['reply_to'] ) );
		}
		$mail['additional_headers'] = implode( "\n", $headers );
	}
	if ( array_key_exists( 'exclude_blank', $patch ) ) {
		$mail['exclude_blank'] = $patch['exclude_blank'];
	}
	if ( array_key_exists( 'format', $patch ) ) {
		$mail['use_html'] = false;
	}
	if ( ! $primary && array_key_exists( 'active', $patch ) ) {
		$mail['active'] = $patch['active'];
	}
	if ( $primary ) {
		$mail['active'] = true;
	}
	return $mail;
}

/** @param array<string,mixed> $mail @return array<string,mixed> */
function wpagent_cf7_default_mail_for_form( WPCF7_ContactForm $form, array $mail ): array {
	$names = array_values( array_filter( array_map( 'strval', $form->collect_mail_tags() ) ) );
	$lines = array();
	foreach ( $names as $name ) {
		$lines[] = $name . ': [' . $name . ']';
	}
	$mail['subject'] = '[_site_title] ' . __( 'Neue Formularnachricht', 'wp-agent-companion' );
	$mail['body']    = $lines ? implode( "\n", $lines ) : __( 'Ein Kontaktformular wurde gesendet.', 'wp-agent-companion' );
	$email_tags = $form->scan_form_tags( array( 'basetype' => 'email' ) );
	$reply_to   = $email_tags && ! empty( $email_tags[0]->name ) ? '[' . $email_tags[0]->name . ']' : '';
	return wpagent_cf7_apply_mail_patch( $mail, array( 'reply_to' => $reply_to ), true );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_cf7_build_save_args( array $input, WPCF7_ContactForm $base, bool $creating ) {
	$args       = array( 'id' => $creating ? -1 : (int) $base->id() );
	$properties = $base->get_properties();
	foreach ( array( 'title', 'locale' ) as $key ) {
		if ( array_key_exists( $key, $input ) ) {
			$args[ $key ] = $input[ $key ];
		}
	}
	if ( array_key_exists( 'form', $input ) ) {
		$form = wpagent_cf7_safe_form_template( $input['form'] );
		if ( is_wp_error( $form ) ) {
			return $form;
		}
		$args['form'] = $form;
	}
	if ( $creating && array_key_exists( 'form', $args ) ) {
		$preview = WPCF7_ContactForm::get_template();
		$preview->set_properties( array( 'form' => $args['form'] ) );
		$properties['mail'] = wpagent_cf7_default_mail_for_form( $preview, is_array( $properties['mail'] ?? null ) ? $properties['mail'] : array() );
		$args['mail'] = $properties['mail'];
	}
	foreach ( array( 'mail' => true, 'mail_2' => false ) as $key => $primary ) {
		if ( array_key_exists( $key, $input ) ) {
			$current      = is_array( $properties[ $key ] ?? null ) ? $properties[ $key ] : array();
			$args[ $key ] = wpagent_cf7_apply_mail_patch( $current, $input[ $key ], $primary );
		}
	}
	if ( array_key_exists( 'messages', $input ) ) {
		$current          = is_array( $properties['messages'] ?? null ) ? $properties['messages'] : array();
		$args['messages'] = array_replace( $current, $input['messages'] );
	}
	return $args;
}
