<?php

namespace WPSN\Modules\Licensing;

use WPSN\Core\Http\HttpClient;
use WPSN\Core\Logger;
use WPSN\Core\Settings;

defined('ABSPATH') || exit;

/**
 * Optional plugin update client (for Pro builds).
 *
 * This integrates with WordPress' update system via:
 * - pre_set_site_transient_update_plugins
 * - plugins_api (plugin_information)
 */
final class UpdateClient
{
    private const CACHE_KEY = 'wpsn_update_cache';

    private Settings $settings;
    private Logger $logger;
    private LicenseService $license;
    private ServerContract $contract;
    private LicenseSignatureVerifier $sig;
    private LicenseSecretsRepository $secrets;

    public function __construct(Settings $settings, Logger $logger, LicenseService $license)
    {
        $this->settings = $settings;
        $this->logger = $logger;
        $this->license = $license;
        $this->contract = new ServerContract($settings);
        $this->sig = new LicenseSignatureVerifier($settings);
        $this->secrets = new LicenseSecretsRepository();
    }

    public function register(): void
    {
        add_filter('pre_set_site_transient_update_plugins', [$this, 'filterUpdateTransient']);
        add_filter('plugins_api', [$this, 'filterPluginInformation'], 20, 3);
    }

    /**
     * @param object|false $transient
     * @return object|false
     */
    public function filterUpdateTransient($transient)
    {
        if (!$transient || !is_object($transient)) {
            return $transient;
        }

        if (!$this->updatesEnabled()) {
            return $transient;
        }

        $pluginFile = plugin_basename((string) WPSN_PLUGIN_FILE);
        if (empty($transient->checked) || !is_array($transient->checked) || empty($transient->checked[$pluginFile])) {
            return $transient;
        }

        $info = $this->getUpdateInfo(false);
        if (!$info) {
            return $transient;
        }

        $newVersion = isset($info['version']) ? (string) $info['version'] : '';
        $package = isset($info['download_url']) ? (string) $info['download_url'] : '';
        if ($newVersion === '' || $package === '') {
            return $transient;
        }

        $current = (string) $transient->checked[$pluginFile];
        if (version_compare($newVersion, $current, '<=')) {
            return $transient;
        }

        $obj = (object) [
            'slug' => $this->pluginSlug(),
            'plugin' => $pluginFile,
            'new_version' => $newVersion,
            'package' => $package,
            'url' => isset($info['homepage']) ? (string) $info['homepage'] : '',
            'tested' => isset($info['tested']) ? (string) $info['tested'] : '',
            'requires' => isset($info['requires']) ? (string) $info['requires'] : '',
            'requires_php' => isset($info['requires_php']) ? (string) $info['requires_php'] : '',
        ];

        if (!isset($transient->response) || !is_array($transient->response)) {
            $transient->response = [];
        }

        $transient->response[$pluginFile] = $obj;
        return $transient;
    }

    /**
     * @param false|object|array<mixed> $result
     * @param string $action
     * @param object $args
     * @return mixed
     */
    public function filterPluginInformation($result, string $action, $args)
    {
        if ($action !== 'plugin_information') {
            return $result;
        }

        if (!is_object($args) || empty($args->slug) || (string) $args->slug !== $this->pluginSlug()) {
            return $result;
        }

        if (!$this->updatesEnabled()) {
            return $result;
        }

        $info = $this->getUpdateInfo(false);
        if (!$info) {
            return $result;
        }

        $sections = isset($info['sections']) && is_array($info['sections']) ? $info['sections'] : [];

        return (object) [
            'name' => isset($info['name']) ? (string) $info['name'] : 'WP SEO Next',
            'slug' => $this->pluginSlug(),
            'version' => isset($info['version']) ? (string) $info['version'] : (defined('WPSN_VERSION') ? (string) WPSN_VERSION : ''),
            'author' => isset($info['author']) ? (string) $info['author'] : '',
            'homepage' => isset($info['homepage']) ? (string) $info['homepage'] : '',
            'requires' => isset($info['requires']) ? (string) $info['requires'] : '',
            'tested' => isset($info['tested']) ? (string) $info['tested'] : '',
            'requires_php' => isset($info['requires_php']) ? (string) $info['requires_php'] : '',
            'download_link' => isset($info['download_url']) ? (string) $info['download_url'] : '',
            'sections' => $sections,
        ];
    }

    private function pluginSlug(): string
    {
        return 'wp-seo-next';
    }

    private function updatesEnabled(): bool
    {
        if (!(bool) $this->settings->get('licensing.enable_updates', false)) {
            return false;
        }

        if ((bool) $this->settings->get('privacy.local_only', false)) {
            return false;
        }

        if (!$this->license->isActive()) {
            return false;
        }

        if ($this->license->requireSigned() && !$this->license->isTrusted()) {
            return false;
        }

        return true;
    }

    /**
     * @return array<string,mixed>|null
     */
    private function getUpdateInfo(bool $force): ?array
    {
        $cached = get_site_transient(self::CACHE_KEY);
        if (!$force && is_array($cached) && !empty($cached['fetched_at'])) {
            $age = time() - (int) $cached['fetched_at'];
            if ($age < (6 * HOUR_IN_SECONDS)) {
                return $cached;
            }
        }

        $res = $this->fetchUpdateInfo();
        if (!$res) {
            return is_array($cached) ? $cached : null;
        }

        $res['fetched_at'] = time();
        set_site_transient(self::CACHE_KEY, $res, 6 * HOUR_IN_SECONDS);
        return $res;
    }

