<?php

namespace Kernowdev\Clanbite\Extensions;
defined( 'ABSPATH' ) || exit;

// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Dynamic tags are `{option_key}_{suffix}`; suffix constants are clanbite-prefixed; option keys are `clanbite_*`.

/**
 * WordPress Settings API wrapper for Clanbite option buckets.
 *
 * Dynamic filter/action names are built with {@see self::clanbite_extension_settings_hook_tag()}.
 * Each full tag is `{option_key}_{suffix}` where `option_key` is a registered `clanbite_*` option
 * name and `suffix` is one of the `HOOK_TAG_*` constants (always starting with `clanbite_`).
 *
 * @package clanbite
 */
abstract class Abstract_Settings {

	private const HOOK_TAG_STANDALONE_PARENT_SLUG     = 'clanbite_standalone_submenu_parent_slug';
	private const HOOK_TAG_PERSISTED_DEFAULT_MAP      = 'clanbite_persisted_option_default_map';
	private const HOOK_TAG_UI_SECTION_REGISTRY        = 'clanbite_settings_ui_section_registry';
	private const HOOK_TAG_UI_SECTION_FIELD_REGISTRY  = 'clanbite_settings_ui_section_field_registry';
	private const HOOK_TAG_SINGLE_FIELD_DEFINITION  = 'clanbite_settings_field_definition_single';
	private const HOOK_TAG_FIELD_MARKUP_DELEGATE    = 'clanbite_settings_field_markup_delegate';
	private const HOOK_TAG_FORM_PAYLOAD_PREFILTER   = 'clanbite_settings_form_payload_prefilter';
	private const HOOK_TAG_FINAL_OPTION_VALUE_BUNDLE = 'clanbite_final_merged_option_values';
	private const HOOK_TAG_SETTINGS_PAGE_SHELL_BEFORE = 'clanbite_settings_page_shell_before';
	private const HOOK_TAG_SETTINGS_PAGE_SHELL_AFTER  = 'clanbite_settings_page_shell_after';

	protected string $option_key;
	protected string $page_slug;
	protected string $settings_group;
	protected string $capability = 'manage_options';

	/**
	 * Builds `{option_key}_{$suffix}` for Clanbite settings filters/actions.
	 *
	 * @param string $suffix Clanbite-prefixed fragment; pass a `HOOK_TAG_*` constant from this class.
	 * @return string Full hook tag.
	 */
	final protected function clanbite_extension_settings_hook_tag( string $suffix ): string {
		return $this->option_key . '_' . $suffix;
	}

	public function __construct() {
		$this->hooks();
	}

	public function hooks(): void {
		add_action( 'admin_menu', array( $this, 'register_page' ) );
		add_action( 'admin_init', array( $this, 'register_settings' ) );
	}

	/**
	 * When true, registers a classic submenu under Clanbite. The unified React
	 * admin keeps this false; use filter `clanbite_extension_settings_register_submenu`.
	 *
	 * @var bool
	 */
	protected bool $register_standalone_submenu = false;

	public function register_page(): void {
		$register = (bool) apply_filters( 'clanbite_extension_settings_register_submenu', $this->register_standalone_submenu, $this );

		if ( ! $register ) {
			return;
		}

		$parent_slug = (string) apply_filters( $this->clanbite_extension_settings_hook_tag( self::HOOK_TAG_STANDALONE_PARENT_SLUG ), 'clanbite', $this );

		add_submenu_page(
			$parent_slug,
			$this->get_page_title(),
			$this->get_menu_title(),
			$this->capability,
			$this->page_slug,
			array( $this, 'render_page' )
		);
	}

	public function register_settings(): void {
		$default_settings = $this->get_defaults();

		/**
		 * Filter extension settings defaults before registration.
		 *
		 * Dynamic hook: `{option_key}_clanbite_persisted_option_default_map`.
		 *
		 * @param array             $default_settings Default settings map.
		 * @param Abstract_Settings $settings         Settings class instance.
		 */
		$default_settings = (array) apply_filters( $this->clanbite_extension_settings_hook_tag( self::HOOK_TAG_PERSISTED_DEFAULT_MAP ), $default_settings, $this );

		register_setting(
			$this->settings_group,
			$this->option_key,
			array(
				'type'              => 'array',
				'sanitize_callback' => array( $this, 'sanitize' ),
				'default'           => $default_settings,
			)
		);

		foreach ( $this->get_resolved_sections_with_fields() as $section_id => $section ) {
			add_settings_section(
				$section_id,
				$section['title'] ?? '',
				$section['callback'] ?? null,
				$this->page_slug
			);

			$fields = isset( $section['fields'] ) && is_array( $section['fields'] ) ? $section['fields'] : array();

			foreach ( $fields as $field_id => $field ) {
				if ( ! is_array( $field ) ) {
					continue;
				}

				add_settings_field(
					$field_id,
					$field['label'] ?? '',
					array( $this, 'render_field' ),
					$this->page_slug,
					$section_id,
					array(
						'id'    => $field_id,
						'field' => $field,
					)
				);
			}
		}
	}

