<?php
/**
 * Bridge between Pay4All Twint (this plugin) and Pay4All Form (the free WP.org plugin).
 *
 *  - Adds the `twint_auto` payment method to the Form's payment picker.
 *  - Routes `p4all_process_payment` through the TWINT wallet engine via
 *    FormPaymentLauncher, using a FormOrderShim duck-typed around the Form's
 *    p4all_order CPT. Zero changes to the WC checkout path.
 *  - Registers FormWebhookHandler so the `?p4all_form_pay=…` callbacks land on
 *    handlers that mark Form orders (not WC ones) as paid/failed/cancelled.
 *  - Plugs FormOrderRegistry into OrderDataRepository's meta resolver so the
 *    transaction id and other engine-managed fields persist correctly on the
 *    Form order's post meta.
 *
 * Pay4All Form is fully optional. If it's not installed, these hooks are
 * registered but never fire (the Form plugin is the only thing that fires
 * `p4all_process_payment`), so this class stays a no-op for WC-only installs.
 */

namespace Twnt4All\Pay4All;

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

final class FormIntegration
{
    public static function bootstrap() : void
    {
        // Public hooks consumed by Pay4All Form.
        add_filter( 'p4all_available_payment_methods', [ self::class, 'add_twint_auto' ], 10, 2 );
        add_filter( 'p4all_payment_required',          [ self::class, 'payment_required' ], 10, 2 );
        add_filter( 'p4all_process_payment',           [ self::class, 'process_payment' ], 10, 3 );

        // Internal: connect the launcher to the inner "charge this order" filter
        // we expose from process_payment(). Kept as a filter (not a direct call)
        // so the launcher can be swapped or short-circuited from tests.
        add_filter( 'p4all_pro_charge_form_order_twint', [ self::class, 'charge_via_engine' ], 10, 3 );

        // Internal: tell OrderDataRepository how to resolve Form order ids.
        add_filter( 'p4all_pro_resolve_order_for_meta',  [ self::class, 'resolve_order_for_meta' ], 10, 2 );

        // Public callback endpoints for the TWINT redirect/cancel + OUN webhook.
        FormWebhookHandler::register();

        // Admin refund panel injected under the "Ordered items" section on
        // Pay4All Form order screens paid with TWINT (hook exposed by
        // pay4all-form >=1.3.6).
        FormOrderRefund::register();

        // TWINT terminals are configured with a SINGLE OUN URL at the merchant
        // portal - it always points at `?wc-api=t4a_twint_webhook_oun` for this
        // install. When TWINT POSTs the OUN for a Form order, the WC handler
        // would fail (no shop order instance) and never flip the payment_status. We
        // intercept at higher priority and short-circuit for Form orders.
        // Guard on WC presence : without WC the `?wc-api=` route doesn't exist
        // and TWINT can't hit it anyway ; skipping the hook keeps this file
        // WC-runtime-free on pay4all-form-only installs.
        if ( class_exists( 'WooCommerce' ) ) {
            add_action(
                'woocommerce_api_' . T4A_PREFIX . '_webhook_oun',
                [ self::class, 'maybe_handle_oun_for_form' ],
                5
            );
        }

        // Background polling for in-flight Form TWINT orders. The two job
        // instances install the WP_Async_Task listeners that turn
        //   do_action('p4all_form_monitor_order[_immediate]', …)
        // into a non-blocking HTTP loopback. Without these the SOAP polling
        // would run synchronously inside the customer's submit request and
        // delay the redirect to TWINT by 30–60 seconds.
        new FormMonitorJob();
        new FormImmediateMonitorJob();
        FormBackgroundMonitor::register();
    }

    // -----------------------------------------------------------------
    // Hook callbacks
    // -----------------------------------------------------------------

    /**
     * Inject the TWINT automated method into the list shown at Form checkout.
     *
     * @param array<string,array{label:string,description?:string}> $methods
     * @param int                                                   $form_id
     * @return array<string,array{label:string,description?:string}>
     */
    public static function add_twint_auto( array $methods, int $form_id ) : array
    {
        // Defer to Pay4All Form's Schema for the per-language description chain
        // (current lang -> primary lang -> legacy pay_twint_desc -> built-in
        // localised default). Guard the call: Form might not be loaded.
        $desc = '';
        if ( class_exists( '\\Pay4All\\Form\\Admin\\Settings\\Schema' ) ) {
            $desc = trim( \Pay4All\Form\Admin\Settings\Schema::payment_description( 'twint_auto' ) );
        }
        if ( $desc === '' ) {
            $desc = __( 'Pay for your order right now with the TWINT app. Payment status is confirmed automatically.', 'pay4all-for-twint' );
        }
        $methods[ 'twint_auto' ] = [
            'label'       => __( 'TWINT', 'pay4all-for-twint' ),
            'description' => $desc,
        ];
        return $methods;
    }

    public static function payment_required( $required, int $form_id )
    {
        return (bool) $required;
    }

