<?php

/**
 * Custom post type for built Gutenberg blocks (Block Builder library).
 *
 * @package LolaCore\BlockBuilder\PostType
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\BlockBuilder\PostType;

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

/**
 * Registers lolacore_block CPT, post meta, and revision tracking.
 */
class BlockPostType {

	public const POST_TYPE = 'lolacore_block';

	/** Legacy disk prefix from FS-S2.0 — install slug is now the bare block slug. */
	public const PLUGIN_SLUG_PREFIX = 'lolacore-block-';

	/** Runtime directory under wp-content (FS-D5 revised). */
	public const RUNTIME_BLOCKS_DIR = 'lolacore-blocks';

	/** Block namespace for block.json name (lolacore/{slug}). */
	public const BLOCK_NAMESPACE = 'lolacore';

	public const META_BLOCK_SLUG     = '_lc_bb_block_slug';
	public const META_NAME           = '_lc_bb_name';
	public const META_DESCRIPTION    = '_lc_bb_description';
	public const META_FILES          = '_lc_bb_files';
	public const META_PLUGIN_SLUG    = '_lc_bb_plugin_slug';
	public const META_VERSION        = '_lc_bb_version';
	public const META_LOAD_SAFE_STATUS = '_lc_bb_load_safe_status';
	public const META_SAFETY_FINDINGS  = '_lc_bb_safety_findings';
	public const META_CONFIDENCE       = '_lc_bb_confidence';
	public const META_CONFIDENCE_NOTE  = '_lc_bb_confidence_note';

	/** Playground gate — see BlockBuilderGate. */
	public const META_PG_GATE_STATUS      = '_lc_bb_pg_gate_status';
	public const META_PG_ATTESTED_VERSION = '_lc_bb_pg_attested_version';
	public const META_PG_ATTESTED_AT        = '_lc_bb_pg_attested_at';
	public const META_PG_OPENED_AT          = '_lc_bb_pg_opened_at';

	/**
	 * @return array<string>
	 */
	public static function metaKeys(): array {
		return array(
			self::META_BLOCK_SLUG,
			self::META_NAME,
			self::META_DESCRIPTION,
			self::META_FILES,
			self::META_PLUGIN_SLUG,
			self::META_VERSION,
			self::META_LOAD_SAFE_STATUS,
			self::META_SAFETY_FINDINGS,
			self::META_CONFIDENCE,
			self::META_CONFIDENCE_NOTE,
			self::META_PG_GATE_STATUS,
			self::META_PG_ATTESTED_VERSION,
			self::META_PG_ATTESTED_AT,
			self::META_PG_OPENED_AT,
		);
	}

	/**
	 * @return array<string>
	 */
	public static function revisionMetaKeys(): array {
		return array(
			self::META_FILES,
			self::META_DESCRIPTION,
			self::META_NAME,
			self::META_BLOCK_SLUG,
		);
	}

	/**
	 * On-disk folder slug under wp-content/lolacore-blocks/{slug}/.
	 */
	public static function installSlugFromBlockSlug( string $block_slug ): string {
		return sanitize_title($block_slug);
	}

	/**
	 * @deprecated Use installSlugFromBlockSlug — meta key _lc_bb_plugin_slug stores the install slug.
	 */
	public static function pluginSlugFromBlockSlug( string $block_slug ): string {
		return self::installSlugFromBlockSlug($block_slug);
	}

	/**
	 * Absolute path to wp-content/lolacore-blocks.
	 */
	public static function runtimeBlocksRoot(): string {
		if (! defined('WP_CONTENT_DIR')) {
			return '';
		}

		return rtrim(WP_CONTENT_DIR, '/\\') . DIRECTORY_SEPARATOR . self::RUNTIME_BLOCKS_DIR;
	}

	/**
	 * Absolute path to a single installed block folder.
	 */
	public static function runtimePathForSlug( string $install_slug ): string {
		$install_slug = sanitize_title($install_slug);
		if ($install_slug === '') {
			return '';
		}

		return self::runtimeBlocksRoot() . DIRECTORY_SEPARATOR . $install_slug;
	}

	/**
	 * Resolve install slug from library meta (handles legacy lolacore-block-* values).
	 */
	public static function resolveInstallSlug( string $block_slug, string $stored_install_slug = '' ): string {
		if ($block_slug !== '') {
			return self::installSlugFromBlockSlug($block_slug);
		}

		if ($stored_install_slug !== '' && str_starts_with($stored_install_slug, self::PLUGIN_SLUG_PREFIX)) {
			return sanitize_title(substr($stored_install_slug, strlen(self::PLUGIN_SLUG_PREFIX)));
		}

		return sanitize_title($stored_install_slug);
	}

	/**
	 * Build the reserved block name from a sanitized block slug.
	 */
	public static function blockNameFromSlug( string $block_slug ): string {
		return self::BLOCK_NAMESPACE . '/' . $block_slug;
	}

