<?php

namespace WPSN\Modules\Licensing;

use WPSN\Core\Settings;

defined('ABSPATH') || exit;

/**
 * Shared request contract for licensing / billing / updates.
 *
 * Goals:
 * - stable install identity for server-side site binding,
 * - consistent request envelope across verify/update/billing/self-serve,
 * - optional request signing when the server issued a per-site request secret.
 */
final class ServerContract
{
    public const VERSION = '1';

    private Settings $settings;
    private LicenseSecretsRepository $secrets;

    public function __construct(Settings $settings, ?LicenseSecretsRepository $secrets = null)
    {
        $this->settings = $settings;
        $this->secrets = $secrets ?: new LicenseSecretsRepository();
    }

    /**
     * @param array<string,mixed> $payload
     * @return array<string,mixed>
     */
    public function buildPayload(string $context, array $payload = [], bool $includeModules = true): array
    {
        $binding = $this->secrets->getServerBinding();
        $requestId = isset($payload['request_id']) && is_string($payload['request_id']) && trim($payload['request_id']) !== ''
            ? trim((string) $payload['request_id'])
            : (function_exists('wp_generate_uuid4') ? (string) wp_generate_uuid4() : md5((string) microtime(true)));

        $base = [
            'contract_version' => self::VERSION,
            'request_context' => sanitize_key($context),
            'request_id' => $requestId,
            'install_id' => $this->installId(),
            'site_fingerprint' => $this->siteFingerprint(),
            'site_url' => home_url('/'),
            'home_url' => home_url('/'),
            'admin_url' => admin_url('/'),
            'site_name' => (string) get_bloginfo('name'),
            'admin_email' => (string) get_bloginfo('admin_email'),
            'locale' => function_exists('get_locale') ? (string) get_locale() : '',
            'wp_version' => (string) get_bloginfo('version'),
            'php_version' => PHP_VERSION,
            'plugin_version' => defined('WPSN_VERSION') ? (string) WPSN_VERSION : '',
            'build' => defined('WPSN_BUILD') ? (string) WPSN_BUILD : 'free',
            'multisite' => (function_exists('is_multisite') && is_multisite()) ? 1 : 0,
            'ts' => time(),
        ];

        foreach (['site_id', 'account_id', 'client_id'] as $key) {
            if (!empty($binding[$key]) && is_string($binding[$key])) {
                $base[$key] = (string) $binding[$key];
            }
        }

        if ($includeModules) {
            $modules = $this->enabledModules();
            if (!empty($modules)) {
                $base['modules_enabled'] = $modules;
            }
        }

        $payload = array_replace($base, $payload);
        $payload = (array) apply_filters('wpsn/licensing/server_contract/payload', $payload, $context, $binding);

        return $this->canonicalize($payload);
    }

    /**
     * @param array<string,mixed> $payload
     * @return array<string,string>
     */
    public function buildHeaders(string $service, string $context, array $payload = []): array
    {
        $headers = [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'User-Agent' => 'WP SEO Next/' . (defined('WPSN_VERSION') ? (string) WPSN_VERSION : 'dev') . '; ' . home_url('/'),
            'X-WPSN-Service' => sanitize_key($service),
            'X-WPSN-Context' => sanitize_key($context),
            'X-WPSN-Contract-Version' => self::VERSION,
            'X-WPSN-Install-ID' => $this->installId(),
            'X-WPSN-Site-Fingerprint' => $this->siteFingerprint(),
            'X-WPSN-Plugin-Version' => defined('WPSN_VERSION') ? (string) WPSN_VERSION : '',
            'X-WPSN-Build' => defined('WPSN_BUILD') ? (string) WPSN_BUILD : 'free',
        ];

        if (!empty($payload['request_id']) && is_string($payload['request_id'])) {
            $headers['X-WPSN-Request-ID'] = (string) $payload['request_id'];
        }
        if (!empty($payload['site_id']) && is_string($payload['site_id'])) {
            $headers['X-WPSN-Site-ID'] = (string) $payload['site_id'];
        }
        if (!empty($payload['client_id']) && is_string($payload['client_id'])) {
            $headers['X-WPSN-Client-ID'] = (string) $payload['client_id'];
        }

        $signature = $this->requestSignature($payload);
        if ($signature !== '') {
            $headers['X-WPSN-Signature'] = 'sha256=' . $signature;
            $headers['X-WPSN-Signature-Input'] = 'canonical-json';
            $kid = $this->signingKeyId();
            if ($kid !== '') {
                $headers['X-WPSN-Key-ID'] = $kid;
            }
        }

        $headers = (array) apply_filters('wpsn/licensing/server_contract/headers', $headers, $service, $context, $payload);

        $normalized = [];
        foreach ($headers as $key => $value) {
            if (!is_string($key)) {
                continue;
            }
            if (!is_scalar($value)) {
                continue;
            }
            $normalized[$key] = (string) $value;
        }

        return $normalized;
    }

    /**
     * @param array<string,mixed> $payload
     */
    public function jsonEncode(array $payload): string
    {
        $json = wp_json_encode($this->canonicalize($payload));
        return is_string($json) ? $json : '{}';
    }

    public function installId(): string
    {
        return $this->secrets->getInstallId();
    }

    public function siteFingerprint(): string
    {
        $home = strtolower(trim((string) home_url('/')));
        $home = preg_replace('#/$#', '', $home);
        $fingerprint = substr(hash('sha256', (string) $home), 0, 24);
        $fingerprint = (string) apply_filters('wpsn/licensing/server_contract/site_fingerprint', $fingerprint, $home);
        return trim($fingerprint);
    }