    /**
     * Launch the TWINT charge once the Form has created the order.
     *
     * Return shape expected by Pay4All Form:
     *   [ 'success' => bool, 'redirect_url' => string, 'error' => string ]
     *
     * @param array<string,mixed> $result
     * @param int                 $order_id
     * @param string              $method
     * @return array<string,mixed>
     */
    public static function process_payment( $result, int $order_id, string $method ) : array
    {
        if ( $method !== 'twint_auto' ) {
            return is_array( $result ) ? $result : [ 'success' => true, 'redirect_url' => '' ];
        }

        // Order is created - flip it to "pending" until TWINT reports back.
        if ( class_exists( '\\Pay4All\\Form\\Domain\\Order' ) ) {
            \Pay4All\Form\Domain\Order::update_payment_status( $order_id, 'pending' );
        }

        $context = [
            'order_id' => $order_id,
            'method'   => $method,
            'source'   => 'pay4all-form',
        ];
        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- historical Pay4All Pro filter kept for backwards compatibility with downstream integrations (Pay4All Form).
        $charge = apply_filters( 'p4all_pro_charge_form_order_twint', null, $order_id, $context );

        if ( is_array( $charge ) && ! empty( $charge[ 'redirect_url' ] ) ) {
            return [
                'success'      => ! empty( $charge[ 'success' ] ),
                'redirect_url' => (string) $charge[ 'redirect_url' ],
            ];
        }

        // Engine returned nothing usable -> order stays in pending so the
        // merchant can still receive the email and reconcile manually.
        $error = ( is_array( $charge ) && ! empty( $charge[ 'error' ] ) )
            ? (string) $charge[ 'error' ]
            : __( 'Unable to start the TWINT payment.', 'pay4all-for-twint' );

        return [
            'success'      => false,
            'redirect_url' => '',
            'error'        => $error,
        ];
    }

    /**
     * Actually call the TWINT wallet engine for a Pay4All Form order.
     *
     * @param mixed              $charge   default is null (nothing handled yet)
     * @param int                $order_id
     * @param array<string,mixed> $context
     */
    public static function charge_via_engine( $charge, int $order_id, array $context )
    {
        // If a higher-priority listener already produced a charge, respect it.
        if ( is_array( $charge ) && ! empty( $charge[ 'redirect_url' ] ) ) {
            return $charge;
        }
        return FormPaymentLauncher::launch( $order_id );
    }

    /**
     * Surface FormOrderShim instances to OrderDataRepository so save/delete
     * meta calls operate on the right post for Form orders.
     *
     * @param mixed       $custom
     * @param int|string  $order_id
     */
    public static function resolve_order_for_meta( $custom, $order_id )
    {
        if ( $custom ) { return $custom; }
        return FormOrderRegistry::get_or_load( (int) $order_id );
    }

    /**
     * If the incoming OUN belongs to a p4all_order, route it through
     * FormTwintCore (which uses FormConfigProvider + FormDomainEvents) so the
     * payment_status flips to `paid` on the Form domain layer. Then exit so
     * the default WC handler doesn't run on a non-WC order.
     *
     * Hooked at priority 5 on `woocommerce_api_t4a_twint_webhook_oun`, ahead
     * of the default WC handler (priority 10).
     */
    public static function maybe_handle_oun_for_form() : void
    {
        // Origin check: TWINT server-to-server callback, no WP nonce possible.
        // We require the caller to know the exact order UUID we handed to
        // TWINT when start_order() was called (stored in _t4a_twint_order).
        // Only the plugin and TWINT know this UUID.
        // phpcs:disable WordPress.Security.NonceVerification.Recommended -- validated below via server-side UUID comparison ; no WP nonce available on this server-to-server OUN callback.
        $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

        \Twnt4All\Support\Journal::event( sprintf(
            'FormIntegration OUN: invoked (order_id=%d uuid=%s)',
            $order_id, $uuid
        ) );

        if ( $order_id <= 0 ) {
            \Twnt4All\Support\Journal::event( 'FormIntegration OUN: no order_id, defer to WC handler' );
            return;
        }
        if ( ! FormOrderShim::is_form_order_id( $order_id ) ) {
            \Twnt4All\Support\Journal::event( sprintf(
                'FormIntegration OUN: order %d is not a Form order, defer to WC handler',
                $order_id
            ) );
            return;
        }

        // UUID validation: fetch the stored server-side UUID for this order
        // and require an exact match via hash_equals (timing-safe).
        // OrderDataRepository::get_order_data() routes Form orders through the
        // shim automatically when passed an integer id.
        $expected_uuid = (string) \Twnt4All\Checkout\OrderDataRepository::get_order_data( $order_id, 'order_uuid' );
        if ( $expected_uuid === '' || $uuid === '' || ! hash_equals( $expected_uuid, $uuid ) ) {
            \Twnt4All\Support\Journal::event( sprintf(
                'FormIntegration OUN: UUID mismatch for order %d, rejecting',
                $order_id
            ) );
            status_header( 403 ); die();
        }

        \Twnt4All\Support\Journal::event( sprintf( 'FormIntegration OUN: claiming order %d', $order_id ) );

        if ( ! defined( 'T4A_MERCHANT_UUID' ) || ! T4A_MERCHANT_UUID ) {
            \Twnt4All\Support\Journal::error( 'FormIntegration OUN: T4A_MERCHANT_UUID missing' );
            status_header( 500 ); die();
        }

        try {
            $twint = FormTwintCore::create_for_transaction( $order_id, T4A_MERCHANT_UUID );
            if ( $twint ) {
                $twint->transactionHandler->handle_webhook_response( $uuid !== '' ? $uuid : null );
            }
        } catch ( \Throwable $e ) {
            \Twnt4All\Support\Journal::error( sprintf(
                'FormIntegration OUN: %s in %s:%d',
                $e->getMessage(), $e->getFile(), $e->getLine()
            ) );
        }

        // TWINT just needs a 200 to stop retrying. Bail out so the default WC
        // handler doesn't run a second polling pass on a non-existent WC order.
        status_header( 200 );
        die();
    }
}
