<?php
/**
 * Store Locator with geolocation search and distance calculation.
 *
 * @package SEOvaultSEO\LocalSEO
 */

namespace SEOvaultSEO\LocalSEO;

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

class StoreLocator {

	protected static $defaults = array(
		'radius'                  => 10,
		'max_number'              => '',
		'show_radius'             => false,
		'show_nearest_suggestion' => true,
		'show_map'                => true,
		'show_filter'             => false,
		'map_width'               => '100%',
		'scrollable'              => true,
		'draggable'               => true,
		'marker_clustering'       => false,
		'show_country'            => false,
		'show_state'              => false,
		'show_phone'              => false,
		'show_phone_2'            => false,
		'show_fax'                => false,
		'show_email'              => false,
		'show_url'                => false,
		'map_style'               => 'ROADMAP',
		'show_route_label'        => '',
		'oneline'                 => false,
		'show_opening_hours'      => false,
		'hide_closed'             => false,
		'show_category_filter'    => false,
		'width'                   => '100%',
		'height'                  => 300,
		'zoom'                    => -1,
	);

	public static function render( $atts ) {
		$atts = shortcode_atts( self::$defaults, $atts, 'seovault_store_locator' );

		foreach ( $atts as $key => $value ) {
			if ( in_array( $key, array( 'show_radius', 'show_nearest_suggestion', 'show_map', 'show_filter', 'scrollable', 'draggable', 'marker_clustering', 'show_country', 'show_state', 'show_phone', 'show_phone_2', 'show_fax', 'show_email', 'show_url', 'oneline', 'show_opening_hours', 'hide_closed', 'show_category_filter' ), true ) ) {
				$atts[ $key ] = self::cast_bool( $value );
			}
		}

		$pt    = PostType::get_post_type();
		$count = wp_count_posts( $pt );
		$total = (int) ( $count->publish ?? 0 );
		if ( $total < 2 ) {
			return '';
		}

		$options = get_option( 'seovaultseo_local_settings', array() );
		if ( ! is_array( $options ) ) {
			$options = array();
		}

		$unit = $options['unit_system'] ?? 'METRIC';

		if ( '' === $atts['show_route_label'] ) {
			$atts['show_route_label'] = __( 'Show route', 'seovault-ai' );
		}

		$search_nonce      = isset( $_POST['seovaultseo_store_locator_nonce'] )
			? sanitize_text_field( wp_unslash( $_POST['seovaultseo_store_locator_nonce'] ) )
			: '';
		$is_search_request = '' !== $search_nonce
			&& wp_verify_nonce( $search_nonce, 'seovaultseo_store_locator_search' );

		$buffer_level = ob_get_level();
		ob_start();
		try {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only form repopulation; search POST is nonce-verified above.
			?>
		<!--seovault_store_locator_start-->
		<form action="#seovault-storelocator-form" method="post" id="seovault-storelocator-form" data-form="seovault-store-locator">
			<fieldset>
				<?php wp_nonce_field( 'seovaultseo_store_locator_search', 'seovaultseo_store_locator_nonce' ); ?>
				<?php
				$search_string = isset( $_REQUEST['seovault-sl-search'] ) ? esc_attr( sanitize_text_field( wp_unslash( $_REQUEST['seovault-sl-search'] ) ) ) : '';
				$sl_category   = ! empty( $_REQUEST['seovault-sl-category'] ) ? esc_attr( sanitize_text_field( wp_unslash( $_REQUEST['seovault-sl-category'] ) ) ) : '';
				?>
				<p>
					<label for="seovault-sl-search"><?php echo esc_html__( 'Enter your postal code, city and / or state', 'seovault-ai' ); ?></label>
					<input type="text" name="seovault-sl-search" id="seovault-sl-form-search" value="<?php echo esc_attr( $search_string ); ?>">
					<input type="hidden" name="seovault-sl-lat" id="seovault-sl-form-lat" value="">
					<input type="hidden" name="seovault-sl-lng" id="seovault-sl-form-lng" value="">

					<?php if ( $atts['show_radius'] ) : ?>
						<?php esc_html_e( 'within', 'seovault-ai' ); ?>
						<select name="seovault-sl-radius" id="seovault-sl-radius">
							<?php
							$radius_array    = array( 1, 5, 10, 25, 50, 100, 250, 500, 1000 );
							$selected_radius = ! empty( $_REQUEST['seovault-sl-radius'] ) ? esc_attr( sanitize_text_field( wp_unslash( $_REQUEST['seovault-sl-radius'] ) ) ) : esc_attr( $atts['radius'] );
							foreach ( $radius_array as $r ) {
								echo '<option value="' . (int) $r . '" ' . selected( $selected_radius, (string) $r, false ) . '>' . (int) $r . ( 'METRIC' === $unit ? 'km' : 'mi' ) . '</option>';
							}
							?>
						</select>
					<?php else : ?>
						<input type="hidden" name="seovault-sl-radius" id="seovault-sl-radius-text" value="<?php echo esc_attr( $atts['radius'] ); ?>">
					<?php endif; ?>
				</p>

				<?php if ( $atts['show_filter'] ) : ?>
					<?php
					$terms = get_terms(
						array(
							'taxonomy'   => PostType::get_taxonomy(),
							'hide_empty' => false,
						)
					);
					if ( ! is_wp_error( $terms ) && count( $terms ) > 0 ) :
						?>
						<p class="sl-filter">
							<label for="seovault-sl-category"><?php esc_html_e( 'Filter by category', 'seovault-ai' ); ?></label>
							<select name="seovault-sl-category" id="seovault-sl-category">
								<option value=""></option>
								<?php foreach ( $terms as $term ) : ?>
									<option value="<?php echo esc_attr( $term->term_id ); ?>" <?php selected( $sl_category, (string) $term->term_id ); ?>><?php echo esc_html( $term->name ); ?></option>
								<?php endforeach; ?>
							</select>
						</p>
					<?php endif; ?>
				<?php endif; ?>

				<p class="sl-submit">
					<input type="submit" value="<?php esc_attr_e( 'Search', 'seovault-ai' ); ?>">
				</p>
			</fieldset>
		</form>

		<div id="seovault-storelocator-results">
			<?php
			$results = false;
			if ( $is_search_request ) {
				$results = self::get_results( $atts, $unit );
			}

			if ( $atts['show_map'] && ! empty( $results ) && ! is_wp_error( $results ) ) {
				$map_ids = array();
				foreach ( $results['locations'] as $loc ) {
					$map_ids[] = $loc['ID'];
				}
				$map_atts = array(
					'id'         => implode( ',', $map_ids ),
					'width'      => $atts['width'],
					'height'     => $atts['height'],
					'zoom'       => $atts['zoom'],
					'map_style'  => $atts['map_style'],
					'scrollable' => $atts['scrollable'],
					'draggable'  => $atts['draggable'],
				);
				echo wp_kses_post( Shortcodes::shortcode_map( $map_atts ) );
			}

			if ( $is_search_request ) {
				if ( ! is_wp_error( $results ) && ! empty( $results ) ) {
					if ( $results['in_radius'] > 0 ) {
						$number = count( $results['locations'] );
						echo '<h2>' . sprintf(
							/* translators: %d: number of store locations found. */
							esc_html( _n( '%d result has been found', '%d results have been found', $number, 'seovault-ai' ) ),
							(int) $number
						) . '</h2>';
						foreach ( $results['locations'] as $location ) {
							self::render_location_details( $location, $atts, $unit );
						}
					} else {
						echo '<h2>' . esc_html__( 'No results found', 'seovault-ai' ) . '</h2>';
						if ( $atts['show_nearest_suggestion'] && ! empty( $results['locations'] ) ) {
							foreach ( $results['locations'] as $location ) {
								$distance = (string) self::get_distance_display( (float) $location['distance'], $unit );
								echo '<p class="nearest_location">';
								if ( 'METRIC' === $unit ) {
									/* translators: %s: distance to the nearest store location in kilometers. */
									printf( esc_html__( 'The nearest location is %s kilometers away', 'seovault-ai' ), esc_html( $distance ) );
								} else {
									/* translators: %s: distance to the nearest store location in miles. */
									printf( esc_html__( 'The nearest location is %s miles away', 'seovault-ai' ), esc_html( $distance ) );
								}
								echo '</p>';
								self::render_location_details( $location, $atts, $unit );
							}
						}
					}
				} else {
					echo '<h2>' . esc_html__( 'No results found', 'seovault-ai' ) . '</h2>';
				}
			}
			?>
		</div>
		<!--seovault_store_locator_end-->
			<?php
		// phpcs:enable WordPress.Security.NonceVerification.Recommended
			return ob_get_clean();
		} finally {
			while ( ob_get_level() > $buffer_level ) {
				ob_end_clean();
			}
		}
	}

