<?php
/**
 * Livada SEO — Hreflang Module
 *
 * Outputs <link rel="alternate" hreflang="..."> tags in the document head for
 * multilingual sites. Three integrations are supported out of the box:
 *
 *   1. Polylang       — pulls translations via pll_get_post() / pll_get_term().
 *   2. WPML           — pulls translations via the wpml_active_languages filter.
 *   3. TranslatePress — pulls translations via TRP_Url_Converter.
 *
 * Sites without any of those can still configure manual alternates via the
 * `_livada_seo_hreflang` post meta (a JSON / serialized array of
 * { lang => url } pairs).
 *
 * The default site language is always emitted as `x-default` as well, which
 * Google recommends for multi-region sites.
 *
 * Integration filters (for advanced users):
 *   livada_seo_hreflang_enabled                        — return false to disable the whole module
 *   livada_seo_hreflang_replaces_translatepress        — return false to keep TP's own hreflang output (default: true)
 *
 * @package Livada_SEO
 */

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

class Livada_SEO_Hreflang {

	// ── Bootstrap ─────────────────────────────────────────────────────────────

	public static function init(): void {
		// Allow site owners to disable the module entirely (e.g. when their
		// multilingual plugin is configured to handle hreflang itself).
		if ( ! apply_filters( 'livada_seo_hreflang_enabled', true ) ) {
			return;
		}

		add_action( 'wp_head', [ self::class, 'output_tags' ], 2 );

		// TranslatePress emits its own hreflang tags. When Livada is providing
		// them we silence TP's output to avoid duplicate <link rel="alternate">.
		// Set the filter to false to keep TP's native hreflang output instead.
		if ( apply_filters( 'livada_seo_hreflang_replaces_translatepress', true ) ) {
			add_filter( 'trp_disable_hreflang', '__return_true' );
		}
	}

	// ── Output ────────────────────────────────────────────────────────────────

	public static function output_tags(): void {
		$alternates = self::collect_alternates();

		if ( empty( $alternates ) ) {
			return;
		}

		// Always include x-default pointing to the site's default language.
		// Use the first non-empty alternate as fallback when no x-default is set.
		if ( ! isset( $alternates['x-default'] ) ) {
			$first              = reset( $alternates );
			$alternates['x-default'] = $first;
		}

		foreach ( $alternates as $lang => $url ) {
			if ( '' === (string) $url ) {
				continue;
			}

			printf(
				'<link rel="alternate" hreflang="%s" href="%s" />' . "\n",
				esc_attr( self::normalize_lang_code( (string) $lang ) ),
				esc_url( (string) $url )
			);
		}
	}

	// ── Sources ───────────────────────────────────────────────────────────────

	/**
	 * Returns an associative array of `lang => absolute URL` alternates for
	 * the current request, merging Polylang/WPML data with manual post meta.
	 *
	 * @return array<string,string>
	 */
	private static function collect_alternates(): array {
		$alternates = [];

		// 1. Polylang.
		if ( function_exists( 'pll_languages_list' ) && function_exists( 'pll_get_post' ) ) {
			$alternates = self::collect_polylang();
		}

		// 2. WPML — only if Polylang did not already provide data.
		if ( empty( $alternates ) && defined( 'ICL_LANGUAGE_CODE' ) && function_exists( 'icl_object_id' ) ) {
			$alternates = self::collect_wpml();
		}

		// 3. TranslatePress — when no other multilingual plugin handled it.
		if ( empty( $alternates ) && class_exists( 'TRP_Translate_Press' ) ) {
			$alternates = self::collect_translatepress();
		}

		// 4. Manual post-meta override (always merged, takes precedence).
		if ( is_singular() ) {
			$post_id = (int) get_queried_object_id();
			$manual  = get_post_meta( $post_id, '_livada_seo_hreflang', true );

			if ( is_string( $manual ) && '' !== $manual ) {
				$decoded = json_decode( $manual, true );
				if ( is_array( $decoded ) ) {
					$manual = $decoded;
				}
			}

			if ( is_array( $manual ) ) {
				foreach ( $manual as $lang => $url ) {
					$lang = sanitize_text_field( (string) $lang );
					$url  = esc_url_raw( (string) $url );
					if ( '' !== $lang && '' !== $url ) {
						$alternates[ $lang ] = $url;
					}
				}
			}
		}

		return $alternates;
	}

