<?php

namespace App\Services\Billing;

use App\Models\Invoice;
use App\Models\InvoiceBillingRule;

/**
 * Multi-criteria rule evaluator for invoice → integrator dispatch.
 *
 * Reads `invoice_billing_rules` (active, event-matching, priority-ordered),
 * builds a context from the invoice, and returns the first rule whose
 * nested AND/OR condition tree evaluates true. Returns NULL when:
 *   - no rule matches, OR
 *   - the matching rule's action is `skip` (whitelist deny pattern)
 *
 * Spec: docs/superpowers/specs/2026-05-10-prc75-integrator-driven-invoicing-design.md
 */
class InvoicingRuleEvaluator
{
    public const ALLOWED_OPERATORS = ['==', '!=', '>', '>=', '<', '<=', 'in', 'not_in', 'contains'];

    public const ALLOWED_FIELDS = [
        'client.country',
        'client.is_company',
        'client.client_group_id',
        'client.language',
        'invoice.total_amount',
        'invoice.currency',
        'invoice.has_tax',
        'invoice.has_product_type',
        'invoice.product_ids',
        'invoice.product_categories',
    ];

    public function evaluate(Invoice $invoice, string $event): ?InvoiceBillingRule
    {
        $context = $this->buildContext($invoice);

        // 1) Normal (non-fallback) kuralları öncelik sırasıyla dene.
        $normal = InvoiceBillingRule::active()->normal()->forEvent($event)->byPriority()->get();
        foreach ($normal as $rule) {
            if ($this->matchesTree($rule->conditions, $context)) {
                return $rule->action === InvoiceBillingRule::ACTION_SKIP ? null : $rule;
            }
        }

        // 2) Hiçbir normal kural eşleşmediyse fallback kuralına düş.
        //    Fallback kuralının koşulları GÖRMEZDEN GELİNİR — olay uyuyorsa her zaman eşleşir.
        $fallback = InvoiceBillingRule::active()->fallback()->forEvent($event)->byPriority()->first();
        if ($fallback) {
            return $fallback->action === InvoiceBillingRule::ACTION_SKIP ? null : $fallback;
        }

        return null;
    }

    /**
     * Public for admin "test" endpoint preview.
     */
    public function buildContext(Invoice $invoice): array
    {
        $invoice->loadMissing(['client.country', 'currency', 'items.product']);
        $productTypes = $invoice->items
            ->pluck('product.type')
            ->filter()
            ->unique()
            ->values()
            ->all();
        $productCategories = $invoice->items
            ->pluck('product.category_id')
            ->filter()
            ->unique()
            ->values()
            ->all();
        $productIds = $invoice->items
            ->pluck('product_id')
            ->filter()
            ->values()
            ->all();

        return [
            'client.country' => $invoice->client?->country?->a2_iso,
            'client.is_company' => (bool) $invoice->client?->is_company,
            'client.client_group_id' => $invoice->client?->client_group_id,
            'client.language' => $invoice->client?->language,
            'invoice.total_amount' => (float) $invoice->total_amount,
            'invoice.currency' => $invoice->currency?->code,
            'invoice.has_tax' => (float) $invoice->tax_amount > 0,
            'invoice.has_product_type' => $productTypes,
            'invoice.product_ids' => $productIds,
            'invoice.product_categories' => $productCategories,
        ];
    }

    public function matchesTree(array $node, array $ctx): bool
    {
        if (($node['type'] ?? null) === 'condition') {
            $field = $node['field'] ?? null;
            $actual = $field !== null ? ($ctx[$field] ?? null) : null;

            return $this->compare($actual, $node['op'] ?? '==', $node['value'] ?? null);
        }

        // type === 'group'
        $children = $node['children'] ?? [];
        if (empty($children)) {
            // Empty group = vacuous match; treat AND as true and OR as false (least surprising)
            return ($node['operator'] ?? 'AND') === 'AND';
        }
        $results = array_map(fn ($c) => $this->matchesTree($c, $ctx), $children);

        return ($node['operator'] ?? 'AND') === 'AND'
            ? ! in_array(false, $results, true)
            : in_array(true, $results, true);
    }

    public function compare($actual, string $op, $expected): bool
    {
        return match ($op) {
            '==' => $actual == $expected,
            '!=' => $actual != $expected,
            '>' => $actual > $expected,
            '>=' => $actual >= $expected,
            '<' => $actual < $expected,
            '<=' => $actual <= $expected,
            'in' => is_array($actual)
                ? ! empty(array_intersect($actual, (array) $expected))
                : in_array($actual, (array) $expected, false),
            'not_in' => is_array($actual)
                ? empty(array_intersect($actual, (array) $expected))
                : ! in_array($actual, (array) $expected, false),
            'contains' => is_array($actual) && in_array($expected, $actual, false),
            default => false,
        };
    }
}
