<?php

declare(strict_types=1);

namespace Duplicator\Addons\ProBase\Telemetry;

use Duplicator\Addons\ProBase\License\License;
use Duplicator\Addons\ProBase\Models\LicenseData;
use Duplicator\Core\UniqueId;
use Duplicator\Utils\UsageStatistics\PluginData;

/**
 * Builds the identified snapshot payload sent to the telemetry worker.
 *
 * Strategy: reuse the Connect-bound payload from PluginData (which already
 * collects every counter we need), then layer on top:
 *   - identified fields (license_key, site_url),
 *   - raw int license_status / plan_tier (worker maps int -> label),
 *   - the failure-streak counter,
 *   - capability flags actually exposed by License::can(),
 *   - rename packages_count -> current_backups_count,
 *   - rename packages_build_count -> lifetime_backups_built.
 *
 * No new collection: every field is already populated for Connect.
 */
class TelemetrySnapshot
{
    const FAILURE_STREAK_OPTION = 'dupli_opt_telemetry_failure_streak';
    const FIRST_SENT_OPTION     = 'dupli_opt_telemetry_first_sent';

    /**
     * Build the snapshot payload.
     *
     * @return array<string, mixed>
     */
    public static function collect(): array
    {
        $base = PluginData::getInstance()->getDataToSend();

        // Plan §12: rename to disambiguate "currently stored" vs "ever built".
        if (array_key_exists('packages_count', $base)) {
            $base['current_backups_count'] = $base['packages_count'];
            unset($base['packages_count']);
        }
        if (array_key_exists('packages_build_count', $base)) {
            $base['lifetime_backups_built'] = $base['packages_build_count'];
            unset($base['packages_build_count']);
        }

        // Identity envelope shared with the events POST (single source of
        // truth for the identification block).
        $base = array_merge($base, self::collectIdentity());

        $base['license_limit'] = LicenseData::getInstance()->getLicenseLimit();

        $base['backup_failures_streak'] = self::getFailureStreak();

        $base = array_merge($base, self::getCapabilities());

        /**
         * Hook for addons to enrich the snapshot with their own counters.
         *
         * Loose coupling: an addon can `add_filter()` here without depending
         * on probase. If probase is disabled the filter never fires.
         *
         * Conventions:
         *   - addons own their own key namespaces (e.g. `schedules_*`,
         *     `staging_*`, `branding_*`) and merge into $payload.
         *   - keep the field naming/units rules from the plan (sizes in MB,
         *     durations in seconds, dates as `_date`, counts as `_count`).
         *
         * @param array<string, mixed> $payload Snapshot built by probase
         */
        return apply_filters('duplicator_telemetry_snapshot', $base);
    }

    /**
     * Identification envelope shared by every payload sent to the worker
     * (snapshot and events). Single source of truth for license/site
     * identification — keeping it here prevents drift between the snapshot
     * and event POSTs.
     *
     * @return array<string, mixed>
     */
    public static function collectIdentity(): array
    {
        return [
            'license_key'    => License::getLicenseKey(),
            'identifier'     => UniqueId::getInstance()->getIdentifier(),
            'site_url'       => get_site_url(),
            // Raw ints; the worker keeps the int -> label table.
            'plan_tier'      => License::getType(),
            'license_status' => License::getLicenseStatus(),
        ];
    }

    /**
     * Capability flags derived from License::can(). Only the FEATURE_*
     * constants actually defined on AbstractLicense are exposed here; gates
     * that live in other addons (staging, multisite, branding, advanced
     * permissions) are derivable by the worker from plan_tier.
     *
     * @return array<string, bool>
     */
    private static function getCapabilities(): array
    {
        return [
            'has_custom_components' => License::can(License::FEATURE_PACKAGE_COMPONENTS_PLUS),
            'has_storage_plus'      => License::can(License::FEATURE_STORAGE_PLUS),
        ];
    }

    /**
     * Send the snapshot to the worker. Returns true on success.
     *
     * @return bool
     */
    public static function send(): bool
    {
        if (!TelemetryClient::isEnabled()) {
            return false;
        }
        return TelemetryClient::post(TelemetryClient::ROUTE_SNAPSHOT, self::collect());
    }

    /**
     * Send the first snapshot on plugin activation/upgrade, gated by a
     * one-shot option flag so it fires exactly once per site across the
     * lifetime of the install.
     *
     * Called from TelemetryBootstrap::cronActivate() on the
     * `duplicator_after_activation` hook.
     *
     * @return void
     */
    public static function maybeSendFirstSnapshot(): void
    {
        if (!TelemetryClient::isEnabled()) {
            return;
        }
        if (get_option(self::FIRST_SENT_OPTION, false)) {
            return;
        }
        if (self::send()) {
            update_option(self::FIRST_SENT_OPTION, time(), false);
        }
    }

    /**
     * Cron handler for the steady-state weekly snapshot. Runs independently
     * from the Connect cron so we don't inherit Connect's randomized delay.
     *
     * @return void
     */
    public static function cronSend(): void
    {
        self::send();
    }

    /**
     * Read the current failure-streak counter.
     *
     * @return int
     */
    public static function getFailureStreak(): int
    {
        return (int) get_option(self::FAILURE_STREAK_OPTION, 0);
    }

    /**
     * Increment the failure-streak counter. Called on `duplicator_build_fail`
     * by TelemetryEvents (cancellations excluded, see plan "Cancellation
     * deduplication").
     *
     * @return void
     */
    public static function incrementFailureStreak(): void
    {
        update_option(self::FAILURE_STREAK_OPTION, self::getFailureStreak() + 1, false);
    }

    /**
     * Reset the failure-streak counter to zero. Called on a successful
     * `duplicator_package_transfer_completed`.
     *
     * @return void
     */
    public static function resetFailureStreak(): void
    {
        update_option(self::FAILURE_STREAK_OPTION, 0, false);
    }
}
