<?php

/**
 * Freemius SDK Integration
 *
 * Integrates the Freemius WordPress SDK for opt-in diagnostics, account
 * management, and — in premium builds — plan enforcement and monetisation.
 *
 * In the free (WordPress.org) build, Freemius handles only:
 *  - Anonymous opt-in diagnostics (users can opt out)
 *  - Contact form and support links
 *  - Account and upgrade page
 *
 * All license validation and feature gating code is wrapped in
 * is__premium_only() guards and is stripped by Freemius from the free
 * build at compile time.
 */
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
// ── PUBLISHER: set these values before distributing the plugin ────────────
//
// You (the plugin seller) must paste your real Freemius Plugin ID and Public
// Key here — or define them in wp-config.php — before packaging the plugin
// for distribution. End users never need to configure these; Freemius handles
// license validation transparently once the correct credentials are embedded.
//
// Obtain your credentials from: Freemius Dashboard → your plugin → Deployment
//
if ( !defined( 'STRP_SUB_FREEMIUS_PLUGIN_ID' ) ) {
    // Publisher: Insert your numeric Plugin ID (e.g. 12345) below prior to release compilation.
    define( 'STRP_SUB_FREEMIUS_PLUGIN_ID', 25202 );
}
if ( !defined( 'STRP_SUB_FREEMIUS_PUBLIC_KEY' ) ) {
    // Publisher: Insert your Public Key (e.g. 'pk_xxxxxx') below prior to release compilation.
    // Public key (not a secret). Replace with your own if forking this repository.
    define( 'STRP_SUB_FREEMIUS_PUBLIC_KEY', 'pk_3f286966d406efe02e9d881632025' );
}
// FAIL_OPEN: development-only override. Hardcoded false for production builds.
// To enable during local development, change this value directly in this file.
// Cannot be overridden from wp-config.php to prevent trivial license bypass.
if ( !defined( 'STRP_SUB_FREEMIUS_FAIL_OPEN' ) ) {
    define( 'STRP_SUB_FREEMIUS_FAIL_OPEN', false );
}
// ─────────────────────────────────────────────────────────────────────────
final class STRP_SUB_Freemius {
    private static $instance = null;

    /** @var \Freemius|null Freemius SDK singleton, null when SDK is absent. */
    private static $fs = null;

    /**
     * Return the Freemius SDK instance (read-only access for strp_sub_fs() helper).
     *
     * @return \Freemius|null
     */
    public static function get_fs() {
        return ( self::$fs instanceof \Freemius ? self::$fs : null );
    }