	protected static function get_results( $atts, $unit ) {
		$nonce = isset( $_POST['seovaultseo_store_locator_nonce'] )
			? sanitize_text_field( wp_unslash( $_POST['seovaultseo_store_locator_nonce'] ) )
			: '';
		if ( '' === $nonce || ! wp_verify_nonce( $nonce, 'seovaultseo_store_locator_search' ) ) {
			return new \WP_Error( 'seovault-invalid-nonce', __( 'Security check failed. Please refresh the page and try again.', 'seovault-ai' ) );
		}

		$request = array(
			'search'   => isset( $_POST['seovault-sl-search'] ) ? sanitize_text_field( wp_unslash( $_POST['seovault-sl-search'] ) ) : '',
			'radius'   => isset( $_POST['seovault-sl-radius'] ) ? sanitize_text_field( wp_unslash( $_POST['seovault-sl-radius'] ) ) : '999999',
			'category' => isset( $_POST['seovault-sl-category'] ) ? sanitize_text_field( wp_unslash( $_POST['seovault-sl-category'] ) ) : '',
			'lat'      => isset( $_POST['seovault-sl-lat'] ) ? (float) $_POST['seovault-sl-lat'] : 0.0,
			'lng'      => isset( $_POST['seovault-sl-lng'] ) ? (float) $_POST['seovault-sl-lng'] : 0.0,
		);

		if ( '' === $request['search'] ) {
			return new \WP_Error( 'seovault-no-input', __( 'Please enter a zipcode or city', 'seovault-ai' ) );
		}

		$nr_results = self::get_nr_results( $atts );

		$locations = self::query_locations( $nr_results, false, $unit, $request );

		if ( 0 === $locations['count'] ) {
			$locations = self::query_locations( 1, true, $unit, $request );
		}

		return array(
			'in_radius' => $locations['count'],
			'locations' => $locations['results'],
		);
	}

