<?php

declare(strict_types=1);

namespace App\Services;

final class DiarSignalExplainer
{
    /**
     * @param array<string,mixed> $source
     * @param array<int,array<string,mixed>> $transactions
     * @return array<int,array<string,mixed>>
     */
    public static function explain(array $source, array $transactions = []): array
    {
        $signalText = trim((string)($source['signals'] ?? ''));
        if ($signalText === '') {
            return [];
        }

        $codes = [];
        foreach (explode('|', $signalText) as $code) {
            $code = strtoupper(trim($code));
            if ($code !== '' && !in_array($code, $codes, true)) {
                $codes[] = $code;
            }
        }

        $movementLines = self::number($source, 'movement_lines');
        $transactionCount = self::number($source, 'transaction_count');
        $distinctUsers = self::number($source, 'distinct_users');
        $inbound = self::number($source, 'inbound_raw_qty');
        $outbound = self::number($source, 'outbound_raw_qty');
        $net = self::number($source, 'net_raw_qty');
        $reconAdd = self::number($source, 'recon_add_qty');
        $reconSub = self::number($source, 'recon_sub_qty');
        $returnIn = self::number($source, 'return_in_qty');
        $returnOut = self::number($source, 'return_out_qty');
        $transferOut = self::number($source, 'transfer_out_qty');
        $inventoryEvents = self::number($source, 'inventory_events');
        $threshold = self::number($source, 'outbound_threshold_used');
        $gapDays = self::number($source, 'outbound_gap_days');
        $userTraceLines = self::number($source, 'user_trace_lines');
        $afterHours = self::number($source, 'h00_05_lines') + self::number($source, 'h18_23_lines');
        if ($afterHours <= 0 && $transactions !== []) {
            foreach ($transactions as $transaction) {
                $afterHours += self::number($transaction, 'after_hours_lines');
            }
        }
        $afterPct = $movementLines > 0 ? ($afterHours / $movementLines) * 100 : 0.0;
        $baseline = strtoupper(trim((string)($source['baseline_status'] ?? '')));

        $items = [];
        foreach ($codes as $code) {
            $item = self::definition($code);
            $item['code'] = $code;
            $item['points'] = self::points($code);
            $item['evidence_en'] = self::evidenceEn(
                $code,
                $outbound,
                $threshold,
                $baseline,
                $transactionCount,
                $movementLines,
                $distinctUsers,
                $inbound,
                $net,
                $reconAdd,
                $reconSub,
                $returnIn,
                $returnOut,
                $transferOut,
                $inventoryEvents,
                $afterHours,
                $afterPct,
                $gapDays,
                $userTraceLines
            );
            $item['evidence_ar'] = self::evidenceAr(
                $code,
                $outbound,
                $threshold,
                $baseline,
                $transactionCount,
                $movementLines,
                $distinctUsers,
                $inbound,
                $net,
                $reconAdd,
                $reconSub,
                $returnIn,
                $returnOut,
                $transferOut,
                $inventoryEvents,
                $afterHours,
                $afterPct,
                $gapDays,
                $userTraceLines
            );
            $items[] = $item;
        }
        return $items;
    }

    /** @return array<int,string> */
    public static function labels(string $signalText, string $lang = 'en'): array
    {
        $labels = [];
        foreach (explode('|', $signalText) as $code) {
            $code = strtoupper(trim($code));
            if ($code === '') {
                continue;
            }
            $definition = self::definition($code);
            $label = $lang === 'ar' ? (string)$definition['title_ar'] : (string)$definition['title_en'];
            if (!in_array($label, $labels, true)) {
                $labels[] = $label;
            }
        }
        return $labels;
    }

