<?php

if (!defined('ABSPATH')) {
    exit;
}

final class BBC_Auth
{
    const MAX_BODY_BYTES = 131072;
    const MAX_CLOCK_SKEW = 300;
    const MAX_LEDGER_SIZE = 200;

    public static function base64url_decode($value)
    {
        if (!is_string($value) || !preg_match('/^[A-Za-z0-9_-]+$/', $value)) {
            return false;
        }
        $padding = strlen($value) % 4;
        if ($padding) {
            $value .= str_repeat('=', 4 - $padding);
        }
        return base64_decode(strtr($value, '-_', '+/'), true);
    }

    public static function normalized_origin($value)
    {
        $parts = wp_parse_url((string) $value);
        if (!is_array($parts) || empty($parts['scheme']) || empty($parts['host'])) {
            return '';
        }
        $scheme = strtolower($parts['scheme']);
        if ($scheme !== 'http' && $scheme !== 'https') {
            return '';
        }
        $origin = $scheme . '://' . strtolower($parts['host']);
        if (!empty($parts['port'])) {
            $origin .= ':' . intval($parts['port']);
        }
        return $origin;
    }

    public static function enroll(WP_REST_Request $request)
    {
        if (get_option(BBC_OPTION_IDENTITY, false)) {
            return new WP_Error('bbc_already_enrolled', 'BBC is already enrolled.', array('status' => 409));
        }
        if (!function_exists('sodium_crypto_sign_verify_detached')) {
            return new WP_Error('bbc_crypto_unavailable', 'Ed25519 verification is unavailable.', array('status' => 503));
        }
        $body = $request->get_json_params();
        $site_id = isset($body['site_id']) ? sanitize_text_field($body['site_id']) : '';
        $key_id = isset($body['key_id']) ? sanitize_text_field($body['key_id']) : '';
        $public_key = isset($body['public_key']) ? sanitize_text_field($body['public_key']) : '';
        $origin = isset($body['origin']) ? self::normalized_origin($body['origin']) : '';
        $raw_key = self::base64url_decode($public_key);
        if (!self::is_uuid($site_id) || !self::is_uuid($key_id) || $raw_key === false || strlen($raw_key) !== 32) {
            return new WP_Error('bbc_invalid_enrollment', 'BBC enrollment data is invalid.', array('status' => 400));
        }
        if ($origin !== self::normalized_origin(home_url('/'))) {
            return new WP_Error('bbc_origin_mismatch', 'BBC enrollment origin does not match this site.', array('status' => 400));
        }
        $identity = array(
            'site_id' => $site_id,
            'key_id' => $key_id,
            'public_key' => $public_key,
            'origin' => $origin,
            'enrolled_at' => time(),
            'update_base_url' => self::valid_update_url(isset($body['update_base_url']) ? $body['update_base_url'] : ''),
            'update_public_key' => self::valid_public_key(isset($body['update_public_key']) ? $body['update_public_key'] : ''),
        );
        if (!add_option(BBC_OPTION_IDENTITY, $identity, '', false)) {
            return new WP_Error('bbc_already_enrolled', 'BBC enrollment was locked by another request.', array('status' => 409));
        }
        return $identity;
    }