    public static function get_instance() {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __clone() {
    }

    public function __wakeup() {
        throw new \RuntimeException('Cannot unserialize singleton.');
    }

    private function __construct() {
        $this->boot();
    }

    // ── Boot ──────────────────────────────────────────────────────────────
    /**
     * Initialise the Freemius SDK if it is present and correctly configured.
     * Shows actionable admin notices when setup is incomplete.
     */
    private function boot() {
        // When FAIL_OPEN is active, skip Freemius entirely so it cannot
        // intercept admin pages (e.g. pending-activation email gate).
        // All feature checks already return true via FAIL_OPEN.
        if ( STRP_SUB_FREEMIUS_FAIL_OPEN ) {
            return;
        }
        $sdk_start = STRP_SUB_PLUGIN_DIR . 'vendor/freemius/wordpress-sdk/start.php';
        if ( !file_exists( $sdk_start ) ) {
            add_action( 'admin_notices', array($this, 'notice_sdk_missing') );
            return;
        }
        if ( STRP_SUB_FREEMIUS_PLUGIN_ID === 0 || self::is_placeholder_key( STRP_SUB_FREEMIUS_PUBLIC_KEY ) ) {
            add_action( 'admin_notices', array($this, 'notice_unconfigured') );
            return;
        }
        if ( !function_exists( 'fs_dynamic_init' ) ) {
            require_once $sdk_start;
        }
        self::$fs = fs_dynamic_init( array(
            'id'               => STRP_SUB_FREEMIUS_PLUGIN_ID,
            'slug'             => 'subscription-manager-for-stripe',
            'type'             => 'plugin',
            'public_key'       => STRP_SUB_FREEMIUS_PUBLIC_KEY,
            'is_premium'       => false,
            'is_premium_only'  => false,
            'premium_suffix'   => 'Pro',
            'has_addons'       => false,
            'has_paid_plans'   => true,
            'is_org_compliant' => true,
            'menu'             => array(
                'slug'    => 'strp-sub-dashboard',
                'contact' => true,
                'support' => false,
                'pricing' => true,
                'account' => true,
            ),
            'is_live'          => true,
        ) );
        self::$fs->add_action( 'after_uninstall', array($this, 'on_uninstall') );
    }

    // ── Plan feature gates (premium build only) ──────────────────────────
    /**
     * Is the current site on any paid plan or active trial?
     *
     * In the free build this always returns false (the is__premium_only
     * block is stripped by Freemius at compile time).
     */
    public static function is_premium() {
        if ( STRP_SUB_FREEMIUS_FAIL_OPEN ) {
            return true;
        }
        if ( function_exists( 'strp_sub_fs' ) && strp_sub_fs() && strp_sub_fs()->is__premium_only() ) {
            if ( self::$fs instanceof \Freemius ) {
                $result = self::$fs->is_paying() || self::$fs->is_trial();
                if ( $result ) {
                    self::cache_plan_state( 'starter' );
                }
                return $result;
            }
            // SDK unreachable — any cached plan level means premium.
            return self::check_cached_plan( 'starter' );
        }
        return false;
    }

    /**
     * Does the current site have access to the given minimum plan?
     *
     * In the free build this always returns false.
     *
     * @param  string $minimum_plan 'starter' | 'professional' | 'agency'
     * @return bool
     */
    public static function has_feature( $minimum_plan ) {
        // Development override — unlocks all features regardless of license state.
        if ( STRP_SUB_FREEMIUS_FAIL_OPEN ) {
            return true;
        }
        if ( function_exists( 'strp_sub_fs' ) && strp_sub_fs() && strp_sub_fs()->is__premium_only() ) {
            // SDK available — perform live check, cache the result on success.
            if ( self::$fs instanceof \Freemius ) {
                $result = self::check_plan_live( $minimum_plan );
                if ( $result ) {
                    self::cache_plan_state( $minimum_plan );
                }
                return $result;
            }
            // SDK unreachable — fall back to cached license state (72-hour grace).
            return self::check_cached_plan( $minimum_plan );
        }
        return false;
    }

    /**
     * Live plan check against the Freemius SDK.
     *
     * @param  string $minimum_plan 'starter' | 'professional' | 'agency'
     * @return bool
     */
    private static function check_plan_live( $minimum_plan ) {
        // Only called from has_feature() which already guards with is__premium_only().
        switch ( $minimum_plan ) {
            case 'starter':
                return self::$fs->is_plan( 'starter', true ) || self::$fs->is_plan( 'professional', true ) || self::$fs->is_plan( 'agency', true );
            case 'professional':
                return self::$fs->is_plan( 'professional', true ) || self::$fs->is_plan( 'agency', true );
            case 'agency':
                return self::$fs->is_plan( 'agency', true );
            default:
                return false;
        }
    }

    // ── License grace period cache (premium build only) ─────────────────
    /** @var int Grace period in seconds (72 hours). */
    private const GRACE_PERIOD = 259200;

    /**
     * Cache a successful plan validation so transient SDK outages don't
     * lock paying customers out of features they've already validated.
     *
     * @param string $minimum_plan Plan level that was validated.
     */
    private static function cache_plan_state( $minimum_plan ) {
        // Only called from is_premium()/has_feature() which already guard with is__premium_only().
        $key = 'strp_sub_lic_' . $minimum_plan;
        $data = array(
            'v' => 1,
            't' => time(),
            'h' => hash_hmac( 'sha256', $minimum_plan . '|' . time(), wp_salt( 'auth' ) ),
        );
        set_site_transient( $key, $data, self::GRACE_PERIOD );
    }

    /**
     * Check the cached license state when the SDK is unreachable.
     *
     * @param  string $minimum_plan Plan level being checked.
     * @return bool
     */
    private static function check_cached_plan( $minimum_plan ) {
        // Only called from is_premium()/has_feature() which already guard with is__premium_only().
        // Plan hierarchy: higher plans grant access to lower features.
        $hierarchy = array(
            'starter'      => array('starter', 'professional', 'agency'),
            'professional' => array('professional', 'agency'),
            'agency'       => array('agency'),
        );
        $plans_to_check = $hierarchy[$minimum_plan] ?? array();
        foreach ( $plans_to_check as $plan ) {
            $key = 'strp_sub_lic_' . $plan;
            $data = get_site_transient( $key );
            if ( !is_array( $data ) || empty( $data['t'] ) || empty( $data['h'] ) ) {
                continue;
            }
            // Verify the cache hasn't expired beyond the grace period.
            if ( time() - (int) $data['t'] > self::GRACE_PERIOD ) {
                delete_site_transient( $key );
                continue;
            }
            // Verify the HMAC to detect direct database tampering.
            $expected = hash_hmac( 'sha256', $plan . '|' . (int) $data['t'], wp_salt( 'auth' ) );
            if ( hash_equals( $expected, $data['h'] ) ) {
                return true;
            }
            // HMAC mismatch — tampered cache entry, discard it.
            delete_site_transient( $key );
        }
        return false;
    }

    /**
     * Render an upgrade notice inside a feature that requires a higher plan.
     *
     * @param string $required_plan 'starter' | 'professional' | 'agency'
     * @param string $feature_name  Human-readable feature name.
     */
    public static function render_upgrade_notice( $required_plan, $feature_name ) {
        if ( function_exists( 'strp_sub_fs' ) && strp_sub_fs() && strp_sub_fs()->is__premium_only() ) {
            if ( !self::$fs instanceof \Freemius ) {
                return;
            }
            $pricing_url = self::$fs->get_upgrade_url();
            $plan_labels = array(
                'starter'      => __( 'Starter ($149/yr)', 'subscription-manager-for-stripe' ),
                'professional' => __( 'Professional ($249/yr)', 'subscription-manager-for-stripe' ),
                'agency'       => __( 'Agency ($499/yr)', 'subscription-manager-for-stripe' ),
            );
            $plan_label = $plan_labels[$required_plan] ?? ucfirst( $required_plan );
            echo '<div class="notice notice-warning" style="padding:12px 16px;">';
            printf(
                '<p><strong>%s</strong> requires the <strong>%s</strong> plan. <a href="%s" target="_blank">Upgrade now &rarr;</a></p>',
                esc_html( $feature_name ),
                esc_html( $plan_label ),
                esc_url( $pricing_url )
            );
            echo '</div>';
        }
    }

    // ── Uninstall ─────────────────────────────────────────────────────────
    /**
     * Clean up all plugin data when the user opts to delete data on uninstall.
     *
     * Hooked to Freemius after_uninstall so their feedback dialog fires first.
     * Uses the native WordPress API where possible so the object cache is invalidated.
     */
    public function on_uninstall() {
        if ( !self::$fs instanceof \Freemius || !self::$fs->is_user_deleting_plugin_data() ) {
            return;
        }
        if ( is_multisite() ) {
            $sites = get_sites( array(
                'fields' => 'ids',
                'number' => 0,
            ) );
            foreach ( $sites as $site_id ) {
                switch_to_blog( $site_id );
                self::run_uninstall_cleanup();
                restore_current_blog();
            }
        } else {
            self::run_uninstall_cleanup();
        }
    }

    /**
     * Full data cleanup for the current blog context.
     */
    private static function run_uninstall_cleanup() {
        global $wpdb;
        // 1. Delete known options via native API (cache-safe).
        $known_options = array(
            'strp_sub_version',
            'strp_sub_settings',
            'strp_sub_stripe_account_id',
            'strp_sub_stripe_access_token',
            'strp_sub_stripe_refresh_token',
            'strp_sub_stripe_publishable_key',
            'strp_sub_freemius_state',
            'strp_sub_oauth_test_access_token',
            'strp_sub_oauth_live_access_token',
            'strp_sub_oauth_test_refresh_token',
            'strp_sub_oauth_live_refresh_token',
            'strp_sub_oauth_test_publishable_key',
            'strp_sub_oauth_live_publishable_key',
            'strp_sub_stripe_oauth_client_id',
            'strp_sub_stripe_oauth_secret_key',
            'strp_sub_stripe_mode',
            'strp_sub_usage_api_key',
            'strp_sub_email_settings_welcome',
            'strp_sub_email_settings_renewal',
            'strp_sub_email_settings_trial_converted',
            'strp_sub_email_settings_payment_failed',
            'strp_sub_email_settings_fraud_block',
            'strp_sub_lite_volume',
            'strp_sub_retention_migrated',
            // Affiliate Add-on
            'strp_sub_version',
            'strp_sub_commission_rate_monthly',
            'strp_sub_commission_rate_annual',
            'strp_sub_commission_rate_lifetime',
            'strp_sub_affiliate_discount_monthly',
            'strp_sub_affiliate_discount_annual',
            'strp_sub_affiliate_discount_lifetime',
            'strp_sub_monthly_product_id',
            'strp_sub_annual_product_id',
            'strp_sub_lifetime_product_id',
            'strp_sub_affiliate_auto_approve',
            'strp_sub_wipe_data_on_uninstall',
        );
        foreach ( $known_options as $option ) {
            delete_option( $option );
        }
        // Delete remaining dynamic options matching our prefixes.
        $dynamic_options = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options}\n                 WHERE option_name LIKE %s OR option_name LIKE %s", $wpdb->esc_like( 'wcsbs_' ) . '%', $wpdb->esc_like( 'strp_sub_' ) . '%' ) );
        foreach ( $dynamic_options as $option_name ) {
            delete_option( $option_name );
        }
        // 2. Delete user meta via native API (cache-safe).
        $meta_keys = $wpdb->get_col( $wpdb->prepare(
            "SELECT DISTINCT meta_key FROM {$wpdb->usermeta}\n                 WHERE meta_key LIKE %s OR meta_key LIKE %s OR meta_key LIKE %s",
            $wpdb->esc_like( '_wcsbs_' ) . '%',
            $wpdb->esc_like( '_strp_sub_' ) . '%',
            $wpdb->esc_like( 'strp_sub_' ) . '%'
        ) );
        foreach ( $meta_keys as $meta_key ) {
            delete_metadata(
                'user',
                0,
                $meta_key,
                '',
                true
            );
        }
        // 3. Delete transients via native API (cache-safe).
        $transient_names = $wpdb->get_col( $wpdb->prepare(
            "SELECT REPLACE(option_name, '_transient_', '') AS transient_name\n                 FROM {$wpdb->options}\n                 WHERE option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s",
            '_transient_' . $wpdb->esc_like( 'wcsbs_' ) . '%',
            '_transient_' . $wpdb->esc_like( 'strp_sub_' ) . '%',
            '_transient_' . $wpdb->esc_like( 'wcsbs_ps_' ) . '%'
        ) );
        foreach ( $transient_names as $name ) {
            delete_transient( $name );
        }
        // 4. Clear scheduled cron events.
        wp_clear_scheduled_hook( 'strp_sub_process_webhook_retries' );
        // 5. Clean up product post meta.
        $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE %s", $wpdb->esc_like( '_strp_sub_' ) . '%' ) );
        // 6. Drop plugin-specific DB tables.
        $tables = array(
            $wpdb->prefix . 'strp_sub_webhook_failures',
            $wpdb->prefix . 'strp_sub_processed_sessions',
            $wpdb->prefix . 'strp_sub_trial_fingerprints',
            $wpdb->prefix . 'strp_sub_order_index',
            $wpdb->prefix . 'strp_sub_webhook_log',
            $wpdb->prefix . 'strp_sub_retention_events',
            $wpdb->prefix . 'strp_sub_affiliate_payouts',
            $wpdb->prefix . 'strp_sub_affiliate_stats'
        );
        foreach ( $tables as $table ) {
            $wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
        }
    }

