<?php
/**
 * Append_content tool: append Gutenberg comment-serialized blocks or small HTML to session buffer.
 *
 * @package AgenticWP
 */

namespace Agentic_WP;

use Agentic_WP\Error_Handler;

defined( 'ABSPATH' ) || exit;

/**
 * Append content tool for appending Gutenberg blocks or HTML to session buffer.
 *
 * @since 1.0.0
 */
class Tool_Append_Content {
	/**
	 * Returns the OpenAI tool schema for appending content.
	 *
	 * @since 1.0.0
	 *
	 * @return array Tool schema definition.
	 */
	public static function schema(): array {
		return array(
			'type'        => 'function',
			'name'        => 'append_content',
			'description' => 'Append Gutenberg block markup chunk or small HTML to the in-progress post session.',
			'strict'      => true,
			'parameters'  => array(
				'type'                 => 'object',
				'properties'           => array(
					'session_id'    => array( 'type' => 'string' ),
					'content_chunk' => array(
						'type'        => 'string',
						'description' => 'Comment-serialized blocks or small HTML.',
					),
				),
				'required'             => array( 'session_id', 'content_chunk' ),
				'additionalProperties' => false,
			),
		);
	}

	/**
	 * Executes the append content tool.
	 *
	 * @since 1.0.0
	 *
	 * @param array $args Tool arguments containing session_id and content_chunk.
	 * @return string JSON response or error message.
	 */
	public static function run( array $args ): string {
		$sid   = isset( $args['session_id'] ) && is_string( $args['session_id'] ) ? sanitize_text_field( $args['session_id'] ) : '';
		$chunk = isset( $args['content_chunk'] ) && is_string( $args['content_chunk'] ) ? (string) $args['content_chunk'] : '';
		if ( '' === $sid || '' === $chunk ) {
			Error_Handler::log_error(
				'tool_append_content',
				'Missing required arguments',
				array(
					'has_session_id' => '' !== $sid,
					'has_chunk'      => '' !== $chunk,
				)
			);
			return 'ERROR: missing_args';
		}

		$session = get_transient( Tool_Start_Post::tkey( $sid ) );
		if ( ! is_array( $session ) || empty( $session['post_id'] ) ) {
			Error_Handler::log_error(
				'tool_append_content',
				'Invalid or expired session',
				array( 'session_id' => $sid )
			);
			return 'ERROR: invalid_session';
		}

		$sanitized_serialized = self::sanitize_and_serialize( $chunk );
		if ( strpos( $sanitized_serialized, 'ERROR:' ) === 0 ) {
			return $sanitized_serialized;
		}

		$timeout                 = max( 5, (int) Settings::get_session_timeout_min() ) * MINUTE_IN_SECONDS;
		$session['content']     .= ( '' === $session['content'] ? '' : "\n" ) . $sanitized_serialized;
		$session['bytes_total'] += strlen( $sanitized_serialized );
		set_transient( Tool_Start_Post::tkey( $sid ), $session, $timeout );

		Error_Handler::debug_log(
			'Content appended to session',
			array(
				'session_id'     => $sid,
				'appended_bytes' => strlen( $sanitized_serialized ),
				'total_bytes'    => (int) $session['bytes_total'],
			)
		);

		return wp_json_encode(
			array(
				'status'         => 'ok',
				'appended_bytes' => strlen( $sanitized_serialized ),
				'total_bytes'    => (int) $session['bytes_total'],
			)
		);
	}

