<?php
/**
 * Central rendering engine for shortcodes, blocks, and SiteOrigin widgets.
 *
 * @package Rsmspark_Content_Widgets
 */

defined( 'ABSPATH' ) || exit;

/**
 * Rsmspark_Content_Widgets_Render
 *
 * Refactored in compat branch - Phase 0
 */
class Rsmspark_Content_Widgets_Render {

	/**
	 * Widget ids that requested front-end assets on this request.
	 *
	 * @var array<string, bool>
	 */
	private static $assets_needed = array();

	/**
	 * Whether shared assets were marked required.
	 *
	 * @var bool
	 */
	private static $shared_assets_needed = false;

	/**
	 * @param string $registry_id Widget registry id.
	 * @return bool
	 */
	public static function should_render( $registry_id ) {
		if ( ! Rsmspark_Content_Widgets_Settings::is_enabled() ) {
			return false;
		}

		$id = sanitize_key( (string) $registry_id );
		if ( ! Rsmspark_Content_Widgets_Settings::is_widget_enabled( $id ) ) {
			return false;
		}

		// Block editor ServerSideRender must preview enabled widgets without a Pro license gate.
		if (
			class_exists( 'Rsmspark_Content_Widgets_Blocks', false )
			&& Rsmspark_Content_Widgets_Blocks::is_editor_preview_request()
		) {
			return (bool) apply_filters( 'rsmspark_content_widgets_should_render', true, $id );
		}

		if ( Rsmspark_Content_Widgets_Registry::is_pro_widget( $id ) ) {
			$feature = Rsmspark_Content_Widgets_Registry::required_feature( $id );
			if ( ! Rsmspark_Content_Widgets_License::has_feature( $feature ) ) {
				return false;
			}
		}

		return (bool) apply_filters( 'rsmspark_content_widgets_should_render', true, $id );
	}

	/**
	 * @param string               $registry_id Widget registry id.
	 * @param string               $template    Template filename.
	 * @param array<string, mixed> $args        Template arguments.
	 * @return string
	 */
	public static function render_template( $registry_id, $template, array $args = array() ) {
		if ( ! self::should_render( $registry_id ) ) {
			if ( Rsmspark_Content_Widgets_Settings::get()['debug_mode'] ?? false ) {
				return sprintf(
					'<!-- rsmspark-content-widgets: widget disabled or blocked: %s -->',
					esc_html( sanitize_key( (string) $registry_id ) )
				);
			}
			return '';
		}

		$path = self::template_path( $registry_id, $template );
		if ( ! $path || ! is_readable( $path ) ) {
			if ( Rsmspark_Content_Widgets_Settings::get()['debug_mode'] ?? false ) {
				return sprintf(
					'<!-- rsmspark-content-widgets: missing template %1$s for %2$s (checked templates/%2$s and templates/%3$s) -->',
					esc_html( $template ),
					esc_html( sanitize_key( (string) $registry_id ) ),
					esc_html( Rsmspark_Content_Widgets_Registry::siteorigin_folder_id( $registry_id ) )
				);
			}
			return '';
		}

		self::mark_assets_needed( $registry_id );

		$wrap_embed = self::should_wrap_embed( $args );

		$args = apply_filters( 'rsmspark_content_widgets_template_args', $args, $registry_id, $template );
		if ( ! is_array( $args ) ) {
			$args = array();
		}

		$embed_context = self::extract_embed_context( $args );

		ob_start();
		// phpcs:ignore WordPress.PHP.DontExtract.extract_extract -- scoped template vars.
		extract( $args, EXTR_SKIP );
		include $path;
		$html = (string) ob_get_clean();

		$html = (string) apply_filters( 'rsmspark_content_widgets_render_output', $html, $registry_id, $args );

		if ( '' === trim( $html ) && ( Rsmspark_Content_Widgets_Settings::get()['debug_mode'] ?? false ) ) {
			return sprintf(
				'<!-- rsmspark-content-widgets: empty render for %1$s via %2$s -->',
				esc_html( sanitize_key( (string) $registry_id ) ),
				esc_html( wp_basename( $path ) )
			);
		}

		return self::wrap_embed( $html, $embed_context, $wrap_embed );
	}

