<?php
/**
 * Consent Manager API
 *
 * @package AmbassadorHub\Consent
 */

namespace AmbassadorHub\Consent;

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

final class ConsentManager {
	/**
	 * Get the consent records table name.
	 *
	 * @return string
	 */
	public static function get_table_name(): string {
		global $wpdb;
		return $wpdb->prefix . 'ambh_consent_records';
	}

	public static function grant(
		string $subject_type,
		int $subject_id,
		string $purpose_slug,
		string $channel,
		string $consent_text,
		string $source,
		string $scope_type = 'global',
		?int $scope_id = null
	): void {
		global $wpdb;
		$table       = esc_sql( self::get_table_name() );
		$proof_token = wp_generate_password( 32, false );
		$now         = current_time( 'mysql', true );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$wpdb->insert(
			$table,
			array(
				'subject_type' => $subject_type,
				'subject_id'   => $subject_id,
				'purpose_slug' => $purpose_slug,
				'channel'      => $channel,
				'scope_type'   => $scope_type,
				'scope_id'     => $scope_id,
				'granted'      => 1,
				'consent_text' => $consent_text,
				'source'       => $source,
				'proof_token'  => $proof_token,
				'consented_at' => $now,
				'withdrawn_at' => null,
				'withdrawn_by' => null,
			)
		);
		$record_id = $wpdb->insert_id;
		do_action( 'ambh_consent_granted', $record_id, $subject_type, $subject_id, $purpose_slug );
	}

