<?php

namespace SoftminalB2B\Http\Controllers;

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

use SoftminalB2B\Compat\Multilingual;
use SoftminalB2B\Http\Controllers\Concerns\VendorOwnedRules;
use SoftminalB2B\Models\Group;
use SoftminalB2B\Models\PriceRule;
use SoftminalB2B\Support\Tier;

class PriceRulesController 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)));

        // A self-service vendor sees only their own rules; the admin sees all.
        $query = $this->scopeToVendor(PriceRule::query());

        $groupId = $request->get_param('group_id');

        if ($groupId !== null && $groupId !== '') {
            $query = $query->andWhere('group_id', '=', (int) $groupId);
        }

        $scope = sanitize_key((string) $request->get_param('scope'));

        if ($scope !== '' && in_array($scope, PriceRule::scopes(), true)) {
            $query = $query->andWhere('scope', '=', $scope);
        }

        $active = $request->get_param('active');

        if ($active !== null && $active !== '') {
            $query = $query->andWhere('active', '=', (int) (bool) $active);
        }

        $result = $query->orderBy('priority', 'DESC')->paginate($perPage, $page);

        // Batch the page's labels: group names in one whereIn, and the product /
        // term / user caches primed in one query each, so present()'s per-row
        // objectLabel() lookups are cache hits instead of a query per rule.
        $groups = $this->groupNamesFor($result['data']);
        $this->primeObjectCaches($result['data']);

        $presented = [];

        foreach ($result['data'] as $rule) {
            $presented[] = $this->present($rule, $groups);
        }

        return $this->success([
            'rules'    => $presented,
            'total'    => (int) $result['meta']['total'],
            'page'     => (int) $result['meta']['current_page'],
            'per_page' => (int) $result['meta']['per_page'],
            'scopes'   => $this->scopeOptions(),
            'types'    => $this->typeOptions(),
            'tier'     => Tier::toArray(),
        ]);
    }

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

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

        $rule = PriceRule::create($this->withVendorOwner($validation));

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

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

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

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

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

        PriceRule::update($id, $this->withVendorOwner($validation));

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

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

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

        $rule->delete();

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

    /**
     * @return array<string, mixed>|string Attributes, or an error message.
     */
    private function validateRule(\WP_REST_Request $request)
    {
        $scope = sanitize_key((string) $request->get_param('scope'));

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

        $type = sanitize_key((string) $request->get_param('type'));

        if (!in_array($type, PriceRule::types(), true)) {
            return __('Choose whether this is a percentage, an amount off, or a fixed price.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        $value = (float) $request->get_param('value');

        if ($value < 0) {
            return __('The value cannot be negative.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        if ($type === PriceRule::TYPE_PERCENT && $value > 100) {
            return __('A percentage discount cannot be more than 100%.', 'softminal-b2b-wholesale-for-woocommerce');
        }

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

        // Customer rules are group-independent; everything else needs a group.
        if ($scope === PriceRule::SCOPE_CUSTOMER) {
            $groupId = null;
        } elseif ($groupId === null || !Group::find($groupId)) {
            return __('Choose which customer group this rule applies to.', 'softminal-b2b-wholesale-for-woocommerce');
        }

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

        if ($scope !== PriceRule::SCOPE_GLOBAL && $objectId <= 0) {
            return __('Choose what this rule applies to.', 'softminal-b2b-wholesale-for-woocommerce');
        }

        // Store the default-language id so the rule is language-agnostic: it
        // then applies whichever translation the shopper is viewing.
        if ($scope === PriceRule::SCOPE_PRODUCT) {
            $objectId = Multilingual::productId($objectId);
        } elseif ($scope === PriceRule::SCOPE_CATEGORY) {
            $objectId = Multilingual::termId($objectId, 'product_cat');
        } elseif ($scope === PriceRule::SCOPE_TAG) {
            $objectId = Multilingual::termId($objectId, 'product_tag');
        }

        $minQty = max(1, (int) ($request->get_param('min_qty') ?: 1));
        $maxQty = $request->get_param('max_qty');
        $maxQty = ($maxQty === null || $maxQty === '') ? null : (int) $maxQty;

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

        // A self-service vendor may only price their own catalogue. No-op for the
        // admin.
        $vendorError = $this->vendorScopeError($scope, $scope === PriceRule::SCOPE_GLOBAL ? 0 : $objectId);

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

        return [
            'group_id'  => $groupId,
            'scope'     => $scope,
            'object_id' => $scope === PriceRule::SCOPE_GLOBAL ? 0 : $objectId,
            'type'      => $type,
            'value'     => $value,
            'min_qty'   => $minQty,
            'max_qty'   => $maxQty,
            'priority'  => (int) $request->get_param('priority'),
            'active'    => $request->get_param('active') === null ? 1 : (int) (bool) $request->get_param('active'),
        ];
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function scopeOptions(): array
    {
        $labels = [
            PriceRule::SCOPE_GLOBAL   => __('Everything in the shop', 'softminal-b2b-wholesale-for-woocommerce'),
            PriceRule::SCOPE_PRODUCT  => __('One product', 'softminal-b2b-wholesale-for-woocommerce'),
            PriceRule::SCOPE_CATEGORY => __('A category', 'softminal-b2b-wholesale-for-woocommerce'),
            PriceRule::SCOPE_TAG      => __('A tag', 'softminal-b2b-wholesale-for-woocommerce'),
            PriceRule::SCOPE_CUSTOMER => __('One customer', 'softminal-b2b-wholesale-for-woocommerce'),
        ];

        $options = [];

        foreach ($labels as $value => $label) {
            $options[] = [
                'value' => $value,
                'label' => $label,
                // Every core scope is available in Free.
                'pro'   => false,
            ];
        }

        /**
         * The scope choices offered by the price-rule dialog. An add-on may append
         * its own scope so the option appears only when that add-on is active — the
         * Dokan bridge adds a `vendor` scope carrying an embedded `choices` list of
         * vendors for the picker. Each option is `['value','label','pro']`, plus an
         * optional `choices` array of `['id','label']` for a fixed-list picker.
         *
         * @param array<int, array<string, mixed>> $options
         * @param string                           $ruleType 'price' or 'quantity'
         */
        return apply_filters('smb2b_rule_scope_options', $options, 'price');
    }

    /**
     * @return array<int, array<string, string>>
     */
    private function typeOptions(): array
    {
        return [
            ['value' => PriceRule::TYPE_PERCENT, 'label' => __('Percentage off', 'softminal-b2b-wholesale-for-woocommerce')],
            ['value' => PriceRule::TYPE_AMOUNT_OFF, 'label' => __('Amount off', 'softminal-b2b-wholesale-for-woocommerce')],
            ['value' => PriceRule::TYPE_FIXED, 'label' => __('Fixed price', 'softminal-b2b-wholesale-for-woocommerce')],
        ];
    }

    /**
     * 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 / term / user caches for a page of rules in one query
     * each, so objectLabel()'s per-row wc_get_product()/get_term()/get_userdata()
     * become cache hits.
     *
     * @param iterable<object> $rules
     */
    private function primeObjectCaches($rules): void
    {
        $productIds = [];
        $termIds    = [];
        $userIds    = [];

        foreach ($rules as $rule) {
            $objectId = (int) $rule->object_id;
            if ($objectId <= 0) {
                continue;
            }
            switch ((string) $rule->scope) {
                case PriceRule::SCOPE_PRODUCT:
                    $productIds[] = $objectId;
                    break;
                case PriceRule::SCOPE_CATEGORY:
                case PriceRule::SCOPE_TAG:
                    $termIds[] = $objectId;
                    break;
                case PriceRule::SCOPE_CUSTOMER:
                    $userIds[] = $objectId;
                    break;
            }
        }

        if (!empty($productIds) && function_exists('_prime_post_caches')) {
            _prime_post_caches(array_values(array_unique($productIds)), false, true);
        }
        if (!empty($termIds)) {
            // No taxonomy → primes the term cache for these ids across taxonomies.
            get_terms(['include' => array_values(array_unique($termIds)), 'hide_empty' => false]);
        }
        if (!empty($userIds)) {
            cache_users(array_values(array_unique($userIds)));
        }
    }

    /**
     * @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;

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

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

    private function objectLabel(string $scope, int $objectId): string
    {
        if ($scope === PriceRule::SCOPE_GLOBAL) {
            return __('Storewide', 'softminal-b2b-wholesale-for-woocommerce');
        }

        switch ($scope) {
            case PriceRule::SCOPE_PRODUCT:
                $product = function_exists('wc_get_product') ? wc_get_product($objectId) : null;
                return $product ? $product->get_name() : __('(deleted product)', 'softminal-b2b-wholesale-for-woocommerce');

            case PriceRule::SCOPE_CATEGORY:
            case PriceRule::SCOPE_TAG:
                $term = get_term($objectId);
                return ($term && !is_wp_error($term)) ? $term->name : __('(deleted term)', 'softminal-b2b-wholesale-for-woocommerce');

            case PriceRule::SCOPE_CUSTOMER:
                $user = get_userdata($objectId);
                return $user ? $user->display_name : __('(deleted customer)', 'softminal-b2b-wholesale-for-woocommerce');
        }

        /**
         * Label the target of a scope core does not own (the Dokan bridge's
         * `vendor` scope, whose `object_id` is a vendor user id → the store name).
         *
         * @param string $label    Empty default.
         * @param string $scope    The rule scope.
         * @param int    $objectId The scope's target id.
         */
        return (string) apply_filters('smb2b_rule_object_label', '', $scope, $objectId);
    }
}
