<?php
/**
 * Donors Module
 *
 * Handles donor profile data stored in the custom ambh_donors table.
 * Donors are the people behind donations — personal info is stored here,
 * while the financial transaction lives in ambh_donations with a donor_id FK.
 *
 * @package AmbassadorHub\Donors
 */

namespace AmbassadorHub\Donors;

// 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, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber

/**
 * Donors Module class.
 */
class DonorsModule {

	/**
	 * Initialize the module.
	 *
	 * @return void
	 */
	public static function init(): void {
		// Register database migrations.
		MigrationRunner::init();

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

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

		/**
		 * Fires when donors module is initialized.
		 *
		 * @since 0.4.0
		 */
		do_action( 'ambh_donors_module_init' );
	}

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

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

		$defaults = array(
			'first_name' => '',
			'last_name'  => '',
			'name'       => '',
			'email'      => '',
			'created_at' => current_time( 'mysql' ),
			'updated_at' => current_time( 'mysql' ),
		);

		$data = wp_parse_args( $data, $defaults );

		// Auto-compose full name from first + last when not explicitly supplied.
		if ( empty( $data['name'] ) ) {
			$data['name'] = trim( $data['first_name'] . ' ' . $data['last_name'] );
		}

		$insert_data = array(
			'first_name'  => $data['first_name'],
			'last_name'   => $data['last_name'],
			'name'        => $data['name'],
			'email'       => $data['email'],
			'street'      => $data['street'] ?? null,
			'city'        => $data['city'] ?? null,
			'postal_code' => $data['postal_code'] ?? null,
			'state'       => $data['state'] ?? null,
			'country'     => $data['country'] ?? null,
			'created_at'  => $data['created_at'],
			'updated_at'  => $data['updated_at'],
		);

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

		if ( ! $result ) {
			return false;
		}

		$donor_id = $wpdb->insert_id;

		/**
		 * Fires after a donor is created.
		 *
		 * @since 0.4.0
		 *
		 * @param int   $donor_id The created donor ID.
		 * @param array $data     The donor data.
		 */
		do_action( 'ambh_donor_created', $donor_id, $data );

