<?php
/**
 * Scan persistence.
 *
 * @package LocalGuardScanner
 */

namespace LocalGuardScanner\Infrastructure\Database;

use RuntimeException;

/**
 * Persists scan lifecycle data.
 */
final class ScanRepository {
	/** @var \wpdb */
	private $wpdb;
	/** @var string */
	private $table;

	/** @param \wpdb $wpdb WordPress database object. */
	public function __construct( $wpdb ) {
		$this->wpdb  = $wpdb;
		$this->table = Schema::table_names( $wpdb )['scans'];
	}

	/**
	 * @param int    $site_id Site ID.
	 * @param string $trigger Trigger type.
	 * @param string $now     UTC MySQL time.
	 * @return int
	 */
	public function create( $site_id, $trigger, $now ) {
		$result = $this->wpdb->insert(
			$this->table,
			array(
				'site_id'      => (int) $site_id,
				'trigger_type' => $trigger,
				'status'       => 'queued',
				'phase'        => 'queued',
				'created_at'   => $now,
			),
			array( '%d', '%s', '%s', '%s', '%s' )
		);
		if ( false === $result ) {
			throw new RuntimeException( 'Scan state could not be created.' );
		}
		return (int) $this->wpdb->insert_id;
	}

	/**
	 * @param int                 $scan_id Scan ID.
	 * @param array<string,mixed> $data    Safe fields.
	 * @return bool
	 */
	public function update( $scan_id, array $data ) {
		$allowed = array_intersect_key(
			$data,
			array_flip( array( 'status', 'phase', 'cursor_json', 'coverage_json', 'stats_json', 'progress_json', 'error_count', 'failure_code', 'failure_stage', 'rules_version', 'started_at', 'last_checkpoint_at', 'finished_at' ) )
		);
		if ( empty( $allowed ) ) {
			return false;
		}
		return false !== $this->wpdb->update( $this->table, $allowed, array( 'id' => (int) $scan_id ) );
	}

	/**
	 * @param int $scan_id Scan ID.
	 * @return array<string,mixed>|null
	 */
	public function find( $scan_id ) {
		$query = $this->wpdb->prepare( 'SELECT * FROM %i WHERE id = %d LIMIT 1', $this->table, (int) $scan_id );
		$row   = $this->wpdb->get_row( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$this->throw_on_database_error();
		return is_array( $row ) ? $row : null;
	}

	/** @return array<string,mixed>|null */
	public function active() {
		$query = $this->wpdb->prepare(
			"SELECT * FROM %i WHERE status IN ('queued','discovering','scanning_files','scanning_database','classifying') ORDER BY id DESC LIMIT 1",
			$this->table
		);
		$row   = $this->wpdb->get_row( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$this->throw_on_database_error();
		return is_array( $row ) ? $row : null;
	}

	/** @return array<string,mixed>|null */
	public function latest() {
		$query = $this->wpdb->prepare( 'SELECT * FROM %i ORDER BY id DESC LIMIT 1', $this->table );
		$row   = $this->wpdb->get_row( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$this->throw_on_database_error();
		return is_array( $row ) ? $row : null;
	}

	/**
	 * Delete old terminal scans while retaining the newest completed history.
	 *
	 * @param string $cutoff UTC MySQL timestamp.
	 * @param int    $keep   Number of newest terminal scans to retain.
	 * @param int    $limit  Maximum rows.
	 * @return int
	 */
	public function delete_old_terminal( $cutoff, $keep, $limit ) {
		$keep_query = $this->wpdb->prepare(
			"SELECT id FROM %i WHERE status IN ('completed','partial','cancelled','failed') ORDER BY id DESC LIMIT %d",
			$this->table,
			max( 1, (int) $keep )
		);
		$keep_ids   = array_values( array_filter( array_map( 'intval', $this->wpdb->get_col( $keep_query ) ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$query      = $this->wpdb->prepare(
			"SELECT id FROM %i WHERE status IN ('completed','partial','cancelled','failed') AND finished_at < %s ORDER BY id ASC LIMIT %d",
			$this->table,
			$cutoff,
			max( 1, (int) $limit ) + count( $keep_ids )
		);
		$ids        = array_values( array_diff( array_map( 'intval', $this->wpdb->get_col( $query ) ), $keep_ids ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$ids        = array_slice( $ids, 0, max( 1, (int) $limit ) );
		$deleted    = 0;
		foreach ( $ids as $id ) {
			$result = $this->wpdb->delete( $this->table, array( 'id' => $id ), array( '%d' ) );
			if ( false !== $result ) {
				$deleted += (int) $result;
			}
		}
		return $deleted;
	}

	/** @return void */
	private function throw_on_database_error() {
		if ( '' !== $this->wpdb->last_error ) {
			throw new RuntimeException( 'Scan state could not be read safely.' );
		}
	}
}
