<?php
/**
 * WooCommerce-style template loader.
 *
 * @package ThemeREX\AutoDealer
 * @version 1.0.0
 */

declare(strict_types=1);

namespace ThemeREX\AutoDealer\Frontend;

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

/**
 * Template loader — intercepts WordPress template resolution for plugin post types.
 */
class TemplateLoader {

	/**
	 * Theme subdirectory name used for template overrides.
	 */
	private const THEME_DIR = 'trx-auto-dealer';

	/**
	 * Theme header/footer block template parts, pre-rendered by
	 * {@see self::renderHeader()} before wp_head() so their script-module
	 * enqueues land in the import map core prints at `wp_head` on block
	 * themes. {@see self::renderFooter()} echoes the footer buffer.
	 */

	/**
	 * Register hooks.
	 *
	 * @return void
	 */
	public function register(): void {
		add_filter( 'template_include', array( $this, 'loadTemplate' ), 99 );
	}

	/**
	 * Filter the template file path for plugin post types.
	 *
	 * @param string $template The current template path.
	 * @return string Filtered template path.
	 */
	public function loadTemplate( string $template ): string {

		if ( is_singular( 'trx_ad_listing' ) ) {
			// Always use our own single-vehicle.php template for ALL
			// layouts — default, gutenberg, AND elementor. The template
			// itself branches on the layout setting and renders the
			// chosen content directly. Returning the theme's template
			// here used to wrap our Elementor/Gutenberg layout in the
			// theme's post chrome (post title, featured image, "Written
			// by" byline, prev-post nav, MORE POSTS), which broke the
			// "identical to PHP default" promise.
			$located = self::locateTemplate( 'single-vehicle.php' );
			if ( '' !== $located ) {
				return $located;
			}
		}

		if ( is_post_type_archive( 'trx_ad_listing' ) || is_tax( 'trx_ad_make' ) || is_tax( 'trx_ad_model' ) || is_tax( 'trx_ad_body_style' ) || is_tax( 'trx_ad_fuel_type' ) || is_tax( 'trx_ad_transmission' ) || is_tax( 'trx_ad_drivetrain' ) || is_tax( 'trx_ad_condition' ) ) {
			$located = self::locateTemplate( 'archive-vehicle.php' );
			if ( '' !== $located ) {
				return $located;
			}
		}

		if ( is_singular( 'trx_ad_dealer' ) ) {
			$located = self::locateTemplate( 'single-dealer.php' );
			if ( '' !== $located ) {
				return $located;
			}
		}

		if ( is_post_type_archive( 'trx_ad_dealer' ) ) {
			$located = self::locateTemplate( 'archive-dealer.php' );
			if ( '' !== $located ) {
				return $located;
			}
		}

		return $template;
	}

	/**
	 * Check if the single listing should use an Elementor template.
	 *
	 * When enabled, injects the Elementor template content via the_content filter
	 * so the theme's single.php renders the Elementor-designed layout.
	 *
	 * @return bool
	 */
	/**
	 * Phase 19 per-listing override meta keys, written by the Pro
	 * `PerListingLayoutMetaBox` and read here. The free plugin owns the
	 * read-side so resolution honors Pro overrides without depending on
	 * Pro classes — when Pro is absent the meta is just never written.
	 */
	private const META_LAYOUT      = '_trx_ad_layout';
	private const META_TEMPLATE_ID = '_trx_ad_layout_template_id';

	/**
	 * Resolve which single-listing layout to use, plus the specific template
	 * ID when Gutenberg/Elementor mode picks a custom template.
	 *
	 * Resolution order:
	 *   1. Per-listing override (Pro feature; reads `_trx_ad_layout` +
	 *      `_trx_ad_layout_template_id` post meta on the listing).
	 *   2. Global `single_layout` setting (existing behavior).
	 *
	 * The function is pure — no side effects beyond an optional filter.
	 * `renderSingleLayout()` consumes the returned tuple.
	 *
	 * @return array{layout: string, template_id: int}
	 */
	public static function resolveSingleLayout(): array {
		$settings = AdminSettings::getSettings();

		// 1. Per-listing override.
		$listing_id = (int) get_the_ID();
		if ( $listing_id > 0 ) {
			$override = sanitize_key( (string) get_post_meta( $listing_id, self::META_LAYOUT, true ) );
			if ( in_array( $override, array( 'default', 'gutenberg', 'elementor' ), true ) ) {
				$resolved = self::resolveOverride( $override, $listing_id );
				if ( null !== $resolved ) {
					/**
					 * Filter the resolved single-listing layout tuple.
					 *
					 * @param array{layout: string, template_id: int} $resolved
					 * @param int                                     $listing_id
					 */
					return apply_filters( 'trx_ad_resolved_single_layout', $resolved, $listing_id );
				}
				// Override set but template missing/invalid — fall through.
			}
		}

		// 2. Global default.
		$layout = (string) ( $settings['single_layout'] ?? 'default' );

		if ( 'gutenberg' === $layout ) {
			$block_id = (int) ( $settings['single_gutenberg_template'] ?? 0 );
			$block    = $block_id > 0 ? get_post( $block_id ) : null;
			if ( $block instanceof \WP_Post && 'wp_block' === $block->post_type ) {
				return apply_filters(
					'trx_ad_resolved_single_layout',
					array(
						'layout'      => 'gutenberg',
						'template_id' => $block_id,
					),
					$listing_id
				);
			}
			return apply_filters(
				'trx_ad_resolved_single_layout',
				array(
					'layout'      => 'default',
					'template_id' => 0,
				),
				$listing_id
			);
		}

		if ( 'elementor' === $layout ) {
			if ( ! defined( 'ELEMENTOR_VERSION' ) ) {
				return apply_filters(
					'trx_ad_resolved_single_layout',
					array(
						'layout'      => 'default',
						'template_id' => 0,
					),
					$listing_id
				);
			}
			$template_id = (int) ( $settings['single_elementor_template'] ?? 0 );
			if ( $template_id < 1 ) {
				return apply_filters(
					'trx_ad_resolved_single_layout',
					array(
						'layout'      => 'default',
						'template_id' => 0,
					),
					$listing_id
				);
			}
			return apply_filters(
				'trx_ad_resolved_single_layout',
				array(
					'layout'      => 'elementor',
					'template_id' => $template_id,
				),
				$listing_id
			);
		}

		return apply_filters(
			'trx_ad_resolved_single_layout',
			array(
				'layout'      => 'default',
				'template_id' => 0,
			),
			$listing_id
		);
	}

