<?php
/**
 * Base REST controller: permission/nonce check, uniform error envelope.
 *
 * @package EmberPrice
 */

namespace EmberPrice\Rest;

defined( 'ABSPATH' ) || exit;

/**
 * All EmberPrice REST controllers extend this for a consistent permission
 * check (capability plus CSRF protection for cookie authentication) and error
 * response shape.
 */
abstract class Controller extends \WP_REST_Controller {

	/**
	 * REST namespace shared by every EmberPrice route.
	 *
	 * @var string
	 */
	protected $namespace = 'emberprice/v1';

	/**
	 * Shared permission_callback: requires the `manage_woocommerce`
	 * capability. Cookie-authenticated requests also require a valid REST
	 * nonce; WordPress Application Password authentication is already
	 * request-bound and does not use a CSRF nonce.
	 *
	 * @param \WP_REST_Request $request The current request.
	 *
	 * @return true|\WP_Error
	 */
	public function permissions_check( \WP_REST_Request $request ) {
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			return $this->error(
				'embp_forbidden',
				__( 'You do not have permission to do this.', 'emberprice' ),
				403
			);
		}

		if ( function_exists( 'rest_get_authenticated_app_password' ) && null !== rest_get_authenticated_app_password() ) {
			return true;
		}

		$nonce = $request->get_header( 'X-WP-Nonce' );

		if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
			return $this->error(
				'embp_invalid_nonce',
				__( 'Missing or invalid nonce.', 'emberprice' ),
				403
			);
		}

		return true;
	}

	/**
	 * Builds the plugin's uniform error envelope: `{ code, message, data }`.
	 *
	 * @param string $code    Machine-readable error code.
	 * @param string $message Human-readable error message.
	 * @param int    $status  HTTP status code.
	 * @param array  $data    Extra error data merged alongside `status`.
	 */
	protected function error( string $code, string $message, int $status = 400, array $data = array() ): \WP_Error {
		return new \WP_Error( $code, $message, array_merge( array( 'status' => $status ), $data ) );
	}

	/**
	 * Stub callback for routes not implemented yet.
	 */
	public function not_implemented(): \WP_Error {
		return $this->error(
			'embp_not_implemented',
			__( 'Not implemented yet.', 'emberprice' ),
			501
		);
	}

	/**
	 * Adds a `matched_product: { name, sku }|null` key to a `embp_rows` row
	 * so any endpoint returning one (row listings, confirm/reject/assign)
	 * can show the matched product's name, not just its id. Shared here
	 * (rather than duplicated per controller) so every action stays
	 * consistent -- JobsController and RowsController both return rows. A
	 * variation's `name` is built as "Parent → attributes" (SPEC/T-307's
	 * "Producto → Variación") rather than `WC_Product_Variation::get_name()`,
	 * which formats its own way and doesn't isolate the parent's name.
	 *
	 * @param array $row A row as returned by Core\Rows.
	 */
	protected function with_matched_product( array $row ): array {
		$row['matched_product'] = null;

		if ( null === $row['match_product_id'] || ! function_exists( 'wc_get_product' ) ) {
			return $row;
		}

		if ( null !== $row['match_variation_id'] ) {
			$variation = wc_get_product( $row['match_variation_id'] );
			$parent    = wc_get_product( $row['match_product_id'] );

			if ( $variation instanceof \WC_Product_Variation && $parent instanceof \WC_Product ) {
				$row['matched_product'] = array(
					'name' => $parent->get_name() . ' → ' . $variation->get_attribute_summary(),
					'sku'  => $variation->get_sku(),
				);
			}

			return $row;
		}

		$product = wc_get_product( $row['match_product_id'] );

		if ( $product instanceof \WC_Product ) {
			$row['matched_product'] = array(
				'name' => $product->get_name(),
				'sku'  => $product->get_sku(),
			);
		}

		return $row;
	}
}
