<?php

namespace SoftminalB2B\Storefront;

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

use SoftminalB2B\Models\Group;
use SoftminalB2B\Pricing\Context;

/**
 * "View as" — the visual preview engine (FEATURES A7, upgraded in 1.2.0).
 *
 * The merchant browses the real storefront seeing exactly what a buyer sees,
 * in one of three modes:
 *
 *   user   — one specific customer's prices, catalogue and methods
 *   group  — an approved buyer in a given group (the merchant's own user with
 *            the group forced, so approval-dependent surfaces behave)
 *   guest  — a logged-out visitor (hidden prices, private catalogue gone)
 *
 * Deliberately read-only and short-lived. It swaps the PRICING CONTEXT only —
 * it never logs anyone in, never touches the session or cart, and refuses to
 * run for anyone without `manage_woocommerce`. The preview persists across
 * page loads via a signed, expiring cookie so the merchant can actually
 * browse; the capability is re-checked on every request, so a copied cookie
 * is worthless to anyone else.
 */
class ViewAsCustomer
{
    public const QUERY_VAR = 'smb2b_view_as';
    private const NONCE_ACTION = 'smb2b_view_as';
    private const COOKIE = 'smb2b_view_as';
    private const TTL = 30 * MINUTE_IN_SECONDS;

    /** @var array{mode:string, id:int}|null */
    private static ?array $preview = null;
    private static bool $registered = false;

    public static function init(): void
    {
        if (self::$registered) {
            return;
        }

        self::$registered = true;

        // Restore a persisted preview EARLY — WooCommerce's wc-ajax endpoints
        // and form handlers run before template_redirect:1 (wc-ajax at
        // priority 0, add-to-cart handlers at wp_loaded:20). Restoring at
        // wp_loaded:1 keeps every price computation in the request — page
        // render AND ajax refreshes — inside the same preview.
        add_action('wp_loaded', [self::class, 'restoreFromCookie'], 1);
        add_action('template_redirect', [self::class, 'handleRequest'], 1);
        add_filter('smb2b_pricing_context_user', [self::class, 'filterContextUser']);
        add_filter('smb2b_pricing_context_group', [self::class, 'filterContextGroup'], 10, 2);
        add_action('admin_bar_menu', [self::class, 'adminBarMenu'], 80);
        add_action('wp_footer', [self::class, 'renderBanner']);

        // The preview is read-only by contract: block a checkout submitted
        // while previewing, so a test walk-through can never place an order
        // at prices that belong to somebody else.
        add_action('woocommerce_checkout_process', [self::class, 'blockCheckout']);
    }

    /**
     * Rehydrate the preview from the signed cookie on frontend requests only.
     * wp_loaded also fires for wp-admin, admin-ajax and REST — all excluded,
     * so the preview can never bleed into admin screens or API responses.
     */
    public static function restoreFromCookie(): void
    {
        if (
            self::$preview !== null
            || is_admin()
            || wp_doing_ajax()
            || (function_exists('wp_is_json_request') && wp_is_json_request())
            || self::isRestRequest()
            || !current_user_can(self::capability())
        ) {
            return;
        }

        $stored = self::fromCookie();

        if ($stored !== null) {
            self::$preview = $stored;
            Context::flush();
        }
    }

    private static function isRestRequest(): bool
    {
        if (defined('REST_REQUEST') && REST_REQUEST) {
            return true;
        }

        $uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';

        return $uri !== '' && function_exists('rest_get_url_prefix')
            && strpos($uri, '/' . rest_get_url_prefix() . '/') !== false;
    }

    public static function blockCheckout(): void
    {
        if (self::isPreviewing() && function_exists('wc_add_notice')) {
            wc_add_notice(
                __('You are previewing the shop as someone else — stop the preview before placing an order.', 'softminal-b2b-wholesale-for-woocommerce'),
                'error'
            );
        }
    }

    public static function capability(): string
    {
        return 'manage_woocommerce';
    }

