<?php
/**
 * Transaction Model
 *
 * @package FairPaymentsConnector
 *
 * phpcs:disable WordPress.DB.DirectDatabaseQuery -- model layer reads/writes a custom table directly; caching is left to callers.
 */

namespace FairPaymentsConnector\Models;

use FairPaymentsConnector\Services\MonthlyFeeCapService;
use FairEventsShared\Money;

defined( 'WPINC' ) || die;

/**
 * Model class for payment transactions
 */
class Transaction {
	/**
	 * UTC datetime after which the launch fee waiver ends and the 1% rate resumes.
	 * Delete this constant (and get_fee_rate()) after 2027-01-01.
	 */
	private const FEE_WAIVER_END_TS = '2027-01-01 00:00:00';

	/**
	 * Returns the current platform fee rate (0.0 during the launch waiver, 0.01 after).
	 *
	 * @param int|null $now Unix timestamp to evaluate against; defaults to current site time.
	 * @return float
	 */
	private static function get_fee_rate( ?int $now = null ): float {
		$now    = $now ?? current_time( 'timestamp' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested -- site TZ intentional; cutoff is midnight local time.
		$cutoff = strtotime( self::FEE_WAIVER_END_TS );
		return $now < $cutoff ? 0.0 : 0.01;
	}
	/**
	 * Create a new transaction record
	 *
	 * @param array $data Transaction data.
	 * @return int|false Transaction ID or false on failure.
	 */
	public static function create( $data ) {
		global $wpdb;
		$table_name = \FairPaymentsConnector\Database\Schema::get_payments_table_name();

		// Determine testmode from current settings.
		$mode     = get_option( 'fair_payment_mode', 'test' );
		$testmode = ( 'live' === $mode ) ? 0 : 1;

		$defaults = array(
			'mollie_payment_id' => '',
			'post_id'           => null,
			'event_date_id'     => null,
			'user_id'           => get_current_user_id(),
			'participant_id'    => null,
			'amount'            => 0,
			'currency'          => Money::site_currency(),
			'status'            => 'draft',
			'testmode'          => $testmode,
			'description'       => '',
			'redirect_url'      => '',
			'webhook_url'       => '',
			'checkout_url'      => '',
			'metadata'          => '',
			'access_token'      => null,
		);

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

		// Calculate application fee (capped at the monthly allowance). Rate is 0 during the launch waiver.
		$application_fee = null;
		if ( $data['amount'] > 0 ) {
			$application_fee = round( $data['amount'] * self::get_fee_rate(), 2 );
			$remaining       = MonthlyFeeCapService::get_remaining();
			$application_fee = min( $application_fee, $remaining );
		}

		$data['application_fee'] = $application_fee;

		// Convert metadata array to JSON if needed.
		if ( is_array( $data['metadata'] ) ) {
			$data['metadata'] = wp_json_encode( $data['metadata'] );
		}

		$inserted = $wpdb->insert(
			$table_name,
			array(
				'mollie_payment_id' => $data['mollie_payment_id'],
				'post_id'           => $data['post_id'],
				'event_date_id'     => $data['event_date_id'],
				'user_id'           => $data['user_id'],
				'participant_id'    => $data['participant_id'],
				'amount'            => $data['amount'],
				'currency'          => $data['currency'],
				'application_fee'   => $data['application_fee'],
				'status'            => $data['status'],
				'testmode'          => $data['testmode'],
				'description'       => $data['description'],
				'redirect_url'      => $data['redirect_url'],
				'webhook_url'       => $data['webhook_url'],
				'checkout_url'      => $data['checkout_url'],
				'metadata'          => $data['metadata'],
				'access_token'      => $data['access_token'],
			),
			array( '%s', '%d', '%d', '%d', '%d', '%f', '%s', '%f', '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s' )
		);

		return $inserted ? $wpdb->insert_id : false;
	}

	/**
	 * Import a transaction from an external source.
	 *
	 * Creates a new transaction record or updates an existing one matched by
	 * mollie_payment_id. Preserves fees, status, mode, description and the
	 * original created_at timestamp so the source and target sites stay in sync.
	 *
	 * @param array $data Transaction data from an export file.
	 * @return string 'created', 'updated', or 'skipped'.
	 */
	public static function import( $data ) {
		global $wpdb;
		$table_name = \FairPaymentsConnector\Database\Schema::get_payments_table_name();

		$mollie_payment_id = isset( $data['mollie_payment_id'] ) ? (string) $data['mollie_payment_id'] : '';

		if ( '' === $mollie_payment_id ) {
			return 'skipped';
		}

		$metadata = array();
		if ( ! empty( $data['source_domain'] ) ) {
			$metadata['source_domain'] = (string) $data['source_domain'];
		}
		if ( ! empty( $data['detail_url'] ) ) {
			$metadata['detail_url'] = (string) $data['detail_url'];
		}
		if ( isset( $data['event_date_id'] ) && null !== $data['event_date_id'] ) {
			$metadata['source_event_date_id'] = (int) $data['event_date_id'];
		}

		$row = array(
			'amount'          => isset( $data['amount'] ) ? (float) $data['amount'] : 0,
			'currency'        => ! empty( $data['currency'] ) ? (string) $data['currency'] : Money::site_currency(),
			'mollie_fee'      => isset( $data['mollie_fee'] ) && null !== $data['mollie_fee'] ? (float) $data['mollie_fee'] : null,
			'application_fee' => isset( $data['application_fee'] ) && null !== $data['application_fee'] ? (float) $data['application_fee'] : null,
			'status'          => ! empty( $data['status'] ) ? (string) $data['status'] : 'paid',
			'testmode'        => ! empty( $data['testmode'] ) ? 1 : 0,
			'description'     => isset( $data['description'] ) ? (string) $data['description'] : '',
			'metadata'        => wp_json_encode( $metadata ),
		);

		$existing = self::get_by_mollie_id( $mollie_payment_id );

		if ( $existing ) {
			$wpdb->update(
				$table_name,
				$row,
				array( 'mollie_payment_id' => $mollie_payment_id ),
				array( '%f', '%s', '%f', '%f', '%s', '%d', '%s', '%s' ),
				array( '%s' )
			);
			return 'updated';
		}

		$row['mollie_payment_id'] = $mollie_payment_id;

		$formats = array( '%f', '%s', '%f', '%f', '%s', '%d', '%s', '%s', '%s' );

		if ( ! empty( $data['created_at'] ) ) {
			$row['created_at'] = (string) $data['created_at'];
			$formats[]         = '%s';
		}

		$inserted = $wpdb->insert( $table_name, $row, $formats );

		return $inserted ? 'created' : 'skipped';
	}

	/**
	 * Update specific fields on a transaction by ID.
	 *
	 * @param int   $transaction_id Transaction ID.
	 * @param array $fields         Associative array of column => value.
	 * @return bool True on success, false on failure.
	 */
	public static function update_fields( $transaction_id, $fields ) {
		global $wpdb;
		$table_name = \FairPaymentsConnector\Database\Schema::get_payments_table_name();

		$allowed = array(
			'participant_id' => '%d',
			'user_id'        => '%d',
			'post_id'        => '%d',
		);

		$data    = array();
		$formats = array();

		foreach ( $fields as $column => $value ) {
			if ( ! isset( $allowed[ $column ] ) ) {
				continue;
			}
			$data[ $column ] = $value;
			$formats[]       = $allowed[ $column ];
		}

		if ( empty( $data ) ) {
			return false;
		}

		return (bool) $wpdb->update(
			$table_name,
			$data,
			array( 'id' => $transaction_id ),
			$formats,
			array( '%d' )
		);
	}

	/**
	 * Update transaction status
	 *
	 * When $expected_status is given, the update is a compare-and-swap: it only
	 * writes (and returns true) if the row's current status still matches
	 * $expected_status. This lets concurrent callers (e.g. a webhook and a
	 * page-load sync racing on the same transaction) detect that another
	 * process already made the transition, so only one of them proceeds to
	 * fire status-change hooks/notifications.
	 *
	 * @param string      $mollie_payment_id Mollie payment ID.
	 * @param string      $status New status.
	 * @param string|null $expected_status If given, only update when the current status matches this value.
	 * @return bool True if this call performed the update, false on failure or if the status no longer matched.
	 */
	public static function update_status( $mollie_payment_id, $status, $expected_status = null ) {
		global $wpdb;
		$table_name = \FairPaymentsConnector\Database\Schema::get_payments_table_name();

		if ( null === $expected_status ) {
			return (bool) $wpdb->update(
				$table_name,
				array( 'status' => $status ),
				array( 'mollie_payment_id' => $mollie_payment_id ),
				array( '%s' ),
				array( '%s' )
			);
		}

		$result = $wpdb->query(
			$wpdb->prepare(
				'UPDATE %i SET status = %s WHERE mollie_payment_id = %s AND status = %s',
				$table_name,
				$status,
				$mollie_payment_id,
				$expected_status
			)
		);

		return false !== $result && $result > 0;
	}

	/**
	 * Update Mollie processing fee for a transaction
	 *
	 * @param string $mollie_payment_id Mollie payment ID.
	 * @param float  $mollie_fee Mollie processing fee amount.
	 * @return bool True on success, false on failure.
	 */
	public static function update_mollie_fee( $mollie_payment_id, $mollie_fee ) {
		global $wpdb;
		$table_name = \FairPaymentsConnector\Database\Schema::get_payments_table_name();

		return (bool) $wpdb->update(
			$table_name,
			array( 'mollie_fee' => $mollie_fee ),
			array( 'mollie_payment_id' => $mollie_payment_id ),
			array( '%f' ),
			array( '%s' )
		);
	}

	/**
	 * Get IDs of paid transactions that have a Mollie payment ID but no Mollie fee captured yet.
	 *
	 * @param string $mode Optional 'live'|'test'|'' for all.
	 * @return int[]
	 */
	public static function get_ids_missing_mollie_fee( $mode = '' ) {
		global $wpdb;
		$table_name = \FairPaymentsConnector\Database\Schema::get_payments_table_name();

		$where_clauses = array(
			"status = 'paid'",
			'mollie_fee IS NULL',
			"mollie_payment_id IS NOT NULL AND mollie_payment_id <> ''",
		);

		if ( '' !== $mode ) {
			$testmode        = 'test' === $mode ? 1 : 0;
			$where_clauses[] = $wpdb->prepare( 'testmode = %d', $testmode );
		}

		$where = implode( ' AND ', $where_clauses );

		$ids = $wpdb->get_col(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $where is an AND-join of clauses already individually run through $wpdb->prepare() above.
				"SELECT id FROM %i WHERE {$where} ORDER BY created_at DESC",
				$table_name
			)
		);

		return array_map( 'intval', $ids );
	}

