<?php
/**
 * Plugin Name: BW Gravity Forms Partial Entries
 * Description: Progressive form submission for Gravity Forms — lets users submit partially, return via signed link, and complete over multiple sessions.
 * Version: 1.0.0
 * Author: Bowden Web
 */

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

class BW_Gform_Partial_Entries {

	private $form_ids = [
		2
	];

	function __construct() {

		add_filter( 'gform_entry_id_pre_save_lead', [ $this, 'force_update_existing_entry' ], 10, 2 );
		add_action( 'gform_after_submission', [ $this, 'maybe_run_hubspot_feed' ], 15, 2 );

		foreach( $this->form_ids as $form_id ) {
			add_filter( 'gform_pre_render_' . $form_id, [ $this, 'progressive_form_prefill' ] );
			add_filter( 'gform_pre_validation_' . $form_id, [ $this, 'progressive_form_prefill' ] );
			add_filter( 'gform_pre_submission_filter_' . $form_id, [ $this, 'mark_entry_for_update' ] );
			add_filter( 'gform_confirmation_' . $form_id, [ $this, 'progressive_confirmation' ], 10, 4 );
		}
	}

	function maybe_run_hubspot_feed( $entry, $form ) {
		if ( ! in_array( (int) $form['id'], $this->form_ids, true ) ) {
			return;
		}

		if ( ! class_exists( 'GFAPI' ) ) {
			return;
		}

		if ( empty( $_POST['bw_update_entry_id'] ) ) {
			return;
		}

		// Let Gravity Forms handle re-execution of HubSpot feeds correctly
		GFAPI::maybe_process_feeds( $entry, $form, 'gravityformshubspot', true );
		GFAPI::maybe_process_feeds( $entry, $form, 'hubspot', true );
	}

	/**
	 * Get a signed token for entry round-trips
	 */
	function sign_token($entry_id) {
		$data = wp_json_encode(['e' => (int) $entry_id]);
		$hmac = hash_hmac('sha256', $data, wp_salt('auth'));
		return base64_encode($data) . '.' . $hmac;
	}

	function verify_token($token) {
		if (strpos($token, '.') === false) return false;
		list($b64, $hmac) = explode('.', $token, 2);
		$data = base64_decode($b64);
		if (!$data) return false;
		$calc = hash_hmac('sha256', $data, wp_salt('auth'));
		if (!hash_equals($calc, $hmac)) return false;
		$obj = json_decode($data, true);
		if (!isset($obj['e'])) return false;
		return $obj; // ['e' => entry_id, 'em' => email]
	}

	/**
	 * Calculate completion on the form object (count non-empty fields vs total user-fillable fields)
	 */
	function calculate_completion($form, $entry = null) {
		$filled = 0;
		$total  = 0;

		foreach ($form['fields'] as $field) {
			// Skip non-user fields entirely
			if (in_array($field->type, ['html', 'section', 'page', 'captcha', 'password', 'consent', 'fileupload'])) {
				continue;
			}

			// Skip fields that contain a specific CSS class
			if ( strpos( $field->cssClass, 'bw-optional-field' ) !== false ) {
				continue; // skip this field and move on to the next one
			}

			// Skip admin-only fields
			if (isset($field->visibility) && $field->visibility !== 'visible') continue;

			// Skip GF hidden types (hidden inputs, CSS hidden)
			if ($field->type === 'hidden') continue;
			if (!empty($field->cssClass) && strpos($field->cssClass, 'gform_hidden') !== false) continue;
			if (!empty($field->cssClass) && strpos($field->cssClass, 'gfield_visibility_hidden') !== false) continue;
			if (!empty($field->cssClass) && strpos($field->cssClass, 'gfield--type-honeypot') !== false) continue;

			// Handle multi-input (Name, Address)
			if (!empty($field->inputs)) {
				$has_value = false;
				foreach ($field->inputs as $input) {
					$input_id = (string)$input['id'];
					$val = $entry ? rgar($entry, $input_id) : '';
					if (trim((string)$val) !== '') {
						$has_value = true;
						break;
					}
				}
				$total++;
				if ($has_value) $filled++;
			} else {
				$total++;
				$val = $entry ? rgar($entry, (string)$field->id) : '';
				if (is_array($val)) $val = implode('', $val);
				if (trim((string)$val) !== '') $filled++;
			}
		}

		$pct = $total > 0 ? round(($filled / $total) * 100) : 0;
		return [$filled, $total, $pct];
	}

