<?php
/**
 * Livada SEO — Module Table of Contents
 *
 * Provides a nested table of contents as a WordPress shortcode [livada_toc]
 * and a global PHP helper livada_seo_toc().
 *
 * Scans post content for <h2> and <h3> headings, auto-injects id attributes
 * via the_content filter (priority 5), and renders a nested <ol> TOC.
 *
 * Loader integration: add to $feature_map in class-loader.php:
 * 'toc' => ['class-toc.php', 'Livada_SEO_TOC'],
 *
 * @package Livada_SEO
 */

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

class Livada_SEO_TOC {

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

	/**
	 * Registers the shortcode, the content filter, and the template helper.
	 * Called once by Livada_SEO_Loader on `plugins_loaded`.
	 */
	public static function init(): void {
		add_shortcode( 'livada_toc', [ self::class, 'render' ] );

		// Inject id attributes into headings before other content filters run.
		add_filter( 'the_content', [ self::class, 'inject_heading_ids' ], 5 );
	}

	// ── Heading ID injection ──────────────────────────────────────────────────

	/**
	 * Adds id attributes to <h2> and <h3> tags that don't already have one.
	 * Hooked to the_content at priority 5.
	 *
	 * @param string $content Raw post content.
	 * @return string Content with id attributes on headings.
	 */
	public static function inject_heading_ids( string $content ): string {
		if ( '' === $content ) {
			return $content;
		}

		return preg_replace_callback(
			'/<(h[23])([^>]*)>(.*?)<\/h[23]>/is',
			static function ( array $matches ): string {
				$tag        = $matches[1];
				$attrs      = $matches[2];
				$inner_text = $matches[3];

				// Already has an id — leave it untouched.
				if ( preg_match( '/\bid=["\'][^"\']*["\']/i', $attrs ) ) {
					return $matches[0];
				}

				// Strip any inner HTML tags to get plain text for the slug.
				$plain_text = wp_strip_all_tags( $inner_text );
				$id         = sanitize_title( $plain_text );

				if ( '' === $id ) {
					return $matches[0];
				}

				return '<' . $tag . $attrs . ' id="' . esc_attr( $id ) . '">' . $inner_text . '</' . $tag . '>';
			},
			$content
		) ?? $content;
	}

	// ── Rendering ─────────────────────────────────────────────────────────────

	/**
	 * Shortcode callback: [livada_toc]
	 * Parses the current post content and returns the TOC HTML string.
	 *
	 * @param array<string,mixed>|string $atts    Shortcode attributes (unused).
	 * @param string|null                $content Enclosed content (unused).
	 * @return string
	 */
	public static function render( $atts = [], ?string $content = null ): string {
		$post = get_post();

		if ( ! ( $post instanceof WP_Post ) || '' === $post->post_content ) {
			return '';
		}

		$headings = self::extract_headings( $post->post_content );

		// Render nothing if fewer than 3 headings.
		if ( count( $headings ) < 3 ) {
			return '';
		}

		return self::build_html( $headings );
	}

	// ── Heading extraction ────────────────────────────────────────────────────

	/**
	 * Extracts h2 / h3 headings with their level and slug from raw post content.
	 *
	 * @param string $content Raw post content (not yet filtered).
	 * @return array<int,array{level:int,text:string,slug:string}>
	 */
	private static function extract_headings( string $content ): array {
		$headings = [];

		preg_match_all(
			'/<h([23])[^>]*>(.*?)<\/h[23]>/is',
			$content,
			$matches,
			PREG_SET_ORDER
		);

		foreach ( $matches as $match ) {
			$level      = (int) $match[1];
			$inner_text = $match[2];
			$plain_text = wp_strip_all_tags( $inner_text );
			$slug       = sanitize_title( $plain_text );

			if ( '' === $plain_text || '' === $slug ) {
				continue;
			}

			$headings[] = [
				'level' => $level,
				'text'  => $plain_text,
				'slug'  => $slug,
			];
		}

		return $headings;
	}

	// ── HTML builder ──────────────────────────────────────────────────────────

	/**
	 * Converts a flat list of headings into a nested TOC HTML string.
	 *
	 * @param array<int,array{level:int,text:string,slug:string}> $headings
	 * @return string
	 */
	private static function build_html( array $headings ): string {
		// Minimal inline styles — no external stylesheet needed.
		$style = implode( ' ', [
			'list-style:none;',
			'margin:0;',
			'padding:0;',
		] );

		$nav_style = implode( ' ', [
			'border-left:3px solid #0073aa;',
			'padding:12px 16px;',
			'margin-bottom:24px;',
			'background:#f8f8f8;',
			'display:inline-block;',
			'min-width:220px;',
		] );

		$title_style = implode( ' ', [
			'margin:0 0 8px;',
			'font-weight:600;',
			'font-size:1em;',
		] );

		$html  = '<nav class="livada-toc" aria-label="Table des matieres" style="' . esc_attr( $nav_style ) . '">' . "\n";
		$html .= "\t" . '<p class="livada-toc-title" style="' . esc_attr( $title_style ) . '">Sommaire</p>' . "\n";
		$html .= "\t" . '<ol style="' . esc_attr( $style ) . '">' . "\n";

		$i     = 0;
		$total = count( $headings );

		while ( $i < $total ) {
			$heading = $headings[ $i ];

			if ( 2 === $heading['level'] ) {
				// H2 item — look ahead for child H3s.
				$html .= "\t\t<li>" . self::heading_link( $heading );

				// Collect subsequent H3 children.
				$children = [];
				$j        = $i + 1;
				while ( $j < $total && 3 === $headings[ $j ]['level'] ) {
					$children[] = $headings[ $j ];
					$j++;
				}

				if ( ! empty( $children ) ) {
					$html .= "\n\t\t\t" . '<ol style="' . esc_attr( $style ) . 'padding-left:1em;margin-top:4px;">' . "\n";
					foreach ( $children as $child ) {
						$html .= "\t\t\t\t<li>" . self::heading_link( $child ) . '</li>' . "\n";
					}
					$html .= "\t\t\t</ol>\n\t\t";
				}

				$html .= '</li>' . "\n";
				$i     = $j; // Advance past the consumed children.

			} else {
				// Orphan H3 (appears before any H2) — render as top-level item.
				$html .= "\t\t<li>" . self::heading_link( $heading ) . '</li>' . "\n";
				$i++;
			}
		}

		$html .= "\t</ol>\n";
		$html .= '</nav>' . "\n";

		return $html;
	}

	/**
	 * Renders a single anchor link for a heading entry.
	 *
	 * @param array{level:int,text:string,slug:string} $heading
	 * @return string
	 */
	private static function heading_link( array $heading ): string {
		return '<a href="' . esc_url( '#' . $heading['slug'] ) . '">'
			. esc_html( $heading['text'] )
			. '</a>';
	}
}

// ── Global template function ───────────────────────────────────────────────────

if ( ! function_exists( 'livada_seo_toc' ) ) {
	/**
	 * Echoes the table of contents for use directly in PHP templates.
	 *
	 * Usage:
	 *   <?php livada_seo_toc(); ?>
	 */
	function livada_seo_toc(): void {
		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo Livada_SEO_TOC::render();
	}
}