		return $donor_id;
	}

	/**
	 * Get a donor by ID.
	 *
	 * @param int $id Donor ID.
	 * @return object|null Donor object or null if not found.
	 */
	public static function get( int $id ) {
		global $wpdb;
		return $wpdb->get_row(
			$wpdb->prepare( 'SELECT * FROM ' . self::get_table_name() . ' WHERE id = %d', $id )
		);
	}

	/**
	 * Get a donor by email address.
	 *
	 * @param string $email Donor email.
	 * @return object|null Donor object or null if not found.
	 */
	public static function get_by_email( string $email ) {
		global $wpdb;
		return $wpdb->get_row(
			$wpdb->prepare( 'SELECT * FROM ' . self::get_table_name() . ' WHERE email = %s LIMIT 1', $email )
		);
	}

	/**
	 * Find an existing donor by email, or create a new one.
	 *
	 * Used by the payment flow to de-duplicate donors.
	 *
	 * @param array $data Donor data (must include 'email').
	 * @return int|false Donor ID or false on failure.
	 */
	public static function find_or_create( array $data ) {
		if ( ! empty( $data['email'] ) ) {
			$existing = self::get_by_email( $data['email'] );
			if ( $existing ) {
				// Update any address fields that are newly provided.
				$address_update = array_filter(
					array(
						'street'      => $data['street'] ?? null,
						'city'        => $data['city'] ?? null,
						'postal_code' => $data['postal_code'] ?? null,
						'state'       => $data['state'] ?? null,
						'country'     => $data['country'] ?? null,
					),
					fn( $v ) => null !== $v && '' !== $v
				);

				if ( ! empty( $address_update ) ) {
					self::update( (int) $existing->id, $address_update );
				}

					return (int) $existing->id;
			}
		}

		return self::create( $data );
	}

	/**
	 * Update a donor.
	 *
	 * @param int   $id   Donor 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;

		$donor = self::get( $id );

		if ( ! $donor ) {
			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 donor is updated.
			 *
			 * @since 0.4.0
			 *
			 * @param int    $id    The donor ID.
			 * @param array  $data  The updated data.
			 * @param object $donor The donor object before update.
			 */
			do_action( 'ambh_donor_updated', $id, $data, $donor );
		}

		return $result;
	}

	/**
	 * Anonymize a donor by overwriting PII with placeholder values.
	 *
	 * Preserves the donor row (and all donation FK references) while
	 * removing personally identifiable information to comply with GDPR
	 * erasure requests.
	 *
	 * @param int $id Donor ID.
	 * @return bool True on success, false on failure.
	 */
	public static function anonymize( int $id ): bool {

		$donor = self::get( $id );

		if ( ! $donor ) {
			return false;
		}

		$anon_first = __( 'Anonymous', 'ambassadorhub' );
		$anon_last  = __( 'Anonymized', 'ambassadorhub' );

		$result = self::update(
			$id,
			array(
				'first_name'   => $anon_first,
				'last_name'    => $anon_last,
				'name'         => $anon_first . ' ' . $anon_last,
				'email'        => "anonymized_{$id}@removed.invalid",
				'is_anonymous' => 1,
				'street'       => null,
				'city'         => null,
				'postal_code'  => null,
				'state'        => null,
				'country'      => null,
			)
		);

		if ( $result ) {
			/**
			 * Fires after a donor is anonymized.
			 *
			 * @since 0.5.0
			 *
			 * @param int    $id    The anonymized donor ID.
			 * @param object $donor The donor object before anonymization.
			 */
			do_action( 'ambh_donor_anonymized', $id, $donor );
		}

		return $result;
	}

	/**
	 * Get all donors with optional filtering and pagination.
	 *
	 * @param array $args Query arguments (limit, offset, search, orderby, order).
	 * @return array Array of donor objects.
	 */
	public static function get_all( array $args = array() ): array {
		global $wpdb;

		$defaults = array(
			'limit'        => 25,
			'offset'       => 0,
			'search'       => '',
			'orderby'      => 'created_at',
			'order'        => 'DESC',
			'is_anonymous' => null,
		);

		$args   = wp_parse_args( $args, $defaults );
		$table  = esc_sql( self::get_table_name() );
		$where  = '1=1';
		$values = array();

		if ( ! empty( $args['search'] ) ) {
			$search   = '%' . $wpdb->esc_like( $args['search'] ) . '%';
			$where   .= ' AND (name LIKE %s OR email LIKE %s)';
			$values[] = $search;
			$values[] = $search;
		}

		if ( null !== $args['is_anonymous'] ) {
			$where   .= ' AND is_anonymous = %d';
			$values[] = $args['is_anonymous'] ? 1 : 0;
		}

		$orderby         = in_array( $args['orderby'], array( 'id', 'name', 'email', 'created_at', 'donation_count' ), true ) ? $args['orderby'] : 'created_at';
		$order           = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC';
		$donations_table = esc_sql( $wpdb->prefix . 'ambh_donations' );

		$values[] = (int) $args['limit'];
		$values[] = (int) $args['offset'];

		return $wpdb->get_results(
			$wpdb->prepare(
				"SELECT *, (SELECT COUNT(*) FROM {$donations_table} WHERE donor_id = {$table}.id) AS donation_count FROM {$table} WHERE {$where} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d",
				...$values
			)
		);
	}

	/**
	 * Count donors with optional search and anonymisation filter.
	 *
	 * @param string    $search       Search term.
	 * @param bool|null $is_anonymous Null = no filter, true = anonymised only, false = non-anonymised only.
	 * @return int Total donor count.
	 */
	public static function count( string $search = '', ?bool $is_anonymous = null ): int {
		global $wpdb;

		$table       = esc_sql( self::get_table_name() );
		$where_parts = array();
		$values      = array();

		if ( ! empty( $search ) ) {
			$like          = '%' . $wpdb->esc_like( $search ) . '%';
			$where_parts[] = '(name LIKE %s OR email LIKE %s)';
			$values[]      = $like;
			$values[]      = $like;
		}

		if ( null !== $is_anonymous ) {
			$where_parts[] = 'is_anonymous = %d';
			$values[]      = $is_anonymous ? 1 : 0;
		}

		if ( $where_parts ) {
			$where_sql = 'WHERE ' . implode( ' AND ', $where_parts );
			return (int) $wpdb->get_var(
				$wpdb->prepare( "SELECT COUNT(*) FROM {$table} {$where_sql}", ...$values )
			);
		}

		return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
	}

	/**
	 * Return tab counts (all / identified / anonymized) in a single aggregated query.
	 *
	 * @param string $search Optional search term.
	 * @return array{all: int, identified: int, anonymized: int}
	 */
	public static function count_tabs( string $search = '' ): array {
		global $wpdb;

		$table  = esc_sql( self::get_table_name() );
		$where  = '1=1';
		$values = array();

		if ( ! empty( $search ) ) {
			$like     = '%' . $wpdb->esc_like( $search ) . '%';
			$where   .= ' AND (name LIKE %s OR email LIKE %s)';
			$values[] = $like;
			$values[] = $like;
		}

		$sql = "SELECT
			COUNT(*) AS count_all,
			SUM(CASE WHEN is_anonymous = 0 THEN 1 ELSE 0 END) AS count_identified,
			SUM(CASE WHEN is_anonymous = 1 THEN 1 ELSE 0 END) AS count_anonymized
			FROM {$table} WHERE {$where}";

		$row = $values ? $wpdb->get_row( $wpdb->prepare( $sql, ...$values ) ) : $wpdb->get_row( $sql );

		return array(
			'all'        => (int) ( $row->count_all ?? 0 ),
			'identified' => (int) ( $row->count_identified ?? 0 ),
			'anonymized' => (int) ( $row->count_anonymized ?? 0 ),
		);
	}

	/**
	 * Count donations linked to a donor.
	 *
	 * @param int $donor_id Donor ID.
	 * @return int Number of donations.
	 */
	public static function count_donations( int $donor_id ): int {
		global $wpdb;
		$donations_table = $wpdb->prefix . 'ambh_donations';
		return (int) $wpdb->get_var(
			$wpdb->prepare( "SELECT COUNT(*) FROM {$donations_table} WHERE donor_id = %d", $donor_id )
		);
	}
}