	/**
	 * Hook CPT registration, meta registration, and revision filters.
	 */
	public function register(): void {
		add_action('init', array($this, 'registerPostType'));
		add_action('init', array($this, 'registerMeta'), 11);
		add_action('admin_menu', array($this, 'addAdminSubmenu'), 21);
		add_filter('parent_file', array($this, 'highlightParentMenu'));
		add_filter('submenu_file', array($this, 'highlightSubmenu'));
		add_filter('wp_post_revision_meta_keys', array($this, 'addRevisionMetaKeys'), 10, 2);
	}

	/**
	 * Add "Block Builder" under Lola (after My Built Plugins).
	 */
	public function addAdminSubmenu(): void {
		add_submenu_page(
			'lolacore',
			__('Block Builder', 'lolacore'),
			__('Block Builder', 'lolacore'),
			'manage_options',
			'edit.php?post_type=' . self::POST_TYPE
		);
	}

	/**
	 * @param string $parent_file Parent menu file slug.
	 */
	public function highlightParentMenu( string $parent_file ): string {
		if ($this->isBlockBuilderAdminScreen()) {
			return 'lolacore';
		}

		return $parent_file;
	}

	/**
	 * @param string|null $submenu_file Submenu file slug.
	 * @return string|null
	 */
	public function highlightSubmenu( $submenu_file ) {
		if ($this->isBlockBuilderAdminScreen()) {
			return 'edit.php?post_type=' . self::POST_TYPE;
		}

		return $submenu_file;
	}

	private function isBlockBuilderAdminScreen(): bool {
		if (! function_exists('get_current_screen')) {
			return false;
		}

		$screen = get_current_screen();
		if (! $screen instanceof \WP_Screen) {
			return false;
		}

		return $screen->post_type === self::POST_TYPE;
	}

	public function registerPostType(): void {
		register_post_type(
			self::POST_TYPE,
			array(
				'labels'              => array(
					'name'               => __('Block Builder', 'lolacore'),
					'singular_name'      => __('Built Block', 'lolacore'),
					'menu_name'          => __('Block Builder', 'lolacore'),
					'add_new'            => __('Add New', 'lolacore'),
					'add_new_item'       => __('Add New Block', 'lolacore'),
					'edit_item'          => __('Edit Block', 'lolacore'),
					'new_item'           => __('New Block', 'lolacore'),
					'view_item'          => __('View Block', 'lolacore'),
					'search_items'       => __('Search Blocks', 'lolacore'),
					'not_found'          => __('No built blocks found.', 'lolacore'),
					'not_found_in_trash' => __('No built blocks found in Trash.', 'lolacore'),
				),
				'public'              => false,
				'show_ui'             => true,
				'show_in_menu'        => false,
				'show_in_rest'        => false,
				'exclude_from_search' => true,
				'publicly_queryable'  => false,
				'has_archive'         => false,
				'hierarchical'        => false,
				'supports'            => array('title', 'revisions'),
				'capability_type'     => 'post',
				'map_meta_cap'        => true,
				'capabilities'        => array(
					'edit_posts'             => 'manage_options',
					'edit_others_posts'      => 'manage_options',
					'delete_posts'           => 'manage_options',
					'delete_others_posts'    => 'manage_options',
					'delete_private_posts'   => 'manage_options',
					'delete_published_posts' => 'manage_options',
					'publish_posts'          => 'manage_options',
					'read_private_posts'     => 'manage_options',
					'create_posts'           => 'manage_options',
				),
			)
		);
	}

	public function registerMeta(): void {
		$auth_callback = static function (): bool {
			return current_user_can('manage_options');
		};

		foreach (
			array(
				self::META_BLOCK_SLUG  => 'sanitize_title',
				self::META_NAME        => 'sanitize_text_field',
				self::META_PLUGIN_SLUG => 'sanitize_title',
				self::META_DESCRIPTION => 'sanitize_textarea_field',
			) as $key => $sanitize
		) {
			register_post_meta(
				self::POST_TYPE,
				$key,
				array(
					'type'              => 'string',
					'single'            => true,
					'show_in_rest'      => false,
					'auth_callback'     => $auth_callback,
					'sanitize_callback' => $sanitize,
				)
			);
		}

		register_post_meta(
			self::POST_TYPE,
			self::META_FILES,
			array(
				'type'              => 'string',
				'single'            => true,
				'show_in_rest'      => false,
				'auth_callback'     => $auth_callback,
				'sanitize_callback' => array($this, 'sanitizeFilesJson'),
			)
		);

		register_post_meta(
			self::POST_TYPE,
			self::META_VERSION,
			array(
				'type'              => 'integer',
				'single'            => true,
				'show_in_rest'      => false,
				'auth_callback'     => $auth_callback,
				'sanitize_callback' => 'absint',
				'default'           => 1,
			)
		);

		register_post_meta(
			self::POST_TYPE,
			self::META_CONFIDENCE,
			array(
				'type'              => 'integer',
				'single'            => true,
				'show_in_rest'      => false,
				'auth_callback'     => $auth_callback,
				'sanitize_callback' => 'absint',
			)
		);

		register_post_meta(
			self::POST_TYPE,
			self::META_CONFIDENCE_NOTE,
			array(
				'type'              => 'string',
				'single'            => true,
				'show_in_rest'      => false,
				'auth_callback'     => $auth_callback,
				'sanitize_callback' => 'sanitize_textarea_field',
			)
		);

		register_post_meta(
			self::POST_TYPE,
			self::META_LOAD_SAFE_STATUS,
			array(
				'type'              => 'string',
				'single'            => true,
				'show_in_rest'      => false,
				'auth_callback'     => $auth_callback,
				'sanitize_callback' => 'sanitize_key',
			)
		);

		register_post_meta(
			self::POST_TYPE,
			self::META_SAFETY_FINDINGS,
			array(
				'type'              => 'string',
				'single'            => true,
				'show_in_rest'      => false,
				'auth_callback'     => $auth_callback,
				'sanitize_callback' => array($this, 'sanitizeSafetyFindingsJson'),
			)
		);

		register_post_meta(
			self::POST_TYPE,
			self::META_PG_GATE_STATUS,
			array(
				'type'              => 'string',
				'single'            => true,
				'show_in_rest'      => false,
				'auth_callback'     => $auth_callback,
				'sanitize_callback' => 'sanitize_key',
			)
		);

		register_post_meta(
			self::POST_TYPE,
			self::META_PG_ATTESTED_VERSION,
			array(
				'type'              => 'integer',
				'single'            => true,
				'show_in_rest'      => false,
				'auth_callback'     => $auth_callback,
				'sanitize_callback' => 'absint',
			)
		);

		foreach (array(self::META_PG_ATTESTED_AT, self::META_PG_OPENED_AT) as $datetime_key) {
			register_post_meta(
				self::POST_TYPE,
				$datetime_key,
				array(
					'type'              => 'string',
					'single'            => true,
					'show_in_rest'      => false,
					'auth_callback'     => $auth_callback,
					'sanitize_callback' => 'sanitize_text_field',
				)
			);
		}
	}