	private static function collect_polylang(): array {
		$alternates = [];
		$languages  = (array) call_user_func( 'pll_languages_list' );

		if ( ! is_singular() && ! is_category() && ! is_tag() && ! is_tax() && ! is_front_page() ) {
			return $alternates;
		}

		foreach ( $languages as $lang_slug ) {
			$lang_slug = (string) $lang_slug;
			$url       = '';

			if ( is_front_page() ) {
				if ( function_exists( 'pll_home_url' ) ) {
					$url = (string) call_user_func( 'pll_home_url', $lang_slug );
				}
			} elseif ( is_singular() ) {
				$translated_id = (int) call_user_func( 'pll_get_post', get_queried_object_id(), $lang_slug );
				if ( $translated_id > 0 ) {
					$url = (string) get_permalink( $translated_id );
				}
			} elseif ( ( is_category() || is_tag() || is_tax() ) && function_exists( 'pll_get_term' ) ) {
				$translated_term = (int) call_user_func( 'pll_get_term', get_queried_object_id(), $lang_slug );
				if ( $translated_term > 0 ) {
					$link = get_term_link( $translated_term );
					if ( ! is_wp_error( $link ) ) {
						$url = (string) $link;
					}
				}
			}

			if ( '' !== $url ) {
				$alternates[ $lang_slug ] = $url;
			}
		}

		// Polylang has a default language — expose it as x-default too.
		if ( function_exists( 'pll_default_language' ) ) {
			$default = (string) call_user_func( 'pll_default_language' );
			if ( '' !== $default && isset( $alternates[ $default ] ) ) {
				$alternates['x-default'] = $alternates[ $default ];
			}
		}

		return $alternates;
	}

	/**
	 * Collects alternates from TranslatePress via TRP_Url_Converter.
	 *
	 * TranslatePress stores its config under the `trp_settings` option:
	 *   - 'default-language'      : default locale (e.g. 'fr_FR')
	 *   - 'translation-languages' : translation locales only
	 *   - 'publish-languages'     : all publicly visible locales (default + translations)
	 *
	 * The URL converter exposes `get_url_for_language( $lang_code, $url )` which
	 * returns the translated URL for a given source URL. Passing the canonical
	 * URL of the current request keeps the alternates in sync with what the
	 * meta module emits as `<link rel="canonical">`.
	 *
	 * @return array<string,string>
	 */
	private static function collect_translatepress(): array {
		if ( ! class_exists( 'TRP_Translate_Press' ) || ! method_exists( 'TRP_Translate_Press', 'get_trp_instance' ) ) {
			return [];
		}

		$trp = TRP_Translate_Press::get_trp_instance();
		if ( ! is_object( $trp ) || ! method_exists( $trp, 'get_component' ) ) {
			return [];
		}

		$url_converter = $trp->get_component( 'url_converter' );
		$settings_obj  = $trp->get_component( 'settings' );

		if ( ! is_object( $url_converter ) || ! method_exists( $url_converter, 'get_url_for_language' ) ) {
			return [];
		}

		// Pull settings; fall back to the raw option if the component doesn't expose them.
		$settings = [];
		if ( is_object( $settings_obj ) && method_exists( $settings_obj, 'get_settings' ) ) {
			$settings = (array) $settings_obj->get_settings();
		}
		if ( empty( $settings ) ) {
			$settings = (array) get_option( 'trp_settings', [] );
		}

		$languages = isset( $settings['publish-languages'] ) && is_array( $settings['publish-languages'] )
			? $settings['publish-languages']
			: [];

		// Some TP versions expose only translation-languages; merge with default.
		if ( empty( $languages ) ) {
			$default      = isset( $settings['default-language'] ) ? (string) $settings['default-language'] : '';
			$translations = isset( $settings['translation-languages'] ) && is_array( $settings['translation-languages'] )
				? $settings['translation-languages']
				: [];
			$languages = array_values( array_filter( array_merge( [ $default ], $translations ) ) );
		}

		if ( empty( $languages ) ) {
			return [];
		}

		// Resolve the source URL for the current request. We mirror canonical
		// resolution from the Meta module so hreflang and canonical agree.
		$source_url = self::current_canonical_url();
		if ( '' === $source_url ) {
			return [];
		}

		$alternates = [];

		foreach ( $languages as $lang_code ) {
			$lang_code = (string) $lang_code;
			if ( '' === $lang_code ) {
				continue;
			}

			$url = '';
			try {
				$url = (string) $url_converter->get_url_for_language( $lang_code, $source_url );
			} catch ( \Throwable $e ) {
				$url = '';
			}

			if ( '' !== $url ) {
				$alternates[ $lang_code ] = $url;
			}
		}

		// x-default points to the default language URL.
		$default = isset( $settings['default-language'] ) ? (string) $settings['default-language'] : '';
		if ( '' !== $default && isset( $alternates[ $default ] ) ) {
			$alternates['x-default'] = $alternates[ $default ];
		}

		return $alternates;
	}