	/**
	 * Echo a rendered template. Markup is escaped inside template partials.
	 *
	 * @param string               $registry_id Widget registry id.
	 * @param string               $template    Template filename.
	 * @param array<string, mixed> $args        Template arguments.
	 * @return void
	 */
	public static function print_template( $registry_id, $template, array $args = array() ) {
		echo wp_kses( self::render_template( $registry_id, $template, $args ), Rsmspark_Content_Widgets_Kses::allowed_html() );
	}

	/**
	 * @param array<string, mixed> $args Template/render arguments (by reference).
	 * @return array<string, mixed>
	 */
	public static function extract_embed_context( array &$args ) {
		$context = array();
		foreach ( array( 'embed_width', 'embed_align', 'rsmEmbedWidth', 'rsmEmbedAlign', '_wrap_embed' ) as $key ) {
			if ( array_key_exists( $key, $args ) ) {
				$context[ $key ] = $args[ $key ];
				unset( $args[ $key ] );
			}
		}

		return $context;
	}

	/**
	 * @param array<string, mixed> $args Render arguments.
	 * @return bool
	 */
	public static function should_wrap_embed( array $args ) {
		if ( array_key_exists( '_wrap_embed', $args ) ) {
			return (bool) $args['_wrap_embed'];
		}

		if (
			( class_exists( 'Rsmspark_Content_Widgets_Blocks', false ) && Rsmspark_Content_Widgets_Blocks::is_in_block_render() )
			|| doing_filter( 'render_block' )
			|| doing_shortcode()
		) {
			return false;
		}

		return ! (bool) apply_filters( 'rsmspark_embed_chrome_skip_wrap', false, $args );
	}

	/**
	 * @param string               $html       Rendered markup.
	 * @param array<string, mixed> $context    Embed settings source.
	 * @param bool                 $wrap_embed Whether to wrap.
	 * @return string
	 */
	public static function wrap_embed( $html, array $context, $wrap_embed = true ) {
		$html = (string) $html;
		if ( ! $wrap_embed || '' === trim( $html ) || ! class_exists( 'Rsmspark_Embed_Chrome', false ) ) {
			return $html;
		}

		return Rsmspark_Embed_Chrome::wrap( $html, Rsmspark_Embed_Chrome::settings_from_array( $context ) );
	}

	/**
	 * Enqueue embed width CSS from this plugin's bundle (avoids 404 from another suite plugin).
	 *
	 * @return void
	 */
	public static function ensure_embed_chrome_styles() {
		if ( is_admin() ) {
			return;
		}

		$css_path = RSMSPARK_CONTENT_WIDGETS_PATH . 'rsm-shared/assets/css/rsm-embed-chrome.css';
		if ( ! is_readable( $css_path ) ) {
			return;
		}

		$css_url = plugins_url( 'rsm-shared/assets/css/rsm-embed-chrome.css', RSMSPARK_CONTENT_WIDGETS_FILE );
		$version = (string) filemtime( $css_path );

		if ( wp_style_is( 'rsm-embed-chrome', 'registered' ) ) {
			wp_deregister_style( 'rsm-embed-chrome' );
		}

		wp_enqueue_style( 'rsm-embed-chrome', $css_url, array(), $version );
	}

	/**
	 * Preserve embed width keys when widget attribute parsers strip unknown shortcode attrs.
	 *
	 * @return void
	 */
	public static function init_embed_support() {
		static $booted = false;
		if ( $booted ) {
			return;
		}
		$booted = true;

		add_filter(
			'rsmspark_content_widgets_parsed_atts',
			static function ( $merged, $context, $raw ) {
				unset( $context );
				if ( ! is_array( $merged ) || ! is_array( $raw ) ) {
					return $merged;
				}
				foreach ( array( 'embed_width', 'embed_align', 'rsmEmbedWidth', 'rsmEmbedAlign' ) as $key ) {
					if ( array_key_exists( $key, $raw ) ) {
						$merged[ $key ] = $raw[ $key ];
					}
				}
				return $merged;
			},
			10,
			3
		);
	}