	protected static function get_nr_results( $atts ) {
		$value   = 10;
		$options = get_option( 'seovaultseo_local_settings', array() );
		if ( ! empty( $options['sl_num_results'] ) ) {
			$value = (int) $options['sl_num_results'];
		}
		if ( ! empty( $atts['max_number'] ) && (int) $atts['max_number'] <= $value ) {
			$value = (int) $atts['max_number'];
		}
		return $value;
	}

	protected static function query_locations( $nr_results, $outside_radius, $unit, array $request ) {
		global $wpdb;

		$radius        = $request['radius'];
		$sl_category   = $request['category'];
		$search_string = $request['search'];
		if ( '' === $search_string ) {
			return array(
				'count'   => 0,
				'results' => array(),
			);
		}

		$lat = $request['lat'];
		$lng = $request['lng'];

		$replacements = array( $lat, $lng, $lat );

		$inner_join = '';
		$pt         = PostType::get_post_type();
		$tax        = PostType::get_taxonomy();

		if ( '' !== $sl_category ) {
			$inner_join .= $wpdb->prepare(
				" INNER JOIN {$wpdb->term_relationships} AS tr ON p.ID = tr.object_id
				INNER JOIN {$wpdb->term_taxonomy} AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
				AND tt.taxonomy = %s AND tt.term_id = %d",
				$tax,
				absint( $sl_category )
			);
		}

		$replacements[] = $pt;

		$post_statuses = array( 'publish' );
		if ( is_user_logged_in() && current_user_can( 'edit_posts' ) ) {
			$post_statuses = array( 'publish', 'draft', 'future', 'pending', 'private' );
		}
		$replacements = array_merge( $replacements, $post_statuses );

		$lat_key        = LocationMeta::META_PREFIX . 'coordinates_lat';
		$lng_key        = LocationMeta::META_PREFIX . 'coordinates_long';
		$replacements[] = $lat_key;
		$replacements[] = $lng_key;

		$distance_value = self::convert_distance( (float) $radius, $unit );
		$having         = '';
		if ( ! $outside_radius ) {
			$having         = 'HAVING distance < %d';
			$replacements[] = $distance_value;
		}

		$replacements[] = $nr_results;

		$status_placeholders = implode( ',', array_fill( 0, count( $post_statuses ), '%s' ) );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- JOIN/HAVING/status IN clauses built from prepared fragments and whitelisted values.
		$sql = $wpdb->prepare(
			"SELECT p.ID, m1.meta_value as lat, m2.meta_value as lng,
			ROUND( ( 6366.564864 * acos ( cos ( radians(%f) ) * cos( radians( m1.meta_value ) ) * cos( radians( m2.meta_value ) - radians(%f) ) + sin ( radians(%f) ) * sin( radians( m1.meta_value ) ) ) ), 2 ) AS distance
			FROM {$wpdb->posts} p
			INNER JOIN {$wpdb->postmeta} m1 ON p.ID = m1.post_id
			INNER JOIN {$wpdb->postmeta} m2 ON p.ID = m2.post_id
			{$inner_join}
			WHERE
				p.post_type = %s AND
				p.post_status IN( {$status_placeholders} ) AND
				m1.meta_key = %s AND
				m2.meta_key = %s
			GROUP BY p.ID, lat, lng
			{$having}
			ORDER BY distance ASC
			LIMIT 0, %d",
			$replacements
		);

		$results = $wpdb->get_results( $sql, ARRAY_A );
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		return array(
			'count'   => $wpdb->num_rows,
			'results' => $results,
		);
	}

