<?php
/**
 * JBM Library blocks (direct Gutenberg insert, no My Blocks row).
 *
 * @package JOCBM
 *
 * Copyright (C) 2026 JayOnCode
 * License: GPL-2.0-or-later
 */

namespace JOCBM\Services;

use JOCBM\Database\Schema;

defined( 'ABSPATH' ) || exit;

/**
 * Class LibraryBlocks
 */
final class LibraryBlocks {

	public const BLOCK_NAMESPACE = 'joc-blockora-morph-lib';
	public const CATEGORY_SLUG   = 'joc-blockora-morph-library';

	/**
	 * Gutenberg block name: joc-blockora-morph-lib/{slug}--{variant}.
	 *
	 * @param string $slug    System slug.
	 * @param string $variant Variant key.
	 */
	public static function block_name( string $slug, string $variant ): string {
		$s = sanitize_title( $slug );
		$v = sanitize_title( $variant ) ?: 'default';
		return self::BLOCK_NAMESPACE . '/' . $s . '--' . $v;
	}

	/**
	 * Parse library block name into [ slug, variant ] or null.
	 *
	 * @param string $name Full block name.
	 * @return array{0: string, 1: string}|null
	 */
	public static function parse_block_name( string $name ): ?array {
		$prefix = self::BLOCK_NAMESPACE . '/';
		if ( 0 !== strpos( $name, $prefix ) ) {
			return null;
		}
		$rest = substr( $name, strlen( $prefix ) );
		if ( ! preg_match( '/^([a-z0-9-]+)--([a-z0-9-]+)$/', $rest, $m ) ) {
			return null;
		}
		return array( $m[1], $m[2] );
	}

	/**
	 * Wrapper CSS class for a library block.
	 *
	 * Must match WordPress `wp_get_block_default_classname()` for
	 * `joc-blockora-morph-lib/{slug}--{variant}` so uploads CSS, the editor
	 * preview, and the block wrapper all share one class (WP keeps the `--`
	 * between slug and variant when converting `/` → `-`).
	 *
	 * @param string $slug    System slug.
	 * @param string $variant Variant key.
	 */
	public static function css_class( string $slug, string $variant ): string {
		$name = self::block_name( $slug, $variant );
		if ( function_exists( 'wp_get_block_default_classname' ) ) {
			return wp_get_block_default_classname( $name );
		}
		// Fallback mirrors wp-includes/block-supports/generated-classname.php.
		return 'wp-block-' . str_replace( '/', '-', $name );
	}

	/**
	 * CSS selector (leading dot).
	 *
	 * @param string $slug    System slug.
	 * @param string $variant Variant key.
	 */
	public static function css_selector( string $slug, string $variant ): string {
		return '.' . self::css_class( $slug, $variant );
	}

	/**
	 * Uploads filename for a library block stylesheet.
	 *
	 * @param string $slug    System slug.
	 * @param string $variant Variant key.
	 */
	public static function css_filename( string $slug, string $variant ): string {
		$s = sanitize_file_name( sanitize_title( $slug ) );
		$v = sanitize_file_name( sanitize_title( $variant ) ?: 'default' );
		return 'library-' . $s . '-' . $v . '.css';
	}

	/**
	 * Stable source key slug--variant.
	 *
	 * @param string $slug    System slug.
	 * @param string $variant Variant key.
	 */
	public static function source_key( string $slug, string $variant ): string {
		return sanitize_title( $slug ) . '--' . ( sanitize_title( $variant ) ?: 'default' );
	}

	/**
	 * Style handle for wp_enqueue_style.
	 *
	 * @param string $slug    System slug.
	 * @param string $variant Variant key.
	 */
	public static function style_handle( string $slug, string $variant ): string {
		return 'joc-blockora-morph-lib-' . sanitize_title( $slug ) . '-' . sanitize_title( $variant );
	}

