<?php
/**
 * License management class
 *
 * Delegates plan and license state to the Freemius SDK while keeping
 * the same public API so that no call sites need to change.
 *
 * @package SEOForge
 */

if (!defined('ABSPATH')) {
    exit;
}

class SEOFORGE_License {

    /** @var SEOFORGE_License|null */
    private static $instance = null;

    /**
 * Free-tier capability whitelist.
 *
 * `can` returns true for these no matter what — they're shipped in the
 * Free plugin and require no Pro add-on. Everything NOT in this list is a
 * Pro feature and is delegated to SEOFORGE_Pro_Bridge::is_enabled.
 *
 * See SPLIT_PLAN.md §5.3.
 */
    private const FREE_FEATURES = [
        'meta_title',
        'meta_description',
        'sitemap',
        'robots_txt',
        'basic_schema',
        // the visual Schema Builder (manual type + fields, incl. Recipe)
        // is a Free feature (Rank Math Free ships an equivalent). It renders in
        // the Free metabox , so its manual schema must SAVE + OUTPUT in
        // Free too — otherwise entered data is silently lost on post save. The
        // AI "Detect"/auto-fill button inside it keeps its own 'ai_features'
        // gate (paywall), so credit-spending stays Pro.
        'schema_builder',
        'og_overrides',
        'twitter_overrides',
        'noindex_toggle',
        'yoast_import',
        'rankmath_import',
        // 2026-05-29 Free/Pro rebalance — these ship in the Free plugin
        // because the equivalent is free in Rank Math / AIOSEO / SEOPress /
        // Slim SEO. They are pure-local features (no AI credits, no Worker
        // calls): Image SEO auto-alt, the Redirect + 404 manager, single
        // LocalBusiness schema, and VideoObject schema + video sitemap.
        // The AI sub-actions inside these screens (e.g. Local SEO AI
        // Auto-Fill) keep their own 'ai_features' gate, so credit-spending
        // paths stay Pro.
        'image_seo',
        'redirects',
        'local_seo',
        'video_seo',
    ];

    /**
 * Pro-feature registry kept here for backward compatibility with callers
 * that read the metadata (e.g. admin UIs listing Pro features for upsell
 * cards). Authoritative gating now happens via SEOFORGE_Pro_Bridge.
 */
    private $features = [
        'meta_generator'     => ['pro', 'trial'],
        'content_brief'      => ['pro', 'trial'],
        'site_audit'         => ['pro', 'trial'],
        'internal_links'     => ['pro', 'trial'],
        'auto_links'         => ['pro', 'trial'],
        'gsc'                => ['pro', 'trial'],
        'ab_testing'         => ['pro', 'trial'],
        'content_optimizer'  => ['pro', 'trial'],
        // SF-QA-001: umbrella feature flag for all AI endpoints in the metabox.
        // Checked both in the UI (hides buttons for Free users) and on the
        // server (REST endpoints return 403 if not pro/trial).
        'ai_features'        => ['pro', 'trial'],
    ];

    private $limits = [];

    private $feature_labels = [
        'meta_generator'    => 'AI Meta Generation',
        'content_brief'     => 'AI Content Brief',
        'site_audit'        => 'Site Audit',
        'internal_links'    => 'Internal Link Suggestions',
        'auto_links'        => 'Auto Internal Links',
        'gsc'               => 'Google Search Console',
        'ab_testing'        => 'A/B Testing',
        'content_optimizer' => 'Content Optimizer',
        'ai_features'       => 'AI Features (metabox)',
    ];

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

    private function __construct() {}

    
    // Freemius bridge
    

