<?php
/**
 * Onboarding REST controller — GET / PATCH /blocksify/v1/dashboard/onboarding.
 *
 * Storage: user_meta `blocksify_dashboard_onboarding` (per-user, not
 * per-site — SPEC §5.4 / handoff §3.1). The two helper detection
 * endpoints (`/has-blocksify-block`, `/design-tokens-configured`) ship
 * in Phase 3 alongside the Welcome tab; this controller owns the meta
 * CRUD only.
 *
 * Auth: `read` capability (any logged-in user with dashboard access edits
 * their own meta). The meta is implicitly scoped to the current user so
 * cross-user writes aren't expressible.
 *
 * @package Blocksify
 */

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

/**
 * REST controller for per-user onboarding state.
 */
class Blocksify_Dashboard_Onboarding_Controller extends WP_REST_Controller {

	const META_KEY = 'blocksify_dashboard_onboarding';

	/**
	 * Wire namespace + base for register_routes().
	 */
	public function __construct() {
		$this->namespace = 'blocksify/v1';
		$this->rest_base = 'dashboard/onboarding';
	}

	/**
	 * Register the onboarding meta CRUD + the two helper detection
	 * endpoints consumed by the Welcome checklist (SPEC §8.1.3).
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'permission_check' ),
				),
				array(
					'methods'             => 'PATCH',
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'permission_check' ),
					'args'                => array(
						'completed' => array(
							'type'     => 'array',
							'required' => false,
							'items'    => array( 'type' => 'string' ),
						),
						'dismissed' => array(
							'type'     => 'boolean',
							'required' => false,
						),
					),
				),
			)
		);

		// Welcome checklist auto-detection helpers (SPEC §8.1.3). Gated
		// to the dashboard capability because they scan site-wide state
		// (post_content, Blocksify token options) and are only useful
		// inside the dashboard UI.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/has-blocksify-block',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'detect_blocksify_block' ),
					'permission_callback' => array( $this, 'dashboard_permission_check' ),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/design-tokens-configured',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'detect_design_tokens_configured' ),
					'permission_callback' => array( $this, 'dashboard_permission_check' ),
				),
			)
		);
	}

	/**
	 * Any logged-in user with read access edits their OWN onboarding meta.
	 * Cross-user writes aren't possible — meta is `get_current_user_id()`
	 * scoped.
	 *
	 * @return bool
	 */
	public function permission_check() {
		return is_user_logged_in() && current_user_can( 'read' );
	}

	/**
	 * Dashboard-scoped gate for the detection helpers. They query
	 * site-wide state (post_content scan, global styles lookup) so the
	 * higher cap is appropriate.
	 *
	 * @return bool
	 */
	public function dashboard_permission_check() {
		return current_user_can( Blocksify_Dashboard::get_capability() );
	}

	/**
	 * Detect whether any post / page contains a Blocksify block. SQL
	 * LIKE bounded to 1 hit so this stays cheap on large content
	 * libraries — we only care whether AT LEAST one block exists, not
	 * how many.
	 *
	 * @return WP_REST_Response { detected: bool }
	 */
	public function detect_blocksify_block() {
		global $wpdb;
		$needle  = '%<!-- wp:blocksify/%';
		$post_id = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$wpdb->prepare(
				"SELECT ID FROM {$wpdb->posts} WHERE post_status IN ('publish','draft','private') AND post_content LIKE %s LIMIT 1",
				$needle
			)
		);

		return rest_ensure_response( array( 'detected' => ! empty( $post_id ) ) );
	}

	/**
	 * Detect whether any Blocksify design tokens have been configured.
	 * Blocksify is a block plugin: tokens are authored in the Blocksify
	 * Settings sidebar panel and stored in Blocksify's own wp_options
	 * registry — NOT the theme's Site-Editor global styles. "Configured"
	 * means the user has added a custom color, a custom gradient, or
	 * touched a shadow preset (a default-preset patch or a custom preset).
	 *
	 * @return WP_REST_Response { configured: bool }
	 */
	public function detect_design_tokens_configured() {
		$configured = false;

		if ( class_exists( 'Blocksify_Settings' ) ) {
			$configured = ! empty( Blocksify_Settings::get_custom_colors() )
				|| ! empty( Blocksify_Settings::get_custom_gradients() );
		}

		if ( ! $configured && class_exists( 'Blocksify_Shadows' ) ) {
			$shadows    = Blocksify_Shadows::get_stored();
			$configured = ! empty( $shadows['defaultEdits'] ) || ! empty( $shadows['customs'] );
		}

		return rest_ensure_response( array( 'configured' => (bool) $configured ) );
	}

	/**
	 * GET handler. Returns the stored shape, defaulting to the empty state
	 * for a fresh user.
	 *
	 * @param WP_REST_Request $request Unused.
	 * @return WP_REST_Response
	 */
	public function get_item( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
		return rest_ensure_response( self::read_state( get_current_user_id() ) );
	}

	/**
	 * PATCH handler. Merges whitelisted keys into the stored shape — fields
	 * absent from the request are preserved. Unknown keys dropped.
	 *
	 * @param WP_REST_Request $request REST request.
	 * @return WP_REST_Response
	 */
	public function update_item( $request ) {
		$user_id = get_current_user_id();
		$body    = $request->get_json_params();
		if ( ! is_array( $body ) ) {
			$body = $request->get_params();
		}
		if ( ! is_array( $body ) ) {
			$body = array();
		}

		$state = self::read_state( $user_id );

		if ( array_key_exists( 'completed', $body ) ) {
			$state['completed'] = self::sanitize_completed( $body['completed'] );
		}
		if ( array_key_exists( 'dismissed', $body ) ) {
			$state['dismissed'] = (bool) $body['dismissed'];
		}

		update_user_meta( $user_id, self::META_KEY, $state );

		return rest_ensure_response( $state );
	}

	/**
	 * Read the stored shape with safe defaults. Used by both GET and the
	 * read-modify-write of PATCH so the two paths can't drift.
	 *
	 * @param int $user_id Target user.
	 * @return array { completed: string[], dismissed: bool }
	 */
	private static function read_state( $user_id ) {
		$raw = get_user_meta( $user_id, self::META_KEY, true );
		if ( ! is_array( $raw ) ) {
			$raw = array();
		}

		return array(
			'completed' => self::sanitize_completed( isset( $raw['completed'] ) ? $raw['completed'] : array() ),
			'dismissed' => ! empty( $raw['dismissed'] ),
		);
	}

	/**
	 * Coerce the `completed` array to a deduplicated list of slug-shaped
	 * strings. Anything that isn't a non-empty string is dropped — the
	 * client can re-add the right value on a follow-up request.
	 *
	 * @param mixed $value Raw value.
	 * @return string[]
	 */
	private static function sanitize_completed( $value ) {
		if ( ! is_array( $value ) ) {
			return array();
		}

		$out = array();
		foreach ( $value as $item ) {
			if ( ! is_string( $item ) ) {
				continue;
			}
			$clean = sanitize_key( $item );
			if ( '' === $clean ) {
				continue;
			}
			if ( ! in_array( $clean, $out, true ) ) {
				$out[] = $clean;
			}
		}
		return $out;
	}
}
