<?php
/**
 * CPT Builder registrar.
 *
 * Builds register_post_type() arguments from stored definitions and registers
 * the enabled post types on the `init` hook. Exposes developer filters/actions
 * so other code can adjust definitions and arguments.
 *
 * @package CptBuilder
 * @since 1.0.0
 */

namespace CptBuilder;

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

/**
 * Registers stored custom post types with WordPress.
 */
final class Registrar {

	private Storage $storage;

	public function __construct( Storage $storage ) {
		$this->storage = $storage;
	}

	/**
	 * Register all enabled, valid post types.
	 *
	 * Hooked to `init`. Each registration is guarded so one bad definition
	 * cannot fatal the request.
	 */
	public function registerAll(): void {
		/**
		 * Filter the full map of stored CPT definitions before registration.
		 *
		 * @param array<string, array<string, mixed>> $definitions Definitions keyed by post type key.
		 */
		$definitions = apply_filters( 'wps_cpt_builder_post_types', $this->storage->all() );

		if ( ! is_array( $definitions ) ) {
			return;
		}

		foreach ( $definitions as $key => $definition ) {
			if ( ! is_string( $key ) || ! is_array( $definition ) ) {
				continue;
			}

			if ( empty( $definition['enabled'] ) ) {
				continue;
			}

			// Never re-register a key already owned by core/another plugin.
			if ( post_type_exists( $key ) ) {
				continue;
			}

			if ( empty( $definition['labels']['singular_name'] ) || empty( $definition['labels']['name'] ) ) {
				continue;
			}

			$args = $this->buildArgs( $key, $definition );

			/**
			 * Filter the final register_post_type() arguments.
			 *
			 * @param array<string, mixed> $args       Arguments passed to register_post_type().
			 * @param string               $key        Post type key.
			 * @param array<string, mixed> $definition Stored definition.
			 */
			$args = apply_filters( 'wps_cpt_builder_register_args', $args, $key, $definition );

			/**
			 * Fires before a CPT Builder post type is registered.
			 *
			 * @param string               $key  Post type key.
			 * @param array<string, mixed> $args Final arguments.
			 */
			do_action( 'wps_cpt_builder_before_register_post_type', $key, $args );

			$result = register_post_type( $key, $args );

			$this->connectTaxonomies( $key, $definition );

			/**
			 * Fires after a CPT Builder post type is registered.
			 *
			 * @param string                          $key    Post type key.
			 * @param \WP_Post_Type|\WP_Error         $result Registration result.
			 */
			do_action( 'wps_cpt_builder_after_register_post_type', $key, $result );
		}
	}

	/**
	 * Connect existing taxonomies to the post type.
	 *
	 * Taxonomies are also passed in args, but registering the object type here
	 * covers taxonomies registered after this post type.
	 *
	 * @param string               $key        Post type key.
	 * @param array<string, mixed> $definition Stored definition.
	 */
	private function connectTaxonomies( string $key, array $definition ): void {
		$taxonomies = isset( $definition['taxonomies'] ) && is_array( $definition['taxonomies'] )
			? $definition['taxonomies']
			: [];

		foreach ( $taxonomies as $taxonomy ) {
			$taxonomy = (string) $taxonomy;
			if ( $taxonomy !== '' && taxonomy_exists( $taxonomy ) ) {
				register_taxonomy_for_object_type( $taxonomy, $key );
			}
		}
	}

