<?php
/**
 * WPMR_Definitions_Trait
 *
 * Generated during Phase 2 restructuring
 * This trait contains methods extracted from the original monolithic wpmr.php
 *
 * @package WP_Malware_Removal
 */

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

}

trait WPMR_Definitions {

	/**
	 * Ensure the plugin has a baseline malware definitions payload stored in settings.
	 *
	 * Call sites:
	 * - Invoked during initialization (see `wpmr.php`) and from admin UI bootstrap.
	 * - Also used as a fallback by `get_definitions()` / `get_definition_version()`.
	 *
	 * Behavior:
	 * - If the `signatures` setting is missing/empty, loads definitions from `wpmr.json`,
	 *   stores them via `update_setting( 'signatures', ... )`, and resets `sig_time` to 0.
	 * - If definitions already exist in settings, this method performs no work.
	 *
	 * Side effects:
	 * - Updates plugin settings (`signatures`, `sig_time`).
	 *
	 * Suggested rename: `ensure_default_definitions_loaded()` or `load_default_definitions_if_missing()`.
	 *
	 * @return array|null Returns the loaded definitions array when it had to seed defaults; otherwise null.
	 */
	function maybe_load_default_definitions() {
		$definitions = $this->get_setting( 'signatures' );
		if ( ! $definitions ) {
			$definitions = file_get_contents( trailingslashit( $this->dir ) . 'wpmr.json' );
			$definitions = json_decode( $definitions, true );
			$update      = $this->update_setting( 'signatures', $definitions );
			$this->update_setting( 'sig_time', 0 );
			return $definitions;
		}
	}

	/**
	 * Retrieve the active malware definitions payload, ordered by severity.
	 *
	 * Call sites:
	 * - File scanner and DB scanner use `get_definitions()['definitions']['files']` and
	 *   `get_definitions()['definitions']['db']` to iterate signatures.
	 *
	 * Behavior:
	 * - Loads definitions from the `signatures` setting; if missing, seeds from `wpmr.json`.
	 * - Removes the top-level version key `v` from the returned structure.
	 * - Re-orders both file and DB definition maps so severe/high signatures are evaluated
	 *   before suspicious ones.
	 *
	 * Suggested rename: `get_ordered_definitions()` or `get_definitions_ordered_by_severity()`.
	 *
	 * @return array<string,mixed> Definitions payload (with `definitions.files` and `definitions.db`), version key removed.
	 */
	function get_definitions() {
		$definitions = $this->get_setting( 'signatures' );
		if ( ! $definitions ) {
			$definitions = $this->maybe_load_default_definitions();
		}
		unset( $definitions['v'] );
		$severe     = array();
		$high       = array();
		$suspicious = array();
		foreach ( $definitions['definitions']['files'] as $definition => $signature ) {
			if ( $signature['severity'] == 'severe' ) {
				$severe[ $definition ] = $definitions['definitions']['files'][ $definition ];
			}
			if ( $signature['severity'] == 'high' ) {
				$high[ $definition ] = $definitions['definitions']['files'][ $definition ];
			}
			if ( $signature['severity'] == 'suspicious' ) {
				$suspicious[ $definition ] = $definitions['definitions']['files'][ $definition ];
			}
		}
		$files      = array_merge( $severe, $high, $suspicious ); // always return definitions in this sequence else suspicious matches are returned first without scanning for severe infections.
		$severe     = array();
		$high       = array();
		$suspicious = array();
		foreach ( $definitions['definitions']['db'] as $definition => $signature ) {
			if ( $signature['severity'] == 'severe' ) {
				$severe[ $definition ] = $definitions['definitions']['db'][ $definition ];
			}
			if ( $signature['severity'] == 'high' ) {
				$high[ $definition ] = $definitions['definitions']['db'][ $definition ];
			}
			if ( $signature['severity'] == 'suspicious' ) {
				$suspicious[ $definition ] = $definitions['definitions']['db'][ $definition ];
			}
		}
		$db                                  = array_merge( $severe, $high, $suspicious );
		$definitions['definitions']['files'] = $files;
		$definitions['definitions']['db']    = $db;
		return $definitions;
	}

