<?php
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- All table names are from $wpdb->prefix (trusted), not user input
defined( 'ABSPATH' ) || exit;

/**
 * Jalpi Page Builder Revision System.
 *
 * Stores page revisions in a dedicated table (wp_jalpi_revisions).
 * Auto-saves and named snapshots are stored separately; identical
 * consecutive saves are deduplicated via an MD5 content hash.
 *
 * Limits:
 *   - MAX_AUTO     auto-save revisions per page (oldest pruned automatically)
 *   - MAX_SNAPSHOT named snapshots per page (oldest pruned automatically)
 *
 * REST endpoints under jalpi-page-builder/v1:
 *   GET    /revisions/{post_id}                   – list (no data payload)
 *   POST   /revisions/{post_id}                   – create (auto-save or snapshot)
 *   GET    /revisions/{post_id}/{revision_id}     – single with data
 *   DELETE /revisions/{post_id}/{revision_id}     – delete one
 *   POST   /revisions/{post_id}/{revision_id}/restore – restore to live
 */
class Jalpi_Revisions {

	private static ?self $instance = null;

	const TABLE_NAME   = 'jalpi_revisions';
	const DB_VERSION   = '1.0';
	const NAMESPACE    = 'jalpi-page-builder/v1';
	const MAX_AUTO     = 50;
	const MAX_SNAPSHOT = 20;

	// =========================================================================
	// Bootstrap
	// =========================================================================

	public static function instance(): self {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	private function __construct() {
		add_action( 'rest_api_init', [ $this, 'register_routes' ] );
		$this->maybe_create_table();
	}

	// =========================================================================
	// Database
	// =========================================================================

	private function table(): string {
		global $wpdb;
		return esc_sql( $wpdb->prefix . self::TABLE_NAME );
	}

	private function maybe_create_table(): void {
		if ( get_option( 'jalpi_revisions_db_version' ) === self::DB_VERSION ) {
			return;
		}
		static::create_table();
	}

	public static function create_table(): void {
		global $wpdb;
		$table   = $wpdb->prefix . self::TABLE_NAME;
		$charset = $wpdb->get_charset_collate();

		$sql = "CREATE TABLE {$table} (
			id          bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
			post_id     bigint(20) UNSIGNED NOT NULL,
			user_id     bigint(20) UNSIGNED NOT NULL DEFAULT 0,
			label       varchar(200)        NOT NULL DEFAULT '',
			data_hash   char(32)            NOT NULL DEFAULT '',
			data        longtext            NOT NULL,
			is_snapshot tinyint(1)          NOT NULL DEFAULT 0,
			created_at  datetime            NOT NULL,
			PRIMARY KEY (id),
			KEY post_created (post_id, is_snapshot, created_at)
		) {$charset};";

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta( $sql );

		update_option( 'jalpi_revisions_db_version', self::DB_VERSION );
	}

	// =========================================================================
	// Routes
	// =========================================================================