	/**
	 * Build register_post_type() arguments from a stored definition.
	 *
	 * @param string               $key        Post type key.
	 * @param array<string, mixed> $definition Stored definition.
	 * @return array<string, mixed>
	 */
	public function buildArgs( string $key, array $definition ): array {
		$src        = isset( $definition['args'] ) && is_array( $definition['args'] ) ? $definition['args'] : [];
		$args       = wp_parse_args( $src, Config::defaultArgs() );
		$labels     = $this->buildLabels( $definition );
		$supports   = isset( $definition['supports'] ) && is_array( $definition['supports'] ) ? $definition['supports'] : [];
		$taxonomies = isset( $definition['taxonomies'] ) && is_array( $definition['taxonomies'] ) ? array_values( array_filter( array_map( 'strval', $definition['taxonomies'] ) ) ) : [];

		$built = [
			'labels'              => $labels,
			'public'              => (bool) $args['public'],
			'publicly_queryable'  => (bool) $args['publicly_queryable'],
			'show_ui'             => (bool) $args['show_ui'],
			'show_in_menu'        => (bool) $args['show_in_menu'],
			'show_in_nav_menus'   => (bool) $args['show_in_nav_menus'],
			'show_in_admin_bar'   => (bool) $args['show_in_admin_bar'],
			'exclude_from_search' => (bool) $args['exclude_from_search'],
			'hierarchical'        => (bool) $args['hierarchical'],
			'can_export'          => (bool) $args['can_export'],
			'delete_with_user'    => (bool) $args['delete_with_user'],
			'show_in_rest'        => (bool) $args['show_in_rest'],
			'map_meta_cap'        => (bool) $args['map_meta_cap'],
			'menu_icon'           => (string) $args['menu_icon'],
			'supports'            => $supports !== [] ? $supports : false,
			'taxonomies'          => $taxonomies,
		];

		// Description.
		if ( ! empty( $args['description'] ) ) {
			$built['description'] = (string) $args['description'];
		}

		// Menu position.
		if ( $args['menu_position'] !== '' && is_numeric( $args['menu_position'] ) ) {
			$built['menu_position'] = (int) $args['menu_position'];
		}

		// REST options.
		if ( ! empty( $args['rest_base'] ) ) {
			$built['rest_base'] = (string) $args['rest_base'];
		}
		if ( ! empty( $args['rest_namespace'] ) ) {
			$built['rest_namespace'] = (string) $args['rest_namespace'];
		}
		if ( ! empty( $args['rest_controller_class'] ) ) {
			$built['rest_controller_class'] = (string) $args['rest_controller_class'];
		}

		// Capabilities.
		$cap_singular = (string) ( $args['capability_type'] ?? 'post' );
		$cap_plural   = (string) ( $args['capability_type_plural'] ?? '' );
		if ( $cap_singular !== '' ) {
			$built['capability_type'] = $cap_plural !== ''
				? [ $cap_singular, $cap_plural ]
				: $cap_singular;
		}

		// Archives.
		if ( ! empty( $args['has_archive'] ) ) {
			$built['has_archive'] = ! empty( $args['has_archive_slug'] )
				? (string) $args['has_archive_slug']
				: true;
		} else {
			$built['has_archive'] = false;
		}

		// Rewrite rules.
		if ( ! empty( $args['rewrite'] ) ) {
			$rewrite = [
				'slug'       => ! empty( $args['rewrite_slug'] ) ? (string) $args['rewrite_slug'] : $key,
				'with_front' => ! empty( $args['rewrite_with_front'] ),
				'feeds'      => ! empty( $args['rewrite_feeds'] ),
				'pages'      => ! empty( $args['rewrite_pages'] ),
			];
			$built['rewrite'] = $rewrite;
		} else {
			$built['rewrite'] = false;
		}

		// Query var.
		if ( ! empty( $args['query_var'] ) ) {
			$built['query_var'] = ! empty( $args['query_var_slug'] )
				? (string) $args['query_var_slug']
				: true;
		} else {
			$built['query_var'] = false;
		}

		return $built;
	}

	/**
	 * Build the labels array.
	 *
	 * Labels left empty in the UI are auto-generated from the singular and
	 * plural labels (e.g. "Edit Book", "No books found."), matching the
	 * behavior users expect from dedicated CPT plugins. Explicit values
	 * always win.
	 *
	 * @param array<string, mixed> $definition Stored definition.
	 * @return array<string, string>
	 */
	private function buildLabels( array $definition ): array {
		$labels   = isset( $definition['labels'] ) && is_array( $definition['labels'] ) ? $definition['labels'] : [];
		$singular = (string) ( $labels['singular_name'] ?? '' );
		$plural   = (string) ( $labels['name'] ?? '' );
		$clean    = [];

		foreach ( Config::allLabelKeys() as $label_key ) {
			if ( isset( $labels[ $label_key ] ) && (string) $labels[ $label_key ] !== '' ) {
				$clean[ $label_key ] = (string) $labels[ $label_key ];
			}
		}

		// Auto-generate any label the user did not override.
		foreach ( Config::labelTemplates() as $label_key => $meta ) {
			if ( isset( $clean[ $label_key ] ) ) {
				continue;
			}

			$generated = Config::generateLabel( $meta, $singular, $plural );
			if ( $generated !== '' ) {
				$clean[ $label_key ] = $generated;
			}
		}

		return $clean;
	}
}