	/**
	 * Retrieve malware definitions along with their version.
	 *
	 * This is a convenience wrapper for callers that need both the ordered
	 * definitions map and the active definitions version.
	 *
	 * Notes:
	 * - `get_definitions()` intentionally unsets the top-level `v` key.
	 * - Stateful scan workflows need `v` to record/compare signature versions.
	 *
	 * Suggested rename: `get_definitions_with_version()`.
	 *
	 * @return array{definitions: array<string, mixed>, v: string} Definitions payload with version.
	 */
	function get_stateless_definitions() {
		$definitions = $this->get_definitions();
		$definitions['v'] = $this->get_definition_version();
		return $definitions;
	}

	/**
	 * Count the total number of malware signatures across all definition groups.
	 *
	 * Used for UI/telemetry and WP-CLI diagnostics.
	 *
	 * Suggested rename: `count_definitions()`.
	 *
	 * @return int Total count of signatures.
	 */
	function get_definition_count() {
		$defs  = $this->get_definitions();
		$count = 0;
		while ( count( $defs['definitions'] ) ) {
			$count += count( array_shift( $defs['definitions'] ) );
		}
		return $count;
	}

	/**
	 * Get the current local malware definitions version.
	 *
	 * This reads the `v` key from the stored `signatures` setting (seeding from `wpmr.json`
	 * if needed). Unlike `get_definitions()`, this method intentionally returns the version.
	 *
	 * Suggested rename: `get_definitions_version()`.
	 *
	 * @return string Definitions version string, or empty string if unavailable.
	 */
	function get_definition_version() {
		$sigs = $this->get_setting( 'signatures' );

		if ( empty( $sigs ) ) {
			$sigs = $this->maybe_load_default_definitions();
		}

		if ( is_array( $sigs ) && array_key_exists( 'v', $sigs ) && $sigs['v'] !== '' ) {
			return $sigs['v'];
		}

		return '';
	}

	/**
	 * Get a human-readable "last updated" string for definitions.
	 *
	 * The underlying timestamp is stored in the `sig_time` setting.
	 *
	 * Suggested rename: `get_definitions_last_updated_ago()`.
	 *
	 * @return string Returns 'Never' when no timestamp is stored, otherwise e.g. '2 hours ago'.
	 */
	function get_last_updated_ago() {
		$updated = $this->get_setting( 'sig_time' );
		if ( ! $updated ) {
			return 'Never';
		} else {
			return human_time_diff( gmdate( 'U', $updated ), gmdate( 'U' ) ) . ' ago';
		}
	}

	/**
	 * Fetch the full malware definitions payload from the SaaS backend.
	 *
	 * This performs a `saas_request('saas_update_definitions')` call and returns the raw
	 * response structure used by `update_definitions()` and `update_definitions_cli()`.
	 *
	 * Suggested rename: `fetch_definitions_payload()`.
	 *
	 * @param array{blocking?:bool,timeout?:float|int} $options Request options.
	 * @return array|WP_Error SaaS response array on success, or WP_Error on failure.
	 */
	function fetch_definitions( $options = array() ) {
		$options = wp_parse_args(
			$options,
			array(
				'blocking'    => true,
				'timeout'     => $this->timeout,
			)
		);

		return $this->saas_request(
			'saas_update_definitions',
			array(
				'method'      => 'GET',
				'send_state'  => 'query',
				'query'       => array(
					'cachebust' => time(),
				),
				'blocking'    => (bool) $options['blocking'],
				'timeout'     => (float) $options['timeout'],
			)
		);
	}

