<?php

declare(strict_types=1);

namespace SecurWP\Modules\Scanner\Repository;

// If this file is called directly, abort.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * File Index Repository - Persistence for file index state.
 *
 */
class FileIndexRepository {

	/**
	 * WordPress database object.
	 *
	 * @var \wpdb
	 */
	private $wpdb;

	/**
	 * Table name.
	 *
	 * @var string
	 */
	private $table_name;

	/**
	 * Table check guard.
	 *
	 * @var bool
	 */
	private static $table_checked = false;

	/**
	 * Constructor.
	 */
	public function __construct() {
		global $wpdb;
		$this->wpdb       = $wpdb;
		$this->table_name = $wpdb->prefix . 'securwp_file_index';
	}

	public static function ensure_schema(): void {
		$repository = new self();
		$repository->ensure_table();
	}

	public static function is_schema_current(): bool {
		$repository = new self();
		if ( ! $repository->table_exists( $repository->table_name ) ) {
			return false;
		}

		$columns = $repository->get_columns( $repository->table_name );
		if ( empty( $columns ) ) {
			return false;
		}

		$required = [ 'scan_id', 'stack', 'total_files', 'is_complete', 'created_at', 'updated_at' ];
		foreach ( $required as $column ) {
			if ( ! in_array( $column, $columns, true ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Ensure file index table exists.
	 *
	 * @return void
	 */
	private function ensure_table(): void {
		if ( self::$table_checked ) {
			return;
		}
		self::$table_checked = true;

		if ( ! $this->table_exists( $this->table_name ) ) {
			$legacy_state_table = $this->wpdb->prefix . 'securwp_discovery_state';
			if ( $this->table_exists( $legacy_state_table ) ) {
				$this->rename_table( $legacy_state_table, $this->table_name );
			} else {
				$legacy_stack_table = $this->wpdb->prefix . 'securwp_discovery_stack';
				if ( $this->table_exists( $legacy_stack_table ) ) {
					$this->rename_table( $legacy_stack_table, $this->table_name );
				}
			}
		}

		if ( $this->table_exists( $this->table_name ) ) {
			$this->maybe_upgrade_table();
			return;
		}

		$this->create_table();
	}

	/**
	 * Ensure required columns exist after a legacy rename.
	 *
	 * @return void
	 */
	private function maybe_upgrade_table(): void {
		$existing_columns = $this->get_columns( $this->table_name );
		if ( empty( $existing_columns ) ) {
			$this->create_table();
			return;
		}

		$required = [ 'scan_id', 'stack', 'total_files', 'is_complete', 'created_at', 'updated_at' ];
		foreach ( $required as $column ) {
			if ( ! in_array( $column, $existing_columns, true ) ) {
				$this->create_table();
				return;
			}
		}
	}

	/**
	 * Create or upgrade the file index table.
	 *
	 * @return void
	 */
	private function create_table(): void {
		$charset_collate = $this->wpdb->get_charset_collate();

		$sql = "CREATE TABLE {$this->table_name} (
			scan_id varchar(64) NOT NULL,
			stack longtext DEFAULT NULL,
			total_files int(10) UNSIGNED NOT NULL DEFAULT 0,
			is_complete tinyint(1) NOT NULL DEFAULT 0,
			created_at int(10) UNSIGNED NOT NULL,
			updated_at int(10) UNSIGNED NOT NULL,
			PRIMARY KEY  (scan_id),
			KEY idx_complete (is_complete),
			KEY idx_updated (updated_at)
		) $charset_collate;";

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

	/**
	 * Fetch columns for a table.
	 *
	 * @param string $table_name Table name.
	 * @return array<int, string>
	 */
	private function get_columns( string $table_name ): array {
		$results = $this->get_results_query( 'SHOW COLUMNS FROM ' . $this->escape_identifier( $table_name ) );
		if ( $results === null ) {
			securwp_log( '[SecurWP] DB error in get_columns for ' . $table_name . ': ' . $this->wpdb->last_error );
			return [];
		}
		if ( empty( $results ) ) {
			return [];
		}

		$columns = [];
		foreach ( $results as $row ) {
			if ( isset( $row['Field'] ) ) {
				$columns[] = $row['Field'];
			}
		}
		return $columns;
	}

	/**
	 * Get file index state.
	 *
	 * @param string $scan_id Scan UUID.
	 * @return array{stack: array|null, total_files: int, is_complete: bool}
	 */
	public function get_state( string $scan_id ): array {
		$row = $this->get_row_prepared(
			"SELECT stack, total_files, is_complete FROM {$this->escape_identifier( $this->table_name )} WHERE scan_id = %s",
			$scan_id
		);

		if ( ! $row ) {
			return [
				'stack' => null,
				'total_files' => 0,
				'is_complete' => false,
			];
		}

		$stack = null;
		if ( ! empty( $row['stack'] ) ) {
			$decoded = json_decode( $row['stack'], true );
			if ( is_array( $decoded ) ) {
				$stack = $decoded;
			}
		}

		return [
			'stack'       => $stack,
			'total_files' => (int) ( $row['total_files'] ?? 0 ),
			'is_complete' => (bool) $row['is_complete'],
		];
	}

	/**
	 * Increment total file count for a scan.
	 *
	 * @param string $scan_id Scan UUID.
	 * @param int    $delta   Increment amount.
	 * @return void
	 */
	public function increment_total( string $scan_id, int $delta ): void {
		if ( $delta <= 0 ) {
			return;
		}

		$now = time();

		$this->query_prepared(
			"INSERT INTO {$this->escape_identifier( $this->table_name )} (scan_id, total_files, is_complete, created_at, updated_at)
			VALUES (%s, %d, 0, %d, %d)
			ON DUPLICATE KEY UPDATE
				total_files = total_files + VALUES(total_files),
				updated_at = VALUES(updated_at)",
			$scan_id,
			$delta,
			$now,
			$now
		);
	}

	/**
	 * Get total file count for a scan.
	 *
	 * @param string $scan_id Scan UUID.
	 * @return int
	 */
	public function get_total_files( string $scan_id ): int {
		$value = $this->get_var_prepared(
			"SELECT total_files FROM {$this->escape_identifier( $this->table_name )} WHERE scan_id = %s",
			$scan_id
		);

		return (int) $value;
	}

	/**
	 * Persist file index stack.
	 *
	 * @param string $scan_id Scan UUID.
	 * @param array  $stack   Pending directory stack.
	 * @return void
	 */
	public function save_stack( string $scan_id, array $stack ): void {
		$now     = time();
		$payload = wp_json_encode( array_values( $stack ) );

		$this->query_prepared(
			"INSERT INTO {$this->escape_identifier( $this->table_name )} (scan_id, stack, is_complete, created_at, updated_at)
			VALUES (%s, %s, 0, %d, %d)
			ON DUPLICATE KEY UPDATE
				stack = VALUES(stack),
				is_complete = 0,
				updated_at = VALUES(updated_at)",
			$scan_id,
			$payload,
			$now,
			$now
		);
	}

	/**
	 * Mark file index as complete.
	 *
	 * @param string $scan_id Scan UUID.
	 * @return void
	 */
	public function mark_complete( string $scan_id ): void {
		$now     = time();
		$payload = wp_json_encode( [] );

		$this->query_prepared(
			"INSERT INTO {$this->escape_identifier( $this->table_name )} (scan_id, stack, is_complete, created_at, updated_at)
			VALUES (%s, %s, 1, %d, %d)
			ON DUPLICATE KEY UPDATE
				stack = VALUES(stack),
				is_complete = 1,
				updated_at = VALUES(updated_at)",
			$scan_id,
			$payload,
			$now,
			$now
		);
	}

	/**
	 * Check whether file index is complete for a scan.
	 *
	 * @param string $scan_id Scan UUID.
	 * @return bool
	 */
	public function is_complete( string $scan_id ): bool {
		$value = $this->get_var_prepared(
			"SELECT is_complete FROM {$this->escape_identifier( $this->table_name )} WHERE scan_id = %s",
			$scan_id
		);

		return (int) $value === 1;
	}

	/**
	 * Delete file index state for a scan.
	 *
	 * @param string $scan_id Scan UUID.
	 * @return int Number of rows deleted.
	 */
	public function delete_by_scan_id( string $scan_id ): int {
		return (int) $this->wpdb->delete(
			$this->table_name,
			[ 'scan_id' => $scan_id ],
			[ '%s' ]
		);
	}

	/**
	 * Check if a table exists.
	 *
	 * @param string $table_name Table name.
	 * @return bool
	 */
	private function table_exists( string $table_name ): bool {
		$found = $this->get_var_prepared( 'SHOW TABLES LIKE %s', $table_name );

		return $found === $table_name;
	}

	/**
	 * Rename an existing table.
	 *
	 * @param string $from Table to rename.
	 * @param string $to   Target table.
	 * @return void
	 */
	private function rename_table( string $from, string $to ): void {
		$from_identifier = $this->escape_identifier( $from );
		$to_identifier   = $this->escape_identifier( $to );

			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Validated table identifiers.
			$this->wpdb->query( "RENAME TABLE {$from_identifier} TO {$to_identifier}" );
	}

	/**
	 * Escape a SQL identifier such as a table name.
	 */
	private function escape_identifier( string $identifier ): string {
		if ( $identifier === '' || strlen( $identifier ) > 64 || ! preg_match( '/^[A-Za-z0-9_]+$/', $identifier ) ) {
			throw new \InvalidArgumentException( 'Invalid SQL identifier.' );
		}

		return '`' . $identifier . '`';
	}

	private function get_results_query( string $sql ): ?array {
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Repository query.
			$results = $this->wpdb->get_results( $sql, ARRAY_A );
		return is_array( $results ) ? $results : null;
	}

	private function get_row_prepared( string $sql, ...$params ): ?array {
		$prepared = $this->prepare_sql( $sql, ...$params );
		if ( '' === $prepared ) {
			return null;
		}

			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Prepared repository query.
			$result = $this->wpdb->get_row( $prepared, ARRAY_A );
		return is_array( $result ) ? $result : null;
	}

	private function get_var_prepared( string $sql, ...$params ) {
		$prepared = $this->prepare_sql( $sql, ...$params );
		if ( '' === $prepared ) {
			return null;
		}

			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Prepared repository query.
			return $this->wpdb->get_var( $prepared );
	}

	private function query_prepared( string $sql, ...$params ) {
		$prepared = $this->prepare_sql( $sql, ...$params );
		if ( '' === $prepared ) {
			return false;
		}

			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Prepared repository query.
			return $this->wpdb->query( $prepared );
	}

	private function prepare_sql( string $sql, ...$params ): string {
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Bound with wpdb::prepare().
			$prepared = $this->wpdb->prepare( $sql, ...$params );
		return is_string( $prepared ) ? $prepared : '';
	}
}
