<?php
/**
 * Handles the three TWINT callbacks for Pay4All Form orders, routed through
 * the SOAP engine via FormTwintCore (since the merchant's install runs in
 * direct/SOAP mode, not wallet/REST).
 *
 *   ?p4all_form_pay=redirect → customer returns from TWINT app (success path)
 *   ?p4all_form_pay=cancel   → customer aborted the payment in the TWINT app
 *   ?p4all_form_pay=webhook  → server-to-server Order Update Notification (OUN)
 *
 * For each kind we hand the order id + uuid to the TWINT engine, which polls
 * TWINT for the current state and dispatches lifecycle hooks (handled by
 * FormDomainEvents → updates the p4all_order's payment_status).
 *
 * No code path here touches WooCommerce runtime: it never resolves shop orders,
 * uses WalletResponse, or hits the shop api_request_url helper.
 */

namespace Twnt4All\Pay4All;

use Twnt4All\Support\Journal;
use Twnt4All\Support\WpBridge;

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

final class FormWebhookHandler
{
    public const QUERY_VAR = 'p4all_form_pay';

    public static function register() : void
    {
        // Run *after* WooCommerce has registered its post types (~init/9) so
        // the inevitable shop-order lookup inside OrderDataRepository fallback
        // paths doesn't trigger the "called before post types are registered"
        // notice. Priority 20 keeps us comfortably after WC's bootstrap.
        add_action( 'init', [ self::class, 'maybe_handle' ], 20 );
    }

    public static function maybe_handle() : void
    {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- callback from TWINT, signed via the `s` token (redirect/cancel) or out-of-band (webhook) ; verified below.
        if ( empty( $_GET[ FormWebhookHandler::QUERY_VAR ] ) ) {
            return;
        }
        // phpcs:disable WordPress.Security.NonceVerification.Recommended -- callback from TWINT, signed via the `s` token (redirect/cancel) or out-of-band (webhook).
        $kind     = sanitize_key( wp_unslash( (string) $_GET[ FormWebhookHandler::QUERY_VAR ] ) );
        $order_id = isset( $_GET[ 'order_id' ] ) ? absint( wp_unslash( $_GET[ 'order_id' ] ) ) : 0;
        $uuid     = isset( $_GET[ 'uuid' ] )     ? sanitize_text_field( wp_unslash( (string) $_GET[ 'uuid' ] ) ) : '';
        // phpcs:enable WordPress.Security.NonceVerification.Recommended

        $shim = FormOrderRegistry::get_or_load( $order_id );
        if ( ! $shim ) {
            Journal::error( 'FormWebhookHandler: unknown order ' . $order_id );
            if ( $kind === 'webhook' ) { status_header( 404 ); die(); }
            wp_safe_redirect( home_url( '/' ) );
            exit;
        }

        switch ( $kind ) {
            case 'redirect':
                self::handle_redirect( $shim, $uuid );
                break;
            case 'cancel':
                self::handle_cancel( $shim, $uuid );
                break;
            case 'webhook':
                self::handle_webhook( $shim, $uuid );
                break;
            default:
                wp_safe_redirect( home_url( '/' ) );
                exit;
        }
    }

    // -----------------------------------------------------------------
    // Handlers
    // -----------------------------------------------------------------

    private static function handle_redirect( FormOrderShim $shim, string $uuid ) : void
    {
        if ( ! self::verify_signature( $shim ) ) {
            Journal::error( 'FormWebhookHandler: invalid signature on redirect for order ' . $shim->get_id() );
            wp_safe_redirect( $shim->get_checkout_payment_url() );
            exit;
        }

        // Ask the TWINT engine to poll the order and dispatch lifecycle hooks.
        // FormDomainEvents will flip the p4all_order to paid/failed as needed.
        self::engine_handle_webhook( $shim, $uuid );

        wp_safe_redirect( $shim->get_checkout_order_received_url() );
        exit;
    }

    private static function handle_cancel( FormOrderShim $shim, string $uuid ) : void
    {
        if ( ! self::verify_signature( $shim ) ) {
            wp_safe_redirect( $shim->get_checkout_payment_url() );
            exit;
        }

        if ( class_exists( '\\Pay4All\\Form\\Domain\\Order' ) ) {
            \Pay4All\Form\Domain\Order::update_payment_status( $shim->get_id(), 'cancelled' );
        }

        // Append a marker query so the form shortcode can render a "commande
        // annulée" panel instead of silently re-rendering an empty form.
        $base = $shim->get_checkout_payment_url();
        $url  = add_query_arg( 'p4all_cancelled', rawurlencode( $shim->get_order_number() ), $base );
        wp_safe_redirect( $url );
        exit;
    }

