<?php
/**
 * سودانا — Gateway v21
 * نقطة الدخول الوحيدة — يُستدعى من visitor_ask.php
 */

require_once __DIR__ . '/InferenceEngine.php';
require_once __DIR__ . '/BankAnswerEngine.php';
require_once __DIR__ . '/RouterEngine.php';
require_once __DIR__ . '/CacheLayer.php';
require_once __DIR__ . '/AIClient.php';
require_once __DIR__ . '/OutputBuilder.php';
require_once __DIR__ . '/ContextMemory.php';
require_once __DIR__ . '/ProviderDecisionEngine.php';
require_once __DIR__ . '/KPILogger.php';
require_once __DIR__ . '/StepController.php'; // FIX 1: StepController integrated
require_once __DIR__ . '/QueueEngine.php';
require_once __DIR__ . '/EventBus.php';
require_once __DIR__ . '/DecisionEngine.php';
require_once __DIR__ . '/PolicyEngine.php';
require_once __DIR__ . '/ControlEngine.php';
require_once __DIR__ . '/TaskEngine.php';
require_once __DIR__ . '/GrowthBrain.php';
require_once __DIR__ . '/AIOrchestrator.php';
require_once __DIR__ . '/Orchestrator.php';       // Phase 8: lightweight decision layer
require_once __DIR__ . '/SupportEngines.php';
require_once __DIR__ . '/BehaviorEngine.php';
require_once __DIR__ . '/FlowEngine.php';
require_once __DIR__ . '/BillingEngine.php';
require_once __DIR__ . '/SudannaPayEngine.php';
require_once __DIR__ . '/QRPayEngine.php';
require_once __DIR__ . '/BillPayEngine.php';
require_once __DIR__ . '/InvestmentEngine.php';
require_once __DIR__ . '/ROSCAEngine.php';
require_once __DIR__ . '/MerchantEngine.php';
require_once __DIR__ . '/AgentEngine.php';
require_once __DIR__ . '/SafetyEngine.php';
require_once __DIR__ . '/IntentDecomposer.php';
require_once __DIR__ . '/ToolsEngine.php';

class Gateway {