    /** @return array<string,string> */
    private static function definition(string $code): array
    {
        $map = [
            'OUTBOUND_EXTREME' => [
                'title_en' => 'Extreme outbound quantity',
                'title_ar' => 'كمية صرف خارجة شديدة الارتفاع',
                'meaning_en' => 'The outbound quantity was materially above the mature statistical baseline for this item, store and raw unit.',
                'meaning_ar' => 'كانت كمية الصرف الخارجة أعلى بدرجة كبيرة من خط الأساس الإحصائي الناضج لهذا الصنف والمخزن والوحدة الخام.',
                'checks_en' => 'Verify patient or department issues, authorisation, issue documents and whether one operational event explains the volume.',
                'checks_ar' => 'راجع الصرف للمرضى أو الأقسام، والاعتمادات ومستندات الصرف، وهل يفسر حدث تشغيلي واحد هذه الكمية.',
            ],
            'OUTBOUND_SPIKE' => [
                'title_en' => 'Unusual outbound spike',
                'title_ar' => 'ارتفاع غير معتاد في الصرف',
                'meaning_en' => 'The outbound quantity crossed the normal review threshold calculated from a mature baseline.',
                'meaning_ar' => 'تجاوزت كمية الصرف حد المراجعة الطبيعي المحسوب من خط أساس إحصائي ناضج.',
                'checks_en' => 'Compare the listed transaction serials with issue documents and the normal consumption pattern.',
                'checks_ar' => 'قارن أرقام الحركات الظاهرة بمستندات الصرف وبنمط الاستهلاك المعتاد.',
            ],
            'OUTBOUND_EXTREME_LIMITED' => [
                'title_en' => 'Extreme outbound quantity with limited history',
                'title_ar' => 'صرف شديد الارتفاع مع تاريخ محدود',
                'meaning_en' => 'The quantity was extreme against the available baseline, but the historical sample is limited, so the statistical confidence is lower than READY.',
                'meaning_ar' => 'كانت الكمية شديدة الارتفاع مقارنة بالخط المتاح، لكن العينة التاريخية محدودة؛ لذلك الثقة الإحصائية أقل من تصنيف READY.',
                'checks_en' => 'Review the source transactions and confirm whether the short history or a new service pattern explains the volume.',
                'checks_ar' => 'راجع الحركات المصدرية وتأكد هل قِصر التاريخ أو نمط خدمة جديد يفسر الكمية.',
            ],
            'OUTBOUND_SPIKE_LIMITED' => [
                'title_en' => 'Unusual outbound spike with limited history',
                'title_ar' => 'ارتفاع غير معتاد في الصرف مع تاريخ محدود',
                'meaning_en' => 'The outbound quantity crossed the review threshold, while the historical baseline is limited rather than fully mature.',
                'meaning_ar' => 'تجاوزت كمية الصرف حد المراجعة، بينما خط الأساس التاريخي محدود وليس ناضجًا بالكامل.',
                'checks_en' => 'Validate each transaction serial and determine whether the volume is supported by approved clinical or operational demand.',
                'checks_ar' => 'تحقق من كل رقم حركة وحدد ما إذا كانت الكمية مدعومة بطلب سريري أو تشغيلي معتمد.',
            ],
            'OUTBOUND_AFTER_SILENCE' => [
                'title_en' => 'Outbound activity after a long inactive period',
                'title_ar' => 'صرف بعد فترة توقف طويلة',
                'meaning_en' => 'Outbound activity resumed after an unusually long gap for this item and store.',
                'meaning_ar' => 'عادت حركة الصرف بعد فجوة طويلة بصورة غير معتادة لهذا الصنف والمخزن.',
                'checks_en' => 'Confirm whether the item was reactivated, newly stocked or used for a specific approved clinical need.',
                'checks_ar' => 'تأكد هل أعيد تفعيل الصنف أو توريده حديثًا أو استخدم لحاجة سريرية محددة ومعتمدة.',
            ],
            'OUTBOUND_SPARSE_EXTREME' => [
                'title_en' => 'High outbound quantity in a sparse baseline',
                'title_ar' => 'صرف مرتفع مع خط أساس متباعد',
                'meaning_en' => 'The item has few active days, but the observed outbound quantity was still unusually high.',
                'meaning_ar' => 'للصنف أيام نشاط قليلة، ومع ذلك كانت كمية الصرف المرصودة مرتفعة بصورة غير معتادة.',
                'checks_en' => 'Review the transaction references and compare with the clinical event or stock request that caused the activity.',
                'checks_ar' => 'راجع مراجع الحركات وقارنها بالحدث السريري أو طلب المخزون الذي سبب النشاط.',
            ],
            'NEW_OUTBOUND_ACTIVITY' => [
                'title_en' => 'New outbound activity after no recorded history',
                'title_ar' => 'نشاط صرف جديد بعد غياب تاريخ مسجل',
                'meaning_en' => 'Outbound activity appeared after a prolonged period with no recorded outbound movement.',
                'meaning_ar' => 'ظهرت حركة صرف بعد فترة طويلة لم تسجل فيها حركات صرف سابقة.',
                'checks_en' => 'Confirm whether this is a new item, new store usage or an exceptional approved issue.',
                'checks_ar' => 'تأكد هل الصنف جديد أو بدأ استخدامه في المخزن حديثًا أو أن الصرف استثنائي ومعتمد.',
            ],
            'RECON_ADD_EXTREME' => [
                'title_en' => 'Extreme reconciliation addition',
                'title_ar' => 'إضافة تسوية شديدة الارتفاع',
                'meaning_en' => 'A reconciliation movement added a quantity materially above the historical pattern.',
                'meaning_ar' => 'أضافت حركة تسوية كمية أعلى بدرجة كبيرة من النمط التاريخي.',
                'checks_en' => 'Verify the inventory sheet, approval, reason for the adjustment and the exact transaction serial.',
                'checks_ar' => 'راجع محضر الجرد والاعتماد وسبب التسوية ورقم الحركة المحدد.',
            ],
            'RECON_ADD' => [
                'title_en' => 'Reconciliation addition',
                'title_ar' => 'إضافة تسوية',
                'meaning_en' => 'A positive stock reconciliation was recorded for the item.',
                'meaning_ar' => 'سُجلت تسوية موجبة أضافت كمية إلى رصيد الصنف.',
                'checks_en' => 'Confirm the supporting inventory or correction document and the authorised user.',
                'checks_ar' => 'تأكد من مستند الجرد أو التصحيح والمستخدم المخول.',
            ],
            'RECON_SUB_EXTREME' => [
                'title_en' => 'Extreme reconciliation deduction',
                'title_ar' => 'خصم تسوية شديد الارتفاع',
                'meaning_en' => 'A reconciliation movement deducted a quantity materially above the historical pattern.',
                'meaning_ar' => 'خصمت حركة تسوية كمية أعلى بدرجة كبيرة من النمط التاريخي.',
                'checks_en' => 'Verify the shortage or correction document, approval and the exact transaction serial.',
                'checks_ar' => 'راجع مستند العجز أو التصحيح والاعتماد ورقم الحركة المحدد.',
            ],
            'RECON_SUB' => [
                'title_en' => 'Reconciliation deduction',
                'title_ar' => 'خصم تسوية',
                'meaning_en' => 'A negative stock reconciliation was recorded for the item.',
                'meaning_ar' => 'سُجلت تسوية سالبة خصمت كمية من رصيد الصنف.',
                'checks_en' => 'Confirm the physical count, reason for the shortage and the approval chain.',
                'checks_ar' => 'تأكد من الجرد الفعلي وسبب العجز وسلسلة الاعتماد.',
            ],
            'RETURN_IN_SPIKE' => [
                'title_en' => 'Unusually high inward return',
                'title_ar' => 'مرتجع داخل مرتفع بصورة غير معتادة',
                'meaning_en' => 'The returned quantity into the store was substantially above its normal pattern.',
                'meaning_ar' => 'كانت الكمية المرتجعة إلى المخزن أعلى بدرجة كبيرة من نمطها المعتاد.',
                'checks_en' => 'Verify the return source, patient or department reference and condition of returned stock.',
                'checks_ar' => 'راجع مصدر المرتجع ومرجع المريض أو القسم وحالة المخزون المرتجع.',
            ],
            'RETURN_IN' => [
                'title_en' => 'Inward return activity',
                'title_ar' => 'حركة مرتجع داخل',
                'meaning_en' => 'A quantity was returned into the store on the review day.',
                'meaning_ar' => 'أعيدت كمية إلى المخزن في يوم المراجعة.',
                'checks_en' => 'Confirm the return document and whether the quantity is reusable, cancelled or corrected.',
                'checks_ar' => 'تحقق من مستند المرتجع وهل الكمية قابلة لإعادة الاستخدام أو ملغاة أو مصححة.',
            ],
            'RETURN_OUT_SPIKE' => [
                'title_en' => 'Unusually high outward return',
                'title_ar' => 'مرتجع خارج مرتفع بصورة غير معتادة',
                'meaning_en' => 'The quantity returned out of the store was substantially above the normal pattern.',
                'meaning_ar' => 'كانت كمية المرتجع الخارجة من المخزن أعلى بدرجة كبيرة من النمط المعتاد.',
                'checks_en' => 'Verify the supplier or destination, return authorisation and item condition.',
                'checks_ar' => 'راجع المورد أو الجهة المستلمة واعتماد المرتجع وحالة الصنف.',
            ],
            'RETURN_OUT' => [
                'title_en' => 'Outward return activity',
                'title_ar' => 'حركة مرتجع خارج',
                'meaning_en' => 'A quantity was returned out of the store on the review day.',
                'meaning_ar' => 'خرجت كمية من المخزن كمرتجع في يوم المراجعة.',
                'checks_en' => 'Confirm the return document, destination and approval.',
                'checks_ar' => 'تحقق من مستند المرتجع والجهة المستلمة والاعتماد.',
            ],
            'TRANSFER_SPIKE' => [
                'title_en' => 'Unusually high transfer out',
                'title_ar' => 'تحويل خارج مرتفع بصورة غير معتادة',
                'meaning_en' => 'The quantity transferred from the store exceeded the normal transfer pattern.',
                'meaning_ar' => 'تجاوزت الكمية المحولة من المخزن نمط التحويل المعتاد.',
                'checks_en' => 'Verify the destination store, transfer receipt and matching receiving evidence.',
                'checks_ar' => 'راجع المخزن المستلم وإيصال التحويل ودليل الاستلام المطابق.',
            ],
            'AFTER_HOURS_CONCENTRATION' => [
                'title_en' => 'Movement concentration outside normal hours',
                'title_ar' => 'تركز الحركات خارج الساعات المعتادة',
                'meaning_en' => 'At least 85% of the source lines occurred in the configured late-evening or early-morning periods.',
                'meaning_ar' => 'حدث ما لا يقل عن 85% من سطور الحركة في فترات المساء المتأخر أو الصباح المبكر المحددة بالقواعد.',
                'checks_en' => 'Compare the timestamps with the store operating schedule and confirm emergency, 24-hour or batch-entry activity.',
                'checks_ar' => 'قارن التوقيتات بساعات تشغيل المخزن وتأكد من وجود طوارئ أو تشغيل 24 ساعة أو إدخال جماعي للحركات.',
            ],
            'SINGLE_USER_HIGH_VOLUME' => [
                'title_en' => 'High-volume activity by one user',
                'title_ar' => 'حجم حركة مرتفع بواسطة مستخدم واحد',
                'meaning_en' => 'High-volume movement was recorded while one user accounted for the traceable activity.',
                'meaning_ar' => 'سُجل حجم حركة مرتفع بينما كان مستخدم واحد مسؤولًا عن النشاط القابل للتتبع.',
                'checks_en' => 'Verify the user authorisation, shift and whether credentials were shared or used for batch entry.',
                'checks_ar' => 'راجع صلاحية المستخدم والوردية وهل استُخدمت بيانات الدخول بصورة مشتركة أو لإدخال جماعي.',
            ],
            'USER_TRACE_GAP' => [
                'title_en' => 'Incomplete user trace',
                'title_ar' => 'نقص في تتبع المستخدم',
                'meaning_en' => 'One or more source lines did not contain a traceable effective user.',
                'meaning_ar' => 'لا يحتوي سطر واحد أو أكثر من حركات المصدر على مستخدم فعال قابل للتتبع.',
                'checks_en' => 'Review system logs and determine why the user identifier is absent.',
                'checks_ar' => 'راجع سجلات النظام وحدد سبب غياب معرف المستخدم.',
            ],
            'SAME_DAY_ROUND_TRIP' => [
                'title_en' => 'Similar inbound and outbound quantities on the same day',
                'title_ar' => 'كميات داخلة وخارجة متقاربة في اليوم نفسه',
                'meaning_en' => 'Substantial quantities entered and left on the same day with a high similarity ratio.',
                'meaning_ar' => 'دخلت وخرجت كميات كبيرة في اليوم نفسه وبنسبة تقارب مرتفعة.',
                'checks_en' => 'Confirm whether this represents a valid return, correction, transfer cycle or duplicate entry.',
                'checks_ar' => 'تأكد هل يمثل ذلك مرتجعًا صحيحًا أو تصحيحًا أو دورة تحويل أو إدخالًا مكررًا.',
            ],
            'INVENTORY_WITH_NET_CHANGE' => [
                'title_en' => 'Inventory event with a non-zero net change',
                'title_ar' => 'حدث جرد نتج عنه تغير صافٍ في الرصيد',
                'meaning_en' => 'An inventory event was recorded and the day ended with a non-zero net quantity change.',
                'meaning_ar' => 'سُجل حدث جرد وانتهى اليوم بتغير صافٍ غير صفري في الكمية.',
                'checks_en' => 'Reconcile the physical count, opening balance, adjustment movements and approval documents.',
                'checks_ar' => 'طابق الجرد الفعلي والرصيد الافتتاحي وحركات التسوية ومستندات الاعتماد.',
            ],
        ];

        return $map[$code] ?? [
            'title_en' => str_replace('_', ' ', $code),
            'title_ar' => str_replace('_', ' ', $code),
            'meaning_en' => 'A configured risk rule was triggered for this case.',
            'meaning_ar' => 'تم تشغيل إحدى قواعد المخاطر المكونة لهذه الحالة.',
            'checks_en' => 'Review the source transaction references and supporting documents.',
            'checks_ar' => 'راجع مراجع الحركات المصدرية والمستندات المؤيدة.',
        ];
    }