	/**
	 * Prefill fields from existing entry if token/entry_id present; also inject progress UI.
	 */
	function progressive_form_prefill($form) {
		// Try to resolve entry from query args
		$entry = null;
		$entry_id = isset($_GET['e']) ? absint($_GET['e']) : 0;
		$token = isset($_GET['t']) ? sanitize_text_field($_GET['t']) : '';

		if ($entry_id && $token) {
			$t = $this->verify_token($token);
			if ($t && (int)$t['e'] === $entry_id) {
				$maybe_entry = GFAPI::get_entry($entry_id);
				if (!is_wp_error($maybe_entry) && $maybe_entry) {
					$entry = $maybe_entry;
				}
			}
		}

		// Inject progress bar into the first HTML field
		list($filled, $total, $pct) = $this->calculate_completion($form, $entry);
		$progress_html = sprintf(
			'<div id="bw-progress" class="bw-progress-wrap" style="margin:12px 0 20px;">
				<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;">
					<strong>
					  Completion: <span id="bw-progress-text">%d%%</span>
					</strong>
					<label style="font-weight:normal;">
						<input type="checkbox" id="bw-show-completed"> Show completed fields
					</label>
				</div>
				<div style="height:10px;border-radius:6px;background:#eee;margin-top:8px;overflow:hidden;">
					<div id="bw-progress-bar" style="height:10px;width:%d%%;"></div>
				</div>
			</div>',
			$pct, $pct
		);

		foreach ($form['fields'] as &$field) {
			if ($field->type === 'html') {
				$field->content = $progress_html . $this->progress_js();
				break;
			}
		}

		// Prefill defaults from entry
		if ($entry) {
			foreach ($form['fields'] as &$field) {
				if (in_array($field->type, ['html', 'section', 'page', 'captcha', 'password', 'consent'])) continue;

				if ( $field->type === 'fileupload' ) {
					$existing_file = rgar( $entry, (string) $field->id );
					if ( ! empty( $existing_file ) ) {
						$field->cssClass .= ' bw-hidden-fileupload';
						$field->visibility = 'hidden';
						$field->isHidden   = true;
						continue;
					}
				}

				// Multi-input fields (Name, Address, Phone, Email)
				if (!empty($field->inputs)) {
					foreach ($field->inputs as &$input) {
						$input_id = (string)$input['id'];
						$val = rgar($entry, $input_id);

						// Prefill confirm email from main email if empty
						if (($val === null || $val === '') && strpos(strtolower($input['label']), 'confirm') !== false) {
							$val = $this->find_email_value($entry);
						}

						// Prefill address subfields if empty
						if (($val === null || $val === '') && in_array($field->type, ['address'])) {
							$address_parts = ['street', 'city', 'state', 'zip', 'country'];
							foreach ($address_parts as $part) {
								$part_val = rgar($entry, $input_id . '.' . $part);
								if ($part_val !== null && $part_val !== '') {
									$val = $part_val;
									break;
								}
							}
						}

						if ($val !== null && $val !== '') {
							$input['defaultValue'] = $val;
						}
					}
				} else {
					$val = rgar($entry, (string)$field->id);
					if ($val !== null && $val !== '') {
						$field->defaultValue = $val;
					}
				}
			}
		}

		return $form;
	}

	/**
	 * Find the email field's submitted value from an entry (works even if Email is part of a compound field set)
	 */
	function find_email_value($entry) {
		foreach ($entry as $k => $v) {
			if (!is_numeric(str_replace('.', '', (string)$k))) continue;
			// simple heuristic: look for "@"
			if (is_string($v) && strpos($v, '@') !== false) {
				return $v;
			}
		}
		return '';
	}

	/**
	 * Before save: if coming back with entry id+token, update the same entry instead of creating a new one
	 * Also stash entry id in $_POST for the next filter.
	 */
	function mark_entry_for_update($form) {
		if (isset($_GET['e'], $_GET['t'])) {
			$entry_id = absint($_GET['e']);
			$token = sanitize_text_field($_GET['t']);
			$t = $this->verify_token($token);
			if ($t && (int)$t['e'] === $entry_id) {
				$_POST['bw_update_entry_id'] = $entry_id;
			} else if ($entry_id) {
				$_POST['bw_update_entry_id'] = $entry_id;
			}
		}
		return $form;
	}

	/**
	 * Force GF to update an existing entry when bw_update_entry_id is present
	 */
	function force_update_existing_entry($entry_id, $form) {
		if (!empty($_POST['bw_update_entry_id'])) {
			return absint($_POST['bw_update_entry_id']);
		}
		return $entry_id;
	}