    public static function handle(array $req): array {
        // BRIDGE: accept trace_id from caller (visitor_ask, ai/answer)
        if (!empty($req['trace_id'])) {
            require_once __DIR__ . '/Logger.php';
            \Logger::setTraceId($req['trace_id']);
        }

        // [0] EXECUTION ID — propagates to all engines
        $executionId = 'exec_' . uniqid('', true);
        $ctx['execution_id'] = $executionId;
        // Default language — overridden by ContextMemory if user profile has lang
        $lang = 'ar';  // Bug fix: $lang must be defined before any conditional return

        $startMs  = round(microtime(true) * 1000);
        $q        = mb_substr(trim($req['question'] ?? ''), 0, 500);
        $user     = $req['user']   ?? null;
        $ip       = $req['ip']     ?? '0.0.0.0';
        // ── USER BINDING: guest users get persistent session ID ─────────
        if ($user && isset($user['id'])) {
            $userId = (int)$user['id'];
        } elseif (isset($_SESSION['guest_user_id'])) {
            $userId = (int)$_SESSION['guest_user_id'];
        } else {
            // Create lightweight guest session ID (no DB — ephemeral)
            $guestId = crc32(session_id() . ($_SERVER['REMOTE_ADDR'] ?? '') . date('Ymd'));
            $userId  = abs($guestId); // positive int, session-scoped only
            $_SESSION['guest_user_id'] = $userId;
        }
        $tier     = $user ? ($user['subscription_tier'] ?? 'free') : 'guest';

        // ── OBSERVABILITY: request_id per request ────────────────
        $requestId = uniqid('req_', true);
        if (!headers_sent()) {
            header('X-Request-ID: ' . $requestId);
        }

        $kpi = [
            'user_id'    => $userId,
            'user_tier'  => $tier,
            'service'    => 'general',
            'ai_used'    => false,
            'ie_hit'     => false,
            'cache_hit'  => false,
            'request_id' => $requestId,
        ];

        // Structured log entry (trace mode)
        self::traceLog('request_received', [
            'request_id' => $requestId,
            'user_id'    => $userId,
            'tier'       => $tier,
            'q_length'   => mb_strlen($q),
            'ip_hash'    => md5($ip),
        ]);

        // ── [1] INJECTION CHECK ──────────────────────────────────
        if (self::isInjection($q)) {
            $kpi['latency_ms'] = self::ms($startMs);
            KPILogger::log($kpi);
            return OutputBuilder::blocked('injection');
        }

        // ── [2] REVENUE GATE ─────────────────────────────────────
        $gate = self::revenueGate($q, $user, $ip);
        if ($gate === 'block') {
            $kpi['latency_ms'] = self::ms($startMs);
            KPILogger::log($kpi);
            return OutputBuilder::limitReached($tier);
        }
        $aiAllowed = ($gate !== 'no_ai');

        // ── [3] LOAD CONTROL ─────────────────────────────────────
        $loadLevel = KPILogger::getLoadLevel();
        if ($loadLevel >= 3) $aiAllowed = false;                    // critical: no AI
        if ($loadLevel >= 2 && $tier === 'guest') $aiAllowed = false; // high: no AI for guests

        // ── [4] SESSION MEMORY ───────────────────────────────────
        $ctx = [];
        $sessionHistory = [];
        if ($userId) {
            $profile = ContextMemory::getUserProfile($userId);
            $ctx['user'] = $profile;
            $ctx['has_children'] = $profile['has_children'] ?? false;
            $sessionHistory = ContextMemory::getSession($userId);
        }

        // ── AI GATE: Risk+Fraud+Limits check for payment amounts ─────
        if ($userId > 0 && !empty($req['amount']) && (float)$req['amount'] > 0) {
            require_once __DIR__ . '/RiskEngine.php';
            require_once __DIR__ . '/FraudGraph.php';
            require_once __DIR__ . '/SmartLimits.php';
            $gateAmount = (float)$req['amount'];
            $gRisk  = RiskEngine::assess($userId, $gateAmount);
            $gFraud = FraudGraph::check($userId, $gateAmount, ['ip' => $ip]);
            $gLim   = SmartLimits::check($userId, $gateAmount);
            if ($gRisk['action']==='block' || $gFraud['action']==='block' || !$gLim['allowed']) {
                KPILogger::log(array_merge($kpi, ['blocked'=>true,'trace_id'=>$gRisk['trace_id']??'']));
                return OutputBuilder::build('⚠️ تم حجب الطلب لأسباب أمنية. تواصل مع الدعم.', 'platform', 'safety_block', [], [], $q);
            }
        }
        // ── [4.5] EARLY CACHE CHECK (before IE to save compute) ────
        // P1 FIX: Cache moved before IE. Service detection uses null IE result (q-based only).
        // If cache hits, IE and AI are both skipped.
        $earlyService = self::detectService($q, null);
        $cachedEarly  = CacheLayer::read($q, $earlyService);
        if ($cachedEarly) {
            $kpi['cache_hit']  = true;
            $kpi['service']    = $earlyService;
            $kpi['latency_ms'] = self::ms($startMs);
            KPILogger::log($kpi);
            // Decision context not available yet — no provider actions on early cache
            return OutputBuilder::build($cachedEarly, $earlyService, 'cache', [], [], $q);
        }

        // ── [5] INFERENCE ENGINE ─────────────────────────────────
        $ie = InferenceEngine::infer($q, $ctx);
        // Enrich with AssistantEngine execution context
        if ($ie) {
            $asCtx = AssistantEngine::route($q, $ie);
            $ie['assistant_action']  = $asCtx['action']          ?? 'ai_answer';
            $ie['assistant_domain']  = $asCtx['domain']          ?? 'hub';
            $ie['assistant_name']    = $asCtx['assistant_name']   ?? 'مساعد سودانا';
            $ie['assistant_prompt']  = $asCtx['system_prompt']    ?? '';
            $ie['assistant_guard']   = $asCtx['guardrail']        ?? '';
            // Domain isolation check
            if ($ie['assistant_domain'] !== 'hub' && !empty($ie['sector'])) {
                if (AssistantEngine::isOutOfDomain($ie['sector'], $ie['assistant_domain'])) {
                    // log but don't block — PDE handles hard gates
                    error_log('[AE] out-of-domain: sector='.$ie['sector'].' domain='.$ie['assistant_domain']);
                }
            }
        }

        // ── [5.1] ORCHESTRATOR — routing decision (Phase 8) ──────────
        // Produces 'IE' | 'HYBRID' | 'AI' — existing flow unchanged.
        // Confidence thresholds: IE≥0.85, HYBRID 0.60–0.84, AI<0.60
        $decision = Orchestrator::decide($ie, $q, $ctx);
        // ── Bank Q&A: route finance sector to BankAnswerEngine ─────────
        if ($ie && ($ie['r'] ?? '') && str_starts_with($ie['r'], 'bank_qa:')) {
            $bankCategory = str_replace('bank_qa:', '', $ie['r']);
            $lang = isset($ctx['lang']) ? $ctx['lang'] : 'auto';
            $bqaResult = BankAnswerEngine::answer($q, $lang);
            $kpi['ie_hit'] = true;
            KPILogger::log($kpi);
            return OutputBuilder::build($bqaResult['answer'], 'finance', 'bank_qa', [], ['trace_id'=>$bqaResult['trace_id']], $q);
        }

        if ($ie && $ie['confidence'] >= 0.85) {
            $ieSector = $ie['sector'] ?? 'general';
            if (!in_array($ieSector, ['medical','family','pharmacy','emergency'])) {
                // ── SAFETY WRAP: evaluate IE answer before returning ──────
                // P0 FIX: IE high-confidence bypassed SafetyEngine. Now evaluated.
                $safetyOut = SafetyEngine::evaluateOutput($ie['answer'], $ieSector);
                if ($safetyOut['action'] === SafetyEngine::BLOCK) {
                    Logger::warn('ie_output_blocked', ['sector' => $ieSector, 'reason' => $safetyOut['reason'] ?? '']);
                    $kpi['latency_ms'] = self::ms($startMs);
                    KPILogger::log($kpi);
                    return OutputBuilder::blocked('safety_ie');
                }
                // WARN: append safety note to answer, then return
                $ieAnswer = ($safetyOut['action'] === SafetyEngine::WARN && !empty($safetyOut['replace']))
                    ? $safetyOut['replace']
                    : $ie['answer'];

                $kpi['ie_hit']        = true;
                $kpi['ie_confidence'] = $ie['confidence'];
                $kpi['service']       = $ieSector;
                $kpi['ie_rule_id']    = $ie['rule_id'] ?? null;  // Phase 6: rule scoring
                $kpi['latency_ms']    = self::ms($startMs);
                KPILogger::log($kpi);
                return OutputBuilder::build($ieAnswer, $ieSector, 'ie', [], [], $q);
            }
            // Medical IE → continue to PDE (hard gate handles this)
            $kpi['ie_hit']        = true;
            $kpi['ie_confidence'] = $ie['confidence'];
            $kpi['ie_rule_id']    = $ie['rule_id'] ?? null;  // Phase 6
        }

        // ── [5.5] SAFETY CHECK ──────────────────────────────────────
        $safetyIn = SafetyEngine::evaluateInput($q, ['user'=>$u,'ip'=>$ip]);
        if ($safetyIn['action'] === SafetyEngine::BLOCK) {
            if (!empty($safetyIn['log'])) SafetyEngine::logViolation($userId, $q, $safetyIn['reason']);
            return OutputBuilder::build(
                ['answer'=>'هذا الطلب لا يمكن معالجته. إذا كنت تحتاج مساعدة، تواصل معنا مباشرة.',
                 'source'=>'safety_block'],
                'general','safety',[],[],$q
            );
        }
        if ($safetyIn['action'] === SafetyEngine::WARN) {
            return OutputBuilder::build(
                ['answer'=>$safetyIn['response'],'source'=>'safety_warn'],
                'general','safety',[],[],$q
            );
        }

        // ── [6] CACHE CHECK ──────────────────────────────────────
        $service   = self::detectService($q, $ie);
        $kpi['service'] = $service;

        // ── [6.45] POLICY GATE ──────────────────────────────────
        $policyResult = PolicyEngine::evaluate('ai.request', $service);
        if ($policyResult === PolicyEngine::BLOCK) {
            return OutputBuilder::build(
                ['answer' => ($lang === 'ar'
                    ? 'هذا الطلب لا يمكن معالجته وفق سياسات المنصة.'
                    : 'This request cannot be processed per platform policy.'),
                 'source' => 'policy_block'],
                $service, 'policy', [], [], $q
            );
        }

        // ── [6.5] PROVIDER DECISION ENGINE ───────────────────────
        $decision = ProviderDecisionEngine::decide($service, $q, $ctx);
        $kpi['decision_type'] = $decision['type'];

        // ── BILLING GATE: before REAL execution ──────────────────────────
        if ($decision['type'] === 'REAL' && $userId > 0) {
            $billingResult = BillingEngine::evaluate($userId, $service, $ie ?? []);
            if ($billingResult['decision'] === 'create_invoice') {
                $payUrl = $billingResult['payment_url'] ?? '';
                return OutputBuilder::build(
                    ['answer' => ($lang==='ar'
                        ? "لإتمام هذه الخدمة، الرجاء الدفع أولاً:\n→ [ادفع الآن]($payUrl)"
                        : "To proceed, please complete payment:\n→ [Pay Now]($payUrl)"),
                     'action' => 'redirect_payment', 'payment_url' => $payUrl,
                    ], $service, 'billing', [], [], $q);
            }
        }

        // ── [6.6] HARD GATE: REAL medical → FORCE StepController ──────────
        // FIX: When PDE=REAL and sector=medical, tryStartFlow FIRST.
        // If it returns null (DB not ready), STILL block AI — return safe medical triage response.
        // NO PATH EXISTS that serves AI answer for REAL medical.
        if ($decision['type'] === 'REAL' && in_array($service, ['medical','family','pharmacy','emergency','legal','business','bde','engineering','agriculture'])) {
            // Route to correct flow based on sector
            $sectorFlowMap = [
                'medical'   => 'medical_telemedicine_v1',
                'family'    => 'medical_telemedicine_v1',
                'pharmacy'  => 'medical_telemedicine_v1',
                'emergency' => 'medical_telemedicine_v1',
                'legal'       => 'legal_consultation_v1',
                'business'    => 'business_consulting_v1',
                'bde'         => 'business_consulting_v1',
                'engineering' => 'engineering_consulting_v1',  // v22
                'agriculture' => 'agriculture_consulting_v1',  // v22
            ];
            $targetFlow = $sectorFlowMap[$service] ?? 'medical_telemedicine_v1';
            // For non-medical: override the hardcoded flow name below
            $flowResult = self::tryStartFlow($targetFlow, $q, $userId, $ctx, $decision);
            if ($flowResult !== null) {
                KPILogger::log(array_merge($kpi, ['latency_ms'=>self::ms($startMs),'flow_started'=>true]));
                return $flowResult;
            }
            // HARD GATE: tryStartFlow failed (DB issue) but we CANNOT serve AI diagnosis
            // Return deterministic medical triage start (no AI, no OutputBuilder bypass)
            KPILogger::log(array_merge($kpi, ['latency_ms'=>self::ms($startMs),'flow_degraded'=>true]));
            return [
                'success'         => true,
                'answer'          => "⚕️ لمساعدتك بشكل صحيح، أحتاج لبعض المعلومات.\n\nصِف أعراضك بالتفصيل:\n• منذ متى وأنت تعاني؟\n• هل الألم مستمر أم متقطع؟\n• هل لديك حرارة؟\n\n⚠️ في حال كان الألم شديداً أو تعاني من صعوبة في التنفس — اتصل بالطوارئ 999 فوراً.",
                'source'          => 'medical_safe',
                'service'         => 'medical',
                'medical_context' => true,
                'blocked_ai'      => true,
                'actions'         => [
                    ['label'=>'👨‍⚕️ تحدث مع طبيب الآن','url'=>'#consultation','icon'=>'👨‍⚕️','priority'=>'high'],
                    ['label'=>'🚨 اتصل 999 للطوارئ','url'=>'tel:999','icon'=>'🚨'],
                ],
                'cross_links' => [['service'=>'consultation','label'=>'احجز موعداً مع طبيب','url'=>'#consultation']],
            ];
        }

        // Post-decision cache check (service now fully resolved via IE+PDE)
        // Early cache at [4.5] catches most hits; this catches service-specific variants
        $cached = CacheLayer::read($q, $service);
        if ($cached) {
            $kpi['cache_hit']  = true;
            $kpi['latency_ms'] = self::ms($startMs);
            KPILogger::log($kpi);
            $dbData  = ProviderDecisionEngine::requiresRealProvider($decision)
                       ? self::fetchServiceData($service, $q, $user)
                       : [];
            $extras  = self::buildDecisionActions($decision, $dbData);
            return OutputBuilder::build($cached, $service, 'cache', $dbData, $extras, $q);
        }

        // [7] FLOWENGINE tool routing (execute_tool action from AssistantEngine)
        $aeAction = $ie['assistant_action'] ?? 'ai_answer';
        if (in_array($aeAction, ['execute_tool','execute_document','execute_code']) && $userId > 0) {
            try {
                $feResult = FlowEngine::run($service, $q, $userId, array_merge($ctx, ['execution_id'=>$executionId??'']));
                if (isset($feResult['execution_id']) && !in_array($feResult['status']??'', ['ai_answered','billing_required'])) {
                    KPILogger::log(array_merge($kpi, ['latency_ms'=>self::ms($startMs),'flow_engine'=>true]));
                    return OutputBuilder::build(
                        ['answer'=>$feResult['output']??($feResult['message']??'جاري المعالجة...'),'execution_id'=>$feResult['execution_id']],
                        $service, 'flow_engine', [], [], $q
                    );
                }
            } catch (Throwable $fe) {
                error_log("[GW→FE] " . $fe->getMessage());
            }
        }

        // IE partial (0.60-0.84): use IE answer + DB enrich
        if ($ie && $ie['confidence'] >= 0.60) {
            // Safety eval on partial IE answers too
            $safetyPartial = SafetyEngine::evaluateOutput($ie['answer'], $service);
            $partialAnswer = ($safetyPartial['action'] === SafetyEngine::WARN && !empty($safetyPartial['replace']))
                ? $safetyPartial['replace']
                : $ie['answer'];
            if ($safetyPartial['action'] === SafetyEngine::BLOCK) {
                // BLOCK: fall through to AI — do NOT call OutputBuilder with null
                // $partialAnswer intentionally not set — flow continues below
            } else {
                $dbData = self::fetchServiceData($service, $q, $user);
                $extras = self::buildDecisionActions($decision, $dbData);
                $kpi['ie_hit']        = true;
                $kpi['ie_confidence'] = $ie['confidence'];
                $kpi['latency_ms']    = self::ms($startMs);
                KPILogger::log($kpi);
                return OutputBuilder::build($partialAnswer, $service, 'ie', $dbData, $extras, $q);
            }
        }

        // ── [6.9] MULTI-INTENT DECOMPOSITION ──────────────────────────
        // Fires only when IE didn't return a confident answer (<0.60).
        // Detects 2-intent queries, returns merged IE answers without AI.
        if (!$ie || $ie['confidence'] < 0.60) {
            $decomposed = IntentDecomposer::decompose($q, $ctx);
            if ($decomposed !== null) {
                $kpi['multi_intent'] = true;
                $kpi['latency_ms']   = self::ms($startMs);
                KPILogger::log($kpi);
                return OutputBuilder::build(
                    $decomposed['merged'],
                    implode('+', $decomposed['sectors']),
                    'multi_intent',
                    [],
                    ['intents' => $decomposed['intents']],
                    $q
                );
            }
        }

        // ── [7] DB FIRST ─────────────────────────────────────────
        $dbData = self::fetchServiceData($service, $q, $user);

        // MANDATORY: if decision=REAL, always fetch providers even if DB "sufficient"
        if ($decision['type'] === 'REAL' && empty($dbData['providers'])) {
            $dbData = array_merge($dbData, self::fetchProviders($service, $user));
        }

        if (self::isDbSufficient($service, $dbData, $q)) {
            $answer = self::buildDbResponse($service, $dbData, $q);
            $extras = self::buildDecisionActions($decision, $dbData);
            $kpi['latency_ms'] = self::ms($startMs);
            KPILogger::log($kpi);
            return OutputBuilder::build($answer, $service, 'db', $dbData, $extras, $q);
        }

        // ── [8] AI BRAIN (LAST RESORT) ───────────────────────────
        if (!$aiAllowed) {
            $kpi['latency_ms'] = self::ms($startMs);
            KPILogger::log($kpi);
            // Fire flow.abandoned if user was in a REAL service context but AI not allowed
            if ($userId && in_array($service, ['medical','legal','business'])) {
                EventBus::emit('flow.abandoned', [
                    'user_id' => $userId,
                    'service' => $service,
                    'step'    => 'ai_blocked',
                    'user_plan'=> $tier,
                ]);
            }
            if ($ie) {
                $bCtx = ['service'=>$service,'source'=>'ie','ie_hit'=>true,'user_id'=>$userId,'flow_active'=>false,'step_completed'=>false];
                $extras_ie = ['behavior' => BehaviorEngine::compose($bCtx)];
                return OutputBuilder::build($ie['answer'], $service, 'ie', $dbData, $extras_ie, $q);
            }
            return OutputBuilder::noAnswer($service);
        }

        // Inject AE specialist prompt into system prompt
        $aePrompt = $ie['assistant_prompt'] ?? '';
        $system   = self::buildSystemPrompt($service, $user, $sessionHistory, $dbData, $q, $aePrompt);
        $mode   = self::selectMode($service, $q, $tier);

        // ── UsageMeter wrap: enforces budget + records tokens + auto-debit ──
        // For logged-in users only. Guests still use direct AIClient (no metering).
        if (!empty($user['id'])) {
            require_once __DIR__ . '/UsageMeter.php';
            $metered = UsageMeter::wrap(
                (int)$user['id'],
                'chat',          // event_type
                $service,        // service (medical/legal/etc)
                fn() => AIClient::call($q, $system, $mode, 400, 0.7, $service === 'medical'),
                ['skip_debit' => true]   // Gateway calls don't debit (chat is in plan limit)
            );
            if (!$metered['ok']) {
                // Budget denied — fall back to InferenceEngine answer if any
                if ($ie) return OutputBuilder::build($ie['answer'], $service, 'ie', $dbData, [], $q);
                return OutputBuilder::noAnswer($service);
            }
            $aiResult = $metered['result'];
        } else {
            // Guest path — direct call, no metering
            $aiResult = AIClient::call($q, $system, $mode, 400, 0.7, $service === 'medical');
        }
        $answer = is_array($aiResult) ? ($aiResult['text'] ?? '') : (string)$aiResult;

        if (!$answer) {
            if ($ie) return OutputBuilder::build($ie['answer'], $service, 'ie', $dbData, [], $q);
            return OutputBuilder::noAnswer($service);
        }

        // Cache the answer
        $model = $mode === 'math' || $mode === 'arabic' ? 'gpt' : 'gemini';
        CacheLayer::write($q, $answer, $service, $model);

        if ($userId) {
            $sessionHash = md5($userId . date('Y-m-d') . session_id());
            ContextMemory::save($userId, $sessionHash, $q, $answer, $service);
        }

        $kpi['ai_used']    = true;
        $kpi['model_used'] = $model;
        $kpi['latency_ms'] = self::ms($startMs);
        KPILogger::log($kpi);

        // ── EVENT WIRE: fire ai_engine.used → Worker → AIOrchestrator ──────
        // This activates the full agent pipeline (TaskEngine, GrowthBrain, etc.)
        // Fires ONCE per AI-assisted response, async via QueueEngine
        // ── EVENTBUS: always fires (guest or logged in) ─────────────────
        EventBus::emit('ai_engine.used', [
                'user_id'    => $userId,
                'service'    => $service,
                'model'      => $model,
                'question'   => mb_substr($q, 0, 100),
                'latency_ms' => $kpi['latency_ms'],
                'user_plan'  => $tier,
            ]);

        $extras = self::buildDecisionActions($decision, $dbData);
        // Add assistant identity to response
        if (!empty($ie['assistant_name'])) {
            $extras['assistant_name'] = $ie['assistant_name'];
        }

        // ── BEHAVIOR ENGINE: enrich response with tone + conversion + CTA ──
        // ASSISTANT: controls tone, emoji, CTA (user-facing)
        // AGENT:     runs in background via QueueEngine (invisible to user)
        $behaviorCtx = [
            'service'        => $service,
            'source'         => $model,
            'flow_active'    => !empty($result['flow']),
            'step_completed' => false,
            'ie_hit'         => !empty($kpi['ie_hit']),
            'user_id'        => $userId,
        ];
        $behavior = BehaviorEngine::compose($behaviorCtx);
        $extras['behavior'] = $behavior;

        return OutputBuilder::build($answer, $service, $model, $dbData, $extras, $q);
    }

