<?php
/**
 * Frontend edit: update a Gutenberg BM block instance in post_content.
 *
 * @package JOCBM
 */

namespace JOCBM\Rest;

use JOCBM\Database\Schema;
use JOCBM\Services\AttrSanitizer;
use JOCBM\Services\BlockMarkup;
use JOCBM\Services\CssVars;
use JOCBM\Services\MarkupInterpolator;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

defined( 'ABSPATH' ) || exit;

/**
 * Class FrontEditController
 */
final class FrontEditController {

	use RestPermissions;

	/**
	 * Register routes.
	 */
	public function register_routes(): void {
		register_rest_route(
			RestApi::NAMESPACE,
			'/front-edit/block',
			array(
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_block' ),
					'permission_callback' => array( $this, 'can_edit_post_param' ),
					'args'                => array(
						'postId'     => array(
							'required' => true,
							'type'     => 'integer',
						),
						'blockIndex' => array(
							'required' => true,
							'type'     => 'integer',
						),
						'name'       => array(
							'required' => true,
							'type'     => 'string',
						),
					),
				),
			)
		);
	}

	/**
	 * @param WP_REST_Request $request Request.
	 * @return bool|WP_Error
	 */
	public function can_edit_post_param( WP_REST_Request $request ) {
		if ( ! \JOCBM\Settings::is_frontend_editor_enabled() ) {
			return new WP_Error(
				'jocbm_frontend_editor_disabled',
				__( 'Frontend editing is disabled in Blockora Morph system settings.', 'joc-blockora-morph' ),
				array( 'status' => 403 )
			);
		}
		$post_id = absint( $request->get_param( 'postId' ) );
		if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) {
			return new WP_Error(
				'jocbm_forbidden',
				__( 'You do not have permission to edit this post.', 'joc-blockora-morph' ),
				array( 'status' => 403 )
			);
		}
		return true;
	}

	/**
	 * Update one BM block in post_content by BM-only index order.
	 *
	 * @param WP_REST_Request $request Request.
	 */
	public function update_block( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		$post_id     = absint( $request->get_param( 'postId' ) );
		$block_index = absint( $request->get_param( 'blockIndex' ) );
		$name        = sanitize_text_field( (string) $request->get_param( 'name' ) );
		$attributes  = $this->json_param( $request, 'attributes', array() );
		$css_vars    = $this->json_param( $request, 'cssVars', null );

		if ( ! is_array( $attributes ) ) {
			$attributes = array();
		}

		$post = get_post( $post_id );
		if ( ! $post ) {
			return new WP_Error(
				'jocbm_not_found',
				__( 'Post not found.', 'joc-blockora-morph' ),
				array( 'status' => 404 )
			);
		}

		if ( 0 !== strpos( $name, 'joc-blockora-morph/' ) ) {
			return new WP_Error(
				'jocbm_invalid',
				__( 'Invalid block name.', 'joc-blockora-morph' ),
				array( 'status' => 400 )
			);
		}

		$blocks = parse_blocks( $post->post_content );
		$found  = $this->update_bm_block_at_index( $blocks, $block_index, $name, $attributes, $css_vars );

		if ( is_wp_error( $found ) ) {
			return $found;
		}

		$updated = wp_update_post(
			array(
				'ID'           => $post_id,
				// wp_slash required so block JSON attrs (cssVars colors, etc.) survive kses/slashes.
				'post_content' => wp_slash( serialize_blocks( $blocks ) ),
			),
			true
		);

		if ( is_wp_error( $updated ) ) {
			return $updated;
		}

		$html = $this->render_instance_html( $name, $found['attrs'] );

		return new WP_REST_Response(
			array(
				'ok'         => true,
				'postId'     => $post_id,
				'blockIndex' => $block_index,
				'name'       => $name,
				'attributes' => $found['attrs'],
				'html'       => $html,
			),
			200
		);
	}

	/**
	 * Walk blocks; BM blocks counted in document order (matches data-bm-index).
	 *
	 * @param array<int, array<string, mixed>> $blocks     Parsed blocks (by ref).
	 * @param int                              $target     BM index.
	 * @param string                           $name       Expected block name.
	 * @param array<string, mixed>             $attributes Field attrs (no cssVars).
	 * @param mixed                            $css_vars   cssVars or null to leave.
	 * @return array<string, mixed>|WP_Error Updated block attrs bag.
	 */
	private function update_bm_block_at_index( array &$blocks, int $target, string $name, array $attributes, $css_vars ) {
		$counter = 0;
		$result  = $this->walk_update( $blocks, $counter, $target, $name, $attributes, $css_vars );
		if ( null === $result ) {
			return new WP_Error(
				'jocbm_block_not_found',
				__( 'Block instance not found.', 'joc-blockora-morph' ),
				array( 'status' => 404 )
			);
		}
		return $result;
	}

	/**
	 * @param array<int, array<string, mixed>> $blocks     Blocks.
	 * @param int                              $counter    Running BM index.
	 * @param int                              $target     Target index.
	 * @param string                           $name       Expected name.
	 * @param array<string, mixed>             $attributes Attrs.
	 * @param mixed                            $css_vars   Css vars.
	 * @return array<string, mixed>|null
	 */
	private function walk_update( array &$blocks, int &$counter, int $target, string $name, array $attributes, $css_vars ) {
		foreach ( $blocks as &$block ) {
			$block_name = (string) ( $block['blockName'] ?? '' );
			if ( 0 === strpos( $block_name, 'joc-blockora-morph/' ) ) {
				if ( $counter === $target ) {
					if ( $block_name !== $name ) {
						return null;
					}
					$attrs = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : array();
					foreach ( $attributes as $key => $value ) {
						if ( 'cssVars' === $key ) {
							continue;
						}
						$attrs[ $key ] = $value;
					}
					$schema = $this->field_schema_for_block_name( $name );
					$attrs  = AttrSanitizer::sanitize( $attrs, $schema );
					if ( null !== $css_vars && is_array( $css_vars ) ) {
						$attrs['cssVars'] = CssVars::normalize( $css_vars );
					}
					$block['attrs'] = $attrs;
					return array( 'attrs' => $attrs );
				}
				++$counter;
			}
			if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
				$inner = $this->walk_update( $block['innerBlocks'], $counter, $target, $name, $attributes, $css_vars );
				if ( null !== $inner ) {
					return $inner;
				}
			}
		}
		return null;
	}

	/**
	 * Field schema for a registered JOC Blockora Morph block name.
	 *
	 * @param string $name Block name (joc-blockora-morph/slug).
	 * @return array<int, array<string, mixed>>
	 */
	private function field_schema_for_block_name( string $name ): array {
		$slug = substr( $name, strlen( 'joc-blockora-morph/' ) );
		if ( '' === $slug ) {
			return array();
		}

		global $wpdb;
		$user_table   = Schema::table( 'user_blocks' );
		$system_table = Schema::table( 'system_blocks' );
		$row          = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT COALESCE(NULLIF(u.field_schema, ''), s.field_schema) AS field_schema
				FROM %i u
				INNER JOIN %i s ON s.id = u.system_block_id
				WHERE u.slug = %s
				LIMIT 1",
				$user_table,
				$system_table,
				$slug
			),
			ARRAY_A
		);

		if ( ! $row ) {
			return array();
		}

		$schema = json_decode( $row['field_schema'] ?? '[]', true );
		return is_array( $schema ) ? $schema : array();
	}

	/**
	 * Re-render one instance for live DOM replace (best-effort).
	 *
	 * @param string               $name  Block name.
	 * @param array<string, mixed> $attrs Block attrs.
	 */
	private function render_instance_html( string $name, array $attrs ): string {
		$slug = substr( $name, strlen( 'joc-blockora-morph/' ) );
		if ( '' === $slug ) {
			return '';
		}

		global $wpdb;
		$user_table   = Schema::table( 'user_blocks' );
		$system_table = Schema::table( 'system_blocks' );
		$row          = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT u.*, COALESCE(NULLIF(u.markup, ''), s.markup) AS markup
				FROM %i u
				INNER JOIN %i s ON s.id = u.system_block_id
				WHERE u.slug = %s AND u.is_enabled = 1
				LIMIT 1",
				$user_table,
				$system_table,
				$slug
			),
			ARRAY_A
		);

		if ( ! $row ) {
			return '';
		}

		$defaults = json_decode( $row['default_attrs'] ?: '{}', true ) ?: array();
		$merged   = array_merge( $defaults, $attrs );
		unset( $merged['cssVars'], $merged['allowedPostTypes'], $merged['align'], $merged['className'], $merged['anchor'] );

		$html = MarkupInterpolator::interpolate( (string) $row['markup'], $merged );
		return BlockMarkup::sanitize_output( $html );
	}
}
