<?php
/**
 * Plugin Name: Group Lang & SEO • Permaliens hiérarchiques + Langue cohérente + Hreflang + OG/Twitter + LocalBusiness & Recipe JSON-LD (multi-sites)
 * Description: Un seul plugin pour tous les sites du groupe (THE 38 BAR, THE 46 BAR, THE ENGLISHMAN…). Détection automatique par domaine.
 * Version: 2.3.1
 * Author: Cyril Signature
 */

declare(strict_types=1);
if (!defined('ABSPATH')) exit;

/* ================================
 * Polyfill PHP < 8 pour str_ends_with
 * ================================ */
if (!function_exists('str_ends_with')) {
	function str_ends_with(string $haystack, string $needle): bool {
		if ($needle === '') return true;
		$len = strlen($needle);
		return $len <= strlen($haystack) && substr($haystack, -$len) === $needle;
	}
}

final class GLS_GroupLangSEO {
	private static array $GLS_DOMAIN_MAP = [
		'the38bar.com' => [
			'langs'       => ['page_parents' => ['fr'=>'fr','en'=>'en'], 'post_cats' => ['fr'=>'fr','en'=>'en'], 'locales'=>['fr'=>'fr_FR','en'=>'en_US']],
			'twitter'     => ['enabled'=>true, 'handle'=>''],
			'jsonld'      => ['enabled'=>true],
			'business'    => [
				'name'=>'THE 38 BAR','url'=>'','image'=>'/wp-content/uploads/COCKTAILS.jpg',
				'telephone'=>'+33 1 86 22 24 38','street'=>'38 Rue René Boulanger','postal'=>'75010','city'=>'Paris','country'=>'FR',
				'serves'=>['Cocktails','Tapas'],'priceRange'=>'€€',
				'sameAs'=>['https://www.instagram.com/the_38_bar/','https://www.facebook.com/le38republique/'],
				'hasMenu'=>'https://the38bar.com/fr/cocktail/','acceptsResa'=>true,'reservationUrl'=>'https://the38bar.com/fr/contact/',
				'opening'=>[[ 'days'=>['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'], 'opens'=>'17:00','closes'=>'02:00' ]],
			],
		],
		'barcocktailparis.com' => [
			'langs'=>['page_parents'=>['fr'=>'fr','en'=>'en'], 'post_cats'=>['fr'=>'fr','en'=>'en'], 'locales'=>['fr'=>'fr_FR','en'=>'en_US']],
			'twitter'=>['enabled'=>true,'handle'=>''],
			'jsonld'=>['enabled'=>true],
			'business'=>[
				'name'=>'THE 38 BAR','url'=>'','image'=>'/wp-content/uploads/COCKTAILS.jpg',
				'telephone'=>'+33 1 86 22 24 38','street'=>'38 Rue René Boulanger','postal'=>'75010','city'=>'Paris','country'=>'FR',
				'serves'=>['Cocktails','Tapas'],'priceRange'=>'€€',
				'sameAs'=>['https://www.instagram.com/the_38_bar/','https://www.facebook.com/le38republique/'],
				'hasMenu'=>'https://barcocktailparis.com/fr/cocktail/','acceptsResa'=>true,'reservationUrl'=>'https://barcocktailparis.com/fr/contact/',
				'opening'=>[[ 'days'=>['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'], 'opens'=>'17:00','closes'=>'02:00' ]],
			],
		],
		'the46bar.com' => [
			'langs'=>['page_parents'=>['fr'=>'fr','en'=>'en'], 'post_cats'=>['fr'=>'fr','en'=>'en'], 'locales'=>['fr'=>'fr_FR','en'=>'en_US']],
			'twitter'=>['enabled'=>true,'handle'=>''],
			'jsonld'=>['enabled'=>true],
			'business'=>[
				'name'=>'THE 46 BAR','url'=>'','image'=>'/wp-content/uploads/hero.jpg',
				'telephone'=>'+33 9 51 26 29 12','street'=>'46 Rue René Boulanger','postal'=>'75010','city'=>'Paris','country'=>'FR',
				'serves'=>['Cocktails','Tapas'],'priceRange'=>'€€',
				'sameAs'=>['https://www.instagram.com/the46bar/','https://www.facebook.com/TheFortySixBar'],
				'hasMenu'=>'https://the46bar.com/fr/cocktail/','acceptsResa'=>true,'reservationUrl'=>'https://the46bar.com/fr/contact/',
				'opening'=>[[ 'days'=>['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'], 'opens'=>'17:00','closes'=>'02:00' ]],
			],
		],
		'theenglishman.fr' => [
			'langs'=>['page_parents'=>['fr'=>'fr','en'=>'en'], 'post_cats'=>['fr'=>'fr','en'=>'en'], 'locales'=>['fr'=>'fr_FR','en'=>'en_US']],
			'twitter'=>['enabled'=>true,'handle'=>''],
			'jsonld'=>['enabled'=>true],
			'business'=>[
				'name'=>'THE ENGLISHMAN','url'=>'','image'=>'/wp-content/uploads/hero.jpg',
				'telephone'=>'+33 1 86 22 24 37','street'=>'38 Rue René Boulanger','postal'=>'75010','city'=>'Paris','country'=>'FR',
				'serves'=>['Cocktails'],'priceRange'=>'€€',
				'sameAs'=>['https://www.instagram.com/theenglishmancocktailclub/','https://www.tripadvisor.fr/Restaurant_Review-g187147-d26364317-Reviews-The_Englishman_Cocktail_Club-Paris_Ile_de_France.html'],
				'hasMenu'=>'https://theenglishman.fr/fr/cocktail/','acceptsResa'=>true,'reservationUrl'=>'https://theenglishman.fr/fr/contact/',
				'opening'=>[[ 'days'=>['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'], 'opens'=>'17:00','closes'=>'02:00' ]],
			],
		],
	];