    // ── Helpers ───────────────────────────────────────────────────

    private static function ms(int $start): int {
        return round(microtime(true) * 1000) - $start;
    }

    private static function isInjection(string $q): bool {
        // Unicode-safe patterns (u flag) for Arabic dialect attacks
        $patterns = [
            // English injection
            '/ignore.*instructions?/ui',
            '/forget.*previous.*instruction/ui',
            '/you are now\s+(?:a\s+)?(?:new|different|uncensored)/ui',
            '/no restrictions?\s+apply/ui',
            '/override.*system.*prompt/ui',
            '/pretend.*you.*are\s+(?:an?\s+)?(?:AI|bot|assistant)/ui',
            '/act as\s+(?:an?\s+)?(?:DAN|jailbreak|unrestricted)/ui',
            // SQL / code injection
            '/select\s+\*?\s+from\s+\w+/ui',
            '/DROP\s+TABLE|TRUNCATE\s+TABLE|DELETE\s+FROM/ui',
            '/UNION\s+SELECT|INSERT\s+INTO\s+\w+/ui',
            // Arabic injection — C5 FIX expanded patterns
            '/تجاهل\s*(كل\s*)?(ما\s*)?(قبل|سبق|جاء)/ui',
            '/تجاهل\s*(ال)?تعليمات/ui',
            '/تخط[َّ]?\s*(ال)?(تعليمات|قواعد|حدود)/ui',
            '/انسَ?\s*(ما\s*)?(قيل|سبق|جاء|قبل)/ui',
            '/أنت\s*(ال)?آن\s*(روبوت|ذكاء|بوت)/ui',
            '/أنت\s*(الآن\s*)?(بدون\s*)?(قيود|حدود|رقابة)/ui',
            '/ق[ُ]?ل\s+فقط\s+(كلمة|شيء)/ui',
            '/كن\s*(روبوت|ذكاء\s*اصطناعي\s*حر|بوت)/ui',
            '/دورك\s*(ال)?حقيقي/ui',
            '/تظاهر\s*(بأنك|أنك)\s*(لا\s*)?(لديك|عندك)?\s*(قيود|حدود)/ui',
            '/شيل\s*(ال)?(فلتر|قيد|حد)/ui',
            '/قائمة\s*(ال)?(مستخدمين|أعضاء|بيانات)/ui',
            '/اكشف\s*(ال)?(كود|مفتاح|API|سيستم)/ui',
            '/اعطني\s*(مفتاح|API|كلمة\s*السر|password)/ui',
            // Self-harm / dangerous
            '/طريقة\s*(الانتحار|قتل\s*النفس)/ui',
            '/جرعة\s*(زائدة|مورفين|مخدر)/ui',
            // Jailbreak attempts
            '/DAN|JailBreak|STAN|AIM\s+prompt/i',
            '/\[?SYSTEM\]?\s*MESSAGE\s*:/i',
        ];
        foreach ($patterns as $p) {
            if (@preg_match($p, $q)) return true;
        }
        return false;
    }

