<?php
/**
 * Loop Grid — dynamic post listing (Elementor Loop Grid / Posts style).
 *
 * @package AuraBuilder
 */

declare(strict_types=1);

namespace AuraBuilder\Widgets\Basic;

use AuraBuilder\Document\Node;
use AuraBuilder\Widgets\WidgetBase;
use WP_Query;

final class LoopGridWidget extends WidgetBase {

	public function get_type(): string {
		return 'loop_grid';
	}

	public function get_title(): string {
		return 'Loop de consulta';
	}

	public function get_category(): string {
		return 'dynamic';
	}

	public function get_icon(): string {
		return 'loop';
	}

	public function get_defaults(): array {
		return array(
			// Layout (desktop + optional tablet/mobile overrides).
			'template_type'         => 'posts',
			'columns'               => '3',
			'columns_tablet'        => '2',
			'columns_mobile'        => '1',
			'gap'                   => '24',
			'gap_tablet'            => '',
			'gap_mobile'            => '',
			'posts_per_page'        => '6',
			'posts_per_page_tablet' => '',
			'posts_per_page_mobile' => '',
			// Skin / card switches (responsive overrides empty = inherit).
			'show_image'            => '1',
			'show_image_tablet'     => '',
			'show_image_mobile'     => '',
			'image_position'        => 'top',
			'image_position_tablet' => '',
			'image_position_mobile' => '',
			'title_position'        => 'outside',
			'title_position_tablet' => '',
			'title_position_mobile' => '',
			'show_date'             => '1',
			'show_date_tablet'      => '',
			'show_date_mobile'      => '',
			'show_author'           => '1',
			'show_author_tablet'    => '',
			'show_author_mobile'    => '',
			'show_excerpt'          => '1',
			'show_excerpt_tablet'   => '',
			'show_excerpt_mobile'   => '',
			'show_read_more'        => '1',
			'show_read_more_tablet' => '',
			'show_read_more_mobile' => '',
			'show_title'            => '1',
			'show_title_tablet'     => '',
			'show_title_mobile'     => '',
			'card_shadow'           => '0',
			'card_shadow_tablet'    => '',
			'card_shadow_mobile'    => '',
			'card_border'           => '1',
			'card_border_tablet'    => '',
			'card_border_mobile'    => '',
			'as_slider'             => '0',
			'as_slider_tablet'      => '',
			'as_slider_mobile'      => '',
			'excerpt_length'        => '18',
			'excerpt_length_tablet' => '',
			'excerpt_length_mobile' => '',
			'read_more_text'        => 'Leia mais',
			'read_more_text_tablet' => '',
			'read_more_text_mobile' => '',
			'image_size'            => 'large',
			// Image card (WP-block-like).
			'image_ratio'           => '16/9',
			'image_object_fit'      => 'cover',
			'image_object_position' => '50% 50%',
			'image_link'            => '1',
			'show_category'         => '0',
			// Query.
			'query_source'          => 'post',
			'orderby'               => 'date',
			'order'                 => 'DESC',
			'offset'                => '0',
			'include_ids'           => '',
			'exclude_ids'           => '',
			'category'              => '', // one or more IDs: "12,34,56"
			'tag'                   => '',
			'author'                => '',
			'ignore_sticky'         => '1',
			'pagination'            => '0',
		);
	}

