<?php
/**
 * Abstract template layout.
 *
 * @package PRECO
 */

namespace PRECO\Includes\Frontend\Core\Layouts;

defined( 'ABSPATH' ) || exit;

/**
 * Base class for the recommendation card template layouts (lay_1 … lay_5).
 *
 * A layout owns the card markup: which elements render, in what order and
 * inside which structural wrappers (vertical stack vs. media/content split).
 * Layout-specific looks (borders, dividers, gaps) live in CSS keyed off the
 * `preco-recommendations-layout-{slug}` wrapper class.
 */
abstract class AbstractLayout {

	/**
	 * Layout slug, e.g. "lay_1".
	 *
	 * @var string
	 */
	protected $slug = 'lay_1';

	/**
	 * Get the layout slug.
	 *
	 * @return string
	 */
	public function get_slug() {
		return $this->slug;
	}

	/**
	 * Whether the card splits into a media column and a content column.
	 *
	 * @return bool
	 */
	public function is_horizontal() {
		return false;
	}

	/**
	 * Wrapper class added to `.preco-recommendations`.
	 *
	 * @return string
	 */
	public function get_wrapper_class() {
		return 'preco-recommendations-layout-' . $this->slug;
	}

	/**
	 * Default element visibility/order when the list has no item_sorting saved.
	 *
	 * @return array<string,bool>
	 */
	public function get_default_item_sorting() {
		return array(
			'product_image'    => true,
			'product_category' => false,
			'product_title'    => true,
			'product_rating'   => false,
			'product_price'    => true,
			'add_to_cart'      => false,
		);
	}

	/**
	 * Elements this layout can render.
	 *
	 * @return string[]
	 */
	public function get_supported_elements() {
		return array_keys( $this->get_default_item_sorting() );
	}

	/**
	 * Resolve the ordered list of enabled element keys.
	 *
	 * @param array $item_sorting Saved item_sorting map (key => enabled).
	 * @return string[]
	 */
	public function get_enabled_elements( $item_sorting ) {
		$sorting = is_array( $item_sorting ) && ! empty( $item_sorting ) ? $item_sorting : $this->get_default_item_sorting();

		$supported = $this->get_supported_elements();
		$enabled   = array();
		foreach ( $sorting as $element => $is_on ) {
			if ( $is_on && in_array( $element, $supported, true ) ) {
				$enabled[] = $element;
			}
		}
		return $enabled;
	}

	/**
	 * Render one product card.
	 *
	 * @param \WC_Product $product      Product to render.
	 * @param array       $item_sorting Saved item_sorting map (key => enabled).
	 * @param array       $args         Extra render args: btn_style.
	 * @return void
	 */
	public function render_card( $product, $item_sorting, $args = array() ) {
		if ( ! $product || ! is_a( $product, 'WC_Product' ) ) {
			return;
		}

		$elements = $this->get_enabled_elements( $item_sorting );

		echo '<div class="preco-card" data-product-id="' . esc_attr( $product->get_id() ) . '">';

		if ( $this->is_horizontal() ) {
			$this->render_horizontal_inner( $product, $elements, $args );
		} else {
			$this->render_vertical_inner( $product, $elements, $args );
		}

		echo '</div>';
	}

	/**
	 * Vertical stack: every element inside a single inner wrapper.
	 *
	 * @param \WC_Product $product  Product.
	 * @param string[]    $elements Ordered enabled element keys.
	 * @param array       $args     Render args.
	 * @return void
	 */
	protected function render_vertical_inner( $product, $elements, $args ) {
		echo '<div class="preco-card__inner">';
		foreach ( $elements as $element ) {
			$this->render_element( $element, $product, $args );
		}
		echo '</div>';
	}

	/**
	 * Media/content split: image in its own column, everything else stacked.
	 *
	 * @param \WC_Product $product  Product.
	 * @param string[]    $elements Ordered enabled element keys.
	 * @param array       $args     Render args.
	 * @return void
	 */
	protected function render_horizontal_inner( $product, $elements, $args ) {
		$has_media     = in_array( 'product_image', $elements, true );
		$inner_classes = array( 'preco-card__inner', 'preco-card__inner--horizontal' );
		if ( ! $has_media ) {
			$inner_classes[] = 'preco-card__inner--no-media';
		}

		echo '<div class="' . esc_attr( implode( ' ', $inner_classes ) ) . '">';

		if ( $has_media ) {
			echo '<div class="preco-card__media">';
			$this->render_element( 'product_image', $product, $args );
			echo '</div>';
		}

		echo '<div class="preco-card__content">';
		foreach ( $elements as $element ) {
			if ( 'product_image' === $element ) {
				continue;
			}
			$this->render_element( $element, $product, $args );
		}
		echo '</div>';

		echo '</div>';
	}

