<?php

namespace Pay4AllForMypos\Checkout;

use Pay4AllForMypos\Backend\ConfigPage;
use Pay4AllForMypos\Support\Journal;

defined( 'ABSPATH' ) || exit;

/**
 * Sweeps orders that started an IPCPurchase redirect but never received
 * an IPCPurchaseNotify (customer closed the tab mid-payment, network
 * blip on myPOS side, etc.).
 *
 * The reconciliation strategy is intentionally light-touch:
 *   - a WP-Cron event fires hourly
 *   - we read up to N rows from `{prefix}pay4all_for_mypos_pending_payments`
 *     older than the "should have arrived by now" threshold
 *   - for each row we bump `checked_at` so the next sweep skips them if
 *     the sweep itself gets slow (avoids piling up work)
 *   - orders older than 24h without any notify are marked
 *     as the merchant's "cancelled" status and pulled from the queue.
 *
 * A future iteration could call myPOS' IPCTxnStatus to actively resolve
 * the outcome instead of just timing rows out. That's stubbed at the
 * bottom of `handle_pending_sweep()`.
 */
final class PendingPaymentsManager
{
    /** WP-Cron hook name (matches Bootstrap::on_deactivation()'s clear call). */
    public const CRON_HOOK = 'pay4all_for_mypos_check_order_statuses';

    /** Max rows to inspect per sweep to keep individual runs bounded. */
    private const SWEEP_BATCH = 25;

    /**
     * Give up on a pending row after this many seconds (24h). At that point
     * we mark the order cancelled and delete the row.
     */
    private const HARD_TIMEOUT_SECONDS = 86400;

    public static function register() : void
    {
        add_action( 'init', [ self::class, 'schedule_cron' ] );
        add_action( self::CRON_HOOK, [ self::class, 'handle_pending_sweep' ] );
    }

    /**
     * Register the hourly cron if it isn't already scheduled.
     */
    public static function schedule_cron() : void
    {
        if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
            wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', self::CRON_HOOK );
        }
    }

    /**
     * Track a new pending redirect. Called from MyposGateway::process_payment.
     */
    public static function track_pending( int $order_id, string $mode ) : void
    {
        if ( $order_id <= 0 ) {
            return;
        }
        $table = self::table_name();
        if ( ! self::table_exists() ) {
            return;
        }

        global $wpdb;

        // Insert-or-refresh - re-tries of the same order (customer paid,
        // failed, tries again) should reset the timer.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table.
        $existing = $wpdb->get_var( $wpdb->prepare(
            'SELECT id FROM ' . esc_sql( $table ) . ' WHERE order_id = %d LIMIT 1',
            $order_id
        ) );

        if ( $existing ) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table.
            $wpdb->update(
                $table,
                [
                    'status'     => 'pending',
                    'mode'       => $mode === 'live' ? 'live' : 'test',
                    'checked_at' => null,
                ],
                [ 'id' => (int) $existing ],
                [ '%s', '%s', '%s' ],
                [ '%d' ]
            );
            return;
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table.
        $wpdb->insert(
            $table,
            [
                'order_id' => $order_id,
                'mode'     => $mode === 'live' ? 'live' : 'test',
                'status'   => 'pending',
            ],
            [ '%d', '%s', '%s' ]
        );
    }

    /**
     * Drop a row - called from WebhookHandler once the outcome is known.
     */
    public static function remove_pending( int $order_id ) : void
    {
        if ( $order_id <= 0 || ! self::table_exists() ) {
            return;
        }
        global $wpdb;
        $table = self::table_name();
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table.
        $wpdb->delete( $table, [ 'order_id' => $order_id ], [ '%d' ] );
    }

    /**
     * Hourly sweep. Marks stale rows as failed and clears the queue so
     * abandoned checkouts don't accumulate forever.
     */
    public static function handle_pending_sweep() : void
    {
        if ( ! self::table_exists() ) {
            return;
        }

        global $wpdb;
        $table = self::table_name();

        // Grab a bounded batch of rows we haven't checked recently.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table.
        $rows = $wpdb->get_results( $wpdb->prepare(
            'SELECT id, order_id, mode, created_at, checked_at FROM ' . esc_sql( $table )
                . " WHERE status = 'pending' ORDER BY COALESCE(checked_at, created_at) ASC LIMIT %d",
            self::SWEEP_BATCH
        ) );

        if ( empty( $rows ) ) {
            return;
        }

        // time() returns UTC seconds since epoch - the direct replacement
        // for the deprecated current_time('timestamp', true) (deprecated
        // since WP 5.3 because of DST ambiguity).
        $now = time();

        foreach ( $rows as $row ) {
            $order_id   = (int) $row->order_id;
            $created_at = strtotime( (string) $row->created_at ) ?: $now;
            $age        = max( 0, $now - $created_at );

            // Bump checked_at first so failures below don't leave us in a
            // hot loop over the same row.
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table.
            $wpdb->update(
                $table,
                [ 'checked_at' => gmdate( 'Y-m-d H:i:s', $now ) ],
                [ 'id' => (int) $row->id ],
                [ '%s' ],
                [ '%d' ]
            );

            $order = wc_get_order( $order_id );
            if ( ! $order instanceof \WC_Order ) {
                // Order deleted - drop the row.
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table.
                $wpdb->delete( $table, [ 'id' => (int) $row->id ], [ '%d' ] );
                continue;
            }

            // If the notify already came in by the time the cron ran, just
            // drop the row and move on.
            if ( $order->is_paid() ) {
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table.
                $wpdb->delete( $table, [ 'id' => (int) $row->id ], [ '%d' ] );
                continue;
            }

            // Hard timeout - after 24h, assume the customer bailed and
            // cancel the order. Keeps the WC "Orders" list clean.
            if ( $age >= self::HARD_TIMEOUT_SECONDS ) {
                try {
                    $order->update_status(
                        ConfigPage::get_status_cancelled(),
                        __( 'Pay4All for myPOS: no myPOS notification received within 24 hours - cancelling.', 'pay4all-for-mypos' )
                    );
                    $order->save();
                } catch ( \Throwable $e ) {
                    Journal::error( 'Sweep cancel failed order #' . $order_id . ': ' . $e->getMessage() );
                }
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table.
                $wpdb->delete( $table, [ 'id' => (int) $row->id ], [ '%d' ] );
                Journal::event( 'Sweep expired order #' . $order_id );
                continue;
            }

            // TODO: call myPOS' IPCTxnStatus (via a future IpcClient method)
            // here to actively resolve the outcome instead of waiting for
            // the hard timeout. Best-effort placeholder for now - the row
            // will hit the 24h expiry branch above eventually.
        }
    }

    private static function table_name() : string
    {
        global $wpdb;
        return $wpdb->prefix . 'pay4all_for_mypos_pending_payments';
    }

    private static function table_exists() : bool
    {
        global $wpdb;
        $table = self::table_name();
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table existence check.
        $exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
        return $exists === $table;
    }
}