	/**
	 * Returns the canonical URL of the current request, used as the source URL
	 * passed to multilingual plugins for translation lookups.
	 */
	private static function current_canonical_url(): string {
		if ( is_singular() ) {
			$id = (int) get_queried_object_id();
			return $id > 0 ? (string) get_permalink( $id ) : '';
		}

		if ( is_front_page() || is_home() ) {
			return home_url( '/' );
		}

		if ( is_category() || is_tag() || is_tax() ) {
			$term = get_queried_object();
			if ( $term instanceof WP_Term ) {
				$link = get_term_link( $term );
				if ( ! is_wp_error( $link ) ) {
					return (string) $link;
				}
			}
		}

		// Fallback: the raw request URL (without query string), absolute.
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$raw = isset( $_SERVER['REQUEST_URI'] ) ? (string) $_SERVER['REQUEST_URI'] : '/';
		$path = (string) wp_parse_url( $raw, PHP_URL_PATH );
		return home_url( $path );
	}

	private static function collect_wpml(): array {
		$alternates = [];

		// Use the wpml_active_languages filter to get the URL set.
		$languages = apply_filters( 'wpml_active_languages', [], [ 'skip_missing' => 1 ] );

		if ( ! is_array( $languages ) ) {
			return $alternates;
		}

		foreach ( $languages as $code => $info ) {
			$lang_code = is_array( $info ) && ! empty( $info['language_code'] ) ? (string) $info['language_code'] : (string) $code;
			$url       = is_array( $info ) && ! empty( $info['url'] )           ? (string) $info['url']           : '';

			if ( '' !== $lang_code && '' !== $url ) {
				$alternates[ $lang_code ] = $url;
			}
		}

		return $alternates;
	}

	/**
	 * Normalizes a language code to the BCP 47 form Google expects:
	 *   - x-default stays as-is
	 *   - "fr-FR" stays as-is
	 *   - "fr_FR" → "fr-FR"
	 *   - bare "fr" stays as-is
	 */
	private static function normalize_lang_code( string $code ): string {
		if ( 'x-default' === $code ) {
			return 'x-default';
		}

		$code = str_replace( '_', '-', $code );

		// Lowercase the language part, uppercase the region part.
		if ( false !== strpos( $code, '-' ) ) {
			$parts    = explode( '-', $code, 2 );
			$parts[0] = strtolower( $parts[0] );
			$parts[1] = strtoupper( $parts[1] );
			return implode( '-', $parts );
		}

		return strtolower( $code );
	}
}
