<?php

namespace SoftminalB2B\Http\Controllers;

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

use SoftminalB2B\Cart\QuantityRules;
use SoftminalB2B\Compat\Multilingual;
use SoftminalB2B\Http\Controllers\Concerns\VendorOwnedRules;
use SoftminalB2B\Models\Group;
use SoftminalB2B\Models\QuantityRule;
use SoftminalB2B\Support\Tier;

class QuantityRulesController extends Controller
{
    use VendorOwnedRules;

    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: the whole table is no longer pulled into memory
        // and sliced on the client — only the current page is fetched and hydrated.
        // A self-service vendor sees only their own rules; the admin sees all.
        $result    = $this->scopeToVendor(QuantityRule::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 present()'s per-row
        // lookups are all cache hits instead of a query per rule.
        $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'],
            'scopes'   => $this->scopeOptions(),
            '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, so the per-row wc_get_product()/get_term() in objectLabel()
     * become cache hits.
     *
     * @param iterable<object> $rules
     */
    private function primeObjectCaches($rules): void
    {
        $productIds = [];
        $termIds    = [];

        foreach ($rules as $rule) {
            if (!$rule->object_id) {
                continue;
            }
            if ($rule->scope === QuantityRule::SCOPE_PRODUCT) {
                $productIds[] = (int) $rule->object_id;
            } elseif ($rule->scope === QuantityRule::SCOPE_CATEGORY) {
                $termIds[] = (int) $rule->object_id;
            }
        }

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

        if (!empty($termIds)) {
            // A single query that populates the term cache for these ids.
            get_terms([
                'taxonomy'   => 'product_cat',
                'include'    => array_values(array_unique($termIds)),
                'hide_empty' => false,
            ]);
        }
    }

    /**
     * The scope choices the dialog offers, each flagged whether it needs Pro so
     * the UI can lock it (with a crown) for free users.
     *
     * @return array<int, array<string, mixed>>
     */
    private function scopeOptions(): array
    {
        $labels = [
            QuantityRule::SCOPE_PRODUCT  => __('One product', 'softminal-b2b-wholesale-for-woocommerce'),
            QuantityRule::SCOPE_CATEGORY => __('A category', 'softminal-b2b-wholesale-for-woocommerce'),
        ];

        $free    = QuantityRule::freeScopes();
        $options = [];

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

        /**
         * The scope choices offered by the quantity-rule dialog. The Dokan bridge
         * appends a `vendor` scope (with an embedded `choices` vendor list) so the
         * option shows only when the bridge is active. See the price-rule twin.
         *
         * @param array<int, array<string, mixed>> $options
         * @param string                           $ruleType 'price' or 'quantity'
         */
        return apply_filters('smb2b_rule_scope_options', $options, 'quantity');
    }

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

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

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

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

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

        // A vendor may only touch their own rules; hide others as "not found".
        if (!$rule || !$this->mayManageRule($rule)) {
            return $this->notFound(__('That quantity rule no longer exists.', 'softminal-b2b-wholesale-for-woocommerce'));
        }

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

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

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

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

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

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

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

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

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

        if (!in_array($scope, QuantityRule::scopes(), true)) {
            return __('Choose what this rule applies to.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        if (!Tier::isPro() && !in_array($scope, QuantityRule::freeScopes(), true)) {
            if ($scope === QuantityRule::SCOPE_CATEGORY) {
                return __('Category quantity rules are part of Pro. The free version sets quantity rules per product.', 'softminal-b2b-wholesale-for-woocommerce');
            }

            return __('Order-wide minimums are part of Pro. The free version sets quantity rules per product.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        $objectId = absint($request->get_param('object_id'));

        if ($scope === QuantityRule::SCOPE_PRODUCT) {
            if ($objectId <= 0 || !wc_get_product($objectId)) {
                return __('Choose which product this rule applies to.', 'softminal-b2b-wholesale-for-woocommerce');
            }
        } elseif ($scope === QuantityRule::SCOPE_CATEGORY) {
            $term = get_term($objectId, 'product_cat');

            if ($objectId <= 0 || !$term || is_wp_error($term)) {
                return __('Choose which category this rule applies to.', 'softminal-b2b-wholesale-for-woocommerce');
            }
        } elseif ($scope === QuantityRule::SCOPE_VENDOR) {
            // The Dokan bridge's vendor scope: object_id is the vendor user id.
            // Core only requires it to be present — the bridge owns vendor
            // semantics through the scope-options and object-label seams.
            if ($objectId <= 0) {
                return __('Choose which vendor this rule applies to.', 'softminal-b2b-wholesale-for-woocommerce');
            }
        } else {
            $objectId = 0;
        }

        // Store the default-language id so the rule applies across translations.
        if ($scope === QuantityRule::SCOPE_PRODUCT) {
            $objectId = Multilingual::productId($objectId);
        } elseif ($scope === QuantityRule::SCOPE_CATEGORY) {
            $objectId = Multilingual::termId($objectId, 'product_cat');
        }

        $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');
        }

        $min  = $this->optionalInt($request->get_param('min_qty'));
        $max  = $this->optionalInt($request->get_param('max_qty'));
        $step = $this->optionalInt($request->get_param('step_qty'));

        if ($min === null && $max === null && $step === null) {
            return __('Set at least one of minimum, maximum or pack size.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        if ($min !== null && $min < 1) {
            return __('The minimum must be at least 1.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        if ($max !== null && $max < 1) {
            return __('The maximum must be at least 1.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        if ($min !== null && $max !== null && $max < $min) {
            return __('The maximum must be higher than the minimum.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        if ($step !== null && $step < 1) {
            return __('The pack size must be at least 1.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        // A pack size that can never be met is worse than no rule at all: the
        // product simply becomes unbuyable with no explanation.
        if ($step !== null && $step > 1 && $min !== null && $max !== null && ($min + $step) > $max) {
            return __('That pack size leaves no valid quantity between the minimum and maximum.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        // A self-service vendor may only constrain their own catalogue. No-op for
        // the admin.
        $vendorError = $this->vendorScopeError($scope, (int) $objectId);

        if ($vendorError !== null) {
            return $vendorError;
        }

        return [
            'scope'           => $scope,
            'object_id'       => $objectId,
            'group_id'        => $groupId,
            'min_qty'         => $min,
            'max_qty'         => $max,
            'step_qty'        => $step,
            'min_order_value' => null,
            'active'          => $request->get_param('active') === null ? 1 : (int) (bool) $request->get_param('active'),
        ];
    }

    private function optionalInt($value): ?int
    {
        if ($value === null || $value === '' || $value === false) {
            return null;
        }

        return (int) $value;
    }

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

    /**
     * @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
    {
        $scope    = (string) $rule->scope;
        $objectId = (int) $rule->object_id;
        $gid      = $rule->group_id === null ? null : (int) $rule->group_id;

        $groupName = __('All customers', 'softminal-b2b-wholesale-for-woocommerce');
        if ($gid !== null) {
            if ($groups !== null) {
                // A group_id that isn't in the map = deleted group → "All customers".
                $groupName = $groups[$gid] ?? $groupName;
            } else {
                $group     = Group::find($gid);
                $groupName = $group ? (string) $group->name : $groupName;
            }
        }

        return [
            'id'           => (int) $rule->id,
            'scope'        => $scope,
            'object_id'    => $objectId,
            'object_label' => $this->objectLabel($scope, $objectId),
            'group_id'     => $gid,
            'group_name'   => $groupName,
            'min_qty'      => $rule->min_qty === null ? null : (int) $rule->min_qty,
            'max_qty'      => $rule->max_qty === null ? null : (int) $rule->max_qty,
            'step_qty'     => $rule->step_qty === null ? null : (int) $rule->step_qty,
            'active'       => (bool) $rule->active,
        ];
    }

    /**
     * Human label for what a rule targets, by scope.
     */
    private function objectLabel(string $scope, int $objectId): string
    {
        if ($scope === QuantityRule::SCOPE_CATEGORY) {
            $term = $objectId ? get_term($objectId, 'product_cat') : null;

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

        if ($scope === QuantityRule::SCOPE_PRODUCT) {
            $product = $objectId ? wc_get_product($objectId) : null;

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

        if ($scope === QuantityRule::SCOPE_GLOBAL) {
            return __('Whole order', 'softminal-b2b-wholesale-for-woocommerce');
        }

        // Scopes core does not own (the Dokan bridge's vendor scope) label
        // themselves via the shared object-label seam.
        return (string) apply_filters('smb2b_rule_object_label', '', $scope, $objectId);
    }
}