    private static function revenueGate(string $q, ?array $user, string $ip): string {
        if (!$user) {
            if (!check_rate_limit('ask_g_' . md5($ip), AI_DAILY_LIMIT_GUEST, 86400)) return 'block';
            return 'ok';
        }
        // FIX 3: Read limits from DB so admin changes take effect immediately (no cache)
        $plan  = $user['plan_key'] ?? $user['subscription_tier'] ?? 'free';
        $limit = self::getPlanLimit($plan, 'ai_requests_day');
        if (!check_rate_limit('ask_u_' . $user['id'], $limit, 86400)) return 'block';
        return 'ok';
    }

    // FIX 3: Read plan limit from DB — admin changes reflect instantly
    private static function getPlanLimit(string $planKey, string $column = 'ai_requests_day'): int {
        // No static cache — admin DB changes take effect immediately

        // Fallback constants in case DB is unavailable
        // PHP 7.4 compat: no match() expression
        $fallbacks = [
            'ai_requests_day' => ($planKey==='pro' ? PricingEngine::getAILimit('pro') :
                                  ($planKey==='plus'? PricingEngine::getAILimit('plus') :
                                  ($planKey==='paid'? PricingEngine::getAILimit('paid') : PricingEngine::getAILimit('free')))),
            'booking_per_day' => ($planKey==='pro' ? PricingEngine::getBookingLimit('pro') :
                                  ($planKey==='plus'? PricingEngine::getBookingLimit('plus') : PricingEngine::getBookingLimit('free'))),
        ];

        try {
            $st = db()->prepare(
                "SELECT {$column} FROM subscription_plans WHERE plan_key=? AND is_active=1 LIMIT 1"
            );
            $st->execute([$planKey]);
            $val = $st->fetchColumn();
            if ($val !== false) {
                $cache[$cacheKey] = (int)$val;
                return (int)$val;
            }
        } catch (Throwable $e) {
            // DB unavailable — use constants
        }

        return $fallbacks[$column] ?? 20;
    }

