<?php
/**
 * Order persistence (CPT + meta).
 *
 * @package WPDudeProducts
 */

declare(strict_types=1);

namespace WPDude\Products\Orders;

/**
 * Create / find / update orders.
 */
final class Order_Repository {

	public const META_PREFIX = '_wpdude_dc_order_';

	/**
	 * Meta keys (logical => storage).
	 *
	 * @return array<string, string>
	 */
	public static function meta_keys(): array {
		return array(
			'uuid'                       => self::META_PREFIX . 'uuid',
			'status'                     => self::META_PREFIX . 'status',
			'product_id'                 => self::META_PREFIX . 'product_id',
			'combination_key'            => self::META_PREFIX . 'combination_key',
			'qty'                        => self::META_PREFIX . 'qty',
			'amount'                     => self::META_PREFIX . 'amount',
			'tax_amount'                 => self::META_PREFIX . 'tax_amount',
			'shipping_amount'            => self::META_PREFIX . 'shipping_amount',
			'currency'                   => self::META_PREFIX . 'currency',
			'customer_email'             => self::META_PREFIX . 'customer_email',
			'customer_name'              => self::META_PREFIX . 'customer_name',
			'stripe_checkout_session_id' => self::META_PREFIX . 'stripe_session_id',
			'stripe_payment_intent_id'   => self::META_PREFIX . 'stripe_pi_id',
			'stripe_customer_id'         => self::META_PREFIX . 'stripe_customer_id',
			'stripe_risk_level'          => self::META_PREFIX . 'stripe_risk_level',
			'stripe_risk_score'          => self::META_PREFIX . 'stripe_risk_score',
			'stripe_fee_amount'          => self::META_PREFIX . 'stripe_fee_amount',
			'stripe_net_amount'          => self::META_PREFIX . 'stripe_net_amount',
			'stripe_fee_currency'        => self::META_PREFIX . 'stripe_fee_currency',
			'shipping_address'           => self::META_PREFIX . 'shipping_address',
			'line_item'                  => self::META_PREFIX . 'line_item',
			'stock_decremented'          => self::META_PREFIX . 'stock_decremented',
			'paid_at'                    => self::META_PREFIX . 'paid_at',
			'picked_at'                  => self::META_PREFIX . 'picked_at',
			'shipped_at'                 => self::META_PREFIX . 'shipped_at',
			'return_token'               => self::META_PREFIX . 'return_token',
		);
	}

	/**
	 * High-entropy token for Stripe return URLs (not derivable from the UUID).
	 */
	public static function generate_return_token(): string {
		return bin2hex( random_bytes( 16 ) );
	}

	/**
	 * Whether a return token is the expected 32-char hex.
	 */
	public static function is_return_token_format( string $token ): bool {
		return (bool) preg_match( '/^[a-f0-9]{32}$/i', $token );
	}

	/**
	 * Constant-time match against the stored checkout return token.
	 *
	 * @param array<string, mixed> $order Order row.
	 */
	public static function return_token_matches( array $order, string $token ): bool {
		$stored = (string) ( $order['return_token'] ?? '' );
		if ( ! self::is_return_token_format( $token ) || ! self::is_return_token_format( $stored ) ) {
			return false;
		}

		return hash_equals( strtolower( $stored ), strtolower( $token ) );
	}

	/**
	 * Create a pending order.
	 *
	 * @param array<string, mixed> $data Data.
	 * @return int Order post ID.
	 */
	public function create( array $data ): int {
		$uuid = (string) ( $data['uuid'] ?? wp_generate_uuid4() );
		$title = sprintf(
			/* translators: %s: order uuid short */
			__( 'Order %s', 'dude-commerce' ),
			substr( $uuid, 0, 8 )
		);

		$post_id = wp_insert_post(
			array(
				'post_type'   => Post_Type::POST_TYPE,
				'post_status' => 'publish',
				'post_title'  => $title,
			),
			true
		);

		if ( is_wp_error( $post_id ) || ! $post_id ) {
			return 0;
		}

		$data['uuid']   = $uuid;
		$data['status'] = (string) ( $data['status'] ?? Statuses::PENDING );
		$this->update( (int) $post_id, $data );

		return (int) $post_id;
	}

	/**
	 * Update order meta.
	 *
	 * @param array<string, mixed> $data Data.
	 */
	public function update( int $order_id, array $data ): void {
		$keys = self::meta_keys();
		foreach ( $data as $logical => $value ) {
			if ( ! isset( $keys[ $logical ] ) ) {
				continue;
			}
			update_post_meta( $order_id, $keys[ $logical ], $value );
		}
	}