	/**
	 * Check whether updated definitions are available and cache the available server version.
	 *
	 * Call sites:
	 * - Daily cron (`wpmr_daily`) and several UI/CLI code paths.
	 *
	 * Behavior:
	 * - Calls `fetch_definitions_version()`.
	 * - In blocking mode (default), validates payload and persists `update-version`.
	 * - In async mode (`$async = true`), issues a non-blocking request and returns true
	 *   if the request was started successfully.
	 *
	 * Side effects:
	 * - Updates `update-version` setting when a valid server version is returned.
	 *
	 * Suggested rename: `check_for_definition_updates()`.
	 *
	 * @param bool $async When true, performs a non-blocking network request.
	 * @return true|null Returns true on a successful check/kickoff; otherwise null.
	 */
	function check_definitions( $async = false ) {
		$blocking = empty( $async );
		$timeout  = $blocking ? $this->timeout : 0.5;

		$response = $this->fetch_definitions_version(
			array(
				'blocking'    => $blocking,
				'timeout'     => $timeout,
			)
		);

		if ( is_wp_error( $response ) ) {
			return;
		}

		if ( ! $blocking ) {
			return true;
		}

		$version = isset( $response['response'] ) ? $response['response'] : null;
		if ( empty( $version ) || empty( $version['success'] ) ) {
			return;
		}

		$payload = isset( $response['payload'] ) ? $response['payload'] : null;

		if ( empty( $payload ) || empty( $payload['server_defver'] ) ) {
			return;
		}

		$this->update_setting( 'update-version', $payload['server_defver'] );
		return true;
	}

	/**
	 * Fetch the latest definitions version metadata from the SaaS backend.
	 *
	 * This is lighter-weight than fetching full signatures and is used by `check_definitions()`.
	 *
	 * Suggested rename: `fetch_definitions_version_payload()`.
	 *
	 * @param array{blocking?:bool,timeout?:float|int} $options Request options.
	 * @return array|WP_Error SaaS response array on success, or WP_Error on failure.
	 */
	function fetch_definitions_version( $options = array() ) {
		$options = wp_parse_args(
			$options,
			array(
				'blocking'    => true,
				'timeout'     => $this->timeout,
			)
		);

		$def_version = $this->get_definition_version();
		$this->flog( 'Checking definitions. Current version: ' . ( empty( $def_version ) ? 'none' : $def_version ) );
		if ( ! is_scalar( $def_version ) || null === $def_version ) {
			$def_version = '';
		} else {
			$def_version = (string) $def_version;
		}

		return $this->saas_request(
			'saas_check_definitions',
			array(
				'method'      => 'GET',
				'send_state'  => 'query',
				'query'       => array(
					'cachebust' => time(),
				),
				'state_extra' => array( 'defver' => $def_version ),
				'blocking'    => (bool) $options['blocking'],
				'timeout'     => (float) $options['timeout'],
			)
		);
	}

	/**
	 * AJAX handler: fetch and persist updated malware definitions.
	 *
	 * Hooked to `wp_ajax_wpmr_update_sigs`.
	 *
	 * Security:
	 * - Verifies nonce `wpmr_update_sigs` via `check_ajax_referer()`.
	 * - Requires the plugin capability stored in `$this->cap`.
	 *
	 * Side effects:
	 * - Updates `signatures` and `sig_time` settings.
	 * - Emits a JSON response via `wp_send_json_success()` / `wp_send_json_error()`.
	 *
	 * Note: `$force` is currently unused.
	 * Suggested rename: `ajax_update_definitions()`.
	 *
	 * @param bool $force Unused.
	 * @return void
	 */
	function update_definitions( $force = false ) {
		check_ajax_referer( 'wpmr_update_sigs', 'wpmr_update_nonce' );
		if ( ! current_user_can( $this->cap ) ) {
			return;
		}
		$this->raise_limits_conditionally();
		$response = $this->fetch_definitions();
		if ( is_wp_error( $response ) ) {
			return wp_send_json_error( $response->get_error_message() );
		}

		$definitions = isset( $response['response'] ) ? $response['response'] : null;
		if ( empty( $definitions ) || empty( $definitions['success'] ) ) {
			return wp_send_json_error( 'Unparsable definition-update.' );
		}

		$payload = isset( $response['payload'] ) ? $response['payload'] : null;

		if ( empty( $payload ) || empty( $payload['signatures'] ) ) {
			return wp_send_json_error( 'Empty definition payload.' );
		}

		$definitions_data = $payload['signatures'];
		$this->update_setting( 'signatures', $definitions_data );
		$time = gmdate( 'U' );
		$this->update_setting( 'sig_time', $time );
		return wp_send_json_success(
			array(
				'count'    => $this->get_definition_count(),
				'version'  => $this->get_definition_version(),
				'sig_time' => $this->get_last_updated_ago(),
			)
		);
	}