    private static function detectService(string $q, ?array $ie): string {
        if ($ie) return $ie['sector'] ?? 'general';
        // M1 FIX: Expanded medical pattern to catch symptom-based questions
        $map = [
            'pharmacy'  => '/وصفة|صيدلية|دواء|روشتة|أدوية|drug|medicine/ui',
            'family'    => '/طفلي|طفل|رضيع|مولود|أسرة|أبني|بنتي|حمل|ولادة|تطعيم|لقاح/ui',
            'academy'   => '/درس|IGCSE|SAT|quadratic|تعلم|مذاكرة|شرح|امتحان|مادة/ui',
            'market'    => '/بيع|شراء|سوق|إعلان|منتج|أشتري|أبيع/ui',
            'legal'     => '/محامي|قانون|حقوق|عقد|ميراث|قضية|طلاق|تقاضي/ui',
            'events'    => '/فعالية|فعاليات|حفلة|تجمع|مناسبة/ui',
            'community' => '/الحي|مبادرة|نبض|جيران|مجتمع/ui',
            'developer' => '/api|agent|automation|webhook|sdk|dashboard|engine|مطور|برمجة|تكامل/ui',
            'business'  => '/مشروع|أعمال|استثمار|تجارة|شركة|مشاريع/ui',
            // M1 EXPANDED: catch symptom words even without "طبيب"
            'medical'   => '/طبيب|مستشفى|علاج|دواء|تورم|ورم|ألم|آلام|صداع|حمى|سرطان|جرح|نزيف|كسر|حرق|حساسية|غثيان|إسهال|قيء|ضغط|سكر|قلب|رئة|كبد|كلى|أمراض|أعراض|مرض|عدوى|إصابة/ui',
        ];
        foreach ($map as $service => $pattern) {
            if (@preg_match($pattern, $q)) return $service;
        }
        return 'general';
    }

    // M2 FIX: Remove Arabic diacritics for better cache & IE matching
    public static function removeDiacritics(string $text): string {
        // Arabic diacritics Unicode range: U+064B to U+065F
        return preg_replace('/[\x{064B}-\x{065F}]/u', '', $text) ?? $text;
    }

