<?php
/**
 * ppok/blocks-find-usage — list posts that contain a given block.
 *
 * Read-only. Cap: edit_posts. Replaces the "fetch every post and grep
 * through it" workaround agents previously used to find where a given
 * block type appears across the corpus.
 *
 * Implementation: index-friendly SQL LIKE prefilter on post_content,
 * then parse_blocks() + recursive walk over the candidate tree to count
 * exact blockName matches. The parse step ensures we don't false-positive
 * on a block name that happens to appear inside another block's
 * innerHTML (e.g. a literal "<!-- wp:core/separator -->" sitting in a
 * code block).
 *
 * Core-namespace shortform: WordPress serializes core blocks without the
 * "core/" prefix in older content (e.g. <!-- wp:separator --> instead of
 * <!-- wp:core/separator -->). parse_blocks() normalizes both to the
 * canonical "core/<name>" blockName, but the LIKE prefilter would miss
 * the shortform — so for core/* names we issue two LIKE alternates.
 *
 * @package Ppok\AIToolkit
 * @license GPL-2.0-or-later
 */

namespace Ppok\AIToolkit\Abilities;

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

final class Blocks_Find_Usage {

	public const SLUG             = 'ppok/blocks-find-usage';
	public const PER_PAGE_MAX     = 100;
	public const PER_PAGE_DEFAULT = 20;

	public static function descriptor(): array {
		return array(
			'label'               => __( 'Find block usage', 'ppok-ai-toolkit' ),
			'description'         => __( 'Returns posts that contain a given block type, with per-post occurrence counts. Walks innerBlocks recursively so nested usage is included. Read-only.', 'ppok-ai-toolkit' ),
			'category'            => 'ppok',
			'input_schema'        => self::input_schema(),
			'output_schema'       => self::output_schema(),
			'permission_callback' => array( __CLASS__, 'permission_callback' ),
			'execute_callback'    => array( __CLASS__, 'execute_callback' ),
			'meta'                => array(
				'show_in_rest' => true,
				'mcp'          => array( 'public' => true ),
				'ppok'         => array( 'write' => false ),
			),
		);
	}

	public static function input_schema(): array {
		return array(
			'type'                 => 'object',
			'required'             => array( 'block_name' ),
			'additionalProperties' => false,
			'properties'           => array(
				'block_name'  => array(
					'type'        => 'string',
					'minLength'   => 1,
					'maxLength'   => 191,
					'pattern'     => '^[a-z0-9][a-z0-9-]*\\/[a-z0-9][a-z0-9-]*$',
					'description' => __( 'Canonical block name, e.g. "core/separator" or "acf/feature-card".', 'ppok-ai-toolkit' ),
				),
				'post_type'   => array(
					'type'    => 'array',
					'items'   => array( 'type' => 'string' ),
					'default' => array( 'post', 'page' ),
				),
				'post_status' => array(
					'type'    => 'array',
					'items'   => array( 'type' => 'string' ),
					'default' => array( 'publish', 'pending' ),
				),
				'per_page'    => array(
					'type'    => 'integer',
					'minimum' => 1,
					'maximum' => self::PER_PAGE_MAX,
					'default' => self::PER_PAGE_DEFAULT,
				),
				'page'        => array(
					'type'    => 'integer',
					'minimum' => 1,
					'default' => 1,
				),
			),
		);
	}

	public static function output_schema(): array {
		return array(
			'type'       => 'object',
			'required'   => array( 'block_name', 'posts', 'total', 'pages' ),
			'properties' => array(
				'block_name' => array( 'type' => 'string' ),
				'posts'      => array(
					'type'  => 'array',
					'items' => array(
						'type'       => 'object',
						'required'   => array( 'post_id', 'count', 'post_title', 'post_status', 'post_type' ),
						'properties' => array(
							'post_id'     => array( 'type' => 'integer' ),
							'count'       => array( 'type' => 'integer' ),
							'post_title'  => array( 'type' => 'string' ),
							'post_status' => array( 'type' => 'string' ),
							'post_type'   => array( 'type' => 'string' ),
						),
					),
				),
				'total'      => array( 'type' => 'integer' ),
				'pages'      => array( 'type' => 'integer' ),
			),
		);
	}

	public static function example(): array {
		return array(
			'block_name' => 'core/separator',
			'post_type'  => array( 'post', 'page' ),
		);
	}

	public static function permission_callback(): bool {
		return current_user_can( 'edit_posts' );
	}

