<?php

namespace Bookaro\Service;

defined( 'ABSPATH' ) || exit;

// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- $hook is a bookaro_-prefixed cron event name resolved from a fixed internal event map, not arbitrary input.

use Bookaro\Repository\BookingRepository;
use Bookaro\Utils\Logger;
use Bookaro\Service\ScheduleService;
use Bookaro\Service\NullCalendarSync;
use Bookaro\Service\NullMessagingGateway;
use Bookaro\Service\NullPaymentsGateway;
use Bookaro\Service\ServiceLocator;

/**
 * Static callbacks used by WP-Cron events.
 */
class CronService
{
    public static function register_hooks(): void
    {
        add_action( 'bookaro_send_reminder', [ self::class, 'send_reminder' ], 10, 2 );
        add_action( 'bookaro_send_review_invite', [ self::class, 'send_review_invite' ], 10, 1 );
        add_action( 'bookaro_auto_complete', [ self::class, 'auto_complete_bookings' ] );
        add_action( 'bookaro_notification_log_cleanup', [ self::class, 'cleanup_notification_logs' ] );
        add_action( 'bookaro_logfile_cleanup', [ self::class, 'cleanup_log_file' ] );
    }

    public static function schedule_events(): void
    {
        if ( ! wp_next_scheduled( 'bookaro_auto_complete' ) ) {
            wp_schedule_event( time(), 'hourly', 'bookaro_auto_complete' );
        }
        if ( ! wp_next_scheduled( 'bookaro_notification_log_cleanup' ) ) {
            wp_schedule_event( time(), 'daily', 'bookaro_notification_log_cleanup' );
        }
        if ( ! wp_next_scheduled( 'bookaro_logfile_cleanup' ) ) {
            wp_schedule_event( time(), 'daily', 'bookaro_logfile_cleanup' );
        }
    }

    public static function clear_events(): void
    {
        wp_clear_scheduled_hook( 'bookaro_auto_complete' );
        wp_clear_scheduled_hook( 'bookaro_send_reminder' );
        wp_clear_scheduled_hook( 'bookaro_send_review_invite' );
        wp_clear_scheduled_hook( 'bookaro_notification_log_cleanup' );
        wp_clear_scheduled_hook( 'bookaro_logfile_cleanup' );
    }


    public static function cleanup_notification_logs(): void
    {
        try {
            $days = absint( bookaro_get_option( 'notification_log_retention_days', 180 ) );
            if ( $days < 1 ) {
                return;
            }
            $repo = new BookingRepository();
            $repo->purge_notification_logs_older_than( $days );
        } catch ( \Throwable $e ) {
            Logger::log( 'notification_log_cleanup_failed', [
                'exception' => $e->getMessage(),
                'code' => $e->getCode(),
            ] );
        }
    }


    public static function cleanup_log_file(): void
    {
        try {
            $days = absint( bookaro_get_option( 'log_file_retention_days', 0 ) );
            if ( $days < 1 ) {
                return;
            }
            Logger::purge_file_older_than( $days );
        } catch ( \Throwable $e ) {
            Logger::log( 'logfile_cleanup_failed', [
                'exception' => $e->getMessage(),
                'code' => $e->getCode(),
            ] );
        }
    }
    public static function send_reminder(int $booking_id, string $template_key): void
    {
        $email = new EmailService();
        $email->sendReminder($booking_id, $template_key);
    }

    public static function send_review_invite(int $booking_id): void
    {
        $email = new EmailService();
        $email->sendReviewInvite($booking_id);
    }

    public static function auto_complete_bookings(): void
    {
        try {
            $auto_complete_enabled = (int) bookaro_get_option( 'auto_complete_enabled', 1 );
            $auto_expire_enabled = (int) bookaro_get_option( 'auto_expire_enabled', 1 );
            if ( ! $auto_complete_enabled && ! $auto_expire_enabled ) {
                return;
            }

            $auto_complete_delay_hours = absint( bookaro_get_option( 'auto_complete_delay_hours', 0 ) );
            $auto_expire_delay_hours = absint( bookaro_get_option( 'auto_expire_delay_hours', 0 ) );
            // Bug 32: date_i18n() expects a true UTC timestamp and adds
            // gmt_offset itself to render WP-localtime. Feeding it
            // current_time('timestamp') (which is ALREADY UTC + gmt_offset)
            // double-applied the offset, producing cutoff strings gmt_offset
            // hours in the wrong direction. On +TZ sites that auto-completed
            // bookings that were still in the future; on -TZ sites it left
            // due bookings pending for gmt_offset hours. Use time() (true
            // UTC) for arithmetic and let date_i18n localize it.
            $now_ts = time();
            $complete_cutoff = date_i18n( 'Y-m-d H:i:s', $now_ts - ( $auto_complete_delay_hours * HOUR_IN_SECONDS ) );
            $expire_cutoff = date_i18n( 'Y-m-d H:i:s', $now_ts - ( $auto_expire_delay_hours * HOUR_IN_SECONDS ) );

            global $wpdb;
            $db    = new BookingRepository();
            $table = esc_sql( $db->get_table_name() );
            $now   = current_time('mysql');

            $approved_ids = [];
            if ( $auto_complete_enabled ) {
                $cutoff = $complete_cutoff ?: $now;
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $approved_ids = $wpdb->get_col($wpdb->prepare(
                    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                    "SELECT id FROM `{$table}` WHERE status = %s AND COALESCE(booking_date_to, booking_date) < %s",
                    'approved',
                    $cutoff
                ));
            }
            $pending_ids = [];
            if ( $auto_expire_enabled ) {
                $cutoff = $expire_cutoff ?: $now;
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $pending_ids = $wpdb->get_col($wpdb->prepare(
                    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                    "SELECT id FROM `{$table}` WHERE status = %s AND COALESCE(booking_date_to, booking_date) < %s",
                    'pending',
                    $cutoff
                ));
            }

            $services = function_exists( 'bookaro_services' ) ? bookaro_services() : null;
            if ( $services instanceof ServiceLocator ) {
                $services->set_repository( $db );
                $calendar = $services->calendar( $db );
                $messages = $services->messaging( $db );
                $payments = $services->payments( $db );
            } else {
                $calendar = new NullCalendarSync();
                $messages = new NullMessagingGateway();
                $payments = new NullPaymentsGateway();
            }

            $service = new BookingService(
                $db,
                new ScheduleService($db),
                new EmailService(),
                $calendar,
                $messages,
                $payments
            );

            foreach ($approved_ids as $id) {
                $service->updateBookingStatus((int)$id, 'completed');
            }
            if ( $auto_expire_enabled && ! empty( $pending_ids ) ) {
                $reason = __( 'Booking expired because it was not confirmed in time.', 'mybookaro' );
                foreach ( $pending_ids as $id ) {
                    $db->update_booking( (int) $id, [ 'cancel_reason' => $reason ] );
                    $service->updateBookingStatus( (int) $id, 'rejected' );
                }
            }
        } catch (\Throwable $e) {
            Logger::log('auto_complete_bookings failed', [
                'exception' => $e->getMessage(),
                'code' => $e->getCode(),
                'trace' => $e->getTraceAsString(),
            ]);
        }
    }

