<?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 LangForge
 */

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

class LF_License {

    /** @var LF_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 LF_Pro_Bridge::is_enabled.
 *
 * See SPLIT_PLAN.md §5.3.
 */
    private const FREE_FEATURES = [
        'url_routing',
        'hreflang',
        'language_switcher',
        'sitemap_hreflang',
        'basic_post_translation',
        'string_translation',
        'two_languages',
        'acf_compat_read',
        // 2026-05-29 Free/Pro rebalance. Owner decision: unlimited languages
        // in Free (WPML gives unlimited on every paid tier; Polylang Free is
        // unlimited). Glossary + WooCommerce are pure-local (no AI credits, no
        // Worker) and free in Weglot / TranslatePress respectively. The
        // AI-powered moat — AI translation, Visual Editor, Translation Memory
        // (fuzzy), Content Diff, QA, Analytics, Frontend Editor (which is
        // AI+TM driven) — stays Pro.
        'unlimited_languages',
        'glossary',
        'woocommerce',
    ];

    /**
 * 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 LF_Pro_Bridge.
 */
    private $features = [
        'ai_translation'      => ['pro', 'trial'],
        'visual_editor'       => ['pro', 'trial'],
        'bulk_actions'        => ['pro', 'trial'],
        'translation_memory'  => ['pro', 'trial'],
        'import_export'       => ['pro', 'trial'],
        'analytics'           => ['pro', 'trial'],
        'roles'               => ['pro', 'trial'],
        'frontend_editor'     => ['pro', 'trial'],
        'translation_qa'      => ['pro', 'trial'],
        'content_diff'        => ['pro', 'trial'],
        'live_preview'        => ['pro', 'trial'],
        'media_translation'   => ['pro', 'trial'],
        'comment_translation' => ['pro', 'trial'],
    ];

    /** @var array<string, array<string, int>> */
    // 2026-05-29: Free language count is now effectively unlimited (owner
    // decision — WPML/Polylang give unlimited). High cap keeps the existing
    // check_limit/over-limit UI math well-defined.
    private $limits = [
        'languages' => ['free' => 999, 'pro' => 999, 'trial' => 999],
    ];

    /** @var array<string, string> */
    private $feature_labels = [
        'ai_translation'      => 'AI Translation',
        'visual_editor'       => 'Visual Editor',
        'bulk_actions'        => 'Bulk Actions',
        'translation_memory'  => 'Translation Memory',
        'import_export'       => 'Import / Export',
        'analytics'           => 'Translation Analytics',
        'roles'               => 'Translator Roles',
        'frontend_editor'     => 'Frontend Editor',
        'translation_qa'      => 'Translation QA',
        'content_diff'        => 'Content Diff',
        'live_preview'        => 'Live Preview',
        'media_translation'   => 'Media Translation',
        'comment_translation' => 'Comment Translation',
    ];

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

    private function __construct() {
        add_action('wp_ajax_lf_dismiss_upgrade_notice', [$this, 'ajax_dismiss_upgrade_notice']);
    }

    
    // Freemius bridge
    

    /**
 * Map Freemius state to our internal plan slug.
 */
    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.
        // LF_Pro_Loader is declared only when langforge-pro is active.
        if (!class_exists('LF_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;
            }
        }

        // this product is Pro only if it has its OWN local license
        // record (lf_license_data). `forge_license_last_pro_at` (used by the
        // override + grace paths below) is GLOBAL across all Forge plugins, so
        // a still-Pro sibling (FormForge/SEOForge/FieldForge) re-arms it; and
        // the Pro add-on's plan probe can stay stale under WP_FS__DEV_MODE. Without
        // this guard a disconnected LangForge keeps reporting Pro ("Current
        // Plan" stuck on Pro) because the shared grace marker is still warm.
        // An explicit Disconnect (Forge_Connect::disconnect_product →
        // cache_license_inactive) NO LONGER deletes lf_license_data under FC-63
        // v2 — it keeps the key as an INACTIVE cache (['inactive'=>true]) for
        // instant Reconnect. So treat BOTH a missing row AND an inactive cache as
        // Free; otherwise a disconnected LangForge keeps reporting Pro off the
        // stale Pro/grace marker (mirrors the resolver's $is_inactive).
        // Matches the split rule: Pro needs an explicit, ACTIVE local activation
        // for THIS plugin, not a sibling's license.
        $lf_data = get_option('lf_license_data');
        if (empty($lf_data) || (is_array($lf_data) && !empty($lf_data['inactive']))) {
            return 'free';
        }

