<?php
namespace Pay4All\Pro\Domain;

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

/**
 * One row per scannable ticket. Created by the order pipeline when a
 * ticket-type product is bought, with one row per qty ordered.
 */
final class Ticket {

	public static function table() : string {
		global $wpdb;
		return $wpdb->prefix . 'p4all_tickets';
	}

	public static function create( int $order_id, int $product_id, array $customer ) : ?string {
		global $wpdb;
		$product = get_post( $product_id );
		if ( ! $product ) { return null; }
		$token = self::random_token();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$wpdb->insert(
			self::table(),
			[
				'order_id'      => $order_id,
				'product_id'    => $product_id,
				'product_slug'  => sanitize_title( (string) $product->post_name ?: ( 'p-' . $product_id ) ),
				'token'         => $token,
				'customer_data' => wp_json_encode( $customer ),
				'created_at'    => current_time( 'mysql' ),
			],
			[ '%d', '%d', '%s', '%s', '%s', '%s' ]
		);
		return $token;
	}

	/**
	 * Lookup by token. Accepts either the full 40-char token (used in legacy
	 * URLs / admin POST forms / nonces) or the {@see SHORT_TOKEN_LENGTH}-char
	 * prefix used in the shortened QR URLs. Anything shorter is rejected to
	 * keep collision probability negligible.
	 */
	public const SHORT_TOKEN_LENGTH = 18;

	public static function find_by_token( string $token ) : ?array {
		global $wpdb;
		$len = strlen( $token );
		// Reject anything below the prefix length so we never match too loosely.
		if ( $len < self::SHORT_TOKEN_LENGTH ) { return null; }
		$table = self::table();
		if ( $len >= 40 ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name built from $wpdb->prefix + hardcoded literal, no user input; value is prepared via %s.
			$row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE token = %s", $token ), ARRAY_A );
			return is_array( $row ) ? $row : null;
		}
		// Prefix lookup. 18 hex chars = 72 bits of entropy → birthday
		// probability of a prefix collision over 10^6 tickets is ~10^-13,
		// which is fine for this use case.
		$like = $wpdb->esc_like( $token ) . '%';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name built from $wpdb->prefix + hardcoded literal; value is prepared via %s.
		$row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE token LIKE %s ORDER BY id ASC LIMIT 1", $like ), ARRAY_A );
		return is_array( $row ) ? $row : null;
	}

	public static function mark_validated( int $id, string $ip ) : bool {
		global $wpdb;
		$table = self::table();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$ok = $wpdb->update(
			$table,
			[
				'validated_at' => current_time( 'mysql' ),
				'validated_ip' => substr( $ip, 0, 45 ),
			],
			[ 'id' => $id, 'validated_at' => null ],
			[ '%s', '%s' ],
			[ '%d', '%s' ]
		);
		return (bool) $ok;
	}

	/**
	 * Permanently remove a ticket row. Used by the admin "Supprimer" button
	 * for tickets created by mistake. Irreversible — the merchant must
	 * resend the order email if they want the customer to get a new QR.
	 */
	public static function delete( int $id ) : bool {
		global $wpdb;
		$table = self::table();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$ok = $wpdb->delete( $table, [ 'id' => $id ], [ '%d' ] );
		return (bool) $ok;
	}

	/**
	 * Release a previously-validated ticket so it can be scanned again — used
	 * by the admin "Libérer" button to recover from a mis-scan at the door.
	 * Sets validated_at back to NULL and clears the recorded IP.
	 */
	public static function mark_unvalidated( int $id ) : bool {
		global $wpdb;
		$table = self::table();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$ok = $wpdb->update(
			$table,
			[
				// Passing null in $data clears the column to SQL NULL — the
				// matching '%s' format entry is ignored by $wpdb->update().
				'validated_at' => null,
				'validated_ip' => '',
			],
			[ 'id' => $id ],
			[ '%s', '%s' ],
			[ '%d' ]
		);
		return (bool) $ok;
	}

	public static function tickets_for_order( int $order_id ) : array {
		global $wpdb;
		$table = self::table();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name built from $wpdb->prefix + hardcoded literal, no user input; value is prepared via %d.
		$rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} WHERE order_id = %d ORDER BY id ASC", $order_id ), ARRAY_A );
		return is_array( $rows ) ? $rows : [];
	}

	/**
	 * Validation URL pattern: /p4all-ticket/{short-token}.
	 * The short token is the first {@see SHORT_TOKEN_LENGTH} chars of the full
	 * token — short URL = fewer QR modules = much easier to scan from a phone
	 * screen. Frontend\Endpoints maps this URL to render_ticket_page().
	 */
	public static function validation_url( array $row ) : string {
		$token = (string) $row['token'];
		$short = substr( $token, 0, self::SHORT_TOKEN_LENGTH );
		return home_url( '/p4all-ticket/' . rawurlencode( $short ) );
	}

	private static function random_token() : string {
		// 40 hex chars = 160 bits — plenty for an unguessable per-seat token.
		return bin2hex( random_bytes( 20 ) );
	}
}