	/**
	 * Resolve a per-listing override into a renderable tuple, or null when
	 * the override points at a missing/invalid template (caller falls
	 * through to the global setting).
	 *
	 * @param string $override One of 'default' | 'gutenberg' | 'elementor'.
	 * @param int    $listing_id
	 * @return array{layout: string, template_id: int}|null
	 */
	private static function resolveOverride( string $override, int $listing_id ): ?array {
		if ( 'default' === $override ) {
			return array(
				'layout'      => 'default',
				'template_id' => 0,
			);
		}

		$tpl_id = (int) get_post_meta( $listing_id, self::META_TEMPLATE_ID, true );

		if ( 'gutenberg' === $override ) {
			if ( $tpl_id < 1 ) {
				return null;
			}
			$block = get_post( $tpl_id );
			if ( ! ( $block instanceof \WP_Post ) || 'wp_block' !== $block->post_type ) {
				return null;
			}
			return array(
				'layout'      => 'gutenberg',
				'template_id' => $tpl_id,
			);
		}

		if ( 'elementor' === $override ) {
			if ( ! defined( 'ELEMENTOR_VERSION' ) || $tpl_id < 1 ) {
				return null;
			}
			$tpl = get_post( $tpl_id );
			if ( ! ( $tpl instanceof \WP_Post ) || 'elementor_library' !== $tpl->post_type ) {
				return null;
			}
			return array(
				'layout'      => 'elementor',
				'template_id' => $tpl_id,
			);
		}

		return null;
	}

	/**
	 * Render the layout-specific content for the current single listing.
	 * Called from single-vehicle.php — branches on the resolved layout.
	 */
	public static function renderSingleLayout(): void {
		$resolved    = self::resolveSingleLayout();
		$layout      = (string) $resolved['layout'];
		$template_id = (int) $resolved['template_id'];

		if ( 'gutenberg' === $layout && $template_id > 0 ) {
			$block = get_post( $template_id );
			if ( $block instanceof \WP_Post && 'wp_block' === $block->post_type ) {
				echo wp_kses( do_blocks( $block->post_content ), \ThemeREX\AutoDealer\allowedHtml() );
				return;
			}
		}

		if ( 'elementor' === $layout && $template_id > 0 && defined( 'ELEMENTOR_VERSION' ) && class_exists( '\Elementor\Plugin' ) ) {
			echo wp_kses( \Elementor\Plugin::instance()->frontend->get_builder_content_for_display( $template_id ), \ThemeREX\AutoDealer\allowedHtml() );
			return;
		}

		// Default — render the plugin's PHP partial.
		self::getTemplatePart( 'single-listing', array( 'post_id' => (int) get_the_ID() ) );
	}