	/**
	 * Find by post ID.
	 *
	 * @return array<string, mixed>|null
	 */
	public function find( int $order_id ): ?array {
		$post = get_post( $order_id );
		if ( ! $post || Post_Type::POST_TYPE !== $post->post_type ) {
			return null;
		}
		return $this->hydrate( $order_id );
	}

	/**
	 * Find by UUID.
	 *
	 * @return array<string, mixed>|null
	 */
	public function find_by_uuid( string $uuid ): ?array {
		$ids = get_posts(
			array(
				'post_type'      => Post_Type::POST_TYPE,
				'post_status'    => 'any',
				'posts_per_page' => 1,
				'fields'         => 'ids',
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- unique order UUID lookup.
				'meta_key'       => self::META_PREFIX . 'uuid',
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- unique order UUID lookup.
				'meta_value'     => $uuid,
			)
		);
		if ( empty( $ids ) ) {
			return null;
		}
		return $this->hydrate( (int) $ids[0] );
	}

	/**
	 * Find by Stripe Checkout Session ID.
	 *
	 * @return array<string, mixed>|null
	 */
	public function find_by_session( string $session_id ): ?array {
		$ids = get_posts(
			array(
				'post_type'      => Post_Type::POST_TYPE,
				'post_status'    => 'any',
				'posts_per_page' => 1,
				'fields'         => 'ids',
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- unique Stripe session lookup.
				'meta_key'       => self::META_PREFIX . 'stripe_session_id',
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- unique Stripe session lookup.
				'meta_value'     => $session_id,
			)
		);
		if ( empty( $ids ) ) {
			return null;
		}
		return $this->hydrate( (int) $ids[0] );
	}

	/**
	 * Find by payment intent.
	 *
	 * @return array<string, mixed>|null
	 */
	public function find_by_payment_intent( string $payment_intent ): ?array {
		$ids = get_posts(
			array(
				'post_type'      => Post_Type::POST_TYPE,
				'post_status'    => 'any',
				'posts_per_page' => 1,
				'fields'         => 'ids',
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- unique Payment Intent lookup.
				'meta_key'       => self::META_PREFIX . 'stripe_pi_id',
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- unique Payment Intent lookup.
				'meta_value'     => $payment_intent,
			)
		);
		if ( empty( $ids ) ) {
			return null;
		}
		return $this->hydrate( (int) $ids[0] );
	}

	/**
	 * Query orders for admin list.
	 *
	 * @param array<string, mixed> $args Filters.
	 * @return array{items: list<array<string, mixed>>, total: int, pages: int}
	 */
	public function query( array $args = array() ): array {
		$defaults = array(
			'status'         => '',
			'product_id'     => 0,
			'customer_email' => '',
			'date_from'      => '',
			'date_to'        => '',
			'date_field'     => 'created',
			'per_page'       => 20,
			'page'           => 1,
		);
		$args     = array_merge( $defaults, $args );

		$meta_query = array( 'relation' => 'AND' );

		$status = $args['status'];
		if ( is_array( $status ) ) {
			$statuses = array_values(
				array_filter(
					array_map( 'sanitize_key', $status ),
					static function ( string $s ): bool {
						return in_array( $s, Statuses::all(), true );
					}
				)
			);
			if ( ! empty( $statuses ) ) {
				$meta_query[] = array(
					'key'     => self::META_PREFIX . 'status',
					'value'   => $statuses,
					'compare' => 'IN',
				);
			}
		} else {
			$status = sanitize_key( (string) $status );
			if ( '' !== $status && in_array( $status, Statuses::all(), true ) ) {
				$meta_query[] = array(
					'key'   => self::META_PREFIX . 'status',
					'value' => $status,
				);
			}
		}

		$product_id = absint( $args['product_id'] );
		if ( $product_id > 0 ) {
			$meta_query[] = array(
				'key'   => self::META_PREFIX . 'product_id',
				'value' => $product_id,
				'type'  => 'NUMERIC',
			);
		}

		$email = sanitize_email( (string) $args['customer_email'] );
		if ( '' !== $email ) {
			$meta_query[] = array(
				'key'   => self::META_PREFIX . 'customer_email',
				'value' => $email,
			);
		}

		$date_from  = $this->sanitize_date_ymd( (string) $args['date_from'] );
		$date_to    = $this->sanitize_date_ymd( (string) $args['date_to'] );
		$date_field = sanitize_key( (string) $args['date_field'] );
		$date_query = array();

		if ( 'paid_at' === $date_field ) {
			$meta_query[] = array(
				'key'     => self::META_PREFIX . 'paid_at',
				'compare' => 'EXISTS',
			);
			if ( '' !== $date_from ) {
				$meta_query[] = array(
					'key'     => self::META_PREFIX . 'paid_at',
					'value'   => $date_from . ' 00:00:00',
					'compare' => '>=',
					'type'    => 'CHAR',
				);
			}
			if ( '' !== $date_to ) {
				$meta_query[] = array(
					'key'     => self::META_PREFIX . 'paid_at',
					'value'   => $date_to . ' 23:59:59',
					'compare' => '<=',
					'type'    => 'CHAR',
				);
			}
		} else {
			if ( '' !== $date_from ) {
				$date_query['after'] = $date_from . ' 00:00:00';
			}
			if ( '' !== $date_to ) {
				$date_query['before'] = $date_to . ' 23:59:59';
			}
			if ( ! empty( $date_query ) ) {
				$date_query['inclusive'] = true;
			}
		}

		$per_page = max( 1, (int) $args['per_page'] );
		$page     = max( 1, (int) $args['page'] );

		$query_args = array(
			'post_type'      => Post_Type::POST_TYPE,
			'post_status'    => 'any',
			'posts_per_page' => $per_page,
			'paged'          => $page,
			'orderby'        => 'date',
			'order'          => 'DESC',
			'fields'         => 'ids',
		);

		if ( count( $meta_query ) > 1 ) {
			$query_args['meta_query'] = $meta_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- admin list filter.
		}
		if ( ! empty( $date_query ) ) {
			$query_args['date_query'] = array( $date_query );
		}

		$q     = new \WP_Query( $query_args );
		$items = array();
		foreach ( $q->posts as $post_id ) {
			$order = $this->hydrate( (int) $post_id );
			if ( $order ) {
				$items[] = $order;
			}
		}

		$total = (int) $q->found_posts;
		$pages = (int) max( 1, (int) ceil( $total / $per_page ) );

		return array(
			'items' => $items,
			'total' => $total,
			'pages' => $pages,
		);
	}