	/**
	 * Resolve template path: templates/{id}/ first, then legacy widgets/{folder}/.
	 *
	 * @param string $registry_id Widget registry id.
	 * @param string $template    Template filename.
	 * @return string
	 */
	public static function template_path( $registry_id, $template ) {
		$id         = sanitize_key( (string) $registry_id );
		$template   = sanitize_file_name( (string) $template );
		$folder_id  = Rsmspark_Content_Widgets_Registry::siteorigin_folder_id( $id );
		$tpl_file   = preg_replace( '/\.php$/', '.tpl.php', $template );
		$candidates = array();

		foreach ( array_unique( array( $id, $folder_id ) ) as $dir ) {
			$candidates[] = RSMSPARK_CONTENT_WIDGETS_PATH . 'templates/' . $dir . '/' . $template;
		}

		// SiteOrigin .tpl.php markup — never use widgets/*/*.php (those are widget class files).
		$candidates[] = RSMSPARK_CONTENT_WIDGETS_PATH . 'widgets/' . $folder_id . '/' . $tpl_file;

		foreach ( $candidates as $candidate ) {
			if ( is_readable( $candidate ) ) {
				return $candidate;
			}
		}

		return '';
	}

	/**
	 * @param array<string, mixed> $atts       Raw shortcode/block attributes.
	 * @param array<string, mixed> $defaults   Default attribute map.
	 * @param string               $context    Sanitization context key.
	 * @return array<string, mixed>
	 */
	public static function parse_atts( array $atts, array $defaults, $context = 'default' ) {
		$merged = shortcode_atts( $defaults, $atts, $context );
		return (array) apply_filters( 'rsmspark_content_widgets_parsed_atts', $merged, $context, $atts );
	}

	/**
	 * @param string $registry_id Widget registry id.
	 * @return void
	 */
	public static function mark_assets_needed( $registry_id ) {
		$id = sanitize_key( (string) $registry_id );
		self::$assets_needed[ $id ] = true;
		self::$shared_assets_needed = true;
		do_action( 'rsmspark_content_widgets_assets_needed', $id );

		self::ensure_embed_chrome_styles();

		// Blocks/widgets render during the_content — after wp_enqueue_scripts.
		if ( did_action( 'wp_enqueue_scripts' ) ) {
			Rsmspark_Content_Widgets_Assets::enqueue_for_widget( $id );
		}
	}

	/**
	 * @return array<string, bool>
	 */
	public static function assets_needed() {
		return self::$assets_needed;
	}

	/**
	 * @return bool
	 */
	public static function shared_assets_needed() {
		return self::$shared_assets_needed;
	}

	/**
	 * @param mixed $media Attachment id or SiteOrigin media array.
	 * @return string
	 */
	public static function media_url( $media, $size = 'large' ) {
		if ( empty( $media ) ) {
			return '';
		}

		$id = 0;
		if ( is_numeric( $media ) ) {
			$id = (int) $media;
		} elseif ( is_array( $media ) ) {
			if ( ! empty( $media['ID'] ) ) {
				$id = (int) $media['ID'];
			} elseif ( ! empty( $media['id'] ) ) {
				$id = (int) $media['id'];
			}
		}

		if ( ! $id ) {
			return '';
		}

		$size = sanitize_key( (string) $size );
		if ( '' === $size ) {
			$size = 'large';
		}

		$url = wp_get_attachment_image_url( $id, $size );
		if ( ! $url && 'full' !== $size ) {
			$url = wp_get_attachment_image_url( $id, 'full' );
		}
		if ( ! $url ) {
			$url = wp_get_attachment_url( $id );
		}

		return $url ? (string) $url : '';
	}

	/**
	 * Resolve a SiteOrigin-style link field to a permalink or URL.
	 *
	 * @param mixed $link Link field value.
	 * @return string
	 */
	public static function link_url( $link ) {
		if ( empty( $link ) ) {
			return '';
		}

		if ( is_array( $link ) ) {
			if ( ! empty( $link['url'] ) ) {
				$link = $link['url'];
			} elseif ( ! empty( $link['post'] ) ) {
				$link = 'post:' . (int) $link['post'];
			} else {
				return '';
			}
		}

		$link = trim( (string) $link );
		if ( '' === $link ) {
			return '';
		}

		// Preserve same-page anchors (e.g. #rp-popup-1068) for integrations like RSM Popups.
		if ( str_starts_with( $link, '#' ) ) {
			$fragment = substr( $link, 1 );
			$fragment = preg_replace( '/[^a-zA-Z0-9\-_.%~]/', '', (string) $fragment );

			return '#' . $fragment;
		}

		if ( function_exists( 'sow_esc_url_raw' ) ) {
			$resolved = sow_esc_url_raw( $link );
			if ( ! $resolved ) {
				return '';
			}

			$resolved = (string) $resolved;

			// SiteOrigin can normalize some in-page links down to a bare trailing '#'.
			// If the original URL had a valid fragment, restore it.
			if ( str_contains( $link, '#' ) && str_ends_with( $resolved, '#' ) ) {
				$fragment = (string) wp_parse_url( $link, PHP_URL_FRAGMENT );
				$fragment = preg_replace( '/[^a-zA-Z0-9\-_.%~]/', '', $fragment );
				if ( '' !== $fragment ) {
					return rtrim( $resolved, '#' ) . '#' . $fragment;
				}
			}

			return $resolved;
		}

		if ( preg_match( '/^post:\s*(\d+)/i', $link, $matches ) ) {
			$permalink = get_permalink( (int) $matches[1] );
			return $permalink ? (string) $permalink : '';
		}

		return (string) esc_url_raw( $link );
	}