    private function effective_plan() {
        // Add-on architecture: without the Pro plugin physically loaded,
        // no plan upgrade is possible regardless of stored license_data
        // or the 24h grace window. Stale forge_license_last_pro_at from
        // a previous Pro activation must NOT surface when Pro is gone.
        // SEOFORGE_Pro_Loader is declared only when seoforge-pro is active.
        if (!class_exists('SEOFORGE_Pro_Loader')) {
            return 'free';
        }

        // E2E test bypass: allow forcing plan via WP option. Gated on
        // `FORGE_SKIP_LICENSE_REVALIDATION` constant — only our dev/test
        // wp-config.php defines it. On a customer site the constant is
        // absent, so the option cannot be used to escalate to PRO even
        // if set through SQLi or a compromised admin account.
        if (defined('FORGE_SKIP_LICENSE_REVALIDATION') && FORGE_SKIP_LICENSE_REVALIDATION) {
            $e2e_plan = get_option('forge_e2e_plan', '');
            if (in_array($e2e_plan, ['pro', 'trial'], true)) {
                return $e2e_plan;
            }
        }

        // / FC-63 v2: an explicit Disconnect (Forge_Connect::disconnect_product
        // → cache_license_inactive) rewrites seoforge_license_data to an INACTIVE
        // cache (key kept, 'inactive'=>true). Treat that as Free so Disconnect drops
        // to Free deterministically instead of trusting a stale Pro signal
        // (which stays true under WP_FS__DEV_MODE). Mirrors the resolver's
        // $is_inactive / 'saved_but_inactive'.
        $sf_data = get_option('seoforge_license_data', []);
        if (is_array($sf_data) && !empty($sf_data['inactive'])) {
            return 'free';
        }

        // Pro-plan confirmation is supplied by the Pro add-on, which hooks
        // 'forge_pro_plan' with its full licensing ladder (paying / trial /
        // active-valid license / sandbox-registered license / expired-or-
        // cancelled). The wp.org Free base ships no licensing SDK — this filter
        // is the single injection point. Returns: '' (add-on absent / could not
        // determine — fall through to the local trial + 24h grace window),
        // 'pro', 'trial', or 'free' (add-on reachable but explicitly NOT
        // entitled — clear stale grace).
        $pro_plan = apply_filters('forge_pro_plan', '', 'seoforge');
        if ($pro_plan === 'pro') {
            update_option('forge_license_last_pro_at', time(), false);
            return 'pro';
        }
        if ($pro_plan === 'trial') {
            return 'trial';
        }

        if ($this->has_worker_trial()) {
            return 'trial';
        }

        if ($pro_plan === 'free') {
            // Add-on reachable and explicitly reports no active paid/trial
            // access — stale grace must not keep Settings or runtime Pro on.
            if ((int) get_option('forge_license_last_pro_at', 0) > 0) {
                update_option('forge_license_last_pro_at', 0, false);
            }
            return 'free';
        }

        // Grace-period fallback only covers a missing/unavailable Pro add-on.
        // It must not override an explicit not-paying / expired response.
        $last_pro = (int) get_option('forge_license_last_pro_at', 0);
        if ($last_pro > 0 && (time() - $last_pro) < DAY_IN_SECONDS) {
            return 'pro';
        }

        return 'free';
    }

    private function has_worker_trial() {
        $data = get_option('seoforge_license_data', []);
        if (!is_array($data) || empty($data['trial_token'])) {
            return false;
        }

        $expires = isset($data['trial_expires_at'])
            ? (string) $data['trial_expires_at']
            : (isset($data['trial_expires']) ? (string) $data['trial_expires'] : '');
        if ($expires !== '') {
            $ts = strtotime($expires);
            if ($ts && $ts < time()) {
                return false;
            }
        }

        return true;
    }

    
    // Public API — Plan queries
    

    public static function is_pro() {
        return in_array(self::instance()->effective_plan(), ['pro', 'trial'], true);
    }

    public static function is_free() {
        return self::instance()->effective_plan() === 'free';
    }

    public static function get_plan() {
        return self::instance()->effective_plan();
    }

    public static function get_plan_name() {
        $names = [
            'free'  => __('Free', 'seoforge'),
            'pro'   => __('Pro', 'seoforge'),
            'trial' => __('Trial', 'seoforge'),
        ];
        $plan = self::instance()->effective_plan();
        return isset($names[$plan]) ? $names[$plan] : $names['free'];
    }

    
    // Public API — Feature & limit checks
    

    public static function can($feature) {
        // Free-tier capabilities are always allowed — they ship in the Free plugin.
        if (in_array($feature, self::FREE_FEATURES, true)) {
            return true;
        }

        // Pro-tier features delegate to the bridge. On a Free-only site the
        // bridge has no features registered → is_enabled returns false. On a
        // Free+Pro site the Pro loader registers + enables features based on
        // license state, so this returns true when the user has a valid
        // license/trial.
        if (class_exists('SEOFORGE_Pro_Bridge')) {
            return SEOFORGE_Pro_Bridge::instance()->is_enabled($feature);
        }

        // Fallback for the brief window before the bridge file loads (e.g.
        // direct file-include from a test that bypasses the bootstrap): use
        // the legacy plan-based check so callers don't break.
        $self = self::instance();
        if (!isset($self->features[$feature])) {
            return false;
        }
        return in_array($self->effective_plan(), $self->features[$feature], true);
    }

