<?php

declare(strict_types=1);

namespace PrettyLinks\Pro\Licensing;

defined('ABSPATH') || exit;

use PrettyLinks\Licensing\EditionMismatch;
use PrettyLinks\Licensing\LicenseClient;
use PrettyLinks\Licensing\LicenseManager;
use PrettyLinks\Licensing\ProState;

/**
 * Per-add-on plugin-update injector.
 *
 * Ports v3.x `PrliAddonUpdates` from `app/lib/PrliAddonUpdates.php`. Each
 * separately-installed Pretty Links add-on plugin instantiates this class in
 * its own bootstrap so its update entries show up on the WordPress Plugins
 * screen:
 *
 *     new \PrettyLinks\Pro\Licensing\AddonUpdateChecker(
 *         $slug,             // e.g. 'pretty-link-bitly'
 *         $mainFile,         // e.g. 'pretty-link-bitly/pretty-link-bitly.php'
 *         $installedVersion, // e.g. '1.2.3'
 *         $name,             // display name for the "View details" modal
 *         $description       // short description for the modal
 *     );
 *
 * Hooks registered per instance:
 *   pre_set_site_transient_update_plugins        → queueUpdate
 *   plugins_api (priority 11)                    → pluginInfo
 *   prli_plugin_edition_changed                  → clearUpdateTransient
 *   prli_license_activated_before_queue_update   → clearUpdateTransient
 *   prli_license_deactivated_before_queue_update → clearUpdateTransient
 *   in_plugin_update_message-{mainFile}          → EditionMismatch::pluginUpdateRowMessage
 *
 * Transient: `prli_update_info_{slug}` (12h TTL). Matches v3 exactly so any
 * tooling that inspects the transient directly keeps working.
 */
final class AddonUpdateChecker
{
    public const UPDATE_INFO_TTL = 12;

    /**
     * Add-on plugin slug.
     *
     * @var string
     */
    private string $slug;

    /**
     * Add-on main plugin file (relative path).
     *
     * @var string
     */
    private string $mainFile;

    /**
     * Installed add-on version.
     *
     * @var string
     */
    private string $installedVersion;

    /**
     * Add-on display name.
     *
     * @var string
     */
    private string $name;

    /**
     * Add-on short description.
     *
     * @var string
     */
    private string $description;

    /**
     * License manager.
     *
     * @var LicenseManager
     */
    private LicenseManager $manager;

    /**
     * License client.
     *
     * @var LicenseClient
     */
    private LicenseClient $client;

    /**
     * Constructor. Registers the per-add-on update hooks.
     *
     * @param string              $slug             Add-on plugin slug.
     * @param string              $mainFile         Add-on main plugin file (relative path).
     * @param string              $installedVersion Installed add-on version.
     * @param string              $name             Add-on display name.
     * @param string              $description      Add-on short description.
     * @param LicenseManager|null $manager          Optional license manager (defaults to a new instance).
     * @param LicenseClient|null  $client           Optional license client (defaults to a new instance).
     */
    public function __construct(
        string $slug,
        string $mainFile,
        string $installedVersion,
        string $name,
        string $description,
        ?LicenseManager $manager = null,
        ?LicenseClient $client = null
    ) {
        $this->slug             = $slug;
        $this->mainFile         = $mainFile;
        $this->installedVersion = $installedVersion;
        $this->name             = $name;
        $this->description      = $description;
        $this->manager          = $manager ?? new LicenseManager();
        $this->client           = $client ?? new LicenseClient();

        add_filter('pre_set_site_transient_update_plugins', [$this, 'queueUpdate']);
        add_filter('plugins_api', [$this, 'pluginInfo'], 11, 3);
        // Surface the parent plugin's edition-mismatch warning on the add-on's
        // own plugin-update row — v3 behavior. Auto-updates can't work when
        // the main plugin's edition doesn't match the license, and this is
        // the user's breadcrumb to fix it.
        add_action(
            'in_plugin_update_message-' . $mainFile,
            [EditionMismatch::class, 'pluginUpdateRowMessage'],
            10,
            2
        );
        add_action('prli_plugin_edition_changed', [$this, 'clearUpdateTransient']);
        add_action('prli_license_activated_before_queue_update', [$this, 'clearUpdateTransient']);
        add_action('prli_license_deactivated_before_queue_update', [$this, 'clearUpdateTransient']);
    }

    /**
     * Inject the add-on's update row into WP's `update_plugins` site transient.
     *
     * @param  mixed $transient The `update_plugins` site transient.
     * @return mixed
     */
    public function queueUpdate($transient)
    {
        if (!is_object($transient) || empty($transient->checked)) {
            return $transient;
        }

        $transientKey = 'prli_update_info_' . $this->slug;
        $info         = get_site_transient($transientKey);

        if (!is_array($info)) {
            $info = $this->fetchUpdateInfo();
            if ($info === null) {
                if (isset($transient->response[$this->mainFile])) {
                    unset($transient->response[$this->mainFile]);
                }
                return $transient;
            }
            set_site_transient($transientKey, $info, self::UPDATE_INFO_TTL * HOUR_IN_SECONDS);
        }

        $currVersion = (string) ($info['curr_version'] ?? $this->installedVersion);
        $downloadUrl = (string) ($info['download_url'] ?? '');

        global $wp_version;

        if (version_compare($currVersion, $this->installedVersion, '>')) {
            if (!isset($transient->response) || !is_array($transient->response)) {
                $transient->response = [];
            }
            $transient->response[$this->mainFile] = (object) [
                'id'          => $this->mainFile,
                'slug'        => $this->slug,
                'plugin'      => $this->mainFile,
                'new_version' => $currVersion,
                'url'         => 'https://prettylinks.com/',
                'package'     => $downloadUrl,
                'tested'      => $wp_version,
            ];
        } else {
            if (isset($transient->response[$this->mainFile])) {
                unset($transient->response[$this->mainFile]);
            }
            if (!isset($transient->no_update) || !is_array($transient->no_update)) {
                $transient->no_update = [];
            }
            // Populating `no_update` is what lets WordPress show the
            // "Enable auto-updates" link on the Plugins screen.
            $transient->no_update[$this->mainFile] = (object) [
                'id'          => $this->mainFile,
                'slug'        => $this->slug,
                'plugin'      => $this->mainFile,
                'new_version' => $this->installedVersion,
                'url'         => 'https://prettylinks.com/',
                'package'     => '',
            ];
        }

        return $transient;
    }