    // ------------------------------------------------------------ start URLs

    /**
     * A signed, capability-checked preview URL for one customer. Returns null
     * when previewing makes no sense (no buyer) or the caller may not preview.
     */
    public static function previewUrl(int $userId): ?string
    {
        if ($userId <= 0 || !current_user_can(self::capability())) {
            return null;
        }

        return self::startUrl((string) $userId);
    }

    public static function groupPreviewUrl(int $groupId): ?string
    {
        if ($groupId <= 0 || !current_user_can(self::capability())) {
            return null;
        }

        return self::startUrl('group-' . $groupId);
    }

    public static function guestPreviewUrl(): ?string
    {
        return current_user_can(self::capability()) ? self::startUrl('guest') : null;
    }

    public static function stopUrl(): string
    {
        return add_query_arg(self::QUERY_VAR, 'stop', self::currentUrl());
    }

    private static function startUrl(string $value): string
    {
        return add_query_arg(
            [
                self::QUERY_VAR => $value,
                '_wpnonce'      => wp_create_nonce(self::NONCE_ACTION . ':' . $value),
            ],
            self::currentUrl()
        );
    }

    /**
     * The page being viewed when the switch happens, so starting or changing
     * a preview reloads in place rather than bouncing to the front page.
     * Admin screens and REST calls fall back home — links minted while the
     * SPA talks to the API (Customers, Price Explainer) must open the
     * storefront, not /wp-json/….
     */
    private static function currentUrl(): string
    {
        if (
            is_admin()
            || (defined('REST_REQUEST') && REST_REQUEST)
            || wp_doing_ajax()
            || empty($_SERVER['REQUEST_URI'])
            || empty($_SERVER['HTTP_HOST'])
        ) {
            return home_url('/');
        }

        // REQUEST_URI already contains the site path on subdirectory installs
        // — home_url($uri) would double it — so rebuild from the host and
        // verify it is actually this site before trusting it.
        $uri  = sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI']));
        $host = sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST']));
        $url  = set_url_scheme('http://' . $host . $uri);

        if (wp_parse_url($url, PHP_URL_HOST) !== wp_parse_url(home_url(), PHP_URL_HOST)) {
            return home_url('/');
        }

        return remove_query_arg([self::QUERY_VAR, '_wpnonce'], $url);
    }

    // -------------------------------------------------------------- lifecycle

    /**
     * Start, change or stop a preview from the query arg, then redirect clean
     * so a refresh is stable. On plain page views, restore from the cookie.
     */
    public static function handleRequest(): void
    {
        if (!current_user_can(self::capability())) {
            return;
        }

        // Capability-gated above; the start path re-verifies the signed preview
        // cookie and stop is idempotent, so no nonce is needed for this read.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        if (isset($_GET[self::QUERY_VAR])) {
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            $value = sanitize_text_field(wp_unslash($_GET[self::QUERY_VAR]));

            if ($value === 'stop') {
                self::clearCookie();
                wp_safe_redirect(remove_query_arg([self::QUERY_VAR, '_wpnonce']));
                exit;
            }

            self::maybeStartPreview();

            if (self::$preview !== null) {
                self::setCookie($value);
                wp_safe_redirect(remove_query_arg([self::QUERY_VAR, '_wpnonce']));
                exit;
            }

            return;
        }

        $stored = self::fromCookie();

