<?php
/**
 * Data access for the `embp_snapshots` table.
 *
 * @package EmberPrice
 */

namespace EmberPrice\Core;

defined( 'ABSPATH' ) || exit;

/**
 * Lives in Core (not Apply) for the same reason as {@see Rows}: written by
 * Apply\Snapshotter before a job's prices are overwritten, and will be read
 * back by the future rollback service -- both need it without violating
 * the dependency matrix.
 */
final class Snapshots {

	/**
	 * Records a product/variation's price before it gets overwritten.
	 *
	 * @param int        $job_id            Job id.
	 * @param int        $product_id        Matched product id (the parent, for variations too).
	 * @param int|null   $variation_id      Matched variation id, or null for a simple product.
	 * @param float|null $old_regular_price Current `_regular_price`, or null if unset.
	 * @param float|null $old_sale_price    Current `_sale_price`, or null if unset.
	 */
	public function create( int $job_id, int $product_id, ?int $variation_id, ?float $old_regular_price, ?float $old_sale_price ): void {
		global $wpdb;
		$table = $wpdb->prefix . 'embp_snapshots';

		$wpdb->insert(
			$table,
			array(
				'job_id'            => $job_id,
				'product_id'        => $product_id,
				'variation_id'      => $variation_id,
				'old_regular_price' => $old_regular_price,
				'old_sale_price'    => $old_sale_price,
			),
			array( '%d', '%d', '%d', '%f', '%f' )
		);
	}

	/**
	 * All snapshots for a job, in the order they were taken -- read by the
	 * future rollback service and by tests verifying Snapshotter's output.
	 *
	 * @param int $job_id Job id.
	 */
	public function get_by_job( int $job_id ): array {
		global $wpdb;
		$table = $wpdb->prefix . 'embp_snapshots';

		$rows = $wpdb->get_results(
			$wpdb->prepare( 'SELECT * FROM %i WHERE job_id = %d ORDER BY id ASC', $table, $job_id ),
			ARRAY_A
		);

		return array_map( array( $this, 'hydrate' ), null !== $rows ? $rows : array() );
	}

	/**
	 * Total number of snapshots for a job -- the fixed "total" for the
	 * rollback batch runner's progress reporting, computed once before the
	 * first batch runs (same role as Rows::count_by_job_and_statuses() for
	 * apply).
	 *
	 * @param int $job_id Job id.
	 */
	public function count_by_job( int $job_id ): int {
		global $wpdb;
		$table = $wpdb->prefix . 'embp_snapshots';

		return (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE job_id = %d', $table, $job_id ) );
	}

	/**
	 * A page of a job's snapshots, in the order they were taken -- used by
	 * Rest\JobRollbackScheduler to walk a job's snapshots in fixed-size
	 * chunks across separate Action Scheduler requests, the same way
	 * Rows::get_batch_by_statuses() does for apply.
	 *
	 * @param int $job_id Job id.
	 * @param int $offset Snapshots to skip.
	 * @param int $limit  Max snapshots to return.
	 */
	public function get_batch_by_job( int $job_id, int $offset, int $limit ): array {
		global $wpdb;
		$table = $wpdb->prefix . 'embp_snapshots';

		$rows = $wpdb->get_results(
			$wpdb->prepare( 'SELECT * FROM %i WHERE job_id = %d ORDER BY id ASC LIMIT %d OFFSET %d', $table, $job_id, $limit, $offset ),
			ARRAY_A
		);

		return array_map( array( $this, 'hydrate' ), null !== $rows ? $rows : array() );
	}

	/**
	 * Deletes snapshots older than $days (SPEC §6's retention cron), based
	 * on `applied_at` -- the moment the snapshot was taken, not a job
	 * status. Returns how many rows were deleted, for the cron's own
	 * logging and for tests.
	 *
	 * @param int $days Retention window, in days.
	 */
	public function delete_older_than( int $days ): int {
		global $wpdb;
		$table = $wpdb->prefix . 'embp_snapshots';

		return (int) $wpdb->query(
			$wpdb->prepare( 'DELETE FROM %i WHERE applied_at < ( NOW() - INTERVAL %d DAY )', $table, $days )
		);
	}

	/**
	 * Deletes all snapshots for a job.
	 *
	 * @param int $job_id Job id.
	 */
	public function delete_by_job( int $job_id ): void {
		global $wpdb;
		$table = $wpdb->prefix . 'embp_snapshots';

		$wpdb->delete( $table, array( 'job_id' => $job_id ), array( '%d' ) );
	}

	/**
	 * Casts nullable numeric columns.
	 *
	 * @param array $row Raw database row.
	 */
	private function hydrate( array $row ): array {
		$row['id']                = (int) $row['id'];
		$row['job_id']            = (int) $row['job_id'];
		$row['product_id']        = (int) $row['product_id'];
		$row['variation_id']      = null !== $row['variation_id'] ? (int) $row['variation_id'] : null;
		$row['old_regular_price'] = null !== $row['old_regular_price'] ? (float) $row['old_regular_price'] : null;
		$row['old_sale_price']    = null !== $row['old_sale_price'] ? (float) $row['old_sale_price'] : null;

		return $row;
	}
}
