<?php

namespace WarrantyCart\REST;

use WP_Error;
use WP_REST_Controller;
use WP_REST_Request;
use WP_REST_Response;

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

/**
 * Base REST API Controller
 *
 * Provides common functionality for all REST API controllers.
 */
abstract class Controller extends WP_REST_Controller {

	/**
	 * Namespace for the REST API
	 *
	 * @var string
	 */
	protected $namespace = 'warranty-cart/v1';

	/**
	 * REST base for the controller
	 *
	 * @var string
	 */
	protected $rest_base = '';

	/**
	 * Check if user has admin permission
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return bool|WP_Error
	 */
	public function check_admin_permission( WP_REST_Request $request ) {
		/**
		 * The capability an admin endpoint requires.
		 *
		 * This checked `manage_options` — full site administration — for every
		 * endpoint, including changing a claim from pending to approved. A shop
		 * manager could not work the claim queue, and a support agent had to be
		 * given plugin installation and user management to do their job.
		 *
		 * `manage_warranty_cart` already existed: it is granted on activation
		 * and is what `Admin\Menu` gates the menu on. Only the REST layer was
		 * asking for something else.
		 *
		 * @since 2.1.0
		 *
		 * @param string          $capability The capability required.
		 * @param WP_REST_Request $request    The request being authorised.
		 */
		$capability = \apply_filters( 'warranty_cart_rest_capability', 'manage_warranty_cart', $request );

		/*
		 * `manage_options` still passes, deliberately.
		 *
		 * An administrator is granted the capability on activation, but a site
		 * that upgraded without re-activating may not have run that yet — and
		 * locking an administrator out of their own plugin to enforce
		 * least-privilege on themselves would be a worse bug than the one being
		 * fixed. Administrators can grant themselves any capability anyway, so
		 * this concedes nothing.
		 */
		if ( \current_user_can( $capability ) || \current_user_can( 'manage_options' ) ) {
			return true;
		}

		return new WP_Error(
			'rest_forbidden',
			\__( 'You do not have permission to access this endpoint.', 'warranty-cart' ),
			array( 'status' => 403 )
		);
	}

	/**
	 * Allow public access (for cart operations)
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return bool
	 */
	public function check_public_permission( WP_REST_Request $request ) {
		return true;
	}

	/**
	 * Create success response
	 *
	 * Generates a standardized success response for REST API endpoints.
	 *
	 * @param mixed $data    Response data.
	 * @param int   $status  HTTP status code.
	 * @return WP_REST_Response REST response object.
	 */
	protected function success_response( $data = null, int $status = 200 ): WP_REST_Response {
		$response = array(
			'success' => true,
		);

		if ( null !== $data ) {
			$response['data'] = $data;
		}

		return new WP_REST_Response( $response, $status );
	}

	/**
	 * Hand a generated file back to the admin.
	 *
	 * The exports used to `header()` and `exit` from inside the REST callback.
	 * That skips everything the API does after a handler returns — the response
	 * filters, the `Content-Type`, the CORS headers — and it cannot be tested,
	 * because the test process exits with it. It also meant an export could not
	 * report a problem: once headers are out, an error is appended to a file the
	 * browser has already begun saving.
	 *
	 * Returning the document as data leaves it an ordinary REST response, which
	 * the admin turns into a download with a Blob. Errors stay errors, and the
	 * nonce travels in the header where apiFetch already puts it — the Reports
	 * button used to be a bare anchor to the REST URL, so it sent no nonce and
	 * downloaded a permission error saved under the report's name.
	 *
	 * @param string $filename What the browser should save it as.
	 * @param string $content  The document.
	 * @param string $mime     Its media type.
	 * @param string $notice   Something the admin should know about the file.
	 *
	 * @return WP_REST_Response
	 */
	protected function file_response( string $filename, string $content, string $mime = 'text/csv', string $notice = '' ): WP_REST_Response {
		return $this->success_response(
			array(
				'filename' => $filename,
				'mime'     => $mime,
				'content'  => $content,
				'notice'   => $notice,
			)
		);
	}

	/**
	 * Tell the admin when an export stopped short of the whole set.
	 *
	 * A file that silently holds the first ten thousand of forty thousand rows
	 * is worse than one that refuses, because it looks complete.
	 *
	 * @param int $total Rows matching the filters.
	 * @param int $limit Rows the export actually took.
	 *
	 * @return string Empty when the whole set fits.
	 */
	protected function truncation_notice( int $total, int $limit ): string {
		if ( $total <= $limit ) {
			return '';
		}

		return sprintf(
			/* translators: 1: rows in the file, 2: rows matching the filters. */
			__( 'This file holds the first %1$s of %2$s rows. Narrow the search or filter to export the rest.', 'warranty-cart' ),
			\number_format_i18n( $limit ),
			\number_format_i18n( $total )
		);
	}

	/**
	 * A filename stamped with the site's date, in the site's timezone.
	 *
	 * @param string $prefix Leading part of the name.
	 * @param string $ext    Extension, without the dot.
	 *
	 * @return string
	 */
	protected function export_filename( string $prefix, string $ext = 'csv' ): string {
		return sprintf( '%s-%s.%s', $prefix, \wp_date( 'Y-m-d' ), $ext );
	}

	/**
	 * Create error response
	 *
	 * Generates a standardized error response for REST API endpoints.
	 *
	 * @param string $message Error message.
	 * @param int    $status  HTTP status code.
	 * @param string $code    Error code.
	 * @return WP_Error REST error object.
	 */
	protected function error_response( string $message, int $status = 400, string $code = 'api_error' ): WP_Error {
		return new WP_Error(
			$code,
			$message,
			array( 'status' => $status )
		);
	}
}
