<?php
/**
 * Frontend Breadcrumbs
 *
 * Full breadcrumb trail builder with configurable separator, prefix,
 * home label, taxonomy hierarchy, shortcode support, theme integration
 * hooks, and BreadcrumbList JSON-LD schema output.
 *
 * @package SEOvaultSEO\Frontend
 */

namespace SEOvaultSEO\Frontend;

use SEOvaultSEO\Core\Options;
use SEOvaultSEO\Core\PostMeta;

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

class Breadcrumbs {

	/**
	 * Singleton instance.
	 *
	 * @var Breadcrumbs|null
	 */
	private static $instance = null;

	/**
	 * Breadcrumb trail items.
	 *
	 * @var array
	 */
	private $crumbs = array();

	/**
	 * Whether the trail has been generated.
	 *
	 * @var bool
	 */
	private $generated = false;

	/**
	 * Resolved settings.
	 *
	 * @var array
	 */
	private $settings = array();

	/**
	 * Resolved label strings.
	 *
	 * @var array
	 */
	private $strings = array();

	/**
	 * Get singleton instance.
	 *
	 * @return Breadcrumbs|null Null if disabled.
	 */
	public static function get() {
		if ( null === self::$instance ) {
			if ( ! self::is_enabled() ) {
				return null;
			}
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Check if breadcrumbs are enabled.
	 *
	 * @return bool
	 */
	public static function is_enabled() {
		if ( current_theme_supports( 'seovault-breadcrumbs' ) ) {
			return true;
		}

		return (bool) Options::get( 'breadcrumbs_enabled', true );
	}

	/**
	 * Constructor — loads settings and strings.
	 */
	private function __construct() {
		$this->settings = wp_parse_args(
			apply_filters( 'seovaultseo_breadcrumb_settings', array() ),
			array(
				'show_home'       => (bool) Options::get( 'breadcrumbs_show_home', true ),
				'show_pagination' => true,
				'remove_title'    => (bool) Options::get( 'breadcrumbs_remove_post_title', false ),
				'show_ancestors'  => (bool) Options::get( 'breadcrumbs_show_ancestors', false ),
				'show_blog'       => (bool) Options::get( 'breadcrumbs_show_blog', false ),
				'hide_tax_name'   => (bool) Options::get( 'breadcrumbs_hide_taxonomy_name', false ),
			)
		);

		$this->strings = wp_parse_args(
			apply_filters( 'seovaultseo_breadcrumb_strings', array() ),
			array(
				'prefix'         => Options::get( 'breadcrumbs_prefix', '' ),
				'home'           => Options::get( 'breadcrumbs_home_label', __( 'Home', 'seovault-ai' ) ),
				'home_link'      => Options::get( 'breadcrumbs_home_link', home_url() ),
				'error404'       => Options::get( 'breadcrumbs_404_label', __( '404 Error: page not found', 'seovault-ai' ) ),
				/* translators: %s: search query text. */
				'search_format'  => Options::get( 'breadcrumbs_search_format', __( 'Results for %s', 'seovault-ai' ) ),
				/* translators: %s: archive title. */
				'archive_format' => Options::get( 'breadcrumbs_archive_format', __( 'Archives for %s', 'seovault-ai' ) ),
			)
		);
	}

	/**
	 * Initialize hooks.
	 */
	public static function init() {
		add_shortcode( 'seovault_breadcrumb', array( __CLASS__, 'shortcode' ) );

		/**
		 * Filter whether legacy third-party breadcrumb shortcode aliases are registered.
		 *
		 * Disabled by default to avoid claiming shortcode names owned by other SEO
		 * plugins. Even when enabled, an existing shortcode callback is preserved.
		 *
		 * @param bool $register_legacy_aliases Whether to register legacy aliases.
		 */
		$register_legacy_aliases = (bool) apply_filters( 'seovaultseo_register_legacy_breadcrumb_shortcodes', false );
		if ( $register_legacy_aliases ) {
			if ( ! shortcode_exists( 'wpseo_breadcrumb' ) ) {
				// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Opt-in Yoast shortcode compatibility alias.
				add_shortcode( 'wpseo_breadcrumb', array( __CLASS__, 'shortcode' ) );
			}
			if ( ! shortcode_exists( 'aioseo_breadcrumbs' ) ) {
				// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Opt-in AIOSEO shortcode compatibility alias.
				add_shortcode( 'aioseo_breadcrumbs', array( __CLASS__, 'shortcode' ) );
			}
		}

		// Auto-inject before content if enabled and SEO output is active.
		if ( \SEOvaultSEO\Compatibility\Detector::should_output_seo() && Options::get( 'breadcrumbs_auto_inject', false ) ) {
			add_filter( 'the_content', array( __CLASS__, 'maybe_inject_content' ), 9 );
		}
	}

	/**
	 * Shortcode callback.
	 *
	 * @param array $args Shortcode attributes.
	 * @return string
	 */
	public static function shortcode( $args = array() ) {
		if ( ! \SEOvaultSEO\Compatibility\Detector::should_output_seo() ) {
			return '';
		}

		$instance = self::get();
		if ( ! $instance ) {
			return '';
		}

		return $instance->render( $args );
	}

	/**
	 * Maybe inject breadcrumbs before post content.
	 *
	 * @param string $content Post content.
	 * @return string
	 */
	public static function maybe_inject_content( $content ) {
		if ( ! is_singular() || is_front_page() ) {
			return $content;
		}

		$instance = self::get();
		if ( ! $instance ) {
			return $content;
		}

		return $instance->render() . $content;
	}

	/**
	 * Render breadcrumbs as HTML.
	 *
	 * @param array $args {
	 *     Optional. Rendering arguments.
	 *     @type string $separator   Separator between items.
	 *     @type string $wrap_before Opening wrapper HTML.
	 *     @type string $wrap_after  Closing wrapper HTML.
	 *     @type string $before      HTML before each item.
	 *     @type string $after       HTML after each item.
	 * }
	 * @return string
	 */
	public function render( $args = array() ) {
		$args = wp_parse_args(
			$args,
			array(
				'separator'   => Options::get( 'breadcrumbs_separator', '&raquo;' ),
				'wrap_before' => '<nav aria-label="breadcrumbs" class="seovault-breadcrumb"><p>',
				'wrap_after'  => '</p></nav>',
				'before'      => '',
				'after'       => '',
			)
		);

		$args = apply_filters( 'seovaultseo_breadcrumb_args', $args );

		$crumbs = $this->get_crumbs();

		$remove_title = ( is_single() || is_page() ) && $this->settings['remove_title'];
		if ( $remove_title && ! empty( $crumbs ) ) {
			array_pop( $crumbs );
		}

		$html = '';
		$size = count( $crumbs );

		if ( ! empty( $this->strings['prefix'] ) ) {
			$html .= sprintf( '<span class="label">%s</span> ', esc_html( $this->strings['prefix'] ) );
		}

		foreach ( $crumbs as $key => $crumb ) {
			$is_last  = ( $size === $key + 1 );
			$has_link = ! empty( $crumb[1] ) && ( $remove_title || ! $is_last );

			if ( $has_link ) {
				$link = '<a href="' . esc_url( $crumb[1] ) . '">' . esc_html( $crumb[0] ) . '</a>';
			} else {
				$link = '<span class="last">' . esc_html( $crumb[0] ) . '</span>';
			}

			$html .= wp_kses_post( $args['before'] ) . $link . wp_kses_post( $args['after'] );

			if ( ! $is_last ) {
				$html .= '<span class="separator"> ' . wp_kses_post( $args['separator'] ) . ' </span>';
			}
		}

		$html = wp_kses_post( $args['wrap_before'] ) . $html . wp_kses_post( $args['wrap_after'] );

		/**
		 * Filter rendered breadcrumb HTML.
		 *
		 * The value returned from this filter is passed through wp_kses_post()
		 * immediately before output. Filtered HTML is restricted to WordPress
		 * post-content markup. Script, style, iframe, and event-handler markup
		 * are not supported.
		 *
		 * @param string      $html             Breadcrumb markup.
		 * @param array       $crumbs           Trail items.
		 * @param Breadcrumbs $crumbs_instance  Renderer instance.
		 */
		$html = (string) apply_filters( 'seovaultseo_breadcrumb_html', $html, $crumbs, $this );

		return wp_kses_post( $html );
	}

	/**
	 * Get the breadcrumb trail items.
	 *
	 * @return array Each item is [label, url, hide_in_schema].
	 */
	public function get_crumbs() {
		if ( ! $this->generated ) {
			$this->generate();
		}

		return apply_filters( 'seovaultseo_breadcrumb_items', $this->crumbs, $this );
	}

	/**
	 * Get the breadcrumb trail as BreadcrumbList schema.
	 *
	 * @param string $canonical Canonical URL for @id.
	 * @return array|false Schema array or false if no crumbs.
	 */
	public function get_schema( $canonical = '' ) {
		$crumbs = $this->get_crumbs();
		if ( empty( $crumbs ) ) {
			return false;
		}

		$entity = array(
			'@type'           => 'BreadcrumbList',
			'@id'             => ( $canonical ?: $this->get_canonical() ) . '#breadcrumb',
			'itemListElement' => array(),
		);

		$position = 1;
		foreach ( $crumbs as $crumb ) {
			if ( ! empty( $crumb['hide_in_schema'] ) || empty( $crumb[1] ) ) {
				continue;
			}

			$entity['itemListElement'][] = array(
				'@type'    => 'ListItem',
				'position' => $position,
				'name'     => $crumb[0],
				'item'     => $crumb[1],
			);

			++$position;
		}

		if ( empty( $entity['itemListElement'] ) ) {
			return false;
		}

		return apply_filters( 'seovaultseo_breadcrumb_schema', $entity );
	}

	/**
	 * Add a crumb to the trail.
	 *
	 * @param string $name           Crumb label.
	 * @param string $url            Crumb URL.
	 * @param bool   $hide_in_schema Skip in JSON-LD output.
	 */
	private function add_crumb( $name, $url = '', $hide_in_schema = false ) {
		$this->crumbs[] = array(
			wp_strip_all_tags( $name ),
			$url,
			'hide_in_schema' => $hide_in_schema,
		);
	}

	/**
	 * Generate the breadcrumb trail based on the current page.
	 */
	private function generate() {
		$this->generated = true;

		$this->maybe_add_home_crumb();

		if ( ! $this->can_generate() ) {
			return;
		}

		$conditionals = array(
			'is_home',
			'is_404',
			'is_search',
			'is_attachment',
			'is_shop',
			'is_product',
			'is_singular',
			'is_product_category',
			'is_product_tag',
			'is_post_type_archive',
			'is_category',
			'is_tag',
			'is_tax',
			'is_date',
			'is_author',
		);

		foreach ( $conditionals as $conditional ) {
			if ( function_exists( $conditional ) && call_user_func( $conditional ) ) {
				$method = 'add_crumbs_' . substr( $conditional, 3 );
				if ( method_exists( $this, $method ) ) {
					call_user_func( array( $this, $method ) );
				}
				break;
			}
		}

		$this->maybe_add_page_crumb();
	}

	/**
	 * Whether the trail can be generated for the current page.
	 *
	 * @return bool
	 */
	private function can_generate() {
		if ( is_front_page() && ! is_paged() ) {
			return false;
		}

		if ( function_exists( 'wc_get_page_id' ) && is_post_type_archive() ) {
			$shop_id = wc_get_page_id( 'shop' );
			if ( intval( get_option( 'page_on_front' ) ) === $shop_id && ! is_paged() ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Add the home crumb if enabled.
	 */
	private function maybe_add_home_crumb() {
		if ( ! empty( $this->settings['show_home'] ) ) {
			$this->add_crumb( $this->strings['home'], $this->strings['home_link'] );
		}
	}

	/**
	 * Add a "Page N" crumb for paginated archives.
	 */
	private function maybe_add_page_crumb() {
		if ( empty( $this->settings['show_pagination'] ) || ! is_paged() ) {
			return;
		}

		$current_page = (int) get_query_var( 'paged', 1 );
		if ( $current_page <= 1 ) {
			return;
		}

		$this->add_crumb(
			sprintf(
				/* translators: %s: current page number. */
				esc_html__( 'Page %s', 'seovault-ai' ),
				$current_page
			),
			'',
			true
		);
	}

	/**
	 * Blog home (posts page) trail.
	 */
	private function add_crumbs_home() {
		$this->add_crumb( single_post_title( '', false ) );
	}

	/**
	 * 404 trail.
	 */
	private function add_crumbs_404() {
		$this->add_crumb( $this->strings['error404'] );
	}

	/**
	 * Search results trail.
	 */
	private function add_crumbs_search() {
		$this->add_crumb(
			$this->replace_placeholder( $this->strings['search_format'], get_search_query() ),
			$this->remove_query_arg( 'paged' )
		);
	}

	/**
	 * Attachment trail.
	 */
	private function add_crumbs_attachment() {
		global $post;

		if ( $post->post_parent ) {
			$this->add_crumbs_singular( $post->post_parent, get_permalink( $post->post_parent ) );
		}
		$this->add_crumb(
			$this->get_crumb_title( 'post', get_the_ID(), get_the_title() ),
			get_permalink()
		);
	}

	/**
	 * Single product trail (WooCommerce).
	 */
	private function add_crumbs_product() {
		global $post;

		$this->prepend_shop_page();

		$primary_tax = $this->get_primary_taxonomy( 'product' );
		if ( $primary_tax ) {
			$this->maybe_add_primary_term( get_the_terms( $post->ID, $primary_tax ) );
		}

		if ( isset( $post->ID ) ) {
			$this->add_crumb(
				$this->get_crumb_title( 'post', $post->ID, get_the_title( $post ) ),
				get_permalink( $post )
			);
		}
	}

	/**
	 * Singular (single post/page) trail.
	 *
	 * @param int    $post_id   Post ID (0 for current).
	 * @param string $permalink Permalink override.
	 */
	private function add_crumbs_singular( $post_id = 0, $permalink = '' ) {
		$post      = $post_id ? get_post( $post_id ) : $GLOBALS['post'];
		$post_type = get_post_type( $post );
		$permalink = $permalink ?: get_permalink( $post );

		$this->add_crumbs_post_type_archive( $post_type );

		if ( ! isset( $post->ID ) || empty( $post->ID ) ) {
			return;
		}

		$this->maybe_add_blog();

		$primary_tax = $this->get_primary_taxonomy( $post_type );
		if ( isset( $post->post_parent ) && 0 === $post->post_parent && $primary_tax ) {
			$this->maybe_add_primary_term( get_the_terms( $post, $primary_tax ) );
		}

		if ( isset( $post->post_parent ) && 0 !== $post->post_parent ) {
			$this->add_post_ancestors( $post );
		}

		$this->add_crumb(
			$this->get_crumb_title( 'post', $post->ID, get_the_title( $post ) ),
			$permalink
		);
	}

	/**
	 * Product category trail (WooCommerce).
	 */
	private function add_crumbs_product_category() {
		$term = $GLOBALS['wp_query']->get_queried_object();
		$this->prepend_shop_page();
		$this->maybe_add_term_ancestors( $term );
		$this->add_crumb(
			$this->get_crumb_title( 'term', $term, $term->name ),
			get_term_link( $term )
		);
	}

	/**
	 * Product tag trail (WooCommerce).
	 */
	private function add_crumbs_product_tag() {
		$term = $GLOBALS['wp_query']->get_queried_object();
		$this->prepend_shop_page();
		$this->add_crumb(
			sprintf(
				/* translators: %s: product tag name. */
				__( 'Products tagged &ldquo;%s&rdquo;', 'seovault-ai' ),
				$this->get_crumb_title( 'term', $term, $term->name )
			),
			get_term_link( $term )
		);
	}

	/**
	 * Shop page trail (WooCommerce).
	 */
	private function add_crumbs_shop() {
		$shop_id = function_exists( 'wc_get_page_id' ) ? wc_get_page_id( 'shop' ) : false;
		if ( $shop_id && intval( get_option( 'page_on_front' ) ) === $shop_id ) {
			return;
		}

		$name = $shop_id ? $this->get_crumb_title( 'post', $shop_id, get_the_title( $shop_id ) ) : '';
		if ( ! $name ) {
			$pto  = get_post_type_object( 'product' );
			$name = $pto ? $pto->labels->singular_name : '';
		}
		$this->add_crumb( $name, get_post_type_archive_link( 'product' ) );
	}

	/**
	 * Post type archive trail.
	 *
	 * @param string|null $post_type Post type slug.
	 */
	private function add_crumbs_post_type_archive( $post_type = null ) {
		if ( ! $post_type ) {
			$post_type = $GLOBALS['wp_query']->get( 'post_type' );
		}

		if ( 'post' === $post_type ) {
			return;
		}

		$pto = get_post_type_object( $post_type );
		if ( $pto && ! empty( $pto->has_archive ) ) {
			$this->add_crumb( $pto->labels->name, get_post_type_archive_link( $post_type ) );
		}
	}

	/**
	 * Category archive trail.
	 */
	private function add_crumbs_category() {
		$this->maybe_add_blog();
		$term = $GLOBALS['wp_query']->get_queried_object();
		if ( empty( $term ) ) {
			return;
		}
		$this->maybe_add_term_ancestors( $term );
		$this->add_crumb(
			$this->get_crumb_title( 'term', $term, $term->name ),
			get_term_link( $term )
		);
	}

	/**
	 * Tag archive trail.
	 */
	private function add_crumbs_tag() {
		$this->maybe_add_blog();
		$term = $GLOBALS['wp_query']->get_queried_object();
		$this->add_crumb(
			$this->get_crumb_title( 'term', $term, $term->name ),
			get_term_link( $term )
		);
	}

	/**
	 * Custom taxonomy archive trail.
	 */
	private function add_crumbs_tax() {
		$term = $GLOBALS['wp_query']->get_queried_object();

		if ( ! $this->settings['hide_tax_name'] && $term ) {
			$taxonomy = get_taxonomy( $term->taxonomy );
			if ( $taxonomy ) {
				$this->add_crumb( $taxonomy->labels->name );
			}
		}

		$this->maybe_add_term_ancestors( $term );
		$this->add_crumb(
			$this->get_crumb_title( 'term', $term, $term->name ),
			get_term_link( $term )
		);
	}

	/**
	 * Date archive trail.
	 */
	private function add_crumbs_date() {
		if ( is_year() || is_month() || is_day() ) {
			$this->add_crumb(
				$this->replace_placeholder( $this->strings['archive_format'], get_the_time( 'Y' ) ),
				get_year_link( get_the_time( 'Y' ) )
			);
		}
		if ( is_month() || is_day() ) {
			$this->add_crumb(
				$this->replace_placeholder( $this->strings['archive_format'], get_the_time( 'F' ) ),
				get_month_link( get_the_time( 'Y' ), get_the_time( 'm' ) )
			);
		}
		if ( is_day() ) {
			$this->add_crumb(
				$this->replace_placeholder( $this->strings['archive_format'], get_the_time( 'd' ) )
			);
		}
	}

	/**
	 * Author archive trail.
	 */
	private function add_crumbs_author() {
		$userdata = get_userdata( get_query_var( 'author' ) );
		if ( ! $userdata || ! isset( $userdata->ID ) ) {
			return;
		}

		$this->add_crumb(
			$this->replace_placeholder(
				$this->strings['archive_format'],
				$this->get_crumb_title( 'user', $userdata->ID, $userdata->display_name )
			)
		);
	}

	/**
	 * Add ancestor pages for hierarchical post types.
	 *
	 * @param \WP_Post $post Post object.
	 */
	private function add_post_ancestors( $post ) {
		$ancestors = array();
		if ( isset( $post->ancestors ) ) {
			$ancestors = is_array( $post->ancestors ) ? array_values( $post->ancestors ) : array( $post->ancestors );
		} elseif ( isset( $post->post_parent ) ) {
			$ancestors = array( $post->post_parent );
		}

		if ( ! is_array( $ancestors ) ) {
			return;
		}

		$ancestors = array_reverse( $ancestors );
		foreach ( $ancestors as $ancestor ) {
			$this->add_crumb(
				$this->get_crumb_title( 'post', $ancestor, get_the_title( $ancestor ) ),
				get_permalink( $ancestor )
			);
		}
	}

	/**
	 * Prepend the shop page to WooCommerce trails.
	 */
	private function prepend_shop_page() {
		if ( ! function_exists( 'wc_get_page_id' ) ) {
			return;
		}

		$shop_id   = wc_get_page_id( 'shop' );
		$shop_page = get_post( $shop_id );

		if ( ! $shop_id || ! $shop_page ) {
			return;
		}

		if ( intval( get_option( 'page_on_front' ) ) === $shop_id ) {
			return;
		}

		$permalinks = wc_get_permalink_structure();
		$uses_base  = isset( $permalinks['product_base'] ) && strstr( $permalinks['product_base'], '/' . $shop_page->post_name );

		if ( apply_filters( 'seovaultseo_breadcrumb_uses_shop_base', $uses_base ) ) {
			$this->add_crumb(
				$this->get_crumb_title( 'post', $shop_id, get_the_title( $shop_page ) ),
				get_permalink( $shop_page )
			);
		}
	}

	/**
	 * Add the primary term crumb for a post.
	 *
	 * @param \WP_Term[]|false|\WP_Error $terms Terms attached to the post.
	 */
	private function maybe_add_primary_term( $terms ) {
		if ( is_wp_error( $terms ) || empty( $terms ) ) {
			return;
		}

		$term = apply_filters( 'seovaultseo_breadcrumb_primary_term', $terms[0], $terms );
		$this->maybe_add_term_ancestors( $term );
		$this->add_crumb(
			$this->get_crumb_title( 'term', $term, $term->name ),
			get_term_link( $term )
		);
	}

	/**
	 * Add ancestor taxonomy crumbs for hierarchical terms.
	 *
	 * @param \WP_Term $term Term object.
	 */
	private function maybe_add_term_ancestors( $term ) {
		if ( ! $this->can_add_term_ancestors( $term ) ) {
			return;
		}

		$ancestors = get_ancestors( $term->term_id, $term->taxonomy );
		$ancestors = array_reverse( $ancestors );

		foreach ( $ancestors as $ancestor_id ) {
			$ancestor = get_term( $ancestor_id, $term->taxonomy );
			if ( ! is_wp_error( $ancestor ) && $ancestor ) {
				$this->add_crumb(
					$this->get_crumb_title( 'term', $ancestor, $ancestor->name ),
					get_term_link( $ancestor )
				);
			}
		}
	}

	/**
	 * Whether term ancestors can be added.
	 *
	 * @param \WP_Term $term Term object.
	 * @return bool
	 */
	private function can_add_term_ancestors( $term ) {
		if ( empty( $term ) || 0 === $term->parent ) {
			return false;
		}
		if ( ! $this->settings['show_ancestors'] ) {
			return false;
		}
		if ( ! is_taxonomy_hierarchical( $term->taxonomy ) ) {
			return false;
		}
		return true;
	}

	/**
	 * Add the blog page crumb if applicable.
	 */
	private function maybe_add_blog() {
		$blog_id = (int) get_option( 'page_for_posts' );
		if ( ! $blog_id || ! $this->can_add_blog() ) {
			return;
		}

		$this->add_crumb(
			$this->get_crumb_title( 'post', $blog_id, get_the_title( $blog_id ) ),
			get_permalink( $blog_id )
		);
	}

	/**
	 * Whether the blog page crumb can be added.
	 *
	 * @return bool
	 */
	private function can_add_blog() {
		if ( ! $this->settings['show_blog'] || 'page' !== get_option( 'show_on_front' ) ) {
			return false;
		}
		if ( ! is_singular( 'post' ) && ! is_category() && ! is_tag() ) {
			return false;
		}
		return true;
	}

	/**
	 * Get a custom breadcrumb title from post/term/user meta.
	 *
	 * @param string $object_type   'post', 'term', or 'user'.
	 * @param int    $object_id     Object ID.
	 * @param string $default_value Fallback title.
	 * @return string
	 */
	private function get_crumb_title( $object_type, $object_id, $default_value ) {
		$title = '';

		if ( 'post' === $object_type ) {
			$title = PostMeta::get( $object_id, 'breadcrumb_title', '' );
		} elseif ( 'term' === $object_type && $object_id instanceof \WP_Term ) {
			$title = get_term_meta( $object_id->term_id, '_seovault_breadcrumb_title', true );
		} elseif ( 'term' === $object_type ) {
			$title = get_term_meta( $object_id, '_seovault_breadcrumb_title', true );
		} elseif ( 'user' === $object_type ) {
			$title = get_user_meta( $object_id, '_seovault_breadcrumb_title', true );
		}

		return $title ? $title : $default_value;
	}

	/**
	 * Get the primary taxonomy for a post type from settings.
	 *
	 * @param string $post_type Post type slug.
	 * @return string|false
	 */
	private function get_primary_taxonomy( $post_type ) {
		$key = 'titles_pt_' . $post_type . '_primary_taxonomy';
		$tax = Options::get( $key, '' );
		return $tax ?: false;
	}

	/**
	 * Replace a %s placeholder in a format string.
	 *
	 * @param string $text  Text containing %s.
	 * @param string $value Value to substitute.
	 * @return string
	 */
	private function replace_placeholder( $text, $value ) {
		return preg_replace( '/%s(?=\s|%|$)/', $value, $text );
	}

	/**
	 * Remove a query arg from the current URL.
	 *
	 * @param string $key Query arg key.
	 * @return string
	 */
	private function remove_query_arg( $key ) {
		return remove_query_arg( $key, $this->get_current_url() );
	}

	/**
	 * Get the current page canonical URL.
	 *
	 * @return string
	 */
	private function get_canonical() {
		if ( is_singular() ) {
			return get_permalink();
		}
		return $this->get_current_url();
	}

	/**
	 * Get the current request URL.
	 *
	 * @return string
	 */
	private function get_current_url() {
		global $wp;
		return home_url( $wp->request ?? '' );
	}
}

/**
 * Template tag: output breadcrumbs.
 *
 * @param array $args Optional. Rendering arguments.
 */
function seovault_the_breadcrumbs( $args = array() ) {
	$instance = Breadcrumbs::get();
	if ( $instance ) {
		echo $instance->render( $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- render() returns wp_kses_post() HTML after the final filter.
	}
}

/**
 * Template tag: get breadcrumbs as HTML string.
 *
 * @param array $args Optional. Rendering arguments.
 * @return string
 */
function seovault_get_breadcrumbs( $args = array() ) {
	$instance = Breadcrumbs::get();
	return $instance ? $instance->render( $args ) : '';
}
