<?php
/**
 * Event store: the flight recorder's tape. One table, append + prune.
 *
 * @package Form_Flight_Recorder
 */

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

/**
 * Events table CRUD.
 */
class Ffrd_Events {

	/**
	 * Table name.
	 *
	 * @return string
	 */
	public static function table() {
		global $wpdb;
		return $wpdb->prefix . 'ffrd_events';
	}

	/**
	 * Append an event.
	 *
	 * @param string $type     Event type slug (submission, notification_missing, notification_failed, form_silent, presence_missing, alert_sent, note).
	 * @param string $ref      Object reference (source:form_id, sub-<id>...).
	 * @param string $severity info|warning|critical.
	 * @param array  $data     Extra payload (JSON-encoded).
	 * @return int Insert id (0 on failure).
	 */
	public function insert( $type, $ref = '', $severity = 'info', $data = array() ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- append to the plugin's own diagnostics table.
		$ok = $wpdb->insert(
			self::table(),
			array(
				'event_type' => substr( sanitize_key( $type ), 0, 32 ),
				'object_ref' => substr( (string) $ref, 0, 191 ),
				'severity'   => in_array( $severity, array( 'info', 'warning', 'critical' ), true ) ? $severity : 'info',
				'data'       => wp_json_encode( $data ),
				'created_at' => gmdate( 'Y-m-d H:i:s' ),
			),
			array( '%s', '%s', '%s', '%s', '%s' )
		);

		$id = $ok ? (int) $wpdb->insert_id : 0;

		if ( $id ) {
			/**
			 * Fires after an event lands on the tape (alerts listen).
			 *
			 * @param string $type     Event type.
			 * @param string $ref      Object reference.
			 * @param string $severity Severity.
			 * @param array  $data     Payload.
			 * @param int    $id       Event row id.
			 */
			do_action( 'ffrd_event', $type, (string) $ref, $severity, $data, $id );
		}

		return $id;
	}

	/**
	 * Query events.
	 *
	 * @param array $args type (string|array), since (unix), until (unix), limit, offset, severity, ref.
	 * @return array Rows (objects) with decoded data.
	 */
	public function query( $args = array() ) {
		global $wpdb;

		$limit  = isset( $args['limit'] ) ? max( 1, min( 500, (int) $args['limit'] ) ) : 100;
		$offset = isset( $args['offset'] ) ? max( 0, (int) $args['offset'] ) : 0;
		$since  = isset( $args['since'] ) ? gmdate( 'Y-m-d H:i:s', (int) $args['since'] ) : gmdate( 'Y-m-d H:i:s', time() - 30 * DAY_IN_SECONDS );

		$where  = 'created_at >= %s';
		$params = array( $since );

		if ( ! empty( $args['until'] ) ) {
			$where   .= ' AND created_at <= %s';
			$params[] = gmdate( 'Y-m-d H:i:s', (int) $args['until'] );
		}
		if ( ! empty( $args['type'] ) ) {
			$types        = array_map( 'sanitize_key', (array) $args['type'] );
			$placeholders = implode( ', ', array_fill( 0, count( $types ), '%s' ) );
			$where       .= " AND event_type IN ( {$placeholders} )";
			$params       = array_merge( $params, $types );
		}
		if ( ! empty( $args['severity'] ) ) {
			$where   .= ' AND severity = %s';
			$params[] = sanitize_key( $args['severity'] );
		}
		if ( ! empty( $args['ref'] ) ) {
			$where   .= ' AND object_ref = %s';
			$params[] = (string) $args['ref'];
		}

		$params[] = $limit;
		$params[] = $offset;

		$table = self::table();
		$rows  = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} WHERE {$where} ORDER BY id DESC LIMIT %d OFFSET %d", $params ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- own diagnostics table; $where is literal fragments + placeholders; $params holds exactly one value per placeholder and all pass through prepare().

		if ( ! is_array( $rows ) ) {
			return array();
		}
		foreach ( $rows as $row ) {
			$decoded   = json_decode( (string) $row->data, true );
			$row->data = is_array( $decoded ) ? $decoded : array();
		}
		return $rows;
	}

	/**
	 * Count events of a type since a time (optionally per ref).
	 *
	 * @param string|array $type  Type(s).
	 * @param int          $since Unix time.
	 * @param string       $ref   Optional exact ref.
	 * @return int
	 */
	public function count( $type, $since, $ref = '' ) {
		global $wpdb;

		$types        = array_map( 'sanitize_key', (array) $type );
		$placeholders = implode( ', ', array_fill( 0, count( $types ), '%s' ) );
		$params       = array_merge( array( gmdate( 'Y-m-d H:i:s', (int) $since ) ), $types );

		$extra = '';
		if ( '' !== $ref ) {
			$extra    = ' AND object_ref = %s';
			$params[] = (string) $ref;
		}

		$table = self::table();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- counting rows in the plugin's own diagnostics table; placeholders built from literals.
		return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE created_at >= %s AND event_type IN ( {$placeholders} ){$extra}", $params ) );
	}

	/**
	 * Latest event of a type (or null).
	 *
	 * @param string $type Type.
	 * @param string $ref  Optional object ref filter.
	 * @return object|null
	 */
	public function latest( $type, $ref = '' ) {
		$rows = $this->query(
			array(
				'type'  => $type,
				'ref'   => $ref,
				'limit' => 1,
				'since' => time() - 90 * DAY_IN_SECONDS,
			)
		);
		return $rows ? $rows[0] : null;
	}

	/**
	 * Prune old rows. Free retention: 30 days (Pro filters to 90).
	 */
	public function prune() {
		global $wpdb;

		/**
		 * Filters how many days of events are retained.
		 *
		 * @param int $days Retention days.
		 */
		$days   = (int) apply_filters( 'ffrd_retention_days', 30 );
		$days   = max( 1, min( 365, $days ) );
		$cutoff = gmdate( 'Y-m-d H:i:s', time() - $days * DAY_IN_SECONDS );

		$table = self::table();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- pruning the plugin's own diagnostics table.
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE created_at < %s", $cutoff ) );
	}
}
