<?php
/**
 * Stripe webhook processing.
 *
 * @package WPDudeProducts
 */

declare(strict_types=1);

namespace WPDude\Products\Webhooks;

use WPDude\Products\Checkout\Payment_Completion_Service;
use WPDude\Products\Logging\Logger;
use WPDude\Products\Orders\Order_Repository;
use WPDude\Products\Orders\Statuses;
use WPDude\Products\Providers\Stripe\Stripe_Provider;
use WPDude\Products\Settings\Settings;

/**
 * Handles Stripe webhook events idempotently.
 */
final class Stripe_Webhook_Handler {

	private Stripe_Provider $stripe;
	private Order_Repository $orders;
	private Payment_Completion_Service $completion;

	public function __construct(
		?Stripe_Provider $stripe = null,
		?Order_Repository $orders = null,
		?Payment_Completion_Service $completion = null
	) {
		$this->stripe     = $stripe ?? new Stripe_Provider();
		$this->orders     = $orders ?? new Order_Repository();
		$this->completion = $completion ?? new Payment_Completion_Service( $this->orders );
	}

	/**
	 * Process raw webhook request.
	 *
	 * @return true|\WP_Error
	 */
	public function handle( string $payload, string $signature ) {
		$event = $this->stripe->verify_webhook( $payload, $signature );
		if ( is_wp_error( $event ) ) {
			return $event;
		}

		$event_id   = (string) ( $event['id'] ?? '' );
		$event_type = (string) ( $event['type'] ?? '' );

		if ( '' === $event_id ) {
			return new \WP_Error( 'invalid_event', __( 'Invalid event.', 'dude-commerce' ), array( 'status' => 400 ) );
		}

		if ( $this->already_processed( $event_id ) ) {
			Logger::instance()->info( 'Duplicate webhook ignored', array( 'event_type' => $event_type ) );
			Settings::mark_webhook_received();
			return true;
		}

		$object = $event['data']['object'] ?? array();
		if ( ! is_array( $object ) ) {
			$object = array();
		}

		switch ( $event_type ) {
			case 'checkout.session.completed':
			case 'checkout.session.async_payment_succeeded':
				// Webhook payloads are unexpanded — re-fetch so Radar risk + fee are available.
				$session_id = sanitize_text_field( (string) ( $object['id'] ?? '' ) );
				if ( '' !== $session_id ) {
					$retrieved = $this->stripe->retrieve_session( $session_id );
					if ( ! is_wp_error( $retrieved ) && is_array( $retrieved ) ) {
						$object = $retrieved;
					}
				}
				$object['_wpdude_dc_completed_via'] = 'webhook';
				$result                             = $this->completion->complete_from_session( $object );
				break;
			case 'checkout.session.async_payment_failed':
				$result = $this->mark_status( $object, Statuses::FAILED );
				break;
			case 'checkout.session.expired':
				$result = $this->mark_status( $object, Statuses::EXPIRED );
				break;
			case 'charge.succeeded':
				$result = $this->maybe_store_charge_details( $object );
				break;
			case 'charge.refunded':
				$result = $this->mark_refunded( $object );
				break;
			default:
				$result = true;
				break;
		}

		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$this->record_event( $event_id, $event_type );
		Settings::mark_webhook_received();
		return true;
	}

	/**
	 * @param array<string, mixed> $session Session.
	 * @return true|\WP_Error
	 */
	private function mark_status( array $session, string $status ) {
		$order = $this->resolve_order( $session );
		if ( ! $order ) {
			return true;
		}
		if ( Statuses::can_transition( (string) $order['status'], $status ) ) {
			$this->orders->update( (int) $order['id'], array( 'status' => $status ) );
			if ( Statuses::FAILED === $status ) {
				$order = $this->orders->find( (int) $order['id'] );
				if ( $order ) {
					do_action( 'wpdude_dc_payment_failed', $order );
				}
			}
		}
		return true;
	}