	/**
	 * Get transaction by Mollie payment ID
	 *
	 * @param string $mollie_payment_id Mollie payment ID.
	 * @return object|null Transaction object or null if not found.
	 */
	public static function get_by_mollie_id( $mollie_payment_id ) {
		global $wpdb;
		$table_name = \FairPaymentsConnector\Database\Schema::get_payments_table_name();

		return $wpdb->get_row(
			$wpdb->prepare(
				'SELECT * FROM %i WHERE mollie_payment_id = %s',
				$table_name,
				$mollie_payment_id
			)
		);
	}

	/**
	 * Get all transactions
	 *
	 * @param array $args Query arguments.
	 * @return array Array of transaction objects.
	 */
	public static function get_all( $args = array() ) {
		global $wpdb;
		$table_name = \FairPaymentsConnector\Database\Schema::get_payments_table_name();

		$defaults = array(
			'limit'         => 50,
			'offset'        => 0,
			'status'        => '',
			'mode'          => '',
			'event_date_id' => 0,
			'date_from'     => '',
			'date_to'       => '',
			'orderby'       => 'created_at',
			'order'         => 'DESC',
		);

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

		$where_clauses = array();

		if ( ! empty( $args['status'] ) ) {
			$where_clauses[] = $wpdb->prepare( 'status = %s', $args['status'] );
		}

		if ( '' !== $args['mode'] ) {
			$testmode        = 'test' === $args['mode'] ? 1 : 0;
			$where_clauses[] = $wpdb->prepare( 'testmode = %d', $testmode );
		}

		if ( ! empty( $args['event_date_id'] ) ) {
			$where_clauses[] = $wpdb->prepare( 'event_date_id = %d', (int) $args['event_date_id'] );
		}

		if ( ! empty( $args['date_from'] ) ) {
			$where_clauses[] = $wpdb->prepare( 'created_at >= %s', $args['date_from'] );
		}

		if ( ! empty( $args['date_to'] ) ) {
			$where_clauses[] = $wpdb->prepare( 'created_at <= %s', $args['date_to'] );
		}

		$where = '';
		if ( ! empty( $where_clauses ) ) {
			$where = ' WHERE ' . implode( ' AND ', $where_clauses );
		}

		$allowed_orderby = array( 'created_at', 'amount', 'status', 'id' );
		$orderby         = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'created_at';
		$order           = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC';

		return $wpdb->get_results(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $where joins clauses already prepared above; $orderby/$order are validated against allowlists.
				"SELECT * FROM %i{$where} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d",
				$table_name,
				$args['limit'],
				$args['offset']
			)
		);
	}

	/**
	 * Count transactions with optional filters
	 *
	 * @param array $args Query arguments.
	 * @return int Total count.
	 */
	public static function count( $args = array() ) {
		global $wpdb;
		$table_name = \FairPaymentsConnector\Database\Schema::get_payments_table_name();

		$where_clauses = array();

		if ( ! empty( $args['status'] ) ) {
			$where_clauses[] = $wpdb->prepare( 'status = %s', $args['status'] );
		}

		if ( isset( $args['mode'] ) && '' !== $args['mode'] ) {
			$testmode        = 'test' === $args['mode'] ? 1 : 0;
			$where_clauses[] = $wpdb->prepare( 'testmode = %d', $testmode );
		}

		if ( ! empty( $args['event_date_id'] ) ) {
			$where_clauses[] = $wpdb->prepare( 'event_date_id = %d', (int) $args['event_date_id'] );
		}

		if ( ! empty( $args['date_from'] ) ) {
			$where_clauses[] = $wpdb->prepare( 'created_at >= %s', $args['date_from'] );
		}

		if ( ! empty( $args['date_to'] ) ) {
			$where_clauses[] = $wpdb->prepare( 'created_at <= %s', $args['date_to'] );
		}

		$where = '';
		if ( ! empty( $where_clauses ) ) {
			$where = ' WHERE ' . implode( ' AND ', $where_clauses );
		}

		return (int) $wpdb->get_var(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $where joins clauses already prepared above.
				"SELECT COUNT(*) FROM %i{$where}",
				$table_name
			)
		);
	}

	/**
	 * Get transaction by ID
	 *
	 * @param int $transaction_id Transaction ID.
	 * @return object|null Transaction object or null if not found.
	 */
	public static function get_by_id( $transaction_id ) {
		global $wpdb;
		$table_name = \FairPaymentsConnector\Database\Schema::get_payments_table_name();

		return $wpdb->get_row(
			$wpdb->prepare(
				'SELECT * FROM %i WHERE id = %d',
				$table_name,
				$transaction_id
			)
		);
	}

	/**
	 * Mark transaction as payment initiated
	 *
	 * @param int    $transaction_id Transaction ID.
	 * @param string $mollie_payment_id Mollie payment ID.
	 * @param string $checkout_url Mollie checkout URL.
	 * @return bool True on success, false on failure.
	 */
	public static function mark_payment_initiated( $transaction_id, $mollie_payment_id, $checkout_url ) {
		global $wpdb;
		$table_name = \FairPaymentsConnector\Database\Schema::get_payments_table_name();

		return (bool) $wpdb->update(
			$table_name,
			array(
				'mollie_payment_id'    => $mollie_payment_id,
				'checkout_url'         => $checkout_url,
				'status'               => 'pending_payment',
				'payment_initiated_at' => current_time( 'mysql' ),
			),
			array( 'id' => $transaction_id ),
			array( '%s', '%s', '%s', '%s' ),
			array( '%d' )
		);
	}

	/**
	 * Check if transaction can initiate payment
	 *
	 * @param int $transaction_id Transaction ID.
	 * @return true|\WP_Error True if can initiate, WP_Error otherwise.
	 */
	public static function can_initiate_payment( $transaction_id ) {
		global $wpdb;
		$table_name = \FairPaymentsConnector\Database\Schema::get_payments_table_name();

		$transaction = $wpdb->get_row(
			$wpdb->prepare(
				'SELECT status, payment_initiated_at FROM %i WHERE id = %d',
				$table_name,
				$transaction_id
			)
		);

		if ( ! $transaction ) {
			return new \WP_Error(
				'transaction_not_found',
				__( 'Transaction not found.', 'fair-payments-connector' )
			);
		}

		// Check if already initiated.
		if ( null !== $transaction->payment_initiated_at ) {
			return new \WP_Error(
				'payment_already_initiated',
				__( 'Payment has already been initiated for this transaction. Create a new transaction to retry.', 'fair-payments-connector' )
			);
		}

		// Check status.
		if ( 'draft' !== $transaction->status ) {
			return new \WP_Error(
				'invalid_transaction_status',
				sprintf(
					/* translators: %s: current transaction status */
					__( 'Transaction status must be "draft" to initiate payment. Current status: %s', 'fair-payments-connector' ),
					$transaction->status
				)
			);
		}

		return true;
	}
}
