<?php
/**
 * Snapshot store. Tools call PCD_PC_Snapshot::capture( $kind, $key, $payload )
 * before they mutate something. Restore puts it back. Powers undo.
 *
 * For v1, snapshots are rows in a custom table. Files are stored as full
 * content blobs (compressed). Good enough up to the multi-MB range; we'll
 * move to disk if anyone complains.
 */

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

class PCD_PC_Snapshot {

	const TABLE_VERSION_OPT = 'pcd_pc_snapshot_db_version';
	const TABLE_VERSION     = '1';

	public static function init(): void {
		self::maybe_install_table();
	}

	private static function table_name(): string {
		global $wpdb;
		return $wpdb->prefix . 'pcd_pc_snapshots';
	}

	private static function maybe_install_table(): void {
		if ( get_option( self::TABLE_VERSION_OPT ) === self::TABLE_VERSION ) {
			return;
		}

		global $wpdb;
		$table   = self::table_name();
		$charset = $wpdb->get_charset_collate();

		$sql = "CREATE TABLE {$table} (
			id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
			created_at DATETIME NOT NULL,
			user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			kind VARCHAR(32) NOT NULL,
			ref_key VARCHAR(191) NOT NULL,
			label VARCHAR(255) NULL,
			payload LONGBLOB NULL,
			PRIMARY KEY (id),
			KEY kind_ref (kind, ref_key),
			KEY created_at (created_at)
		) {$charset};";

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta( $sql );
		update_option( self::TABLE_VERSION_OPT, self::TABLE_VERSION );
	}

	/**
	 * Capture a snapshot before mutation.
	 *
	 * @param string $kind    'post' | 'option' | 'file' | 'meta'
	 * @param string $ref_key Identifier (post id, option name, file path, etc.)
	 * @param mixed  $payload The pre-mutation value.
	 * @param string $label   Human-readable label.
	 * @return int Snapshot ID.
	 */
	public static function capture( string $kind, string $ref_key, $payload, string $label = '' ): int {
		global $wpdb;

		$serialized = is_string( $payload ) ? $payload : wp_json_encode( $payload );
		$compressed = function_exists( 'gzcompress' ) ? gzcompress( (string) $serialized, 6 ) : (string) $serialized;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- writing to the plugin's own snapshot table; no caching applicable.
		$wpdb->insert(
			self::table_name(),
			[
				'created_at' => current_time( 'mysql', true ),
				'user_id'    => get_current_user_id(),
				'kind'       => $kind,
				'ref_key'    => $ref_key,
				'label'      => $label,
				'payload'    => $compressed,
			]
		);

		return (int) $wpdb->insert_id;
	}

	public static function get( int $id ): ?array {
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL -- internal table name from $wpdb->prefix; all user values use prepare().
		$row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM ' . self::table_name() . ' WHERE id = %d', $id ), ARRAY_A );
		if ( ! $row ) {
			return null;
		}
		$row['payload'] = function_exists( 'gzuncompress' ) ? @gzuncompress( $row['payload'] ) : $row['payload']; // phpcs:ignore Generic.PHP.NoSilencedErrors
		if ( false === $row['payload'] ) {
			$row['payload'] = '';
		}
		return $row;
	}

	public static function recent( int $limit = 100 ): array {
		global $wpdb;
		$limit = max( 1, min( 500, $limit ) );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL -- internal table name from $wpdb->prefix; all user values use prepare().
		return (array) $wpdb->get_results( $wpdb->prepare( 'SELECT id, created_at, user_id, kind, ref_key, label FROM ' . self::table_name() . ' ORDER BY id DESC LIMIT %d', $limit ), ARRAY_A );
	}

	/**
	 * Restore a snapshot. Returns true on success, WP_Error on failure.
	 */
	public static function restore( int $id ) {
		$snap = self::get( $id );
		if ( ! $snap ) {
			return new WP_Error( 'not_found', 'Snapshot not found.', [ 'status' => 404 ] );
		}

		$kind    = $snap['kind'];
		$ref     = $snap['ref_key'];
		$payload = $snap['payload'];

		switch ( $kind ) {
			case 'post':
				$data = json_decode( $payload, true );
				if ( ! is_array( $data ) || empty( $data['ID'] ) ) {
					return new WP_Error( 'bad_payload', 'Snapshot payload invalid.' );
				}
				$result = wp_update_post( $data, true );
				return is_wp_error( $result ) ? $result : [ 'restored' => 'post', 'id' => (int) $data['ID'] ];

			case 'option':
				$data = json_decode( $payload, true );
				update_option( $ref, $data );
				return [ 'restored' => 'option', 'key' => $ref ];

			case 'meta':
				$data = json_decode( $payload, true );
				if ( ! is_array( $data ) || empty( $data['post_id'] ) || empty( $data['key'] ) ) {
					return new WP_Error( 'bad_payload', 'Snapshot payload invalid.' );
				}
				update_post_meta( (int) $data['post_id'], $data['key'], $data['value'] ?? '' );
				return [ 'restored' => 'meta', 'post_id' => (int) $data['post_id'], 'key' => $data['key'] ];

			case 'file':
				// File writes live in PCD_PC_File_Ops, which is excluded from the
				// WordPress.org (Lite) build. Lite never captures file snapshots,
				// so this path is unreachable there.
				if ( ! class_exists( 'PCD_PC_File_Ops' ) ) {
					return new WP_Error( 'file_restore_unavailable', 'File restore is not available in this build.' );
				}
				return PCD_PC_File_Ops::restore_file( $ref, $payload );
		}

		return new WP_Error( 'unknown_kind', "Cannot restore kind: {$kind}" );
	}
}