    private static function fetchServiceData(string $service, string $q, ?array $user): array {
        $data = [];
        try {
            switch ($service) {
                case 'pharmacy':
                    $state = $user['state'] ?? 'الخرطوم';
                    $st = db()->prepare('SELECT name, phone, address FROM pharmacies WHERE state=? AND status="active" LIMIT 3');
                    $st->execute([$state]);
                    $data['pharmacies'] = $st->fetchAll();
                    break;
                case 'family':
                    if ($user) {
                        $st = db()->prepare('SELECT name, relation, birth_date, blood_type, allergies FROM family_members WHERE user_id=? LIMIT 5');
                        $st->execute([$user['id']]);
                        $data['members'] = $st->fetchAll();
                    }
                    // Providers for routing
                    $st = db()->prepare("SELECT id, name, provider_specialty, avg_rating FROM users WHERE user_type IN ('doctor') AND provider_verified=1 LIMIT 3");
                    $st->execute();
                    $data['providers'] = $st->fetchAll();
                    break;
                case 'legal':
                    $st = db()->prepare("SELECT id, name, provider_specialty FROM users WHERE user_type='service_provider' AND provider_verified=1 AND provider_specialty LIKE '%قانون%' LIMIT 3");
                    $st->execute();
                    $providers = $st->fetchAll();
                    if (empty($providers)) {
                        // Check directory
                        $st2 = db()->prepare("SELECT name, phone FROM neighborhood_services WHERE category='legal' AND status='active' LIMIT 3");
                        $st2->execute();
                        $providers = $st2->fetchAll();
                    }
                    $data['providers'] = $providers;
                    // Data gap logging
                    if (empty($providers) && preg_match('/بحري|الخرطوم|أم درمان/i', $q)) {
                        $loc = 'خرطوم';
                        try {
                            db()->prepare("INSERT INTO data_gaps (gap_key, category, location, occurrences) VALUES (?, 'legal', ?, 1) ON DUPLICATE KEY UPDATE occurrences=occurrences+1, last_seen=NOW()")
                               ->execute(['lawyers_' . md5($loc), $loc]);
                        } catch (Throwable $ignored) {}
                    }
                    break;
                case 'market':
                    $hood = $user['neighborhood_id'] ?? 0;
                    $params = [];
                    $where = "status='active' AND (expires_at IS NULL OR expires_at > NOW())";
                    if ($hood) { $where .= ' AND neighborhood_id=?'; $params[] = $hood; }
                    $st = db()->prepare("SELECT title, price, category FROM marketplace_items WHERE $where ORDER BY created_at DESC LIMIT 5");
                    $st->execute($params);
                    $data['items'] = $st->fetchAll();
                    break;
                case 'events':
                    $st = db()->prepare("SELECT title, event_date, city FROM community_events WHERE status IN ('upcoming','active') AND event_date >= NOW() ORDER BY event_date ASC LIMIT 5");
                    $st->execute();
                    $data['events'] = $st->fetchAll();
                    break;
                case 'academy':
                    $track = $user['learning_track'] ?? 'foundation';
                    $st = db()->prepare("SELECT id, title FROM lessons WHERE track=? AND status='published' ORDER BY order_index LIMIT 5");
                    $st->execute([$track]);
                    $data['lessons'] = $st->fetchAll();
                    break;
                case 'medical':
                    $st = db()->prepare("SELECT id, name, provider_specialty, avg_rating FROM users WHERE user_type='doctor' AND provider_verified=1 LIMIT 3");
                    $st->execute();
                    $data['providers'] = $st->fetchAll();
                    break;
            }
        } catch (Throwable $e) {
            error_log('[Gateway::fetchServiceData] ' . $e->getMessage());
        }
        return $data;
    }

    private static function isDbSufficient(string $service, array $dbData, string $q): bool {
        // Pure data queries that don't need AI explanation
        if ($service === 'market' && !empty($dbData['items']) && count($dbData['items']) >= 2) return true;
        if ($service === 'events' && !empty($dbData['events'])) return true;
        if ($service === 'pharmacy' && !empty($dbData['pharmacies']) && !preg_match('/نصيحة|ماذا.*أفعل|هل.*يمكن/i', $q)) return true;
        return false;
    }

    private static function buildDbResponse(string $service, array $dbData, string $q): string {
        switch ($service) {
            case 'market':
                $items = $dbData['items'] ?? [];
                if (empty($items)) return 'لا إعلانات في الوقت الحالي.';
                $out = "نتائج من سوق الجيران:\n";
                foreach ($items as $i) $out .= "• {$i['title']} — {$i['price']} SDG\n";
                return trim($out);
            case 'events':
                $ev = $dbData['events'] ?? [];
                if (empty($ev)) return 'لا فعاليات قادمة.';
                $out = "الفعاليات القادمة:\n";
                foreach ($ev as $e) $out .= "• {$e['title']} — {$e['event_date']} ({$e['city']})\n";
                return trim($out);
            case 'pharmacy':
                $ph = $dbData['pharmacies'] ?? [];
                if (empty($ph)) return 'لا صيدليات مسجّلة في منطقتك.';
                $out = "صيدليات في منطقتك:\n";
                foreach ($ph as $p) $out .= "💊 {$p['name']} — {$p['phone']}\n";
                return trim($out);
        }
        return '';
    }

