<?php

namespace Pay4AllForMypos\Integrations;

use Pay4AllForMypos\Backend\ConfigPage;
use Pay4AllForMypos\Checkout\MyposGateway;
use Pay4AllForMypos\Checkout\PendingPaymentsManager;
use Pay4AllForMypos\Support\Countries;
use Pay4AllForMypos\Support\Journal;

defined( 'ABSPATH' ) || exit;

/**
 * Bridge between Pay4All for myPOS and the Pay4All Form (« Pay4All Shop »)
 * plugin. When Pay4All Form is installed and active, this class:
 *
 *  1. Adds « myPOS » to the list of payment methods Pay4All Form
 *     exposes per-form (filter `p4all_available_payment_methods`).
 *  2. Intercepts the order-creation flow (filter `p4all_process_payment`)
 *     to build a signed IPCPurchase payload, persist it in a short-lived
 *     transient keyed by order id, and return a redirect URL that points
 *     at an intermediate `admin-post.php?action=pay4all_for_mypos_form_redirect`
 *     handler. That handler renders the auto-submit HTML form that posts
 *     the customer to the myPOS-hosted checkout page.
 *  3. Persists the pending row in the plugin's `pending_payments` table
 *     via `PendingPaymentsManager::track_pending()` so the hourly sweeper
 *     can time it out if myPOS never notifies back.
 *
 *   IMPORTANT: unlike the Stripe integration (which just returns a hosted
 *   checkout URL), myPOS requires a real HTML `<form method="post">` posted
 *   from the customer's browser so signature + session cookies + fingerprint
 *   travel together. That's why we cannot return the myPOS endpoint URL
 *   directly, we have to go through the intermediate redirect page.
 */
final class Pay4AllForm
{
    public const METHOD_KEY = 'mypos';

    /** Post meta storing the myPOS IPC_Trnref once the notify is received. */
    public const META_TRNREF = '_pay4all_for_mypos_trnref';

    /**
     * Post meta storing the URL of the page that hosted the p4form
     * shortcode - captured at payment-preparation time from HTTP_REFERER.
     * Used to bring the customer back to that exact page after myPOS
     * redirects them to URL_OK / URL_Cancel (fallback: shop / home).
     *
     * Same key as pay4all-for-twint uses so the two plugins agree on where
     * the customer came from when a merchant switches gateway mid-order.
     */
    public const META_FORM_REFERER = '_p4all_form_referer';

    /** Transient prefix for the pending IPCPurchase payload (30 min TTL). */
    private const TRANSIENT_PREFIX = 'pay4all_for_mypos_form_payload_';

    /** Intermediate admin-post action rendering the auto-submit form. */
    public const REDIRECT_ACTION = 'pay4all_for_mypos_form_redirect';

    public static function register() : void
    {
        if ( ! self::is_pay4all_form_active() ) {
            return;
        }

        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- third-party Pay4All Shop hook name, verbatim.
        add_filter( 'p4all_available_payment_methods', [ self::class, 'register_method' ], 20, 2 );
        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- third-party Pay4All Shop hook name, verbatim.
        add_filter( 'p4all_process_payment',           [ self::class, 'handle_payment' ],  20, 3 );
        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- third-party Pay4All Shop hook name, verbatim.
        add_filter( 'p4all_free_order_paid_status',    [ self::class, 'filter_free_paid_status' ], 20, 3 );

        // Intermediate redirect page (front-end + logged-out visitors).
        add_action( 'admin_post_'        . self::REDIRECT_ACTION, [ self::class, 'handle_redirect' ] );
        add_action( 'admin_post_nopriv_' . self::REDIRECT_ACTION, [ self::class, 'handle_redirect' ] );

        // Admin refund panel on the order detail screen.
        FormOrderRefund::register();
        add_action( 'admin_notices', [ FormOrderRefund::class, 'maybe_print_notice' ] );
    }

    public static function is_pay4all_form_active() : bool
    {
        return class_exists( '\\Pay4All\\Form\\PostTypes' )
            && class_exists( '\\Pay4All\\Form\\Domain\\Order' );
    }

    public static function is_mypos_ready() : bool
    {
        return ConfigPage::has_credentials() && ! empty( ConfigPage::enabled_methods() );
    }

