<?php
/**
 * Handles interactions with the database table for audit logs.
 *
 * @package WP_Defender\Model
 */

namespace WP_Defender\Model;

use WP_Defender\DB;
use ReflectionException;

/**
 * Model for the  audit log table.
 */
class Audit_Log extends DB {

	public const EVENT_TYPE_USER = 'user', EVENT_TYPE_SYSTEM = 'system', EVENT_TYPE_COMMENT = 'comment',
		EVENT_TYPE_MEDIA         = 'media', EVENT_TYPE_SETTINGS = 'settings', EVENT_TYPE_CONTENT = 'content',
		EVENT_TYPE_MENU          = 'menu';

	/**
	 * Table name.
	 *
	 * @var string
	 */
	protected $table = 'defender_audit_log';

	/**
	 * Primary key column.
	 *
	 * @var int
	 * @defender_property
	 */
	public $id;
	/**
	 * Table column for timestamp.
	 *
	 * @var int
	 * @defender_property
	 */
	public $timestamp;
	/**
	 * Table column for event type.
	 *
	 * @var string
	 * @defender_property
	 */
	public $event_type;
	/**
	 * Table column for action type.
	 *
	 * @var string
	 * @defender_property
	 */
	public $action_type;
	/**
	 * Table column for site url.
	 *
	 * @var string
	 * @defender_property
	 */
	public $site_url;
	/**
	 * Table column for user id.
	 *
	 * @var int
	 * @defender_property
	 */
	public $user_id;
	/**
	 * Table column for context.
	 *
	 * @var string
	 * @defender_property
	 */
	public $context;
	/**
	 * Table column for ip.
	 *
	 * @var string
	 * @defender_property
	 */
	public $ip;
	/**
	 * Table column for message.
	 *
	 * @var string
	 * @defender_property
	 */
	public $msg;
	/**
	 * Table column for blog id.
	 *
	 * @var int
	 * @defender_property
	 */
	public $blog_id;
	/**
	 * Table column for synced flag.
	 *
	 * @var bool
	 * @defender_property
	 */
	public $synced;
	/**
	 * Table column for ttl.
	 *
	 * @var int
	 * @defender_property
	 */
	public $ttl;
	/**
	 * Safe columns.
	 *
	 * @var array
	 */
	public $safe = array(
		'id',
		'timestamp',
		'event_type',
		'action_type',
		'site_url',
		'user_id',
		'context',
		'ip',
		'msg',
		'blog_id',
		'synced',
		'ttl',
	);

	/**
	 * Truncate the table, as this is mostly use for cache, when we fetch new data, old data should be removed for
	 * consistent sync with API side.
	 */
	public static function truncate() {
		$orm = self::get_orm();
		$orm->get_repository( self::class )->truncate();
	}

	/**
	 * Get the very last item, mostly use for testing.
	 *
	 * @return self|null
	 */
	public static function get_last() {
		$orm = self::get_orm();

		return $orm->get_repository( self::class )
					->where( 'synced', 'in', array( 0, 1 ) )
					->order_by( 'id', 'desc' )
					->first();
	}

	/**
	 * Sometimes we need the pre of last, for testing.
	 *
	 * @return self|null
	 */
	public static function get_pre_last() {
		$orm   = self::get_orm();
		$model = $orm->get_repository( self::class )
					->where( 'synced', 'in', array( 0, 1 ) )
					->order_by( 'id', 'desc' )
					->limit( 2, 0 )
					->get();

		return array_pop( $model );
	}

	/**
	 * Get logs that need to be flushed.
	 *
	 * @return self[]
	 */
	public static function get_logs_need_flush() {
		$orm = self::get_orm();

		return $orm->get_repository( self::class )->where( 'synced', 0 )->limit( 50 )->get();
	}