	public function sanitize_settings( array $settings ): array {
		$d = $this->get_defaults();

		$template = sanitize_key( (string) ( $settings['template_type'] ?? $d['template_type'] ) );
		if ( ! in_array( $template, array( 'posts', 'products' ), true ) ) {
			$template = 'posts';
		}

		$orderby         = sanitize_key( (string) ( $settings['orderby'] ?? $d['orderby'] ) );
		$allowed_orderby = array( 'date', 'title', 'menu_order', 'rand', 'modified', 'comment_count', 'ID', 'author' );
		if ( ! in_array( $orderby, $allowed_orderby, true ) ) {
			$orderby = 'date';
		}

		$order = strtoupper( (string) ( $settings['order'] ?? 'DESC' ) );
		if ( ! in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
			$order = 'DESC';
		}

		$query_source = sanitize_key( (string) ( $settings['query_source'] ?? 'post' ) );
		if ( $query_source === '' ) {
			$query_source = 'post';
		}

		$image_size = sanitize_key( (string) ( $settings['image_size'] ?? 'medium_large' ) );
		if ( $image_size === '' ) {
			$image_size = 'medium_large';
		}

		$image_position = sanitize_key( (string) ( $settings['image_position'] ?? 'top' ) );
		if ( ! in_array( $image_position, array( 'top', 'left', 'right' ), true ) ) {
			$image_position = 'top';
		}
		$title_position = sanitize_key( (string) ( $settings['title_position'] ?? 'outside' ) );
		if ( ! in_array( $title_position, array( 'outside', 'over_image' ), true ) ) {
			$title_position = 'outside';
		}

		$flag = static function ( $v, string $default = '1' ): string {
			if ( $v === null || $v === '' ) {
				return $default;
			}
			return ( $v === true || $v === 1 || $v === '1' || $v === 'true' || $v === 'yes' || $v === 'on' ) ? '1' : '0';
		};

		// Backward compat: show_meta → date + author.
		if ( ! isset( $settings['show_date'] ) && isset( $settings['show_meta'] ) ) {
			$settings['show_date']   = $settings['show_meta'];
			$settings['show_author'] = $settings['show_meta'];
		}

		$opt_flag = static function ( $v ): string {
			// Empty string = inherit (tablet/mobile override not set).
			if ( $v === null || $v === '' ) {
				return '';
			}
			return ( $v === true || $v === 1 || $v === '1' || $v === 'true' || $v === 'yes' || $v === 'on' ) ? '1' : '0';
		};
		$opt_int = static function ( $v, int $min, int $max ): string {
			if ( $v === null || $v === '' ) {
				return '';
			}
			return (string) max( $min, min( $max, (int) $v ) );
		};
		$opt_key = static function ( $v, array $allowed ): string {
			if ( $v === null || $v === '' ) {
				return '';
			}
			$k = sanitize_key( (string) $v );
			return in_array( $k, $allowed, true ) ? $k : '';
		};

		return array(
			'template_type'         => $template,
			'columns'               => (string) max( 1, min( 6, (int) ( $settings['columns'] ?? 3 ) ) ),
			'columns_tablet'        => (string) max( 1, min( 6, (int) ( $settings['columns_tablet'] ?? 2 ) ) ),
			'columns_mobile'        => (string) max( 1, min( 3, (int) ( $settings['columns_mobile'] ?? 1 ) ) ),
			'gap'                   => (string) max( 0, min( 80, (int) ( $settings['gap'] ?? 24 ) ) ),
			'gap_tablet'            => $opt_int( $settings['gap_tablet'] ?? '', 0, 80 ),
			'gap_mobile'            => $opt_int( $settings['gap_mobile'] ?? '', 0, 80 ),
			'posts_per_page'        => (string) max( 1, min( 48, (int) ( $settings['posts_per_page'] ?? 6 ) ) ),
			'posts_per_page_tablet' => $opt_int( $settings['posts_per_page_tablet'] ?? '', 1, 48 ),
			'posts_per_page_mobile' => $opt_int( $settings['posts_per_page_mobile'] ?? '', 1, 48 ),
			'show_image'            => $flag( $settings['show_image'] ?? null ),
			'show_image_tablet'     => $opt_flag( $settings['show_image_tablet'] ?? '' ),
			'show_image_mobile'     => $opt_flag( $settings['show_image_mobile'] ?? '' ),
			'image_position'        => $image_position,
			'image_position_tablet' => $opt_key( $settings['image_position_tablet'] ?? '', array( 'top', 'left', 'right' ) ),
			'image_position_mobile' => $opt_key( $settings['image_position_mobile'] ?? '', array( 'top', 'left', 'right' ) ),
			'title_position'        => $title_position,
			'title_position_tablet' => $opt_key( $settings['title_position_tablet'] ?? '', array( 'outside', 'over_image' ) ),
			'title_position_mobile' => $opt_key( $settings['title_position_mobile'] ?? '', array( 'outside', 'over_image' ) ),
			'show_date'             => $flag( $settings['show_date'] ?? null ),
			'show_date_tablet'      => $opt_flag( $settings['show_date_tablet'] ?? '' ),
			'show_date_mobile'      => $opt_flag( $settings['show_date_mobile'] ?? '' ),
			'show_author'           => $flag( $settings['show_author'] ?? null ),
			'show_author_tablet'    => $opt_flag( $settings['show_author_tablet'] ?? '' ),
			'show_author_mobile'    => $opt_flag( $settings['show_author_mobile'] ?? '' ),
			'show_excerpt'          => $flag( $settings['show_excerpt'] ?? null ),
			'show_excerpt_tablet'   => $opt_flag( $settings['show_excerpt_tablet'] ?? '' ),
			'show_excerpt_mobile'   => $opt_flag( $settings['show_excerpt_mobile'] ?? '' ),
			'show_read_more'        => $flag( $settings['show_read_more'] ?? null ),
			'show_read_more_tablet' => $opt_flag( $settings['show_read_more_tablet'] ?? '' ),
			'show_read_more_mobile' => $opt_flag( $settings['show_read_more_mobile'] ?? '' ),
			'show_title'            => $flag( $settings['show_title'] ?? null ),
			'show_title_tablet'     => $opt_flag( $settings['show_title_tablet'] ?? '' ),
			'show_title_mobile'     => $opt_flag( $settings['show_title_mobile'] ?? '' ),
			'card_shadow'           => $flag( $settings['card_shadow'] ?? null, '0' ),
			'card_shadow_tablet'    => $opt_flag( $settings['card_shadow_tablet'] ?? '' ),
			'card_shadow_mobile'    => $opt_flag( $settings['card_shadow_mobile'] ?? '' ),
			'card_border'           => $flag( $settings['card_border'] ?? null, '1' ),
			'card_border_tablet'    => $opt_flag( $settings['card_border_tablet'] ?? '' ),
			'card_border_mobile'    => $opt_flag( $settings['card_border_mobile'] ?? '' ),
			'as_slider'             => $flag( $settings['as_slider'] ?? null, '0' ),
			'as_slider_tablet'      => $opt_flag( $settings['as_slider_tablet'] ?? '' ),
			'as_slider_mobile'      => $opt_flag( $settings['as_slider_mobile'] ?? '' ),
			'excerpt_length'        => (string) max( 5, min( 80, (int) ( $settings['excerpt_length'] ?? 18 ) ) ),
			'excerpt_length_tablet' => $opt_int( $settings['excerpt_length_tablet'] ?? '', 5, 80 ),
			'excerpt_length_mobile' => $opt_int( $settings['excerpt_length_mobile'] ?? '', 5, 80 ),
			'read_more_text'        => sanitize_text_field( (string) ( $settings['read_more_text'] ?? $d['read_more_text'] ) ),
			'read_more_text_tablet' => sanitize_text_field( (string) ( $settings['read_more_text_tablet'] ?? '' ) ),
			'read_more_text_mobile' => sanitize_text_field( (string) ( $settings['read_more_text_mobile'] ?? '' ) ),
			'image_size'            => $image_size,
			'image_ratio'           => $this->sanitize_ratio( (string) ( $settings['image_ratio'] ?? '16/9' ) ),
			'image_object_fit'      => in_array( sanitize_key( (string) ( $settings['image_object_fit'] ?? 'cover' ) ), array( 'cover', 'contain', 'fill' ), true )
				? sanitize_key( (string) ( $settings['image_object_fit'] ?? 'cover' ) )
				: 'cover',
			'image_object_position' => self::sanitize_object_position( $settings['image_object_position'] ?? '50% 50%' ),
			'image_link'            => $flag( $settings['image_link'] ?? null, '1' ),
			'show_category'         => $flag( $settings['show_category'] ?? null, '0' ),
			'query_source'          => $query_source,
			'orderby'               => $orderby,
			'order'                 => $order,
			'offset'                => (string) max( 0, min( 200, (int) ( $settings['offset'] ?? 0 ) ) ),
			'include_ids'           => $this->sanitize_id_list( (string) ( $settings['include_ids'] ?? '' ) ),
			'exclude_ids'           => $this->sanitize_id_list( (string) ( $settings['exclude_ids'] ?? '' ) ),
			// Aceita um ou vários IDs/slugs: "12,34" ou "artigos,comunicados".
			'category'              => $this->sanitize_category_list( (string) ( $settings['category'] ?? '' ) ),
			'tag'                   => sanitize_text_field( (string) ( $settings['tag'] ?? '' ) ),
			'author'                => sanitize_text_field( (string) ( $settings['author'] ?? '' ) ),
			'ignore_sticky'         => $flag( $settings['ignore_sticky'] ?? null, '1' ),
			'pagination'            => empty( $settings['pagination'] ) || $settings['pagination'] === '0' ? '0' : 'numbers',
		);
	}