    /**
     * Bug 56: When DISABLE_WP_CRON=true is set in wp-config.php and no
     * system cron is configured to ping wp-cron.php on a schedule, none
     * of the bookaro_* cron events ever fire. That silently breaks:
     *  - bookaro_auto_complete (bookings stuck at "approved" forever)
     *  - bookaro_send_reminder (24h/1h pre-booking reminders never sent)
     *  - bookaro_send_review_invite (post-booking review prompts lost)
     *  - bookaro_notification_log_cleanup (logs grow unbounded)
     *  - bookaro_logfile_cleanup (file logs grow unbounded)
     * The system-status admin page shows a red pill for "WP-Cron",
     * but with no explanation of which plugin features fail and no
     * call-to-action. Give site owners a single command to manually
     * fire every overdue mybookaro event so they can run it from
     * server cron or a one-off WP-CLI invocation.
     *
     * Usage:
     *   wp bookaro cron-run                  # fire all 5 events synchronously
     *   wp bookaro cron-run --event=reminder # fire only one event (alias)
     *
     * Returns a JSON-ish summary line per event.
     *
     * @param array<int,string>    $args
     * @param array<string,string> $assoc_args
     */
    public static function cli_cron_run( array $args, array $assoc_args ): void
    {
        if ( ! class_exists( '\WP_CLI' ) ) {
            return;
        }

        $event_arg = isset( $assoc_args['event'] ) ? sanitize_key( (string) $assoc_args['event'] ) : '';
        $all_events = [
            'auto_complete'         => 'bookaro_auto_complete',
            'notification_cleanup'  => 'bookaro_notification_log_cleanup',
            'logfile_cleanup'       => 'bookaro_logfile_cleanup',
            'reminder'              => 'bookaro_send_reminder',
            'review_invite'         => 'bookaro_send_review_invite',
        ];

        $to_run = $all_events;
        if ( $event_arg !== '' ) {
            if ( ! array_key_exists( $event_arg, $all_events ) ) {
                \WP_CLI::error( sprintf(
                    'Unknown event "%s". Available: %s',
                    $event_arg,
                    implode( ', ', array_keys( $all_events ) )
                ) );
                return;
            }
            $to_run = [ $event_arg => $all_events[ $event_arg ] ];
        }

        foreach ( $to_run as $alias => $hook ) {
            // Reminder and review_invite events are single-event (per-booking)
            // hooks scheduled via wp_schedule_single_event. We can either fire
            // every queued instance now, or do nothing if the queue is empty.
            $is_recurring = in_array( $hook, [
                'bookaro_auto_complete',
                'bookaro_notification_log_cleanup',
                'bookaro_logfile_cleanup',
            ], true );

            if ( $is_recurring ) {
                try {
                    do_action( $hook );
                    \WP_CLI::log( sprintf( '[%s] fired recurring event', $alias ) );
                } catch ( \Throwable $e ) {
                    \WP_CLI::warning( sprintf( '[%s] failed: %s', $alias, $e->getMessage() ) );
                }
                continue;
            }

            // Single-event hook: pull anything currently due and fire it.
            // wp-cron stores due events in the `cron` option, keyed by ts.
            $cron = _get_cron_array();
            if ( ! is_array( $cron ) ) {
                \WP_CLI::log( sprintf( '[%s] no queue', $alias ) );
                continue;
            }
            $fired = 0;
            foreach ( $cron as $ts => $hooks ) {
                if ( ! isset( $hooks[ $hook ] ) ) {
                    continue;
                }
                foreach ( $hooks[ $hook ] as $sig => $event ) {
                    $args = is_array( $event['args'] ?? null ) ? $event['args'] : [];
                    try {
                        do_action_ref_array( $hook, $args );
                        wp_unschedule_event( $ts, $hook, $args );
                        $fired++;
                    } catch ( \Throwable $e ) {
                        \WP_CLI::warning( sprintf( '[%s] failed (sig=%s): %s', $alias, $sig, $e->getMessage() ) );
                    }
                }
            }
            \WP_CLI::log( sprintf( '[%s] fired %d single events', $alias, $fired ) );
        }

        \WP_CLI::success( 'bookaro cron-run complete' );
    }
}
