<?php
/**
 * Shared payment completion after Stripe confirms payment.
 *
 * @package WPDudeProducts
 */

declare(strict_types=1);

namespace WPDude\Products\Checkout;

use WPDude\Products\Logging\Logger;
use WPDude\Products\Orders\Order_Repository;
use WPDude\Products\Orders\Statuses;
use WPDude\Products\Products\Product_Meta;
use WPDude\Products\Products\Stock_Service;
use WPDude\Products\Providers\Stripe\Stripe_Provider;
use WPDude\Products\Support\Money;

/**
 * Completes a pending order from a Stripe Checkout Session payload.
 */
final class Payment_Completion_Service {

	private Order_Repository $orders;
	private Stripe_Provider $stripe;

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

	/**
	 * Confirm a pending order by re-fetching its Stripe session.
	 *
	 * @return array{status: string, success_message: string, product_id: int, product_name: string, options_label: string, qty: int, amount_formatted: string, summary_html: string, summary_text: string}|\WP_Error
	 */
	public function confirm_from_stripe( string $uuid ) {
		$order = $this->orders->find_by_uuid( $uuid );
		if ( ! $order ) {
			return new \WP_Error( 'not_found', __( 'Order not found.', 'dude-commerce' ), array( 'status' => 404 ) );
		}

		if ( Statuses::NEW === (string) $order['status'] ) {
			return $this->success_payload( $order );
		}

		$session_id = (string) ( $order['stripe_checkout_session_id'] ?? '' );
		if ( '' === $session_id ) {
			return new \WP_Error( 'no_session', __( 'No Stripe checkout session is linked to this order.', 'dude-commerce' ), array( 'status' => 400 ) );
		}

		$session = $this->stripe->retrieve_session( $session_id );
		if ( is_wp_error( $session ) ) {
			return $session;
		}

		$session['_wpdude_dc_completed_via'] = 'local_confirm';
		$result = $this->complete_from_session( $session );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$order = $this->orders->find_by_uuid( $uuid );
		if ( ! $order || Statuses::NEW !== (string) $order['status'] ) {
			$payment_status = (string) ( $session['payment_status'] ?? '' );
			return new \WP_Error(
				'not_paid',
				sprintf(
					/* translators: %s: Stripe payment_status */
					__( 'Stripe has not marked this payment as paid yet (status: %s).', 'dude-commerce' ),
					$payment_status !== '' ? $payment_status : 'unknown'
				),
				array( 'status' => 409 )
			);
		}

		return $this->success_payload( $order );
	}

	/**
	 * Complete from a Stripe session array (webhook or local confirm).
	 *
	 * @param array<string, mixed> $session Session.
	 * @return true|\WP_Error
	 */
	public function complete_from_session( array $session ) {
		$payment_status = (string) ( $session['payment_status'] ?? '' );
		if ( 'paid' !== $payment_status && 'no_payment_required' !== $payment_status ) {
			return true;
		}

		$order = $this->resolve_order( $session );
		if ( ! $order ) {
			Logger::instance()->warning( 'Checkout completed for unknown order' );
			return true;
		}

		// Already paid: still recover stock decrement if a previous run crashed mid-way.
		if ( Statuses::NEW === (string) $order['status'] ) {
			$this->maybe_decrement_stock( $order );
			$this->maybe_store_risk( $order, $session );
			return true;
		}

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

		$customer_email = (string) ( $session['customer_details']['email'] ?? $session['customer_email'] ?? '' );
		$customer_name  = (string) ( $session['customer_details']['name'] ?? '' );
		$payment_intent = $session['payment_intent'] ?? '';
		$details        = Stripe_Provider::empty_charge_details();
		if ( is_array( $payment_intent ) ) {
			$details        = Stripe_Provider::charge_details_from_payment_intent( $payment_intent );
			$payment_intent = (string) ( $payment_intent['id'] ?? '' );
		}
		$payment_intent = (string) $payment_intent;
		// Always re-fetch charge details when we have a PI — session expand is often incomplete for Radar.
		if ( '' !== $payment_intent ) {
			$fetched = $this->stripe->get_payment_charge_details( $payment_intent );
			$details = self::merge_charge_details( $details, $fetched );
		}

		$shipping = array();
		if ( isset( $session['shipping_details'] ) && is_array( $session['shipping_details'] ) ) {
			$shipping = $session['shipping_details'];
		} elseif ( isset( $session['collected_information']['shipping_details'] ) && is_array( $session['collected_information']['shipping_details'] ) ) {
			$shipping = $session['collected_information']['shipping_details'];
		}

		$update = array(
			'status'                     => Statuses::NEW,
			'customer_email'             => sanitize_email( $customer_email ),
			'customer_name'              => sanitize_text_field( $customer_name ),
			'stripe_payment_intent_id'   => sanitize_text_field( $payment_intent ),
			'stripe_customer_id'         => sanitize_text_field( (string) ( $session['customer'] ?? '' ) ),
			'stripe_checkout_session_id' => sanitize_text_field( (string) ( $session['id'] ?? $order['stripe_checkout_session_id'] ) ),
			'amount'                     => isset( $session['amount_total'] ) ? (int) $session['amount_total'] : (int) $order['amount'],
			'tax_amount'                 => isset( $session['total_details']['amount_tax'] ) ? (int) $session['total_details']['amount_tax'] : (int) ( $order['tax_amount'] ?? 0 ),
			'currency'                   => isset( $session['currency'] ) ? strtolower( (string) $session['currency'] ) : $order['currency'],
			'shipping_address'           => $shipping,
			'paid_at'                    => current_time( 'mysql', true ),
		);
		$update = array_merge( $update, self::charge_details_to_meta( $details ) );

		$this->orders->update( (int) $order['id'], $update );

		$order = $this->orders->find( (int) $order['id'] );
		if ( ! $order ) {
			return true;
		}

		$this->maybe_decrement_stock( $order );

		/**
		 * Fires when an order payment is completed.
		 *
		 * @param array<string, mixed> $order Order.
		 */
		do_action( 'wpdude_dc_payment_completed', $order );

		Logger::instance()->info( 'Payment completed', array( 'order_uuid' => $order['uuid'] ) );

		return true;
	}

