<?php
/**
 * Automatic redirect backups. A snapshot is taken before every bulk
 * operation (imports, CSV import, bulk delete, restore) and before plugin
 * updates — "an update wiped my redirects" must be impossible here.
 *
 * @package RedirectZen
 */

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

class RDZ_Backups {

	const KEEP = 5;

	/**
	 * Hooks.
	 */
	public static function init() {
		add_action( 'upgrader_process_complete', array( __CLASS__, 'backup_before_update' ), 10, 2 );
	}

	/**
	 * @return string Fully-prefixed backups table name.
	 */
	public static function table() {
		global $wpdb;

		return $wpdb->prefix . 'rdz_backups';
	}

	/**
	 * Snapshots the redirects table.
	 *
	 * @param string $label Human-readable reason ("Before import from Redirection").
	 * @return int|false Backup ID, or false when there is nothing to back up.
	 */
	public static function snapshot( $label ) {
		global $wpdb;

		$redirects = $wpdb->get_results( 'SELECT * FROM ' . RDZ_Redirects::table(), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		if ( empty( $redirects ) ) {
			return false;
		}

		$wpdb->insert(
			self::table(),
			array(
				'label'      => substr( sanitize_text_field( $label ), 0, 190 ),
				'rows_count' => count( $redirects ),
				'data'       => wp_json_encode( $redirects ),
				'created_at' => current_time( 'mysql', true ),
			),
			array( '%s', '%d', '%s', '%s' )
		);

		$id = (int) $wpdb->insert_id;
		self::trim();

		return $id;
	}

	/**
	 * Restores a snapshot, replacing all current redirects. The current
	 * state is snapshotted first so a restore is itself restorable.
	 *
	 * @param int $id Backup ID.
	 * @return int|WP_Error Rules restored.
	 */
	public static function restore( $id ) {
		global $wpdb;

		$backup = self::get( $id );
		if ( ! $backup ) {
			return new WP_Error( 'rdz_no_backup', __( 'Backup not found.', 'redirectzen-301-redirect-manager' ) );
		}

		$rows = json_decode( $backup['data'], true );
		if ( ! is_array( $rows ) ) {
			return new WP_Error( 'rdz_bad_backup', __( 'This backup could not be read.', 'redirectzen-301-redirect-manager' ) );
		}

		self::snapshot( __( 'Before restore', 'redirectzen-301-redirect-manager' ) );

		$table = RDZ_Redirects::table();
		$wpdb->query( "DELETE FROM {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		$restored = 0;
		foreach ( $rows as $row ) {
			unset( $row['id'] );
			$row = array_intersect_key(
				$row,
				array_flip( array( 'source', 'source_hash', 'target', 'redirect_type', 'is_regex', 'is_active', 'hits', 'last_hit', 'verify_status', 'verify_http', 'verify_hops', 'verify_final', 'last_verified', 'created_at', 'updated_at' ) )
			);
			if ( empty( $row['source'] ) || empty( $row['target'] ) ) {
				continue;
			}
			if ( $wpdb->insert( $table, $row ) ) {
				$restored++;
			}
		}

		return $restored;
	}

	/**
	 * Fetches one backup row.
	 *
	 * @param int $id Backup ID.
	 * @return array|null
	 */
	public static function get( $id ) {
		global $wpdb;

		$table = self::table();
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", (int) $id ), ARRAY_A );

		return $row ? $row : null;
	}

	/**
	 * Lists backups, newest first (without payloads).
	 *
	 * @return array[]
	 */
	public static function all() {
		global $wpdb;

		$table = self::table();

		return (array) $wpdb->get_results( "SELECT id, label, rows_count, created_at FROM {$table} ORDER BY id DESC", ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	}

	/**
	 * Keeps only the newest snapshots.
	 */
	private static function trim() {
		global $wpdb;

		$table = self::table();
		$ids   = $wpdb->get_col( "SELECT id FROM {$table} ORDER BY id DESC" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$extra = array_slice( array_map( 'intval', $ids ), self::KEEP );

		if ( ! empty( $extra ) ) {
			$placeholders = implode( ',', array_fill( 0, count( $extra ), '%d' ) );
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE id IN ({$placeholders})", $extra ) );
		}
	}

	/**
	 * Auto-backup when THIS plugin is about to be replaced by an update.
	 *
	 * @param object $upgrader Upgrader instance.
	 * @param array  $options  Update details.
	 */
	public static function backup_before_update( $upgrader, $options ) {
		if ( empty( $options['type'] ) || 'plugin' !== $options['type'] ) {
			return;
		}

		$plugins = array();
		if ( ! empty( $options['plugins'] ) && is_array( $options['plugins'] ) ) {
			$plugins = $options['plugins'];
		} elseif ( ! empty( $options['plugin'] ) ) {
			$plugins = array( $options['plugin'] );
		}

		if ( in_array( RDZ_PLUGIN_BASENAME, $plugins, true ) ) {
			self::snapshot( __( 'Before plugin update', 'redirectzen-301-redirect-manager' ) );
		}
	}
}