	/**
	 * Render a single card element by key.
	 *
	 * @param string      $element Element key.
	 * @param \WC_Product $product Product.
	 * @param array       $args    Render args.
	 * @return void
	 */
	protected function render_element( $element, $product, $args ) {
		switch ( $element ) {
			case 'product_image':
				$this->render_image( $product );
				break;
			case 'product_title':
				$this->render_title( $product );
				break;
			case 'product_category':
				$this->render_category( $product );
				break;
			case 'product_rating':
				$this->render_rating( $product );
				break;
			case 'product_price':
				$this->render_price( $product );
				break;
			case 'add_to_cart':
				$this->render_add_to_cart( $product, $args );
				break;
		}
	}

	/**
	 * Product image element.
	 *
	 * @param \WC_Product $product Product.
	 * @return void
	 */
	protected function render_image( $product ) {
		echo '<div class="preco-card__image"><a href="' . esc_url( $product->get_permalink() ) . '">' . wp_kses_post( $product->get_image( 'woocommerce_thumbnail' ) ) . '</a></div>';
	}

	/**
	 * Product title element.
	 *
	 * @param \WC_Product $product Product.
	 * @return void
	 */
	protected function render_title( $product ) {
		echo '<div class="preco-card__title"><a href="' . esc_url( $product->get_permalink() ) . '">' . esc_html( $product->get_name() ) . '</a></div>';
	}

	/**
	 * Product category element.
	 *
	 * @param \WC_Product $product Product.
	 * @return void
	 */
	protected function render_category( $product ) {
		// Preview-mode DummyProduct has no post row for get_the_terms() to
		// find, so it exposes its mock categories through this method instead.
		if ( method_exists( $product, 'get_preco_dummy_categories' ) ) {
			$labels = array_slice( $product->get_preco_dummy_categories(), 0, 2 );
			if ( empty( $labels ) ) {
				return;
			}
			echo '<div class="preco-card__category">';
			foreach ( $labels as $i => $label ) {
				if ( $i > 0 ) {
					echo '<span class="preco-card__category-sep">, </span>';
				}
				echo '<a class="preco-card__category-item">' . esc_html( $this->truncate_category_name( $label ) ) . '</a>';
			}
			echo '</div>';
			return;
		}

		$terms = get_the_terms( $product->get_id(), 'product_cat' );
		if ( ! $terms || is_wp_error( $terms ) ) {
			return;
		}
		$terms = array_slice( array_values( $terms ), 0, 2 );
		echo '<div class="preco-card__category">';
		foreach ( $terms as $i => $term ) {
			$link = get_term_link( $term );
			if ( is_wp_error( $link ) ) {
				continue;
			}
			if ( $i > 0 ) {
				echo '<span class="preco-card__category-sep">, </span>';
			}
			echo '<a class="preco-card__category-item" href="' . esc_url( $link ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $this->truncate_category_name( $term->name ) ) . '</a>';
		}
		echo '</div>';
	}

	/**
	 * Multibyte-safe truncation for category labels — keeps a single long or
	 * malformed term name (e.g. one pasted from a description field) from
	 * blowing up the card layout.
	 *
	 * @param string $name  Category name.
	 * @param int    $limit Maximum characters before truncating.
	 * @return string
	 */
	protected function truncate_category_name( $name, $limit = 10 ) {
		$name = trim( $name );
		if ( function_exists( 'mb_strlen' ) ) {
			return mb_strlen( $name ) > $limit ? mb_substr( $name, 0, $limit ) . '…' : $name;
		}
		return strlen( $name ) > $limit ? substr( $name, 0, $limit ) . '…' : $name;
	}

