<?php
/** Shared helpers for bounded Gravity Forms entry access. */

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

define( 'WPAGENT_GRAVITY_ENTRY_LOCK_PREFIX', 'wp_agent_gravity_entry_lock_' );
define( 'WPAGENT_GRAVITY_ENTRY_LOCK_TTL', 120 );

/** @param mixed $input */
function wpagent_perm_gravity_entries_read( $input = null ): bool {
	return current_user_can( 'gravityforms_view_entries' );
}

/** @param mixed $input */
function wpagent_perm_gravity_entries_edit( $input = null ): bool {
	return current_user_can( 'gravityforms_edit_entries' );
}

/** @param mixed $input */
function wpagent_perm_gravity_entries_delete( $input = null ): bool {
	return current_user_can( 'gravityforms_delete_entries' );
}

/** @param string[] $methods */
function wpagent_gravity_entries_available( array $methods ): bool {
	if ( ! wpagent_gravity_available() ) {
		return false;
	}
	foreach ( $methods as $method ) {
		if ( ! is_callable( array( 'GFAPI', $method ) ) ) {
			return false;
		}
	}
	return true;
}

function wpagent_gravity_entries_missing_error(): WP_Error {
	return new WP_Error(
		'wpagent_gravity_entries_unavailable',
		__( 'Gravity Forms ist nicht aktiv oder seine öffentliche Entry-GFAPI ist nicht vollständig verfügbar.', 'wp-agent-companion' ),
		array( 'status' => 400 )
	);
}

/** @param mixed $value @return string|int|float|bool|null */
function wpagent_gravity_entry_scalar( $value ) {
	if ( null === $value || is_scalar( $value ) ) {
		return $value;
	}
	$encoded = wp_json_encode( $value );
	return false === $encoded ? null : $encoded;
}

/** @return array<string,mixed> */
function wpagent_gravity_entry_payment_payload( array $entry ): array {
	return array(
		'currency'         => sanitize_text_field( (string) ( $entry['currency'] ?? '' ) ),
		'status'           => null === ( $entry['payment_status'] ?? null ) ? null : sanitize_text_field( (string) $entry['payment_status'] ),
		'date'             => null === ( $entry['payment_date'] ?? null ) ? null : sanitize_text_field( (string) $entry['payment_date'] ),
		'amount'           => null === ( $entry['payment_amount'] ?? null ) ? null : (string) $entry['payment_amount'],
		'method'           => null === ( $entry['payment_method'] ?? null ) ? null : sanitize_text_field( (string) $entry['payment_method'] ),
		'transaction_id'   => null === ( $entry['transaction_id'] ?? null ) ? null : sanitize_text_field( (string) $entry['transaction_id'] ),
		'transaction_type' => null === ( $entry['transaction_type'] ?? null ) ? null : absint( $entry['transaction_type'] ),
		'is_fulfilled'     => null === ( $entry['is_fulfilled'] ?? null ) ? null : (bool) $entry['is_fulfilled'],
	);
}

/**
 * Binds a write to the complete entry state while excluding add-on secrets.
 * The hash is returned only with value-bearing entry reads, so it cannot act
 * as an oracle for PII that a list response deliberately hides.
 *
 * @param array<string,mixed> $entry
 */
