<?php
/**
 * Plugin Name: Form Forge
 * Plugin URI: https://formforgewp.com
 * Description: Drag-and-drop form builder with unlimited forms, 22 core field types, conditional logic, generic webhooks, file uploads, anti-spam, and submissions with CSV export.
 * Version: 1.0.21
 * Author: Avakode
 * Author URI: https://avakode.com
 * Text Domain: formforge
 * Domain Path: /languages
 * Requires at least: 6.5
 * Requires PHP: 7.4
 * License: GPLv2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 */

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

define('FORMFORGE_VERSION', '1.0.21');
// Schema version — bumped only when wp_formforge_* tables change. Compared
// against the `formforge_db_version` option on every request via
// FORMFORGE_Schema::maybe_upgrade; divergence triggers idempotent dbDelta.
// Bumping VERSION without DB_VERSION is fine (no-op for the schema check).
define('FORMFORGE_DB_VERSION', '1.0.2');
define('FORMFORGE_DIR', plugin_dir_path(__FILE__));
define('FORMFORGE_URL', plugin_dir_url(__FILE__));
define('FORMFORGE_BASENAME', plugin_basename(__FILE__));
// Shared ed25519 public key for verifying signed auto-update packages (#8/M6).
// Same value across all Forge plugins → guard against double-define when several
// are active. Verification stays inert until a release manifest carries a signature.
if (!defined('FORGE_UPDATE_PUBKEY')) define('FORGE_UPDATE_PUBKEY', '54EEDBTTEytITaNTDKTcP+ugpv9JFBOLAJRJMeLGaQo=');

/**
 * Safe replacement for maybe_unserialize: mirrors its semantics
 * (non-serialized values pass through) but never instantiates objects,
 * blocking PHP object-injection via unserialize gadget chains. Shared
 * across all Forge plugins behind a function_exists guard.
 */
if (!function_exists('forge_safe_unserialize')) {
    function forge_safe_unserialize($value) {
        if (!is_string($value)) {
            return $value;
        }
        return is_serialized($value)
            ? unserialize($value, ['allowed_classes' => false])
            : $value;
    }
}


// Pro Bridge — the Free side of the Free/Pro extension contract.
// Loaded before any other class so License + everything downstream can
// consult it. See SPLIT_PLAN.md §5.3.

require_once FORMFORGE_DIR . 'includes/class-pro-bridge.php';
FORMFORGE_Pro_Bridge::instance();

// FC-63: shared license resolver — single source of truth for licensing UI +
// gating. First Forge plugin to load wins via the class_exists wrap.
if (!class_exists('Forge_License_Resolver')) {
    require_once FORMFORGE_DIR . 'includes/class-forge-license-resolver.php';
}

// Forge Queue — shared job queue (first plugin to load wins)
if (!class_exists('Forge_Queue')) {
    require_once __DIR__ . '/includes/class-forge-queue.php';
}

// Shared transactional email renderer (first Forge plugin to load wins).
if (!class_exists('Forge_Email_Template')) {
    require_once __DIR__ . '/includes/class-forge-email.php';
}

// Shared SSRF-safe wp_remote_* wrapper (first Forge plugin to load wins).
if (!class_exists('Forge_Net')) {
    require_once __DIR__ . '/includes/class-forge-net.php';
}

spl_autoload_register(function ($class) {
    if (strpos($class, 'FORMFORGE_') !== 0) return;
    $name = strtolower(str_replace('_', '-', str_replace('FORMFORGE_', '', $class)));
    foreach (['includes/', 'admin/'] as $dir) {
        $f = FORMFORGE_DIR . $dir . 'class-' . $name . '.php';
        if (file_exists($f)) { require_once $f; return; }
    }
});

