<?php
/**
 * DownloadableModule — bootstrap for the Downloadable Products feature.
 *
 * Registers:
 *   - Hook listener on `titancart_order_status_changed` to issue/revoke keys
 *   - WP-Cron daily job to expire stale keys + prune log rows
 *   - REST routes (Phase 1: stubs returning 501; Phase 2-5 fill them in)
 *
 * Phase 1 scope:
 *   This module is wired into Plugin::boot() but the customer-facing UI is
 *   not built yet. Keys will be ISSUED correctly when a paid order arrives,
 *   and REVOKED on refund (per the catalog_revoke_on_refund setting), but
 *   no email contains them and no UI shows them. This is intentional —
 *   Phase 4 wires emails, Phase 3 wires the customer Downloads page.
 *
 *   The key issuance happens regardless of `catalog_enable_downloads` —
 *   if a product has `is_downloadable=1` and files attached, keys issue.
 *   The master setting becomes a UI/storefront gate in Phase 5.
 *
 * @package TitanCart\Modules\Downloadables
 */

namespace TitanCart\Modules\Downloadables;

use TitanCart\Modules\Orders\OrderManager;
use TitanCart\REST\Downloadables\DownloadableController;

defined( 'ABSPATH' ) || exit;

class DownloadableModule {

    /** Cron hook handle for the daily expirer. */
    public const CRON_HOOK = 'titancart_downloadable_daily';

    /** Status handles that mark an order as "paid" — triggers key issuance. */
    public const PAID_STATUSES = [
        OrderManager::STATUS_PROCESSING,
        OrderManager::STATUS_COMPLETED,
    ];

    /** Status handles that mark an order as "no longer eligible" — triggers revocation. */
    public const REVOKE_STATUSES = [
        OrderManager::STATUS_CANCELLED,
        OrderManager::STATUS_REFUNDED,
    ];

    public static function init(): void {
        $instance = new self();

        // ── Order lifecycle integration ────────────────────────────────────────
        add_action( 'titancart_order_status_changed', [ $instance, 'on_order_status_changed' ], 10, 4 );

        // ── Cron: daily expirer + log pruner ───────────────────────────────────
        add_action( self::CRON_HOOK, [ $instance, 'run_daily_cron' ] );

        // Ensure the schedule is in place. Plugin activation ALSO schedules this
        // (see schedule_cron() called from elsewhere if needed); this guard
        // re-arms it if WP-Cron forgot. Idempotent — wp_schedule_event no-ops
        // when the same hook + recurrence is already scheduled.
        if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
            wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', self::CRON_HOOK );
        }

        // ── REST routes (Phase 1 stubs) ─────────────────────────────────────────
        add_action( 'rest_api_init', [ $instance, 'register_rest_routes' ] );
    }

    // ══════════════════════════════════════════════════════════════════════════
    // ORDER STATUS LISTENER
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Issue or revoke download keys when an order's status changes.
     *
     * Fires on the `titancart_order_status_changed` action — see
     * OrderManager::update_status() (line ~370) for the dispatch site.
     * Signature is fixed by the action: ($order_id, $new_handle, $old_handle, $notify).
     *
     * Issue path:
     *   New status is one of PAID_STATUSES → call issue_keys_for_order().
     *   Idempotent — DownloadableManager skips files that already have keys.
     *   This means pending → processing → completed issues exactly once.
     *
     * Revoke path:
     *   New status is one of REVOKE_STATUSES → call revoke_keys_for_order().
     *   Gated internally by the catalog_revoke_on_refund setting (default ON).
     *
     * @param  int    $order_id
     * @param  string $new_handle
     * @param  string $old_handle
     * @param  bool   $notify    (unused here — emails are Phase 4)
     * @return void
     */
    public function on_order_status_changed( int $order_id, string $new_handle, string $old_handle, bool $notify ): void {
        if ( in_array( $new_handle, self::PAID_STATUSES, true ) ) {
            ( new DownloadableManager() )->issue_keys_for_order( $order_id );
            return;
        }

        if ( in_array( $new_handle, self::REVOKE_STATUSES, true ) ) {
            ( new DownloadableManager() )->revoke_keys_for_order( $order_id, 'order_status_' . $new_handle );
            return;
        }
    }

    // ══════════════════════════════════════════════════════════════════════════
    // CRON
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Daily cron handler — expire stale keys, prune old log rows.
     *
     * @return void
     */
    public function run_daily_cron(): void {
        ( new DownloadableManager() )->expire_keys();
    }

    // ══════════════════════════════════════════════════════════════════════════
    // REST ROUTES
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Register the Downloadables REST controller. Phase 1 routes are stubs
     * returning HTTP 501 Not Implemented — they exist so frontend code can
     * detect the endpoints' presence and so authentication infrastructure
     * is in place from day one.
     *
     * @return void
     */
    public function register_rest_routes(): void {
        ( new DownloadableController() )->register_routes();
    }

    // ══════════════════════════════════════════════════════════════════════════
    // ACTIVATION / DEACTIVATION HOOKS
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Schedule the daily cron. Called from plugin activation if the
     * activation flow includes per-module hooks. Safe to call multiple times.
     *
     * @return void
     */
    public static function schedule_cron(): void {
        if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
            wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', self::CRON_HOOK );
        }
    }

    /**
     * Unschedule the daily cron. Called from plugin deactivation to keep
     * WP_Cron clean when the plugin is turned off.
     *
     * @return void
     */
    public static function unschedule_cron(): void {
        $timestamp = wp_next_scheduled( self::CRON_HOOK );
        if ( $timestamp ) {
            wp_unschedule_event( $timestamp, self::CRON_HOOK );
        }
    }
}
