<?php

declare(strict_types=1);

namespace ShieldX\Support;

defined('ABSPATH') || exit;

final class ExternalRunnerToken
{
    public static function generate(): string
    {
        try {
            return bin2hex(random_bytes(24));
        } catch (\Throwable $error) {
            return '';
        }
    }

    public static function sanitizeToken(string $token): string
    {
        $token = trim($token);

        return preg_match('/^[A-Za-z0-9]{32,96}$/', $token) === 1 ? $token : '';
    }

    public static function hash(string $token): string
    {
        $token = self::sanitizeToken($token);

        return $token !== '' ? hash_hmac('sha256', $token, self::salt()) : '';
    }

    public static function sanitizeHash(string $hash): string
    {
        $hash = trim(strtolower($hash));

        return preg_match('/^[a-f0-9]{64}$/', $hash) === 1 ? $hash : '';
    }

    public static function protect(string $token): string
    {
        $token = self::sanitizeToken($token);
        if ($token === '') {
            return '';
        }

        try {
            return CryptoBox::protect($token, self::encryptionKey());
        } catch (\Throwable $error) {
            return '';
        }
    }

    public static function reveal(string $stored): string
    {
        $stored = trim($stored);
        if (!CryptoBox::isV2($stored)) {
            return '';
        }

        return self::sanitizeToken(CryptoBox::reveal($stored, self::encryptionKey()));
    }

    /**
     * @return array{external_token:string,external_token_hash:string}
     */
    public static function storageValues(string $storedToken = '', string $storedHash = ''): array
    {
        $hash = self::sanitizeHash($storedHash);
        $storedToken = trim($storedToken);
        $token = self::reveal($storedToken);
        $tokenHash = $token !== '' ? self::hash($token) : '';

        if (self::isProtectedToken($storedToken) && $token !== '' && ($hash === '' || hash_equals($hash, $tokenHash))) {
            return [
                'external_token' => $storedToken,
                'external_token_hash' => $tokenHash,
            ];
        }

        if ($token === '') {
            $token = self::generate();
        }

        $protected = self::protect($token);

        return [
            'external_token' => $protected,
            'external_token_hash' => self::hash($token),
        ];
    }

    public static function matches(string $providedToken, string $storedHash): bool
    {
        $providedToken = self::sanitizeToken($providedToken);
        $storedHash = self::sanitizeHash($storedHash);

        return $providedToken !== '' && $storedHash !== '' && hash_equals($storedHash, self::hash($providedToken));
    }

    public static function endpoint(string $path): string
    {
        $path = '/' . trim($path, '/');

        return function_exists('home_url') ? (string) home_url($path) : $path;
    }

    /**
     * Match an incoming request path against an external runner endpoint on
     * this WordPress installation. Comparing the generated endpoint path
     * keeps subdirectory installs (for example, /security/autoscan) working
     * without accepting that endpoint outside the configured home URL path.
     */
    public static function matchesEndpointPath(string $requestPath, string $endpoint, string $homeUrl = ''): bool
    {
        return self::normalizePath($requestPath) === self::endpointPath($endpoint, $homeUrl);
    }

    private static function endpointPath(string $endpoint, string $homeUrl): string
    {
        $endpoint = self::normalizePath($endpoint);
        if ($homeUrl === '') {
            return self::normalizePath(self::endpoint($endpoint));
        }

        return self::normalizePath(rtrim(self::normalizePath($homeUrl), '/') . $endpoint);
    }

    private static function normalizePath(string $value): string
    {
        $path = wp_parse_url($value, PHP_URL_PATH);
        if (!is_string($path) || $path === '') {
            $path = explode('?', explode('#', $value, 2)[0], 2)[0];
        }

        $path = '/' . ltrim($path, '/');
        $path = rtrim($path, '/');

        return $path !== '' ? $path : '/';
    }

    private static function encryptionKey(): string
    {
        return hash('sha256', 'shieldx-external-runner-token|' . self::salt(), true);
    }

    private static function isProtectedToken(string $stored): bool
    {
        return CryptoBox::isV2($stored);
    }

    private static function salt(): string
    {
        $salt = function_exists('wp_salt') ? (string) wp_salt('auth') : '';

        return $salt !== '' ? $salt : 'shieldx-external-runner-token';
    }
}
