<?php

declare(strict_types=1);

namespace Duplicator\Package;

use Duplicator\Libs\Snap\SnapURL;
use Duplicator\Libs\Snap\SnapUtil;
use Duplicator\Models\ActivityLog\LogEventLoopbackFailure;
use Duplicator\Models\DynamicGlobalEntity;
use Duplicator\Models\GlobalEntity;
use Duplicator\Package\DupPackage;
use Duplicator\Utils\ExpireOptions;
use Duplicator\Utils\Logging\DupLog;
use RuntimeException;
use Throwable;

/**
 * Handles client-side kickoff detection and configuration
 */
class ClientSideKick
{
    /** @var string Parameter name for loopback connectivity test */
    public const LOOPBACK_TEST_PARAM = 'duplicator_loopback_test';

    /** @var string ExpireOptions key for caching loopback test result */
    private const LOOPBACK_CACHE_KEY = 'loopback_test';

    /** @var int Cache duration for loopback test result in seconds (1 hour) */
    private const LOOPBACK_CACHE_EXPIRATION = HOUR_IN_SECONDS;

    /** @var int Max length (bytes) of the response body excerpt captured for diagnostics */
    public const LOOPBACK_BODY_EXCERPT_MAX = 1000;

    /**
     * Respond 'ok' and terminate if the current request is a loopback test
     *
     * @return void
     */
    public static function handleLoopbackTest(): void
    {
        $action       = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'action');
        $hasTestParam = SnapUtil::sanitizeBoolInput(SnapUtil::INPUT_REQUEST, self::LOOPBACK_TEST_PARAM, false);

        if ($action !== 'duplicator_process_worker' || !$hasTestParam) {
            return;
        }

        SnapUtil::obCleanAll(false);
        header('HTTP/1.1 200 OK');
        echo 'ok';
        exit();
    }

    /**
     * Initialize hooks for client-side kickoff
     *
     * @return void
     */
    public static function init(): void
    {
        // Only register enqueue hooks if client-side kickoff is enabled for active package
        if (self::isClientSideKickoffEnabled()) {
            add_action('wp_enqueue_scripts', [self::class, 'enqueueKickoffScript']);
            add_action('admin_enqueue_scripts', [self::class, 'enqueueKickoffScript']);
        }
    }

    /**
     * Build the worker AJAX URL and request args with auth header
     *
     * @param array<string, scalar> $extraQuery Additional query parameters to append to the URL
     * @param array<string, mixed>  $extraArgs  Additional args to merge into the request
     *
     * @return array{url: string, args: array<string, mixed>}
     */
    public static function buildWorkerRequest(array $extraQuery = [], array $extraArgs = []): array
    {
        $global  = GlobalEntity::getInstance();
        $dGlobal = DynamicGlobalEntity::getInstance();

        $url = $global->getAjaxUrl();
        $url = SnapURL::appendQueryValue($url, 'action', 'duplicator_process_worker');

        foreach ($extraQuery as $key => $value) {
            $url = SnapURL::appendQueryValue($url, $key, $value);
        }

        $args = array_merge(
            ['sslverify' => false],
            $extraArgs
        );

        $authHeader = $dGlobal->getBasicAuthHeader();
        if ($authHeader !== null) {
            $args['headers'] = ['Authorization' => $authHeader];
        }

        return [
            'url'  => $url,
            'args' => $args,
        ];
    }

    /**
     * Tests whether the server can make HTTP requests to itself (loopback connectivity)
     *
     * @return bool True if server can reach itself, false otherwise
     */
    public static function canServerSelfRequest(): bool
    {
        /** @var array{result: bool}|null $cached */
        $cached = ExpireOptions::get(self::LOOPBACK_CACHE_KEY, null);
        if (is_array($cached)) {
            DupLog::trace('KICKOFF: Using cached loopback test result: ' . ($cached['result'] ? 'PASS' : 'FAIL'));
            return $cached['result'];
        }

        $result        = false;
        $failureReason = '';
        $context       = [];
        try {
            if (DUPLICATOR_FORCE_CLIENT_KICKOFF) { // @phpstan-ignore if.alwaysFalse
                throw new RuntimeException('Forced via DUPLICATOR_FORCE_CLIENT_KICKOFF constant');
            }

            $workerRequest = self::buildWorkerRequest(
                [self::LOOPBACK_TEST_PARAM => '1'],
                ['timeout' => 10]
            );
            $ajax_url      = $workerRequest['url'];
            $args          = $workerRequest['args'];

            $context['url'] = $ajax_url;
            DupLog::trace('KICKOFF: Testing server self-request to: ' . $ajax_url);

            $response = wp_remote_get($ajax_url, $args);

            if (is_wp_error($response)) {
                $context['wp_error_code'] = $response->get_error_code();
                throw new RuntimeException('WP_Error: ' . $response->get_error_message());
            }

            $responseCode            = wp_remote_retrieve_response_code($response);
            $responseBody            = trim(wp_remote_retrieve_body($response));
            $context['http_code']    = (int) $responseCode;
            $context['body_excerpt'] = substr($responseBody, 0, self::LOOPBACK_BODY_EXCERPT_MAX);

            if ($responseCode < 200 || $responseCode > 299) {
                throw new RuntimeException('HTTP ' . $responseCode . ' (expected 2xx)');
            }

            if ($responseBody !== 'ok') {
                throw new RuntimeException('Unexpected response body');
            }

            DupLog::trace('KICKOFF: Self-request PASSED - Server can reach itself');
            $result = true;
        } catch (Throwable $e) {
            $result        = false;
            $failureReason = $e->getMessage();
            DupLog::trace('KICKOFF: Self-request FAILED - ' . $failureReason);
        } finally {
            ExpireOptions::set(
                self::LOOPBACK_CACHE_KEY,
                ['result' => $result],
                self::LOOPBACK_CACHE_EXPIRATION
            );

            if (!$result) {
                LogEventLoopbackFailure::create($failureReason, $context);
            }
        }

        return $result;
    }

    /**
     * Check if client-side kickoff is enabled for the currently active package
     *
     * @return bool True if there's an active package that requires client-side kickoff
     */
    public static function isClientSideKickoffEnabled(): bool
    {
        $package = DupPackage::getNextActive();
        return $package !== null && $package->isClientSideKickoff();
    }

    /**
     * Enqueues the JavaScript for client-side kickoff
     *
     * @return void
     */
    public static function enqueueKickoffScript(): void
    {
        $global         = GlobalEntity::getInstance();
        $callPeriodInMs = 20000; // How often client calls into the service

        $ajax_url = $global->getAjaxUrl();

        $gateway = [
            'ajaxurl'               => $ajax_url,
            'client_call_frequency' => $callPeriodInMs,
        ];

        wp_register_script('dupli-kick', DUPLICATOR_PLUGIN_URL . 'assets/js/dupli-kick.js', ['jquery'], DUPLICATOR_VERSION);
        wp_localize_script('dupli-kick', 'dupli_gateway', $gateway);
        DupLog::trace('KICKOFF: Client-side kickoff script enqueued (dupli-kick.js)');
        wp_enqueue_script('dupli-kick');
    }
}