    private static function evidenceEn(
        string $code,
        float $outbound,
        float $threshold,
        string $baseline,
        float $transactionCount,
        float $movementLines,
        float $distinctUsers,
        float $inbound,
        float $net,
        float $reconAdd,
        float $reconSub,
        float $returnIn,
        float $returnOut,
        float $transferOut,
        float $inventoryEvents,
        float $afterHours,
        float $afterPct,
        float $gapDays,
        float $userTraceLines
    ): string {
        if (strpos($code, 'OUTBOUND_') === 0 && $code !== 'OUTBOUND_AFTER_SILENCE') {
            return 'Outbound ' . self::fmt($outbound) . '; calculated threshold ' . self::fmtNullable($threshold) . '; baseline ' . ($baseline !== '' ? $baseline : 'not recorded') . '; ' . self::fmt($transactionCount) . ' transactions.';
        }
        if ($code === 'OUTBOUND_AFTER_SILENCE' || $code === 'NEW_OUTBOUND_ACTIVITY') {
            return 'Outbound ' . self::fmt($outbound) . ' after a gap of ' . self::fmt($gapDays) . ' days; threshold ' . self::fmtNullable($threshold) . '.';
        }
        if ($code === 'RECON_ADD_EXTREME' || $code === 'RECON_ADD') {
            return 'Reconciliation addition ' . self::fmt($reconAdd) . '; net quantity ' . self::signed($net) . '; inventory events ' . self::fmt($inventoryEvents) . '.';
        }
        if ($code === 'RECON_SUB_EXTREME' || $code === 'RECON_SUB') {
            return 'Reconciliation deduction ' . self::fmt($reconSub) . '; net quantity ' . self::signed($net) . '; inventory events ' . self::fmt($inventoryEvents) . '.';
        }
        if ($code === 'RETURN_IN' || $code === 'RETURN_IN_SPIKE') {
            return 'Inward return quantity ' . self::fmt($returnIn) . '; inbound total ' . self::fmt($inbound) . '.';
        }
        if ($code === 'RETURN_OUT' || $code === 'RETURN_OUT_SPIKE') {
            return 'Outward return quantity ' . self::fmt($returnOut) . '; outbound total ' . self::fmt($outbound) . '.';
        }
        if ($code === 'TRANSFER_SPIKE') {
            return 'Transfer-out quantity ' . self::fmt($transferOut) . '; outbound total ' . self::fmt($outbound) . '.';
        }
        if ($code === 'AFTER_HOURS_CONCENTRATION') {
            return self::fmt($afterHours) . ' of ' . self::fmt($movementLines) . ' source lines were outside normal hours (' . self::fmt($afterPct) . '%).';
        }
        if ($code === 'SINGLE_USER_HIGH_VOLUME') {
            return self::fmt($movementLines) . ' source lines, ' . self::fmt($transactionCount) . ' transactions and ' . self::fmt($distinctUsers) . ' distinct user(s).';
        }
        if ($code === 'USER_TRACE_GAP') {
            return self::fmt($userTraceLines) . ' of ' . self::fmt($movementLines) . ' source lines contained a traceable user; gap ' . self::fmt(max(0.0, $movementLines - $userTraceLines)) . ' line(s).';
        }
        if ($code === 'SAME_DAY_ROUND_TRIP') {
            $ratio = max($inbound, $outbound) > 0 ? min($inbound, $outbound) / max($inbound, $outbound) * 100 : 0;
            return 'Inbound ' . self::fmt($inbound) . '; outbound ' . self::fmt($outbound) . '; similarity ' . self::fmt($ratio) . '%; net ' . self::signed($net) . '.';
        }
        if ($code === 'INVENTORY_WITH_NET_CHANGE') {
            return self::fmt($inventoryEvents) . ' inventory event(s); inbound ' . self::fmt($inbound) . '; outbound ' . self::fmt($outbound) . '; net change ' . self::signed($net) . '.';
        }
        return self::fmt($transactionCount) . ' transactions across ' . self::fmt($movementLines) . ' source lines.';
    }

