<?php
/**
 * Central widget registry: load, normalize, filter, and flatten sifency-widgets.php.
 *
 * @package Sifency_Addons
 */

namespace Sifency_Addons\Includes\Elementor;

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

/**
 * Single source for the categorized widget list used by Elementor registration and admin UI.
 */
class Sifency_Addons_Widget_Registry {

	/** @var array<string, array<string, array>>|null */
	private static $categorized_cache = null;

	/**
	 * Path to the core widget definition file.
	 */
	public static function get_config_file(): string {
		return SIFENCY_ADDONS_DIR . 'includes/elementor/sifency-widgets.php';
	}

	/**
	 * Loads the raw widget config from disk (unfiltered).
	 *
	 * @return array<string, array<string, array>>
	 */
	public static function load_raw(): array {
		$file = self::get_config_file();

		if ( ! file_exists( $file ) ) {
			return array();
		}

		$config = include $file;

		return is_array( $config ) ? $config : array();
	}

	/**
	 * Categorized widget list after normalization and all list-level filters.
	 *
	 * Filters applied (in order):
	 * 1. sifency_addons_widget_list — add/remove/reorder categories and widgets
	 * 2. sifency_addons_excluded_widget_categories — category keys to drop
	 * 3. sifency_addons_excluded_widgets — widget IDs to drop (all categories)
	 *
	 * @return array<string, array<string, array>>
	 */
	public static function get_categorized(): array {
		if ( null !== self::$categorized_cache ) {
			return self::$categorized_cache;
		}

		$categorized = self::normalize_categorized( self::load_raw() );

		/**
		 * Filter the full categorized widget registry before exclusions.
		 *
		 * Use this to merge categories, add widgets from another plugin, or reorder.
		 * Structure: [ 'Category Name' => [ 'widget-id' => [ 'title', 'class', 'file', ... ], ... ], ... ]
		 *
		 * @param array<string, array<string, array>> $categorized Categorized widgets.
		 */
		$categorized = apply_filters( 'sifency_addons_widget_list', $categorized );
		$categorized = self::normalize_categorized( $categorized );

		$excluded_categories = apply_filters( 'sifency_addons_excluded_widget_categories', array() );
		if ( is_array( $excluded_categories ) && ! empty( $excluded_categories ) ) {
			foreach ( $excluded_categories as $category_key ) {
				unset( $categorized[ $category_key ] );
			}
		}

		$excluded_widgets = apply_filters( 'sifency_addons_excluded_widgets', array() );
		if ( is_array( $excluded_widgets ) && ! empty( $excluded_widgets ) ) {
			$categorized = self::remove_widgets_by_id( $categorized, $excluded_widgets );
		}

		$categorized = self::drop_empty_categories( $categorized );

		self::$categorized_cache = $categorized;

		return self::$categorized_cache;
	}

	/**
	 * Flat widget map keyed by widget ID (for Elementor registration).
	 *
	 * @return array<string, array>
	 */
	public static function get_flat(): array {
		$flat = array();

		foreach ( self::get_categorized() as $category => $widgets ) {
			if ( ! is_array( $widgets ) ) {
				continue;
			}
			foreach ( $widgets as $widget_id => $widget_data ) {
				if ( ! is_array( $widget_data ) ) {
					continue;
				}
				$widget_data['_registry_category'] = $category;
				$flat[ $widget_id ]                = $widget_data;
			}
		}

		/**
		 * Filter the flattened widget config used at Elementor registration.
		 *
		 * @param array<string, array> $flat Widget ID => config.
		 */
		return apply_filters( 'sifency_addons_all_widgets_config', $flat );
	}

	/**
	 * Whether a widget should be registered with Elementor (after admin on/off and built-in checks).
	 *
	 * @param string               $widget_id   Widget registry key.
	 * @param array<string, mixed> $widget_data Widget config entry.
	 */
	public static function should_register( string $widget_id, array $widget_data ): bool {
		/**
		 * Final gate before a widget class is loaded and registered.
		 *
		 * @param bool                 $register    Default true.
		 * @param string               $widget_id   Widget registry key.
		 * @param array<string, mixed> $widget_data Widget config entry.
		 */
		return (bool) apply_filters( 'sifency_addons_register_widget', true, $widget_id, $widget_data );
	}