	public function register_routes(): void {
		$post_arg = [
			'post_id' => [
				'required'          => true,
				'type'              => 'integer',
				'minimum'           => 1,
				'sanitize_callback' => 'absint',
			],
		];

		$rev_arg = array_merge( $post_arg, [
			'revision_id' => [
				'required'          => true,
				'type'              => 'integer',
				'minimum'           => 1,
				'sanitize_callback' => 'absint',
			],
		] );

		// List + Create
		register_rest_route( self::NAMESPACE, '/revisions/(?P<post_id>\d+)', [
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'list_revisions' ],
				'permission_callback' => [ $this, 'read_permission' ],
				'args'                => $post_arg,
			],
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'create_revision' ],
				'permission_callback' => [ $this, 'write_permission' ],
				'args'                => array_merge( $post_arg, [
					'data'        => [ 'required' => true ],
					'label'       => [
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
						'default'           => '',
					],
					'is_snapshot' => [
						'type'    => 'boolean',
						'default' => false,
					],
				] ),
			],
		] );

		// Restore (before /{revision_id} so static segment wins)
		register_rest_route( self::NAMESPACE, '/revisions/(?P<post_id>\d+)/(?P<revision_id>\d+)/restore', [
			'methods'             => WP_REST_Server::CREATABLE,
			'callback'            => [ $this, 'restore_revision' ],
			'permission_callback' => [ $this, 'write_permission' ],
			'args'                => $rev_arg,
		] );

		// Get + Delete single
		register_rest_route( self::NAMESPACE, '/revisions/(?P<post_id>\d+)/(?P<revision_id>\d+)', [
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_revision' ],
				'permission_callback' => [ $this, 'read_permission' ],
				'args'                => $rev_arg,
			],
			[
				'methods'             => WP_REST_Server::DELETABLE,
				'callback'            => [ $this, 'delete_revision' ],
				'permission_callback' => [ $this, 'write_permission' ],
				'args'                => $rev_arg,
			],
		] );
	}

	// =========================================================================
	// Permissions
	// =========================================================================

	public function read_permission( WP_REST_Request $request ): bool {
		return current_user_can( 'edit_post', absint( $request->get_param( 'post_id' ) ) );
	}

	public function write_permission( WP_REST_Request $request ): bool {
		return current_user_can( 'edit_post', absint( $request->get_param( 'post_id' ) ) );
	}

	// =========================================================================
	// Callbacks
	// =========================================================================

	public function list_revisions( WP_REST_Request $request ): WP_REST_Response {
		global $wpdb;
		$post_id = absint( $request->get_param( 'post_id' ) );
		$table   = esc_sql( $this->table() );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT id, post_id, user_id, label, is_snapshot, created_at
				 FROM {$table}
				 WHERE post_id = %d
				 ORDER BY created_at DESC
				 LIMIT 200",
				$post_id
			),
			ARRAY_A
		);

		$user_cache = [];
		$items      = array_map( function ( $row ) use ( &$user_cache ) {
			$uid = (int) $row['user_id'];
			if ( ! isset( $user_cache[ $uid ] ) ) {
				$user_cache[ $uid ] = get_userdata( $uid );
			}
			$user = $user_cache[ $uid ];
			return [
				'id'          => (int) $row['id'],
				'post_id'     => (int) $row['post_id'],
				'user_id'     => $uid,
				'user_name'   => $user ? $user->display_name : __( 'Unknown', 'jalpi-page-builder' ),
				'label'       => $row['label'],
				'is_snapshot' => (bool) $row['is_snapshot'],
				'created_at'  => $row['created_at'],
			];
		}, $rows ?? [] );

		return rest_ensure_response( $items );
	}

	public function get_revision( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		global $wpdb;
		$revision_id = absint( $request->get_param( 'revision_id' ) );
		$post_id     = absint( $request->get_param( 'post_id' ) );
		$table       = esc_sql( $this->table() );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter
		$row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$table} WHERE id = %d AND post_id = %d",
				$revision_id,
				$post_id
			),
			ARRAY_A
		);

		if ( ! $row ) {
			return new WP_Error( 'not_found', __( 'Revision not found.', 'jalpi-page-builder' ), [ 'status' => 404 ] );
		}

		$uid  = (int) $row['user_id'];
		$user = get_userdata( $uid );

		return rest_ensure_response( [
			'id'          => (int) $row['id'],
			'post_id'     => (int) $row['post_id'],
			'user_id'     => $uid,
			'user_name'   => $user ? $user->display_name : __( 'Unknown', 'jalpi-page-builder' ),
			'label'       => $row['label'],
			'is_snapshot' => (bool) $row['is_snapshot'],
			'created_at'  => $row['created_at'],
			'data'        => json_decode( $row['data'], true ),
		] );
	}

	public function create_revision( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		global $wpdb;

		$post_id     = absint( $request->get_param( 'post_id' ) );
		$raw_data    = $request->get_param( 'data' );
		$label       = sanitize_text_field( $request->get_param( 'label' ) );
		$is_snapshot = (bool) $request->get_param( 'is_snapshot' );

		if ( ! get_post( $post_id ) ) {
			return new WP_Error( 'not_found', __( 'Post not found.', 'jalpi-page-builder' ), [ 'status' => 404 ] );
		}

		$json      = wp_json_encode( $raw_data );
		$data_hash = md5( $json );
		$table     = esc_sql( $this->table() );

		// Skip if content unchanged since last revision
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter
		$latest_hash = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT data_hash FROM {$table}
				 WHERE post_id = %d
				 ORDER BY created_at DESC
				 LIMIT 1",
				$post_id
			)
		);

		if ( $data_hash === $latest_hash ) {
			return rest_ensure_response( [ 'skipped' => true, 'reason' => 'no_change' ] );
		}

		$auto_label = $is_snapshot
			? ( $label ?: __( 'Manual Snapshot', 'jalpi-page-builder' ) )
			: __( 'Auto-save', 'jalpi-page-builder' );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$inserted = $wpdb->insert(
			$table,
			[
				'post_id'     => $post_id,
				'user_id'     => get_current_user_id(),
				'label'       => $auto_label,
				'data_hash'   => $data_hash,
				'data'        => $json,
				'is_snapshot' => $is_snapshot ? 1 : 0,
				'created_at'  => current_time( 'mysql' ),
			],
			[ '%d', '%d', '%s', '%s', '%s', '%d', '%s' ]
		);

		if ( ! $inserted ) {
			return new WP_Error( 'insert_failed', __( 'Failed to create revision.', 'jalpi-page-builder' ), [ 'status' => 500 ] );
		}

		$new_id = (int) $wpdb->insert_id;

		$is_snapshot ? $this->prune_snapshots( $post_id ) : $this->prune_auto_saves( $post_id );

		return rest_ensure_response( [ 'id' => $new_id, 'created' => true ] );
	}

	public function restore_revision( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		global $wpdb;

		$post_id     = absint( $request->get_param( 'post_id' ) );
		$revision_id = absint( $request->get_param( 'revision_id' ) );
		$table       = esc_sql( $this->table() );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter
		$row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT data FROM {$table} WHERE id = %d AND post_id = %d",
				$revision_id,
				$post_id
			),
			ARRAY_A
		);

		if ( ! $row ) {
			return new WP_Error( 'not_found', __( 'Revision not found.', 'jalpi-page-builder' ), [ 'status' => 404 ] );
		}

		$data  = json_decode( $row['data'], true );
		$clean = Jalpi_Sanitizer::sanitize_page_data( $data );

		// Persist restored data as the live page state (write to wp_jalpi_pages table).
		Jalpi_API::instance()->write_page_data( $post_id, $clean );

		// Invalidate the render cache so the restored version is shown immediately.
		delete_transient( 'jalpi_render_' . $post_id . '_' . JALPI_VERSION );

		// Record a revision for this restore action
		$this->insert_direct(
			$post_id,
			$clean,
			/* translators: %d: restored revision ID */
			sprintf( __( 'Restored from #%d', 'jalpi-page-builder' ), $revision_id ),
			false
		);

		return rest_ensure_response( [ 'restored' => true, 'data' => $clean ] );
	}

	public function delete_revision( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		global $wpdb;

		$revision_id = absint( $request->get_param( 'revision_id' ) );
		$post_id     = absint( $request->get_param( 'post_id' ) );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$deleted = $wpdb->delete(
			$this->table(),
			[ 'id' => $revision_id, 'post_id' => $post_id ],
			[ '%d', '%d' ]
		);

		if ( ! $deleted ) {
			return new WP_Error( 'not_found', __( 'Revision not found.', 'jalpi-page-builder' ), [ 'status' => 404 ] );
		}

		return rest_ensure_response( [ 'deleted' => true, 'id' => $revision_id ] );
	}

	// =========================================================================
	// Helpers
	// =========================================================================

	private function insert_direct( int $post_id, array $data, string $label, bool $is_snapshot ): int {
		global $wpdb;
		$json = wp_json_encode( $data );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$wpdb->insert(
			$this->table(),
			[
				'post_id'     => $post_id,
				'user_id'     => get_current_user_id(),
				'label'       => $label,
				'data_hash'   => md5( $json ),
				'data'        => $json,
				'is_snapshot' => $is_snapshot ? 1 : 0,
				'created_at'  => current_time( 'mysql' ),
			],
			[ '%d', '%d', '%s', '%s', '%s', '%d', '%s' ]
		);
		return (int) $wpdb->insert_id;
	}

	private function prune_auto_saves( int $post_id ): void {
		global $wpdb;
		$table = esc_sql( $this->table() );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter
		$keep = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT id FROM {$table}
				 WHERE post_id = %d AND is_snapshot = 0
				 ORDER BY created_at DESC
				 LIMIT %d",
				$post_id,
				self::MAX_AUTO
			)
		);

		if ( count( $keep ) < self::MAX_AUTO ) {
			return;
		}

		$ph = implode( ',', array_fill( 0, count( $keep ), '%d' ) );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQLPlaceholders, PluginCheck.Security.DirectDB.UnescapedDBParameter
		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$table}
				 WHERE post_id = %d AND is_snapshot = 0 AND id NOT IN ({$ph})",
				array_merge( [ $post_id ], $keep )
			)
		);
	}

	private function prune_snapshots( int $post_id ): void {
		global $wpdb;
		$table = esc_sql( $this->table() );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter
		$keep = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT id FROM {$table}
				 WHERE post_id = %d AND is_snapshot = 1
				 ORDER BY created_at DESC
				 LIMIT %d",
				$post_id,
				self::MAX_SNAPSHOT
			)
		);

		if ( count( $keep ) < self::MAX_SNAPSHOT ) {
			return;
		}

		$ph = implode( ',', array_fill( 0, count( $keep ), '%d' ) );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQLPlaceholders, PluginCheck.Security.DirectDB.UnescapedDBParameter
		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$table}
				 WHERE post_id = %d AND is_snapshot = 1 AND id NOT IN ({$ph})",
				array_merge( [ $post_id ], $keep )
			)
		);
	}
}