	/**
	 * Sections and fields after the same filters used for Settings API registration.
	 *
	 * Used by {@see self::export_rest_schema()} and {@see self::get_flat_fields()} so the React
	 * admin matches classic registration and sanitization.
	 *
	 * @return array<string, array<string, mixed>>
	 */
	protected function get_resolved_sections_with_fields(): array {
		$sections = $this->get_sections();

		/**
		 * Filter all sections before they are registered.
		 *
		 * Dynamic hook: `{option_key}_clanbite_settings_ui_section_registry`.
		 *
		 * @param array             $sections Sections array.
		 * @param Abstract_Settings $settings Settings class instance.
		 */
		$sections = (array) apply_filters( $this->clanbite_extension_settings_hook_tag( self::HOOK_TAG_UI_SECTION_REGISTRY ), $sections, $this );

		$resolved = array();

		foreach ( $sections as $section_id => $section ) {
			if ( ! is_array( $section ) ) {
				continue;
			}

			$fields = isset( $section['fields'] ) && is_array( $section['fields'] ) ? $section['fields'] : array();

			/**
			 * Filter section fields before they are registered.
			 *
			 * Dynamic hook: `{option_key}_clanbite_settings_ui_section_field_registry`.
			 *
			 * @param array             $fields     Field config map.
			 * @param string            $section_id Section id.
			 * @param array             $section    Section config.
			 * @param Abstract_Settings $settings   Settings class instance.
			 */
			$fields = (array) apply_filters( $this->clanbite_extension_settings_hook_tag( self::HOOK_TAG_UI_SECTION_FIELD_REGISTRY ), $fields, $section_id, $section, $this );

			$resolved_fields = array();

			foreach ( $fields as $field_id => $field ) {
				if ( ! is_array( $field ) ) {
					continue;
				}

				/**
				 * Filter a single field config before registration.
				 *
				 * Dynamic hook: `{option_key}_clanbite_settings_field_definition_single`.
				 *
				 * @param array             $field      Field config.
				 * @param string            $field_id   Field id.
				 * @param string            $section_id Section id.
				 * @param Abstract_Settings $settings   Settings class instance.
				 */
				$resolved_fields[ $field_id ] = (array) apply_filters( $this->clanbite_extension_settings_hook_tag( self::HOOK_TAG_SINGLE_FIELD_DEFINITION ), $field, $field_id, $section_id, $this );
			}

			$section['fields']      = $resolved_fields;
			$resolved[ $section_id ] = $section;
		}

		return $resolved;
	}

	public function render_field( array $args ): void {
		$field   = $args['field'];
		$id      = $args['id'];
		$options = $this->get_all();
		$value   = $options[ $id ] ?? $field['default'] ?? '';

		/**
		 * Allow full field rendering override.
		 *
		 * Return true to signal rendering handled by a custom callback.
		 *
		 * Dynamic hook: `{option_key}_clanbite_settings_field_markup_delegate`.
		 *
		 * @param bool              $handled  Whether rendering has already been handled.
		 * @param string            $id       Field id.
		 * @param array             $field    Field config.
		 * @param mixed             $value    Current field value.
		 * @param Abstract_Settings $settings Settings class instance.
		 */
		$handled = (bool) apply_filters( $this->clanbite_extension_settings_hook_tag( self::HOOK_TAG_FIELD_MARKUP_DELEGATE ), false, $id, $field, $value, $this );
		if ( $handled ) {
			return;
		}

		$type = $field['type'] ?? 'text';

		switch ( $type ) {
			case 'checkbox':
				printf(
					'<input type="checkbox" name="%1$s[%2$s]" value="1" %3$s />',
					esc_attr( $this->option_key ),
					esc_attr( $id ),
					checked( $value, true, false )
				);
				break;
			case 'textarea':
				printf(
					'<textarea class="large-text" rows="4" name="%1$s[%2$s]">%3$s</textarea>',
					esc_attr( $this->option_key ),
					esc_attr( $id ),
					esc_textarea( (string) $value )
				);
				break;
			case 'select':
				printf(
					'<select name="%1$s[%2$s]">',
					esc_attr( $this->option_key ),
					esc_attr( $id )
				);

				$options = isset( $field['options'] ) && is_array( $field['options'] )
					? $field['options']
					: array();

				foreach ( $options as $option_value => $option_label ) {
					printf(
						'<option value="%1$s" %2$s>%3$s</option>',
						esc_attr( (string) $option_value ),
						selected( (string) $value, (string) $option_value, false ),
						esc_html( (string) $option_label )
					);
				}

				echo '</select>';
				break;

			default:
				printf(
					'<input type="%1$s" name="%2$s[%3$s]" value="%4$s" class="regular-text" />',
					esc_attr( $type ),
					esc_attr( $this->option_key ),
					esc_attr( $id ),
					esc_attr( $value )
				);
		}

		if ( ! empty( $field['description'] ) ) {
			printf( '<p class="description">%s</p>', esc_html( $field['description'] ) );
		}
	}