	private const META_ALT_URL        = 'the_alt_url';
	private const META_ALT_HREF       = 'the_alt_hreflang';
	// Doit être public car utilisée dans les callbacks admin en dehors de la classe.
	public const META_INGREDIENTS    = 'gls_recipe_ingredients';
	private const VERSION             = '2.3.1';

	private static function host(): string {
		$h = $_SERVER['HTTP_HOST'] ?? '';
		return strtolower(preg_replace('/:\d+$/', '', $h));
	}

	private static function cfg(): array {
		$host = self::host();
		if (isset(self::$GLS_DOMAIN_MAP[$host])) {
			return apply_filters('gls_config', self::$GLS_DOMAIN_MAP[$host], $host);
		}
		$default = [
			'langs'=>['page_parents'=>['fr'=>'fr','en'=>'en'],'post_cats'=>['fr'=>'fr','en'=>'en'],'locales'=>['fr'=>'fr_FR','en'=>'en_US']],
			'twitter'=>['enabled'=>true,'handle'=>''],
			'jsonld'=>['enabled'=>true],
			'business'=>[
				'name'=>get_bloginfo('name'),'url'=>'','image'=>'','telephone'=>'','street'=>'','postal'=>'','city'=>'','country'=>'FR',
				'serves'=>[],'priceRange'=>'€€','sameAs'=>[],'hasMenu'=>'','acceptsResa'=>true,'reservationUrl'=>'','opening'=>[]
			],
		];
		return apply_filters('gls_config', $default, $host);
	}

	private static function abs_url(string $u): string {
		if ($u === '' || preg_match('#^https?://#i', $u)) return $u;
		if ($u[0] === '/') return rtrim(home_url('/'), '/') . $u;
		return $u;
	}

	public static function init(): void {
		add_action('init', [__CLASS__,'enable_page_excerpt'], 5);
		add_action('init', [__CLASS__,'hierarchical_page_permalinks'], 9);
		add_action('init', [__CLASS__,'maybe_flush_rules'], 99);
		add_filter('determine_locale', [__CLASS__, 'filter_determine_locale'], 20);
		add_filter('language_attributes', [__CLASS__, 'filter_language_attributes'], 20);

		add_action('wp_head', [__CLASS__, 'print_meta_og'], 80);
		add_action('wp_head', [__CLASS__, 'print_twitter_cards'], 82);
		add_action('wp_head', [__CLASS__, 'print_hreflang'], 90);

		// Recipe JSON-LD (avant LocalBusiness pour lier l'@id author)
		add_action('wp_head', [__CLASS__, 'print_jsonld_recipe_for_posts'], 94);

		add_action('wp_head', [__CLASS__, 'print_jsonld_localbusiness'], 95);
	}

	public static function enable_page_excerpt(): void { add_post_type_support('page', 'excerpt'); }

	public static function hierarchical_page_permalinks(): void {
		global $wp_rewrite;
		$wp_rewrite->page_structure = $wp_rewrite->root . '%pagename%/';
		$wp_rewrite->use_trailing_slashes = true;
	}

	public static function maybe_flush_rules(): void {
		$key = 'gls_flush_v_' . self::VERSION . '_' . md5(self::host());
		if (!get_site_transient($key)) {
			flush_rewrite_rules(false);
			set_site_transient($key, '1', 12 * HOUR_IN_SECONDS);
		}
	}