	/**
	 * @param array<string> $keys      Registered revision meta keys.
	 * @param string        $post_type Post type slug.
	 * @return array<string>
	 */
	public function addRevisionMetaKeys( array $keys, string $post_type ): array {
		if ($post_type !== self::POST_TYPE) {
			return $keys;
		}

		return array_values(array_unique(array_merge($keys, self::revisionMetaKeys())));
	}

	/**
	 * Normalize stored _lc_bb_files meta into a path => content map.
	 *
	 * DB values are JSON strings without magic quotes; wp_unslash() on read breaks
	 * valid JSON (slashes inside file contents get stripped). Try plain decode first.
	 *
	 * @return array<string, string>|null
	 */
	public static function decodeFilesMeta( mixed $raw ): ?array {
		if (is_array($raw)) {
			$decoded = $raw;
		} elseif (is_object($raw)) {
			$decoded = json_decode(wp_json_encode($raw) ?: '{}', true);
		} elseif (is_string($raw) && $raw !== '') {
			$decoded = json_decode($raw, true);
			if (! is_array($decoded)) {
				$decoded = json_decode(wp_unslash($raw), true);
			}
		} else {
			return null;
		}

		if (! is_array($decoded)) {
			return null;
		}

		$files = array();
		foreach ($decoded as $path => $content) {
			if (is_string($path) && is_string($content)) {
				$files[ $path ] = $content;
			}
		}

		return $files !== array() ? $files : null;
	}

	/**
	 * @param mixed $value Raw meta value.
	 */
	public function sanitizeFilesJson( $value ): string {
		if (is_string($value)) {
			// WordPress may pass slashed JSON into the sanitize callback; decode fails
			// without wp_unslash() and the meta collapses to "{}" (empty file map).
			$decoded = json_decode(wp_unslash($value), true);
			if (is_array($decoded)) {
				$value = $decoded;
			}
		}

		if (! is_array($value)) {
			return '{}';
		}

		$clean = array();
		foreach ($value as $path => $content) {
			if (! is_string($path) || ! is_string($content)) {
				continue;
			}
			$clean[ $path ] = $content;
		}

		return wp_json_encode($clean) ?: '{}';
	}

	/**
	 * @param mixed $value Raw meta value.
	 */
	public function sanitizeSafetyFindingsJson( $value ): string {
		if (is_string($value)) {
			$decoded = json_decode(wp_unslash($value), true);
			if (is_array($decoded)) {
				$value = $decoded;
			}
		}

		if (! is_array($value)) {
			return '[]';
		}

		$findings = array();
		foreach ($value as $finding) {
			if (! is_array($finding)) {
				continue;
			}

			$code = sanitize_key((string) ( $finding['code'] ?? '' ));
			if ($code === '') {
				continue;
			}

			$severity = sanitize_key((string) ( $finding['severity'] ?? 'warning' ));
			if (! in_array($severity, array('fatal', 'error', 'warning'), true)) {
				$severity = 'warning';
			}

			$entry = array(
				'severity' => $severity,
				'code'     => $code,
				'message'  => sanitize_text_field((string) ( $finding['message'] ?? '' )),
			);

			if (isset($finding['line'])) {
				$entry['line'] = max(0, (int) $finding['line']);
			}

			$findings[] = $entry;
			if (count($findings) >= 20) {
				break;
			}
		}

		return wp_json_encode($findings) ?: '[]';
	}
}