    /**
     * Adds 'mypos' to the per-form payment-method picker.
     *
     * @param array<string,array{label:string,description?:string}> $methods
     * @param int $form_id
     * @return array
     */
    public static function register_method( $methods, $form_id ) : array
    {
        if ( ! is_array( $methods ) ) { $methods = []; }
        if ( ! self::is_mypos_ready() ) {
            return $methods;
        }

        // Delegate the customer-facing description to Pay4All Shop's Schema
        // when available - the merchant edits it under Réglages > Paiements
        // per language. Falls back to our own localized default otherwise.
        $description = '';
        if ( class_exists( '\\Pay4All\\Form\\Admin\\Settings\\Schema' ) ) {
            $description = trim( \Pay4All\Form\Admin\Settings\Schema::payment_description( self::METHOD_KEY ) );
        }
        if ( $description === '' ) {
            $description = __( 'Pay by card, TWINT, iDEAL or Satispay. You will be redirected to a secure page hosted by myPOS.', 'pay4all-for-mypos' );
        }

        $methods[ self::METHOD_KEY ] = [
            'label'       => self::compose_label(),
            'description' => $description,
            'icon'        => defined( 'PAY4ALL_FOR_MYPOS_PLUGIN_URL' )
                ? PAY4ALL_FOR_MYPOS_PLUGIN_URL . 'assets/img/mypos-logo.svg'
                : '',
        ];
        return $methods;
    }

    /**
     * Builds the customer-facing label listing the payment methods the
     * merchant activated for myPOS at the wizard step 4. Falls back to
     * the generic « myPOS » string when nothing is enabled.
     */
    private static function compose_label() : string
    {
        $fallback = __( 'Pay by myPOS', 'pay4all-for-mypos' );
        $enabled  = ConfigPage::enabled_methods();
        if ( empty( $enabled ) ) { return $fallback; }

        $labels = [
            'card'     => __( 'Card', 'pay4all-for-mypos' ),
            'ideal'    => 'iDEAL',
            'both'     => __( 'Card + iDEAL', 'pay4all-for-mypos' ),
            'satispay' => 'Satispay',
            'twint'    => 'TWINT',
            'iris'     => 'IRIS',
            'all'      => __( 'All methods available at myPOS', 'pay4all-for-mypos' ),
        ];
        $parts = [];
        foreach ( $enabled as $slug ) {
            $parts[] = $labels[ $slug ] ?? ucfirst( str_replace( '_', ' ', (string) $slug ) );
        }
        $parts = array_values( array_unique( array_filter( $parts, 'strlen' ) ) );
        if ( empty( $parts ) ) { return $fallback; }
        return implode( ', ', $parts );
    }