function wpagent_gravity_entry_state_hash( array $entry ): string {
	$raw     = wpagent_gravity_json_value( $entry );
	$state   = wpagent_gravity_canonicalize_value( wpagent_gravity_redacted_state_value_normalized( $raw ) );
	$encoded = wp_json_encode( $state, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
	return hash( 'sha256', false === $encoded ? serialize( $state ) : $encoded );
}

/** @return string|WP_Error */
function wpagent_gravity_acquire_entry_lock( int $entry_id ) {
	if ( $entry_id <= 0 ) {
		return new WP_Error( 'wpagent_bad_entry_id', __( 'entry_id muss eine positive Ganzzahl sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$token = wpagent_companion_acquire_option_lock( WPAGENT_GRAVITY_ENTRY_LOCK_PREFIX . $entry_id, WPAGENT_GRAVITY_ENTRY_LOCK_TTL );
	if ( is_string( $token ) ) {
		return $token;
	}
	return new WP_Error( 'wpagent_gravity_entry_locked', __( 'Dieser Gravity-Forms-Eintrag wird gerade durch einen anderen Vorgang geändert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

function wpagent_gravity_release_entry_lock( int $entry_id, string $token ): void {
	wpagent_companion_release_option_lock( WPAGENT_GRAVITY_ENTRY_LOCK_PREFIX . $entry_id, $token );
}

function wpagent_gravity_entry_lock_is_owned( int $entry_id, string $token ): bool {
	return $entry_id > 0
		&& wpagent_companion_option_lock_is_owned( WPAGENT_GRAVITY_ENTRY_LOCK_PREFIX . $entry_id, $token );
}

function wpagent_gravity_refresh_entry_lock( int $entry_id, string $token ): bool {
	return $entry_id > 0
		&& wpagent_companion_refresh_option_lock( WPAGENT_GRAVITY_ENTRY_LOCK_PREFIX . $entry_id, $token, WPAGENT_GRAVITY_ENTRY_LOCK_TTL );
}

/** @return array<string,bool> */
function wpagent_gravity_entry_value_keys( array $form ): array {
	$keys = array();
	foreach ( is_array( $form['fields'] ?? null ) ? $form['fields'] : array() as $field ) {
		$field_id = is_object( $field ) ? (string) ( $field->id ?? '' ) : ( is_array( $field ) ? (string) ( $field['id'] ?? '' ) : '' );
		if ( preg_match( '/^[1-9]\d*(?:\.\d+)?$/', $field_id ) ) {
			$keys[ $field_id ] = true;
		}
		$inputs = is_object( $field ) ? ( $field->inputs ?? array() ) : ( is_array( $field ) ? ( $field['inputs'] ?? array() ) : array() );
		foreach ( is_array( $inputs ) ? $inputs : array() as $input ) {
			$input_id = is_object( $input ) ? (string) ( $input->id ?? '' ) : ( is_array( $input ) ? (string) ( $input['id'] ?? '' ) : '' );
			if ( preg_match( '/^[1-9]\d*(?:\.\d+)?$/', $input_id ) ) {
				$keys[ $input_id ] = true;
			}
		}
	}
	return $keys;
}

/**
 * Unknown entry meta is omitted because add-ons can store secrets there. Only
 * field and input IDs that exist in the current form are returned.
 *
 * @return array<string,mixed>
 */
function wpagent_gravity_entry_payload( array $entry, bool $include_values = true, ?array $allowed_value_keys = null ): array {
	$payload = array(
		'provider'     => 'gravity-forms',
		'id'           => absint( $entry['id'] ?? 0 ),
		'form_id'      => absint( $entry['form_id'] ?? 0 ),
		'date_created' => sanitize_text_field( (string) ( $entry['date_created'] ?? '' ) ),
		'date_updated' => sanitize_text_field( (string) ( $entry['date_updated'] ?? '' ) ),
		'status'       => sanitize_key( (string) ( $entry['status'] ?? '' ) ),
		'is_read'      => ! empty( $entry['is_read'] ),
		'is_starred'   => ! empty( $entry['is_starred'] ),
	);
	if ( ! $include_values ) {
		return $payload;
	}
	if ( null === $allowed_value_keys ) {
		$form               = GFAPI::get_form( absint( $entry['form_id'] ?? 0 ) );
		$allowed_value_keys = is_array( $form ) ? wpagent_gravity_entry_value_keys( $form ) : array();
	}
	$values = array();
	foreach ( $entry as $key => $value ) {
		$key = (string) $key;
		if ( isset( $allowed_value_keys[ $key ] ) ) {
			$values[ $key ] = wpagent_gravity_entry_scalar( $value );
		}
	}
	$payload['created_by'] = empty( $entry['created_by'] ) ? null : absint( $entry['created_by'] );
	$payload['ip']         = sanitize_text_field( (string) ( $entry['ip'] ?? '' ) );
	$payload['source_url'] = esc_url_raw( (string) ( $entry['source_url'] ?? '' ) );
	$payload['user_agent'] = sanitize_text_field( (string) ( $entry['user_agent'] ?? '' ) );
	$payload['payment']    = wpagent_gravity_entry_payment_payload( $entry );
	$payload['values']     = $values;
	$payload['state_hash'] = wpagent_gravity_entry_state_hash( $entry );
	return $payload;
}

/** @return array<string,mixed> */
function wpagent_gravity_entry_output_schema(): array {
	$nullable_string = array( 'type' => array( 'string', 'null' ) );
	return array(
		'type'                 => 'object',
		'additionalProperties' => false,
		'properties'           => array(
			'provider'     => array( 'type' => 'string' ),
			'id'           => array( 'type' => 'integer' ),
			'form_id'      => array( 'type' => 'integer' ),
			'date_created' => array( 'type' => 'string' ),
			'date_updated' => array( 'type' => 'string' ),
			'status'       => array( 'type' => 'string' ),
			'is_read'      => array( 'type' => 'boolean' ),
			'is_starred'   => array( 'type' => 'boolean' ),
			'created_by'   => array( 'type' => array( 'integer', 'null' ) ),
			'ip'           => array( 'type' => 'string' ),
			'source_url'   => array( 'type' => 'string' ),
			'user_agent'   => array( 'type' => 'string' ),
			'payment'      => array(
				'type'                 => 'object',
				'additionalProperties' => false,
				'properties'           => array(
					'currency'         => array( 'type' => 'string' ),
					'status'           => $nullable_string,
					'date'             => $nullable_string,
					'amount'           => $nullable_string,
					'method'           => $nullable_string,
					'transaction_id'   => $nullable_string,
					'transaction_type' => array( 'type' => array( 'integer', 'null' ) ),
					'is_fulfilled'     => array( 'type' => array( 'boolean', 'null' ) ),
				),
			),
				'values'       => array(
					'type'                 => 'object',
					'additionalProperties' => array( 'type' => array( 'string', 'integer', 'number', 'boolean', 'null' ) ),
				),
				'state_hash'   => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
		),
	);
}

/** @param mixed $input @return array<string,mixed>|WP_Error */
function wpagent_gravity_get_authorized_entry( $input, string $capability ) {
	if ( ! wpagent_gravity_entries_available( array( 'get_entry' ) ) ) {
		return wpagent_gravity_entries_missing_error();
	}
	$input    = is_array( $input ) ? $input : array();
	$entry_id = absint( $input['entry_id'] ?? 0 );
	if ( $entry_id <= 0 ) {
		return new WP_Error( 'wpagent_bad_entry_id', __( 'entry_id muss eine positive Ganzzahl sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( ! current_user_can( $capability ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung für diesen Gravity-Forms-Eintrag.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	$entry = GFAPI::get_entry( $entry_id );
	if ( is_wp_error( $entry ) || ! is_array( $entry ) ) {
		return new WP_Error( 'wpagent_gravity_entry_not_found', __( 'Gravity-Forms-Eintrag nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	return $entry;
}

function wpagent_gravity_entry_expected_version_matches( array $input, array $entry ): bool {
	$expected = trim( (string) ( $input['expected_date_updated'] ?? '' ) );
	return '' === $expected || hash_equals( $expected, (string) ( $entry['date_updated'] ?? '' ) );
}

/** @return string|WP_Error */
function wpagent_gravity_entry_date_filter( $value ) {
	$value = trim( (string) $value );
	if ( '' === $value ) {
		return '';
	}
	if ( ! preg_match( '/^(\d{4})-(\d{2})-(\d{2})(?: (\d{2}):(\d{2}):(\d{2}))?$/', $value, $parts ) ) {
		return new WP_Error( 'wpagent_gravity_entry_bad_date', __( 'Datumsfilter müssen YYYY-MM-DD oder YYYY-MM-DD HH:MM:SS verwenden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$valid_date = checkdate( (int) $parts[2], (int) $parts[3], (int) $parts[1] );
	$valid_time = ! isset( $parts[4] ) || ( (int) $parts[4] <= 23 && (int) $parts[5] <= 59 && (int) $parts[6] <= 59 );
	if ( ! $valid_date || ! $valid_time ) {
		return new WP_Error( 'wpagent_gravity_entry_bad_date', __( 'Datumsfilter enthalten kein gültiges Kalenderdatum oder keine gültige Uhrzeit.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return $value;
}
