<?php
/**
 * User content blocks (shortcodes) REST controller.
 *
 * @package JOCBM
 *
 * Copyright (C) 2026 JayOnCode
 * License: GPL-2.0-or-later
 */

namespace JOCBM\Rest;

use JOCBM\Database\Schema;
use JOCBM\Services\AttrSanitizer;
use JOCBM\Services\BlockCategories;
use JOCBM\Services\CssVars;
use JOCBM\Services\CssWriter;
use JOCBM\Services\TemplateLibrary;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

defined( 'ABSPATH' ) || exit;

/**
 * Class UserContentsController
 */
final class UserContentsController {

	use RestPermissions;

	/**
	 * Register routes.
	 */
	public function register_routes(): void {
		register_rest_route(
			RestApi::NAMESPACE,
			'/user-contents',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'index' ),
					'permission_callback' => array( $this, 'can_manage' ),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create' ),
					'permission_callback' => array( $this, 'can_manage' ),
				),
			)
		);

		register_rest_route(
			RestApi::NAMESPACE,
			'/user-contents/(?P<id>\d+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'show' ),
					'permission_callback' => array( $this, 'can_manage' ),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update' ),
					'permission_callback' => array( $this, 'can_manage' ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete' ),
					'permission_callback' => array( $this, 'can_manage' ),
				),
			)
		);
	}

	/**
	 * List content blocks.
	 */
	public function index(): WP_REST_Response {
		global $wpdb;

		$table = Schema::table( 'user_contents' );
		$ub    = Schema::table( 'user_blocks' );
		$sys   = Schema::table( 'system_blocks' );
		$cats  = Schema::table( 'categories' );

		$rows = $wpdb->get_results(
			// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- %i table identifiers are not counted by PHPCS.
			$wpdb->prepare(
				"SELECT c.*, u.name AS user_block_name, u.slug AS user_block_slug, u.icon AS user_block_icon,
				u.default_attrs AS user_block_default_attrs,
				u.template_hash AS user_template_hash,
				u.system_block_id AS system_block_id,
				COALESCE(NULLIF(u.markup, ''), s.markup) AS markup,
				COALESCE(NULLIF(u.field_schema, ''), s.field_schema) AS field_schema,
				s.field_schema AS library_field_schema,
				s.template_hash AS library_template_hash,
				COALESCE(NULLIF(u.style_schema, ''), s.style_schema) AS style_schema,
				cat.slug AS category_slug,
				cat.name AS category_name
			FROM %i c
			INNER JOIN %i u ON u.id = c.user_block_id
			INNER JOIN %i s ON s.id = u.system_block_id
			LEFT JOIN %i cat ON cat.id = s.category_id
			ORDER BY c.updated_at DESC",
				$table,
				$ub,
				$sys,
				$cats
			),
			ARRAY_A
		);

		$items = array_map( array( $this, 'format' ), $rows ?: array() );

		return new WP_REST_Response( array( 'items' => $items ), 200 );
	}

	/**
	 * Get one content block.
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function show( WP_REST_Request $request ) {
		$row = $this->find( (int) $request['id'] );

		if ( ! $row ) {
			return new WP_Error( 'jocbm_not_found', __( 'Content block not found.', 'joc-blockora-morph' ), array( 'status' => 404 ) );
		}

		return new WP_REST_Response( $this->format( $row ), 200 );
	}

	/**
	 * Create content block from an editor block.
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function create( WP_REST_Request $request ) {
		global $wpdb;

		$user_block_id = (int) $request->get_param( 'userBlockId' );
		$name          = sanitize_text_field( (string) $request->get_param( 'name' ) );
		$attrs         = $this->json_param( $request, 'contentAttrs', array() );
		$css_vars      = CssVars::normalize( $this->json_param( $request, 'cssVars', array() ) );

		$ub_table = Schema::table( 'user_blocks' );
		$ub       = $wpdb->get_row(
			$wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $ub_table, $user_block_id ),
			ARRAY_A
		);

		if ( ! $ub ) {
			return new WP_Error(
				'jocbm_no_editor_block',
				__( 'Create an Editor Block first before adding Content Blocks.', 'joc-blockora-morph' ),
				array( 'status' => 400 )
			);
		}

		if ( empty( $ub['is_enabled'] ) ) {
			return new WP_Error(
				'jocbm_block_hidden',
				__( 'Hidden Gutenberg blocks cannot be used for shortcodes. Enable the block first.', 'joc-blockora-morph' ),
				array( 'status' => 400 )
			);
		}

		if ( ! $name ) {
			$name = $ub['name'] . ' Content';
		}

		if ( empty( $attrs ) ) {
			$attrs = json_decode( $ub['default_attrs'] ?: '{}', true ) ?: array();
		}

		$field_schema = $this->field_schema_for_user_block( $ub );
		$attrs        = $this->sanitize_attrs( (array) $attrs, $field_schema );
		$attrs        = $this->fill_missing_attrs( $attrs, $ub, $field_schema );
		$attrs        = $this->heal_structured_attrs( $attrs, $ub, $field_schema );

		if ( ! CssVars::has_any( $css_vars ) ) {
			$css_vars = CssVars::normalize( json_decode( $ub['css_vars'] ?: '{}', true ) ?: array() );
		}

		$slug = sanitize_title( $name );
		$key  = 'bm_' . wp_generate_password( 8, false, false );

		$wpdb->insert(
			Schema::table( 'user_contents' ),
			array(
				'user_block_id' => $user_block_id,
				'user_id'       => get_current_user_id(),
				'name'          => $name,
				'slug'          => $slug . '-' . substr( $key, -4 ),
				'shortcode_key' => $key,
				'content_attrs' => wp_json_encode( $attrs ),
				'css_vars'      => wp_json_encode( $css_vars ),
				'is_enabled'    => 1,
			),
			array( '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%d' )
		);

		$id = (int) $wpdb->insert_id;
		CssWriter::write_content_block( $key, $ub['slug'], $css_vars, $attrs );

		return new WP_REST_Response( $this->format( $this->find( $id ) ), 201 );
	}

	/**
	 * Update content block.
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function update( WP_REST_Request $request ) {
		global $wpdb;

		$id  = (int) $request['id'];
		$row = $this->find( $id );

		if ( ! $row ) {
			return new WP_Error( 'jocbm_not_found', __( 'Content block not found.', 'joc-blockora-morph' ), array( 'status' => 404 ) );
		}

		$data = array();
		$fmt  = array();

		if ( null !== $request->get_param( 'name' ) ) {
			$data['name'] = sanitize_text_field( (string) $request->get_param( 'name' ) );
			$fmt[]        = '%s';
		}

		$attrs = $this->json_param( $request, 'contentAttrs', null );
		if ( null !== $attrs ) {
			$field_schema          = json_decode( $row['field_schema'] ?? '[]', true ) ?: array();
			$attrs                 = $this->sanitize_attrs( (array) $attrs, $field_schema );
			$data['content_attrs'] = wp_json_encode( $attrs );
			$fmt[]                 = '%s';
		}

		$css_vars = $this->json_param( $request, 'cssVars', null );
		if ( null !== $css_vars ) {
			$css_vars         = CssVars::normalize( $css_vars );
			$data['css_vars'] = wp_json_encode( $css_vars );
			$fmt[]            = '%s';
		}

		if ( null !== $request->get_param( 'isEnabled' ) ) {
			$data['is_enabled'] = (int) (bool) $request->get_param( 'isEnabled' );
			$fmt[]              = '%d';
		}

		if ( $data ) {
			$wpdb->update( Schema::table( 'user_contents' ), $data, array( 'id' => $id ), $fmt, array( '%d' ) );
		}

		if ( null !== $attrs || null !== $css_vars ) {
			$fresh = $this->find( $id );
			$write_vars = null !== $css_vars
				? $css_vars
				: CssVars::normalize( json_decode( $fresh['css_vars'] ?: '{}', true ) ?: array() );
			$write_attrs = null !== $attrs
				? $attrs
				: ( json_decode( $fresh['content_attrs'] ?: '{}', true ) ?: array() );
			CssWriter::write_content_block(
				$row['shortcode_key'],
				$row['user_block_slug'],
				$write_vars,
				is_array( $write_attrs ) ? $write_attrs : array()
			);
		}

		return new WP_REST_Response( $this->format( $this->find( $id ) ), 200 );
	}

	/**
	 * Delete content block.
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function delete( WP_REST_Request $request ) {
		global $wpdb;

		$id  = (int) $request['id'];
		$row = $this->find( $id );

		if ( ! $row ) {
			return new WP_Error( 'jocbm_not_found', __( 'Content block not found.', 'joc-blockora-morph' ), array( 'status' => 404 ) );
		}

		CssWriter::delete_content_block( $row['shortcode_key'] );
		$wpdb->delete( Schema::table( 'user_contents' ), array( 'id' => $id ), array( '%d' ) );

		return new WP_REST_Response( array( 'success' => true ), 200 );
	}

	/**
	 * Find joined row.
	 *
	 * @param int $id ID.
	 * @return array<string, mixed>|null
	 */
	private function find( int $id ): ?array {
		global $wpdb;

		$table = Schema::table( 'user_contents' );
		$ub    = Schema::table( 'user_blocks' );
		$sys   = Schema::table( 'system_blocks' );
		$cats  = Schema::table( 'categories' );

		$row = $wpdb->get_row(
			// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- %i table identifiers are not counted by PHPCS.
			$wpdb->prepare(
				"SELECT c.*, u.name AS user_block_name, u.slug AS user_block_slug, u.icon AS user_block_icon,
					u.default_attrs AS user_block_default_attrs,
					u.template_hash AS user_template_hash,
					u.system_block_id AS system_block_id,
					COALESCE(NULLIF(u.markup, ''), s.markup) AS markup,
					COALESCE(NULLIF(u.field_schema, ''), s.field_schema) AS field_schema,
					s.field_schema AS library_field_schema,
					s.template_hash AS library_template_hash,
					COALESCE(NULLIF(u.style_schema, ''), s.style_schema) AS style_schema,
					cat.slug AS category_slug,
					cat.name AS category_name
				FROM %i c
				INNER JOIN %i u ON u.id = c.user_block_id
				INNER JOIN %i s ON s.id = u.system_block_id
				LEFT JOIN %i cat ON cat.id = s.category_id
				WHERE c.id = %d",
				$table,
				$ub,
				$sys,
				$cats,
				$id
			),
			ARRAY_A
		);

		return $row ?: null;
	}

	/**
	 * Format row.
	 *
	 * @param array<string, mixed> $row Row.
	 * @return array<string, mixed>
	 */
	private function format( array $row ): array {
		$key          = $row['shortcode_key'];
		$field_schema = TemplateLibrary::decode_field_schema( $row['field_schema'] ?? '[]' );
		$library      = TemplateLibrary::decode_field_schema( $row['library_field_schema'] ?? '[]' );
		$field_schema = TemplateLibrary::apply_library_field_types( $field_schema, $library );
		$attrs        = json_decode( $row['content_attrs'] ?: '{}', true ) ?: array();
		$attrs        = $this->align_attrs_to_schema( (array) $attrs, $field_schema );

		$ub_defaults = array(
			'default_attrs' => $row['user_block_default_attrs'] ?? '{}',
		);
		$attrs    = $this->fill_missing_attrs( $attrs, $ub_defaults, $field_schema );
		$attrs    = $this->heal_structured_attrs( $attrs, $ub_defaults, $field_schema );
		$defaults = json_decode( $row['user_block_default_attrs'] ?? '{}', true ) ?: array();

		$user_hash = (string) ( $row['user_template_hash'] ?? '' );
		$lib_hash  = (string) ( $row['library_template_hash'] ?? '' );
		$update    = ( '' !== $lib_hash && $user_hash !== $lib_hash );

		$system_id = (int) ( $row['system_block_id'] ?? 0 );
		$cat_slugs = $system_id ? BlockCategories::slugs_for_block( $system_id ) : array();
		if ( ! $cat_slugs ) {
			$primary = sanitize_key( (string) ( $row['category_slug'] ?? '' ) );
			$cat_slugs = $primary ? array( $primary ) : array();
		}

		return array(
			'id'              => (int) $row['id'],
			'userBlockId'     => (int) $row['user_block_id'],
			'userBlockName'   => $row['user_block_name'] ?? '',
			'userBlockSlug'   => $row['user_block_slug'] ?? '',
			'icon'            => ! empty( $row['user_block_icon'] ) ? $row['user_block_icon'] : 'screenoptions',
			'name'            => $row['name'],
			'slug'            => $row['slug'],
			'categorySlug'    => $row['category_slug'] ?? '',
			'categoryName'    => $row['category_name'] ?? '',
			'categorySlugs'   => $cat_slugs,
			'shortcodeKey'    => $key,
			'shortcode'       => sprintf( '[%s id="%s"]', JOCBM_SHORTCODE, $key ),
			'contentAttrs'    => $attrs,
			'defaultAttrs'    => is_array( $defaults ) ? $defaults : array(),
			'cssVars'         => CssVars::normalize( json_decode( $row['css_vars'] ?: '{}', true ) ?: array() ),
			'markup'          => $row['markup'] ?? '',
			'fieldSchema'     => $field_schema,
			'styleSchema'     => json_decode( $row['style_schema'] ?? '[]', true ) ?: array(),
			'templateHash'    => $user_hash,
			'libraryHash'     => $lib_hash,
			'updateAvailable' => $update,
			'isEnabled'       => (bool) $row['is_enabled'],
			'createdAt'       => $row['created_at'],
			'updatedAt'       => $row['updated_at'],
		);
	}

	/**
	 * Field schema for a user block row (falls back to system template).
	 *
	 * @param array<string, mixed> $ub User block row.
	 * @return array<int, array<string, mixed>>
	 */
	private function field_schema_for_user_block( array $ub ): array {
		global $wpdb;

		$schema = json_decode( $ub['field_schema'] ?? '', true );
		if ( is_array( $schema ) && $schema ) {
			return $schema;
		}

		$sys = Schema::table( 'system_blocks' );
		$row = $wpdb->get_row(
			$wpdb->prepare(
				'SELECT field_schema FROM %i WHERE id = %d',
				$sys,
				(int) ( $ub['system_block_id'] ?? 0 )
			),
			ARRAY_A
		);

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

	/**
	 * Fill any schema keys missing (or corrupted) from attrs using parent defaults.
	 *
	 * @param array<string, mixed>             $attrs         Attrs.
	 * @param array<string, mixed>             $ub            User block row.
	 * @param array<int, array<string, mixed>> $field_schema  Schema.
	 * @return array<string, mixed>
	 */
	private function fill_missing_attrs( array $attrs, array $ub, array $field_schema ): array {
		$defaults = json_decode( $ub['default_attrs'] ?: '{}', true ) ?: array();
		$defaults = $this->align_attrs_to_schema( (array) $defaults, $field_schema );

		foreach ( $field_schema as $field ) {
			$key = isset( $field['key'] ) ? (string) $field['key'] : '';
			if ( '' === $key || ! array_key_exists( $key, $defaults ) ) {
				continue;
			}
			if ( ! array_key_exists( $key, $attrs ) || $this->is_corrupted_attr_value( $attrs[ $key ], $field ) ) {
				$attrs[ $key ] = $defaults[ $key ];
			}
		}

		return $attrs;
	}

	/**
	 * Detect values destroyed by the old string-cast sanitizer (e.g. "Array").
	 *
	 * @param mixed                $value Value.
	 * @param array<string, mixed> $field Field schema entry.
	 */
	private function is_corrupted_attr_value( $value, array $field ): bool {
		$type = isset( $field['type'] ) ? (string) $field['type'] : 'text';

		if ( in_array( $type, array( 'repeater', 'elementsOrder', 'media', 'image', 'link' ), true ) ) {
			return ! is_array( $value );
		}
		// Legacy bug: (string) array → "Array".
		return is_string( $value ) && 'Array' === $value;
	}

	/**
	 * Restore structured attrs (visibility maps, etc.) wiped by the legacy sanitizer.
	 *
	 * @param array<string, mixed>             $attrs         Attrs.
	 * @param array<string, mixed>             $ub            User block row-like with default_attrs.
	 * @param array<int, array<string, mixed>> $field_schema  Schema.
	 * @return array<string, mixed>
	 */
	private function heal_structured_attrs( array $attrs, array $ub, array $field_schema ): array {
		$defaults = json_decode( $ub['default_attrs'] ?: '{}', true ) ?: array();
		$defaults = $this->align_attrs_to_schema( (array) $defaults, $field_schema );

		foreach ( array( 'elementsVisibility', 'buttons', 'logos', 'image', 'elementsOrder' ) as $key ) {
			if ( ! array_key_exists( $key, $attrs ) ) {
				continue;
			}
			$bad = ( is_string( $attrs[ $key ] ) && 'Array' === $attrs[ $key ] )
				|| ( in_array( $key, array( 'buttons', 'logos', 'image', 'elementsOrder', 'elementsVisibility' ), true ) && ! is_array( $attrs[ $key ] ) );
			if ( ! $bad ) {
				continue;
			}
			if ( array_key_exists( $key, $defaults ) ) {
				$attrs[ $key ] = $defaults[ $key ];
			} elseif ( 'elementsVisibility' === $key ) {
				$attrs[ $key ] = array();
			} else {
				unset( $attrs[ $key ] );
			}
		}

		return $attrs;
	}

	/**
	 * Map attr keys onto field_schema casing (repairs sanitize_key lowercasing).
	 *
	 * @param array<string, mixed>             $attrs        Attrs.
	 * @param array<int, array<string, mixed>> $field_schema Schema.
	 * @return array<string, mixed>
	 */
	private function align_attrs_to_schema( array $attrs, array $field_schema ): array {
		return AttrSanitizer::align_attrs_to_schema( $attrs, $field_schema );
	}

	/**
	 * Sanitize content attribute map.
	 *
	 * @param array<string, mixed>                  $attrs        Attrs.
	 * @param array<int, array<string, mixed>>|null $field_schema Optional schema.
	 * @return array<string, mixed>
	 */
	private function sanitize_attrs( array $attrs, ?array $field_schema = null ): array {
		return AttrSanitizer::sanitize( $attrs, $field_schema );
	}

}