    public function hasRequestSecret(): bool
    {
        return trim($this->secrets->getRequestSecret()) !== '';
    }

    public function signingKeyId(): string
    {
        return trim($this->secrets->getRequestKeyId());
    }

    /**
     * @param array<string,mixed> $decoded
     * @return array<string,string>
     */
    public static function extractResponseMeta(array $decoded): array
    {
        $meta = isset($decoded['meta']) && is_array($decoded['meta']) ? $decoded['meta'] : [];

        $out = [];
        foreach (['request_id', 'trace_id', 'kid'] as $key) {
            $value = '';
            if (isset($meta[$key]) && is_scalar($meta[$key])) {
                $value = trim((string) $meta[$key]);
            } elseif (isset($decoded[$key]) && is_scalar($decoded[$key])) {
                $value = trim((string) $decoded[$key]);
            }
            if ($value !== '') {
                $out[$key] = substr($value, 0, 190);
            }
        }

        $contract = '';
        foreach (['contract_version', 'contract'] as $key) {
            if (isset($meta[$key]) && is_scalar($meta[$key])) {
                $contract = trim((string) $meta[$key]);
                break;
            }
            if (isset($decoded[$key]) && is_scalar($decoded[$key])) {
                $contract = trim((string) $decoded[$key]);
                break;
            }
        }
        if ($contract !== '') {
            $out['contract_version'] = substr($contract, 0, 40);
        }

        $serverTime = '';
        foreach (['server_time', 'generated_at', 'issued_at'] as $key) {
            if (isset($meta[$key]) && is_scalar($meta[$key])) {
                $serverTime = trim((string) $meta[$key]);
                break;
            }
            if (isset($decoded[$key]) && is_scalar($decoded[$key])) {
                $serverTime = trim((string) $decoded[$key]);
                break;
            }
        }
        if ($serverTime !== '') {
            $out['server_time'] = substr($serverTime, 0, 64);
        }

        return $out;
    }

    /**
     * @param array<string,mixed> $decoded
     * @return array<string,string>
     */
    public static function extractConnection(array $decoded): array
    {
        $sources = [];
        foreach (['connection', 'binding'] as $key) {
            if (isset($decoded[$key]) && is_array($decoded[$key])) {
                $sources[] = $decoded[$key];
            }
        }

        $topLevel = [];
        foreach (['site_id', 'account_id', 'client_id', 'request_secret', 'request_kid', 'kid'] as $key) {
            if (isset($decoded[$key]) && is_scalar($decoded[$key])) {
                $topLevel[$key] = (string) $decoded[$key];
            }
        }
        if (!empty($topLevel)) {
            $sources[] = $topLevel;
        }

        $out = [];
        foreach ($sources as $source) {
            $source = is_array($source) ? $source : [];
            foreach ([
                'site_id' => ['site_id', 'installation_id', 'install_id', 'site_uuid'],
                'account_id' => ['account_id', 'customer_id', 'account_uuid'],
                'client_id' => ['client_id', 'client_uuid'],
                'request_secret' => ['request_secret', 'request_token', 'site_token', 'client_secret'],
                'request_kid' => ['request_kid', 'kid', 'key_id'],
            ] as $target => $aliases) {
                if (!empty($out[$target])) {
                    continue;
                }
                foreach ($aliases as $alias) {
                    if (!isset($source[$alias]) || !is_scalar($source[$alias])) {
                        continue;
                    }
                    $value = trim((string) $source[$alias]);
                    if ($value === '') {
                        continue;
                    }
                    if ($target === 'request_secret') {
                        $value = substr($value, 0, 4096);
                    } else {
                        $value = preg_replace('/[^A-Za-z0-9._:@\-]/', '', $value);
                        $value = substr((string) $value, 0, 190);
                    }
                    if ($value !== '') {
                        $out[$target] = $value;
                        break;
                    }
                }
            }
        }

        $meta = self::extractResponseMeta($decoded);
        if (!empty($meta['contract_version'])) {
            $out['contract_version'] = (string) $meta['contract_version'];
        }
        if (!empty($meta['kid']) && empty($out['request_kid'])) {
            $out['request_kid'] = (string) $meta['kid'];
        }

        return $out;
    }

    /**
     * @return string[]
     */
    private function enabledModules(): array
    {
        $mods = (array) $this->settings->get('modules', []);
        $enabled = [];
        foreach ($mods as $moduleId => $isEnabled) {
            if (!is_string($moduleId) || !$isEnabled) {
                continue;
            }
            $moduleId = sanitize_key($moduleId);
            if ($moduleId !== '') {
                $enabled[] = $moduleId;
            }
        }
        sort($enabled, SORT_STRING);

        return array_values(array_unique($enabled));
    }

    /**
     * @param array<string,mixed> $payload
     */
    private function requestSignature(array $payload): string
    {
        $secret = trim($this->secrets->getRequestSecret());
        if ($secret === '') {
            return '';
        }

        $json = $this->jsonEncode($payload);
        if ($json === '') {
            return '';
        }

        return hash_hmac('sha256', $json, $secret);
    }

    /**
     * @param mixed $value
     * @return mixed
     */
    private function canonicalize($value)
    {
        if (is_array($value)) {
            if ($this->isAssoc($value)) {
                ksort($value);
            }
            foreach ($value as $key => $item) {
                $value[$key] = $this->canonicalize($item);
            }
        }

        return $value;
    }

    /**
     * @param array<mixed> $arr
     */
    private function isAssoc(array $arr): bool
    {
        $keys = array_keys($arr);
        return array_keys($keys) !== $keys;
    }
}
