<?php
/**
 * ppok/set-post-blocks — write a parsed block tree back to a post.
 *
 * Counterpart to ppok/get-post-blocks: accepts the same parse_blocks() shape
 * that get-post-blocks returns, runs the input through serialize_blocks(),
 * and saves the result via wp_update_post(). A no-op round-trip — get then
 * set with no modifications — produces byte-identical post_content.
 *
 * Gating (per architecture.exposure_policy + architecture.scoped_writes_layers):
 *   L1 capability   — current_user_can('edit_post', $post_id)
 *   L2 schema       — rest_validate_value_from_schema rejects bad input
 *   L3 block list   — Block_Allowlist::check() validates every blockName
 *   L4 scope        — Scope_Gate::check_post_type/status() against settings
 *   L5 audit        — Ability_Registrar wrapper records one row per call
 *
 * Returns revision_id of the pre-update revision (the agent's rollback
 * target). When WP revisions are disabled or unsupported for the post type
 * revision_id is null — the write still happens but no rollback handle is
 * issued, matching the documented behavior in
 * .context/project.json → logging.payload_shape.diff.revision_id.
 *
 * @package Ppok\AIToolkit
 * @license GPL-2.0-or-later
 */

namespace Ppok\AIToolkit\Abilities;

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

final class Post_Set_Blocks {

	public const SLUG = 'ppok/set-post-blocks';

	public static function descriptor(): array {
		return array(
			'label'               => __( 'Set post blocks', 'ppok-ai-toolkit' ),
			'description'         => __( 'Replaces a post\'s content with a serialized block tree. Pass blocks in the exact shape ppok/get-post-blocks returns. Server runs serialize_blocks() so the result round-trips cleanly through parse_blocks(). Gated by edit_post capability + admin-configured write scope (post types, statuses) + block allowlist. Returns revision_id of the pre-update revision for one-call rollback when revisions are enabled.', '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' => true ),
			),
		);
	}

	public static function example(): array {
		return array(
			'post_id' => 1,
			'blocks'  => array(
				array(
					'blockName'    => 'core/paragraph',
					'attrs'        => new \stdClass(),
					'innerBlocks'  => array(),
					'innerHTML'    => '<p>Hello world.</p>',
					'innerContent' => array( '<p>Hello world.</p>' ),
				),
			),
			'reason'  => 'Reset post content to a single paragraph.',
		);
	}

	public static function input_schema(): array {
		return array(
			'type'                 => 'object',
			'additionalProperties' => false,
			'required'             => array( 'post_id', 'blocks', 'reason' ),
			'properties'           => array(
				'post_id' => array(
					'type'    => 'integer',
					'minimum' => 1,
				),
				'blocks'  => array(
					'type'        => 'array',
					'description' => 'Parse_blocks()-shape tree. Each item: {blockName, attrs, innerBlocks, innerHTML, innerContent}. blockName may be null for classic-editor freeform markup.',
				),
				'reason'  => array(
					'type'        => 'string',
					'description' => 'Short human-readable rationale for the change. Required, audit-logged.',
					'minLength'   => 8,
					'maxLength'   => 500,
				),
			),
		);
	}

	public static function output_schema(): array {
		return array(
			'type'       => 'object',
			'required'   => array( 'post_id', 'post_status', 'bytes_written' ),
			'properties' => array(
				'post_id'       => array( 'type' => 'integer' ),
				'post_status'   => array( 'type' => 'string' ),
				'bytes_written' => array( 'type' => 'integer' ),
				'revision_id'   => array(
					'type'        => array( 'integer', 'null' ),
					'description' => 'ID of the revision saved BEFORE this write (rollback target). null if revisions are disabled or unsupported for the post type.',
				),
			),
		);
	}

	public static function permission_callback( array $args = array() ): bool {
		$post_id = isset( $args['post_id'] ) ? (int) $args['post_id'] : 0;
		if ( $post_id < 1 ) {
			return false;
		}
		return current_user_can( 'edit_post', $post_id );
	}

	public static function execute_callback( array $args = array() ) {
		$post_id = isset( $args['post_id'] ) ? (int) $args['post_id'] : 0;
		$blocks  = isset( $args['blocks'] ) && is_array( $args['blocks'] ) ? $args['blocks'] : array();

		$post = $post_id > 0 ? get_post( $post_id ) : null;
		if ( ! $post instanceof \WP_Post ) {
			return new \WP_Error(
				'ppok_post_not_found',
				__( 'Post not found.', 'ppok-ai-toolkit' ),
				array( 'status' => 404 )
			);
		}

		// L4: scope gate.
		$scope = Scope_Gate::check_post_type( (string) $post->post_type );
		if ( is_wp_error( $scope ) ) {
			return $scope;
		}
		$scope = Scope_Gate::check_post_status( (string) $post->post_status );
		if ( is_wp_error( $scope ) ) {
			return $scope;
		}

		// L3: block allowlist.
		$allow = Block_Allowlist::check( $blocks );
		if ( is_wp_error( $allow ) ) {
			return $allow;
		}

		// Capture a rollback handle BEFORE we mutate. wp_save_post_revision
		// returns int|null; non-int values mean revisions are disabled or
		// unsupported, in which case we just hand the agent a null handle.
		$revision_id = null;
		if ( function_exists( 'wp_save_post_revision' ) ) {
			$maybe = wp_save_post_revision( $post_id );
			if ( is_numeric( $maybe ) && (int) $maybe > 0 ) {
				$revision_id = (int) $maybe;
			}
		}

		$serialized = serialize_blocks( $blocks );

		$update_result = wp_update_post(
			array(
				'ID'           => $post_id,
				'post_content' => $serialized,
			),
			true
		);
		if ( is_wp_error( $update_result ) ) {
			return $update_result;
		}

		// Re-read the post so the response carries the current status the DB
		// actually ended up with (wp_update_post may transition through filters).
		$updated = get_post( $post_id );
		$status  = $updated instanceof \WP_Post ? (string) $updated->post_status : (string) $post->post_status;

		return array(
			'post_id'       => $post_id,
			'post_status'   => $status,
			'bytes_written' => strlen( $serialized ),
			'revision_id'   => $revision_id,
		);
	}
}
