<?php
/**
 * Donations Module
 *
 * Handles donation data stored in custom table -> ambh_donations.
 *
 * @package AmbassadorHub\Donations
 */

namespace AmbassadorHub\Donations;

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

use AmbassadorHub\Donors\DonorsModule;
use AmbassadorHub\Donations\DonationsCapabilities;

// 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 Module class.
 */
class DonationsModule {
	/**
	 * Initialize the module.
	 *
	 * @return void
	 */
	public static function init(): void {
		// Register donor PII capability for the administrator role.
		DonationsCapabilities::register();

		// Register database migrations.
		MigrationRunner::init();

		// Initialize REST API.
		DonationsApi::init();

		// Admin menu.
		Admin\Menu::init();

		/**
		 * Fires when donations module is initialized.
		 *
		 * @since 0.1.0
		 */
		do_action( 'ambh_donations_module_init' );
	}

	/**
	 * Count total donations, optionally filtered by status.
	 *
	 * @param string $status Optional status filter (e.g. 'completed'). Empty = all.
	 * @return int
	 */
	public static function count( string $status = '' ): int {
		global $wpdb;
		$table = self::get_table_name();
		if ( '' !== $status ) {
			return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE status = %s", $status ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		}
		return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	}

	/**
	 * Get donations table name.
	 *
	 * @return string
	 */
	public static function get_table_name(): string {
		global $wpdb;
		return $wpdb->prefix . 'ambh_donations';
	}

	/**
	 * Insert a new donation.
	 *
	 * @param array $data Donation data.
	 * @return int|false Donation ID or false on failure.
	 */
	public static function create( array $data ) {
		global $wpdb;

		// Extract donor identity fields — used to find or create the donor profile
		// but NOT stored in the donations table (PII lives exclusively in wp_ambh_donors).
		$first_name = (string) ( $data['donor_first_name'] ?? $data['first_name'] ?? '' );
		$last_name  = (string) ( $data['donor_last_name'] ?? $data['last_name'] ?? '' );
		$email      = (string) ( $data['donor_email'] ?? $data['email'] ?? '' );

		$donor_id = isset( $data['donor_id'] ) && $data['donor_id'] ? (int) $data['donor_id'] : null;

		// Auto-link to a donor profile when no donor_id is supplied but personal data is present.
		if ( ! $donor_id && ( $email || $first_name ) ) {
			$found = DonorsModule::find_or_create(
				array(
					'first_name'  => $first_name,
					'last_name'   => $last_name,
					'email'       => $email,
					'street'      => $data['donor_street'] ?? $data['street'] ?? null,
					'city'        => $data['donor_city'] ?? $data['city'] ?? null,
					'postal_code' => $data['donor_postal_code'] ?? $data['postal_code'] ?? null,
					'state'       => $data['donor_state'] ?? $data['state'] ?? null,
					'country'     => $data['donor_country'] ?? $data['country'] ?? null,
				)
			);
			if ( $found ) {
				$donor_id = (int) $found;
			}
		}

		// Build insert data in a fixed column order.
		// PII columns are intentionally excluded — they live in wp_ambh_donors.
		$insert_data = array(
			'fundraiser_id'  => (int) ( $data['fundraiser_id'] ?? 0 ),
			'donor_id'       => $donor_id,
			'amount'         => (float) ( $data['amount'] ?? 0.00 ),
			'currency'       => sanitize_key( $data['currency'] ?? 'EUR' ),
			'status'         => sanitize_key( $data['status'] ?? 'pending' ),
			'transaction_id' => ( isset( $data['transaction_id'] ) && '' !== $data['transaction_id'] ) ? (string) $data['transaction_id'] : null,
			'notes'          => ( isset( $data['notes'] ) && null !== $data['notes'] ) ? (string) $data['notes'] : null,
			'message'        => ( isset( $data['message'] ) && null !== $data['message'] ) ? (string) $data['message'] : null,
			'is_anonymous'   => (int) ( $data['is_anonymous'] ?? 0 ),
			'created_at'     => $data['created_at'] ?? current_time( 'mysql' ),
			'updated_at'     => $data['updated_at'] ?? current_time( 'mysql' ),
		);

		$result = $wpdb->insert(
			self::get_table_name(),
			$insert_data,
			array( '%d', '%d', '%f', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s' )
		);

		if ( ! $result ) {
			return false;
		}

		$donation_id = $wpdb->insert_id;

		/**
		 * Fires after a donation is created.
		 *
		 * @since 0.3.0
		 *
		 * @param int   $donation_id   The created donation ID.
		 * @param array $data          The donation data.
		 */
		do_action( 'ambh_donation_created', $donation_id, $data );

		return $donation_id;
	}

	/**
	 * Get a donation by ID.
	 *
	 * @param int $id Donation ID.
	 * @return object|null Donation object or null if not found.
	 */
	public static function get( int $id ) {
		global $wpdb;
		$table        = self::get_table_name();
		$donors_table = DonorsModule::get_table_name();
		return $wpdb->get_row(
			$wpdb->prepare(
				"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} d
				 LEFT JOIN {$donors_table} dn ON dn.id = d.donor_id
				 WHERE d.id = %d",
				$id
			)
		);
	}

	/**
	 * Update a donation.
	 *
	 * @param int   $id   Donation ID.
	 * @param array $data Data to update.
	 * @return bool True on success, false on failure.
	 */
	public static function update( int $id, array $data ): bool {
		global $wpdb;

		// Get the donation before update to access fundraiser_id.
		$donation = self::get( $id );

		if ( ! $donation ) {
			return false;
		}

		$data['updated_at'] = current_time( 'mysql' );

		$result = (bool) $wpdb->update(
			self::get_table_name(),
			$data,
			array( 'id' => $id ),
			null,
			array( '%d' )
		);

		if ( $result ) {
			/**
			 * Fires after a donation is updated.
			 *
			 * @since 0.3.0
			 *
			 * @param int    $id           The donation ID.
			 * @param array  $data         The updated data.
			 * @param object $donation     The donation object before update.
			 */
			do_action( 'ambh_donation_updated', $id, $data, $donation );
		}

		return $result;
	}

	/**
	 * Delete a donation.
	 *
	 * @param int $id Donation ID.
	 * @return bool True on success, false on failure.
	 */
	public static function delete( int $id ): bool {
		global $wpdb;

		// Get the donation before delete to access fundraiser_id.
		$donation = self::get( $id );

		if ( ! $donation ) {
			return false;
		}

		$result = (bool) $wpdb->delete(
			self::get_table_name(),
			array( 'id' => $id ),
			array( '%d' )
		);

		if ( $result ) {
			/**
			 * Fires after a donation is deleted.
			 *
			 * @since 0.3.0
			 *
			 * @param int    $id       The deleted donation ID.
			 * @param object $donation The donation object before deletion.
			 */
			do_action( 'ambh_donation_deleted', $id, $donation );
		}

		return $result;
	}

	/**
	 * Get donations by fundraiser ID.
	 *
	 * @param array $fundraiser_id Fundraiser IDs.
	 * @param string|null $status Optional status filter. Empty = all.
	 *
	 * @return array Array of donation objects.
	 */
	public static function get_by_fundraisers( array $fundraiser_ids, string|null $status = null ): array {
		global $wpdb;
		$table        = self::get_table_name();
		$donors_table = DonorsModule::get_table_name();
		$placeholders = implode( ', ', array_fill( 0, count( $fundraiser_ids ), '%d' ) );

		return $wpdb->get_results(
			$wpdb->prepare(
				"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} d
				 LEFT JOIN {$donors_table} dn ON dn.id = d.donor_id
				 WHERE fundraiser_id IN ({$placeholders})
				 " . ( $status ? $wpdb->prepare( 'AND d.status = %s', $status ) : '' ) . '
				 ORDER BY d.created_at DESC',
				$fundraiser_ids
			)
		);
	}

	/**
	 * Get recent donations.
	 *
	 * @param int $days Number of days to look back.
	 * @return array Array of donation objects.
	 */
	public static function get_recent( int $days = 7 ): array {
		global $wpdb;
		$days_ago     = gmdate( 'Y-m-d H:i:s', time() - ( $days * DAY_IN_SECONDS ) );
		$table        = self::get_table_name();
		$donors_table = DonorsModule::get_table_name();

		return $wpdb->get_results(
			$wpdb->prepare(
				"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} d
				 LEFT JOIN {$donors_table} dn ON dn.id = d.donor_id
				 WHERE d.created_at >= %s
				 ORDER BY d.created_at DESC",
				$days_ago
			)
		);
	}

	/**
	 * Get the display name for a donation, respecting the linked donor and anonymity.
	 *
	 * @param object $donation Donation object (may include linked_donor_* columns from a JOIN).
	 * @return string Display name.
	 */
	public static function get_donor_name_for_display( object $donation ): string {
		// is_anonymous is a per-donation display flag stored on the donation row itself.
		if ( ! empty( $donation->is_anonymous ) ) {
			return __( 'Anonymous', 'ambassadorhub' );
		}
		if ( ! empty( $donation->linked_donor_name ) ) {
			return $donation->linked_donor_name;
		}
		return __( 'Anonymous', 'ambassadorhub' );
	}

	/**
	 * Get the display email for a donation, preferring the linked donor record.
	 *
	 * @param object $donation Donation object.
	 * @return string Email address or empty string.
	 */
	public static function get_donor_email_for_display( object $donation ): string {
		if ( ! empty( $donation->is_anonymous ) ) {
			return '';
		}
		return $donation->linked_donor_email ?? '';
	}

	/**
	 * Resolve a donation from an automation rule instance.
	 *
	 * Tries (in order):
	 *  1. context_data['donation_id'] stored on the rule instance.
	 *  2. The most recent donation on the rule instance's target fundraiser.
	 *
	 * Called by PSP connector Actions classes so the logic lives once, here,
	 * rather than being duplicated per connector.
	 *
	 * @param int $rule_instance_id Rule instance ID.
	 * @return object|null Donation row or null if not resolvable.
	 */
	public static function get_from_rule_instance( int $rule_instance_id ): ?object {
		if ( ! class_exists( \AmbassadorHub\Automations\RuleInstanceRepository::class ) ) {
			return null;
		}

		$rule_instance = \AmbassadorHub\Automations\RuleInstanceRepository::get_by_id( $rule_instance_id );
		if ( ! $rule_instance ) {
			return null;
		}

		// Donation-level triggers (donation_initiated, donation_completed, donation_payment_failed)
		// use entity_type = 'donation' — target_id is the donation ID itself, direct and race-condition-free.
		if ( 'donation' === $rule_instance->target_type ) {
			return self::get( $rule_instance->target_id );
		}

		// Fundraiser-level triggers (donation_received, donation_pending, donation_failed)
		// use entity_type = 'fundraiser'. Guard to avoid nonsensical queries for
		// other entity types (e.g. ambassador_fundraiser_donation uses 'user').
		if ( 'fundraiser' !== $rule_instance->target_type ) {
			return null;
		}

		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; no WP API equivalent. Caching omitted: transactional lookup must always reflect the current database state.
		return $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}ambh_donations
				 WHERE fundraiser_id = %d
				 ORDER BY created_at DESC
				 LIMIT 1",
				$rule_instance->target_id
			)
		);
	}
}
