<?php
/**
 * Address → lat/lng geocoder (ADR-11).
 *
 * Two backends, selected by the active map provider:
 *   - Nominatim (OpenStreetMap)  — used when provider == leaflet (free, no key)
 *   - Google Geocoding API       — used when provider == google (uses API key)
 *
 * Runs ONLY when a map provider is enabled: with the default
 * `map_provider = 'none'` no request ever leaves the site — sending dealer
 * addresses to a third party while maps are off would be undisclosed
 * phoning-home (wp.org Guidelines 6/7, 2026-07 review finding). The two
 * services are disclosed in readme.txt "External services".
 *
 * Results are cached in a 30-day transient keyed on sha1(address|backend) so
 * repeated saves don't re-hit the upstream service. Hooks save_post for the
 * dealer CPT at priority 25 (after the meta-box save fires at 12) and writes
 * the resolved coords into _trx_ad_lat / _trx_ad_lng if either is empty.
 *
 * @package ThemeREX\AutoDealer
 */

declare(strict_types=1);

namespace ThemeREX\AutoDealer\Integrations\Maps;

defined( 'ABSPATH' ) || exit;
use ThemeREX\AutoDealer\Admin\AdminSettings;
use WP_Error;

final class Geocoder {

	private const CACHE_PREFIX = 'trx_ad_geo_';
	private const CACHE_TTL    = MONTH_IN_SECONDS;
	private const NOMINATIM    = 'https://nominatim.openstreetmap.org/search';
	private const GOOGLE       = 'https://maps.googleapis.com/maps/api/geocode/json';

	public function register(): void {
		add_action( 'save_post_trx_ad_dealer', array( $this, 'maybeGeocodeDealer' ), 25, 2 );
	}

	/**
	 * Hook handler — geocodes a dealer's address on save when coords are missing.
	 *
	 * @param int      $post_id Dealer post ID.
	 * @param \WP_Post $post    Dealer post.
	 */
	public function maybeGeocodeDealer( int $post_id, $post ): void {
		if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
			return;
		}
		if ( ! $post instanceof \WP_Post || 'trx_ad_dealer' !== $post->post_type ) {
			return;
		}

		$lat = (float) get_post_meta( $post_id, '_trx_ad_lat', true );
		$lng = (float) get_post_meta( $post_id, '_trx_ad_lng', true );

		// Already pinned — respect the manually-set coords.
		if ( 0.0 !== $lat || 0.0 !== $lng ) {
			return;
		}

		$address = $this->collectAddress( $post_id );
		if ( '' === $address ) {
			return;
		}

		$result = $this->geocode( $address );
		if ( is_wp_error( $result ) ) {
			return;
		}