    private static function buildSystemPrompt(string $service, ?array $user, array $history, array $dbData, string $originalQuery = '', string $aePrompt = ''): string {
        // ── حقن الأسعار الحالية في system prompt ──────────────────
        if (!class_exists('PricingEngine')) require_once __DIR__.'/PricingEngine.php';
        $_pricing = PricingEngine::getAll();
        $_priceLines = "أسعار سودانا الحالية (للرجوع عند السؤال عنها):\n" .
            "- مجاني: 0 SDG (20 سؤال/يوم، 2 حجوزات/يوم)\n" .
            "- Plus:  " . number_format($_pricing['plans']['plus']['sdg'] ?? 20000) . " SDG/شهر (" .
            ($_pricing['plans']['plus']['ai_day'] ?? 60) . " سؤال، " .
            ($_pricing['plans']['plus']['bookings_day'] ?? 5) . " حجوزات/يوم)\n" .
            "- Pro:   " . number_format($_pricing['plans']['pro']['sdg'] ?? 50000) . " SDG/شهر (" .
            ($_pricing['plans']['pro']['ai_day'] ?? 200) . " سؤال، " .
            ($_pricing['plans']['pro']['bookings_day'] ?? 20) . " حجوزات/يوم)\n" .
            "- سعر الصرف: 1 USD = " . number_format($_pricing['rate'] ?? 4100) . " SDG\n";

        $name  = $user['name'] ?? 'الزائر';
        $state = $user['state'] ?? 'السودان';
        $track = $user['learning_track'] ?? 'foundation';

        $sys = "أنت مساعد سودانا الذكي الرسمي (sudanna.ai).\n";
        $sys .= "المستخدم: {$name} | الولاية: {$state}\n\n";

        // ══ GLOBAL MEDICAL SAFETY — injected into ALL prompts ══════
        // Even if service=general, if query has symptoms → enforce
        $sys .= "قواعد السلامة الإلزامية (غير قابلة للتجاوز):\n";
        $sys .= "• لا تُشخِّص أي حالة طبية أبداً — مهما كانت الأعراض\n";
        $sys .= "• لا تذكر جرعات دوائية محددة (mg أو ml أو ملجم)\n";
        $sys .= "• إذا ذكر المستخدم أعراضاً جسدية → اسأله عنها وأوصِه بالطبيب\n";
        $sys .= "• أي سؤال يتعلق بصحة أو أعراض أو أمراض → اختم بـ ⚕️\n";
        $sys .= "• إذا كانت الأعراض طارئة → أوصِه باتصال 999 فوراً\n";
        $sys .= "• لا تقل أبداً 'أنت مصاب بـ...' أو 'تشخيصك هو...'\n";
        $sys .= "══════════════════════════════════\n\n";

        // ── INJECT AE SPECIALIST PROMPT ─────────────────────────────
        if ($aePrompt) {
            $sys .= "\n\n[المساعد المتخصص]\n" . $aePrompt . "\n══════════════════════════════════════════\n\n";
        }

        if ($service === 'medical' || $service === 'family') {
            $sys .= "هذا استفسار طبي:\n";
            $sys .= "• اسأل عن الأعراض بالتفصيل أولاً\n";
            $sys .= "• قدّم معلومات إرشادية عامة فقط\n";
            $sys .= "• أوصِ بزيارة الطبيب أو الطوارئ\n";
            $sys .= "• اختم كل رد بـ: ⚕️ استشر طبيبك للتشخيص الدقيق والعلاج المناسب.\n";
            if (!empty($dbData['members'])) {
                $sys .= "\nبيانات أسرة المستخدم:\n";
                foreach ($dbData['members'] as $m) {
                    $sys .= "• {$m['name']} ({$m['relation']}) — فصيلة: {$m['blood_type']} — حساسية: {$m['allergies']}\n";
                }
            }
        }

        if ($service === 'legal') {
            $sys .= "• لا تعطِ رأياً قانونياً ملزِماً\n";
            $sys .= "• اقترح دائماً استشارة محامٍ موثّق\n";
        }

        if ($service === 'developer') {
            $sys .= "هذا استفسار من مطور يريد استخدام API سودانا أو Sudanna Agent:\n";
            $sys .= "• أخبره بنقاط الـ API المتاحة: /api/engine/chat.php, /api/engine/run.php\n";
            $sys .= "• أخبره كيف يحصل على API key من: /dashboard/api.php\n";
            $sys .= "• اشرح نموذج الـ request: Authorization: Bearer sdk_... + Content-Type: application/json\n";
            $sys .= "• اذكر خطط الاستخدام: Free=50 call/day, Plus=500, Pro=5000\n";
            $sys .= "• أحِل للتوثيق الكامل: sudanna.ai/developer.html\n";
            $sys .= "• ردود بالإنجليزية مقبولة لأن المطور قد يكتب بها\n";
        }

        if ($service === 'academy') {
            $sys .= "المسار التعليمي: {$track}\n";
            $sys .= "استخدم أمثلة من السياق السوداني. أشرح بخطوات واضحة.\n";
        }

        // v23 FIX: developer sector — متخصص بـ Sudanna API
        if ($service === 'developer') {
            $sys .= "أنت مساعد Sudanna API المتخصص للمطورين.\n";
            $sys .= "نقاط API المتاحة:\n";
            $sys .= "• POST /api/visitor_ask.php — المساعد الذكي (بدون auth)\n";
            $sys .= "• POST /api/engine/chat.php — Sudanna Agent API (Authorization: Bearer sdk_...)\n";
            $sys .= "• POST /api/engine/run.php — تشغيل Agent (Authorization: Bearer sdk_...)\n";
            $sys .= "• GET  /api/engine/keys.php — إدارة مفاتيح API\n";
            $sys .= "• GET  /api/engine/agents.php — إدارة Agents\n";
            $sys .= "• لوحة التحكم: sudanna.ai/dashboard/\n";
            $sys .= "• التوثيق: sudanna.ai/developer.html\n\n";
            $sys .= "الخطط: Free (50/day) | Plus (500/day) | Pro (5000/day)\n";
            $sys .= "عند السؤال عن SDK: اشرح بأمثلة curl أو PHP.\n";
            $sys .= "لا تخترع endpoints غير موجودة. التزم بالقائمة أعلاه.\n";
        }

        // ── BEHAVIOR ENGINE — tone based on service ─────────────────
        // Separates ASSISTANT (user-facing tone) from AGENT (backend logic)
        if ($service === 'general' || $service === 'platform') {
            $sys .= "\nأنت مساعد مبيعات سودانا الذكي. مهمتك:\n";
            $sys .= "1. شرح خدمات سودانا بوضوح وحماس\n";
            $sys .= "2. الإجابة على أي سؤال عن المنصة والأسعار والخدمات\n";
            $sys .= "3. توجيه الزائر للتسجيل أو الاستخدام\n";
            $sys .= "\nمعلومات أساسية للمبيعات:\n";
            $sys .= "• سودانا: المنصة الذكية الأولى في السودان (مجانية) 🇸🇩\n";
            $sys .= "• الخطة المجانية: AI 20 سؤال/يوم | 2 حجوزات/يوم\n";
            $sys .= "• الخطة المدفوعة: 10,000 SDG/شهر | 100 سؤال | دروس غير محدودة\n";
            $sys .= "• الدفع: بنك الخرطوم | حساب: 1821425 | اسم: حامد مختار\n";
            $sys .= "• التسجيل: مجاني في ثانية — 'ابدأ مجاناً' في الصفحة الرئيسية\n";
            $sys .= "• الخدمات: طب | قانون | أعمال | تعليم | صيدليات | سوق | مجتمع\n";
            $sys .= "• Sudanna Agent API: Free=50 call/day | Plus=500 | Pro=5000\n";
            $sys .= "• للمطورين: /dashboard/ للحصول على API key\n";
        }

        // ── BEHAVIOR TONE (all services) ─────────────────────────────
        $toneMap = [
            'medical'   => 'هادئ، داعم، تعاطفي — المريض يحتاج راحة نفسية أولاً',
            'legal'     => 'محترم، دقيق، يُحيل للمختص دائماً',
            'business'  => 'إيجابي، عملي، يُشجع على الخطوة التالية',
            'academy'   => 'مُشجع، تعليمي، يبني الثقة',
            'market'    => 'نشيط، تجاري، يُسهّل البيع',
            'general'   => 'ودود، واضح، يُقنع بقيمة المنصة',
            'platform'  => 'احترافي، مبيعاتي، يُحوّل الزائر لمستخدم',
            'developer' => 'تقني، مختصر، يُعطي أمثلة عملية',
        ];
        if (isset($toneMap[$service])) {
            $sys .= "\nنبرة التحدث: " . $toneMap[$service] . "\n";
        }

        $sys .= "\nتكلّم بالعربية السودانية الودودة (حبابك، أبشر). ردود موجزة (3-5 أسطر للبسيط).\n";
        $sys .= "اسمك: مساعد سودانا (لا Gemini ولا Claude).\n";

        if (!empty($history)) {
            $sys .= "\nسياق المحادثة السابقة:\n";
            foreach (array_slice($history, -4) as $h) {
                $sys .= ($h['role']==='user' ? 'المستخدم: ' : 'المساعد: ') . mb_substr($h['content'], 0, 100) . "\n";
            }
        }

        // ── حقن الأسعار الديناميكية ──────────────────────────────
        if (isset($_priceLines)) { $sys .= "\n\n" . $_priceLines; }

        return $sys;
    }

