<?php

namespace SoftminalB2B\Http\Controllers;

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

use SoftminalB2B\Cart\BogoRewards;
use SoftminalB2B\Compat\Multilingual;
use SoftminalB2B\Models\BogoRule;
use SoftminalB2B\Models\Group;
use SoftminalB2B\Support\Tier;

/**
 * Admin CRUD for buy-X-get-Y promotions.
 *
 * Free creates product-trigger rules whose reward is the same product; category
 * triggers are gated behind Pro here the same way price and quantity rules gate
 * their category scope, so the tier line is consistent across the plugin.
 */
class BogoController extends Controller
{
    public function index(\WP_REST_Request $request, $params = [])
    {
        $page    = max(1, (int) $request->get_param('page'));
        $perPage = min(100, max(1, (int) ($request->get_param('per_page') ?: 20)));

        // Server-side pagination: only the current page is fetched, not the whole
        // bogo_rules table sliced on the client.
        $result    = BogoRule::query()->orderBy('id', 'DESC')->paginate($perPage, $page);
        $pageRules = $result['data'];

        // Batch the labels for the page: group names in one whereIn, and the
        // product/term caches primed in one query each, so the several objectLabel()
        // calls per rule (buy target + reward product, in both present() and
        // summary()) are all cache hits instead of a query apiece.
        $groups = $this->groupNamesFor($pageRules);
        $this->primeObjectCaches($pageRules);

        $rules = [];
        foreach ($pageRules as $rule) {
            $rules[] = $this->present($rule, $groups);
        }

        return $this->success([
            'rules'         => $rules,
            'total'         => (int) $result['meta']['total'],
            'page'          => (int) $result['meta']['current_page'],
            'per_page'      => (int) $result['meta']['per_page'],
            'buy_types'     => $this->buyTypeOptions(),
            'reward_types'  => $this->rewardTypeOptions(),
            'reward_scopes' => $this->rewardScopeOptions(),
            'tier'          => Tier::toArray(),
        ]);
    }

    /**
     * Group id => name for the groups referenced by a page of rules, in one query.
     *
     * @param iterable<object> $rules
     * @return array<int, string>
     */
    private function groupNamesFor($rules): array
    {
        $ids = [];
        foreach ($rules as $rule) {
            if ($rule->group_id) {
                $ids[(int) $rule->group_id] = true;
            }
        }

        if (empty($ids)) {
            return [];
        }

        $names = [];
        foreach (Group::query()->whereIn('id', array_keys($ids))->get() as $group) {
            $names[(int) $group->id] = (string) $group->name;
        }

        return $names;
    }

    /**
     * Prime the product post-cache and term cache for a page of rules in one
     * query each: buy targets (product or category) and reward products.
     *
     * @param iterable<object> $rules
     */
    private function primeObjectCaches($rules): void
    {
        $productIds = [];
        $termIds    = [];

        foreach ($rules as $rule) {
            if ($rule->buy_type === BogoRule::BUY_CATEGORY) {
                if ($rule->buy_id) {
                    $termIds[] = (int) $rule->buy_id;
                }
            } elseif ($rule->buy_id) {
                $productIds[] = (int) $rule->buy_id;
            }

            if ($rule->reward_product_id) {
                $productIds[] = (int) $rule->reward_product_id;
            }
        }

        if (!empty($productIds) && function_exists('_prime_post_caches')) {
            _prime_post_caches(array_values(array_unique($productIds)), false, true);
        }

        if (!empty($termIds)) {
            get_terms([
                'taxonomy'   => 'product_cat',
                'include'    => array_values(array_unique($termIds)),
                'hide_empty' => false,
            ]);
        }
    }

    public function store(\WP_REST_Request $request, $params = [])
    {
        $validated = $this->validateRule($request);

        if (is_string($validated)) {
            return $this->error($validated, 422);
        }

        $rule = BogoRule::create($validated);
        $this->flush();

        return $this->success(
            ['rule' => $this->present($rule)],
            __('Deal saved.', 'softminal-b2b-wholesale-for-woocommerce')
        );
    }

    public function update(\WP_REST_Request $request, $params = [])
    {
        $id   = (int) ($params['id'] ?? 0);
        $rule = BogoRule::find($id);

        if (!$rule) {
            return $this->notFound(__('That deal no longer exists.', 'softminal-b2b-wholesale-for-woocommerce'));
        }

        $validated = $this->validateRule($request);

        if (is_string($validated)) {
            return $this->error($validated, 422);
        }

        BogoRule::update($id, $validated);
        $this->flush();

        return $this->success(
            ['rule' => $this->present(BogoRule::find($id))],
            __('Deal saved.', 'softminal-b2b-wholesale-for-woocommerce')
        );
    }