	/**
	 * Product rating element.
	 *
	 * @param \WC_Product $product Product.
	 * @return void
	 */
	protected function render_rating( $product ) {
		$avg_rating = $product->get_average_rating();
		if ( $avg_rating > 0 ) {
			// Self-built SVG markup, not user input — wp_kses_post() would strip the <svg>/<path> tags.
			echo '<div class="preco-card__rating">' . $this->get_rating_stars_html( $avg_rating ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}
	}

	/**
	 * Self-contained SVG star rating widget.
	 *
	 * Draws its own stars instead of calling wc_get_rating_html(), which
	 * renders via WooCommerce's "star" icon font — a look that depends on
	 * the active theme loading/not overriding that font and is inconsistent
	 * across stores. Two identical 5-star SVGs stacked (empty base + filled
	 * overlay), the overlay clipped to rating/5 width — same formula
	 * wc_get_star_rating_html() uses, just with real vector stars so the
	 * result looks the same everywhere. Mirrored in JS by
	 * recommended-products/layouts/index.js's getRatingHtml() for the
	 * builder preview.
	 *
	 * @param float $rating Average rating, 0–5.
	 * @return string
	 */
	protected function get_rating_stars_html( $rating ) {
		$label = sprintf(
			/* translators: %s: rating */
			__( 'Rated %s out of 5', 'wow-ai-product-recommendations' ),
			$rating
		);

		return sprintf(
			'<span class="preco-card__rating-stars" role="img" aria-label="%1$s">%2$s<span class="preco-card__rating-stars-fill" style="width:%3$s%%">%2$s</span></span>',
			esc_attr( $label ),
			self::get_stars_svg(),
			esc_attr( round( ( $rating / 5 ) * 100, 2 ) )
		);
	}

	/**
	 * Five-star SVG sprite: one star per 22-unit cell in a 110×22 viewBox.
	 *
	 * @return string
	 */
	private static function get_stars_svg() {
		static $svg = null;
		if ( null === $svg ) {
			$star  = 'M11 1L13.47 7.6L20.51 7.91L14.99 12.3L16.88 19.09L11 15.2L5.12 19.09L7.01 12.3L1.49 7.91L8.53 7.6Z';
			$paths = '';
			for ( $i = 0; $i < 5; $i++ ) {
				$paths .= $i > 0
					? '<path transform="translate(' . ( $i * 22 ) . ' 0)" d="' . $star . '"/>'
					: '<path d="' . $star . '"/>';
			}
			$svg = '<svg class="preco-card__rating-stars-svg" viewBox="0 0 110 22" aria-hidden="true" focusable="false">' . $paths . '</svg>';
		}
		return $svg;
	}

	/**
	 * Product price element.
	 *
	 * @param \WC_Product $product Product.
	 * @return void
	 */
	protected function render_price( $product ) {
		echo '<div class="preco-card__price">' . wp_kses_post( $product->get_price_html() ) . '</div>';
	}

	/**
	 * Add to cart element.
	 *
	 * @param \WC_Product $product Product.
	 * @param array       $args    Render args (btn_style).
	 * @return void
	 */
	protected function render_add_to_cart( $product, $args ) {

		echo '<div class="preco-card__add-to-cart">';
		// Grouped/external products are never "purchasable" directly (WooCommerce
		// core always returns false there — they need a child selection or an
		// external redirect), so gating on is_in_stock() alone lets their own
		// type-aware add_to_cart_url()/add_to_cart_text() (View products / Buy
		// product / Select options) render instead of a false "Out of stock".
		if ( $product->is_in_stock() ) {
			$add_url    = $product->add_to_cart_url();
			$add_text   = $product->add_to_cart_text();
			$ajax_class = ( $product->is_purchasable() && $product->supports( 'ajax_add_to_cart' ) ) ? ' ajax_add_to_cart' : '';
			echo '<a href="' . esc_url( $add_url ) . '" data-quantity="1" data-product_id="' . esc_attr( $product->get_id() ) . '" class="button add_to_cart_button' . esc_attr( $ajax_class ) . '" aria-label="' . esc_attr( $add_text ) . '">' . esc_html( $add_text ) . '</a>';
		} else {
			echo '<button class="button" disabled>' . esc_html__( 'Out of stock', 'wow-ai-product-recommendations' ) . '</button>';
		}
		echo '</div>';
	}
}