	/**
	 * Output the page header in a way that works on BOTH classic and block
	 * (FSE) themes.
	 *
	 * Classic themes load `header.php` via `get_header()`. Block themes ship
	 * no `header.php`, so calling `get_header()` there loads WordPress'
	 * `wp-includes/theme-compat/header.php` fallback, which emits the
	 * "Theme without header.php is deprecated since version 3.0.0" notice
	 * (visible at the top of every plugin page on TT5 and other block themes).
	 * For block themes we emit the document scaffold ourselves and render the
	 * theme's `header` block template part instead.
	 *
	 * @return void
	 */
	public static function renderHeader(): void {
		if ( ! self::isBlockTheme() ) {
			get_header();
			return;
		}

		// Pre-render (and discard) the theme's header + footer template parts
		// BEFORE wp_head() runs. On block themes core hooks the script-module
		// import map to `wp_head` (WP_Script_Modules::add_hooks()) because the
		// block template canvas renders the whole template first — so any
		// script module a theme block enqueues (e.g. the navigation block's
		// @wordpress/interactivity view) must be enqueued by the time
		// wp_head() fires. Rendering the parts after wp_head() prints an
		// EMPTY import map and the module fails to resolve in the browser
		// (dead mobile nav on every plugin-templated page). The output is
		// discarded here — the parts are rendered live by core's
		// block_template_part() at their actual output positions below and in
		// renderFooter(), so no captured HTML variable is ever echoed.
		if ( function_exists( 'block_template_part' ) ) {
			ob_start();
			block_template_part( 'header' );
			block_template_part( 'footer' );
			ob_end_clean();
		}

		echo '<!doctype html>';
		echo '<html ';
		language_attributes();
		echo '>';
		echo '<head>';
		echo '<meta charset="' . esc_attr( get_bloginfo( 'charset' ) ) . '">';
		echo '<meta name="viewport" content="width=device-width, initial-scale=1">';
		wp_head();
		echo '</head>';

		$body_classes = implode( ' ', array_map( 'sanitize_html_class', get_body_class() ) );
		echo '<body class="' . esc_attr( $body_classes ) . '">';

		wp_body_open();

		if ( function_exists( 'block_template_part' ) ) {
			block_template_part( 'header' );
		}
	}

	/**
	 * Counterpart to {@see self::renderHeader()}. Renders the theme's `footer`
	 * block template part + `wp_footer()` + the closing document tags on block
	 * themes; defers to `get_footer()` on classic themes.
	 *
	 * @return void
	 */
	public static function renderFooter(): void {
		if ( ! self::isBlockTheme() ) {
			get_footer();
			return;
		}

		// Rendered live by core (module enqueues were already warmed by the
		// pre-render in renderHeader() — see the import-map note there).
		if ( function_exists( 'block_template_part' ) ) {
			block_template_part( 'footer' );
		}

		wp_footer();
		echo '</body></html>';
	}

	/**
	 * Whether the active theme is a block (FSE) theme. Wrapped in one guard so
	 * the header/footer helpers share a single decision point and tests can
	 * override it via the `trx_ad_is_block_theme` filter.
	 *
	 * @return bool
	 */
	private static function isBlockTheme(): bool {
		$is_block = function_exists( 'wp_is_block_theme' ) && wp_is_block_theme();

		/**
		 * Filter whether Auto Dealer treats the active theme as a block theme
		 * for the purpose of header/footer rendering.
		 *
		 * @param bool $is_block Whether the active theme is a block theme.
		 */
		return (bool) apply_filters( 'trx_ad_is_block_theme', $is_block );
	}

	/**
	 * Locate a template file: first in the active theme, then in the plugin.
	 *
	 * @param string $templateName Template file name (e.g. "single-vehicle.php").
	 * @return string Full path to the template, or empty string if not found.
	 */
	public static function locateTemplate( string $templateName ): string {
		// 1. Check child theme / parent theme.
		$theme_path = locate_template(
			array(
				self::THEME_DIR . '/' . $templateName,
			)
		);

		if ( '' !== $theme_path ) {
			return $theme_path;
		}

		// 2. Fall back to plugin templates directory.
		$plugin_path = getPluginTemplatesPath() . $templateName;

		if ( file_exists( $plugin_path ) ) {
			return $plugin_path;
		}

		return '';
	}

	/**
	 * Locate and include a template part, passing variables via extract.
	 *
	 * Looks in:
	 *   1. {theme}/trx-auto-dealer/parts/{slug}.php
	 *   2. {plugin}/templates/parts/{slug}.php
	 *
	 * @param string               $slug Template part slug (e.g. "vehicle-card").
	 * @param array<string, mixed> $args Variables to pass to the template.
	 * @return void
	 */
	public static function getTemplatePart( string $slug, array $args = array() ): void {
		$file = 'parts/' . $slug . '.php';

		$located = self::locateTemplate( $file );

		if ( '' === $located ) {
			return;
		}

		/**
		 * Filter the template part arguments.
		 *
		 * @param array<string, mixed> $args Template arguments.
		 * @param string               $slug Template part slug.
		 */
		$args = apply_filters( 'trx_ad_template_part_args', $args, $slug );

		if ( ! empty( $args ) ) {
			// phpcs:ignore WordPress.PHP.DontExtract.extract_extract -- Intentional for template variable passing.
			extract( $args, EXTR_SKIP );
		}

		/**
		 * Fires before a template part is loaded.
		 *
		 * @param string               $slug    Template part slug.
		 * @param array<string, mixed> $args    Template arguments.
		 * @param string               $located Full path to located template.
		 */
		do_action( 'trx_ad_template_part_before', $slug, $args, $located );

		include $located;

		/**
		 * Fires after a template part is loaded.
		 *
		 * @param string $slug Template part slug.
		 */
		do_action( 'trx_ad_template_part_after', $slug );
	}
}