    /**
     * Intercepts Pay4All Form's `p4all_process_payment` filter.
     *
     * @param array{success:bool, redirect_url?:string, error?:string} $result
     * @param int    $order_id
     * @param string $payment_method
     * @return array
     */
    public static function handle_payment( $result, $order_id, $payment_method ) : array
    {
        if ( $payment_method !== self::METHOD_KEY ) {
            return $result;
        }
        if ( ! is_array( $result ) ) {
            $result = [ 'success' => true, 'redirect_url' => '' ];
        }

        $order = get_post( (int) $order_id );
        if ( ! $order || $order->post_type !== \Pay4All\Form\PostTypes::ORDER ) {
            $result['success'] = false;
            $result['error']   = __( 'Pay4All for myPOS: order not found.', 'pay4all-for-mypos' );
            return $result;
        }

        $totals    = \Pay4All\Form\Domain\Order::totals( (int) $order_id );
        $customer  = \Pay4All\Form\Domain\Order::customer( (int) $order_id );
        $order_num = (string) \Pay4All\Form\Domain\Order::number( (int) $order_id );

        $total = (float) ( $totals['total'] ?? 0 );
        if ( $total <= 0 ) {
            $result['success'] = false;
            $result['error']   = __( 'Pay4All for myPOS: total amount is zero or negative.', 'pay4all-for-mypos' );
            return $result;
        }

        $currency_raw = $totals['currency'] ?? 'CHF';
        if ( is_array( $currency_raw ) ) {
            $currency_raw = (string) ( $currency_raw['code'] ?? 'CHF' );
        }
        $currency = strtoupper( (string) $currency_raw );

        $mode = ConfigPage::get_mode();
        $sid  = ConfigPage::get_pack_field( $mode, 'sid' );
        $cn   = ConfigPage::get_pack_field( $mode, 'cn' );
        $idx  = ConfigPage::get_pack_field( $mode, 'idx' );
        if ( $sid === '' || $cn === '' || $idx === '' ) {
            $result['success'] = false;
            $result['error']   = __( 'Pay4All for myPOS: missing myPOS Configuration Pack.', 'pay4all-for-mypos' );
            return $result;
        }

        // Build the same signed IPCPurchase payload IpcFormBuilder would
        // produce for a WC order - but from Pay4All Shop order data. Kept
        // inline (rather than teaching IpcFormBuilder to accept a p4form
        // order) so the WC-side stays strictly typed on \WC_Order.
        $fields = self::build_p4form_purchase_fields(
            (int) $order_id,
            $order_num,
            $total,
            $currency,
            $customer,
            $mode
        );
        if ( $fields === [] ) {
            $result['success'] = false;
            $result['error']   = __( 'Pay4All for myPOS: unable to sign the payment request.', 'pay4all-for-mypos' );
            return $result;
        }

        $endpoint = $mode === 'live' ? PAY4ALL_FOR_MYPOS_IPC_URL_LIVE : PAY4ALL_FOR_MYPOS_IPC_URL_TEST;

        // Persist the payload for 30 min under a per-order transient. The
        // intermediate redirect page reads it, renders the auto-submit form
        // then deletes it. Using a transient (rather than post-meta) keeps
        // the WooCommerce-shaped payload out of the p4form order metadata.
        set_transient(
            self::TRANSIENT_PREFIX . (int) $order_id,
            [ 'fields' => $fields, 'url' => $endpoint ],
            30 * MINUTE_IN_SECONDS
        );

        \Pay4All\Form\Domain\Order::update_payment_status( (int) $order_id, 'pending' );
        PendingPaymentsManager::track_pending( (int) $order_id, $mode );

        // Capture the page that hosted the form so the return handler can
        // bring the customer back to it after myPOS. Same meta key as the
        // TWINT plugin uses so both agree.
        $referer = isset( $_SERVER['HTTP_REFERER'] )
            ? esc_url_raw( wp_unslash( (string) $_SERVER['HTTP_REFERER'] ) )
            : '';
        if ( $referer !== '' ) {
            update_post_meta( (int) $order_id, self::META_FORM_REFERER, $referer );
        }

        // Sign the redirect URL with a short-lived nonce so a stray referrer
        // link can't trigger the auto-submit for someone else's order.
        $redirect = add_query_arg(
            [
                'action'   => self::REDIRECT_ACTION,
                'order_id' => (int) $order_id,
                '_wpnonce' => wp_create_nonce( self::REDIRECT_ACTION . '_' . (int) $order_id ),
            ],
            admin_url( 'admin-post.php' )
        );

        $result['success']      = true;
        $result['redirect_url'] = $redirect;
        return $result;
    }

    /**
     * Renders the intermediate auto-submit HTML form that redirects the
     * customer to the myPOS checkout. Reads the payload from the transient
     * written by `handle_payment()` and deletes it after emitting the page.
     *
     * Serves both logged-in and logged-out visitors (customers are typically
     * anonymous on Pay4All Shop) - the nonce alone protects against CSRF.
     */
    public static function handle_redirect() : void
    {
        $order_id = isset( $_GET['order_id'] ) ? (int) $_GET['order_id'] : 0;
        $nonce    = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['_wpnonce'] ) ) : '';
        if ( $order_id <= 0 || ! wp_verify_nonce( $nonce, self::REDIRECT_ACTION . '_' . $order_id ) ) {
            wp_die( esc_html__( 'Invalid or expired payment link.', 'pay4all-for-mypos' ), 400 );
        }

        $payload = get_transient( self::TRANSIENT_PREFIX . $order_id );
        if ( ! is_array( $payload ) || empty( $payload['fields'] ) || empty( $payload['url'] ) ) {
            wp_die( esc_html__( 'Payment session expired. Please restart the order from the checkout.', 'pay4all-for-mypos' ), 410 );
        }
        delete_transient( self::TRANSIENT_PREFIX . $order_id );

        $url    = (string) $payload['url'];
        $fields = (array) $payload['fields'];

        // Standalone HTTP response (no wp_head/footer). We still route CSS
        // + JS through wp_enqueue_* + wp_print_styles / wp_print_footer_scripts
        // so Plugin Check accepts them ; bare <link>/<script src> tags
        // trigger the NonEnqueuedStylesheet / NonEnqueuedScript rules.
        wp_enqueue_style(
            'pay4all-for-mypos-receipt',
            PAY4ALL_FOR_MYPOS_PLUGIN_URL . 'assets/css/receipt.css',
            [],
            PAY4ALL_FOR_MYPOS_PLUGIN_VERSION
        );
        wp_enqueue_script(
            'pay4all-for-mypos-receipt',
            PAY4ALL_FOR_MYPOS_PLUGIN_URL . 'assets/js/receipt.js',
            [],
            PAY4ALL_FOR_MYPOS_PLUGIN_VERSION,
            true
        );

        nocache_headers();
        header( 'Content-Type: text/html; charset=utf-8' );
        header( 'X-Robots-Tag: noindex, nofollow' );
        ?>