        if ($stored !== null) {
            self::$preview = $stored;
            Context::flush();
        }
    }

    /**
     * Validate the query arg and, if it holds a signed preview request from a
     * capable user, activate the preview for this request. No cookie, no
     * redirect — handleRequest() adds those on real page views; the verify
     * suite calls this directly.
     */
    public static function maybeStartPreview(): void
    {
        if (!isset($_GET[self::QUERY_VAR]) || !current_user_can(self::capability())) {
            return;
        }

        $value = sanitize_text_field(wp_unslash($_GET[self::QUERY_VAR]));
        $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash($_GET['_wpnonce'])) : '';

        if (!wp_verify_nonce($nonce, self::NONCE_ACTION . ':' . $value)) {
            return;
        }

        $parsed = self::parse($value);

        if ($parsed === null) {
            return;
        }

        self::$preview = $parsed;
        Context::flush();
    }

    /**
     * @return array{mode:string, id:int}|null
     */
    private static function parse(string $value): ?array
    {
        if ($value === 'guest') {
            return ['mode' => 'guest', 'id' => 0];
        }

        if (str_starts_with($value, 'group-')) {
            $groupId = absint(substr($value, 6));

            return $groupId > 0 && Group::find($groupId) ? ['mode' => 'group', 'id' => $groupId] : null;
        }

        $userId = absint($value);

        return $userId > 0 && get_userdata($userId) ? ['mode' => 'user', 'id' => $userId] : null;
    }

    private static function setCookie(string $value): void
    {
        $expires = time() + self::TTL;
        $payload = $value . '|' . $expires . '|' . get_current_user_id();
        $cookie  = $payload . '|' . hash_hmac('sha256', $payload, wp_salt('auth'));

        setcookie(self::COOKIE, $cookie, $expires, defined('COOKIEPATH') ? COOKIEPATH : '/', defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : '', is_ssl(), true);
        $_COOKIE[self::COOKIE] = $cookie;
    }

    private static function clearCookie(): void
    {
        setcookie(self::COOKIE, '', time() - HOUR_IN_SECONDS, defined('COOKIEPATH') ? COOKIEPATH : '/', defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : '');
        unset($_COOKIE[self::COOKIE]);
    }

    /**
     * @return array{mode:string, id:int}|null
     */
    private static function fromCookie(): ?array
    {
        if (empty($_COOKIE[self::COOKIE])) {
            return null;
        }

        $parts = explode('|', sanitize_text_field(wp_unslash($_COOKIE[self::COOKIE])));

        if (count($parts) !== 4) {
            return null;
        }

        [$value, $expires, $userId, $sig] = $parts;
        $payload = $value . '|' . $expires . '|' . $userId;

        if (
            !hash_equals(hash_hmac('sha256', $payload, wp_salt('auth')), $sig)
            || (int) $expires < time()
            || (int) $userId !== get_current_user_id()
        ) {
            return null;
        }

        return self::parse($value);
    }

    // ---------------------------------------------------------------- queries

    public static function isPreviewing(): bool
    {
        return self::$preview !== null;
    }

    public static function previewUserId(): ?int
    {
        return self::$preview !== null && self::$preview['mode'] === 'user' ? self::$preview['id'] : null;
    }

    public static function previewGroupId(): ?int
    {
        return self::$preview !== null && self::$preview['mode'] === 'group' ? self::$preview['id'] : null;
    }

    public static function isGuestPreview(): bool
    {
        return self::$preview !== null && self::$preview['mode'] === 'guest';
    }

    /**
     * The user id downstream code should treat as "the viewer" while a
     * preview is active, or $fallback when it isn't. Group mode keeps the
     * merchant's own id (with the group forced through the context filter),
     * so approval-dependent checks behave like an approved member.
     */
    public static function effectiveViewerId(int $fallback): int
    {
        if (self::$preview === null) {
            return $fallback;
        }

        switch (self::$preview['mode']) {
            case 'user':
                return self::$preview['id'];
            case 'guest':
                return 0;
            default:
                return $fallback;
        }
    }

    // ---------------------------------------------------------------- filters

    /**
     * @param int $userId
     * @return int
     */
    public static function filterContextUser($userId)
    {
        if (self::$preview === null) {
            return $userId;
        }

        switch (self::$preview['mode']) {
            case 'user':
                return self::$preview['id'];
            case 'guest':
                return 0;
            default:
                return $userId;
        }
    }

    /**
     * Force the previewed group — but ONLY for the merchant's own context.
     * Other users' contexts resolved during the same request (emails, hooks)
     * must keep their real groups, and Context memoizes per user id.
     *
     * @param int|null $groupId
     * @param int      $userId
     * @return int|null
     */
    public static function filterContextGroup($groupId, $userId)
    {
        if ((int) $userId !== get_current_user_id()) {
            return $groupId;
        }

        return self::previewGroupId() ?? $groupId;
    }

    // --------------------------------------------------------------- chrome

    /**
     * The admin-bar switcher: Guest, every group, and a way out. Per-customer
     * preview starts from the Customers screen or the Price Explainer, where
     * picking a person actually makes sense.
     */
    public static function adminBarMenu(\WP_Admin_Bar $bar): void
    {
        if (is_admin() || !current_user_can(self::capability())) {
            return;
        }

        $bar->add_node([
            'id'    => 'smb2b-view-as',
            'title' => self::isPreviewing()
                ? sprintf(
                    /* translators: %s: who the merchant is previewing as */
                    __('Viewing as: %s', 'softminal-b2b-wholesale-for-woocommerce'),
                    self::label()
                )
                : __('View shop as…', 'softminal-b2b-wholesale-for-woocommerce'),
        ]);

        if (self::isPreviewing()) {
            $bar->add_node([
                'id'     => 'smb2b-view-as-stop',
                'parent' => 'smb2b-view-as',
                'title'  => __('Stop previewing', 'softminal-b2b-wholesale-for-woocommerce'),
                'href'   => self::stopUrl(),
            ]);
        }

        $bar->add_node([
            'id'     => 'smb2b-view-as-guest',
            'parent' => 'smb2b-view-as',
            'title'  => __('A guest (logged out)', 'softminal-b2b-wholesale-for-woocommerce'),
            'href'   => self::guestPreviewUrl(),
        ]);

        foreach (Group::query()->orderBy('name')->get() as $group) {
            $bar->add_node([
                'id'     => 'smb2b-view-as-group-' . (int) $group->id,
                'parent' => 'smb2b-view-as',
                'title'  => sprintf(
                    /* translators: %s: customer group name */
                    __('Group: %s', 'softminal-b2b-wholesale-for-woocommerce'),
                    (string) $group->name
                ),
                'href'   => self::groupPreviewUrl((int) $group->id),
            ]);
        }
    }

    private static function label(): string
    {
        if (self::$preview === null) {
            return '';
        }

        switch (self::$preview['mode']) {
            case 'guest':
                return __('a guest', 'softminal-b2b-wholesale-for-woocommerce');
            case 'group':
                $group = Group::find(self::$preview['id']);

                return $group ? (string) $group->name : __('a group', 'softminal-b2b-wholesale-for-woocommerce');
            default:
                $user = get_userdata(self::$preview['id']);

                return $user ? $user->display_name : __('a customer', 'softminal-b2b-wholesale-for-woocommerce');
        }
    }

    /**
     * A preview that does not announce itself is a support ticket waiting to
     * happen ("why are my prices wrong?"), so the banner is not dismissible.
     */
    public static function renderBanner(): void
    {
        if (!self::isPreviewing()) {
            return;
        }

        printf(
            '<div style="position:fixed;left:0;right:0;bottom:0;z-index:99999;background:#1e1e1e;color:#fff;padding:12px 16px;font:14px/1.4 -apple-system,BlinkMacSystemFont,\'Segoe UI\',sans-serif;display:flex;gap:16px;align-items:center;justify-content:center;flex-wrap:wrap">
                <span>%s</span><a href="%s" style="color:#fff;text-decoration:underline">%s</a>
            </div>',
            esc_html(
                sprintf(
                    /* translators: %s: who the merchant is previewing as */
                    __('Previewing the shop as %s. Everything below is what they see.', 'softminal-b2b-wholesale-for-woocommerce'),
                    self::label()
                )
            ),
            esc_url(self::stopUrl()),
            esc_html__('Stop previewing', 'softminal-b2b-wholesale-for-woocommerce')
        );
    }
}