	public static function execute_callback( array $args = array() ) {
		global $wpdb;

		$block_name = isset( $args['block_name'] ) ? (string) $args['block_name'] : '';
		if ( $block_name === '' || strpos( $block_name, '/' ) === false ) {
			return new \WP_Error(
				'rest_invalid_param',
				__( 'block_name is required and must be "<namespace>/<name>".', 'ppok-ai-toolkit' ),
				array( 'status' => 400 )
			);
		}

		$post_types  = self::normalize_string_list( $args['post_type'] ?? array( 'post', 'page' ) );
		$post_status = self::normalize_string_list( $args['post_status'] ?? array( 'publish', 'pending' ) );
		$per_page    = min( self::PER_PAGE_MAX, max( 1, (int) ( $args['per_page'] ?? self::PER_PAGE_DEFAULT ) ) );
		$page        = max( 1, (int) ( $args['page'] ?? 1 ) );

		// Build the LIKE-prefilter alternates: canonical, plus core
		// shortform when block_name starts with "core/".
		$like_terms = self::like_terms_for( $block_name );

		// Compose the SQL by hand because WP_Query has no IS_BLOCK_USAGE
		// primitive and 'posts_where' filters would force two passes —
		// once to filter, once to count.
		$where_parts = array();
		$params      = array();

		$like_clauses = array();
		foreach ( $like_terms as $term ) {
			$like_clauses[] = 'post_content LIKE %s';
			$params[]       = '%' . $wpdb->esc_like( $term ) . '%';
		}
		$where_parts[] = '(' . implode( ' OR ', $like_clauses ) . ')';

		if ( ! empty( $post_types ) ) {
			$placeholders   = implode( ',', array_fill( 0, count( $post_types ), '%s' ) );
			$where_parts[]  = "post_type IN ($placeholders)";
			$params         = array_merge( $params, $post_types );
		}
		if ( ! empty( $post_status ) ) {
			$placeholders   = implode( ',', array_fill( 0, count( $post_status ), '%s' ) );
			$where_parts[]  = "post_status IN ($placeholders)";
			$params         = array_merge( $params, $post_status );
		}

		$where_sql = 'WHERE ' . implode( ' AND ', $where_parts );

		// Pull every candidate row up front. We need to parse each one
		// to confirm the LIKE hit was real, and the rendered total
		// reflects post-parse truth, not LIKE-prefilter truth — so
		// SQL-side LIMIT/OFFSET would distort the count. Candidates are
		// bounded by the LIKE selectivity in practice (block-content
		// LIKE on a specific marker rarely returns more than a few
		// hundred rows even on large sites).
		$sql = "SELECT ID, post_title, post_status, post_type, post_content FROM {$wpdb->posts} {$where_sql} ORDER BY post_modified DESC";
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$rows = $wpdb->get_results( $wpdb->prepare( $sql, $params ) );

		$matches = array();
		foreach ( (array) $rows as $row ) {
			$count = self::count_blocks_in_content( $block_name, (string) $row->post_content );
			if ( $count < 1 ) {
				continue;
			}
			$matches[] = array(
				'post_id'     => (int) $row->ID,
				'count'       => $count,
				'post_title'  => (string) $row->post_title,
				'post_status' => (string) $row->post_status,
				'post_type'   => (string) $row->post_type,
			);
		}

		$total  = count( $matches );
		$pages  = $total === 0 ? 0 : (int) ceil( $total / $per_page );
		$slice  = array_slice( $matches, ( $page - 1 ) * $per_page, $per_page );

		return array(
			'block_name' => $block_name,
			'posts'      => $slice,
			'total'      => $total,
			'pages'      => $pages,
		);
	}

	/**
	 * Recursively count occurrences of $block_name across a parsed
	 * block tree. Exposed public for testability.
	 *
	 * @param string $block_name Canonical block name (with namespace).
	 * @param string $content    Raw post_content.
	 */
	public static function count_blocks_in_content( string $block_name, string $content ): int {
		if ( $content === '' ) {
			return 0;
		}
		$blocks = parse_blocks( $content );
		return self::count_in_tree( $block_name, $blocks );
	}

	/**
	 * @param array<int,array<string,mixed>> $blocks
	 */
	private static function count_in_tree( string $block_name, array $blocks ): int {
		$count = 0;
		foreach ( $blocks as $block ) {
			if ( ! is_array( $block ) ) {
				continue;
			}
			if ( isset( $block['blockName'] ) && (string) $block['blockName'] === $block_name ) {
				$count++;
			}
			if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
				$count += self::count_in_tree( $block_name, $block['innerBlocks'] );
			}
		}
		return $count;
	}

	/**
	 * LIKE terms the prefilter should match. Canonical marker plus the
	 * core-shortform alias for core/* names.
	 *
	 * @return array<int,string>
	 */
	public static function like_terms_for( string $block_name ): array {
		$terms = array( '<!-- wp:' . $block_name . ' ' );
		if ( strpos( $block_name, 'core/' ) === 0 ) {
			$terms[] = '<!-- wp:' . substr( $block_name, strlen( 'core/' ) ) . ' ';
		}
		return $terms;
	}

	/**
	 * @param mixed $value
	 * @return array<int,string>
	 */
	private static function normalize_string_list( $value ): array {
		if ( is_string( $value ) ) {
			$value = array( $value );
		}
		if ( ! is_array( $value ) ) {
			return array();
		}
		return array_values( array_map( 'strval', array_filter( $value, 'is_scalar' ) ) );
	}
}
