<?php

namespace SoftminalB2B\Storefront;

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

use SoftminalB2B\Pricing\Context;
use SoftminalB2B\Services\Settings;

/**
 * Hide prices and add-to-cart from guests (FEATURES B1).
 *
 * Hiding the rendered price is the easy half. The half that incumbents get
 * wrong is the leaks: schema.org product markup and the Store API both publish
 * the price as data, so a hidden price is still one "view source" or one
 * /wp-json/wc/store/v1/products request away. Both are closed here, and the
 * verification suite asserts it.
 */
class PriceVisibility
{
    private static bool $registered = false;

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

        self::$registered = true;

        add_filter('woocommerce_get_price_html', [self::class, 'filterPriceHtml'], 99, 2);
        add_filter('woocommerce_variable_price_html', [self::class, 'filterPriceHtml'], 99, 2);

        // Purchasability drives the add-to-cart button everywhere Woo renders
        // one, including Blocks, so this single filter covers the lot.
        add_filter('woocommerce_is_purchasable', [self::class, 'filterPurchasable'], 99, 2);

        // Leak 1: schema.org markup embeds the price in the page as JSON-LD.
        add_filter('woocommerce_structured_data_product', [self::class, 'filterStructuredData'], 99, 2);

        // Leak 2: the WooCommerce REST product endpoints.
        add_filter('rest_prepare_product_object', [self::class, 'filterRestProduct'], 99, 3);

