<?php
/**
 * TitanCart Orphan Order Cleanup — Cron Handler.
 *
 * Auto-cancels orphan pending orders older than N hours (default 24).
 *
 * BACKGROUND
 * ----------
 * An "orphan pending" order is one that:
 *   - sits in status 'pending' for >24 hours
 *   - has no `tc_payment_transactions` row of type='charge' AND status='completed'
 *
 * These accumulate when a customer's card declines on checkout. The cart-preservation
 * pattern shipped in S113 (Bug 1) and extended in S114 (Stripe inline) intentionally
 * leaves the order in `pending` state so the retry path works without the customer
 * losing their cart. Each retry creates another pending order. Without cleanup,
 * tc_orders accumulates abandoned rows indefinitely.
 *
 * SETTINGS
 * --------
 *   orders.orphan_cleanup_enabled    — kill switch (default: true)
 *   orders.orphan_cleanup_age_hours  — minimum age before eligible (default: 24)
 *
 * EMAIL SUPPRESSION
 * -----------------
 * The 'cancelled' status has notify_customer=1 in the seed, which would normally
 * trigger a status-update email via OrdersModule::on_status_changed. We suppress
 * that email for auto-cancellations: customers may not remember their abandoned
 * order and a 24h-delayed "Order Cancelled" email would be confusing.
 *
 * @package TitanCart\Modules\Orders
 */

namespace TitanCart\Modules\Orders;

use TitanCart\Core\LogService;
use TitanCart\Services\SettingsService;

defined( 'ABSPATH' ) || exit;

class OrphanOrderCleanup {

    /** Cron hook name. Scheduled in Installer::schedule_cron(). */
    const CRON_HOOK = 'tc_orders_cleanup';

    /** Default age (hours) before a pending order is eligible for auto-cancel. */
    const DEFAULT_AGE_HOURS = 24;

    /** Cap on rows processed per run to keep cron responsive. */
    const BATCH_LIMIT = 500;

    /**
     * Register the WP-Cron action handler.
     *
     * Called from OrdersModule::__construct(). Idempotent — WP add_action
     * dedupes identical callbacks.
     */
    public static function init(): void {
        add_action( self::CRON_HOOK, [ self::class, 'run_cleanup' ] );
    }

    /**
     * Cron handler — runs daily via WP-Cron.
     *
     * Steps:
     *   1. Honor the kill-switch setting.
     *   2. Find orders in 'pending' status older than the configured age that have
     *      no completed charge transaction.
     *   3. Auto-cancel each via OrderManager::update_status() with email suppressed.
     *   4. Log every cancellation + a summary line.
     *
     * Safe to call manually for testing:
     *   wp cron event run tc_orders_cleanup
     */
    public static function run_cleanup(): void {
        if ( ! SettingsService::bool( 'orders', 'orphan_cleanup_enabled', true ) ) {
            return;
        }

        $age_hours = (int) SettingsService::get(
            'orders',
            'orphan_cleanup_age_hours',
            (string) self::DEFAULT_AGE_HOURS
        );
        if ( $age_hours < 1 ) {
            $age_hours = self::DEFAULT_AGE_HOURS;
        }

        $db             = titancart_db();
        $orders_table   = $db->table( 'orders' );
        $statuses_table = $db->table( 'order_statuses' );
        $txns_table     = $db->table( 'payment_transactions' );

        $cutoff = gmdate( 'Y-m-d H:i:s', time() - ( $age_hours * HOUR_IN_SECONDS ) );

        // Candidates: pending status + older than cutoff + no completed charge txn.
        // The NOT EXISTS subquery filters out orders that DID complete payment but
        // somehow got stuck in pending (rare edge case — those need manual review,
        // not auto-cancel).
        $candidates = $db->getArray(
            "SELECT o.id, o.order_number
             FROM {$orders_table} o
             INNER JOIN {$statuses_table} os ON os.id = o.status_id
             WHERE os.handle = 'pending'
               AND o.deleted_at IS NULL
               AND o.created_at < ?s
               AND NOT EXISTS (
                 SELECT 1 FROM {$txns_table} t
                 WHERE t.order_id = o.id
                   AND t.type = 'charge'
                   AND t.status = 'completed'
               )
             ORDER BY o.id ASC
             LIMIT ?i",
            $cutoff,
            self::BATCH_LIMIT
        );

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

        $om   = new OrderManager();
        $note = sprintf(
            /* translators: %d: hours */
            __( 'Auto-cancelled by cleanup cron — no completed payment after %d hours.', 'titancart-lite' ),
            $age_hours
        );

        // Suppress the customer-facing status-update email. Without this filter,
        // every auto-cancellation would email the customer 24h+ after their last
        // touchpoint (probably a card decline they may not remember).
        add_filter( 'titancart_send_status_update_email', '__return_false', 99 );

        $cancelled_count = 0;
        foreach ( $candidates as $row ) {
            $order_id = (int) $row['id'];
            $ok       = $om->update_status(
                $order_id,
                OrderManager::STATUS_CANCELLED,
                $note,
                false
            );

            if ( $ok ) {
                $cancelled_count++;
                LogService::info(
                    'orders',
                    'auto_cancelled',
                    sprintf(
                        'Auto-cancelled orphan pending order #%d (%s) — no completed payment after %d hours.',
                        $order_id,
                        $row['order_number'] ?? '',
                        $age_hours
                    ),
                    [
                        'object_type' => 'order',
                        'object_id'   => $order_id,
                        'cron'        => self::CRON_HOOK,
                    ]
                );
            }
        }

        remove_filter( 'titancart_send_status_update_email', '__return_false', 99 );

        if ( $cancelled_count > 0 ) {
            LogService::info(
                'orders',
                'cleanup_summary',
                sprintf( 'Orphan pending order cleanup ran — %d orders cancelled.', $cancelled_count ),
                [
                    'cron'  => self::CRON_HOOK,
                    'count' => $cancelled_count,
                ]
            );
        }
    }
}