	/**
	 * Persist Radar risk / fee when missing on an already-paid order.
	 *
	 * @param array<string, mixed> $order   Order.
	 * @param array<string, mixed> $session Checkout Session.
	 */
	private function maybe_store_risk( array $order, array $session ): void {
		$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;
		}

		$payment_intent = $session['payment_intent'] ?? ( $order['stripe_payment_intent_id'] ?? '' );
		$details        = Stripe_Provider::empty_charge_details();
		if ( is_array( $payment_intent ) ) {
			$details        = Stripe_Provider::charge_details_from_payment_intent( $payment_intent );
			$payment_intent = (string) ( $payment_intent['id'] ?? '' );
		}
		$payment_intent = (string) $payment_intent;
		if ( '' !== $payment_intent ) {
			$fetched = $this->stripe->get_payment_charge_details( $payment_intent );
			$details = self::merge_charge_details( $details, $fetched );
		}

		$update = self::charge_details_to_meta( $details );
		if ( empty( $update ) ) {
			return;
		}
		$this->orders->update( (int) $order['id'], $update );
	}

	/**
	 * @param array{level: string, score: int|null, fee: int|null, net: int|null, currency: string} $base
	 * @param array{level: string, score: int|null, fee: int|null, net: int|null, currency: string} $overlay
	 * @return array{level: string, score: int|null, fee: int|null, net: int|null, currency: string}
	 */
	private static function merge_charge_details( array $base, array $overlay ): array {
		if ( '' === $base['level'] && '' !== $overlay['level'] ) {
			$base['level'] = $overlay['level'];
		}
		if ( null === $base['score'] && null !== $overlay['score'] ) {
			$base['score'] = $overlay['score'];
		}
		if ( null === $base['fee'] && null !== $overlay['fee'] ) {
			$base['fee'] = $overlay['fee'];
		}
		if ( null === $base['net'] && null !== $overlay['net'] ) {
			$base['net'] = $overlay['net'];
		}
		if ( '' === $base['currency'] && '' !== $overlay['currency'] ) {
			$base['currency'] = $overlay['currency'];
		}
		return $base;
	}

	/**
	 * @param array{level: string, score: int|null, fee: int|null, net: int|null, currency: string} $details
	 * @return array<string, mixed>
	 */
	private static function charge_details_to_meta( array $details ): array {
		$update = array();
		if ( '' !== $details['level'] ) {
			$update['stripe_risk_level'] = $details['level'];
		}
		if ( null !== $details['score'] ) {
			$update['stripe_risk_score'] = (int) $details['score'];
		}
		if ( 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'];
		}
		return $update;
	}

	/**
	 * Decrement stock once per order.
	 *
	 * @param array<string, mixed> $order Order.
	 */
	private function maybe_decrement_stock( array $order ): void {
		if ( ! empty( $order['stock_decremented'] ) ) {
			return;
		}

		$dec = Stock_Service::decrement(
			(int) $order['product_id'],
			(string) ( $order['combination_key'] ?? '' ),
			(int) $order['qty']
		);
		if ( ! is_wp_error( $dec ) ) {
			$this->orders->update( (int) $order['id'], array( 'stock_decremented' => 1 ) );
			return;
		}

		Logger::instance()->error(
			'Stock decrement failed after payment',
			array(
				'order_uuid' => $order['uuid'] ?? '',
				'error'      => $dec->get_error_message(),
			)
		);
	}

	/**
	 * @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;
	}

	/**
	 * @param array<string, mixed> $order Order.
	 * @return array{status: string, success_message: string, product_id: int, product_name: string, options_label: string, qty: int, amount_formatted: string, summary_html: string}
	 */
	public static function public_success_payload( array $order ): array {
		$product_id   = (int) ( $order['product_id'] ?? 0 );
		$qty          = max( 1, (int) ( $order['qty'] ?? 1 ) );
		$currency     = (string) ( $order['currency'] ?? 'usd' );
		$amount       = (int) ( $order['amount'] ?? 0 );
		$line         = is_array( $order['line_item'] ?? null ) ? $order['line_item'] : array();
		$line_name    = trim( (string) ( $line['name'] ?? '' ) );
		$product_name = get_the_title( $product_id );
		if ( ! is_string( $product_name ) || '' === $product_name ) {
			$product_name = __( 'Product', 'dude-commerce' );
		}

		$options_label = self::options_label_from_order( $order, $line_name, $product_name );

		$summary_parts = array( $product_name );
		if ( '' !== $options_label ) {
			$summary_parts[] = $options_label;
		}
		$summary_parts[] = sprintf(
			/* translators: %d: quantity */
			_n( 'Qty %d', 'Qty %d', $qty, 'dude-commerce' ),
			$qty
		);
		$summary_parts[] = Money::format( $amount, $currency );

		$message = __( 'Thank you! Your payment was successful. We will process your order shortly.', 'dude-commerce' );

		$summary_html = '<ul class="wpdude-dc-return__details">';
		$summary_html .= '<li><strong>' . esc_html__( 'Product', 'dude-commerce' ) . ':</strong> ' . esc_html( $product_name ) . '</li>';
		if ( '' !== $options_label ) {
			$summary_html .= '<li><strong>' . esc_html__( 'Options', 'dude-commerce' ) . ':</strong> ' . esc_html( $options_label ) . '</li>';
		}
		$summary_html .= '<li><strong>' . esc_html__( 'Quantity', 'dude-commerce' ) . ':</strong> ' . esc_html( (string) $qty ) . '</li>';
		$summary_html .= '<li><strong>' . esc_html__( 'Total', 'dude-commerce' ) . ':</strong> ' . esc_html( Money::format( $amount, $currency ) ) . '</li>';
		$summary_html .= '</ul>';

		return array(
			'status'           => Statuses::NEW,
			'success_message'  => $message,
			'product_id'       => $product_id,
			'product_name'     => $product_name,
			'options_label'    => $options_label,
			'qty'              => $qty,
			'amount_formatted' => Money::format( $amount, $currency ),
			'summary_html'     => $summary_html,
			'summary_text'     => implode( ' · ', $summary_parts ),
		);
	}

	/**
	 * Human-readable options string for an order.
	 *
	 * @param array<string, mixed> $order     Order.
	 * @param string               $line_name Line item name.
	 * @param string               $product_name Product title.
	 */
	private static function options_label_from_order( array $order, string $line_name, string $product_name ): string {
		$key = (string) ( $order['combination_key'] ?? '' );
		$product_id = (int) ( $order['product_id'] ?? 0 );
		if ( '' !== $key && $product_id > 0 ) {
			foreach ( Product_Meta::get_combinations( $product_id ) as $combo ) {
				if ( (string) ( $combo['key'] ?? '' ) !== $key ) {
					continue;
				}
				$parts = array();
				foreach ( (array) ( $combo['options'] ?? array() ) as $name => $value ) {
					$parts[] = (string) $name . ': ' . (string) $value;
				}
				if ( ! empty( $parts ) ) {
					return implode( ' / ', $parts );
				}
			}
		}

		if ( '' !== $key ) {
			$parts = array();
			foreach ( explode( '|', $key ) as $pair ) {
				$bits = explode( '=', $pair, 2 );
				if ( 2 !== count( $bits ) ) {
					continue;
				}
				$name    = ucwords( str_replace( array( '-', '_' ), ' ', $bits[0] ) );
				$parts[] = $name . ': ' . $bits[1];
			}
			if ( ! empty( $parts ) ) {
				return implode( ' / ', $parts );
			}
		}

		// Fallback: strip product title prefix from line name ("Title — opts").
		if ( '' !== $line_name && str_contains( $line_name, '—' ) ) {
			$after = trim( (string) preg_replace( '/^.*?—\s*/u', '', $line_name ) );
			if ( '' !== $after && $after !== $product_name ) {
				return $after;
			}
		}

		return '';
	}

	/**
	 * @param array<string, mixed> $order Order.
	 * @return array{status: string, success_message: string, product_id: int, product_name: string, options_label: string, qty: int, amount_formatted: string, summary_html: string, summary_text: string}
	 */
	private function success_payload( array $order ): array {
		return self::public_success_payload( $order );
	}
}