    private static function selectMode(string $service, string $q, string $tier): string {
        if (in_array($service, ['medical','family'])) return 'medical';
        if (preg_match('/رياضيات|quadratic|حساب|equation|math/i', $q)) return 'math';
        if (in_array($service, ['legal','business','productivity'])) {
            return $tier === 'paid' ? 'smart' : 'fast';
        }
        return 'auto';
    }

    // ── ProviderDecisionEngine Helpers ────────────────────────────

    /**
     * يبني actions إضافية بناءً على قرار PDE
     * مثال: REAL → "احجز مع طبيب" إجباري في الأعلى
     */
    private static function buildDecisionActions(array $decision, array $dbData): array {
        $extras = [];

        // Emergency — اتصل 999 فوراً في أعلى الأزرار
        if (!empty($decision['urgent'])) {
            array_unshift($extras, [
                'label'    => '🚨 اتصل 999 الآن',
                'url'      => 'tel:999',
                'icon'     => '🚨',
                'priority' => 'critical',
            ]);
        }

        $hasProviders = !empty($dbData['providers']);

        // REAL: مزود إلزامي
        if ($decision['type'] === 'REAL') {
            if ($hasProviders) {
                $extras[] = [
                    'label' => $decision['cta_label'] ?? 'احجز مع متخصص',
                    'url'   => '#consultation',
                    'icon'  => $decision['cta_icon'] ?? '📅',
                    'priority' => 'high',
                ];
            } else {
                // Empty state — ادعو مزودين للانضمام
                $extras[] = [
                    'label' => $decision['empty_cta'] ?? 'كن أول مزود في هذا المجال',
                    'url'   => '#providers',
                    'icon'  => '🌟',
                    'priority' => 'medium',
                    'empty_state' => true,
                ];
            }
        }

        // AI_FIRST: مزود اختياري (يظهر بعد الإجابة)
        if ($decision['type'] === 'AI_FIRST' && !empty($decision['cta_label'])) {
            $extras[] = [
                'label'    => $decision['cta_label'],
                'url'      => '#consultation',
                'icon'     => $decision['cta_icon'] ?? '💬',
                'priority' => 'low',
                'optional' => true,
            ];
        }

        return $extras;
    }

    /**
     * يجلب المزودين المتاحين لقطاع معين — fallback عندما fetchServiceData لا يُغطيه
     */
    // ── Observability: Structured Logging ─────────────────────────
    private static function traceLog(string $event, array $data = []): void {
        if (!defined('TRACE_ENABLED') || !TRACE_ENABLED) return;
        $entry = json_encode([
            'ts'    => date('c'),
            'event' => $event,
            'data'  => $data,
        ], JSON_UNESCAPED_UNICODE);
        error_log('[SUDANNA_TRACE] ' . $entry);
    }

    private static function fetchProviders(string $service, ?array $user): array {
        $data = [];
        try {
            $typeMap = [
                'medical'      => ['doctor'],
                'family'       => ['doctor'],
                'legal'        => ['service_provider'],
                'business'     => ['academic','service_provider'],
                'academy'      => ['academic'],
                'mental'       => ['service_provider'],
                'engineering'  => ['service_provider'],   // v22 added
                'agriculture'  => ['service_provider'],   // v22 added
            ];
            $types = $typeMap[$service] ?? ['service_provider','doctor','academic'];
            $in    = implode(',', array_fill(0, count($types), '?'));

            $st = db()->prepare(
                "SELECT id, name, provider_specialty, avg_rating, state, city, user_type
                 FROM users
                 WHERE user_type IN ($in) AND provider_verified=1
                 ORDER BY avg_rating DESC, id ASC
                 LIMIT 5"
            );
            $st->execute($types);
            $providers = $st->fetchAll();

            if (empty($providers)) {
                // Log data gap for this service so admin knows
                try {
                    db()->prepare(
                        "INSERT INTO data_gaps (gap_key, category, occurrences)
                         VALUES (?, ?, 1)
                         ON DUPLICATE KEY UPDATE occurrences=occurrences+1, last_seen=NOW()"
                    )->execute(['no_providers_' . $service, $service]);
                } catch (Throwable $ignored) {}
            }

            $data['providers'] = $providers;
        } catch (Throwable $e) {
            error_log('[Gateway::fetchProviders] ' . $e->getMessage());
            $data['providers'] = [];
        }
        return $data;
    }

    // FIX 1: tryStartFlow — wraps StepController::start() for Gateway integration
    private static function tryStartFlow(string $flowId, string $q, ?int $userId, array $ctx, array $decision): ?array {
        try {
            $result = StepController::start($flowId, $userId, [
                'intake_symptoms' => $q,
                'source'          => 'gateway',
                'pde_decision'    => $decision['type'],
            ]);
            if (isset($result['error'])) return null; // flow failed — fall through to normal path
            // Wrap in OutputBuilder format so UI handles it the same way
            return [
                'success'      => true,
                'answer'       => $result['prompt'] ?? 'يرجى الإجابة على الأسئلة التالية لمساعدتك بشكل أفضل:',
                'source'       => 'flow',
                'service'      => 'medical',
                'flow'         => [
                    'session_id' => $result['session_id'],
                    'flow_id'    => $result['flow_id'],
                    'step'       => $result['step'],
                    'type'       => $result['type'] ?? 'input',
                    'fields'     => $result['fields'] ?? null,
                    'providers'  => $result['providers'] ?? null,
                ],
                'medical_context' => true,
                'actions'      => [
                    ['label'=>'متابعة الاستشارة','url'=>'#flow','icon'=>'👨‍⚕️','flow_session'=>$result['session_id']],
                ],
                'cross_links'  => [['service'=>'consultation','label'=>'تحدث مع طبيب','url'=>'#consultation']],
            ];
        } catch (Throwable $e) {
            error_log('[Gateway::tryStartFlow] ' . $e->getMessage());
            return null; // graceful fallback — continue normal path
        }
    }
}