	public function sanitize( array $input ): array {
		$output   = array();
		$defaults = $this->get_defaults();
		$fields   = $this->get_flat_fields();

		/**
		 * Filter raw settings payload before field-level sanitization.
		 *
		 * Dynamic hook: `{option_key}_clanbite_settings_form_payload_prefilter`.
		 *
		 * @param array             $input    Raw input.
		 * @param array             $fields   Flat field config map.
		 * @param Abstract_Settings $settings Settings class instance.
		 */
		$input = (array) apply_filters( $this->clanbite_extension_settings_hook_tag( self::HOOK_TAG_FORM_PAYLOAD_PREFILTER ), $input, $fields, $this );

		foreach ( $fields as $key => $field ) {
			$value = $input[ $key ] ?? null;

			if ( isset( $field['sanitize'] ) && is_callable( $field['sanitize'] ) ) {
				$output[ $key ] = call_user_func( $field['sanitize'], $value );
			} else {
				// Default sanitization
				if ( ( $field['type'] ?? '' ) === 'checkbox' ) {
					$output[ $key ] = ! empty( $value );
				} elseif ( ( $field['type'] ?? '' ) === 'textarea' ) {
					$output[ $key ] = sanitize_textarea_field( (string) ( $value ?? '' ) );
				} else {
					$output[ $key ] = sanitize_text_field( $value ?? '' );
				}
			}
		}

		// Merge defaults to ensure no missing keys
		$output = wp_parse_args( $output, $defaults );

		/**
		 * Filter the merged, field-sanitized option array before persistence.
		 *
		 * Dynamic hook: `{option_key}_clanbite_final_merged_option_values`.
		 *
		 * @param array<string, mixed> $output Merged settings.
		 * @param array<string, mixed> $input  Raw input (same shape as passed to sanitize).
		 */
		return (array) apply_filters( $this->clanbite_extension_settings_hook_tag( self::HOOK_TAG_FINAL_OPTION_VALUE_BUNDLE ), $output, $input );
	}

	public function get_all(): array {
		return wp_parse_args(
			get_option( $this->option_key, array() ),
			$this->get_defaults()
		);
	}

	public function get( string $key, $default = null ) {
		$settings = $this->get_all();
		return $settings[ $key ] ?? $default;
	}

	/**
	 * Render a standard settings page shell.
	 *
	 * @param string $title Settings page heading.
	 * @return void
	 */
	protected function render_settings_page( string $title ): void {
		/**
		 * Fires before the classic settings page shell markup.
		 *
		 * Dynamic hook: `{option_key}_clanbite_settings_page_shell_before`.
		 *
		 * @param Abstract_Settings $settings Settings class instance.
		 */
		do_action( $this->clanbite_extension_settings_hook_tag( self::HOOK_TAG_SETTINGS_PAGE_SHELL_BEFORE ), $this );
		?>
		<div class="wrap">
			<h1><?php echo esc_html( $title ); ?></h1>
			<form method="post" action="options.php">
				<?php
				settings_fields( $this->settings_group );
				do_settings_sections( $this->page_slug );
				submit_button();
				?>
			</form>
		</div>
		<?php
		/**
		 * Fires after the classic settings page shell markup.
		 *
		 * Dynamic hook: `{option_key}_clanbite_settings_page_shell_after`.
		 *
		 * @param Abstract_Settings $settings Settings class instance.
		 */
		do_action( $this->clanbite_extension_settings_hook_tag( self::HOOK_TAG_SETTINGS_PAGE_SHELL_AFTER ), $this );
	}

	protected function get_flat_fields(): array {
		$flat = array();
		foreach ( $this->get_resolved_sections_with_fields() as $section ) {
			if ( empty( $section['fields'] ) || ! is_array( $section['fields'] ) ) {
				continue;
			}
			foreach ( $section['fields'] as $id => $field ) {
				if ( is_array( $field ) ) {
					$flat[ $id ] = $field;
				}
			}
		}
		return $flat;
	}

	/**
	 * Option name used in the database.
	 */
	public function get_option_key(): string {
		return $this->option_key;
	}