<!doctype html>
<html lang="<?php echo esc_attr( substr( (string) get_locale(), 0, 2 ) ); ?>">
<head>
    <meta charset="utf-8">
    <meta name="robots" content="noindex,nofollow">
    <title><?php esc_html_e( 'Redirecting to myPOS', 'pay4all-for-mypos' ); ?></title>
    <?php wp_print_styles( [ 'pay4all-for-mypos-receipt' ] ); ?>
</head>
<body>
    <div class="pay4all-mypos-loading">
        <p class="pay4all-mypos-loading__msg"><?php esc_html_e( 'Redirecting you to the secure myPOS payment page...', 'pay4all-for-mypos' ); ?></p>
        <div class="pay4all-mypos-loading__spinner"></div>
    </div>
    <form id="pay4all-for-mypos-form"
          class="pay4all-mypos-autosubmit"
          data-pay4all-mypos-autosubmit
          method="post"
          action="<?php echo esc_url( $url ); ?>">
        <?php foreach ( $fields as $name => $value ) : ?>
            <input type="hidden" name="<?php echo esc_attr( (string) $name ); ?>" value="<?php echo esc_attr( (string) $value ); ?>">
        <?php endforeach; ?>
        <noscript>
            <p class="pay4all-mypos-autosubmit__noscript"><?php esc_html_e( 'JavaScript is disabled. Click the button below to continue to myPOS.', 'pay4all-for-mypos' ); ?></p>
            <p class="pay4all-mypos-autosubmit__noscript"><button type="submit"><?php esc_html_e( 'Continue to myPOS', 'pay4all-for-mypos' ); ?></button></p>
        </noscript>
    </form>
    <?php wp_print_scripts( [ 'pay4all-for-mypos-receipt' ] ); ?>