	/**
	 * Query logs from internal cache.
	 *
	 * @param  int      $date_from  The start date we want to query, in timestamp format.
	 * @param  int      $date_to  The date end for the query, in timestamp format.
	 * @param  array    $events  Type of the event, e.g. comment, user, system.
	 * @param  string   $user_id  Who trigger this event, if it 0, will be guest.
	 * @param  string   $ip  IP of who trigger this.
	 * @param  int|bool $paged  Current page.
	 *
	 * @return array
	 */
	public static function query(
		$date_from,
		$date_to,
		$events = array(),
		$user_id = '',
		$ip = '',
		$paged = 1
	): array {
		$orm     = self::get_orm();
		$builder = $orm->get_repository( self::class );
		$builder->where( 'timestamp', '>=', $date_from )
				->where( 'timestamp', '<=', $date_to );

		if ( is_array( $events )
			&& count( $events )
			&& count( array_diff( $events, self::allowed_events() ) ) === 0 ) {
			$builder->where( 'event_type', 'in', $events );
		}

		if ( is_numeric( $user_id ) && $user_id > 0 ) {
			$builder->where( 'user_id', $user_id );
		}

		if ( is_string( $ip ) && '' !== trim( $ip ) ) {
			$builder->where( 'ip', 'like', "%$ip%" );
		}
		$builder->order_by( 'timestamp', 'desc' );

		if ( false !== $paged ) {
			// If paged == false, then it will be no paging.
			$per_page = 20;
			$offset   = ( $paged - 1 ) * $per_page;
			$builder->limit( $per_page, $offset );
		}

		return $builder->get();
	}

	/**
	 * Clean up the old logs depending on the storage settings.
	 *
	 * @param  int      $date_from  The start date we want to query, in timestamp format.
	 * @param  int      $date_to  The date end for the query, in timestamp format.
	 * @param  int|null $limit  Limit.
	 *
	 * @return void
	 */
	public static function delete_old_logs( $date_from, $date_to, $limit = null ) {
		$orm     = self::get_orm();
		$builder = $orm->get_repository( self::class );
		$builder->where( 'timestamp', '>=', $date_from )
				->where( 'timestamp', '<=', $date_to );

		if ( null === $limit ) {
			$builder->delete_all();
		} else {
			$builder->order_by( 'id' )
					->limit( $limit )
					->delete_by_limit();
		}
	}

	/**
	 * Counts the number of records in the database table that match the given criteria.
	 *
	 * @param  int    $date_from  The start date to filter records by.
	 * @param  int    $date_to  The end date to filter records by.
	 * @param  array  $events  (optional) An array of event types to filter records by.
	 * @param  string $user_id  (optional) The user ID to filter records by.
	 * @param  string $ip  (optional) The IP address to filter records by.
	 *
	 * @return int The number of records that match the given criteria.
	 */
	public static function count( $date_from, $date_to, $events = array(), $user_id = '', $ip = '' ) {
		$orm     = self::get_orm();
		$builder = $orm->get_repository( self::class );
		$builder->where( 'timestamp', '>=', $date_from )
				->where( 'timestamp', '<=', $date_to );
		if ( count( $events ) && count( array_diff( $events, self::allowed_events() ) ) === 0 ) {
			$builder->where( 'event_type', 'in', $events );
		}

		if ( is_numeric( $user_id ) && $user_id > 0 ) {
			$builder->where( 'user_id', $user_id );
		}

		if ( is_string( $ip ) && '' !== trim( $ip ) ) {
			$builder->where( 'ip', 'like', "%$ip%" );
		}

		return (int) $builder->count();
	}

	/**
	 * Mass insert logs, usually fetched from API.
	 *
	 * @param  array $data  An array of data to be inserted into the Audit_Log table.
	 *
	 * @return void
	 * @throws ReflectionException If the import method of the Audit_Log class throws a ReflectionException.
	 */
	public static function mass_insert( $data ) {
		// Use raw sql for faster.
		foreach ( $data as $datum ) {
			$item = new Audit_Log();
			$item->import( $datum );
			$item->synced = 1;
			$item->save();
		}
	}

	/**
	 * Retrieves a record from the database by its ID.
	 *
	 * @param  int $id  The ID of the record to retrieve.
	 *
	 * @return mixed The retrieved record, or null if not found.
	 */
	public static function get_by_id( $id ) {
		$orm = self::get_orm();

		return $orm->get_repository( self::class )->find_by_id( $id );
	}

	/**
	 * Get allowed types.
	 *
	 * @return array
	 */
	public static function allowed_events(): array {
		return array(
			self::EVENT_TYPE_USER,
			self::EVENT_TYPE_SYSTEM,
			self::EVENT_TYPE_COMMENT,
			self::EVENT_TYPE_MEDIA,
			self::EVENT_TYPE_SETTINGS,
			self::EVENT_TYPE_CONTENT,
			self::EVENT_TYPE_MENU,
		);
	}
}