<?php
/**
 * Donations REST API
 *
 * Provides REST API endpoints for donation operations.
 *
 * @package AmbassadorHub\Donations
 */

namespace AmbassadorHub\Donations;

use WP_REST_Controller;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use AmbassadorHub\Donors\DonorsModule;
use AmbassadorHub\Fundraisers\FundraisersHelpers;
use AmbassadorHub\Fundraisers\FundraisersModule;
use AmbassadorHub\Spam\SpamShield;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare

/**
 * Donations API class.
 */
class DonationsApi extends WP_REST_Controller {
	/**
	 * API namespace.
	 *
	 * @var string
	 */
	protected $namespace = \AmbassadorHub\Api\ApiModule::NAMESPACE;

	/**
	 * Resource base.
	 *
	 * @var string
	 */
	protected $rest_base = 'donations';

	/**
	 * Initialize the API.
	 *
	 * @return void
	 */
	public static function init(): void {
		$instance = new self();
			add_action( 'ambh_register_rest_routes', array( $instance, 'register_routes' ) );
	}

	/**
	 * Constructor.
	 *
	 * @return void
	 */
	private function __construct() {
		// Private constructor for static init pattern.
	}

	/**
	 * Register REST API routes.
	 *
	 * @return void
	 */
	public function register_routes(): void {
		// GET /donations - List donations.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_donations' ),
				'permission_callback' => array( $this, 'get_donations_permissions_check' ),
				'args'                => array(
					'fundraiser_id' => array(
						'description' => __( 'Filter by fundraiser ID.', 'ambassadorhub' ),
						'type'        => 'integer',
						'required'    => false,
					),
					'status'        => array(
						'description' => __( 'Filter by status.', 'ambassadorhub' ),
						'type'        => 'string',
						'enum'        => array( 'pending', 'completed', 'failed' ),
						'required'    => false,
					),
					'limit'         => array(
						'description' => __( 'Maximum number of results.', 'ambassadorhub' ),
						'type'        => 'integer',
						'default'     => 50,
						'minimum'     => 1,
						'maximum'     => 100,
					),
					'offset'        => array(
						'description' => __( 'Number of results to skip.', 'ambassadorhub' ),
						'type'        => 'integer',
						'default'     => 0,
						'minimum'     => 0,
					),
				),
			)
		);

		// GET /donations/{id} - Get single donation.
		// PUT /donations/{id} - Update donation.
		// DELETE /donations/{id} - Delete donation.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_donation' ),
					'permission_callback' => array( $this, 'get_donation_permissions_check' ),
					'args'                => array(
						'id' => array(
							'description' => __( 'Donation ID.', 'ambassadorhub' ),
							'type'        => 'integer',
							'required'    => true,
						),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_donation' ),
					'permission_callback' => array( $this, 'update_donation_permissions_check' ),
					'args'                => $this->get_update_donation_params(),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_donation' ),
					'permission_callback' => array( $this, 'delete_donation_permissions_check' ),
					'args'                => array(
						'id' => array(
							'description' => __( 'Donation ID.', 'ambassadorhub' ),
							'type'        => 'integer',
							'required'    => true,
						),
					),
				),
			)
		);

		// POST /donations - Create donation.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'create_donation' ),
				'permission_callback' => '__return_true', // Public: unauthenticated donors submit donations; input is validated and sanitised in the callback.
				'args'                => $this->get_create_donation_params(),
			)
		);
	}

	/**
	 * Get donations list.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_donations( WP_REST_Request $request ) {
		global $wpdb;
		$table_name   = DonationsModule::get_table_name();
		$donors_table = DonorsModule::get_table_name();

		$fundraiser_id = $request->get_param( 'fundraiser_id' );
		$status        = $request->get_param( 'status' );
		$limit         = (int) $request->get_param( 'limit' );
		$offset        = (int) $request->get_param( 'offset' );

		// Build WHERE clauses — use `d.` alias to avoid ambiguity after the JOIN.
		$where_clauses = array();
		$where_values  = array();

		if ( $fundraiser_id ) {
			$where_clauses[] = 'd.fundraiser_id = %d';
			$where_values[]  = $fundraiser_id;
		}

		if ( $status ) {
			$where_clauses[] = 'd.status = %s';
			$where_values[]  = $status;
		}

		$where_sql = ! empty( $where_clauses ) ? 'WHERE ' . implode( ' AND ', $where_clauses ) : '';

		// $table_name and $donors_table are built from $wpdb->prefix (safe); $where_sql contains only literal tokens.
		$query          = "SELECT d.*, dn.first_name AS linked_donor_first_name, dn.last_name AS linked_donor_last_name,
		           dn.name AS linked_donor_name, dn.email AS linked_donor_email
		           FROM {$table_name} d
		           LEFT JOIN {$donors_table} dn ON dn.id = d.donor_id
		           {$where_sql}
		           ORDER BY d.created_at DESC LIMIT %d OFFSET %d";
		$where_values[] = $limit;
		$where_values[] = $offset;

		$donations_raw = $wpdb->get_results( $wpdb->prepare( $query, $where_values ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		$donations = array();
		foreach ( $donations_raw as $donation ) {
			$fundraiser = get_post( $donation->fundraiser_id );

			$donations[] = array(
				'id'               => (int) $donation->id,
				'fundraiser_id'    => (int) $donation->fundraiser_id,
				'fundraiser'       => $fundraiser ? array(
					'id'    => $fundraiser->ID,
					'title' => $fundraiser->post_title,
				) : null,
				'donor_id'         => (int) $donation->donor_id,
				'donor_name'       => $donation->linked_donor_name,
				'donor_first_name' => $donation->linked_donor_first_name,
				'donor_last_name'  => $donation->linked_donor_last_name,
				'donor_email'      => $donation->linked_donor_email,
				'is_anonymous'     => (int) $donation->is_anonymous,
				'amount'           => floatval( $donation->amount ),
				'currency'         => $donation->currency,
				'status'           => $donation->status,
				'transaction_id'   => $donation->transaction_id,
				'message'          => $donation->message,
				'notes'            => $donation->notes,
				'created_at'       => $donation->created_at,
				'updated_at'       => $donation->updated_at,
			);
		}

		// Get total count for pagination.
		// $table_name, $donors_table, $where_sql built from safe sources — see above.
		$count_query  = "SELECT COUNT(*) FROM {$table_name} d LEFT JOIN {$donors_table} dn ON dn.id = d.donor_id {$where_sql}";
		$count_values = array_slice( $where_values, 0, -2 ); // Remove limit and offset.
		$total        = (int) $wpdb->get_var(
			! empty( $count_values ) ? $wpdb->prepare( $count_query, $count_values ) : $count_query // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		);

		$response = new WP_REST_Response( $donations, 200 );
		$response->header( 'X-Total-Count', $total );
		$response->header( 'X-Total-Pages', ceil( $total / $limit ) );

		return $response;
	}

	/**
	 * Get single donation.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_donation( WP_REST_Request $request ) {
		$donation_id = (int) $request->get_param( 'id' );
		$donation    = DonationsModule::get( $donation_id );

		if ( ! $donation ) {
			return new WP_Error(
				'donation_not_found',
				__( 'Donation not found.', 'ambassadorhub' ),
				array( 'status' => 404 )
			);
		}

		$fundraiser = get_post( $donation->fundraiser_id );

		$data = array(
			'id'               => (int) $donation->id,
			'fundraiser_id'    => (int) $donation->fundraiser_id,
			'fundraiser'       => $fundraiser ? array(
				'id'    => $fundraiser->ID,
				'title' => $fundraiser->post_title,
			) : null,
			'donor_id'         => (int) $donation->donor_id,
			'donor_name'       => $donation->linked_donor_name,
			'donor_first_name' => $donation->linked_donor_first_name,
			'donor_last_name'  => $donation->linked_donor_last_name,
			'donor_email'      => $donation->linked_donor_email,
			'is_anonymous'     => (int) $donation->is_anonymous,
			'amount'           => floatval( $donation->amount ),
			'currency'         => $donation->currency,
			'status'           => $donation->status,
			'transaction_id'   => $donation->transaction_id,
			'message'          => $donation->message,
			'notes'            => $donation->notes,
			'created_at'       => $donation->created_at,
			'updated_at'       => $donation->updated_at,
		);

		return new WP_REST_Response( $data, 200 );
	}

	/**
	 * Create donation.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function create_donation( WP_REST_Request $request ) {
		$fundraiser_id = (int) $request->get_param( 'fundraiser_id' );
		$fundraiser    = get_post( $fundraiser_id );

		if ( ! $fundraiser || FundraisersModule::POST_TYPE !== $fundraiser->post_type ) {
			return new WP_Error(
				'invalid_fundraiser',
				__( 'Invalid fundraiser.', 'ambassadorhub' ),
				array( 'status' => 400 )
			);
		}

		if ( ! FundraisersHelpers::fundraiser_is_open( $fundraiser_id ) ) {
			return new WP_Error(
				'fundraiser_closed',
				__( 'This fundraiser is no longer accepting donations.', 'ambassadorhub' ),
				array( 'status' => 403 )
			);
		}

		$currency_param = $request->get_param( 'currency' );
		$message_param  = $request->get_param( 'message' );
		$donation_data  = array(
			'fundraiser_id'    => $fundraiser_id,
			'donor_first_name' => sanitize_text_field( $request->get_param( 'donor_first_name' ) ),
			'donor_last_name'  => sanitize_text_field( $request->get_param( 'donor_last_name' ) ),
			'donor_email'      => sanitize_email( $request->get_param( 'donor_email' ) ),
			'amount'           => floatval( $request->get_param( 'amount' ) ),
			'currency'         => sanitize_text_field( $currency_param ? $currency_param : 'EUR' ),
			// Always created as 'pending', regardless of what the request sends —
			// this route is publicly accessible (unauthenticated donors), so a
			// client-supplied 'status' or 'transaction_id' would let anyone
			// fabricate an already-"completed" donation with no payment ever
			// having happened. Only the PSP webhook handlers (which verify the
			// payment out-of-band) are allowed to advance status/transaction_id
			// after creation — see DonationsModule::update().
			'status'           => 'pending',
			'message'          => sanitize_textarea_field( $message_param ? $message_param : '' ),
		);

		if ( ! SpamShield::throttle_by_ip( 'donation', SpamShield::donation_rate_limit(), 10 * MINUTE_IN_SECONDS )
			|| ! SpamShield::throttle_by_email( 'donation', $donation_data['donor_email'], 5, HOUR_IN_SECONDS ) ) {
			return new WP_Error(
				'rate_limited',
				__( 'Too many donation submissions. Please try again later.', 'ambassadorhub' ),
				array( 'status' => 429 )
			);
		}

		if ( $request->has_param( 'notes' ) ) {
			$donation_data['notes'] = sanitize_textarea_field( $request->get_param( 'notes' ) );
		}

		$donation_id = DonationsModule::create( $donation_data );

		if ( ! $donation_id ) {
			return new WP_Error(
				'creation_failed',
				__( 'Error creating donation.', 'ambassadorhub' ),
				array( 'status' => 500 )
			);
		}

		/**
		 * Fires after a donation is created via the REST API.
		 *
		 * Extensions (e.g. custom fields add-on) hook here to persist additional
		 * data tied to the new donation, using parameters injected via the
		 * `ambh_donation_create_params` filter.
		 *
		 * @param int             $donation_id The new donation ID.
		 * @param WP_REST_Request $request     The REST request that created the donation.
		 */
		do_action( 'ambh_donation_rest_saved', $donation_id, $request );

		$donation = DonationsModule::get( $donation_id );

		return new WP_REST_Response(
			array(
				'id'      => (int) $donation->id,
				'message' => __( 'Donation successfully created.', 'ambassadorhub' ),
			),
			201
		);
	}

	/**
	 * Update donation.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function update_donation( WP_REST_Request $request ) {
		$donation_id = (int) $request->get_param( 'id' );
		$donation    = DonationsModule::get( $donation_id );

		if ( ! $donation ) {
			return new WP_Error(
				'donation_not_found',
				__( 'Donation not found.', 'ambassadorhub' ),
				array( 'status' => 404 )
			);
		}

		$update_data = array();

		// Only update provided fields.
		if ( $request->has_param( 'amount' ) ) {
			$update_data['amount'] = floatval( $request->get_param( 'amount' ) );
		}

		if ( $request->has_param( 'status' ) ) {
			$update_data['status'] = sanitize_text_field( $request->get_param( 'status' ) );
		}

		if ( $request->has_param( 'transaction_id' ) ) {
			$update_data['transaction_id'] = sanitize_text_field( $request->get_param( 'transaction_id' ) );
		}

		if ( $request->has_param( 'message' ) ) {
			$update_data['message'] = sanitize_textarea_field( $request->get_param( 'message' ) );
		}

		if ( $request->has_param( 'notes' ) ) {
			$update_data['notes'] = sanitize_textarea_field( $request->get_param( 'notes' ) );
		}

		$result = DonationsModule::update( $donation_id, $update_data );

		if ( ! $result ) {
			return new WP_Error(
				'update_failed',
				__( 'Error updating donation.', 'ambassadorhub' ),
				array( 'status' => 500 )
			);
		}

		return $this->get_donation( $request );
	}

	/**
	 * Delete donation.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function delete_donation( WP_REST_Request $request ) {
		$donation_id = (int) $request->get_param( 'id' );
		$donation    = DonationsModule::get( $donation_id );

		if ( ! $donation ) {
			return new WP_Error(
				'donation_not_found',
				__( 'Donation not found.', 'ambassadorhub' ),
				array( 'status' => 404 )
			);
		}

		$result = DonationsModule::delete( $donation_id );

		if ( ! $result ) {
			return new WP_Error(
				'delete_failed',
				__( 'Error deleting donation.', 'ambassadorhub' ),
				array( 'status' => 500 )
			);
		}

		return new WP_REST_Response(
			array(
				'deleted' => true,
				'message' => __( 'Donation successfully deleted.', 'ambassadorhub' ),
			),
			200
		);
	}

	/**
	 * Check permissions for getting donations list.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return bool|WP_Error
	 */
	public function get_donations_permissions_check( WP_REST_Request $request ) {
		// Require the donor-PII capability (granted to administrators via DonationsCapabilities).
		if ( ! current_user_can( 'ambh_view_donor_pii' ) ) {
			return new WP_Error(
				'forbidden',
				__( "You don't have permission to view donations.", 'ambassadorhub' ),
				array( 'status' => 403 )
			);
		}

		return true;
	}

	/**
	 * Check permissions for getting single donation.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return bool|WP_Error
	 */
	public function get_donation_permissions_check( WP_REST_Request $request ) {
		// Require the donor-PII capability (granted to administrators via DonationsCapabilities).
		if ( ! current_user_can( 'ambh_view_donor_pii' ) ) {
			return new WP_Error(
				'forbidden',
				__( "You don't have permission to view this donation.", 'ambassadorhub' ),
				array( 'status' => 403 )
			);
		}

		return true;
	}

	/**
	 * Check permissions for updating donation.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return bool|WP_Error
	 */
	public function update_donation_permissions_check( WP_REST_Request $request ) {
		// Require admin capability to update donations.
		if ( ! current_user_can( 'manage_options' ) ) {
			return new WP_Error(
				'forbidden',
				__( "You don't have permission to edit donations.", 'ambassadorhub' ),
				array( 'status' => 403 )
			);
		}

		return true;
	}

	/**
	 * Check permissions for deleting donation.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return bool|WP_Error
	 */
	public function delete_donation_permissions_check( WP_REST_Request $request ) {
		// Require admin capability to delete donations.
		if ( ! current_user_can( 'manage_options' ) ) {
			return new WP_Error(
				'forbidden',
				__( "You don't have permission to delete donations.", 'ambassadorhub' ),
				array( 'status' => 403 )
			);
		}

		return true;
	}

	/**
	 * Get parameters for creating donation.
	 *
	 * @return array
	 */
	protected function get_create_donation_params(): array {
		$params = array(
			'fundraiser_id'    => array(
				'description' => __( 'Fundraiser ID.', 'ambassadorhub' ),
				'type'        => 'integer',
				'required'    => true,
			),
			'donor_first_name' => array(
				'description' => __( 'Donor first name.', 'ambassadorhub' ),
				'type'        => 'string',
				'required'    => true,
			),
			'donor_last_name'  => array(
				'description' => __( 'Donor last name.', 'ambassadorhub' ),
				'type'        => 'string',
				'required'    => true,
			),
			'donor_email'      => array(
				'description' => __( 'Donor email address.', 'ambassadorhub' ),
				'type'        => 'string',
				'format'      => 'email',
				'required'    => true,
			),
			'amount'           => array(
				'description' => __( 'Donation amount.', 'ambassadorhub' ),
				'type'        => 'number',
				'required'    => true,
				'minimum'     => 0.01,
			),
			'currency'         => array(
				'description' => __( 'Currency code.', 'ambassadorhub' ),
				'type'        => 'string',
				'default'     => 'EUR',
				'required'    => false,
			),
			// 'status' and 'transaction_id' are deliberately not accepted as
			// input here — this route has no permission_callback beyond
			// '__return_true' (any site visitor may call it), and both fields
			// are set authoritatively by the plugin: status always starts as
			// 'pending' (see create_donation()), and transaction_id is only
			// ever written by a verified PSP webhook handler after payment.
			'message'          => array(
				'description' => __( 'Public visible message.', 'ambassadorhub' ),
				'type'        => 'string',
				'required'    => false,
				'default'     => '',
			),
			'notes'            => array(
				'description' => __( 'Internal notes.', 'ambassadorhub' ),
				'type'        => 'string',
				'required'    => false,
			),
		);

		/**
		 * Filters the REST parameter schema for donation creation.
		 *
		 * Allows extensions (e.g. custom fields add-on) to inject additional
		 * parameters accepted by the donation creation endpoint. The hardcoded
		 * donor/amount fields above remain authoritative; extensions should
		 * use the `ambh_donation_rest_saved` action to persist the values.
		 *
		 * @param array $params Parameter schema keyed by parameter name.
		 */
		return apply_filters( 'ambh_donation_create_params', $params );
	}

	/**
	 * Get parameters for updating donation.
	 *
	 * @return array
	 */
	protected function get_update_donation_params(): array {
		return array(
			'id'             => array(
				'description' => __( 'Donation ID.', 'ambassadorhub' ),
				'type'        => 'integer',
				'required'    => true,
			),
			'amount'         => array(
				'description' => __( 'Donation amount.', 'ambassadorhub' ),
				'type'        => 'number',
				'required'    => false,
				'minimum'     => 0.01,
			),
			'status'         => array(
				'description' => __( 'Donation status.', 'ambassadorhub' ),
				'type'        => 'string',
				'enum'        => array( 'pending', 'completed', 'failed' ),
				'required'    => false,
			),
			'transaction_id' => array(
				'description' => __( 'Transaction ID.', 'ambassadorhub' ),
				'type'        => 'string',
				'required'    => false,
			),
			'message'        => array(
				'description' => __( 'Public visible message.', 'ambassadorhub' ),
				'type'        => 'string',
				'required'    => false,
			),
			'notes'          => array(
				'description' => __( 'Internal notes.', 'ambassadorhub' ),
				'type'        => 'string',
				'required'    => false,
			),
		);
	}
}