	private static function detect_page_lang_from_hierarchy(int $post_id): ?string {
		$post = get_post($post_id);
		if (!$post || $post->post_type !== 'page') return null;
		$anc = get_post_ancestors($post_id);
		$top_id = $post_id;
		if (!empty($anc)) $top_id = end($anc);
		$top = get_post($top_id); if (!$top) return null;
		$parents = self::cfg()['langs']['page_parents'] ?? ['fr'=>'fr','en'=>'en'];
		$top_slug = $top->post_name;
		foreach ($parents as $code => $slug) {
			if ($top_slug === $slug) return $code;
		}
		return null;
	}

	private static function detect_post_lang_from_category(int $post_id): ?string {
		$terms = get_the_terms($post_id, 'category');
		if (is_wp_error($terms) || empty($terms)) return null;
		$cats = self::cfg()['langs']['post_cats'] ?? ['fr'=>'fr','en'=>'en'];
		foreach ($terms as $t) {
			foreach ($cats as $code => $slug) {
				if ($t->slug === $slug) return $code;
			}
		}
		return null;
	}

	private static function current_lang_code(): ?string {
		if (is_page())  {
			$o = get_queried_object();
			if ($o && !empty($o->ID)) return self::detect_page_lang_from_hierarchy((int)$o->ID);
		}
		if (is_single()){
			$o = get_queried_object();
			if ($o && !empty($o->ID)) return self::detect_post_lang_from_category((int)$o->ID);
		}
		return null;
	}

	private static function locale_for_code(?string $code): ?string {
		$locs = self::cfg()['langs']['locales'] ?? ['fr'=>'fr_FR','en'=>'en_US'];
		return ($code && isset($locs[$code])) ? $locs[$code] : null;
	}

	private static function xdefault_url(): string { return trailingslashit(home_url('/')); }

	public static function filter_determine_locale(string $locale): string {
		if (is_admin() || is_feed() || wp_doing_cron() || wp_doing_ajax()) return $locale;
		$code = self::current_lang_code();
		$loc  = self::locale_for_code($code);
		return $loc ?: $locale;
	}

	public static function filter_language_attributes(string $output): string {
		if (is_admin() || is_feed()) return $output;
		$code = self::current_lang_code();
		$lang = $code === 'en' ? 'en-US' : ($code === 'fr' ? 'fr-FR' : null);
		if ($lang) {
			$out = preg_replace('/\blang="[^"]*"/', 'lang="' . esc_attr($lang) . '"', $output);
			if (!preg_match('/\blang="/', $out)) {
				$out .= ' lang="' . esc_attr($lang) . '"';
			}
			return $out;
		}
		return $output;
	}

	private static function try_autoderive_alternate_url(): ?string {
		$o = get_queried_object(); if (!$o || empty($o->ID)) return null;
		$code = self::current_lang_code(); if ($code !== 'fr' && $code !== 'en') return null;
		$other = ($code === 'fr') ? 'en' : 'fr';
		$perma = get_permalink((int)$o->ID);
		$parents = self::cfg()['langs']['page_parents'] ?? ['fr'=>'fr','en'=>'en'];
		$pattern = '#/(' . implode('|', array_map(static function($v){ return preg_quote($v, '#'); }, array_values($parents))) . ')/#';
		$target  = $parents[$other] ?? $other;
		$alt_candidate = preg_replace($pattern, '/'.$target.'/', $perma, 1);
		if (!$alt_candidate || $alt_candidate === $perma) return null;
		$post_id = url_to_postid($alt_candidate);
		return ($post_id > 0) ? $alt_candidate : null;
	}

	public static function print_hreflang(): void {
		if (is_admin() || is_feed()) return;
		$o = get_queried_object(); if (!$o || empty($o->ID)) return;

		$current_url = get_permalink((int)$o->ID);
		$code = self::current_lang_code();
		if ($code === 'fr' || $code === 'en') {
			printf('<link rel="alternate" hreflang="%s" href="%s" />' . PHP_EOL, esc_attr($code), esc_url($current_url));
		}

		$alt_raw = get_post_meta((int)$o->ID, self::META_ALT_URL, true);
		$alt = is_string($alt_raw) ? trim($alt_raw) : '';
		if ($alt === '') {
			$auto = self::try_autoderive_alternate_url();
			if ($auto) $alt = $auto;
		}
		if ($alt !== '') {
			$alt_url = esc_url($alt);
			if ($alt_url && preg_match('#^https?://#i', $alt_url)) {
				$hreflang_override = get_post_meta((int)$o->ID, self::META_ALT_HREF, true);
				$hreflang = $hreflang_override ? sanitize_text_field($hreflang_override) : null;
				if (!$hreflang) $hreflang = ($code === 'fr') ? 'en' : (($code === 'en') ? 'fr' : 'en');
				printf('<link rel="alternate" hreflang="%s" href="%s" />' . PHP_EOL, esc_attr($hreflang), esc_url($alt_url));
			}
		}
		printf('<link rel="alternate" hreflang="x-default" href="%s" />' . PHP_EOL, esc_url(self::xdefault_url()));
	}