	/**
	 * Update malware definitions in CLI/automation contexts.
	 *
	 * Call sites:
	 * - Used by WP-CLI commands and by internal automation paths (e.g., stateful scanning).
	 *
	 * Behavior:
	 * - Fetches the full definitions payload via `fetch_definitions()`.
	 * - On success, persists `signatures` and `sig_time`.
	 * - When `$echo` is true, prints feedback either via WP-CLI or via `echo`.
	 * - When `$echo` is false, returns a boolean status.
	 *
	 * Caveat:
	 * - In WP-CLI mode with `$echo` true, `WP_CLI::error()` may exit execution.
	 *
	 * Suggested rename: `sync_definitions()` or `update_definitions_non_ajax()`.
	 *
	 * @param bool $echo Whether to emit user-facing output.
	 * @return bool Returns true on successful update when not echoing; false on failure when not echoing.
	 */
	function update_definitions_cli( $echo = false ) {
		$this->raise_limits_conditionally();
		$response = $this->fetch_definitions();
		
		if ( is_wp_error( $response ) ) {
			if ( $echo ) {
				if ( $this->wpmr_iscli() ) {
					WP_CLI::error( $response->get_error_message() );
				} else {
					echo 'Error: ' . esc_html( $response->get_error_message() );
				}
			} else {
				return false;
			}
		}

		$definitions = isset( $response['response'] ) ? $response['response'] : null;
		if ( empty( $definitions ) || empty( $definitions['success'] ) ) {
			if ( $echo ) {
				if ( $this->wpmr_iscli() ) {
					WP_CLI::error( 'Unparsable definition-update.' );
				} else {
					echo 'Unparsable definition-update.';
				}
			} else {
				return false;
			}
		}

		$payload = isset( $response['payload'] ) ? $response['payload'] : null;

		if ( empty( $payload ) || empty( $payload['signatures'] ) ) {
			if ( $echo ) {
				if ( $this->wpmr_iscli() ) {
					WP_CLI::error( 'Empty definition payload.' );
				} else {
					echo 'Empty definition payload.';
				}
			} else {
				return false;
			}
		}

		$definitions_data = $payload['signatures'];
		$this->update_setting( 'signatures', $definitions_data );
		$time = gmdate( 'U' );
		$this->update_setting( 'sig_time', $time );
		if ( $echo ) {
			if ( $this->wpmr_iscli() ) {
				WP_CLI::success( 'Updated Malcure definitions to version: ' . WP_CLI::colorize( '%Y' . $definitions_data['v'] . '. %nCount: %Y' . $this->get_definition_count() . '%n' ) . ' definitions.' );
			} else {
				echo 'Updated Malcure definitions to version <strong>' . esc_html( $definitions_data['v'] ) . '</strong>. Count: <strong>' . esc_html( $this->get_definition_count() ) . '</strong> definitions.';
			}
		} else {
			return true;
		}
	}

	/**
	 * Determine whether a newer definitions version is available.
	 *
	 * Compares the local definitions version (`get_definition_version()`) against the cached
	 * server version stored as `update-version` (typically populated by `check_definitions()`).
	 *
	 * Suggested rename: `get_definitions_update_info()` or `get_available_definitions_update()`.
	 *
	 * @return array{new:string,current:string}|null Returns update info when versions differ, otherwise null.
	 */
	function definition_updates_available() {
		$current = $this->get_definition_version();
		$new     = $this->get_setting( 'update-version' );
		// $this->flog( 'Definition versions: current=' . $current . '= new=' . $new . '=' );
		if ( ! empty( $current ) && ! empty( $new ) && $current != $new ) {
			return array(
				'new'     => $new,
				'current' => $current,
			);
		}
	}
}