	/**
	 * Whether a catalog/DB row is a Pro-only library block.
	 *
	 * @param array<string, mixed> $row Raw or formatted row.
	 */
	public static function is_pro_row( array $row ): bool {
		if ( ! empty( $row['is_pro'] ) || ! empty( $row['isPro'] ) ) {
			return true;
		}

		$source = sanitize_key( (string) ( $row['source'] ?? '' ) );
		if ( in_array( $source, array( 'pro', 'premium' ), true ) ) {
			return true;
		}

		if ( ! empty( $row['category_is_pro'] ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Whether a catalog row may appear in the Gutenberg inserter.
	 *
	 * Owner variant packs stay registered (for insert-after-picker) but never
	 * appear as their own inserter entries.
	 *
	 * @param array<string, mixed> $block Formatted catalog row.
	 */
	public static function is_inserter_visible( array $block ): bool {
		$pack_type = sanitize_key(
			(string) ( $block['packType'] ?? $block['pack_type'] ?? '' )
		);
		if ( 'variant' === $pack_type ) {
			return false;
		}
		if ( self::is_pro_row( $block ) && ! jocbm_is_pro() ) {
			return false;
		}
		return true;
	}

	/**
	 * Attach owner variant packs onto their parent catalog rows.
	 *
	 * @param array<int, array<string, mixed>> $items Catalog / library rows.
	 * @return array<int, array<string, mixed>>
	 */
	public static function attach_variants_to_parents( array $items ): array {
		$buckets = array();
		foreach ( $items as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			$pack_type = sanitize_key(
				(string) ( $item['packType'] ?? $item['pack_type'] ?? '' )
			);
			if ( 'variant' !== $pack_type ) {
				continue;
			}
			$extends = $item['extends'] ?? null;
			if ( ! is_array( $extends ) ) {
				continue;
			}
			$parent_slug = sanitize_key( (string) ( $extends['slug'] ?? '' ) );
			$parent_var  = sanitize_key( (string) ( $extends['variant'] ?? 'default' ) ) ?: 'default';
			if ( '' === $parent_slug ) {
				continue;
			}
			$key               = $parent_slug . '::' . $parent_var;
			$buckets[ $key ][] = $item;
		}

		foreach ( $items as $i => $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			$slug = sanitize_key(
				(string) ( $item['systemSlug'] ?? $item['slug'] ?? '' )
			);
			$var  = sanitize_key( (string) ( $item['variant'] ?? 'default' ) ) ?: 'default';
			$key  = $slug . '::' . $var;
			$items[ $i ]['variants'] = isset( $buckets[ $key ] )
				? array_values( $buckets[ $key ] )
				: array();
			if ( empty( $items[ $i ]['packType'] ) && empty( $items[ $i ]['pack_type'] ) ) {
				$items[ $i ]['packType'] = 'standalone';
			}
		}

		return $items;
	}

	/**
	 * Active JBM Library catalog for Gutenberg (DB + published Owner packs).
	 *
	 * @return array<int, array<string, mixed>>
	 */
	public static function catalog(): array {
		return JsonHelper::guard(
			static function () {
				return self::build_catalog();
			},
			array(),
			'library_catalog'
		);
	}

	/**
	 * @return array<int, array<string, mixed>>
	 */
	private static function build_catalog(): array {
		global $wpdb;

		$table      = Schema::table( 'system_blocks' );
		$cats       = Schema::table( 'categories' );
		$owner_keys = TemplateLibrary::owner_pack_keys();
		$items      = array();
		$seen       = array();

		if ( $table && $cats && self::tables_exist( $table, $cats ) ) {
			$rows = $wpdb->get_results(
				$wpdb->prepare(
					'SELECT b.*, c.slug AS category_slug, c.name AS category_name, c.parent_slug AS category_parent_slug,
				c.is_pro AS category_is_pro
				FROM %i b
				INNER JOIN %i c ON c.id = b.category_id
				WHERE b.is_active = 1
				AND COALESCE(b.source, \'core\') <> \'creator\'
				ORDER BY b.name ASC, b.variant ASC',
					$table,
					$cats
				),
				ARRAY_A
			);

			foreach ( $rows ?: array() as $row ) {
				$slug    = sanitize_key( (string) ( $row['slug'] ?? '' ) );
				$variant = sanitize_key( (string) ( $row['variant'] ?? 'default' ) ) ?: 'default';
				$key     = $slug . '::' . $variant;
				if ( '' === $slug || isset( $owner_keys[ $key ] ) ) {
					continue;
				}
				$formatted = self::try_format_catalog_row( $row, false );
				if ( null === $formatted ) {
					continue;
				}
				$items[]      = $formatted;
				$seen[ $key ] = true;
			}
		}

		foreach ( TemplateLibrary::ready_owner_blocks() as $block ) {
			$slug    = sanitize_key( (string) ( $block['slug'] ?? '' ) );
			$variant = sanitize_key( (string) ( $block['variant'] ?? 'default' ) ) ?: 'default';
			$key     = $slug . '::' . $variant;
			if ( '' === $slug || isset( $seen[ $key ] ) ) {
				continue;
			}
			$formatted = self::try_format_catalog_row( $block, true );
			if ( null === $formatted ) {
				continue;
			}
			$items[]      = $formatted;
			$seen[ $key ] = true;
		}

		$items = self::attach_variants_to_parents( $items );

		usort(
			$items,
			static function ( $a, $b ) {
				$by_name = strcmp( (string) ( $a['title'] ?? '' ), (string) ( $b['title'] ?? '' ) );
				if ( 0 !== $by_name ) {
					return $by_name;
				}
				return strcmp( (string) ( $a['variant'] ?? '' ), (string) ( $b['variant'] ?? '' ) );
			}
		);

		/**
		 * Filter the JBM Library catalog exposed to Gutenberg.
		 *
		 * @param array<int, array<string, mixed>> $items Catalog rows.
		 */
		return apply_filters( 'jocbm_library_catalog', $items );
	}

	/**
	 * Resolve a catalog/runtime block row by slug + variant.
	 *
	 * @param string $slug    System slug.
	 * @param string $variant Variant key.
	 * @return array<string, mixed>|null
	 */
	public static function find( string $slug, string $variant ): ?array {
		$slug    = sanitize_title( $slug );
		$variant = sanitize_title( $variant ) ?: 'default';
		if ( '' === $slug ) {
			return null;
		}
		foreach ( self::catalog() as $block ) {
			if (
				sanitize_title( (string) ( $block['systemSlug'] ?? '' ) ) === $slug &&
				sanitize_title( (string) ( $block['variant'] ?? 'default' ) ) === $variant
			) {
				return $block;
			}
		}
		return null;
	}

	/**
	 * @param array<string, mixed> $row      DB or disk row.
	 * @param bool                 $from_disk Owner pack from disk.
	 * @return array<string, mixed>|null
	 */
	private static function try_format_catalog_row( array $row, bool $from_disk ): ?array {
		$slug = sanitize_key( (string) ( $row['slug'] ?? 'unknown' ) );
		return JsonHelper::guard(
			static function () use ( $row, $from_disk ) {
				return self::format_catalog_row( $row, $from_disk );
			},
			null,
			'format_catalog_row:' . $slug
		);
	}

	/**
	 * @param array<string, mixed> $row      DB or disk row.
	 * @param bool                 $from_disk Owner pack from disk.
	 * @return array<string, mixed>
	 */
	private static function format_catalog_row( array $row, bool $from_disk ): array {
		$slug    = sanitize_key( (string) ( $row['slug'] ?? '' ) );
		$variant = sanitize_key( (string) ( $row['variant'] ?? 'default' ) ) ?: 'default';
		$name    = (string) ( $row['name'] ?? $slug );
		$fields  = TemplateLibrary::decode_field_schema( $row['field_schema'] ?? '[]' );
		$library = TemplateLibrary::decode_field_schema( $row['library_field_schema'] ?? '[]' );
		if ( $library ) {
			$fields = TemplateLibrary::apply_library_field_types( $fields, $library );
		}

		$css_vars      = CssVars::normalize( JsonHelper::decode_array( $row['css_vars'] ?? null, array() ) );
		$default_attrs = JsonHelper::decode_array( $row['default_attrs'] ?? null, array() );

		$title = self::catalog_display_title(
			(string) ( $row['title'] ?? '' ),
			$name,
			$variant
		);

		$cat_slug = (string) ( $row['category_slug'] ?? $row['category'] ?? '' );
		$cat_slugs = array();
		if ( ! $from_disk && ! empty( $row['id'] ) ) {
			$cat_slugs = BlockCategories::slugs_for_block( (int) $row['id'] );
		} elseif ( is_array( $row['categories'] ?? null ) ) {
			$cat_slugs = array_values( array_filter( array_map( 'sanitize_key', $row['categories'] ) ) );
		}
		if ( ! $cat_slugs && $cat_slug ) {
			$cat_slugs = array( sanitize_key( $cat_slug ) );
		}

		return array(
			'name'           => self::block_name( $slug, $variant ),
			'slug'           => self::source_key( $slug, $variant ),
			'systemSlug'     => $slug,
			'variant'        => $variant,
			'id'             => $from_disk ? 0 : (int) ( $row['id'] ?? 0 ),
			'systemBlockId'  => $from_disk ? 0 : (int) ( $row['id'] ?? 0 ),
			'title'          => $title,
			'icon'           => BlockIcons::for_library_row( $row ),
			'markup'         => (string) ( $row['markup'] ?? '' ),
			'fieldSchema'    => $fields,
			'styleSchema'    => TemplateLibrary::decode_field_schema( $row['style_schema'] ?? '[]' ),
			'cssVars'        => $css_vars,
			'defaultAttrs'   => $default_attrs,
			'variantLabel'   => $variant,
			'systemName'     => $name,
			'categorySlug'   => $cat_slug,
			'categoryName'   => (string) ( $row['category_name'] ?? '' ),
			'categorySlugs'  => $cat_slugs,
			'isPro'          => self::is_pro_row( $row ),
			'isLibraryBlock' => true,
			'ownerPack'      => $from_disk,
			'packType'       => $from_disk
				? TemplateLibrary::resolve_pack_type( $row )
				: 'standalone',
			'extends'        => $from_disk && ! empty( $row['extends'] ) && is_array( $row['extends'] )
				? $row['extends']
				: null,
			'variants'       => array(),
		);
	}

	/**
	 * Gutenberg / catalog label. Prefer block.json title, then name.
	 * Do not append a humanized variant slug when the name is already qualified
	 * (e.g. "Hero — Media Right" + variant image-right must not become
	 * "Hero — Media Right — Image Right").
	 *
	 * @param string $explicit_title Optional title from block.json / row.
	 * @param string $name           Template / system display name.
	 * @param string $variant        Variant slug (e.g. image-right).
	 */
	private static function catalog_display_title( string $explicit_title, string $name, string $variant ): string {
		$title = trim( $explicit_title );
		if ( '' === $title ) {
			$title = trim( $name );
		}
		if ( '' === $title ) {
			$title = $variant ?: 'Block';
		}

		$variant = sanitize_key( $variant );
		if ( '' === $variant || 'default' === $variant ) {
			return $title;
		}

		$variant_label = ucwords( str_replace( '-', ' ', $variant ) );
		$normalize     = static function ( string $value ): string {
			$value = strtolower( $value );
			$value = preg_replace( '/[\s—–\-]+/u', ' ', $value ) ?? $value;
			return trim( $value );
		};

		$norm_title   = $normalize( $title );
		$norm_variant = $normalize( $variant_label );
		if ( '' !== $norm_variant && false !== strpos( $norm_title, $norm_variant ) ) {
			return $title;
		}

		// Friendly names already include a qualifier (Media Right, Simple, …).
		if ( preg_match( '/(?:—|–|\s-\s).+/u', $title ) ) {
			return $title;
		}

		return $title . ' — ' . $variant_label;
	}

	/**
	 * Write / refresh CSS files for every catalog block.
	 */
	public static function sync_stylesheets(): void {
		foreach ( self::catalog() as $block ) {
			$slug    = (string) ( $block['systemSlug'] ?? '' );
			$variant = (string) ( $block['variant'] ?? 'default' );
			if ( '' === $slug ) {
				continue;
			}
			JsonHelper::guard(
				static function () use ( $slug, $variant, $block ) {
					CssWriter::write_library_block(
						$slug,
						$variant,
						$block['cssVars'] ?? array(),
						is_array( $block['defaultAttrs'] ?? null ) ? $block['defaultAttrs'] : array(),
						is_array( $block['fieldSchema'] ?? null ) ? $block['fieldSchema'] : array()
					);
				},
				null,
				'sync_library_stylesheet:' . $slug . '--' . $variant
			);
		}
	}

	/**
	 * Whether plugin-owned tables are still present (e.g. skip reads during uninstall).
	 *
	 * @param string ...$tables Table names from Schema::table().
	 */
	private static function tables_exist( string ...$tables ): bool {
		global $wpdb;

		foreach ( $tables as $table ) {
			if ( '' === $table ) {
				return false;
			}
			$found = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
			if ( $found !== $table ) {
				return false;
			}
		}

		return true;
	}
}