    public static function verify_command(WP_REST_Request $request)
    {
        $identity = get_option(BBC_OPTION_IDENTITY, false);
        if (!is_array($identity)) {
            return new WP_Error('bbc_not_enrolled', 'BBC is not enrolled.', array('status' => 403));
        }
        $raw_body = $request->get_body();
        if (!is_string($raw_body) || strlen($raw_body) > self::MAX_BODY_BYTES) {
            return new WP_Error('bbc_body_rejected', 'BBC command body is invalid.', array('status' => 413));
        }
        if (!self::consume_rate_limit()) {
            return new WP_Error('bbc_rate_limited', 'BBC command rate exceeded.', array('status' => 429));
        }
        $body = json_decode($raw_body, true);
        if (!is_array($body)) {
            return new WP_Error('bbc_invalid_json', 'BBC command body is invalid.', array('status' => 400));
        }
        $command_id = isset($body['command_id']) ? (string) $body['command_id'] : '';
        $site_id = isset($body['site_id']) ? (string) $body['site_id'] : '';
        $origin = isset($body['origin']) ? self::normalized_origin($body['origin']) : '';
        $expires_at = isset($body['expires_at']) ? intval($body['expires_at']) : 0;
        $key_id = (string) $request->get_header('x-bbc-key-id');
        $timestamp = intval($request->get_header('x-bbc-timestamp'));
        $nonce = (string) $request->get_header('x-bbc-nonce');
        $signature = self::base64url_decode($request->get_header('x-bbc-signature'));
        $body_hash = hash('sha256', $raw_body);
        $ledger = self::ledger();
        $now = time();
        if (!self::is_uuid($command_id) || !self::is_uuid($site_id) || !self::is_uuid($key_id) || !preg_match('/^[A-Za-z0-9_-]{22,}$/', $nonce)) {
            return new WP_Error('bbc_bad_identity', 'BBC command identity is invalid.', array('status' => 403));
        }
        $verification_key = '';
        if ($key_id === $identity['key_id']) {
            $verification_key = $identity['public_key'];
        } elseif (
            isset($identity['previous_key_id'], $identity['previous_public_key'], $identity['previous_valid_until']) &&
            $key_id === $identity['previous_key_id'] && intval($identity['previous_valid_until']) >= $now &&
            isset($ledger[$command_id]) && hash_equals((string) $ledger[$command_id]['body_hash'], $body_hash)
        ) {
            $verification_key = $identity['previous_public_key'];
        }
        if ($site_id !== $identity['site_id'] || $verification_key === '' || $origin !== $identity['origin']) {
            return new WP_Error('bbc_identity_mismatch', 'BBC command identity does not match.', array('status' => 403));
        }
        if (abs($now - $timestamp) > self::MAX_CLOCK_SKEW || $expires_at < $now || $expires_at > $timestamp + self::MAX_CLOCK_SKEW) {
            return new WP_Error('bbc_command_expired', 'BBC command timestamp is invalid.', array('status' => 403, 'server_time' => $now));
        }
        $canonical = "BBC1\nPOST\n/bbc/v1/command\n{$site_id}\n{$key_id}\n{$command_id}\n{$timestamp}\n{$expires_at}\n{$nonce}\n{$body_hash}";
        $public_key = self::base64url_decode($verification_key);
        if ($signature === false || $public_key === false || !sodium_crypto_sign_verify_detached($signature, $canonical, $public_key)) {
            return new WP_Error('bbc_bad_signature', 'BBC command signature is invalid.', array('status' => 403));
        }
        if (isset($ledger[$command_id]) && hash_equals((string) $ledger[$command_id]['body_hash'], $body_hash)) {
            $request->set_param('_bbc_cached_result', $ledger[$command_id]['result']);
            return true;
        }
        foreach ($ledger as $entry) {
            if (isset($entry['nonce']) && hash_equals((string) $entry['nonce'], $nonce)) {
                return new WP_Error('bbc_replay', 'BBC command was already used.', array('status' => 409));
            }
        }
        $request->set_param('_bbc_command_meta', array('body_hash' => $body_hash, 'nonce' => $nonce));
        return true;
    }

    public static function record_result(WP_REST_Request $request, $result)
    {
        $body = $request->get_json_params();
        $meta = $request->get_param('_bbc_command_meta');
        if (!is_array($body) || !is_array($meta) || !isset($body['command_id'])) {
            return;
        }
        $ledger = self::ledger();
        $ledger[(string) $body['command_id']] = array(
            'body_hash' => $meta['body_hash'],
            'nonce' => $meta['nonce'],
            'recorded_at' => time(),
            'result' => $result,
        );
        uasort($ledger, function ($left, $right) {
            return intval($right['recorded_at']) - intval($left['recorded_at']);
        });
        update_option(BBC_OPTION_COMMANDS, array_slice($ledger, 0, self::MAX_LEDGER_SIZE, true), false);
    }

    public static function rotate_key($key_id, $public_key)
    {
        $identity = get_option(BBC_OPTION_IDENTITY, false);
        $raw_key = self::base64url_decode($public_key);
        if (!is_array($identity) || !self::is_uuid($key_id) || $raw_key === false || strlen($raw_key) !== 32) {
            return new WP_Error('bbc_invalid_key', 'BBC replacement key is invalid.', array('status' => 400));
        }
        $identity['previous_key_id'] = isset($identity['key_id']) ? $identity['key_id'] : '';
        $identity['previous_public_key'] = isset($identity['public_key']) ? $identity['public_key'] : '';
        $identity['previous_valid_until'] = time() + self::MAX_CLOCK_SKEW;
        $identity['key_id'] = $key_id;
        $identity['public_key'] = $public_key;
        $identity['rotated_at'] = time();
        update_option(BBC_OPTION_IDENTITY, $identity, false);
        return true;
    }

    private static function ledger()
    {
        $ledger = get_option(BBC_OPTION_COMMANDS, array());
        return is_array($ledger) ? $ledger : array();
    }

    private static function consume_rate_limit()
    {
        $minute = gmdate('YmdHi');
        $ip = isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : 'unknown';
        $keys = array('bbc_rate_global_' . $minute => 60, 'bbc_rate_ip_' . hash('sha256', $ip) . '_' . $minute => 20);
        foreach ($keys as $key => $limit) {
            $count = intval(get_transient($key));
            if ($count >= $limit) {
                return false;
            }
            set_transient($key, $count + 1, 120);
        }
        return true;
    }

    private static function valid_update_url($value)
    {
        $value = esc_url_raw((string) $value, array('https'));
        return strpos($value, 'https://') === 0 ? untrailingslashit($value) : '';
    }

    private static function valid_public_key($value)
    {
        $value = sanitize_text_field((string) $value);
        $raw = self::base64url_decode($value);
        return $raw !== false && strlen($raw) === 32 ? $value : '';
    }

    public static function is_uuid($value)
    {
        return is_string($value) && preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $value);
    }
}