    private static function evidenceAr(
        string $code,
        float $outbound,
        float $threshold,
        string $baseline,
        float $transactionCount,
        float $movementLines,
        float $distinctUsers,
        float $inbound,
        float $net,
        float $reconAdd,
        float $reconSub,
        float $returnIn,
        float $returnOut,
        float $transferOut,
        float $inventoryEvents,
        float $afterHours,
        float $afterPct,
        float $gapDays,
        float $userTraceLines
    ): string {
        if (strpos($code, 'OUTBOUND_') === 0 && $code !== 'OUTBOUND_AFTER_SILENCE') {
            return 'الكمية الخارجة ' . self::fmt($outbound) . '؛ الحد المحسوب ' . self::fmtNullable($threshold) . '؛ تصنيف الخط الأساسي ' . ($baseline !== '' ? $baseline : 'غير مسجل') . '؛ وعدد الحركات ' . self::fmt($transactionCount) . '.';
        }
        if ($code === 'OUTBOUND_AFTER_SILENCE' || $code === 'NEW_OUTBOUND_ACTIVITY') {
            return 'الكمية الخارجة ' . self::fmt($outbound) . ' بعد فجوة قدرها ' . self::fmt($gapDays) . ' يومًا؛ والحد المحسوب ' . self::fmtNullable($threshold) . '.';
        }
        if ($code === 'RECON_ADD_EXTREME' || $code === 'RECON_ADD') {
            return 'إضافة التسوية ' . self::fmt($reconAdd) . '؛ صافي الكمية ' . self::signed($net) . '؛ وعدد أحداث الجرد ' . self::fmt($inventoryEvents) . '.';
        }
        if ($code === 'RECON_SUB_EXTREME' || $code === 'RECON_SUB') {
            return 'خصم التسوية ' . self::fmt($reconSub) . '؛ صافي الكمية ' . self::signed($net) . '؛ وعدد أحداث الجرد ' . self::fmt($inventoryEvents) . '.';
        }
        if ($code === 'RETURN_IN' || $code === 'RETURN_IN_SPIKE') {
            return 'كمية المرتجع الداخل ' . self::fmt($returnIn) . '؛ وإجمالي الداخل ' . self::fmt($inbound) . '.';
        }
        if ($code === 'RETURN_OUT' || $code === 'RETURN_OUT_SPIKE') {
            return 'كمية المرتجع الخارج ' . self::fmt($returnOut) . '؛ وإجمالي الخارج ' . self::fmt($outbound) . '.';
        }
        if ($code === 'TRANSFER_SPIKE') {
            return 'كمية التحويل الخارج ' . self::fmt($transferOut) . '؛ وإجمالي الخارج ' . self::fmt($outbound) . '.';
        }
        if ($code === 'AFTER_HOURS_CONCENTRATION') {
            return 'حدث ' . self::fmt($afterHours) . ' من أصل ' . self::fmt($movementLines) . ' سطر حركة خارج الساعات المعتادة بنسبة ' . self::fmt($afterPct) . '٪.';
        }
        if ($code === 'SINGLE_USER_HIGH_VOLUME') {
            return self::fmt($movementLines) . ' سطر حركة و' . self::fmt($transactionCount) . ' حركة بواسطة ' . self::fmt($distinctUsers) . ' مستخدم مختلف.';
        }
        if ($code === 'USER_TRACE_GAP') {
            return 'احتوى ' . self::fmt($userTraceLines) . ' من أصل ' . self::fmt($movementLines) . ' سطرًا على مستخدم قابل للتتبع؛ والفجوة ' . self::fmt(max(0.0, $movementLines - $userTraceLines)) . ' سطر.';
        }
        if ($code === 'SAME_DAY_ROUND_TRIP') {
            $ratio = max($inbound, $outbound) > 0 ? min($inbound, $outbound) / max($inbound, $outbound) * 100 : 0;
            return 'الداخل ' . self::fmt($inbound) . '؛ الخارج ' . self::fmt($outbound) . '؛ نسبة التقارب ' . self::fmt($ratio) . '٪؛ والصافي ' . self::signed($net) . '.';
        }
        if ($code === 'INVENTORY_WITH_NET_CHANGE') {
            return 'عدد أحداث الجرد ' . self::fmt($inventoryEvents) . '؛ الداخل ' . self::fmt($inbound) . '؛ الخارج ' . self::fmt($outbound) . '؛ والتغير الصافي ' . self::signed($net) . '.';
        }
        return self::fmt($transactionCount) . ' حركة موزعة على ' . self::fmt($movementLines) . ' سطر مصدر.';
    }

