<?php
/**
 * Append-only event log for work orders.
 *
 * One shared table records both status-change events and free-text notes against
 * a work order. The log is append-only: there is no update or delete method, so
 * the history can never be silently rewritten. Author identity is denormalized
 * (author_id plus a snapshot of the display name) so a later user rename or
 * deletion does not orphan the history. All writes go through $wpdb with explicit
 * format arrays; the only interpolated identifier is the trusted, prefix-built
 * table name.
 *
 * @package WrenchPress
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class WrenchPress_Events_Repository
 */
class WrenchPress_Events_Repository {

	/**
	 * Fully qualified events table name.
	 *
	 * @return string
	 */
	protected static function table() {
		global $wpdb;

		return $wpdb->prefix . 'wrenchpress_events';
	}

	/**
	 * Record a status-change event for a work order.
	 *
	 * Captures the from/to statuses and a snapshot of the acting user. The note
	 * field is left empty for a pure status change. The caller is responsible for
	 * performing the actual transition through the work-orders repository; this
	 * only writes the audit row.
	 *
	 * @param int    $work_order_id Work-order id.
	 * @param string $from          Status the work order moved from.
	 * @param string $to            Status the work order moved to.
	 * @return int|false New event id, or false on a bad work order / failure.
	 */
	public static function log_status_change( $work_order_id, $from, $to ) {
		return self::insert_event(
			$work_order_id,
			'status_change',
			(string) $from,
			(string) $to,
			''
		);
	}

	/**
	 * Record a free-text note against a work order.
	 *
	 * The note text must already be sanitized by the caller (for example with
	 * sanitize_textarea_field) before it reaches this method; the repository binds
	 * it via prepare but does not itself strip markup.
	 *
	 * @param int    $work_order_id Work-order id.
	 * @param string $note          Sanitized note body.
	 * @return int|false New event id, or false on a bad work order / failure.
	 */
	public static function add_note( $work_order_id, $note ) {
		return self::insert_event(
			$work_order_id,
			'note',
			'',
			'',
			(string) $note
		);
	}

	/**
	 * Record a document-generated event against a work order.
	 *
	 * Called only on the first issue of a document type (the create branch of the
	 * document service), so a reprint never appends a second event (INV-06). The
	 * note captures the human-facing label and the assigned number; from/to are
	 * empty because this is not a status transition. No schema change is needed:
	 * the shared events table already carries event_type, note, and the author
	 * snapshot.
	 *
	 * @param int    $work_order_id Work-order id.
	 * @param string $doc_type      'invoice' or 'estimate'.
	 * @param int    $doc_number    Assigned sequential number.
	 * @return int|false New event id, or false on a bad work order / failure.
	 */
	public static function log_document( $work_order_id, $doc_type, $doc_number ) {
		if ( 'estimate' === (string) $doc_type ) {
			/* translators: %s: document number */
			$note = sprintf( __( 'Estimate #%s generated', 'pitdesk-repair-crm' ), (string) $doc_number );
		} else {
			/* translators: %s: document number */
			$note = sprintf( __( 'Invoice #%s generated', 'pitdesk-repair-crm' ), (string) $doc_number );
		}

		return self::insert_event( $work_order_id, 'document', '', '', $note );
	}

	/**
	 * List every event for one work order, newest first.
	 *
	 * @param int $work_order_id Work-order id.
	 * @return array List of row objects, newest first.
	 */
	public static function list_for_work_order( $work_order_id ) {
		global $wpdb;

		$work_order_id = absint( $work_order_id );

		if ( 0 === $work_order_id ) {
			return array();
		}

		$table = self::table();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table, intentionally not object-cached.
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				'SELECT * FROM %i WHERE work_order_id = %d ORDER BY created_at DESC',
				$table,
				$work_order_id
			)
		);

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

	/**
	 * Insert one event row with a denormalized author snapshot.
	 *
	 * Shared by log_status_change() and add_note(); rejects a zero work_order_id.
	 * The author id and display-name snapshot are read from the current user so
	 * the history survives a later rename or deletion of that user.
	 *
	 * @param int    $work_order_id Work-order id.
	 * @param string $event_type    'status_change' or 'note'.
	 * @param string $from_status   Source status (status changes only).
	 * @param string $to_status     Target status (status changes only).
	 * @param string $note          Note body (notes only).
	 * @return int|false New event id, or false on a bad work order / failure.
	 */
	private static function insert_event( $work_order_id, $event_type, $from_status, $to_status, $note ) {
		global $wpdb;

		$work_order_id = absint( $work_order_id );

		if ( 0 === $work_order_id ) {
			return false;
		}

		// wp_get_current_user() always returns a WP_User; a logged-out request
		// yields one with ID 0 and an empty display name, so no null guard is
		// needed before reading the snapshot fields.
		$user = wp_get_current_user();

		$row = array(
			'work_order_id' => $work_order_id,
			'event_type'    => (string) $event_type,
			'from_status'   => (string) $from_status,
			'to_status'     => (string) $to_status,
			'note'          => (string) $note,
			'author_id'     => (int) $user->ID,
			'author_name'   => (string) $user->display_name,
			'created_at'    => current_time( 'mysql' ),
		);

		$formats = array(
			'%d', // work_order_id.
			'%s', // event_type.
			'%s', // from_status.
			'%s', // to_status.
			'%s', // note.
			'%d', // author_id.
			'%s', // author_name.
			'%s', // created_at.
		);

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table write via the $wpdb array API, intentionally not object-cached.
		$inserted = $wpdb->insert( self::table(), $row, $formats );

		if ( false === $inserted ) {
			return false;
		}

		return (int) $wpdb->insert_id;
	}
}