	/**
	 * Persist Radar risk / fee from a Charge when the order is missing them.
	 *
	 * @param array<string, mixed> $charge Charge object.
	 * @return true|\WP_Error
	 */
	private function maybe_store_charge_details( array $charge ) {
		$payment_intent = (string) ( $charge['payment_intent'] ?? '' );
		if ( '' === $payment_intent ) {
			return true;
		}

		$order = $this->orders->find_by_payment_intent( $payment_intent );
		if ( ! $order ) {
			return true;
		}

		$has_risk = '' !== sanitize_key( (string) ( $order['stripe_risk_level'] ?? '' ) );
		$has_fee  = metadata_exists( 'post', (int) $order['id'], Order_Repository::META_PREFIX . 'stripe_fee_amount' );
		if ( $has_risk && $has_fee ) {
			return true;
		}

		$details = Stripe_Provider::charge_details_from_charge( $charge );
		if ( ( '' === $details['level'] || null === $details['fee'] ) && '' !== $payment_intent ) {
			$fetched = $this->stripe->get_payment_charge_details( $payment_intent );
			if ( '' === $details['level'] && '' !== $fetched['level'] ) {
				$details['level'] = $fetched['level'];
			}
			if ( null === $details['score'] && null !== $fetched['score'] ) {
				$details['score'] = $fetched['score'];
			}
			if ( null === $details['fee'] && null !== $fetched['fee'] ) {
				$details['fee'] = $fetched['fee'];
			}
			if ( null === $details['net'] && null !== $fetched['net'] ) {
				$details['net'] = $fetched['net'];
			}
			if ( '' === $details['currency'] && '' !== $fetched['currency'] ) {
				$details['currency'] = $fetched['currency'];
			}
		}

		if ( '' === $details['level'] && 'succeeded' === (string) ( $charge['status'] ?? '' ) ) {
			$details['level'] = 'not_assessed';
		}

		$update = array();
		if ( ! $has_risk && '' !== $details['level'] ) {
			$update['stripe_risk_level'] = $details['level'];
			if ( null !== $details['score'] ) {
				$update['stripe_risk_score'] = (int) $details['score'];
			}
		}
		if ( ! $has_fee && null !== $details['fee'] ) {
			$update['stripe_fee_amount'] = (int) $details['fee'];
			if ( null !== $details['net'] ) {
				$update['stripe_net_amount'] = (int) $details['net'];
			}
			if ( '' !== $details['currency'] ) {
				$update['stripe_fee_currency'] = $details['currency'];
			}
		}

		if ( ! empty( $update ) ) {
			$this->orders->update( (int) $order['id'], $update );
		}

		return true;
	}

	/**
	 * @param array<string, mixed> $charge Charge object.
	 * @return true|\WP_Error
	 */
	private function mark_refunded( array $charge ) {
		$payment_intent = (string) ( $charge['payment_intent'] ?? '' );
		if ( '' === $payment_intent ) {
			return true;
		}

		$order = $this->orders->find_by_payment_intent( $payment_intent );
		if ( ! $order ) {
			return true;
		}

		if ( ! Statuses::can_transition( (string) $order['status'], Statuses::REFUNDED ) ) {
			return true;
		}

		$result = $this->orders->transition_status( (int) $order['id'], Statuses::REFUNDED );
		if ( is_wp_error( $result ) ) {
			return true;
		}

		$order = $this->orders->find( (int) $order['id'] );
		if ( $order ) {
			do_action( 'wpdude_dc_refund_recorded', $order );
		}

		return true;
	}

	/**
	 * @param array<string, mixed> $session Session.
	 * @return array<string, mixed>|null
	 */
	private function resolve_order( array $session ): ?array {
		$meta = isset( $session['metadata'] ) && is_array( $session['metadata'] ) ? $session['metadata'] : array();
		$uuid = (string) ( $session['client_reference_id'] ?? $meta['wpdude_dc_order_uuid'] ?? '' );
		if ( $uuid ) {
			$order = $this->orders->find_by_uuid( $uuid );
			if ( $order ) {
				return $order;
			}
		}
		$session_id = (string) ( $session['id'] ?? '' );
		if ( $session_id ) {
			return $this->orders->find_by_session( $session_id );
		}
		return null;
	}

	private function already_processed( string $event_id ): bool {
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$found = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT id FROM {$wpdb->prefix}wpdude_dc_stripe_events WHERE event_id = %s",
				$event_id
			)
		);
		return null !== $found;
	}

	private function record_event( string $event_id, string $event_type ): void {
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$wpdb->insert(
			$wpdb->prefix . 'wpdude_dc_stripe_events',
			array(
				'event_id'     => $event_id,
				'event_type'   => $event_type,
				'processed_at' => current_time( 'mysql', true ),
			),
			array( '%s', '%s', '%s' )
		);
	}
}
