<?php

declare(strict_types=1);

namespace FormConnector\Features\Retry;

use FormConnector\Integration\CareHQ\APIClient;
use FormConnector\Features\Logs\Logs;

defined('ABSPATH') || exit;

/**
 * Retry Queue for failed CareHQ submissions.
 *
 * Stores failed submissions in a custom table and retries them
 * via WP-Cron with exponential backoff.
 */
class RetryQueue {

    private static ?RetryQueue $instance = null;

    /** Maximum retry attempts before marking as permanently failed */
    private const MAX_RETRIES = 5;

    /** Base delay in seconds (doubles each attempt) */
    private const BASE_DELAY = 300; // 5 minutes

    /** Maximum number of queue items to process in a single cron run. */
    private const BATCH_SIZE = 25;

    /** Wall-clock budget (seconds) for a single cron run. */
    private const TIME_BUDGET_SECONDS = 20.0;

    /** WP-Cron hook name */
    public const CRON_HOOK = 'fc_process_retry_queue';

    public static function get_instance(): self {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Get the retry queue table name.
     */
    public function get_table_name(): string {
        global $wpdb;
        return $wpdb->prefix . 'fc_retry_queue';
    }

    /**
     * Create the retry queue table.
     */
    public function create_table(): void {
        global $wpdb;

        $table_name = $this->get_table_name();
        $charset_collate = $wpdb->get_charset_collate();

        $sql = "CREATE TABLE $table_name (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
            next_retry_at datetime NOT NULL,
            retry_count int(11) DEFAULT 0 NOT NULL,
            max_retries int(11) DEFAULT 5 NOT NULL,
            status varchar(20) DEFAULT 'pending' NOT NULL,
            form_plugin varchar(50) DEFAULT '' NOT NULL,
            form_id varchar(100) DEFAULT '' NOT NULL,
            form_name varchar(255) DEFAULT '' NOT NULL,
            location_id varchar(100) DEFAULT '' NOT NULL,
            location_name varchar(255) DEFAULT '' NOT NULL,
            payload longtext NOT NULL,
            last_error longtext DEFAULT '' NOT NULL,
            log_id bigint(20) DEFAULT 0 NOT NULL,
            PRIMARY KEY (id),
            KEY status_next_retry (status, next_retry_at),
            KEY created_at (created_at)
        ) $charset_collate;";

        require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
        dbDelta($sql);
    }

    /**
     * Register WP-Cron hooks.
     */
    public function register_cron(): void {
        add_action(self::CRON_HOOK, [$this, 'process_queue']);
    }

    /**
     * Schedule the cron event if not already scheduled.
     */
    public function schedule_cron(): void {
        if (!wp_next_scheduled(self::CRON_HOOK)) {
            wp_schedule_event(time(), 'fc_five_minutes', self::CRON_HOOK);
        }
    }

    /**
     * Unschedule the cron event.
     */
    public function unschedule_cron(): void {
        $timestamp = wp_next_scheduled(self::CRON_HOOK);
        if ($timestamp) {
            wp_unschedule_event($timestamp, self::CRON_HOOK);
        }
    }

    /**
     * Register custom cron interval (5 minutes).
     */
    public static function add_cron_interval(array $schedules): array {
        $schedules['fc_five_minutes'] = [
            'interval' => 300,
            'display'  => __('Every 5 Minutes', 'form-connector'),
        ];
        return $schedules;
    }

    /**
     * Enqueue a failed submission for retry.
     *
     * @param array $payload The CareHQ enquiry payload
     * @param string $form_plugin Form plugin key
     * @param string $form_id Form ID
     * @param string $form_name Form name
     * @param string $location_id Location ID
     * @param string $location_name Location name
     * @param string $error_message The error from the first attempt
     * @param int $log_id The associated log entry ID
     * @return int|false The retry queue entry ID or false on failure
     */
    public function enqueue(
        array $payload,
        string $form_plugin,
        string $form_id,
        string $form_name,
        string $location_id,
        string $location_name,
        string $error_message,
        int $log_id = 0
    ): int|false {
        global $wpdb;

        $table_name = $this->get_table_name();
        $now = current_time('mysql', true);
        $next_retry = gmdate('Y-m-d H:i:s', time() + self::BASE_DELAY);

        $result = $wpdb->insert(
            $table_name,
            [
                'created_at'    => $now,
                'next_retry_at' => $next_retry,
                'retry_count'   => 0,
                'max_retries'   => self::MAX_RETRIES,
                'status'        => 'pending',
                'form_plugin'   => sanitize_text_field($form_plugin),
                'form_id'       => sanitize_text_field($form_id),
                'form_name'     => sanitize_text_field($form_name),
                'location_id'   => sanitize_text_field($location_id),
                'location_name' => sanitize_text_field($location_name),
                'payload'       => wp_json_encode($payload, JSON_UNESCAPED_SLASHES),
                'last_error'    => sanitize_text_field($error_message),
                'log_id'        => $log_id,
            ],
            ['%s', '%s', '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d']
        );

        if ($result === false) {
            return false;
        }

        // Ensure cron is scheduled
        $this->schedule_cron();

        return $wpdb->insert_id;
    }

