<?php
/**
 * BitForgeNet Guest Tickets - Audit trail and error logging.
 *
 * @package BitForgeNet_Guest_Tickets
 * @since   1.0.0
 */

defined( 'ABSPATH' ) || exit;

// This file talks to the plugin's own tables. WordPress has no API for them --
// they are not posts and not meta -- so $wpdb is the only way in. The rows
// change on every booking and every scan, which makes an object cache wrong
// more often than it is right. Table names are built from $wpdb->prefix and
// never from a request, so interpolating one carries nothing to inject; every
// value goes through $wpdb->prepare(), and the few identifiers that cannot
// (ORDER BY columns) are matched against a whitelist first.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
// phpcs:disable WordPress.DB.DirectDatabaseQuery.SchemaChange
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter


/**
 * Writes to the bfnet_logs table.
 */
class BFNET_Log {

	/**
	 * Record an audit entry.
	 *
	 * @param string $object_type Entity type (event, order, ticket, system...).
	 * @param int    $object_id   Entity id.
	 * @param string $action      Action slug.
	 * @param array  $context     Structured context.
	 * @param string $level       info|warning|error|security.
	 * @return int Inserted id, or 0.
	 */
	public static function audit( $object_type, $object_id, $action, $context = array(), $level = 'info' ) {
		global $wpdb;

		$table = BFNET_Install::table( 'logs' );

		$ok = $wpdb->insert(
			$table,
			array(
				'user_id'     => get_current_user_id(),
				'object_type' => substr( (string) $object_type, 0, 40 ),
				'object_id'   => (int) $object_id,
				'action'      => substr( (string) $action, 0, 60 ),
				'context'     => wp_json_encode( $context ),
				'level'       => substr( (string) $level, 0, 20 ),
				'ip_hash'     => BFNET_Security::ip_hash(),
				'created_at'  => current_time( 'mysql' ),
			),
			array( '%d', '%s', '%d', '%s', '%s', '%s', '%s', '%s' )
		);

		return $ok ? (int) $wpdb->insert_id : 0;
	}

	/**
	 * Record a security-relevant event.
	 *
	 * @param string $action  Action slug.
	 * @param array  $context Context data.
	 * @return void
	 */
	public static function security( $action, $context = array() ) {
		self::audit( 'security', 0, $action, $context, 'security' );
	}

	/**
	 * Record an error, and mirror it to the PHP error log when debugging.
	 *
	 * @param string $action  Action slug.
	 * @param string $message Human readable message.
	 * @param array  $context Context data.
	 * @return void
	 */
	public static function error( $action, $message, $context = array() ) {
		$context['message'] = $message;
		self::audit( 'system', 0, $action, $context, 'error' );

		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			error_log( '[BitForgeNet Guest Tickets] ' . $action . ': ' . $message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
		}
	}

	/**
	 * Fetch recent log rows.
	 *
	 * @param array $args Query arguments.
	 * @return array
	 */
	public static function recent( $args = array() ) {
		global $wpdb;

		$args = wp_parse_args(
			$args,
			array(
				'level'       => '',
				'object_type' => '',
				'object_id'   => 0,
				'limit'       => 50,
				'offset'      => 0,
			)
		);

		$table  = BFNET_Install::table( 'logs' );
		$where  = array( '1=1' );
		$params = array();

		if ( $args['level'] ) {
			$where[]  = 'level = %s';
			$params[] = $args['level'];
		}
		if ( $args['object_type'] ) {
			$where[]  = 'object_type = %s';
			$params[] = $args['object_type'];
		}
		if ( $args['object_id'] ) {
			$where[]  = 'object_id = %d';
			$params[] = (int) $args['object_id'];
		}

		$params[] = (int) $args['limit'];
		$params[] = (int) $args['offset'];

		$sql = 'SELECT * FROM ' . $table . ' WHERE ' . implode( ' AND ', $where ) . ' ORDER BY id DESC LIMIT %d OFFSET %d';

		return $wpdb->get_results( $wpdb->prepare( $sql, $params ) ); // phpcs:ignore WordPress.DB.PreparedSQL
	}

	/**
	 * Delete log rows older than the configured retention period.
	 *
	 * @return int Rows removed.
	 */
	public static function purge() {
		global $wpdb;

		$days = (int) BFNET_Settings::get( 'log_retention_days', 365 );
		if ( $days <= 0 ) {
			return 0;
		}

		$table  = BFNET_Install::table( 'logs' );
		$cutoff = gmdate( 'Y-m-d H:i:s', time() - ( $days * DAY_IN_SECONDS ) );

		return (int) $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE created_at < %s AND level = 'info'", $cutoff ) ); // phpcs:ignore WordPress.DB.PreparedSQL
	}
}
