<?php
/**
 * Order view magic links.
 *
 * @package WPDudeProducts
 */

declare(strict_types=1);

namespace WPDude\Products\Access;

use WPDude\Products\Orders\Order_Repository;

/**
 * Create and resolve passwordless order view links (Downloads grant style).
 */
final class Order_Magic_Link_Service {

	public const QUERY_ARG = 'wpdude_dc_view_order';

	/**
	 * Default link lifetime in days.
	 */
	public const DEFAULT_EXPIRY_DAYS = 90;

	private Order_Token_Repository $tokens;
	private Order_Repository $orders;

	public function __construct(
		?Order_Token_Repository $tokens = null,
		?Order_Repository $orders = null
	) {
		$this->tokens = $tokens ?? new Order_Token_Repository();
		$this->orders = $orders ?? new Order_Repository();
	}

	/**
	 * Get or create a view-order URL for the customer email.
	 *
	 * @param array<string, mixed> $order Order.
	 */
	public function get_or_create_url( array $order ): string {
		$order_id = (int) ( $order['id'] ?? 0 );
		$uuid     = (string) ( $order['uuid'] ?? '' );
		if ( $order_id <= 0 || '' === $uuid ) {
			return '';
		}

		$cached = get_transient( $this->cache_key( $uuid ) );
		if ( is_string( $cached ) && '' !== $cached ) {
			return $cached;
		}

		$existing = $this->tokens->find_by_order( $order_id );
		if ( ! $existing ) {
			$result = $this->create_for_order( $order );
			return is_wp_error( $result ) ? '' : $result['url'];
		}

		return $this->reissue_token( $order, $existing );
	}

	/**
	 * Create a new token grant for an order.
	 *
	 * @param array<string, mixed> $order Order.
	 * @return array{token: string, url: string, token_id: int}|\WP_Error
	 */
	public function create_for_order( array $order ) {
		$order_id = (int) ( $order['id'] ?? 0 );
		$uuid     = (string) ( $order['uuid'] ?? '' );
		if ( $order_id <= 0 ) {
			return new \WP_Error( 'invalid_order', __( 'Invalid order.', 'dude-commerce' ) );
		}

		$existing = $this->tokens->find_by_order( $order_id );
		if ( $existing ) {
			$url = $this->reissue_token( $order, $existing );
			return array(
				'token'    => '',
				'url'      => $url,
				'token_id' => (int) $existing['id'],
			);
		}

		$expiry_days = (int) apply_filters( 'wpdude_dc_order_link_expiry', self::DEFAULT_EXPIRY_DAYS, $order );
		$expiry_days = max( 1, $expiry_days );

		$token = bin2hex( random_bytes( 32 ) );
		$hash  = hash( 'sha256', $token );

		$id = $this->tokens->create(
			array(
				'order_id'   => $order_id,
				'token_hash' => $hash,
				'expires_at' => gmdate( 'Y-m-d H:i:s', time() + ( $expiry_days * DAY_IN_SECONDS ) ),
			)
		);

		$url = $this->url_for_token( $token );
		$this->cache_display_url( $uuid, $url, $expiry_days * DAY_IN_SECONDS );

		return array(
			'token'    => $token,
			'url'      => $url,
			'token_id' => $id,
		);
	}

	/**
	 * Resolve a raw token to an order, or WP_Error.
	 *
	 * @return array<string, mixed>|\WP_Error
	 */
	public function resolve_order( string $raw_token ) {
		$raw_token = strtolower( trim( $raw_token ) );
		if ( ! preg_match( '/^[a-f0-9]{64}$/', $raw_token ) ) {
			return new \WP_Error( 'invalid_token', __( 'This order link is invalid.', 'dude-commerce' ) );
		}

		$row = $this->tokens->find_by_hash( hash( 'sha256', $raw_token ) );
		if ( ! $row ) {
			return new \WP_Error( 'invalid_token', __( 'This order link is invalid or has expired.', 'dude-commerce' ) );
		}

		if ( ! empty( $row['expires_at'] ) ) {
			$expires = strtotime( (string) $row['expires_at'] . ' UTC' );
			if ( false !== $expires && $expires < time() ) {
				return new \WP_Error( 'expired_token', __( 'This order link has expired.', 'dude-commerce' ) );
			}
		}

		$order = $this->orders->find( (int) $row['order_id'] );
		if ( ! $order ) {
			return new \WP_Error( 'not_found', __( 'Order not found.', 'dude-commerce' ) );
		}

		return $order;
	}

	/**
	 * Build front-end URL for a raw token.
	 */
	public function url_for_token( string $token ): string {
		return add_query_arg( self::QUERY_ARG, $token, home_url( '/' ) );
	}

	/**
	 * @param array<string, mixed> $order    Order.
	 * @param array<string, mixed> $existing Token row.
	 */
	private function reissue_token( array $order, array $existing ): string {
		$token = bin2hex( random_bytes( 32 ) );
		$hash  = hash( 'sha256', $token );
		$this->tokens->update_token_hash( (int) $existing['id'], $hash );

		$url = $this->url_for_token( $token );
		$ttl = DAY_IN_SECONDS;
		if ( ! empty( $existing['expires_at'] ) ) {
			$remaining = strtotime( (string) $existing['expires_at'] . ' UTC' ) - time();
			if ( $remaining > 0 ) {
				$ttl = $remaining;
			}
		}
		$this->cache_display_url( (string) ( $order['uuid'] ?? '' ), $url, $ttl );

		return $url;
	}

	private function cache_display_url( string $uuid, string $url, int $ttl ): void {
		if ( '' === $uuid || '' === $url ) {
			return;
		}
		set_transient( $this->cache_key( $uuid ), $url, max( HOUR_IN_SECONDS, (int) $ttl ) );
	}

	private function cache_key( string $uuid ): string {
		return 'wpdude_dc_order_url_' . $uuid;
	}
}