	/**
	 * Lookup a single widget config by ID (searches filtered registry).
	 *
	 * @param string $widget_id Widget registry key.
	 * @return array<string, mixed>
	 */
	public static function get_widget( string $widget_id ): array {
		foreach ( self::get_categorized() as $widgets ) {
			if ( isset( $widgets[ $widget_id ] ) && is_array( $widgets[ $widget_id ] ) ) {
				return $widgets[ $widget_id ];
			}
		}

		return array();
	}

	/**
	 * Clears in-request cache (e.g. after tests or late filter registration).
	 */
	public static function clear_cache(): void {
		self::$categorized_cache = null;
	}

	/**
	 * Ensures every widget has a string ID key; supports entries added with numeric keys.
	 *
	 * @param array<string, array<string, array>> $categorized Raw or filtered categorized list.
	 * @return array<string, array<string, array>>
	 */
	public static function normalize_categorized( array $categorized ): array {
		$normalized = array();

		foreach ( $categorized as $category => $widgets ) {
			if ( ! is_string( $category ) || '' === $category || ! is_array( $widgets ) ) {
				continue;
			}

			$category_widgets = array();

			foreach ( $widgets as $widget_key => $widget_data ) {
				if ( ! is_array( $widget_data ) ) {
					continue;
				}

				$widget_id = self::resolve_widget_id( $widget_key, $widget_data );

				if ( '' === $widget_id ) {
					continue;
				}

				$category_widgets[ $widget_id ] = $widget_data;
			}

			if ( ! empty( $category_widgets ) ) {
				$normalized[ $category ] = $category_widgets;
			}
		}

		return $normalized;
	}

	/**
	 * @param string|int           $widget_key  Array key from registry.
	 * @param array<string, mixed> $widget_data Widget config.
	 */
	private static function resolve_widget_id( $widget_key, array $widget_data ): string {
		if ( is_string( $widget_key ) && '' !== $widget_key && ! is_numeric( $widget_key ) ) {
			return $widget_key;
		}

		if ( ! empty( $widget_data['id'] ) && is_string( $widget_data['id'] ) ) {
			return $widget_data['id'];
		}

		if ( ! empty( $widget_data['widget_id'] ) && is_string( $widget_data['widget_id'] ) ) {
			return $widget_data['widget_id'];
		}

		if ( ! empty( $widget_data['class'] ) && is_string( $widget_data['class'] ) ) {
			return self::class_to_widget_id( $widget_data['class'] );
		}

		return '';
	}

	/**
	 * Derives a stable slug from a PHP class name when no explicit widget ID is set.
	 */
	private static function class_to_widget_id( string $class_name ): string {
		$slug = strtolower( preg_replace( '/[^a-zA-Z0-9]+/', '-', $class_name ) );
		$slug = trim( $slug, '-' );

		return $slug;
	}

	/**
	 * @param array<string, array<string, array>> $categorized Categorized widgets.
	 * @param array<int, string>                  $widget_ids  IDs to remove.
	 * @return array<string, array<string, array>>
	 */
	private static function remove_widgets_by_id( array $categorized, array $widget_ids ): array {
		$widget_ids = array_flip( array_map( 'strval', $widget_ids ) );

		foreach ( $categorized as $category => $widgets ) {
			if ( ! is_array( $widgets ) ) {
				unset( $categorized[ $category ] );
				continue;
			}
			foreach ( array_keys( $widgets ) as $widget_id ) {
				if ( isset( $widget_ids[ (string) $widget_id ] ) ) {
					unset( $categorized[ $category ][ $widget_id ] );
				}
			}
		}

		return $categorized;
	}

	/**
	 * @param array<string, array<string, array>> $categorized Categorized widgets.
	 * @return array<string, array<string, array>>
	 */
	private static function drop_empty_categories( array $categorized ): array {
		foreach ( $categorized as $category => $widgets ) {
			if ( ! is_array( $widgets ) || empty( $widgets ) ) {
				unset( $categorized[ $category ] );
			}
		}

		return $categorized;
	}
}