	private static function build_description(): ?string {
		$o = get_queried_object(); if (!$o || empty($o->ID)) return null;
		$ex = get_the_excerpt((int)$o->ID);
		if (is_string($ex)) {
			$ex = trim(wp_strip_all_tags(strip_shortcodes($ex)));
			if ($ex !== '') return mb_substr($ex, 0, 500);
		}
		$yoast = get_post_meta((int)$o->ID, '_yoast_wpseo_metadesc', true);
		if (is_string($yoast)) {
			$yoast = trim(wp_strip_all_tags(strip_shortcodes($yoast)));
			if ($yoast !== '') return mb_substr($yoast, 0, 500);
		}
		$content = get_post_field('post_content', (int)$o->ID);
		if (!is_string($content) || trim($content) === '') return null;
		$content = strip_shortcodes($content);
		$content = wp_strip_all_tags($content);
		$content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, get_bloginfo('charset') ?: 'UTF-8');
		$content = preg_replace('/\s+/u', ' ', $content);
		$content = trim($content);
		return $content === '' ? null : mb_substr($content, 0, 500);
	}

	private static function build_og_image(): ?array {
		$o = get_queried_object(); if (!$o || empty($o->ID)) return null;
		if (has_post_thumbnail((int)$o->ID)) {
			$img_id = get_post_thumbnail_id((int)$o->ID);
			$src = wp_get_attachment_image_src($img_id, 'full');
			if ($src && !empty($src[0])) {
				$mime = get_post_mime_type($img_id) ?: null;
				return ['url'=>esc_url($src[0]), 'width'=>$src[1]??null, 'height'=>$src[2]??null, 'type'=>$mime];
			}
		}
		$icon_id = get_option('site_icon');
		if ($icon_id) {
			$icon = wp_get_attachment_image_src((int)$icon_id, 'full');
			if ($icon && !empty($icon[0])) {
				return ['url'=>esc_url($icon[0]), 'width'=>$icon[1]??null, 'height'=>$icon[2]??null, 'type'=>'image/png'];
			}
		}
		$biz = self::cfg()['business'] ?? [];
		if (!empty($biz['image'])) {
			$abs = self::abs_url((string)$biz['image']);
			return ['url'=>esc_url($abs), 'width'=>null, 'height'=>null, 'type'=>'image/jpeg'];
		}
		return null;
	}

	private static function build_social_image_alt(): ?string {
		$title = wp_get_document_title();
		$title = is_string($title) ? trim($title) : '';
		return $title !== '' ? wp_strip_all_tags($title, true) : null;
	}

	public static function print_meta_og(): void {
		if (is_admin() || is_feed()) return;
		$o = get_queried_object(); if (!$o || empty($o->ID)) return;

		$desc = self::build_description();
		if ($desc) printf('<meta name="description" content="%s" />' . PHP_EOL, esc_attr($desc));

		$title = wp_get_document_title();
		$url   = get_permalink((int)$o->ID);
		$code  = self::current_lang_code();
		$loc   = self::locale_for_code($code) ?: get_locale();
		$og_type = is_single() ? 'article' : 'website';

		$current_og_locale = str_replace('-', '_', $loc);
		printf('<meta property="og:locale" content="%s" />' . PHP_EOL, esc_attr($current_og_locale));
		printf('<meta property="og:type" content="%s" />' . PHP_EOL, esc_attr($og_type));
		printf('<meta property="og:site_name" content="%s" />' . PHP_EOL, esc_attr(get_bloginfo('name')));
		printf('<meta property="og:title" content="%s" />' . PHP_EOL, esc_attr($title));
		if ($desc) printf('<meta property="og:description" content="%s" />' . PHP_EOL, esc_attr($desc));
		printf('<meta property="og:url" content="%s" />' . PHP_EOL, esc_url($url));

		$all_locales = self::cfg()['langs']['locales'] ?? [];
		if (is_array($all_locales) && !empty($all_locales)) {
			foreach ($all_locales as $code_key => $locale_val) {
				if (!is_string($locale_val) || $locale_val === '') continue;
				$alt = str_replace('-', '_', $locale_val);
				if ($alt === $current_og_locale) continue;
				printf('<meta property="og:locale:alternate" content="%s" />' . PHP_EOL, esc_attr($alt));
			}
		}

		$img = self::build_og_image();
		if ($img) {
			printf('<meta property="og:image" content="%s" />' . PHP_EOL, esc_url($img['url']));
			if (!empty($img['width']))  printf('<meta property="og:image:width" content="%d" />' . PHP_EOL, (int)$img['width']);
			if (!empty($img['height'])) printf('<meta property="og:image:height" content="%d" />' . PHP_EOL, (int)$img['height']);
			$mime = !empty($img['type']) ? $img['type'] : 'image/jpeg';
			printf('<meta property="og:image:type" content="%s" />' . PHP_EOL, esc_attr($mime));
			$alt = self::build_social_image_alt();
			if ($alt) printf('<meta property="og:image:alt" content="%s" />' . PHP_EOL, esc_attr($alt));
		}

		// Enrichissement article:* pour les posts
		self::print_og_article_extras();
	}

	// article:* (dates, section, tags) uniquement pour is_single()
	private static function print_og_article_extras(): void {
		if (!is_single()) return;
		$post_id = (int) get_queried_object_id();

		$published = get_post_time('c', true, $post_id);
		$modified  = get_post_modified_time('c', true, $post_id);

		if ($published) printf('<meta property="article:published_time" content="%s" />' . PHP_EOL, esc_attr($published));
		if ($modified)  printf('<meta property="article:modified_time" content="%s" />' . PHP_EOL, esc_attr($modified));
		if ($modified)  printf('<meta property="og:updated_time" content="%s" />' . PHP_EOL, esc_attr($modified));

		// Section = 1ère catégorie non-langue; fallback "Cocktail"
		$section = null;
		$cats = get_the_category($post_id);
		if (!empty($cats) && !is_wp_error($cats)) {
			foreach ($cats as $c) {
				$slug = strtolower($c->slug);
				if ($slug === 'fr' || $slug === 'en') continue;
				$section = $c->name;
				break;
			}
		}
		if (!$section) $section = 'Cocktail';
		printf('<meta property="article:section" content="%s" />' . PHP_EOL, esc_attr($section));

		$tags = get_the_tags($post_id);
		if ($tags && !is_wp_error($tags)) {
			foreach ($tags as $t) printf('<meta property="article:tag" content="%s" />' . PHP_EOL, esc_attr($t->name));
		}
	}

	public static function print_twitter_cards(): void {
		if (is_admin() || is_feed()) return;
		$tw = self::cfg()['twitter'] ?? ['enabled'=>true,'handle'=>''];
		if (empty($tw['enabled'])) return;

		$o = get_queried_object(); if (!$o || empty($o->ID)) return;

		$title = wp_get_document_title();
		$desc  = self::build_description();
		$img   = self::build_og_image();

		echo '<meta name="twitter:card" content="summary_large_image" />' . PHP_EOL;
		if (!empty($tw['handle'])) {
			printf('<meta name="twitter:site" content="%s" />' . PHP_EOL, esc_attr($tw['handle']));
			printf('<meta name="twitter:creator" content="%s" />' . PHP_EOL, esc_attr($tw['handle']));
		}
		printf('<meta name="twitter:title" content="%s" />' . PHP_EOL, esc_attr($title));
		if ($desc) printf('<meta name="twitter:description" content="%s" />' . PHP_EOL, esc_attr($desc));
		if ($img && !empty($img['url'])) {
			printf('<meta name="twitter:image" content="%s" />' . PHP_EOL, esc_url($img['url']));
			$alt = self::build_social_image_alt();
			if ($alt) printf('<meta name="twitter:image:alt" content="%s" />' . PHP_EOL, esc_attr($alt));
		}
	}

	/* === JSON-LD Recipe pour les ARTICLES (cocktails) === */

	private static function extract_ingredients_from_content(string $html): array {
		$ingredients = [];

		// a) <li>...</li>
		if (preg_match_all('/<li[^>]*>(.*?)<\/li>/is', $html, $m)) {
			foreach ($m[1] as $raw) {
				$txt = wp_strip_all_tags($raw, true);
				$txt = trim(preg_replace('/\s+/u', ' ', $txt));
				if ($txt !== '') $ingredients[] = $txt;
			}
		}

		// b) fallback : lignes commençant par - * •
		if (empty($ingredients)) {
			$plain = wp_strip_all_tags($html, true);
			$plain = str_replace(["\r\n", "\r"], "\n", $plain);
			foreach (explode("\n", $plain) as $line) {
				$line = trim($line);
				if ($line === '') continue;
				if (preg_match('/^(\-|\*|•)\s+(.*)$/u', $line, $mm)) {
					$item = trim($mm[2]);
					if ($item !== '') $ingredients[] = $item;
				}
			}
		}

		// c) filtre
		$ingredients = array_values(array_unique(array_filter($ingredients, static function ($x) {
			return mb_strlen($x) >= 2;
		})));
		return $ingredients;
	}

	private static function extract_ingredients_meta(int $post_id): array {
		$raw = get_post_meta($post_id, self::META_INGREDIENTS, true);
		if (!is_string($raw) || trim($raw) === '') return [];
		$lines = preg_split('/\r\n|\r|\n/', $raw);
		$out = [];
		foreach ($lines as $l) {
			$l = trim(wp_strip_all_tags($l, true));
			if ($l !== '') $out[] = $l;
		}
		return array_values(array_unique($out));
	}

	public static function print_jsonld_recipe_for_posts(): void {
		if (is_admin() || is_feed() || !is_single()) return;

		$post_id = (int) get_queried_object_id();
		$content = (string) get_post_field('post_content', $post_id);

		// Priorité à la meta explicite
		$ingredients = self::extract_ingredients_meta($post_id);
		$is_recipe = !empty($ingredients);

		// Sinon heuristique contenu
		if (!$is_recipe && $content !== '') {
			$is_recipe = (bool) preg_match('/\b(Ingredients|Ingrédients)\b/i', $content) || preg_match('/<li\b/i', $content);
			if ($is_recipe) $ingredients = self::extract_ingredients_from_content($content);
		}
		if (!$is_recipe || empty($ingredients)) return;

		$title   = wp_get_document_title();
		$url     = get_permalink($post_id);
		$img     = self::build_og_image();
		$imgUrl  = $img['url'] ?? '';
		$datePublished = get_post_time('c', true, $post_id);
		$dateModified  = get_post_modified_time('c', true, $post_id);

		$site_url = home_url('/');
		$org_id   = trailingslashit($site_url) . '#org';

		$data = [
			'@context'         => 'https://schema.org',
			'@type'            => 'Recipe',
			'@id'              => $url . '#recipe',
			'name'             => wp_strip_all_tags((string) $title, true),
			'url'              => $url,
			'image'            => $imgUrl ? [$imgUrl] : null,
			'author'           => ['@type'=>'Organization', '@id'=>$org_id, 'name'=> get_bloginfo('name')],
			'datePublished'    => $datePublished ?: null,
			'dateModified'     => $dateModified  ?: null,
			'recipeCategory'   => 'Cocktail',
			'recipeCuisine'    => 'Bar',
			'recipeIngredient' => $ingredients,
			'description'      => self::build_description() ?: null,
		];

		$data = array_filter($data, static fn($v) => !($v === null || $v === '' || $v === []));
		echo '<script type="application/ld+json">'. wp_json_encode($data, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE) .'</script>' . PHP_EOL;
	}

	/* === JSON-LD LocalBusiness — version localisée FR/EN pour menu & réservation === */
	public static function print_jsonld_localbusiness(): void {
		if (is_admin() || is_feed()) return;

		$cfg = self::cfg();
		$enable = $cfg['jsonld']['enabled'] ?? true;
		if (!$enable) return;

		$biz = $cfg['business'] ?? [];
		$url = !empty($biz['url']) ? self::abs_url((string)$biz['url']) : home_url('/');

		// Langue courante détectée par la hiérarchie/catégorie
		$code = self::current_lang_code(); // 'fr' | 'en' | null

		// Normalise une URL (FR↔EN) si possible
		$localize = static function (?string $u) use ($code): ?string {
			if (!$u || !preg_match('#^https?://#i', $u)) return $u;
			if ($code === 'en') {
				$u = preg_replace('#/(fr|en)/#', '/en/', $u, 1);
			} elseif ($code === 'fr') {
				$u = preg_replace('#/(fr|en)/#', '/fr/', $u, 1);
			}
			return $u;
		};

		$hasMenu        = !empty($biz['hasMenu']) ? self::abs_url((string)$biz['hasMenu']) : '';
		$reservationUrl = !empty($biz['reservationUrl']) ? self::abs_url((string)$biz['reservationUrl']) : '';

		$hasMenu        = $localize($hasMenu);
		$reservationUrl = $localize($reservationUrl);

		$site_url = home_url('/');
		$org_id   = trailingslashit($site_url) . '#org';

		$data = [
			'@context'  => 'https://schema.org',
			'@type'     => 'BarOrPub',
			'@id'       => $org_id, // ID stable, référencé par Recipe.author
			'name'      => $biz['name'] ?? get_bloginfo('name'),
			'url'       => $url,
			'image'     => !empty($biz['image']) ? self::abs_url((string)$biz['image']) : '',
			'telephone' => $biz['telephone'] ?? '',
			'address'   => [
				'@type'           => 'PostalAddress',
				'streetAddress'   => $biz['street']  ?? '',
				'postalCode'      => $biz['postal']  ?? '',
				'addressLocality' => $biz['city']    ?? '',
				'addressCountry'  => $biz['country'] ?? 'FR',
			],
			'servesCuisine' => $biz['serves'] ?? [],
			'priceRange'    => $biz['priceRange'] ?? '€€',
			'sameAs'        => $biz['sameAs'] ?? [],
		];

		if (!empty($hasMenu))           $data['hasMenu'] = $hasMenu;
		if (isset($biz['acceptsResa'])) $data['acceptsReservations'] = (bool)$biz['acceptsResa'];
		if (!empty($reservationUrl))    $data['reservationUrl'] = $reservationUrl;

		if (!empty($biz['opening']) && is_array($biz['opening'])) {
			$oh = [];
			foreach ($biz['opening'] as $row) {
				$oh[] = [
					'@type'     => 'OpeningHoursSpecification',
					'dayOfWeek' => $row['days']  ?? [],
					'opens'     => $row['opens'] ?? '',
					'closes'    => $row['closes']?? '',
				];
			}
			if ($oh) $data['openingHoursSpecification'] = $oh;
		}

		echo '<script type="application/ld+json">'. wp_json_encode($data, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE) .'</script>' . PHP_EOL;
	}
}