	/**
	 * Export one field definition for the React admin (supports nested `repeater` fields).
	 *
	 * @param string               $field_id Stable field key (top-level map key or nested `id`).
	 * @param array<string, mixed> $field    Field config.
	 * @return array<string, mixed>
	 */
	protected function export_field_for_rest_schema( string $field_id, array $field ): array {
		$row = array(
			'id'          => (string) $field_id,
			'label'       => (string) ( $field['label'] ?? '' ),
			'type'        => (string) ( $field['type'] ?? 'text' ),
			'description' => (string) ( $field['description'] ?? '' ),
			'default'     => $field['default'] ?? null,
		);

		if ( isset( $field['fallback_url'] ) ) {
			$row['fallback_url'] = (string) $field['fallback_url'];
		}

		if ( isset( $field['depends_on'] ) && is_array( $field['depends_on'] ) ) {
			$row['depends_on'] = $field['depends_on'];
		}

		if ( ! empty( $field['icon_picker'] ) ) {
			$row['iconPicker'] = true;
		}

		if ( isset( $field['icon_pack_scope'] ) && is_string( $field['icon_pack_scope'] ) ) {
			$scope = sanitize_key( $field['icon_pack_scope'] );
			if ( '' !== $scope ) {
				$row['iconPackScope'] = $scope;
			}
		}

		if ( isset( $field['media_title'] ) ) {
			$row['mediaTitle'] = (string) $field['media_title'];
		}

		if ( isset( $field['media_button_text'] ) ) {
			$row['mediaButtonText'] = (string) $field['media_button_text'];
		}

		if ( 'select' === $row['type'] && ! empty( $field['options'] ) && is_array( $field['options'] ) ) {
			$opts = array();
			foreach ( $field['options'] as $opt_val => $opt_label ) {
				$opts[] = array(
					'value' => (string) $opt_val,
					'label' => (string) $opt_label,
				);
			}
			$row['options'] = $opts;
		}

		if ( 'user_id_list' === $row['type'] ) {
			$row['user_search_path'] = isset( $field['user_search_path'] ) && is_string( $field['user_search_path'] )
				? $field['user_search_path']
				: 'wp/v2/users';
		}

		if ( 'post_id_list' === $row['type'] ) {
			$row['post_search_path'] = isset( $field['post_search_path'] ) && is_string( $field['post_search_path'] )
				? $field['post_search_path']
				: 'wp/v2/posts';
		}

		if ( 'repeater' === $row['type'] && ! empty( $field['fields'] ) && is_array( $field['fields'] ) ) {
			$nested = array();
			foreach ( $field['fields'] as $sub_field ) {
				if ( ! is_array( $sub_field ) ) {
					continue;
				}
				$sub_id = isset( $sub_field['id'] ) ? (string) $sub_field['id'] : '';
				if ( '' === $sub_id ) {
					continue;
				}
				$nested[] = $this->export_field_for_rest_schema( $sub_id, $sub_field );
			}
			$row['fields'] = $nested;

			if ( isset( $field['add_label'] ) ) {
				$row['add_label'] = (string) $field['add_label'];
			}

			if ( isset( $field['default_row'] ) && is_array( $field['default_row'] ) ) {
				$row['default_row'] = $field['default_row'];
			}
		}

		return $row;
	}

	/**
	 * Schema for the React settings UI (sections + fields).
	 *
	 * @return array<int, array<string, mixed>>
	 */
	public function export_rest_schema(): array {
		$out = array();

		foreach ( $this->get_resolved_sections_with_fields() as $section_id => $section ) {
			$fields_out = array();
			$fields     = isset( $section['fields'] ) && is_array( $section['fields'] ) ? $section['fields'] : array();

			foreach ( $fields as $field_id => $field ) {
				if ( ! is_array( $field ) ) {
					continue;
				}

				$fields_out[] = $this->export_field_for_rest_schema( (string) $field_id, $field );
			}

			$out[] = array(
				'id'     => (string) $section_id,
				'title'  => (string) ( $section['title'] ?? '' ),
				'fields' => $fields_out,
			);
		}

		return $out;
	}

	/**
	 * Validate and persist settings from a REST request body.
	 *
	 * @param array<string, mixed> $input Raw keyed field values.
	 * @return array<string, mixed>
	 */
	public function save_from_input( array $input ): array {
		$sanitized = $this->sanitize( $input );

		if ( is_multisite() && is_network_admin() ) {
			update_site_option( $this->option_key, $sanitized );
		} else {
			update_option( $this->option_key, $sanitized );
		}

		return $sanitized;
	}

	abstract protected function get_page_title(): string;
	abstract protected function get_menu_title(): string;
	abstract protected function get_defaults(): array;
	abstract protected function get_sections(): array;
	abstract public function render_page(): void;
}

// phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