	private function sanitize_ratio( string $raw ): string {
		$raw = trim( $raw );
		$ok  = array( 'auto', '1/1', '4/3', '3/2', '16/9', '3/4', '2/3', '21/9' );
		return in_array( $raw, $ok, true ) ? $raw : '16/9';
	}

	/** "12,34" or "slug-a,slug-b" → cleaned comma list. */
	private function sanitize_category_list( string $raw ): string {
		$raw = trim( $raw );
		if ( $raw === '' ) {
			return '';
		}
		// JSON array from multi-select UI.
		if ( $raw[0] === '[' ) {
			$dec = json_decode( $raw, true );
			if ( is_array( $dec ) ) {
				$parts = array();
				foreach ( $dec as $item ) {
					if ( is_array( $item ) ) {
						$parts[] = (string) ( $item['id'] ?? $item['slug'] ?? '' );
					} else {
						$parts[] = (string) $item;
					}
				}
				$raw = implode( ',', $parts );
			}
		}
		$out = array();
		foreach ( preg_split( '/[\s,;|]+/', $raw ) ?: array() as $p ) {
			$p = sanitize_text_field( trim( $p ) );
			if ( $p !== '' ) {
				$out[] = $p;
			}
		}
		return implode( ',', array_unique( $out ) );
	}

