<?php
/**
 * Main plugin class – singleton entry point.
 *
 * @package SkyEmagSync
 */

namespace SkyEmagSync;

defined( 'ABSPATH' ) || exit;

final class Plugin {

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

    private function __construct() {}

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

    public function init(): void {
        $this->maybe_upgrade_db();
        $this->register_services();
    }

    /**
     * Run table creation if DB version doesn't match plugin version.
     * Only creates new tables (dbDelta is safe for existing ones).
     * Does NOT re-run full activate() to avoid side effects.
     */
    private function maybe_upgrade_db(): void {
        if ( ! is_admin() ) {
            return;
        }
        $db_version = get_option( 'skyemag_db_version', '0' );
        if ( version_compare( $db_version, SKYEMAG_VERSION, '<' ) ) {
            Installer::upgrade_tables();
        }
    }

    private function register_services(): void {
        // Admin UI
        if ( is_admin() ) {
            ( new Admin\Menu() )->register();
            ( new Admin\SettingsPage() )->register();
            ( new Admin\ProductsPage() )->register();
            ( new Admin\HelpPage() )->register();
            ( new Admin\LogsPage() )->register();
            ( new Admin\Assets() )->register();
            ( new Admin\SetupWizard() )->register();

            // Admin pages that live in the Premium add-on. The Free build
            // on WordPress.org physically excludes these classes via the
            // Freemius @fs_premium_only directive, so class_exists() alone
            // is sufficient and authoritative — no license check needed,
            // the dependency either physically exists in the build or it
            // does not.
            if ( class_exists( Admin\CategoryMapPage::class ) )  { ( new Admin\CategoryMapPage() )->register(); }
            if ( class_exists( Admin\CharMapPage::class ) )      { ( new Admin\CharMapPage() )->register(); }
            if ( class_exists( Admin\FamilyMapPage::class ) )    { ( new Admin\FamilyMapPage() )->register(); }
            if ( class_exists( Admin\PublishPage::class ) )      { ( new Admin\PublishPage() )->register(); }
            if ( class_exists( Admin\OrdersPage::class ) )       { ( new Admin\OrdersPage() )->register(); }
            if ( class_exists( Admin\NotificationsPage::class ) ){ ( new Admin\NotificationsPage() )->register(); }
            if ( class_exists( Admin\OrderMetabox::class ) )     { ( new Admin\OrderMetabox() )->register(); }
            if ( class_exists( Admin\OrderListBadge::class ) )   { Admin\OrderListBadge::register(); }

            // Setup wizard auto-redirect. Runs at priority 20 so Freemius
            // opt-in (priority 10 on admin_init) gets first dibs and can
            // redirect the seller to its own connect screen on first
            // activation. After the seller completes opt-in, the next
            // admin request lands here and our transient is still live —
            // the activation hook sets it for 5 minutes (was 30 s) which
            // is plenty of headroom for the opt-in detour. Skipped /
            // completed sellers never reach the consume + redirect.
            add_action( 'admin_init', [ $this, 'maybe_setup_wizard_redirect' ], 20 );

            // Decrypt-failure notice — surfaces when stored credentials can no
            // longer be decrypted (typical cause: AUTH_KEY rotation). Options::
            // decrypt_failure() sets the transient and clears the corrupted
            // password row; this hook just renders the recovery banner.
            add_action( 'admin_notices', [ Options::class, 'render_decrypt_failure_notice' ] );
        }

        // Order callback REST routes — registered outside is_admin() so
        // eMAG's webhook can hit /wp-json/skyemag/v1/order-callback even
        // when no WP admin is active. class_exists() guards: the Orders/
        // namespace is part of the Premium add-on and is physically
        // stripped from the Free build by the Freemius @fs_premium_only
        // directive, so the class is either present (Premium build) or
        // not (Free build) — no license check is layered on top.
        if ( class_exists( Orders\OrderCallback::class ) ) {
            ( new Orders\OrderCallback() )->register();
        }
        if ( class_exists( Orders\StockManager::class ) ) {
            Orders\StockManager::register();
        }
        if ( class_exists( Orders\OrderMapper::class ) ) {

            // Invalidate the OrderMapper SKU index whenever a WC product
            // is saved or deleted. Cheap defence on worker-pool runtimes
            // (FrankenPHP / RoadRunner) where the static cache would
            // otherwise outlive the request that built it.
            $invalidate = [ Orders\OrderMapper::class, 'invalidate_sku_index' ];
            add_action( 'woocommerce_new_product',                $invalidate );
            add_action( 'woocommerce_update_product',             $invalidate );
            add_action( 'woocommerce_new_product_variation',      $invalidate );
            add_action( 'woocommerce_update_product_variation',   $invalidate );
            add_action( 'woocommerce_trash_product',              $invalidate );
            add_action( 'woocommerce_delete_product',             $invalidate );

            // Email the seller when a new eMAG order lands in WooCommerce.
            // Throttle suffix is the eMAG order id so each order gets its
            // own email — the 1h global throttle would otherwise collapse
            // bursts into a single notification.
            add_action( 'skyemag_order_imported', function ( $order, $emag_order ) {
                if ( ! ( $order instanceof \WC_Order ) ) {
                    return;
                }
                $items = [];
                foreach ( $order->get_items() as $item ) {
                    $items[] = [
                        'qty'  => (int) $item->get_quantity(),
                        'name' => (string) $item->get_name(),
                    ];
                }
                Notifications\Dispatcher::notify( Notifications\Events::NEW_ORDER_IMPORTED, [
                    'emag_order_id' => (int) ( $emag_order['id'] ?? 0 ),
                    'wc_order_id'   => (int) $order->get_id(),
                    'total'         => (float) $order->get_total(),
                    'currency'      => (string) $order->get_currency(),
                    'customer_name' => trim( $order->get_formatted_billing_full_name() ),
                    'items'         => $items,
                ], (string) ( $emag_order['id'] ?? '' ) );
            }, 20, 2 );

            // Cancellation pipeline: when OrderImporter flips an existing
            // order's status to cancelled / refunded in response to an
            // updated eMAG payload, add an internal WC order note (admin-
            // only) and dispatch the order_cancelled notification email.
            // Idempotency guard via _skyemag_cancel_noted post meta to
            // protect against duplicate eMAG callback retries.
            add_action( 'skyemag_order_status_updated', function ( $order, $payload, $old_status, $new_status ) {
                if ( ! ( $order instanceof \WC_Order ) ) {
                    return;
                }
                // AwbManager owns the completed-after-AWB transition: it adds its
                // own note ("AWB issued") and dispatches the awb_issued
                // notification, so we skip here to avoid duplicates. Other
                // transitions (processing, on-hold, etc.) are silent — only
                // cancelled / refunded get the cancellation note + email below.
                if ( ! in_array( $new_status, [ 'cancelled', 'refunded' ], true ) ) {
                    return;
                }

                $already_noted = '1' === (string) $order->get_meta( '_skyemag_cancel_noted' );
                if ( ! $already_noted ) {
                    $note = sprintf(
                        /* translators: %1$s: UTC timestamp, %2$s: previous WC status, %3$s: new WC status */
                        __( '[eMAG] Order cancelled via callback at %1$s UTC. Status: %2$s -> %3$s. Open the order in marketplace.emag.ro for the cancellation reason and to confirm on the eMAG side.', 'skyservers-product-sync-for-emag' ),
                        current_time( 'Y-m-d H:i:s', true ),
                        $old_status,
                        $new_status
                    );
                    $order->add_order_note( $note, 0 ); // admin-only, not customer-visible
                    $order->update_meta_data( '_skyemag_cancel_noted', '1' );
                    $order->save();
                }

                Notifications\Dispatcher::notify( Notifications\Events::ORDER_CANCELLED, [
                    'emag_order_id' => (int) ( $payload['id'] ?? 0 ),
                    'wc_order_id'   => (int) $order->get_id(),
                    'total'         => (float) $order->get_total(),
                    'currency'      => (string) $order->get_currency(),
                    'customer_name' => trim( $order->get_formatted_billing_full_name() ),
                    'old_status'    => $old_status,
                ], (string) ( $payload['id'] ?? '' ) );
            }, 20, 4 );
        }

        // Cron scheduler (free — price/stock sync)
        ( new Sync\Scheduler() )->register();

        // Keep eMAG identity meta off duplicated products (free)
        ( new Products\DuplicateGuard() )->register();

        // WooCommerce hooks for real-time price/stock sync triggers (free)
        add_action( 'woocommerce_loaded', function () {
            Sync\Runner::register_hooks();
        } );

        // Single-product async sync (free)
        add_action( 'skyemag_sync_single', [ Sync\Runner::class, 'handle_single_sync' ] );

        // 3.8.0 — Action Scheduler hook. Each batch enqueued by Dispatcher
        // fires this action with [chunk_ids, sync_id] as the args. The
        // handler is in BatchProcessor and is responsible for one chunk
        // of up to BATCH_SIZE (25) products. Action Scheduler ships with
        // WooCommerce, which the plugin already requires, so the hook
        // wiring is safe to register unconditionally.
        add_action( 'skyemag_sync_batch', [ Sync\BatchProcessor::class, 'handle' ], 10, 2 );

        // Margin change history recorder — bound to the action fired by PostMeta::set_margin.
        add_action( 'skyemag_margin_changed', [ History\MarginHistory::class, 'record' ], 10, 3 );

        // Clean up history when a product is permanently deleted.
        add_action( 'before_delete_post', function ( $post_id ) {
            if ( 'product' === get_post_type( $post_id ) || 'product_variation' === get_post_type( $post_id ) ) {
                History\MarginHistory::clear_for_product( (int) $post_id );
            }
        } );

        // Daily log prune — uses Options::log_retention_days() (7 free / 7-365 pro).
        add_action( 'skyemag_log_prune', function () {
            Sync\Logger::prune( Options::log_retention_days() );
        } );
        // Self-heal: ensure the daily cron exists for installs that activated
        // before this hook existed.
        if ( ! wp_next_scheduled( 'skyemag_log_prune' ) ) {
            wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'skyemag_log_prune' );
        }