    // ── Admin notices ─────────────────────────────────────────────────────
    /**
     * Notice shown when the Freemius SDK directory is not present.
     *
     * This state should never occur in a properly packaged release build.
     * It indicates an incomplete plugin installation.
     */
    public function notice_sdk_missing() {
        if ( !current_user_can( 'manage_options' ) ) {
            return;
        }
        ?>
        <div class="notice notice-error">
            <p>
                <strong><?php 
        esc_html_e( 'Stripe Subscription Manager — Component Missing', 'subscription-manager-for-stripe' );
        ?></strong><br>
                <?php 
        esc_html_e( 'A required plugin component could not be loaded. Please reinstall the plugin or contact support for assistance.', 'subscription-manager-for-stripe' );
        ?>
            </p>
        </div>
        <?php 
    }

    /**
     * Notice shown when the SDK is present but credentials are not embedded.
     *
     * This state should never occur in a properly packaged release build.
     * It indicates the plugin was distributed before the publisher embedded
     * their Freemius credentials.
     */
    public function notice_unconfigured() {
        if ( !current_user_can( 'manage_options' ) ) {
            return;
        }
        ?>
        <div class="notice notice-warning">
            <p>
                <strong><?php 
        esc_html_e( 'Stripe Subscription Manager — Setup Incomplete', 'subscription-manager-for-stripe' );
        ?></strong><br>
                <?php 
        esc_html_e( 'The plugin configuration is incomplete. Please reinstall the plugin or contact support to resolve this.', 'subscription-manager-for-stripe' );
        ?>
            </p>
        </div>
        <?php 
    }

    // ── Private helpers ───────────────────────────────────────────────────
    /**
     * Detect whether a public key string is still the shipped placeholder.
     */
    private static function is_placeholder_key( $key ) {
        return empty( $key ) || $key === 'pk_placeholder';
    }

}