		update_post_meta( $post_id, '_trx_ad_lat', $result['lat'] );
		update_post_meta( $post_id, '_trx_ad_lng', $result['lng'] );
	}

	/**
	 * Resolve an address to coordinates. Cached for 30 days.
	 *
	 * @return array{lat:float,lng:float}|WP_Error
	 */
	public function geocode( string $address ) {
		$address = trim( $address );
		if ( '' === $address ) {
			return new WP_Error( 'trx_ad_geo_empty', __( 'Empty address.', 'trx-auto-dealer' ) );
		}

		// Maps disabled = no geocoding at all. Guard here (not only in the
		// save hook) so REST/CLI/Pro callers can't leak addresses either.
		if ( ! $this->isEnabled() ) {
			return new WP_Error( 'trx_ad_geo_disabled', __( 'Geocoding is disabled while no map provider is selected.', 'trx-auto-dealer' ) );
		}

		$backend = $this->backend();
		$key     = self::CACHE_PREFIX . sha1( $backend . '|' . $address );
		$cached  = get_transient( $key );
		if ( is_array( $cached ) && isset( $cached['lat'], $cached['lng'] ) ) {
			return array(
				'lat' => (float) $cached['lat'],
				'lng' => (float) $cached['lng'],
			);
		}

		/**
		 * Filter the geocoder backend resolver. Tests / Pro extensions
		 * can short-circuit the HTTP call by returning ['lat','lng']
		 * or a WP_Error directly.
		 *
		 * @param mixed  $preempt Default null. Return array|WP_Error to skip HTTP.
		 * @param string $address Trimmed address.
		 * @param string $backend "nominatim" or "google".
		 */
		$preempt = apply_filters( 'trx_ad_geocode_preempt', null, $address, $backend );
		if ( is_array( $preempt ) && isset( $preempt['lat'], $preempt['lng'] ) ) {
			$result = array(
				'lat' => (float) $preempt['lat'],
				'lng' => (float) $preempt['lng'],
			);
			set_transient( $key, $result, self::CACHE_TTL );
			return $result;
		}
		if ( is_wp_error( $preempt ) ) {
			return $preempt;
		}

		$result = 'google' === $backend
			? $this->googleLookup( $address )
			: $this->nominatimLookup( $address );

		if ( is_wp_error( $result ) ) {
			// Negative-cache failures briefly so a bad address doesn't hammer
			// the upstream service on every save.
			set_transient( $key, array( 'error' => $result->get_error_code() ), HOUR_IN_SECONDS );
			return $result;
		}

		set_transient( $key, $result, self::CACHE_TTL );
		return $result;
	}

	/**
	 * @return array{lat:float,lng:float}|WP_Error
	 */
	private function nominatimLookup( string $address ) {
		$url = add_query_arg(
			array(
				'q'              => $address,
				'format'         => 'json',
				'limit'          => 1,
				'addressdetails' => 0,
			),
			self::NOMINATIM
		);

		$response = wp_remote_get(
			$url,
			array(
				'timeout' => 10,
				'headers' => array(
					// Nominatim's usage policy requires a descriptive User-Agent
					// identifying the application; site URL satisfies this.
					'User-Agent' => 'ThemeREX-Auto-Dealer/' . ( defined( 'TRX_AD_VERSION' ) ? TRX_AD_VERSION : '1.0' ) . ' (' . home_url() . ')',
				),
			)
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		if ( $code < 200 || $code >= 300 ) {
			return new WP_Error( 'trx_ad_geo_http', sprintf( 'Nominatim returned HTTP %d', $code ) );
		}

		$body = json_decode( (string) wp_remote_retrieve_body( $response ), true );
		if ( ! is_array( $body ) || empty( $body[0]['lat'] ) || empty( $body[0]['lon'] ) ) {
			return new WP_Error( 'trx_ad_geo_no_match', __( 'Address not found.', 'trx-auto-dealer' ) );
		}

		return array(
			'lat' => (float) $body[0]['lat'],
			'lng' => (float) $body[0]['lon'],
		);
	}

	/**
	 * @return array{lat:float,lng:float}|WP_Error
	 */
	private function googleLookup( string $address ) {
		$key = trim( (string) AdminSettings::get( 'google_maps_api_key', '' ) );
		if ( '' === $key ) {
			return new WP_Error( 'trx_ad_geo_no_key', __( 'Google Maps API key missing.', 'trx-auto-dealer' ) );
		}

		$url = add_query_arg(
			array(
				'address' => $address,
				'key'     => $key,
			),
			self::GOOGLE
		);

		$response = wp_remote_get( $url, array( 'timeout' => 10 ) );
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		if ( $code < 200 || $code >= 300 ) {
			return new WP_Error( 'trx_ad_geo_http', sprintf( 'Google Geocoding returned HTTP %d', $code ) );
		}

		$body = json_decode( (string) wp_remote_retrieve_body( $response ), true );
		$loc  = $body['results'][0]['geometry']['location'] ?? null;
		if ( ! is_array( $loc ) || ! isset( $loc['lat'], $loc['lng'] ) ) {
			$status = is_array( $body ) ? (string) ( $body['status'] ?? '' ) : '';
			return new WP_Error(
				'trx_ad_geo_no_match',
				'' !== $status ? sprintf( 'Google geocoding status: %s', $status ) : __( 'Address not found.', 'trx-auto-dealer' )
			);
		}

		return array(
			'lat' => (float) $loc['lat'],
			'lng' => (float) $loc['lng'],
		);
	}

	private function isEnabled(): bool {
		$provider = (string) AdminSettings::get( 'map_provider', 'none' );
		return in_array( $provider, array( 'leaflet', 'google' ), true );
	}

	private function backend(): string {
		$provider = (string) AdminSettings::get( 'map_provider', 'none' );
		if ( 'google' === $provider ) {
			return 'google';
		}
		return 'nominatim';
	}

	private function collectAddress( int $post_id ): string {
		$parts = array(
			(string) get_post_meta( $post_id, '_trx_ad_street_address', true ),
			(string) get_post_meta( $post_id, '_trx_ad_city', true ),
			(string) get_post_meta( $post_id, '_trx_ad_state', true ),
			(string) get_post_meta( $post_id, '_trx_ad_postcode', true ),
			(string) get_post_meta( $post_id, '_trx_ad_country', true ),
		);
		$parts = array_map( 'trim', $parts );
		$parts = array_filter(
			$parts,
			static function ( string $part ): bool {
				return '' !== $part;
			}
		);
		return implode( ', ', $parts );
	}
}