    /**
     * Provide the "View details" modal payload on the Plugins screen.
     *
     * @param  mixed  $api    Result from earlier plugins_api filters.
     * @param  string $action The plugins_api action being performed.
     * @param  object $args   The plugins_api request arguments.
     * @return mixed
     */
    public function pluginInfo($api, $action, $args) // phpcs:ignore Squiz.Commenting.FunctionComment.ScalarTypeHintMissing -- WP plugins_api filter callback signature is fixed; $action stays untyped.
    {
        if ($action !== 'plugin_information') {
            return $api;
        }
        if (!isset($args->slug) || (string) $args->slug !== $this->slug) {
            return $api;
        }

        $callArgs = [];
        if ($this->manager->edgeEnabled()) {
            $callArgs['edge'] = 'true';
        }

        $currVersion = null;
        $versionDate = '';
        $downloadUrl = '';

        // Only query the licensed endpoint when Pro is activated — an
        // expired-key install should fall through to the anonymous
        // latest-version endpoint so the modal still populates a
        // version number (without a download link).
        $key = (string) get_option(LicenseManager::OPTION_LICENSE_KEY, '');
        if ($key !== '' && ProState::isProInstalledAndActivated()) {
            $licensed = $this->client->addonVersionInfo($this->slug, $key, $callArgs);
            if (is_array($licensed) && !isset($licensed['error']) && !empty($licensed['version'])) {
                $currVersion = (string) $licensed['version'];
                $versionDate = (string) ($licensed['version_date'] ?? '');
                $downloadUrl = (string) ($licensed['url'] ?? '');
            }
        }

        if ($currVersion === null) {
            $latest = $this->client->latestVersion($this->slug, $callArgs);
            if (!is_array($latest) || isset($latest['error']) || empty($latest['version'])) {
                return $api;
            }
            $currVersion = (string) $latest['version'];
            $versionDate = (string) ($latest['version_date'] ?? '');
        }

        if (EditionMismatch::detect() !== null) {
            $downloadUrl = '';
        }

        global $wp_version;

        return (object) [
            'slug'           => $this->slug,
            'name'           => esc_html($this->name),
            'author'         => defined('PRLI_AUTHOR') ? (string) constant('PRLI_AUTHOR') : 'Pretty Links',
            'author_profile' => defined('PRLI_AUTHOR_URI') ? (string) constant('PRLI_AUTHOR_URI') : 'https://prettylinks.com',
            'homepage'       => 'https://prettylinks.com/',
            'version'        => $currVersion,
            'new_version'    => $currVersion,
            'requires'       => '5.2',
            'requires_php'   => '7.4',
            'tested'         => $wp_version,
            // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- $wp_version is read here as an array key, not reassigned.
            'compatibility'  => [$wp_version => [$currVersion => [100, 0, 0]]],
            'rating'         => '100.00',
            'num_ratings'    => '1',
            'last_updated'   => $versionDate,
            'sections'       => [
                'description' => '<p>' . $this->description . '</p>',
            ],
            'download_link'  => $downloadUrl,
        ];
    }

    /**
     * Clears the cached add-on update-info transient.
     *
     * @return void
     */
    public function clearUpdateTransient(): void
    {
        delete_site_transient('prli_update_info_' . $this->slug);
    }

    /**
     * Fetches the latest add-on version info from the licensing endpoint.
     *
     * @return array{curr_version: string, download_url: string}|null
     */
    private function fetchUpdateInfo(): ?array
    {
        $args = [];
        if ($this->manager->edgeEnabled()) {
            $args['edge'] = 'true';
        }

        $key = (string) get_option(LicenseManager::OPTION_LICENSE_KEY, '');
        if ($key === '' || !ProState::isProInstalledAndActivated()) {
            $response = $this->client->latestVersion($this->slug, $args);
            if (isset($response['error']) || empty($response['version'])) {
                return null;
            }
            return [
                'curr_version' => (string) $response['version'],
                'download_url' => '',
            ];
        }

        $response = $this->client->addonVersionInfo($this->slug, $key, $args);
        if (is_array($response) && !isset($response['error']) && !empty($response['version'])) {
            // V3 parity: blank the download URL when the parent plugin's
            // installed edition doesn't match the license. Add-on updates
            // share the parent's license gate — if the license won't issue
            // updates to the main plugin, the add-on package URL is stale.
            $downloadUrl = (string) ($response['url'] ?? '');
            if (EditionMismatch::detect() !== null) {
                $downloadUrl = '';
            }
            return [
                'curr_version' => (string) $response['version'],
                'download_url' => $downloadUrl,
            ];
        }

        $fallback = $this->client->latestVersion($this->slug, $args);
        if (isset($fallback['error']) || empty($fallback['version'])) {
            return null;
        }
        return [
            'curr_version' => (string) $fallback['version'],
            'download_url' => '',
        ];
    }
}