	/**
	 * Sanitizes and serializes content chunk.
	 *
	 * @since 1.0.0
	 *
	 * @param string $chunk Content chunk to process.
	 * @return string Sanitized and serialized content or error string.
	 */
	private static function sanitize_and_serialize( string $chunk ): string {
		$chunk_trim = trim( $chunk );
		if ( preg_match( '/^```[a-zA-Z0-9_-]*\s*\n(.*)\n```$/s', $chunk_trim, $m ) ) {
			$chunk_trim = trim( (string) $m[1] );
		}
		$has_wp = strpos( $chunk_trim, '<!-- wp:' ) !== false;
		if ( ! $has_wp ) {
			$safe_html = wp_kses_post( $chunk_trim );
			$is_small  = strlen( $safe_html ) <= 512;
			if ( ! $is_small ) {
				Error_Handler::log_error(
					'tool_append_content',
					'Content too large for HTML snippet',
					array( 'size' => strlen( $safe_html ) )
				);
				return 'ERROR: expected_block_markup';
			}
			return '<!-- wp:html -->' . $safe_html . '<!-- /wp:html -->';
		}

		if ( ! function_exists( 'parse_blocks' ) || ! function_exists( 'serialize_blocks' ) ) {
			Error_Handler::log_error(
				'tool_append_content',
				'WordPress block functions unavailable'
			);
			return 'ERROR: wp_block_functions_unavailable';
		}

		$attr_hint_count = 0;
		if ( preg_match_all( '/<!--\s*wp:[^>]*\{/', $chunk_trim, $mm ) ) {
			$attr_hint_count = (int) count( $mm[0] );
		}

		$blocks = parse_blocks( $chunk_trim );
		if ( ! is_array( $blocks ) || empty( $blocks ) ) {
			Error_Handler::log_error(
				'tool_append_content',
				'Failed to parse blocks from chunk'
			);
			return 'ERROR: parse_failed';
		}

		$registry = \WP_Block_Type_Registry::get_instance();
		foreach ( $blocks as &$b ) {
			$valid = self::sanitize_block_recursive( $b, $registry );
			if ( ! $valid ) {
				$block_name = isset( $b['blockName'] ) ? $b['blockName'] : 'unknown';
				Error_Handler::log_error(
					'tool_append_content',
					'Invalid or unregistered block',
					array( 'block_name' => $block_name )
				);
				return 'ERROR: invalid_block_slug';
			}
		}
		unset( $b );

		if ( $attr_hint_count > 0 ) {
			$parsed_attr_blocks = self::count_blocks_with_nonempty_attrs( $blocks );
			if ( $parsed_attr_blocks < $attr_hint_count ) {
				Error_Handler::log_error(
					'tool_append_content',
					'Block attributes JSON parsing failed',
					array(
						'expected_attrs' => $attr_hint_count,
						'parsed_attrs'   => $parsed_attr_blocks,
					)
				);
				return 'ERROR: invalid_block_attributes_json';
			}
		}

		return serialize_blocks( $blocks );
	}

	/**
	 * Sanitizes block recursively and validates against registry.
	 *
	 * @since 1.0.0
	 *
	 * @param array $block    Block to sanitize (passed by reference).
	 * @param mixed $registry Block registry instance.
	 * @return bool True if valid, false otherwise.
	 */
	private static function sanitize_block_recursive( array &$block, $registry ): bool {
		$name = isset( $block['blockName'] ) ? (string) $block['blockName'] : '';
		if ( '' !== $name ) {
			if ( method_exists( $registry, 'is_registered' ) ) {
				if ( ! $registry->is_registered( $name ) ) {
					return false;
				}
			} else {
				$all = $registry->get_all_registered();
				if ( ! isset( $all[ $name ] ) ) {
					return false;
				}
			}
		}

		if ( isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
			foreach ( $block['innerBlocks'] as &$child ) {
				$ok = self::sanitize_block_recursive( $child, $registry );
				if ( ! $ok ) {
					return false;
				}
			}
			unset( $child );
		}

		return true;
	}

	/**
	 * Counts blocks with non-empty attributes recursively.
	 *
	 * @since 1.0.0
	 *
	 * @param array $blocks Blocks array to count.
	 * @return int Count of blocks with attributes.
	 */
	private static function count_blocks_with_nonempty_attrs( array $blocks ): int {
		$count = 0;
		foreach ( $blocks as $b ) {
			if ( isset( $b['attrs'] ) && is_array( $b['attrs'] ) && ! empty( $b['attrs'] ) ) {
				++$count;
			}
			if ( isset( $b['innerBlocks'] ) && is_array( $b['innerBlocks'] ) && ! empty( $b['innerBlocks'] ) ) {
				$count += self::count_blocks_with_nonempty_attrs( $b['innerBlocks'] );
			}
		}
		return $count;
	}
}