	/**
	 * Decode a JSON shortcode/block attribute into an array.
	 *
	 * @param mixed                $value   Raw attribute value.
	 * @param array<string, mixed> $default Fallback when decode fails.
	 * @return array<string, mixed>
	 */
	public static function parse_json_attr( $value, array $default = array() ) {
		if ( is_array( $value ) ) {
			return $value;
		}

		if ( ! is_string( $value ) || '' === trim( $value ) ) {
			return $default;
		}

		$decoded = json_decode( html_entity_decode( $value, ENT_QUOTES, 'UTF-8' ), true );
		return is_array( $decoded ) ? $decoded : $default;
	}

	/**
	 * SiteOrigin section forms store values under a section key.
	 *
	 * @param array<string, mixed> $instance Widget instance.
	 * @param string               $section  Section id.
	 * @return array<string, mixed>
	 */
	public static function section_instance( $instance, $section = 'content' ) {
		if ( ! is_array( $instance ) ) {
			return array();
		}
		if ( isset( $instance[ $section ] ) && is_array( $instance[ $section ] ) ) {
			return $instance[ $section ];
		}
		return $instance;
	}

	/**
	 * @return string
	 */
	public static function inline_style_vars() {
		$settings = Rsmspark_Content_Widgets_Settings::get();
		$color    = sanitize_hex_color( (string) ( $settings['primary_color'] ?? '#2563eb' ) ) ?: '#2563eb';
		$radius   = max( 0, absint( $settings['border_radius'] ?? 8 ) );
		return sprintf( '--rsmspark-cw-primary:%1$s;--rsmspark-cw-radius:%2$dpx;', esc_attr( $color ), $radius );
	}

	/**
	 * Wrapper attributes shared by widget output.
	 *
	 * @param string               $registry_id Widget id.
	 * @param array<string, mixed> $extra       Extra HTML attributes.
	 * @return string
	 */
	public static function wrapper_attributes( $registry_id, array $extra = array() ) {
		$id = sanitize_key( (string) $registry_id );

		$attrs = array_merge(
			array(
				'class' => 'rsm-widget rsm-widget--' . $id,
				'style' => self::inline_style_vars(),
			),
			$extra
		);

		$parts = array();
		foreach ( $attrs as $key => $value ) {
			if ( '' === $value || null === $value ) {
				continue;
			}
			$parts[] = sprintf( '%s="%s"', esc_attr( (string) $key ), esc_attr( (string) $value ) );
		}

		return implode( ' ', $parts );
	}

	/**
	 * Stable hash slug for tabs/accordion items.
	 *
	 * @param string             $title Item title.
	 * @param int                $index Zero-based index.
	 * @param array<int, string> $used  Slugs already assigned in this list.
	 * @return string
	 */
	public static function item_hash_slug( $title, $index, array $used = array() ) {
		$base = sanitize_title( (string) $title );
		if ( '' === $base ) {
			$base = 'item-' . ( (int) $index + 1 );
		}

		$slug = $base;
		$n    = 2;
		while ( in_array( $slug, $used, true ) ) {
			$slug = $base . '-' . $n;
			++$n;
		}

		return $slug;
	}

	/**
	 * Normalize checkbox / boolean values from shortcodes, blocks, or SiteOrigin.
	 *
	 * @param mixed $value Raw value.
	 * @return bool
	 */
	public static function boolish( $value ) {
		if ( is_bool( $value ) ) {
			return $value;
		}

		if ( is_numeric( $value ) ) {
			return (int) $value === 1;
		}

		$string = strtolower( trim( (string) $value ) );
		if ( '' === $string ) {
			return false;
		}

		return in_array( $string, array( '1', 'true', 'yes', 'on' ), true );
	}
}