        if ($this->allows_local_license_expiration_override()) {
            $last_pro = (int) get_option('forge_license_last_pro_at', 0);
            if ($last_pro > 0) {
                return (time() - $last_pro) < DAY_IN_SECONDS ? 'pro' : 'free';
            }
        }

        // Pro-plan confirmation is supplied by the Pro add-on, which hooks
        // 'forge_pro_plan' with its own licensing backend. 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
        // explicitly reports no active paid/trial access — clear stale grace).
        $pro_plan = apply_filters('forge_pro_plan', '', 'langforge');
        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 NOT entitled — stale grace must
            // not keep 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: when the Pro add-on can't confirm right now but
        // we recorded 'pro' within the last 24h, stay on pro. Catches webhook lag
        // after renewals and transient backend 5xx. After 24h without
        // reconfirmation we drop to free so cancelled subscriptions take effect
        // within a day.
        $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 allows_local_license_expiration_override() {
        return (defined('FORGE_SKIP_LICENSE_REVALIDATION') && FORGE_SKIP_LICENSE_REVALIDATION)
            || (defined('WP_FS__DEV_MODE') && WP_FS__DEV_MODE);
    }

    private function has_worker_trial() {
        $data = get_option('lf_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', 'langforge'),
            'pro'   => __('Pro', 'langforge'),
            'trial' => __('Trial', 'langforge'),
        ];
        $plan = self::instance()->effective_plan();
        return isset($names[$plan]) ? $names[$plan] : $names['free'];
    }

    
    // Public API — Feature & limit checks
    

    public static function can($feature) {
        $self = self::instance();

        // 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('LF_Pro_Bridge')) {
            return LF_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.
        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)
    

    /**
 * Activate a license key via Freemius SDK.
 */
    public static function activate($key) {
        // Freemius handles activation through its own UI.
        // This method is kept for backward compatibility.
        return true;
    }

    public static function deactivate() {
        // Freemius handles deactivation through its own UI.
        return true;
    }

    /**
 * Upgrade URL — Freemius checkout.
 */
    public static function upgrade_url() {
        // Phase 3 of AVAKODE_AUTH_PURCHASE_PLAN — route "Upgrade" CTAs to
        // avakode.com/dashboard/buy instead of Freemius inline checkout.
        $base = defined('FORGE_AVAKODE_URL') ? rtrim(FORGE_AVAKODE_URL, '/') : 'https://avakode.com';
        return $base . '/dashboard/buy/?product=langforge&plan=pro';
    }

    
    // Admin notice
    

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

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

        if (get_user_meta(get_current_user_id(), 'lf_upgrade_notice_dismissed', true)) {
            return;
        }

        $upgrade_url = esc_url(self::upgrade_url());
        $nonce = wp_create_nonce('lf_dismiss_upgrade');

        printf(
            '<div class="forge-upgrade-notice" id="lf-upgrade-notice" style="border-left:4px solid #0EC49E;background:#ECFDF5;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:#0EC49E;font-weight:600;">%s</a></p><button type="button" class="lf-dismiss-upgrade" data-nonce="%s" 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>' .
            '<script>jQuery(function($){$(".lf-dismiss-upgrade").on("click",function(){var $n=$("#lf-upgrade-notice");$n.slideUp(200);$.post(ajaxurl,{action:"lf_dismiss_upgrade_notice",nonce:$(this).data("nonce")});});});</script>',
            esc_html__("You're using LangForge Free. Upgrade to Pro for AI translation, Visual Editor, and more.", 'langforge'),
            $upgrade_url, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped via esc_url at assignment
            esc_html__('Upgrade Now', 'langforge'),
            esc_attr($nonce)
        );
    }

    public function ajax_dismiss_upgrade_notice() {
        check_ajax_referer('lf_dismiss_upgrade', 'nonce');
        update_user_meta(get_current_user_id(), 'lf_upgrade_notice_dismissed', 1);
        wp_send_json_success();
    }
}