        // Buy Button tracker cron — registered only on builds that
        // physically ship the tracker class (the Free WordPress.org build
        // strips it via the Freemius @fs_premium_only directive).
        if ( class_exists( Sync\BuyButtonTracker::class ) ) {
            Sync\BuyButtonTracker::register();
            Sync\BuyButtonTracker::schedule();
        }

        // AWB cache daily refresh — same dependency-presence rule as
        // the Buy Button tracker above.
        if ( class_exists( Admin\AwbCache::class ) ) {
            add_action( Admin\AwbCache::CRON_HOOK, [ Admin\AwbCache::class, 'cron_refresh_all' ] );
            Admin\AwbCache::ensure_scheduled();
        }
    }

    /**
     * Check whether the current install should activate Premium-gated
     * code paths. Two conditions must be satisfied:
     *
     *   1. The Freemius SDK identifies this code as the Premium BUILD
     *      (the `is_premium` flag passed to fs_dynamic_init). The Free
     *      build distributed via WordPress.org has this flag flipped to
     *      false by the Freemius deployment processor, so $fs->is_premium()
     *      returns false there even on sites that hold a paid license.
     *      Without this build check, an existing Pro customer who installs
     *      the Free ZIP from WordPress.org would have the runtime try to
     *      reference Premium-only classes that the Free build physically
     *      does not contain — instant fatal on plugins_loaded.
     *
     *   2. The site holds an active paid license or is in trial. This
     *      keeps Pro features off for sites running the Premium build
     *      without a valid license (e.g. license expired, refunded).
     */
    public static function is_premium(): bool {
        if ( ! function_exists( 'epsbss_fs' ) ) {
            return false;
        }
        $fs = epsbss_fs();
        return $fs->is_premium() && ( $fs->is_paying() || $fs->is_trial() );
    }

    /**
     * Consume the activation transient and redirect to the setup wizard.
     * No-op if the transient is missing, the user lacks the capability,
     * the wizard is already completed, or this is an AJAX/cron request.
     */
    public function maybe_setup_wizard_redirect(): void {
        if ( wp_doing_ajax() || wp_doing_cron() ) {
            return;
        }
        if ( ! current_user_can( Admin\Menu::CAPABILITY ) ) {
            return;
        }
        if ( ! get_transient( Options::SETUP_REDIRECT_TRANSIENT ) ) {
            return;
        }
        // One-shot — clear before redirect to prevent loops.
        delete_transient( Options::SETUP_REDIRECT_TRANSIENT );

        if ( Options::setup_completed() ) {
            return;
        }

        // Don't redirect if we're already on the wizard page (defensive).
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page-slug check during admin redirect, no state mutation.
        $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
        if ( 'skyservers-sync-setup' === $page ) {
            return;
        }

        wp_safe_redirect( admin_url( 'admin.php?page=skyservers-sync-setup' ) );
        exit;
    }
}