    private static function points(string $code): int
    {
        $points = [
            'OUTBOUND_EXTREME' => 38,
            'OUTBOUND_SPIKE' => 26,
            'OUTBOUND_EXTREME_LIMITED' => 32,
            'OUTBOUND_SPIKE_LIMITED' => 22,
            'OUTBOUND_AFTER_SILENCE' => 24,
            'OUTBOUND_SPARSE_EXTREME' => 18,
            'NEW_OUTBOUND_ACTIVITY' => 18,
            'RECON_ADD_EXTREME' => 28,
            'RECON_ADD' => 20,
            'RECON_SUB_EXTREME' => 28,
            'RECON_SUB' => 20,
            'RETURN_OUT_SPIKE' => 25,
            'RETURN_OUT' => 18,
            'RETURN_IN_SPIKE' => 17,
            'RETURN_IN' => 10,
            'TRANSFER_SPIKE' => 14,
            'AFTER_HOURS_CONCENTRATION' => 7,
            'SINGLE_USER_HIGH_VOLUME' => 9,
            'USER_TRACE_GAP' => 12,
            'SAME_DAY_ROUND_TRIP' => 9,
            'INVENTORY_WITH_NET_CHANGE' => 7,
        ];
        return $points[$code] ?? 0;
    }

    /** @param array<string,mixed> $row */
    private static function number(array $row, string $key): float
    {
        $value = $row[$key] ?? 0;
        return is_numeric($value) ? (float)$value : 0.0;
    }

    private static function fmt(float $value): string
    {
        if (abs($value - round($value)) < 0.000001) {
            return number_format($value, 0, '.', ',');
        }
        return number_format($value, 2, '.', ',');
    }

    private static function fmtNullable(float $value): string
    {
        return $value > 0 ? self::fmt($value) : 'not retained / غير محفوظ';
    }

    private static function signed(float $value): string
    {
        return ($value > 0 ? '+' : '') . self::fmt($value);
    }
}