        // Leak 3: the Store API. It is a separate controller that never runs
        // rest_prepare_product_object, and it is public — so it is the leak
        // that actually matters. Filter the dispatched response instead.
        add_filter('rest_post_dispatch', [self::class, 'filterStoreApiResponse'], 99, 3);
    }

    /**
     * Whether prices are hidden for whoever is browsing right now.
     *
     * Guests only: a logged-in customer, B2B or retail, always sees a price.
     * The point of B1 is to make people register, not to hide the shop from
     * paying customers.
     */
    public static function isHidden(): bool
    {
        if (Settings::get('hide_prices_for_guests') !== 'yes') {
            return false;
        }

        // Guest preview must show what a real guest sees — hidden prices too.
        $hidden = !is_user_logged_in() || ViewAsCustomer::isGuestPreview();

        /**
         * Lets Pro extend hiding to catalogue-mode and per-group visibility
         * (FEATURES B2/B3) without touching this class.
         *
         * @param bool $hidden
         */
        return (bool) apply_filters('smb2b_hide_prices', $hidden);
    }

    private static function hideAddToCart(): bool
    {
        return self::isHidden() && Settings::get('hide_add_to_cart_for_guests') === 'yes';
    }

    /**
     * The replacement for a hidden price. A bare "login to view" is a dead end,
     * so the default links to the account page.
     *
     * @param string $priceHtml
     * @return string
     */
    public static function filterPriceHtml($priceHtml, $product = null)
    {
        if (!self::isHidden()) {
            return $priceHtml;
        }

        return self::message();
    }

    public static function message(): string
    {
        $custom = (string) Settings::get('hide_prices_message');

        /**
         * Filter the replacement shown where a hidden price would be.
         *
         * Return non-empty HTML to take over entirely (it is kses-filtered).
         *
         * @param string $custom The merchant-configured message ('' for default).
         */
        $custom = (string) apply_filters('smb2b_hidden_price_message', $custom);

        if (trim($custom) !== '') {
            return '<span class="smb2b-price-hidden">' . wp_kses_post($custom) . '</span>';
        }

        $accountUrl = function_exists('wc_get_page_permalink') ? wc_get_page_permalink('myaccount') : '';

        $text = __('Log in to see prices', 'softminal-b2b-wholesale-for-woocommerce');

        if (!$accountUrl) {
            return '<span class="smb2b-price-hidden">' . esc_html($text) . '</span>';
        }

        return sprintf(
            '<a class="smb2b-price-hidden" href="%s">%s</a>',
            esc_url($accountUrl),
            esc_html($text)
        );
    }

    /**
     * @param bool $purchasable
     * @return bool
     */
    public static function filterPurchasable($purchasable, $product)
    {
        return self::hideAddToCart() ? false : $purchasable;
    }

    /**
     * Strip price and availability from schema.org output. Search engines then
     * see a product with no offer, which is correct: there is no public price.
     *
     * @param array<string, mixed> $markup
     * @return array<string, mixed>
     */
    public static function filterStructuredData($markup, $product = null)
    {
        if (!self::isHidden() || !is_array($markup)) {
            return $markup;
        }

        unset($markup['offers']);

        return $markup;
    }

    /**
     * Blank prices in Store API product responses.
     *
     * The Store API is public and unauthenticated, so this is the endpoint an
     * incumbent typically leaves leaking after hiding the rendered price.
     *
     * @param \WP_REST_Response $result
     * @param \WP_REST_Server   $server
     * @param \WP_REST_Request  $request
     * @return \WP_REST_Response
     */
    public static function filterStoreApiResponse($result, $server, $request)
    {
        if (!self::isHidden() || !$result instanceof \WP_REST_Response) {
            return $result;
        }

        if (!$request instanceof \WP_REST_Request || strpos((string) $request->get_route(), '/wc/store/') !== 0) {
            return $result;
        }

        $result->set_data(self::blankPrices($result->get_data()));

        return $result;
    }

    /**
     * Walk a Store API payload and empty every price field, leaving the shape
     * intact so clients still parse.
     *
     * The Store API nests its price data in stdClass objects rather than
     * arrays, so both have to be handled — an array-only walk silently misses
     * `prices` entirely and the endpoint keeps leaking.
     *
     * @param mixed $data
     * @return mixed
     */
    private static function blankPrices($data)
    {
        $priceFields = ['price', 'regular_price', 'sale_price', 'price_range', 'raw_prices'];

        if (is_object($data) && !$data instanceof \JsonSerializable) {
            foreach (get_object_vars($data) as $key => $value) {
                $data->{$key} = self::blankValue($key, $value, $priceFields);
            }

            return $data;
        }

        if (is_array($data)) {
            foreach ($data as $key => $value) {
                $data[$key] = self::blankValue($key, $value, $priceFields);
            }
        }

        return $data;
    }

    /**
     * @param array-key $key
     * @param mixed     $value
     * @param string[]  $priceFields
     * @return mixed
     */
    private static function blankValue($key, $value, array $priceFields)
    {
        if ($key === 'price_html') {
            return self::message();
        }

        if ($key === 'prices' && (is_array($value) || is_object($value))) {
            foreach ($priceFields as $field) {
                if (is_object($value) && property_exists($value, $field)) {
                    $value->{$field} = is_scalar($value->{$field}) ? '' : $value->{$field};
                } elseif (is_array($value) && array_key_exists($field, $value)) {
                    $value[$field] = is_scalar($value[$field]) ? '' : $value[$field];
                }
            }

            return $value;
        }

        if (is_array($value) || is_object($value)) {
            return self::blankPrices($value);
        }

        return $value;
    }

    /**
     * The WooCommerce REST and Store API responses both flow through
     * rest_prepare_product_object for the public product endpoints. Blank the
     * price fields rather than removing them, so clients expecting the shape
     * still parse.
     *
     * @param \WP_REST_Response $response
     * @return \WP_REST_Response
     */
    public static function filterRestProduct($response, $product, $request)
    {
        if (!self::isHidden() || !$response instanceof \WP_REST_Response) {
            return $response;
        }

        $data = $response->get_data();

        if (!is_array($data)) {
            return $response;
        }

        foreach (['price', 'regular_price', 'sale_price', 'price_html'] as $field) {
            if (array_key_exists($field, $data)) {
                $data[$field] = '';
            }
        }

        if (isset($data['prices']) && is_array($data['prices'])) {
            foreach (['price', 'regular_price', 'sale_price'] as $field) {
                if (array_key_exists($field, $data['prices'])) {
                    $data['prices'][$field] = '';
                }
            }
        }

        $response->set_data($data);

        return $response;
    }
}