</body>
</html>
        <?php
        exit;
    }

    /**
     * Build a signed IPCPurchase field array from a Pay4All Shop order.
     * Mirrors IpcFormBuilder::build_purchase() for the WC-order path but
     * reads its data from the Pay4All Form Domain layer.
     *
     * @param array<string,mixed> $customer
     * @return array<string,string>  Empty array if signature fails.
     */
    private static function build_p4form_purchase_fields(
        int    $order_id,
        string $order_num,
        float  $total,
        string $currency,
        array  $customer,
        string $mode
    ) : array {
        $sid = ConfigPage::get_pack_field( $mode, 'sid' );
        $cn  = ConfigPage::get_pack_field( $mode, 'cn' );
        $idx = ConfigPage::get_pack_field( $mode, 'idx' );

        // Single wc-api callback URL used for OK / CANCEL / Notify - matches
        // the official myPOS plugin's approach. Extra query params (ipc, order_id,
        // p4all_form) were rejected by myPOS with Error 5. The dispatcher
        // in WebhookHandler routes on IPCmethod (POST body from myPOS)
        // and finds the target p4form order via OrderID lookup, so we don't
        // need any URL-level metadata anymore.
        $base_url   = str_replace( 'http:', 'https:', home_url( '/?wc-api=wc_gateway_pay4all_for_mypos' ) );
        $url_ok     = $base_url;
        $url_cancel = $base_url;
        $url_notify = $base_url;

        $iso2   = strtoupper( (string) ( $customer['country'] ?? 'CH' ) );
        $mypos3 = Countries::iso2_to_mypos_code( $iso2 ) ?? 'CHE';

        $methods = ConfigPage::enabled_methods();
        $ipc_pm  = self::methods_to_ipc_codes( $methods );

        $customer_ip = isset( $_SERVER['REMOTE_ADDR'] )
            ? sanitize_text_field( wp_unslash( (string) $_SERVER['REMOTE_ADDR'] ) )
            : '';

        $amount   = number_format( $total, 2, '.', '' );
        $currency = strtoupper( $currency );

        // Field NAMES must match myPOS' spec exactly : `keyindex` (lower
        // case), `URL_CANCEL` (all caps), `PaymentMethod` (singular),
        // `CustomerIP` (required for anti-fraud). Any deviation triggers
        // E_MISSING_REQ_PARAMS at the checkout redirect.
        $fields = [
            'IPCmethod'    => 'IPCPurchase',
            'IPCVersion'   => '1.4',
            'IPCLanguage'  => self::current_language(),
            'WalletNumber' => $cn,
            'SID'          => $sid,
            'keyindex'     => $idx,
            'Source'       => 'sc_wp_p4all_form ' . PAY4ALL_FOR_MYPOS_PLUGIN_VERSION
                                . ' ' . PHP_VERSION
                                . ' ' . get_bloginfo( 'version' ),

            'Amount'     => $amount,
            'Currency'   => $currency,
            // Per-attempt unique prefix - shared demo Packs cause OrderID
            // collisions on myPOS' side that surface as Error 5. Same
            // pattern as IpcFormBuilder + the official myPOS plugin. The
            // WebhookHandler extracts the trailing numeric segment to
            // find the Pay4All Shop order back.
            'OrderID'    => uniqid( '', false ) . '_' . (string) $order_id,
            'URL_OK'     => $url_ok,
            'URL_CANCEL' => $url_cancel,
            'URL_Notify' => $url_notify,

            // Pay4All Shop stores form field values under the field's own
            // key (e.g. `firstname`, `lastname`, `pay4allshop_npa`). Older
            // integrations sometimes wrote `first_name` / `last_name` /
            // `npa` - accept both so a merchant renaming a field doesn't
            // break the payload. Empty required fields fall back to "-"
            // (myPOS rejects empty strings with Error 5, but "-" prints
            // discreetly on their hosted page).
            'CustomerIP'         => $customer_ip !== '' ? $customer_ip : '0.0.0.0',
            'CustomerEmail'      => (string) ( $customer['email']      ?? 'noreply@' . ( wp_parse_url( home_url(), PHP_URL_HOST ) ?: 'example.com' ) ),
            'CustomerFirstNames' => self::escape( self::pick( $customer, [ 'firstname', 'first_name' ], '-' ) ),
            'CustomerFamilyName' => self::escape( self::pick( $customer, [ 'lastname',  'last_name'  ], '-' ) ),
            'CustomerCountry'    => $mypos3,
            'CustomerCity'       => self::escape( self::pick( $customer, [ 'city' ], '-' ) ),
            'CustomerZIPCode'    => self::pick( $customer, [ 'pay4allshop_npa', 'npa', 'zip' ], '0000' ),
            'CustomerAddress'    => self::escape( self::pick( $customer, [ 'address' ], '-' ) ),
            'CustomerPhone'      => self::pick( $customer, [ 'phone' ], '' ),
            'Note'               => 'Pay4All Shop - Order ' . ( $order_num !== '' ? $order_num : (string) $order_id ),

            'CardTokenRequest'          => '0',
            // Boolean-ish flag: 1 = "post the extra payment parameters back
            // in the IPCPurchaseNotify". myPOS rejects any other value with
            // Error 5 - confirmed by diffing against the official plugin.
            'PaymentParametersRequired' => '1',
            'PaymentMethod'             => $ipc_pm,
        ];

        // Cart items are REQUIRED by myPOS. The p4form checkout doesn't
        // give us per-line data here (only the aggregated total), so we
        // emit ONE synthetic line covering the whole order. Enough to
        // satisfy the E_MISSING_REQ_PARAMS: cart_items validation.
        $fields[ 'Article_1'  ] = self::escape( 'Order ' . ( $order_num !== '' ? $order_num : (string) $order_id ) );
        $fields[ 'Quantity_1' ] = '1';
        $fields[ 'Price_1'    ] = $amount;
        $fields[ 'Amount_1'   ] = $amount;
        $fields[ 'Currency_1' ] = $currency;
        $fields[ 'CartItems'  ] = '1';

        $signature = self::sign( $fields, $mode );
        if ( $signature === '' ) {
            return [];
        }
        $fields['Signature'] = $signature;
        return $fields;
    }

    /**
     * Same METHOD_MAP as IpcFormBuilder - duplicated (rather than exposed)
     * because IpcFormBuilder's map is private and this class is a peer, not
     * a subclass. Keeps IpcFormBuilder's public surface unchanged.
     */
    private static function methods_to_ipc_codes( array $enabled_methods ) : string
    {
        static $map = [
            'card'     => 1,
            'ideal'    => 2,
            'both'     => 3,
            'satispay' => 4,
            'twint'    => 5,
            'iris'     => 6,
        ];
        // "3" is myPOS' code for "all methods on one page". The official
        // plugin sends "3" as the default and it works ; sending "-1"
        // triggers Error 5 even though the myPOS docs mention it.
        if ( in_array( 'all', $enabled_methods, true ) || empty( $enabled_methods ) ) {
            return '3';
        }
        $codes = [];
        foreach ( $enabled_methods as $slug ) {
            $slug = strtolower( (string) $slug );
            if ( isset( $map[ $slug ] ) ) {
                $codes[] = $map[ $slug ];
            }
        }
        $codes = array_values( array_unique( $codes ) );
        return empty( $codes ) ? '3' : implode( ',', $codes );
    }

    private static function current_language() : string
    {
        if ( defined( 'ICL_LANGUAGE_CODE' ) && ICL_LANGUAGE_CODE ) {
            return (string) ICL_LANGUAGE_CODE;
        }
        return substr( (string) get_locale(), 0, 2 ) ?: 'en';
    }

    private static function escape( string $value ) : string
    {
        return str_replace( [ '\\', '/', ':', '*', '?', '"', '<', '>', '|' ], '', $value );
    }

    /**
     * Return the first non-empty value found under any of the given keys
     * in the customer array, or $fallback if none matches. Lets us accept
     * both the Pay4All Shop-native key ("firstname") and the legacy
     * "first_name" variant without a chain of ternary operators.
     *
     * @param array<string,mixed> $customer
     * @param string[]            $keys
     */
    private static function pick( array $customer, array $keys, string $fallback ) : string
    {
        foreach ( $keys as $k ) {
            if ( isset( $customer[ $k ] ) ) {
                $v = trim( (string) $customer[ $k ] );
                if ( $v !== '' ) {
                    return $v;
                }
            }
        }
        return $fallback;
    }

    /**
     * Sign an ordered field array. Identical to IpcClient::sign() and
     * IpcFormBuilder::sign() - see IpcClient for the scheme description.
     *
     * @param array<string,scalar> $ordered_fields
     */
    private static function sign( array $ordered_fields, string $mode ) : string
    {
        $pk_pem = ConfigPage::get_pack_field( $mode, 'pk' );
        if ( $pk_pem === '' ) {
            Journal::error( 'Pay4AllForm signature: private key missing from Pack' );
            return '';
        }

        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- required by myPOS signature scheme
        $conc = base64_encode( implode( '-', array_values( $ordered_fields ) ) );

        $pk = openssl_pkey_get_private( $pk_pem );
        if ( ! $pk ) {
            Journal::error( 'Pay4AllForm signature: openssl_pkey_get_private() failed' );
            return '';
        }

        $signature = '';
        $ok        = openssl_sign( $conc, $signature, $pk, OPENSSL_ALGO_SHA256 );
        if ( ! $ok || $signature === '' ) {
            Journal::error( 'Pay4AllForm signature: openssl_sign() failed' );
            return '';
        }

        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- required by myPOS signature scheme
        return base64_encode( $signature );
    }

    /**
     * URL to bring the customer back to after myPOS redirects them to
     * URL_OK / URL_Cancel. Appends the same query flags Pay4All Shop's
     * frontend shortcode already understands :
     *
     *   ok     -> ?p4all_thankyou=<order_number>&pay4all_for_mypos_order=<id>
     *   cancel -> ?p4all_cancelled=<order_number>&pay4all_for_mypos_order=<id>
     *
     * The shortcode renders a thank-you or "commande annulée" panel from
     * these hints ; the pay4all_for_mypos_order param is kept for later
     * lookups (matches Stripe / TWINT plugin conventions).
     *
     * @param string $outcome 'ok' | 'cancel'
     */
    public static function get_return_url( int $order_id, string $outcome = 'ok' ) : string
    {
        $base = (string) get_post_meta( $order_id, self::META_FORM_REFERER, true );
        if ( $base === '' ) {
            $base = home_url( '/' );
        }

        // p4form order_number ("P4ALL-000058") - what the shortcode expects
        // for the ?p4all_thankyou / ?p4all_cancelled marker.
        $number = class_exists( '\\Pay4All\\Form\\Domain\\Order' )
            ? (string) \Pay4All\Form\Domain\Order::number( $order_id )
            : (string) $order_id;

        $args = [ 'pay4all_for_mypos_order' => (int) $order_id ];
        if ( $outcome === 'cancel' ) {
            $args['p4all_cancelled'] = $number;
        } else {
            $args['p4all_thankyou'] = $number;
        }

        return add_query_arg( array_map( 'rawurlencode', $args ), $base );
    }

    /**
     * Look up a p4form order by the myPOS transaction reference stored
     * on it. Used by WebhookHandler when routing an IPCPurchaseNotify
     * back to the originating order after the fact (edge case: an
     * IPCPurchase completes but our OrderID lookup fails - see
     * WebhookHandler::find_order_id_by_trnref()).
     */
    public static function find_order_by_trnref( string $trnref ) : ?\WP_Post
    {
        if ( $trnref === '' || ! self::is_pay4all_form_active() ) {
            return null;
        }
        $posts = get_posts( [
            'post_type'      => \Pay4All\Form\PostTypes::ORDER,
            'post_status'    => 'any',
            'posts_per_page' => 1,
            'meta_key'       => self::META_TRNREF, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
            'meta_value'     => $trnref,           // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
            'fields'         => 'all',
            'no_found_rows'  => true,
        ] );
        if ( ! is_array( $posts ) || empty( $posts ) ) {
            return null;
        }
        return $posts[0] instanceof \WP_Post ? $posts[0] : null;
    }

    /**
     * Mark a p4form order as paid after myPOS confirms the payment
     * (either via IPCPurchaseNotify or via a manual reconcile).
     *
     * TODO: wire this into WebhookHandler once the sibling issue about
     * routing p4all_form=1 flagged notifies is resolved. Currently only
     * called from FormOrderRefund on manual reconcile.
     */
    /**
     * Hook for Pay4All Shop's `p4all_free_order_paid_status` filter :
     * returns the merchant's own paid-status config when the shopper
     * actually picked myPOS. Otherwise passes the default through so
     * a non-gateway pick (Retrait / Virement / …) keeps its natural
     * target status.
     */
    public static function filter_free_paid_status( $default, $order_id, $payment_method )
    {
        if ( $payment_method !== self::METHOD_KEY ) { return $default; }
        return ConfigPage::paid_status_for_p4all_form_order( (int) $order_id );
    }

    public static function mark_paid( int $order_id, string $trnref, string $note_prefix ) : void
    {
        if ( $trnref !== '' ) {
            update_post_meta( $order_id, self::META_TRNREF, $trnref );
        }
        \Pay4All\Form\Domain\Order::update_payment_status( $order_id, 'paid' );

        if ( class_exists( '\\Pay4All\\Form\\Domain\\Order' ) ) {
            $target = ConfigPage::paid_status_for_p4all_form_order( $order_id );
            \Pay4All\Form\Domain\Order::update_status( $order_id, $target );
        }

        wp_insert_comment( [
            'comment_post_ID'  => $order_id,
            'comment_author'   => 'Pay4All for myPOS',
            'comment_content'  => sprintf(
                /* translators: 1: prefix (e.g. « return-URL » or « webhook »), 2: myPOS IPC_Trnref */
                __( '%1$s - myPOS payment confirmed (%2$s).', 'pay4all-for-mypos' ),
                $note_prefix,
                $trnref !== '' ? $trnref : __( 'no id', 'pay4all-for-mypos' )
            ),
            'comment_type'     => 'pay4all_for_mypos_note',
            'comment_approved' => 1,
        ] );

        MyposGateway::SLUG; // keep the use-statement referenced for phpstan
    }

    public static function mark_failed( int $order_id, string $reason ) : void
    {
        \Pay4All\Form\Domain\Order::update_payment_status( $order_id, 'failed' );
        wp_insert_comment( [
            'comment_post_ID'  => $order_id,
            'comment_author'   => 'Pay4All for myPOS',
            'comment_content'  => sprintf(
                /* translators: %s = failure reason */
                __( 'myPOS payment failed - %s.', 'pay4all-for-mypos' ),
                $reason
            ),
            'comment_type'     => 'pay4all_for_mypos_note',
            'comment_approved' => 1,
        ] );
    }
}
