<?php
/**
 * Recent activity — one row per send, retry and failure.
 *
 * @package Ordsync
 */

defined( 'ABSPATH' ) || exit;

/**
 * A ring buffer of the last MAX events, kept in a single option.
 *
 * Deliberately not a database table. A table would mean dbDelta, an upgrade
 * path, uninstall cleanup and hand-written SQL that Plugin Check flags — all
 * to store something the user reads for thirty seconds when a send breaks.
 * Recent activity is the whole job, so a bounded buffer is the honest shape.
 *
 * This is a convenience view and never the source of truth. The order meta
 * (_ordsync_done_ / _ordsync_error_) and the WooCommerce log both outlive it
 * and both stay authoritative — which is what makes the trade-off in write()
 * acceptable.
 */
class Ordsync_Events {

	const OPTION = 'ordsync_events';

	/**
	 * How many events to keep. 200 rows is roughly 25 KB serialised.
	 */
	const MAX = 200;

	/**
	 * Longest error message we store, in characters.
	 *
	 * A destination can return a whole HTML error page. Storing 200 of those
	 * would turn a 25 KB option into a multi-megabyte one.
	 */
	const MSG_MAX = 160;

	/**
	 * Every event, oldest first.
	 *
	 * @return array
	 */
	public static function all() {
		$events = get_option( self::OPTION, array() );

		return is_array( $events ) ? $events : array();
	}

	/**
	 * Every event, newest first — the order the table displays them in.
	 *
	 * @return array
	 */
	public static function recent() {
		return array_reverse( self::all() );
	}

	/**
	 * A send succeeded.
	 *
	 * @param int    $order_id    Order ID.
	 * @param string $instance_id Destination instance id.
	 * @param int    $attempt     Which attempt succeeded.
	 * @return void
	 */
	public static function record_sent( $order_id, $instance_id, $attempt = 1 ) {
		self::write( $order_id, $instance_id, 'sent', '', $attempt );
	}

	/**
	 * A manual "Send Test Order" succeeded.
	 *
	 * Deliberately a distinct state from 'sent', not order 0 with state
	 * 'sent'. A test proves the connection works, not that a real order
	 * arrived, and the table must not claim the stronger thing -- the same
	 * reasoning that already keeps Ordsync_Settings::record_test() separate
	 * from record_success() for the status chip.
	 *
	 * @param string $instance_id Destination instance id.
	 * @return void
	 */
	public static function record_test( $instance_id ) {
		self::write( 0, $instance_id, 'test', '', 1 );
	}

	/**
	 * A send failed and will be tried again.
	 *
	 * @param int    $order_id    Order ID.
	 * @param string $instance_id Destination instance id.
	 * @param string $message     Why.
	 * @param int    $attempt     Which attempt just failed.
	 * @return void
	 */
	public static function record_retrying( $order_id, $instance_id, $message, $attempt = 1 ) {
		self::write( $order_id, $instance_id, 'retrying', $message, $attempt );
	}

	/**
	 * A send failed for good.
	 *
	 * @param int    $order_id    Order ID.
	 * @param string $instance_id Destination instance id.
	 * @param string $message     Why.
	 * @param int    $attempt     Which attempt gave up.
	 * @return void
	 */
	public static function record_failed( $order_id, $instance_id, $message, $attempt = 1 ) {
		self::write( $order_id, $instance_id, 'failed', $message, $attempt );
	}

	/**
	 * Append one event, dropping the oldest once the buffer is full.
	 *
	 * Read-modify-write on a single option, so two workers finishing in the
	 * same instant can lose one row. That is accepted rather than overlooked:
	 * locking every send to protect a display buffer would cost more than the
	 * row is worth, and nothing depends on this data being complete.
	 *
	 * @param int    $order_id    Order ID.
	 * @param string $instance_id Destination instance id.
	 * @param string $state       sent|retrying|failed.
	 * @param string $message     Error text, empty on success.
	 * @param int    $attempt     Attempt number.
	 * @return void
	 */
	private static function write( $order_id, $instance_id, $state, $message, $attempt ) {

		$events = self::all();

		$events[] = array(
			'time'    => time(),
			'order'   => (int) $order_id,
			'dest'    => (string) $instance_id,
			'state'   => (string) $state,
			'message' => self::trim_message( $message ),
			'attempt' => max( 1, (int) $attempt ),
		);

		if ( count( $events ) > self::MAX ) {
			$events = array_slice( $events, - self::MAX );
		}

		// Autoload off: this is only ever read on one admin screen.
		update_option( self::OPTION, $events, false );
	}

	/**
	 * Shorten a message without splitting a multi-byte character.
	 *
	 * @param string $message Raw message.
	 * @return string
	 */
	private static function trim_message( $message ) {
		$message = trim( wp_strip_all_tags( (string) $message ) );

		if ( '' === $message ) {
			return '';
		}

		/*
		 * mb_substr, not substr. Cutting a German error message mid-character
		 * produces invalid UTF-8, which json_encode then refuses, taking the
		 * whole admin screen down with it.
		 */
		if ( function_exists( 'mb_substr' ) && function_exists( 'mb_strlen' ) ) {
			if ( mb_strlen( $message ) > self::MSG_MAX ) {
				$message = mb_substr( $message, 0, self::MSG_MAX ) . '…';
			}

			return $message;
		}

		return ( strlen( $message ) > self::MSG_MAX )
			? substr( $message, 0, self::MSG_MAX ) . '…'
			: $message;
	}

	/**
	 * Throw the buffer away.
	 *
	 * @return void
	 */
	public static function clear() {
		delete_option( self::OPTION );
	}
}