function formforge_init() {
    load_plugin_textdomain('formforge', false, dirname(FORMFORGE_BASENAME) . '/languages'); // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound -- needed for bundled .mo on the Avakode bridge/Pro channel
    // Shared Forge UI (class-forge-*.php) uses the 'forge' textdomain — alias it
    // to this plugin's own compiled catalog so the shared dashboard/credits/
    // connect/feedback/updater strings translate (they're extracted into this
    // plugin's .po/.mo too). Without this, 'forge' loads nowhere → English-only.
    $ff_forge_mo = FORMFORGE_DIR . 'languages/formforge-' . determine_locale() . '.mo';
    if (is_readable($ff_forge_mo)) { load_textdomain('forge', $ff_forge_mo); }

    // Schema migrations — must run BEFORE any class that touches the FF
    // tables. dbDelta is idempotent so this is essentially free on the
    // hot path (one option compare). Catches the auto-update case where
    // register_activation_hook never fires.
    FORMFORGE_Schema::maybe_upgrade();

    // Initialize license (must be first — other classes check license state)
    FORMFORGE_License::instance();

    FORMFORGE_Core::instance();
    FORMFORGE_Field_Types::instance();
    FORMFORGE_Form_Builder::instance();
    FORMFORGE_Form_Renderer::instance();
    FORMFORGE_Form_Handler::instance();
    FORMFORGE_Webhooks::instance();
    FORMFORGE_Notifications::instance();
    FORMFORGE_Anti_Spam::instance();

    // Forge Queue
    Forge_Queue::instance();
    FORMFORGE_Rest_Api::instance();
    FORMFORGE_Templates::instance();
    if (is_admin()) {
        $formforge_dashboard_file = FORMFORGE_DIR . 'includes/class-forge-dashboard.php';
        if (is_readable($formforge_dashboard_file)) {
            require_once $formforge_dashboard_file;
            if (class_exists('Forge_Suite_Dashboard')) {
                Forge_Suite_Dashboard::instance();
            }
        }
        require_once FORMFORGE_DIR . 'includes/class-forge-docs-link.php';
        $formforge_feedback_file = FORMFORGE_DIR . 'includes/class-forge-feedback-widget.php';
        if (is_readable($formforge_feedback_file)) {
            require_once $formforge_feedback_file;
        }
        FORMFORGE_Admin::instance();
        FORMFORGE_License_Admin::instance();
        require_once FORMFORGE_DIR . 'includes/class-migration.php';
        FORMFORGE_Migration::instance();
    }
}
add_action('plugins_loaded', 'formforge_init', 5);

// Shortcode
add_shortcode('formforge', function ($atts) {
    $atts = shortcode_atts(['id' => 0], $atts);
    return FORMFORGE_Form_Renderer::instance()->render((int) $atts['id']);
});

register_activation_hook(__FILE__, function () {
    require_once FORMFORGE_DIR . 'includes/class-core.php';
    FORMFORGE_Core::activate();

    // Seed the schema version so maybe_upgrade short-circuits on the
    // very first request after activation. Without this the next page load
    // would call dbDelta a second time — harmless but wasted IO.
    update_option('formforge_db_version', FORMFORGE_DB_VERSION, false);

    // Telemetry (Pro-only). On Free-only sites the class isn't loaded.
    if (class_exists('Forge_Telemetry')) {
        $prev = (string) get_option('formforge_previous_version', '');
        Forge_Telemetry::track('plugin.activated', [
            'first_install'    => $prev === '',
            'previous_version' => $prev,
        ]);
        update_option('formforge_previous_version', FORMFORGE_VERSION, false);
        if (!get_option('formforge_installed_at')) {
            update_option('formforge_installed_at', time(), false);
        }
    }
});
register_deactivation_hook(__FILE__, function () {
    // B9: clear FormForge-owned single-event retry crons. After deactivation
    // the queued retries try to load FORMFORGE_Integrations whose class file
    // no longer auto-loads, throwing a fatal on every cron tick.
    wp_clear_scheduled_hook('formforge_retry_sheets_append');
    wp_clear_scheduled_hook('formforge_retry_calendar_event');

    if (class_exists('Forge_Telemetry')) {
        $installed_at = (int) get_option('formforge_installed_at', time());
        Forge_Telemetry::track('plugin.deactivated', [
            'uptime_days' => max(0, (int) floor((time() - $installed_at) / DAY_IN_SECONDS)),
        ]);
        Forge_Telemetry::flush();
    }
});