	/**
	 * Resolve a setting with tablet/mobile cascade (empty override = inherit).
	 *
	 * @param array<string, mixed> $s
	 */
	private function resolve_bp( array $s, string $key, string $default = '' ): array {
		$d = (string) ( $s[ $key ] ?? $default );
		$t = (string) ( $s[ $key . '_tablet' ] ?? '' );
		$m = (string) ( $s[ $key . '_mobile' ] ?? '' );
		$t = $t !== '' ? $t : $d;
		$m = $m !== '' ? $m : $t;
		return array( 'd' => $d, 't' => $t, 'm' => $m );
	}

	private function sanitize_id_list( string $raw ): string {
		$parts = preg_split( '/[\s,;]+/', $raw ) ?: array();
		$ids   = array();
		foreach ( $parts as $p ) {
			$n = absint( $p );
			if ( $n > 0 ) {
				$ids[] = $n;
			}
		}
		return implode( ',', array_unique( $ids ) );
	}

	/** @return list<int> */
	private function parse_ids( string $raw ): array {
		if ( $raw === '' ) {
			return array();
		}
		return array_values( array_filter( array_map( 'absint', explode( ',', $raw ) ) ) );
	}

	/**
	 * @param array<string, mixed> $s
	 * @return array<string, mixed>
	 */
	private function build_query_args( array $s, int $paged = 1 ): array {
		$source = (string) ( $s['query_source'] ?? 'post' );
		$ppp    = max( 1, (int) ( $s['posts_per_page'] ?? 6 ) );

		if ( $source === 'current_query' && ! is_admin() ) {
			global $wp_query;
			if ( $wp_query instanceof \WP_Query ) {
				$args                   = $wp_query->query_vars;
				$args['posts_per_page'] = $ppp;
				$args['paged']          = $paged;
				return $args;
			}
		}

		$post_type = 'post';
		if ( (string) ( $s['template_type'] ?? 'posts' ) === 'products' && post_type_exists( 'product' ) ) {
			$post_type = 'product';
		} elseif ( $source !== 'post' && $source !== 'current_query' && post_type_exists( $source ) ) {
			$post_type = $source;
		}

		$args = array(
			'post_type'           => $post_type,
			'post_status'         => 'publish',
			'posts_per_page'      => $ppp,
			'paged'               => max( 1, $paged ),
			'orderby'             => (string) ( $s['orderby'] ?? 'date' ),
			'order'               => (string) ( $s['order'] ?? 'DESC' ),
			'ignore_sticky_posts' => ( $s['ignore_sticky'] ?? '1' ) === '1',
			'no_found_rows'       => ( $s['pagination'] ?? '0' ) === '0',
		);

		$offset = (int) ( $s['offset'] ?? 0 );
		if ( $offset > 0 && $paged <= 1 ) {
			$args['offset'] = $offset;
		}

		$include = $this->parse_ids( (string) ( $s['include_ids'] ?? '' ) );
		if ( $include ) {
			$args['post__in'] = $include;
			$args['orderby']  = 'post__in';
		}

		$exclude = $this->parse_ids( (string) ( $s['exclude_ids'] ?? '' ) );
		if ( is_singular() ) {
			$cur = get_queried_object_id();
			if ( $cur ) {
				$exclude[] = $cur;
			}
		}
		if ( $exclude ) {
			$args['post__not_in'] = array_values( array_unique( $exclude ) );
		}

		$cat = trim( (string) ( $s['category'] ?? '' ) );
		if ( $cat !== '' && $post_type === 'post' ) {
			$ids   = array();
			$slugs = array();
			foreach ( explode( ',', $cat ) as $part ) {
				$part = trim( $part );
				if ( $part === '' ) {
					continue;
				}
				if ( ctype_digit( $part ) ) {
					$ids[] = (int) $part;
				} else {
					$slugs[] = sanitize_title( $part );
				}
			}
			if ( $ids ) {
				$args['category__in'] = array_values( array_unique( $ids ) );
			} elseif ( $slugs ) {
				// Multiple slugs: resolve to IDs for OR query.
				$resolved = array();
				foreach ( $slugs as $slug ) {
					$term = get_term_by( 'slug', $slug, 'category' );
					if ( $term && ! is_wp_error( $term ) ) {
						$resolved[] = (int) $term->term_id;
					}
				}
				if ( $resolved ) {
					$args['category__in'] = $resolved;
				} else {
					$args['category_name'] = implode( ',', $slugs );
				}
			}
		}

		$tag = trim( (string) ( $s['tag'] ?? '' ) );
		if ( $tag !== '' && $post_type === 'post' ) {
			if ( preg_match( '/^\d+(,\d+)*$/', $tag ) ) {
				$args['tag__in'] = array_map( 'absint', explode( ',', $tag ) );
			} else {
				$args['tag'] = $tag;
			}
		}

		$author = trim( (string) ( $s['author'] ?? '' ) );
		if ( $author !== '' ) {
			if ( is_numeric( $author ) ) {
				$args['author'] = absint( $author );
			} else {
				$args['author_name'] = sanitize_title( $author );
			}
		}

		return $args;
	}