	public static function convert_distance( $value, $unit ) {
		if ( 'mi' === $unit ) {
			return $value * 1.60934;
		}
		return $value;
	}

	public static function get_distance_display( $km, $unit ) {
		if ( 'mi' === $unit ) {
			return round( $km * 0.621371, 2 );
		}
		return round( $km, 2 );
	}

	protected static function render_location_details( $location, $atts, $unit ) {
		$post_id  = absint( $location['ID'] );
		$lat      = LocationMeta::get( $post_id, 'coordinates_lat' );
		$lng      = LocationMeta::get( $post_id, 'coordinates_long' );
		$distance = self::get_distance_display( (float) $location['distance'], $unit );

		$loc = LocationMeta::get_location_data_object( $post_id );
		if ( ! $loc ) {
			return;
		}

		?>
		<div class="seovault-sl-result">
			<?php
			$address_atts = array(
				'id'                 => $post_id,
				'show_state'         => $atts['show_state'],
				'show_country'       => $atts['show_country'],
				'show_phone'         => $atts['show_phone'],
				'show_phone_2'       => $atts['show_phone_2'],
				'show_fax'           => $atts['show_fax'],
				'show_email'         => $atts['show_email'],
				'show_url'           => $atts['show_url'],
				'show_opening_hours' => $atts['show_opening_hours'],
				'hide_closed'        => $atts['hide_closed'],
				'oneline'            => $atts['oneline'],
				'hide_json_ld'       => true,
			);
			echo wp_kses_post( Shortcodes::render_address( $loc, $address_atts ) );
			?>
			<div class="seovault-sl-route">
				<a href="<?php echo esc_url( add_query_arg( array( 'api' => '1', 'destination' => $lat . ',' . $lng ), 'https://www.google.com/maps/dir/' ) ); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html( $atts['show_route_label'] ); ?></a>
			</div>
		</div>
		<?php
	}

	protected static function cast_bool( $value ) {
		if ( is_bool( $value ) ) {
			return $value;
		}
		return in_array( (string) $value, array( 'true', '1', 'yes', 'on' ), true );
	}
}