    public static function check_limit($resource, $current_count) {
        $limit = self::get_limit($resource);
        if ($limit === null) {
            return true;
        }
        return $current_count < $limit;
    }

    public static function get_limit($resource) {
        $self = self::instance();
        if (!isset($self->limits[$resource])) {
            return null;
        }
        $plan = $self->effective_plan();
        return isset($self->limits[$resource][$plan]) ? $self->limits[$resource][$plan] : null;
    }

    public static function get_features() {
        $self   = self::instance();
        $result = [];
        foreach ($self->features as $slug => $plans) {
            $result[] = [
                'slug'  => $slug,
                'label' => isset($self->feature_labels[$slug]) ? $self->feature_labels[$slug] : $slug,
                'plans' => $plans,
            ];
        }
        return $result;
    }

    
    // Public API — Activation / Deactivation (delegated to Freemius)
    

    public static function activate($key) {
        return true;
    }

    public static function deactivate() {
        return true;
    }

    public static function upgrade_url() {
        // Phase 3 of AVAKODE_AUTH_PURCHASE_PLAN — route every "Upgrade" CTA
        // to the centralized purchase flow on avakode.com instead of the
        // inline Freemius checkout dialog. The Freemius SDK stays in place
        // for license validation, updates, and SDK-visible license_key —
        // only the UI handoff moves.
        $base = defined('FORGE_AVAKODE_URL') ? rtrim(FORGE_AVAKODE_URL, '/') : 'https://avakode.com';
        return $base . '/dashboard/buy/?product=seoforge&plan=pro';
    }

    /**
 * render a locked AI entry point for Free users.
 *
 * The conversion strategy wants Free users to SEE the AI affordances
 * (generate meta, content analysis, schema auto-fill) but never trigger a
 * real generation. These buttons carry NO REST wiring and deliberately
 * lack the `.sf-ai-gen-*` / id hooks the admin.js generators bind to, so a
 * click can't reach api.avakode.com or spend OpenAI credits. A single
 * delegated `[data-sf-paywall]` handler opens the upsell modal instead.
 *
 * @param string $feature Paywall copy key (meta|readiness|schema|optimize|autolink).
 * @param string $label Visible button label.
 * @param string $variant Visual size: 'pill' (inline), 'block' (full width), 'action' (toolbar).
 */
    public static function ai_lock_html($feature, $label = 'AI', $variant = 'pill') {
        return sprintf(
            '<button type="button" class="sf-ai-locked sf-ai-locked--%s" data-sf-paywall="%s" title="%s">%s<span class="sf-ai-locked__pro"><span class="dashicons dashicons-lock" aria-hidden="true"></span>PRO</span></button>',
            esc_attr($variant),
            esc_attr($feature),
            esc_attr__('AI is a Pro feature — click to learn more', 'seoforge'),
            esc_html($label) . ' '
        );
    }

    
    // Admin notice
    

    public function render_upgrade_banner() { $this->render_upgrade_notice(); }

    public function render_upgrade_notice() {
        if (!self::is_free()) {
            return;
        }

        $upgrade_url = self::upgrade_url();

        printf(
            '<div class="forge-upgrade-notice" style="border-left:4px solid #FF6B35;background:#FFF7ED;border-radius:0 12px 12px 0;padding:14px 18px;margin:5px 0 15px;display:flex;align-items:center;justify-content:space-between;gap:12px;"><p style="margin:0;font-size:14px;color:#1e293b;flex:1;">%s <a href="%s" target="_blank" style="color:#FF6B35;font-weight:600;">%s</a></p><button type="button" onclick="this.parentElement.style.display=\x27none\x27" style="display:inline-flex;align-items:center;justify-content:center;background:#f8fafc;border:1px solid #e2e8f0;color:#94a3b8;cursor:pointer;padding:0;width:28px;height:28px;border-radius:8px;flex-shrink:0;transition:all 0.15s;" title="Dismiss" onmouseover="this.style.background=\x27#fef2f2\x27;this.style.borderColor=\x27#fca5a5\x27;this.style.color=\x27#ef4444\x27" onmouseout="this.style.background=\x27#f8fafc\x27;this.style.borderColor=\x27#e2e8f0\x27;this.style.color=\x27#94a3b8\x27"><span class="dashicons dashicons-no-alt" style="font-size:18px;width:18px;height:18px;"></span></button></div>',
            esc_html__("You're using SEO Forge Free. Upgrade to Pro for AI meta generation, Site Audit, Schema Builder, and more.", 'seoforge'),
            esc_url($upgrade_url),
            esc_html__('Upgrade Now', 'seoforge')
        );
    }
}