	/**
	 * Transition order status if allowed.
	 *
	 * @return true|\WP_Error
	 */
	public function transition_status( int $order_id, string $to ) {
		$order = $this->find( $order_id );
		if ( ! $order ) {
			return new \WP_Error( 'not_found', __( 'Order not found.', 'dude-commerce' ) );
		}

		$to   = sanitize_key( $to );
		$from = (string) ( $order['status'] ?? '' );

		if ( ! Statuses::can_transition( $from, $to ) ) {
			return new \WP_Error(
				'invalid_transition',
				__( 'That status change is not allowed for this order.', 'dude-commerce' )
			);
		}

		$data = array( 'status' => $to );
		if ( Statuses::SHIPPED === $to && empty( $order['shipped_at'] ) ) {
			$data['shipped_at'] = current_time( 'mysql', true );
		}

		$this->update( $order_id, $data );

		$updated = $this->find( $order_id );
		if ( $updated ) {
			/**
			 * Fires after an order status changes.
			 *
			 * @param array<string, mixed> $order Updated order.
			 * @param string               $from  Previous status.
			 * @param string               $to    New status.
			 */
			do_action( 'wpdude_dc_order_status_changed', $updated, $from, $to );
		}

		return true;
	}

	/**
	 * @return array<string, mixed>
	 */
	private function hydrate( int $order_id ): array {
		$post = get_post( $order_id );
		$row  = array(
			'id'         => $order_id,
			'created_at' => $post ? (string) $post->post_date_gmt : '',
		);
		foreach ( self::meta_keys() as $logical => $meta_key ) {
			$row[ $logical ] = get_post_meta( $order_id, $meta_key, true );
		}
		$row['product_id']        = (int) ( $row['product_id'] ?? 0 );
		$row['qty']               = max( 1, (int) ( $row['qty'] ?? 1 ) );
		$row['amount']            = (int) ( $row['amount'] ?? 0 );
		$row['tax_amount']        = (int) ( $row['tax_amount'] ?? 0 );
		$row['shipping_amount']   = (int) ( $row['shipping_amount'] ?? 0 );
		$row['stock_decremented'] = ! empty( $row['stock_decremented'] );
		$row['status']            = (string) ( $row['status'] ?? Statuses::PENDING );
		$row['currency']          = strtolower( (string) ( $row['currency'] ?? 'gbp' ) );
		$row['uuid']              = (string) ( $row['uuid'] ?? '' );
		if ( ! is_array( $row['shipping_address'] ?? null ) ) {
			$row['shipping_address'] = array();
		}
		if ( ! is_array( $row['line_item'] ?? null ) ) {
			$row['line_item'] = array();
		}
		return $row;
	}

	/**
	 * Sanitize YYYY-MM-DD.
	 */
	private function sanitize_date_ymd( string $value ): string {
		$value = sanitize_text_field( $value );
		if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ) {
			return '';
		}
		return $value;
	}
}