    /**
     * Process pending retries that are due.
     *
     * Rate-limited to {@see RetryQueue::BATCH_SIZE} items per run
     * and a wall-clock budget of {@see RetryQueue::TIME_BUDGET_SECONDS}
     * seconds, so cron events never block other scheduled tasks.
     */
    public function process_queue(): void {
        global $wpdb;

        $table_name = $this->get_table_name();

        // Check table exists
        if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) !== $table_name) {
            return;
        }

        $now = current_time('mysql', true);

        // Allow filtering the per-run batch size (for hosts with strict resource limits).
        $batch_size = (int) apply_filters('fc_retry_batch_size', self::BATCH_SIZE);
        $batch_size = max(1, min(100, $batch_size));
        $time_budget = (float) apply_filters('fc_retry_time_budget', self::TIME_BUDGET_SECONDS);
        $started_at = microtime(true);

        // Get items ready for retry
        $items = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM {$table_name} WHERE status = 'pending' AND next_retry_at <= %s ORDER BY next_retry_at ASC LIMIT %d",
                $now,
                $batch_size
            ),
            ARRAY_A
        );

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

        $api = APIClient::get_instance();
        $logs = Logs::get_instance();

        foreach ($items as $item) {
            // Stop early if we've burned through the time budget.
            if ((microtime(true) - $started_at) > $time_budget) {
                break;
            }

            $payload = json_decode($item['payload'], true);
            if (!is_array($payload)) {
                $this->mark_failed((int) $item['id'], 'Invalid payload data');
                continue;
            }

            $result = $api->send_care_enquiry($payload);
            $new_retry_count = (int) $item['retry_count'] + 1;

            if (is_wp_error($result)) {
                $error_msg = $result->get_error_message();

                if ($new_retry_count >= (int) $item['max_retries']) {
                    // Max retries reached — mark as permanently failed
                    $this->mark_failed((int) $item['id'], $error_msg);

                    // Update associated log entry
                    if ((int) $item['log_id'] > 0) {
                        $this->update_log_retry_status(
                            (int) $item['log_id'],
                            'retry_failed',
                            $error_msg,
                            $new_retry_count
                        );
                    }

                    // Trigger notification hook
                    do_action('fc_retry_permanently_failed', $item, $error_msg);
                } else {
                    // Schedule next retry with exponential backoff
                    $delay = self::BASE_DELAY * pow(2, $new_retry_count);
                    $next_retry = gmdate('Y-m-d H:i:s', time() + $delay);

                    $wpdb->update(
                        $table_name,
                        [
                            'retry_count'   => $new_retry_count,
                            'next_retry_at' => $next_retry,
                            'last_error'    => sanitize_text_field($error_msg),
                        ],
                        ['id' => (int) $item['id']],
                        ['%d', '%s', '%s'],
                        ['%d']
                    );
                }
            } else {
                // Success — remove from queue
                $this->mark_success((int) $item['id']);

                // Update associated log entry to success
                if ((int) $item['log_id'] > 0) {
                    $this->update_log_retry_status(
                        (int) $item['log_id'],
                        'success',
                        'Retry succeeded',
                        $new_retry_count
                    );
                }

                // Log in submissions log as retry success
                $logs->log_submission([
                    'form_id'      => $item['form_id'],
                    'form_name'    => $item['form_name'],
                    'form_plugin'  => $item['form_plugin'],
                    'submitted_data' => ['retry' => true, 'original_queue_id' => $item['id'], 'mapped_to_carehq' => $payload],
                    'status'       => 'success',
                    'response'     => $result,
                    'location_id'  => $item['location_id'],
                    'location_name'=> $item['location_name'],
                ]);

                do_action('fc_retry_succeeded', $item, $result);
            }
        }
    }

    /**
     * Mark a queue item as permanently failed.
     */
    private function mark_failed(int $id, string $error): void {
        global $wpdb;
        $wpdb->update(
            $this->get_table_name(),
            ['status' => 'failed', 'last_error' => sanitize_text_field($error)],
            ['id' => $id],
            ['%s', '%s'],
            ['%d']
        );
    }

    /**
     * Mark a queue item as successfully retried.
     */
    private function mark_success(int $id): void {
        global $wpdb;
        $wpdb->update(
            $this->get_table_name(),
            ['status' => 'success'],
            ['id' => $id],
            ['%s'],
            ['%d']
        );
    }

    /**
     * Update the associated log entry with retry status.
     */
    private function update_log_retry_status(int $log_id, string $status, string $message, int $retry_count): void {
        global $wpdb;
        $logs_table = Logs::get_instance()->get_table_name();

        $log = Logs::get_instance()->get_log($log_id);
        if (!$log) {
            return;
        }

        $response = $log['response_message'];
        $response['retry_status'] = $status;
        $response['retry_message'] = $message;
        $response['retry_count'] = $retry_count;

        $wpdb->update(
            $logs_table,
            [
                'response_status'  => $status === 'success' ? 'success' : 'retry_failed',
                'response_message' => wp_json_encode($response, JSON_UNESCAPED_SLASHES),
            ],
            ['id' => $log_id],
            ['%s', '%s'],
            ['%d']
        );
    }

    /**
     * Get queue statistics.
     */
    public function get_stats(): array {
        global $wpdb;
        $table_name = $this->get_table_name();

        if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) !== $table_name) {
            return ['pending' => 0, 'failed' => 0, 'success' => 0, 'total' => 0];
        }

        $stats = $wpdb->get_row(
            "SELECT
                COUNT(*) as total,
                SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
                SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed,
                SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success
            FROM {$table_name}",
            ARRAY_A
        );

        return [
            'pending' => (int) ($stats['pending'] ?? 0),
            'failed'  => (int) ($stats['failed'] ?? 0),
            'success' => (int) ($stats['success'] ?? 0),
            'total'   => (int) ($stats['total'] ?? 0),
        ];
    }

    /**
     * Get queue items with optional filtering.
     */
    public function get_items(array $args = []): array {
        global $wpdb;
        $table_name = $this->get_table_name();

        if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) !== $table_name) {
            return ['items' => [], 'total' => 0, 'pages' => 0];
        }

        $defaults = [
            'page'     => 1,
            'per_page' => 20,
            'status'   => '',
        ];
        $args = wp_parse_args($args, $defaults);

        $where = ['1=1'];
        $values = [];

        if (!empty($args['status'])) {
            $where[] = 'status = %s';
            $values[] = $args['status'];
        }

        $where_sql = implode(' AND ', $where);

        // Total count
        $count_sql = "SELECT COUNT(*) FROM {$table_name} WHERE {$where_sql}";
        if (!empty($values)) {
            $count_sql = $wpdb->prepare($count_sql, $values);
        }
        $total = (int) $wpdb->get_var($count_sql);

        $per_page = max(1, min(100, (int) $args['per_page']));
        $page = max(1, (int) $args['page']);
        $pages = (int) ceil($total / $per_page);
        $offset = ($page - 1) * $per_page;

        $sql = "SELECT * FROM {$table_name} WHERE {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
        $values[] = $per_page;
        $values[] = $offset;

        $results = $wpdb->get_results($wpdb->prepare($sql, $values), ARRAY_A);

        foreach ($results as &$row) {
            $row['payload'] = json_decode($row['payload'], true) ?? [];
        }

        return ['items' => $results, 'total' => $total, 'pages' => $pages];
    }

    /**
     * Cancel a pending retry.
     */
    public function cancel(int $id): bool {
        global $wpdb;
        return $wpdb->update(
            $this->get_table_name(),
            ['status' => 'cancelled'],
            ['id' => $id, 'status' => 'pending'],
            ['%s'],
            ['%d', '%s']
        ) !== false;
    }

    /**
     * Retry a failed item immediately.
     */
    public function retry_now(int $id): bool {
        global $wpdb;
        return $wpdb->update(
            $this->get_table_name(),
            [
                'status'        => 'pending',
                'next_retry_at' => current_time('mysql', true),
                'retry_count'   => 0,
            ],
            ['id' => $id],
            ['%s', '%s', '%d'],
            ['%d']
        ) !== false;
    }

    /**
     * Clear completed/failed items older than given days.
     */
    public function cleanup(int $days = 30): int {
        global $wpdb;
        $table_name = $this->get_table_name();
        $date = gmdate('Y-m-d H:i:s', strtotime("-{$days} days"));

        return (int) $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM {$table_name} WHERE status IN ('success', 'failed', 'cancelled') AND created_at < %s",
                $date
            )
        );
    }
}