	public static function has_consent( // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- $scope_id kept for API symmetry with grant()/withdraw() scope arguments; scope filtering not yet implemented here.
		string $subject_type,
		int $subject_id,
		string $purpose_slug,
		string $channel,
		string $scope_type = 'global',
		?int $scope_id = null
	): bool {
		global $wpdb;
		$table = esc_sql( self::get_table_name() );
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$granted = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT granted FROM $table WHERE subject_type = %s AND subject_id = %d AND purpose_slug = %s AND channel = %s AND scope_type = %s AND withdrawn_at IS NULL ORDER BY consented_at DESC LIMIT 1",
				$subject_type,
				$subject_id,
				$purpose_slug,
				$channel,
				$scope_type
			)
		);
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return 1 === (int) $granted;
	}

	public static function withdraw( int $record_id, string $initiated_by ): void {
		global $wpdb;
		$table = esc_sql( self::get_table_name() );
		$now   = current_time( 'mysql', true );
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$wpdb->update(
			$table,
			array(
				'withdrawn_at' => $now,
				'withdrawn_by' => $initiated_by,
			),
			array( 'id' => $record_id )
		);
		$row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table WHERE id = %d", $record_id ) );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		do_action( 'ambh_consent_withdrawn', $record_id, $row->subject_type ?? '', $row->subject_id ?? 0, $row->purpose_slug ?? '' );
	}

	/**
	 * Revoke all active consent records for a given subject.
	 *
	 * Fetches all non-withdrawn records for the subject and calls withdraw()
	 * on each, so that individual `ambh_consent_withdrawn` actions still fire.
	 *
	 * @param string $subject_type Subject type ('wp_user' | 'donor').
	 * @param int    $subject_id   Subject ID.
	 * @param string $initiated_by Who initiated the revocation ('admin' | 'user' | 'system').
	 * @return void
	 */
	public static function withdraw_all_for_subject( string $subject_type, int $subject_id, string $initiated_by ): void {
		global $wpdb;
		$table = esc_sql( self::get_table_name() );
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT id FROM {$table} WHERE subject_type = %s AND subject_id = %d AND withdrawn_at IS NULL",
				$subject_type,
				$subject_id
			)
		);
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		foreach ( $ids as $id ) {
			self::withdraw( (int) $id, $initiated_by );
		}
	}

	public static function get_export( string $subject_type, int $subject_id ): array {
		global $wpdb;
		$table = esc_sql( self::get_table_name() );
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM $table WHERE subject_type = %s AND subject_id = %d ORDER BY consented_at DESC",
				$subject_type,
				$subject_id
			),
			ARRAY_A
		);
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	}

	public static function get( int $record_id ): ?object {
		global $wpdb;
		$table = esc_sql( self::get_table_name() );
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table WHERE id = %d", $record_id ) );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	}

	/**
	 * Get all consent records with optional filtering and pagination.
	 *
	 * @param array $args Query arguments (limit, offset, search, orderby, order).
	 * @return object[]
	 */
	public static function get_all( array $args = array() ): array {
		global $wpdb;

		$defaults = array(
			'limit'        => 25,
			'offset'       => 0,
			'search'       => '',
			'orderby'      => 'consented_at',
			'order'        => 'DESC',
			'subject_type' => '',
			'subject_id'   => 0,
			'status'       => 'all',
			'purpose_slug' => '',
			'channel'      => '',
			'source'       => '',
		);

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

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

		if ( ! empty( $args['subject_type'] ) ) {
			$where   .= ' AND subject_type = %s';
			$values[] = $args['subject_type'];
		}

		if ( $args['subject_id'] > 0 ) {
			$where   .= ' AND subject_id = %d';
			$values[] = (int) $args['subject_id'];
		}

		if ( 'granted' === $args['status'] ) {
			$where .= ' AND withdrawn_at IS NULL';
		} elseif ( 'revoked' === $args['status'] ) {
			$where .= ' AND withdrawn_at IS NOT NULL';
		}

		if ( ! empty( $args['purpose_slug'] ) ) {
			$where   .= ' AND purpose_slug = %s';
			$values[] = $args['purpose_slug'];
		}

		if ( ! empty( $args['channel'] ) ) {
			$where   .= ' AND channel = %s';
			$values[] = $args['channel'];
		}

		if ( ! empty( $args['source'] ) ) {
			$where   .= ' AND source = %s';
			$values[] = $args['source'];
		}

		$orderby  = in_array( $args['orderby'], array( 'id', 'subject_id', 'purpose_slug', 'consented_at', 'withdrawn_at' ), true )
			? $args['orderby']
			: 'consented_at';
		$order    = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC';
		$values[] = (int) $args['limit'];
		$values[] = (int) $args['offset'];

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter
		$results = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$table} WHERE {$where} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d",
				...$values
			)
		);
		return $results ? $results : array();
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter
	}

	/**
	 * Return tab counts (all / granted / revoked) in a single aggregated query.
	 *
	 * Counts are not search-scoped — matching WP convention for view tab display.
	 *
	 * @param string $subject_type Optional subject type filter.
	 * @param int    $subject_id   Optional subject ID filter.
	 * @return array{all: int, granted: int, revoked: int}
	 */
	public static function count_tabs( string $subject_type = '', int $subject_id = 0, string $purpose_slug = '', string $channel = '', string $source = '' ): array {
		global $wpdb;
		$table  = esc_sql( self::get_table_name() );
		$where  = '1=1';
		$values = array();

		if ( ! empty( $subject_type ) ) {
			$where   .= ' AND subject_type = %s';
			$values[] = $subject_type;
		}

		if ( $subject_id > 0 ) {
			$where   .= ' AND subject_id = %d';
			$values[] = $subject_id;
		}

		if ( ! empty( $purpose_slug ) ) {
			$where   .= ' AND purpose_slug = %s';
			$values[] = $purpose_slug;
		}

		if ( ! empty( $channel ) ) {
			$where   .= ' AND channel = %s';
			$values[] = $channel;
		}

		if ( ! empty( $source ) ) {
			$where   .= ' AND source = %s';
			$values[] = $source;
		}

		$sql = "SELECT
            COUNT(*) AS count_all,
            SUM(CASE WHEN withdrawn_at IS NULL     THEN 1 ELSE 0 END) AS count_granted,
            SUM(CASE WHEN withdrawn_at IS NOT NULL THEN 1 ELSE 0 END) AS count_revoked
            FROM {$table} WHERE {$where}"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
		$row = $values ? $wpdb->get_row( $wpdb->prepare( $sql, ...$values ) ) : $wpdb->get_row( $sql );
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter

		return array(
			'all'     => (int) ( $row->count_all ?? 0 ),
			'granted' => (int) ( $row->count_granted ?? 0 ),
			'revoked' => (int) ( $row->count_revoked ?? 0 ),
		);
	}

	/**
	 * Count consent records with optional search and subject filters.
	 *
	 * @param string $search       Search term.
	 * @param string $subject_type Optional subject type filter (e.g. 'wp_user', 'donor').
	 * @param int    $subject_id   Optional subject ID filter.
	 * @return int
	 */
	public static function count( string $search = '', string $subject_type = '', int $subject_id = 0, string $status = 'all', string $purpose_slug = '', string $channel = '', string $source = '' ): int {
		global $wpdb;
		$table  = esc_sql( self::get_table_name() );
		$where  = '1=1';
		$values = array();

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

		if ( ! empty( $subject_type ) ) {
			$where   .= ' AND subject_type = %s';
			$values[] = $subject_type;
		}

		if ( $subject_id > 0 ) {
			$where   .= ' AND subject_id = %d';
			$values[] = $subject_id;
		}

		if ( 'granted' === $status ) {
			$where .= ' AND withdrawn_at IS NULL';
		} elseif ( 'revoked' === $status ) {
			$where .= ' AND withdrawn_at IS NOT NULL';
		}

		if ( ! empty( $purpose_slug ) ) {
			$where   .= ' AND purpose_slug = %s';
			$values[] = $purpose_slug;
		}

		if ( ! empty( $channel ) ) {
			$where   .= ' AND channel = %s';
			$values[] = $channel;
		}

		if ( ! empty( $source ) ) {
			$where   .= ' AND source = %s';
			$values[] = $source;
		}

		if ( empty( $values ) ) {
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
			return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE {$where}" );
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
		}

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, PluginCheck.Security.DirectDB.UnescapedDBParameter
		return (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$table} WHERE {$where}",
				...$values
			)
		);
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, PluginCheck.Security.DirectDB.UnescapedDBParameter
	}

	/**
	 * Get distinct non-empty values for a column (used to populate filter dropdowns).
	 *
	 * @param string $column Column name — must be one of: purpose_slug, channel, source.
	 * @return string[]
	 */
	public static function get_distinct_values( string $column ): array {
		global $wpdb;
		$allowed = array( 'purpose_slug', 'channel', 'source' );
		if ( ! in_array( $column, $allowed, true ) ) {
			return array();
		}
		$table = esc_sql( $wpdb->prefix . 'ambh_consent_records' );
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
		$values = $wpdb->get_col( "SELECT DISTINCT {$column} FROM {$table} WHERE {$column} != '' ORDER BY {$column} ASC" );
		return $values ? $values : array();
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
	}
}