/* ============================================================
 * UI d’édition des méta SANS ACF : méta-box natif WP (posts & pages)
 * ============================================================ */

if (is_admin() && !function_exists('acf_add_local_field_group')) {

	add_action('add_meta_boxes', static function () {
		add_meta_box(
			'gls_alt_lang_box',
			'Langue & SEO — URL alternative',
			function ($post) {
				$alt_url  = get_post_meta($post->ID, 'the_alt_url', true);
				$alt_href = get_post_meta($post->ID, 'the_alt_hreflang', true);
				wp_nonce_field('gls_alt_lang_save', 'gls_alt_lang_nonce');
				?>
				<style>
					.gls-field { margin-bottom: 10px; }
					.gls-field label { display:block; font-weight:600; margin-bottom:4px; }
					.gls-field input[type="url"], .gls-field select, .gls-field textarea { width:100%; max-width:100%; }
					.gls-help { color:#666; font-size:12px; margin-top:4px; }
				</style>

				<div class="gls-field">
					<label for="gls_alt_url">URL de la page équivalente</label>
					<input type="url" id="gls_alt_url" name="gls_alt_url"
						placeholder="https://the38bar.com/en/contact/"
						value="<?php echo esc_attr(is_string($alt_url) ? $alt_url : ''); ?>">
					<p class="gls-help">Colle une URL complète (http/https). Laisse vide si la page miroir peut être auto-déduite.</p>
				</div>

				<div class="gls-field">
					<label for="gls_alt_hreflang">Code hreflang de cette URL</label>
					<select id="gls_alt_hreflang" name="gls_alt_hreflang">
						<option value="">(déduction automatique)</option>
						<option value="fr" <?php selected($alt_href, 'fr'); ?>>fr (Français)</option>
						<option value="en" <?php selected($alt_href, 'en'); ?>>en (English)</option>
					</select>
					<p class="gls-help">Laisse vide pour auto (fr ↔ en).</p>
				</div>

				<?php
				if ($post->post_type === 'post') {
					$ingredients = get_post_meta($post->ID, GLS_GroupLangSEO::META_INGREDIENTS, true);
					?>
					<div class="gls-field">
						<label for="gls_recipe_ingredients">Ingrédients (un par ligne)</label>
						<textarea id="gls_recipe_ingredients" name="gls_recipe_ingredients" rows="6" placeholder="Tequila 40 ml&#10;Calvados 10 ml&#10;St-Germain infusion 10 ml&#10;Cinnamon&#10;Pear&#10;Apple&#10;Pineapple&#10;Grapefruit Shrub&#10;Egg white (allergen)"><?php
							echo esc_textarea(is_string($ingredients) ? $ingredients : '');
						?></textarea>
						<p class="gls-help">Si rempli, génère un JSON-LD <code>Recipe</code> même si le contenu ne contient pas de liste.</p>
					</div>
					<?php
				}
			},
			['page', 'post'],
			'side',
			'default'
		);
	});

	add_action('save_post', static function (int $post_id) {
		if (!isset($_POST['gls_alt_lang_nonce']) || !wp_verify_nonce($_POST['gls_alt_lang_nonce'], 'gls_alt_lang_save')) return;
		if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;

		$post_type = get_post_type($post_id);
		if ($post_type === 'page' && !current_user_can('edit_page', $post_id)) return;
		if ($post_type !== 'page' && !current_user_can('edit_post', $post_id)) return;

		// Alt URL
		$raw_url = isset($_POST['gls_alt_url']) ? trim((string) $_POST['gls_alt_url']) : '';
		$val_url = '';
		if ($raw_url !== '') {
			$san = esc_url_raw($raw_url);
			if (preg_match('#^https?://#i', $san)) $val_url = $san;
		}
		update_post_meta($post_id, 'the_alt_url', $val_url);

		// Alt hreflang
		$raw_hreflang = isset($_POST['gls_alt_hreflang']) ? trim((string) $_POST['gls_alt_hreflang']) : '';
		$val_hreflang = in_array($raw_hreflang, ['fr','en'], true) ? $raw_hreflang : '';
		if ($val_hreflang === '') delete_post_meta($post_id, 'the_alt_hreflang');
		else update_post_meta($post_id, 'the_alt_hreflang', $val_hreflang);

		// Ingrédients (posts uniquement)
		if ($post_type === 'post') {
			$raw_ing = isset($_POST['gls_recipe_ingredients']) ? (string) $_POST['gls_recipe_ingredients'] : '';
			$raw_ing = trim($raw_ing);
			if ($raw_ing === '') {
				delete_post_meta($post_id, GLS_GroupLangSEO::META_INGREDIENTS);
			} else {
				update_post_meta($post_id, GLS_GroupLangSEO::META_INGREDIENTS, wp_kses_post($raw_ing));
			}
		}
	});
}

/* ============================================================
 * ACF (si présent) : on expose les mêmes métas
 * ============================================================ */
add_action('acf/init', static function () {
	if (!function_exists('acf_add_local_field_group')) return;

	acf_add_local_field_group([
		'key' => 'group_gls_alt_lang',
		'title' => 'Langue & SEO — URL alternative',
		'fields' => [
			[
				'key' => 'field_gls_alt_url',
				'label' => 'URL de la page équivalente',
				'name' => 'the_alt_url',
				'type' => 'url',
				'instructions' => 'URL complète (ex : https://the38bar.com/en/contact/)',
				'required' => 0,
				'placeholder' => 'https://the38bar.com/en/...',
			],
			[
				'key' => 'field_gls_alt_hreflang',
				'label' => 'Code hreflang de cette URL',
				'name' => 'the_alt_hreflang',
				'type' => 'select',
				'choices' => ['fr' => 'fr (Français)', 'en' => 'en (English)'],
				'default_value' => '',
				'allow_null' => 1,
				'ui' => 1,
				'instructions' => 'Laisse vide pour déduction automatique (fr ↔ en)',
			],
			[
				'key' => 'field_gls_recipe_ingredients',
				'label' => 'Ingrédients (un par ligne)',
				'name' => 'gls_recipe_ingredients',
				'type' => 'textarea',
				'instructions' => 'Si rempli sur un article, génère un JSON-LD Recipe. Exemple : "Tequila 40 ml"',
				'rows' => 8,
				'new_lines' => '',
				'placeholder' => "Tequila 40 ml\nCalvados 10 ml\nSt-Germain infusion 10 ml\nCinnamon\nPear\nApple\nPineapple\nGrapefruit Shrub\nEgg white (allergen)",
				'conditional_logic' => [
					[
						[
							'field' => 'acf_field_post_type',
							'operator' => '==',
							'value' => 'post'
						]
					]
				],
			],
		],
		'location' => [
			[ [ 'param' => 'post_type', 'operator' => '==', 'value' => 'page' ] ],
			[ [ 'param' => 'post_type', 'operator' => '==', 'value' => 'post' ] ],
		],
		'position' => 'side',
		'style' => 'seamless',
		'label_placement' => 'top',
		'instruction_placement' => 'label',
		'menu_order' => 99,
		'active' => true,
	]);
});

GLS_GroupLangSEO::init();