	/**
	 * After submit: decide whether to loop back (not complete) or go to thank-you (complete).
	 */
	function progressive_confirmation($confirmation, $form, $entry, $ajax) {
		list($filled, $total, $pct) = $this->calculate_completion($form, $entry);

		// if ($pct >= 100) {
		// 	// 100% complete -> thank-you page
		// 	// $confirmation = [
		// 	// 	'redirect' => 'https://example.com/thank-you' // <-- your URL
		// 	// ];
		// } else {
		if ($pct < 100) {
			// Not complete -> redirect back to the same form page with signed token
			$token = $this->sign_token($entry['id']);
			// Use current page (or a dedicated "complete your application" page)
			$return_url = add_query_arg([
				'e' => (int)$entry['id'],
				't' => rawurlencode($token),
				'r' => time()
			], remove_query_arg(['gf_token', 'no_cache'])); // clean extras

			$confirmation = [ 'redirect' => $return_url ];
		}

		return $confirmation;
	}

	/**
	 * Small JS/CSS to hide completed fields and keep the progress bar updated on the client too.
	 * (Server percent is the source of truth; this just improves UX while editing.)
	 */
	function progress_js() {
		ob_start(); ?>
		<style>
			#bw-progress-bar { background: #3b82f6; transition: width .25s ease; }
			.bw-hidden-fileupload,
			.bw-hide-completed .bw-completed { display: none !important; }
			.bw-hide-completed .bw-completed.gfield_error { display: block !important; }
			.bw-field { transition: opacity .15s ease; }
		</style>
		<script>
			(function(){
				function showCompleted(){
					const cb = document.getElementById('bw-show-completed');
					return cb && cb.checked;
				}

				// Recursive function to check if any input/select/textarea inside a container has value
				function hasValue(container){
					const inputs = container.querySelectorAll('input, select, textarea');
					return Array.from(inputs).some(input => {
						if (!input || input.type === 'hidden') return false;
						if (input.type === 'checkbox' || input.type === 'radio') {
							const group = container.querySelectorAll('[name="'+input.name+'"]');
							return Array.from(group).some(i => i.checked);
						}
						if (input.tagName.toLowerCase() === 'select') {
							return input.value && input.value.trim().length > 0;
						}
						return (input.value || '').trim().length > 0;
					});
				}

				function debounce(func, delay) {
					let timeoutId;
					return function(...args) {
						const context = this;
						clearTimeout(timeoutId);
						timeoutId = setTimeout(() => {
							func.apply(context, args);
						}, delay);
					};
				}

				let initialUpdate = true;

				function _updateUI(){
					const fields = Array.from(document.querySelectorAll('.gform_wrapper form .gfield'));
					let total = 0, filled = 0;

					fields.forEach(f => {
						// Skip non-user fields or hidden by conditional logic
						if (f.classList.contains('bw-optional-field') ||
							f.classList.contains('gfield--type-html') ||
							f.classList.contains('gsection') ||
							f.classList.contains('gform_hidden') ||
							f.classList.contains('gfield_visibility_hidden') ||
							f.classList.contains('gfield--type-honeypot') ||
							f.getAttribute('data-conditional-logic') === 'hidden') return;

						f.classList.add('bw-field');
						total++;

						const done = hasValue(f);
						if (done) filled++;

						if (initialUpdate) {
							f.classList.toggle('bw-completed', done);
						}
						if (!done)console.log(f)
					});


					if (initialUpdate) {
						initialUpdate = false;
					}

					document.querySelector('.gform_wrapper').classList.toggle('bw-hide-completed', !showCompleted());

					const pct = total ? Math.round((filled / total) * 100) : 0;
					const bar = document.getElementById('bw-progress-bar');
					const txt = document.getElementById('bw-progress-text');

					if (bar) bar.style.width = pct + '%';
					if (txt) txt.textContent = pct + '%';
				}

				const updateUI = debounce(_updateUI, 50);

				document.addEventListener('input', updateUI, true);
				document.addEventListener('change', updateUI, true);

				// document.addEventListener('change', function(e){
				// 	if (e.target && e.target.id === 'bw-show-completed'){
				// 		const fields = document.querySelectorAll('.gform_wrapper form .bw-field');
				// 		fields.forEach(f=>{
				// 			const done = hasValue(f);
				// 			f.classList.toggle('bw-completed', done && !showCompleted());
				// 		});
				// 	}
				// }, true);

				window.addEventListener('load', updateUI);
			})();
		</script>
		<?php
		return ob_get_clean();
	}

}

new BW_Gform_Partial_Entries();