    public function destroy(\WP_REST_Request $request, $params = [])
    {
        $id   = (int) ($params['id'] ?? 0);
        $rule = BogoRule::find($id);

        if (!$rule) {
            return $this->notFound(__('That deal no longer exists.', 'softminal-b2b-wholesale-for-woocommerce'));
        }

        $rule->delete();
        $this->flush();

        return $this->success(null, __('Deal deleted.', 'softminal-b2b-wholesale-for-woocommerce'));
    }

    /**
     * @return array<string, mixed>|string
     */
    private function validateRule(\WP_REST_Request $request)
    {
        $buyType = sanitize_key((string) $request->get_param('buy_type')) ?: BogoRule::BUY_PRODUCT;

        if (!in_array($buyType, BogoRule::buyTypes(), true)) {
            return __('Choose what triggers this deal.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        if (!Tier::isPro() && !in_array($buyType, BogoRule::freeBuyTypes(), true)) {
            return __('Category deals are part of Pro. The free version triggers a deal from a single product.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        $buyId = absint($request->get_param('buy_id'));

        if ($buyType === BogoRule::BUY_PRODUCT) {
            $product = $buyId ? wc_get_product($buyId) : null;

            if (!$product) {
                return __('Choose which product triggers this deal.', 'softminal-b2b-wholesale-for-woocommerce');
            }

            // The reward is the same product added as its own line, so it must
            // be a single, directly-addable product. Variable products (whose
            // gift variation is ambiguous) are a Pro capability.
            if ($product->is_type('variable')) {
                return __('Variable products need Pro for buy-X-get-Y. The free version works with simple products.', 'softminal-b2b-wholesale-for-woocommerce');
            }
        } else {
            $term = get_term($buyId, 'product_cat');

            if ($buyId <= 0 || !$term || is_wp_error($term)) {
                return __('Choose which category triggers this deal.', 'softminal-b2b-wholesale-for-woocommerce');
            }
        }

        $buyQty    = absint($request->get_param('buy_qty'));
        $rewardQty = absint($request->get_param('reward_qty'));

        if ($buyQty < 1) {
            return __('The buy quantity must be at least 1.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        if ($rewardQty < 1) {
            return __('The reward quantity must be at least 1.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        $rewardType = sanitize_key((string) $request->get_param('reward_type')) ?: BogoRule::REWARD_FREE;

        if (!in_array($rewardType, BogoRule::rewardTypes(), true)) {
            return __('Choose how the reward is discounted.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        $rewardValue = (float) $request->get_param('reward_value');

        if ($rewardType === BogoRule::REWARD_FREE) {
            $rewardValue = 0.0;
        } elseif ($rewardType === BogoRule::REWARD_PERCENT) {
            if ($rewardValue <= 0 || $rewardValue > 100) {
                return __('Enter a percentage between 1 and 100.', 'softminal-b2b-wholesale-for-woocommerce');
            }
        } elseif ($rewardType === BogoRule::REWARD_AMOUNT) {
            if ($rewardValue <= 0) {
                return __('Enter an amount greater than zero.', 'softminal-b2b-wholesale-for-woocommerce');
            }
        }

        $rewardScope = sanitize_key((string) $request->get_param('reward_scope')) ?: BogoRule::REWARD_SCOPE_SAME;

        if (!in_array($rewardScope, BogoRule::rewardScopes(), true)) {
            return __('Choose what the reward is.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        if (!Tier::isPro() && !in_array($rewardScope, BogoRule::freeRewardScopes(), true)) {
            return __('Gifting a different product, or the cheapest item, is part of Pro. The free version rewards more of the same product.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        // A category trigger cannot reward "more of the same" — there is no one
        // product to give — so it must nominate a gift product or the cheapest.
        if ($buyType === BogoRule::BUY_CATEGORY && $rewardScope === BogoRule::REWARD_SCOPE_SAME) {
            return __('A category deal needs a specific gift product or the cheapest item as its reward.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        $rewardProductId = 0;

        if ($rewardScope === BogoRule::REWARD_SCOPE_PRODUCT) {
            $rewardProductId = absint($request->get_param('reward_product_id'));
            $gift            = $rewardProductId ? wc_get_product($rewardProductId) : null;

            if (!$gift) {
                return __('Choose which product to give as the reward.', 'softminal-b2b-wholesale-for-woocommerce');
            }

            if ($gift->is_type('variable')) {
                return __('The gift must be a simple product.', 'softminal-b2b-wholesale-for-woocommerce');
            }
        }

        // Store default-language ids so the deal applies across translations.
        if ($buyType === BogoRule::BUY_PRODUCT) {
            $buyId = Multilingual::productId($buyId);
        } elseif ($buyType === BogoRule::BUY_CATEGORY) {
            $buyId = Multilingual::termId($buyId, 'product_cat');
        }

        if ($rewardProductId) {
            $rewardProductId = Multilingual::productId($rewardProductId);
        }

        $groupId = $request->get_param('group_id');
        $groupId = ($groupId === null || $groupId === '' || (int) $groupId === 0) ? null : (int) $groupId;

        if ($groupId !== null && !Group::find($groupId)) {
            return __('That customer group no longer exists.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        return [
            'title'         => sanitize_text_field((string) $request->get_param('title')),
            'buy_type'      => $buyType,
            'buy_id'        => $buyId,
            'buy_qty'       => $buyQty,
            'reward_type'       => $rewardType,
            'reward_qty'        => $rewardQty,
            'reward_value'      => $rewardValue,
            'reward_scope'      => $rewardScope,
            'reward_product_id' => $rewardProductId,
            'group_id'          => $groupId,
            'repeat_reward' => $request->get_param('repeat_reward') === null ? 1 : (int) (bool) $request->get_param('repeat_reward'),
            'active'        => $request->get_param('active') === null ? 1 : (int) (bool) $request->get_param('active'),
            'priority'      => (int) $request->get_param('priority'),
        ];
    }

    private function flush(): void
    {
        do_action('smb2b_pricing_data_changed');
        BogoRewards::flush();
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function buyTypeOptions(): array
    {
        $labels = [
            BogoRule::BUY_PRODUCT  => __('A product', 'softminal-b2b-wholesale-for-woocommerce'),
            BogoRule::BUY_CATEGORY => __('A category', 'softminal-b2b-wholesale-for-woocommerce'),
        ];

        $free    = BogoRule::freeBuyTypes();
        $options = [];

        foreach ($labels as $value => $label) {
            $options[] = [
                'value' => $value,
                'label' => $label,
                'pro'   => !in_array($value, $free, true),
            ];
        }

        return $options;
    }

    /**
     * @return array<int, array<string, string>>
     */
    private function rewardTypeOptions(): array
    {
        return [
            ['value' => BogoRule::REWARD_FREE, 'label' => __('Free', 'softminal-b2b-wholesale-for-woocommerce')],
            ['value' => BogoRule::REWARD_PERCENT, 'label' => __('Percentage off', 'softminal-b2b-wholesale-for-woocommerce')],
            ['value' => BogoRule::REWARD_AMOUNT, 'label' => __('Amount off', 'softminal-b2b-wholesale-for-woocommerce')],
        ];
    }

    /**
     * What the reward is: more of the same product (Free), a specific gift
     * product, or the cheapest qualifying item (both Pro).
     *
     * @return array<int, array<string, mixed>>
     */
    private function rewardScopeOptions(): array
    {
        $labels = [
            BogoRule::REWARD_SCOPE_SAME     => __('More of the same product', 'softminal-b2b-wholesale-for-woocommerce'),
            BogoRule::REWARD_SCOPE_PRODUCT  => __('A specific gift product', 'softminal-b2b-wholesale-for-woocommerce'),
            BogoRule::REWARD_SCOPE_CHEAPEST => __('The cheapest qualifying item', 'softminal-b2b-wholesale-for-woocommerce'),
        ];

        $free    = BogoRule::freeRewardScopes();
        $options = [];

        foreach ($labels as $value => $label) {
            $options[] = [
                'value' => $value,
                'label' => $label,
                'pro'   => !in_array($value, $free, true),
            ];
        }

        return $options;
    }

    /**
     * @param array<int, string>|null $groups id => name map for a page (batched);
     *                                         null for single-rule callers.
     * @return array<string, mixed>
     */
    private function present($rule, ?array $groups = null): array
    {
        $buyType = (string) $rule->buy_type;
        $buyId   = (int) $rule->buy_id;
        $gid     = $rule->group_id === null ? null : (int) $rule->group_id;

        $groupName    = __('All customers', 'softminal-b2b-wholesale-for-woocommerce');
        $groupMissing = false;

        if ($gid !== null) {
            $resolved = null;

            if ($groups !== null) {
                $resolved = $groups[$gid] ?? null;
            } else {
                $group    = Group::find($gid);
                $resolved = $group ? (string) $group->name : null;
            }

            if ($resolved !== null && $resolved !== '') {
                $groupName = (string) $resolved;
            } else {
                // The rule points at a group that no longer exists, so it now
                // matches no live buyer. Say so rather than mislabelling it "All
                // customers", which reads as the opposite of what it does.
                $groupMissing = true;
                $groupName    = __('Deleted group', 'softminal-b2b-wholesale-for-woocommerce');
            }
        }

        return [
            'id'            => (int) $rule->id,
            'title'         => (string) $rule->title,
            'buy_type'      => $buyType,
            'buy_id'        => $buyId,
            'buy_label'     => $this->objectLabel($buyType, $buyId),
            'buy_qty'       => (int) $rule->buy_qty,
            'reward_type'       => (string) $rule->reward_type,
            'reward_qty'        => (int) $rule->reward_qty,
            'reward_value'      => (float) $rule->reward_value,
            'reward_scope'      => (string) $rule->reward_scope,
            'reward_product_id' => (int) $rule->reward_product_id,
            'reward_product_label' => (int) $rule->reward_product_id
                ? $this->objectLabel(BogoRule::BUY_PRODUCT, (int) $rule->reward_product_id)
                : '',
            'group_id'      => $gid,
            'group_name'    => $groupName,
            'group_missing' => $groupMissing,
            'repeat_reward' => (bool) $rule->repeat_reward,
            'active'        => (bool) $rule->active,
            'priority'      => (int) $rule->priority,
            'summary'       => $this->summary($rule),
        ];
    }

    private function objectLabel(string $buyType, int $buyId): string
    {
        if ($buyType === BogoRule::BUY_CATEGORY) {
            $term = $buyId ? get_term($buyId, 'product_cat') : null;

            return ($term && !is_wp_error($term))
                ? $term->name
                : __('(deleted category)', 'softminal-b2b-wholesale-for-woocommerce');
        }

        $product = $buyId ? wc_get_product($buyId) : null;

        return $product ? $product->get_name() : __('(deleted product)', 'softminal-b2b-wholesale-for-woocommerce');
    }

    /**
     * A plain-language one-liner for the rules table.
     */
    private function summary($rule): string
    {
        $reward = (string) $rule->reward_type;

        if ($reward === BogoRule::REWARD_PERCENT) {
            $rewardText = sprintf(
                /* translators: 1: reward quantity, 2: percentage */
                __('%1$d at %2$s%% off', 'softminal-b2b-wholesale-for-woocommerce'),
                (int) $rule->reward_qty,
                rtrim(rtrim((string) (float) $rule->reward_value, '0'), '.')
            );
        } elseif ($reward === BogoRule::REWARD_AMOUNT) {
            $rewardText = sprintf(
                /* translators: 1: reward quantity, 2: money amount off */
                __('%1$d at %2$s off', 'softminal-b2b-wholesale-for-woocommerce'),
                (int) $rule->reward_qty,
                wp_strip_all_tags(wc_price((float) $rule->reward_value))
            );
        } else {
            $rewardText = sprintf(
                /* translators: %d: reward quantity */
                _n('%d free', '%d free', (int) $rule->reward_qty, 'softminal-b2b-wholesale-for-woocommerce'),
                (int) $rule->reward_qty
            );
        }

        // Name the reward target when it is not simply more of the trigger.
        $scope = (string) $rule->reward_scope;

        if ($scope === BogoRule::REWARD_SCOPE_PRODUCT) {
            $rewardText = sprintf(
                /* translators: 1: reward text (e.g. "2 free"), 2: gift product name */
                __('%1$s of %2$s', 'softminal-b2b-wholesale-for-woocommerce'),
                $rewardText,
                $this->objectLabel(BogoRule::BUY_PRODUCT, (int) $rule->reward_product_id)
            );
        } elseif ($scope === BogoRule::REWARD_SCOPE_CHEAPEST) {
            $rewardText = sprintf(
                /* translators: %s: reward text (e.g. "1 free") */
                __('the cheapest item %s', 'softminal-b2b-wholesale-for-woocommerce'),
                $rewardText
            );
        }

        return sprintf(
            /* translators: 1: buy quantity, 2: product/category name, 3: reward text */
            __('Buy %1$d of %2$s, get %3$s', 'softminal-b2b-wholesale-for-woocommerce'),
            (int) $rule->buy_qty,
            $this->objectLabel((string) $rule->buy_type, (int) $rule->buy_id),
            $rewardText
        );
    }
}