    private static function handle_webhook( FormOrderShim $shim, string $uuid ) : void
    {
        // Server-to-server: no signature `s` is present on OUN callbacks, so
        // origin is validated by comparing the incoming UUID to the one we
        // handed to TWINT at start_order() time (stored in _t4a_twint_order).
        // Uses hash_equals to avoid timing-based enumeration.
        $expected_uuid = (string) \Twnt4All\Checkout\OrderDataRepository::get_order_data( $shim->get_id(), 'order_uuid' );
        if ( $expected_uuid === '' || $uuid === '' || ! hash_equals( $expected_uuid, $uuid ) ) {
            Journal::error( sprintf( 'FormWebhookHandler: UUID mismatch on webhook for order %d, rejecting', $shim->get_id() ) );
            status_header( 403 );
            die();
        }

        // Dispatch through the engine which polls the SOAP endpoint and
        // updates the order via FormDomainEvents. Respond with a silent 200
        // so TWINT stops retrying.
        self::engine_handle_webhook( $shim, $uuid );
        status_header( 200 );
        die();
    }

    // -----------------------------------------------------------------
    // SOAP engine entry point
    // -----------------------------------------------------------------

    private static function engine_handle_webhook( FormOrderShim $shim, string $uuid ) : void
    {
        if ( ! defined( 'T4A_MERCHANT_UUID' ) || ! T4A_MERCHANT_UUID ) {
            Journal::error( 'FormWebhookHandler: T4A_MERCHANT_UUID missing' );
            return;
        }
        try {
            $twint = FormTwintCore::create_for_transaction( $shim->get_id(), T4A_MERCHANT_UUID );
            if ( ! $twint ) {
                Journal::error( sprintf( 'FormWebhookHandler: FormTwintCore null for order %d', $shim->get_id() ) );
                return;
            }
            $twint->transactionHandler->handle_webhook_response( $uuid !== '' ? $uuid : null );
        } catch ( \Throwable $e ) {
            Journal::error( sprintf(
                'FormWebhookHandler engine error: %s in %s:%d',
                $e->getMessage(), $e->getFile(), $e->getLine()
            ) );
        }
    }

    // -----------------------------------------------------------------
    // Signature
    // -----------------------------------------------------------------

    private static function verify_signature( FormOrderShim $shim ) : bool
    {
        // phpcs:disable WordPress.Security.NonceVerification.Recommended
        $provided = isset( $_GET[ 's' ] ) ? sanitize_text_field( wp_unslash( (string) $_GET[ 's' ] ) ) : '';
        // phpcs:enable WordPress.Security.NonceVerification.Recommended
        if ( $provided === '' ) { return false; }

        // Signature format : « <ts>-<hash> » — we re-derive the hash
        // using the timestamp embedded in the provided value, then :
        //   1. constant-time compare the two,
        //   2. reject if the timestamp is older than the configured
        //      expiration window (default 60 s, exposed via the constant
        //      T4A_REDIRECT_SIGNATURE_EXPIRATION for merchants whose
        //      TWINT flow legitimately takes longer). This closes the
        //      « signature replayable indefinitely » gap that came from
        //      the previous fixed-timestamp implementation.
        $dash = strpos( $provided, '-' );
        if ( $dash === false ) { return false; }
        $ts = (int) substr( $provided, 0, $dash );
        if ( $ts <= 0 ) { return false; }

        $max_age = defined( 'T4A_REDIRECT_SIGNATURE_EXPIRATION' ) ? (int) T4A_REDIRECT_SIGNATURE_EXPIRATION : 60;
        // Allow a symmetric skew so a legit customer whose clock is
        // slightly ahead doesn't hit false-negatives ; the hard bound
        // is still ~ max_age seconds.
        if ( abs( time() - $ts ) > $max_age ) { return false; }

        $expected = self::build_redirect_key( $shim, $ts );
        return hash_equals( (string) $expected, $provided );
    }

    /**
     * Same signature scheme that FormConfigProvider::build_signature() uses
     * when building the TWINT redirect/cancel URLs at start_order time.
     */
    private static function build_redirect_key( FormOrderShim $shim, ?int $ts = null ) : string
    {
        $data = [
            'order_id'           => $shim->get_id(),
            'amount'             => $shim->get_total(),
            'currency'           => $shim->get_currency(),
            'billing_first_name' => $shim->get_billing_first_name(),
            'billing_last_name'  => $shim->get_billing_last_name(),
            'billing_postcode'   => $shim->get_billing_postcode(),
            'integration'        => 'twnt',
        ];
        return (string) WpBridge::create_hash_key( http_build_query( $data, '', ';' ), $ts ?? time() );
    }
}