	public function render( Node $node, array $context = array() ): string {
		$s      = $node->settings;
		$editor = ! empty( $context['editor'] );
		$paged  = 1;
		if ( ! $editor && isset( $_GET['ab_loop_page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$paged = max( 1, absint( $_GET['ab_loop_page'] ) ); // phpcs:ignore
		}

		$args  = $this->build_query_args( $s, $paged );
		$query = new WP_Query( $args );

		$cols_d   = max( 1, min( 6, (int) ( $s['columns'] ?? 3 ) ) );
		$cols_t   = max( 1, min( 6, (int) ( $s['columns_tablet'] ?? 2 ) ) );
		$cols_m   = max( 1, min( 3, (int) ( $s['columns_mobile'] ?? 1 ) ) );
		$gap_bp   = $this->resolve_bp( $s, 'gap', '24' );
		$gap_d    = max( 0, min( 80, (int) $gap_bp['d'] ) );
		$gap_t    = max( 0, min( 80, (int) $gap_bp['t'] ) );
		$gap_m    = max( 0, min( 80, (int) $gap_bp['m'] ) );
		$slide_bp = $this->resolve_bp( $s, 'as_slider', '0' );
		$as_slide = $slide_bp['d'] === '1'; // desktop default for markup structure

		$shadow_bp = $this->resolve_bp( $s, 'card_shadow', '0' );
		$border_bp = $this->resolve_bp( $s, 'card_border', '1' );
		$img_bp    = $this->resolve_bp( $s, 'show_image', '1' );
		$date_bp   = $this->resolve_bp( $s, 'show_date', '1' );
		$auth_bp   = $this->resolve_bp( $s, 'show_author', '1' );
		$title_bp  = $this->resolve_bp( $s, 'show_title', '1' );
		$ex_bp     = $this->resolve_bp( $s, 'show_excerpt', '1' );
		$more_bp   = $this->resolve_bp( $s, 'show_read_more', '1' );
		$imgpos_bp = $this->resolve_bp( $s, 'image_position', 'top' );

		$classes = array( 'ab-loop-grid' );
		if ( $as_slide ) {
			$classes[] = 'ab-loop-grid--slider';
		}
		if ( $shadow_bp['d'] === '1' ) {
			$classes[] = 'ab-loop-grid--shadow';
		}
		if ( $border_bp['d'] === '1' ) {
			$classes[] = 'ab-loop-grid--border';
		}

		$style = sprintf(
			'--ab-loop-cols:%d;--ab-loop-cols-t:%d;--ab-loop-cols-m:%d;'
			. '--ab-loop-gap:%dpx;--ab-loop-gap-t:%dpx;--ab-loop-gap-m:%dpx',
			$as_slide ? 1 : $cols_d,
			$slide_bp['t'] === '1' ? 1 : $cols_t,
			$slide_bp['m'] === '1' ? 1 : $cols_m,
			$gap_d,
			$gap_t,
			$gap_m
		);

		// Data attrs drive CSS visibility per breakpoint for card parts.
		$data_resp = sprintf(
			' data-img-d="%s" data-img-t="%s" data-img-m="%s"'
			. ' data-date-d="%s" data-date-t="%s" data-date-m="%s"'
			. ' data-auth-d="%s" data-auth-t="%s" data-auth-m="%s"'
			. ' data-title-d="%s" data-title-t="%s" data-title-m="%s"'
			. ' data-ex-d="%s" data-ex-t="%s" data-ex-m="%s"'
			. ' data-more-d="%s" data-more-t="%s" data-more-m="%s"'
			. ' data-shadow-d="%s" data-shadow-t="%s" data-shadow-m="%s"'
			. ' data-border-d="%s" data-border-t="%s" data-border-m="%s"'
			. ' data-imgpos-d="%s" data-imgpos-t="%s" data-imgpos-m="%s"'
			. ' data-slide-d="%s" data-slide-t="%s" data-slide-m="%s"',
			esc_attr( $img_bp['d'] ),
			esc_attr( $img_bp['t'] ),
			esc_attr( $img_bp['m'] ),
			esc_attr( $date_bp['d'] ),
			esc_attr( $date_bp['t'] ),
			esc_attr( $date_bp['m'] ),
			esc_attr( $auth_bp['d'] ),
			esc_attr( $auth_bp['t'] ),
			esc_attr( $auth_bp['m'] ),
			esc_attr( $title_bp['d'] ),
			esc_attr( $title_bp['t'] ),
			esc_attr( $title_bp['m'] ),
			esc_attr( $ex_bp['d'] ),
			esc_attr( $ex_bp['t'] ),
			esc_attr( $ex_bp['m'] ),
			esc_attr( $more_bp['d'] ),
			esc_attr( $more_bp['t'] ),
			esc_attr( $more_bp['m'] ),
			esc_attr( $shadow_bp['d'] ),
			esc_attr( $shadow_bp['t'] ),
			esc_attr( $shadow_bp['m'] ),
			esc_attr( $border_bp['d'] ),
			esc_attr( $border_bp['t'] ),
			esc_attr( $border_bp['m'] ),
			esc_attr( $imgpos_bp['d'] ),
			esc_attr( $imgpos_bp['t'] ),
			esc_attr( $imgpos_bp['m'] ),
			esc_attr( $slide_bp['d'] ),
			esc_attr( $slide_bp['t'] ),
			esc_attr( $slide_bp['m'] )
		);

		if ( ! $query->have_posts() ) {
			wp_reset_postdata();
			return $editor
				? '<div class="' . esc_attr( implode( ' ', $classes ) ) . ' ab-empty" style="' . esc_attr( $style ) . '">Loop de consulta — nenhum post encontrado.</div>'
				: '<div class="' . esc_attr( implode( ' ', $classes ) ) . ' ab-loop-grid--empty" style="' . esc_attr( $style ) . '"><p>' . esc_html__( 'Nenhum item encontrado.', 'auranet-page-theme-builder' ) . '</p></div>';
		}

		$count = (int) $query->post_count;
		$html  = '<div class="' . esc_attr( implode( ' ', $classes ) ) . '" style="' . esc_attr( $style ) . '" data-ab-loop="1"'
			. $data_resp
			. ( $as_slide ? ' data-ab-loop-slider="1" data-ab-loop-count="' . esc_attr( (string) $count ) . '"' : '' ) . '>';

		if ( $as_slide ) {
			$html .= '<div class="ab-loop-slider-viewport">';
		}

		$html .= '<div class="ab-loop-grid__list' . ( $as_slide ? ' ab-loop-slider__track' : '' ) . '">';

		$i = 0;
		while ( $query->have_posts() ) {
			$query->the_post();
			$html .= $this->render_card( $s, $editor, $i );
			++$i;
		}
		$html .= '</div>';

		// Dots over the slides (horizontal, on top of image area).
		if ( $as_slide && $count > 1 ) {
			$html .= '<div class="ab-loop-slider__dots" role="tablist" aria-label="Slides">';
			for ( $d = 0; $d < $count; $d++ ) {
				$html .= '<button type="button" class="ab-loop-slider__dot' . ( $d === 0 ? ' is-active' : '' ) . '" data-ab-loop-dot="' . esc_attr( (string) $d ) . '" aria-label="' . esc_attr( sprintf( /* translators: slide number */ __( 'Slide %d', 'auranet-page-theme-builder' ), $d + 1 ) ) . '"></button>';
			}
			$html .= '</div>';
			$html .= '<div class="ab-loop-slider__nav">'
				. '<button type="button" class="ab-loop-slider__btn" data-ab-loop-prev aria-label="Anterior">‹</button>'
				. '<button type="button" class="ab-loop-slider__btn" data-ab-loop-next aria-label="Próximo">›</button>'
				. '</div>';
		}

		if ( $as_slide ) {
			$html .= '</div>'; // viewport
		}

		if ( ! $as_slide && ( $s['pagination'] ?? '0' ) === 'numbers' && $query->max_num_pages > 1 && ! $editor ) {
			$html .= $this->render_pagination( $query->max_num_pages, $paged );
		}

		$html .= '</div>';
		wp_reset_postdata();
		return $html;
	}

	/**
	 * Resolve featured image URL robustly (size fallbacks).
	 */
	private function get_featured_html( int $post_id, string $size ): string {
		$sizes = array( $size, 'large', 'medium_large', 'medium', 'full', 'thumbnail' );
		$sizes = array_values( array_unique( $sizes ) );
		foreach ( $sizes as $sz ) {
			$html = get_the_post_thumbnail(
				$post_id,
				$sz,
				array(
					'class'   => 'ab-loop-card__img',
					'loading' => 'lazy',
					'decoding'=> 'async',
				)
			);
			if ( is_string( $html ) && $html !== '' ) {
				return $html;
			}
		}
		// Fallback: first image attachment in post.
		$atts = get_attached_media( 'image', $post_id );
		if ( $atts ) {
			$first = reset( $atts );
			if ( $first instanceof \WP_Post ) {
				$src = wp_get_attachment_image_url( $first->ID, 'large' );
				if ( $src ) {
					return '<img class="ab-loop-card__img" src="' . esc_url( $src ) . '" alt="" loading="lazy" decoding="async"/>';
				}
			}
		}
		return '';
	}

	/**
	 * @param array<string, mixed> $s
	 */
	private function render_card( array $s, bool $editor, int $index = 0 ): string {
		$id        = (int) get_the_ID();
		$title     = get_the_title();
		$link      = get_permalink() ?: '#';
		// Render for any BP that needs the part; CSS hides per device.
		$img_bp    = $this->resolve_bp( $s, 'show_image', '1' );
		$title_bp  = $this->resolve_bp( $s, 'show_title', '1' );
		$date_bp   = $this->resolve_bp( $s, 'show_date', '1' );
		$auth_bp   = $this->resolve_bp( $s, 'show_author', '1' );
		$ex_bp     = $this->resolve_bp( $s, 'show_excerpt', '1' );
		$more_bp   = $this->resolve_bp( $s, 'show_read_more', '1' );
		$imgpos_bp = $this->resolve_bp( $s, 'image_position', 'top' );
		$titlepos  = $this->resolve_bp( $s, 'title_position', 'outside' );
		$img_pos   = $imgpos_bp['d'];
		$show_img  = $img_bp['d'] === '1' || $img_bp['t'] === '1' || $img_bp['m'] === '1';
		$show_title = $title_bp['d'] === '1' || $title_bp['t'] === '1' || $title_bp['m'] === '1';
		$title_over = $show_title && $titlepos['d'] === 'over_image' && $show_img;

		$card_cls = array( 'ab-loop-card', 'post-' . (string) $id, 'ab-loop-card--img-' . $img_pos );
		if ( $title_over ) {
			$card_cls[] = 'ab-loop-card--title-over';
		}
		if ( ( $s['card_shadow'] ?? '0' ) === '1' ) {
			$card_cls[] = 'has-shadow';
		}
		if ( ( $s['card_border'] ?? '1' ) === '1' ) {
			$card_cls[] = 'has-border';
		}
		if ( $index === 0 ) {
			$card_cls[] = 'is-active-slide';
		}

		$media_inner = '';
		$ratio       = (string) ( $s['image_ratio'] ?? '16/9' );
		$fit         = (string) ( $s['image_object_fit'] ?? 'cover' );
		if ( ! in_array( $fit, array( 'cover', 'contain', 'fill' ), true ) ) {
			$fit = 'cover';
		}
		$obj_pos = self::sanitize_object_position( $s['image_object_position'] ?? '50% 50%' );
		// CSS vars beat theme img{height:auto}; wrap uses aspect-ratio + absolute img.
		$ratio_var = $ratio === 'auto' ? 'auto' : str_replace( ':', ' / ', $ratio );
		if ( $ratio !== 'auto' && strpos( $ratio_var, '/' ) === false ) {
			$ratio_var = str_replace( ' ', '', $ratio ); // "16/9"
			$ratio_var = str_replace( '/', ' / ', $ratio_var );
		}
		$wrap_style = '--ab-loop-ratio:' . esc_attr( $ratio_var ) . ';--ab-loop-fit:' . esc_attr( $fit ) . ';--ab-loop-pos:' . esc_attr( $obj_pos ) . ';';

		if ( $show_img ) {
			$size = (string) ( $s['image_size'] ?? 'large' );
			$img  = $this->get_featured_html( $id, $size );
			if ( $img !== '' ) {
				// Class for CSS; strip size attrs that fight cover layout.
				$img = preg_replace(
					'/<img\b([^>]*)>/i',
					'<img class="ab-loop-card__img"$1 />',
					$img,
					1
				) ?? $img;
				// Ensure class even if img already had class.
				if ( strpos( $img, 'ab-loop-card__img' ) === false ) {
					$img = preg_replace( '/<img\b/i', '<img class="ab-loop-card__img"', $img, 1 ) ?? $img;
				}
				$media_inner = $img;
			} else {
				$media_inner = '<div class="ab-loop-card__media-ph" aria-hidden="true"></div>';
			}
		}

		$title_html = '';
		if ( $show_title ) {
			$title_html = '<h3 class="ab-loop-card__title"><a href="' . esc_url( $link ) . '">' . esc_html( $title ) . '</a></h3>';
		}

		$cat_badge = '';
		if ( ( $s['show_category'] ?? '0' ) === '1' ) {
			$terms = get_the_category( $id );
			if ( $terms ) {
				$names = array();
				foreach ( array_slice( $terms, 0, 3 ) as $term ) {
					$names[] = esc_html( $term->name );
				}
				if ( $names ) {
					$cat_badge = '<span class="ab-loop-card__cat">' . implode( ' · ', $names ) . '</span>';
				}
			}
		}

		$media = '';
		if ( $show_img ) {
			$link_img = ( $s['image_link'] ?? '1' ) === '1';
			$media    = '<div class="ab-loop-card__media-wrap" style="' . esc_attr( $wrap_style ) . '">';
			if ( $cat_badge ) {
				$media .= $cat_badge;
			}
			if ( $link_img ) {
				$media .= '<a class="ab-loop-card__media" href="' . esc_url( $link ) . '">' . $media_inner . '</a>';
			} else {
				$media .= '<div class="ab-loop-card__media">' . $media_inner . '</div>';
			}
			if ( $title_over ) {
				$media .= '<div class="ab-loop-card__overlay">' . $title_html . '</div>';
			}
			$media .= '</div>';
		}

		$body = '<div class="ab-loop-card__body">';

		$meta_bits = array();
		if ( $date_bp['d'] === '1' || $date_bp['t'] === '1' || $date_bp['m'] === '1' ) {
			$date_fmt    = (string) get_option( 'date_format', 'd/m/Y' );
			$meta_bits[] = '<time class="ab-loop-card__date" datetime="' . esc_attr( get_the_date( 'c' ) ) . '">' . esc_html( get_the_date( $date_fmt ) ) . '</time>';
		}
		if ( $auth_bp['d'] === '1' || $auth_bp['t'] === '1' || $auth_bp['m'] === '1' ) {
			$meta_bits[] = '<span class="ab-loop-card__author">' . esc_html( get_the_author() ) . '</span>';
		}
		if ( $meta_bits ) {
			$body .= '<div class="ab-loop-card__meta">' . implode( '<span class="ab-loop-card__sep">·</span>', $meta_bits ) . '</div>';
		}

		// Title outside the image.
		if ( $show_title && ! $title_over ) {
			$body .= $title_html;
		}

		if ( $ex_bp['d'] === '1' || $ex_bp['t'] === '1' || $ex_bp['m'] === '1' ) {
			$len = max( 5, (int) ( $s['excerpt_length'] ?? 18 ) );
			$ex  = wp_trim_words( wp_strip_all_tags( (string) get_the_excerpt() ), $len, '…' );
			$body .= '<p class="ab-loop-card__excerpt">' . esc_html( $ex ) . '</p>';
		}

		if ( $more_bp['d'] === '1' || $more_bp['t'] === '1' || $more_bp['m'] === '1' ) {
			$txt   = (string) ( $s['read_more_text'] ?? 'Leia mais' );
			$body .= '<a class="ab-loop-card__more" href="' . esc_url( $link ) . '">' . esc_html( $txt ) . '</a>';
		}

		$body .= '</div>';

		$out = '<article class="' . esc_attr( implode( ' ', $card_cls ) ) . '" data-slide-index="' . esc_attr( (string) $index ) . '">';
		if ( $img_pos === 'right' ) {
			$out .= $body . $media;
		} else {
			$out .= $media . $body;
		}
		$out .= '</article>';
		return $out;
	}

	private function render_pagination( int $max, int $current ): string {
		$html = '<nav class="ab-loop-pagination" aria-label="' . esc_attr__( 'Paginação', 'auranet-page-theme-builder' ) . '">';
		for ( $i = 1; $i <= $max; $i++ ) {
			$url   = esc_url( add_query_arg( 'ab_loop_page', $i ) );
			$cls   = $i === $current ? ' is-current' : '';
			$html .= '<a class="ab-loop-pagination__item' . $cls . '" href="' . $url . '">' . esc_html( (string) $i ) . '</a>';
		}
		$html .= '</nav>';
		return $html;
	}
}