    /**
     * @return array<string,mixed>|null
     */
    private function fetchUpdateInfo(): ?array
    {
        $serverUrl = trim((string) $this->settings->get('licensing.update_server_url', ''));
        if ($serverUrl === '') {
            $serverUrl = trim((string) $this->settings->get('licensing.server_url', ''));
        }
        if ($serverUrl === '') {
            return null;
        }

        $path = trim((string) $this->settings->get('licensing.update_path', '/wp-seo-next/plugin/update'));
        if ($path === '') {
            $path = '/wp-seo-next/plugin/update';
        }

        $endpoint = rtrim($serverUrl, '/') . '/' . ltrim($path, '/');
        $payload = $this->contract->buildPayload('update_check', [
            'license_key' => $this->license->getLicenseKey(),
        ]);

        $res = HttpClient::post($endpoint, [
            'timeout' => 15,
            'headers' => $this->contract->buildHeaders('licensing', 'update_check', $payload),
            'body' => $this->contract->jsonEncode($payload),
        ], [
            'service' => 'licensing',
            'context' => 'update_check',
            'retries' => 1,
            'retry_statuses' => [429, 500, 502, 503, 504],
        ]);
        if (is_wp_error($res)) {
            $this->logger->warning('Update check failed: ' . $res->get_error_message());
            return null;
        }

        $code = (int) wp_remote_retrieve_response_code($res);
        $body = (string) wp_remote_retrieve_body($res);
        $decoded = $body !== '' ? json_decode($body, true) : null;
        if (!is_array($decoded)) {
            $this->logger->warning('Update check returned invalid JSON (HTTP ' . $code . ').');
            return null;
        }

        if (!array_key_exists('ok', $decoded) || empty($decoded['ok'])) {
            $err = isset($decoded['error']) ? (string) $decoded['error'] : 'Update server returned ok=false.';
            $this->logger->warning('Update check failed (HTTP ' . $code . '): ' . $err);
            return null;
        }

        $meta = $this->augmentMeta(ServerContract::extractResponseMeta($decoded), $res);
        $connection = ServerContract::extractConnection($decoded);
        $kid = isset($meta['kid']) ? (string) $meta['kid'] : '';

        $update = $this->extractUpdatePayload($decoded);
        if (empty($update)) {
            $this->logger->warning('Update payload was empty after normalization.');
            return null;
        }

        $signature = isset($decoded['signature']) ? (string) $decoded['signature'] : '';
        $alg = isset($decoded['alg']) ? (string) $decoded['alg'] : '';

        $trusted = false;
        if ($signature !== '') {
            $trusted = $this->sig->verify($update, $signature, $alg, $kid);
        }
        $trusted = (bool) apply_filters('wpsn/licensing/update/trusted', $trusted, $update, $signature, $alg, $kid, $meta);

        $this->persistConnection($connection, $trusted);

        if ($this->license->requireSigned() && !$trusted) {
            $this->logger->warning('Update manifest rejected because the signature is missing or untrusted.');
            return null;
        }

        $update['trusted'] = $trusted;
        $update['signature'] = $signature;
        $update['alg'] = $alg;
        $update['server_meta'] = $meta;
        $cacheConnection = $connection;
        unset($cacheConnection['request_secret']);
        if (!empty($cacheConnection)) {
            $update['server_connection'] = $cacheConnection;
        }

        return $update;
    }

    /**
     * @param array<string,mixed> $decoded
     * @return array<string,mixed>
     */
    private function extractUpdatePayload(array $decoded): array
    {
        if (isset($decoded['update']) && is_array($decoded['update'])) {
            return $decoded['update'];
        }

        $copy = $decoded;
        unset(
            $copy['ok'],
            $copy['status'],
            $copy['error'],
            $copy['signature'],
            $copy['alg'],
            $copy['message'],
            $copy['meta'],
            $copy['connection'],
            $copy['binding'],
            $copy['request_id'],
            $copy['trace_id'],
            $copy['kid'],
            $copy['contract_version'],
            $copy['server_time'],
            $copy['site_id'],
            $copy['account_id'],
            $copy['client_id'],
            $copy['request_secret'],
            $copy['request_kid']
        );

        return is_array($copy) ? $copy : [];
    }

    /**
     * @param array<string,string> $meta
     * @return array<string,string>
     */
    private function augmentMeta(array $meta, $res): array
    {
        $map = [
            'kid' => ['x-wpsn-key-id', 'x-wpsn-kid'],
            'trace_id' => ['x-wpsn-trace-id'],
            'request_id' => ['x-wpsn-request-id'],
            'contract_version' => ['x-wpsn-contract-version'],
            'server_time' => ['date', 'x-wpsn-server-time'],
        ];

        foreach ($map as $target => $headers) {
            if (!empty($meta[$target])) {
                continue;
            }
            foreach ($headers as $header) {
                $value = wp_remote_retrieve_header($res, $header);
                if (!is_scalar($value)) {
                    continue;
                }
                $value = trim((string) $value);
                if ($value !== '') {
                    $meta[$target] = substr($value, 0, 190);
                    break;
                }
            }
        }

        return $meta;
    }

    /**
     * @param array<string,string> $connection
     */
    private function persistConnection(array $connection, bool $trusted): void
    {
        if (empty($connection)) {
            return;
        }

        if (!$trusted) {
            unset($connection['request_secret']);
        }

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

        $this->secrets->updateServerBinding($connection);
    }
}
