<?php

declare(strict_types=1);

namespace App\Services;

/**
 * Phase 10.5.2
 * Alert Explainability & Executive Recommendations
 *
 * This class converts Executive Summary KPI rows into deterministic executive
 * actions.  It does not query Oracle directly; it only evaluates already cached
 * summary values, so Action Center stays fast.
 */
final class ExecutiveAlertRuleEngine
{
    /** @var string */
    private $hospitalCode;

    /** @var array<string,float|int> */
    private $thresholds;

    public function __construct(string $hospitalCode)
    {
        $this->hospitalCode = strtolower(trim($hospitalCode)) ?: 'njch';
        $this->thresholds = [
            'occupancy_watch' => $this->floatEnv('EXEC_ALERT_OCCUPANCY_WATCH', 85),
            'occupancy_critical' => $this->floatEnv('EXEC_ALERT_OCCUPANCY_CRITICAL', 95),
            'available_beds_watch' => $this->intEnv('EXEC_ALERT_AVAILABLE_BEDS_WATCH', 15),
            'available_beds_critical' => $this->intEnv('EXEC_ALERT_AVAILABLE_BEDS_CRITICAL', 5),
            'er_watch' => $this->intEnv('EXEC_ALERT_ER_WATCH', 35),
            'er_critical' => $this->intEnv('EXEC_ALERT_ER_CRITICAL', 60),
            'admission_discharge_gap_watch' => $this->intEnv('EXEC_ALERT_ADMISSION_DISCHARGE_GAP_WATCH', 8),
            'or_gap_watch' => $this->intEnv('EXEC_ALERT_OR_GAP_WATCH', 1),
            'blocked_beds_watch' => $this->intEnv('EXEC_ALERT_BLOCKED_BEDS_WATCH', 10),
            'low_occupancy_opportunity' => $this->floatEnv('EXEC_ALERT_LOW_OCCUPANCY_OPPORTUNITY', 50),
            'revenue_zero_activity_min_flow' => $this->intEnv('EXEC_ALERT_REVENUE_ZERO_ACTIVITY_MIN_FLOW', 1),
            'revenue_pace_ratio_watch' => $this->floatEnv('EXEC_ALERT_REVENUE_PACE_RATIO_WATCH', 0.25),
            'revenue_pace_after_hour' => $this->intEnv('EXEC_ALERT_REVENUE_PACE_AFTER_HOUR', 10),
            'revenue_pace_min_elapsed_hours' => $this->floatEnv('EXEC_ALERT_REVENUE_PACE_MIN_ELAPSED_HOURS', 6),
            'revenue_pace_min_flow' => $this->intEnv('EXEC_ALERT_REVENUE_PACE_MIN_FLOW', 50),
            'revenue_pace_time_weight_floor' => $this->floatEnv('EXEC_ALERT_REVENUE_PACE_TIME_WEIGHT_FLOOR', 0.12),
        ];
    }

    /** @return array<string,float|int> */
    public function thresholds(): array
    {
        return $this->thresholds;
    }

    /**
     * @param array<string,array<string,mixed>> $rows
     * @param array<string,mixed> $m
     * @param array<string,mixed> $operational
     * @return array<int,array<string,mixed>>
     */
    public function evaluate(array $rows, array $m, bool $summaryAvailable, bool $isStale, $error, array $operational = []): array
    {
        $actions = [];

        if (!$summaryAvailable) {
            $this->add($actions, 'summary_missing', 'critical', 'system', 'Executive summary unavailable', 'طبقة الملخص التنفيذي غير متاحة', 'The dashboard cannot read today KPIs from DASH_EXEC_SUMMARY_KPI. Run the refresh job and check the summary table.', 'لا يمكن قراءة مؤشرات اليوم من جدول الملخص التنفيذي. شغّل مهمة التحديث وافحص الجدول.', 'Open summary check', 'فحص الملخص', 'public', 'executive-summary-check.php?hospital=' . rawurlencode($this->hospitalCode), 5, 'Summary', 'summary_available=NO', 'must be YES');
        }

        if ($isStale) {
            $this->add($actions, 'summary_stale', 'critical', 'system', 'Refresh summary now', 'تحديث الملخص الآن', 'Summary data is older than the configured freshness window. Trigger the scheduled refresh task or run the manual refresh URL.', 'بيانات الملخص أقدم من المدة المسموحة. شغّل مهمة التحديث أو رابط التحديث اليدوي.', 'Refresh status', 'حالة التحديث', 'public', 'executive-summary-refresh-status.php?hospital=' . rawurlencode($this->hospitalCode), 10, 'Stale', 'summary_stale=YES', 'must be NO');
        }

        if ($error) {
            $this->add($actions, 'action_center_error', 'critical', 'system', 'Review Action Center error', 'مراجعة خطأ مركز الإجراءات', (string)$error, (string)$error, 'Open logs', 'فتح الحالة', 'public', 'executive-summary-refresh-status.php?hospital=' . rawurlencode($this->hospitalCode), 15, 'Error', 'exception', 'none');
        }

        $occupancy = $this->num($m['occupancy_rate'] ?? null);
        $available = $this->num($m['available_beds'] ?? null);
        $totalBeds = $this->num($m['total_beds'] ?? null);
        $blocked = $this->num($m['blocked_beds'] ?? null);

        if ($totalBeds !== null && $available !== null) {
            if ($available <= $this->thresholds['available_beds_critical'] || ($occupancy !== null && $occupancy >= $this->thresholds['occupancy_critical'])) {
                $this->add($actions, 'capacity_critical', 'critical', 'capacity', 'Capacity risk', 'خطر في السعة', 'Available beds are very low or occupancy is critical. Review blocked beds, expected discharges and bed turnaround immediately.', 'الأسرة المتاحة منخفضة جدًا أو الإشغال حرج. راجع الأسرة المحجوزة والخروج المتوقع ودورة تجهيز الأسرة فورًا.', 'Review capacity', 'مراجعة السعة', 'page', 'beds', 20, $available . ' beds', 'available=' . $available . ', occupancy=' . $this->fmt($occupancy) . '%', 'available<=' . $this->thresholds['available_beds_critical'] . ' OR occupancy>=' . $this->thresholds['occupancy_critical']);
            } elseif ($available <= $this->thresholds['available_beds_watch'] || ($occupancy !== null && $occupancy >= $this->thresholds['occupancy_watch'])) {
                $this->add($actions, 'capacity_watch', 'watch', 'capacity', 'Monitor bed capacity', 'متابعة سعة الأسرة', 'Occupancy is moving toward a pressure zone. Confirm expected discharges and blocked beds.', 'الإشغال يقترب من منطقة ضغط. راجع الخروج المتوقع والأسرة المحجوزة.', 'Open beds', 'فتح الأسرة', 'page', 'beds', 40, $occupancy !== null ? number_format($occupancy, 0) . '%' : $available . ' beds', 'available=' . $available . ', occupancy=' . $this->fmt($occupancy) . '%', 'available<=' . $this->thresholds['available_beds_watch'] . ' OR occupancy>=' . $this->thresholds['occupancy_watch']);
            } elseif ($occupancy !== null && $occupancy <= $this->thresholds['low_occupancy_opportunity']) {
                $this->add($actions, 'capacity_opportunity', 'opportunity', 'capacity', 'Capacity available', 'سعة متاحة', 'Occupancy is comfortable. Consider reviewing admission conversion, OR scheduling and referral flow.', 'الإشغال مريح. يمكن مراجعة تحويلات الدخول وجدولة العمليات وتدفق الإحالات.', 'Review patient flow', 'مراجعة التدفق', 'page', 'visits', 80, number_format($occupancy, 0) . '%', 'occupancy=' . $this->fmt($occupancy) . '%', 'occupancy<=' . $this->thresholds['low_occupancy_opportunity']);
            }
        }

        if ($blocked !== null && $blocked >= $this->thresholds['blocked_beds_watch']) {
            $this->add($actions, 'blocked_beds_watch', 'watch', 'capacity', 'Review blocked beds', 'مراجعة الأسرة المحجوزة', 'Blocked beds are above the configured review threshold. Check block reasons and turnaround actions.', 'الأسرة المحجوزة أعلى من حد المراجعة. راجع أسباب الحجز وإجراءات التجهيز.', 'Open beds', 'فتح الأسرة', 'page', 'beds', 42, (string)(int)$blocked, 'blocked=' . $blocked, 'blocked>=' . $this->thresholds['blocked_beds_watch']);
        }

        $er = $this->num($m['er'] ?? null);
        if ($er !== null) {
            if ($er >= $this->thresholds['er_critical']) {
                $this->add($actions, 'er_critical', 'critical', 'flow', 'High ER pressure', 'ضغط طوارئ مرتفع', 'ER volume is high for the operational day. Review triage, bed requests and physician coverage.', 'حجم الطوارئ مرتفع خلال اليوم التشغيلي. راجع الفرز وطلبات الأسرة وتغطية الأطباء.', 'Open visits', 'فتح الزيارات', 'page', 'visits', 25, (string)(int)$er, 'er=' . $er, 'er>=' . $this->thresholds['er_critical']);
            } elseif ($er >= $this->thresholds['er_watch']) {
                $this->add($actions, 'er_watch', 'watch', 'flow', 'Watch ER pressure', 'متابعة ضغط الطوارئ', 'ER activity requires monitoring. Keep capacity and discharge readiness visible.', 'نشاط الطوارئ يحتاج متابعة. أبقِ السعة والاستعداد للخروج واضحين.', 'Open visits', 'فتح الزيارات', 'page', 'visits', 45, (string)(int)$er, 'er=' . $er, 'er>=' . $this->thresholds['er_watch']);
            }
        }

        $admissions = $this->num($m['admissions'] ?? null);
        $discharges = $this->num($m['discharges'] ?? null);
        if ($admissions !== null && $discharges !== null && ($admissions - $discharges) > $this->thresholds['admission_discharge_gap_watch']) {
            $this->add($actions, 'admission_discharge_gap', 'watch', 'flow', 'Admissions exceed discharges', 'الدخول أعلى من الخروج', 'Admissions are outpacing discharges. Review discharge list and bed readiness.', 'الدخول أعلى من الخروج. راجع قائمة الخروج وجاهزية الأسرة.', 'Review flow', 'مراجعة التدفق', 'page', 'visits', 48, ((int)$admissions) . ' / ' . ((int)$discharges), 'gap=' . ($admissions - $discharges), 'gap>' . $this->thresholds['admission_discharge_gap_watch']);
        }

        $scheduled = $this->num($m['scheduled_operations'] ?? null);
        $performed = $this->num($m['performed_operations'] ?? null);
        if ($scheduled !== null && $performed !== null && ($scheduled - $performed) >= $this->thresholds['or_gap_watch'] && $scheduled > 0) {
            $this->add($actions, 'or_gap_watch', 'watch', 'operations', 'OR execution gap', 'فجوة تنفيذ العمليات', 'Scheduled operations exceed performed operations. Review OR delays and cancellations.', 'العمليات المجدولة أعلى من المنفذة. راجع التأخيرات والإلغاءات.', 'Open visits', 'فتح المتابعة', 'page', 'visits', 55, ((int)$performed) . '/' . ((int)$scheduled), 'gap=' . ($scheduled - $performed), 'gap>=' . $this->thresholds['or_gap_watch']);
        }

        $todayRevenue = $this->num($m['today_total'] ?? null);
        $monthRevenue = $this->num($m['month_total'] ?? null);
        $opd = $this->num($m['opd'] ?? null);
        $patientFlow = (float)($opd ?: 0) + (float)($er ?: 0) + (float)($admissions ?: 0);
        if ($todayRevenue !== null && $todayRevenue <= 0 && $patientFlow >= $this->thresholds['revenue_zero_activity_min_flow']) {
            $this->add($actions, 'revenue_zero_with_activity', 'watch', 'finance', 'Revenue looks low', 'إيراد اليوم منخفض', 'Clinical activity exists but today revenue is zero. Review finance date alignment and revenue batch refresh.', 'يوجد نشاط سريري لكن إيراد اليوم صفر. راجع توافق التاريخ المالي وتحديث batch الإيرادات.', 'Open finance check', 'فحص المالي', 'public', 'operational-finance-check.php?hospital=' . rawurlencode($this->hospitalCode), 50, 'SAR 0', 'flow=' . $patientFlow . ', revenue=0', 'flow>=' . $this->thresholds['revenue_zero_activity_min_flow']);
        } elseif ($todayRevenue !== null && $monthRevenue !== null && $monthRevenue > 0) {
            $pace = $this->revenuePaceCalibration([
                'today_total' => $todayRevenue,
                'month_total' => $monthRevenue,
                'patient_flow' => $patientFlow,
            ], $operational);

            if (!empty($pace['check_due']) && $patientFlow >= $this->thresholds['revenue_pace_min_flow'] && $todayRevenue < (float)$pace['watch_floor']) {
                $this->add($actions, 'revenue_below_time_adjusted_pace', 'watch', 'finance', 'Revenue below time-adjusted pace', 'الإيراد أقل من الوتيرة الزمنية', 'Today revenue is below the time-adjusted expected pace for the elapsed operational hours. Review finance posting and current operational collections.', 'إيراد اليوم أقل من الوتيرة المتوقعة حسب الساعات المنقضية من اليوم التشغيلي. راجع الترحيل المالي والتحصيل الحالي.', 'Open revenue', 'فتح الإيرادات', 'page', 'revenue', 58, 'SAR ' . $this->compactMoney($todayRevenue), 'today=' . $this->compactMoney($todayRevenue) . ', expected_now=' . $this->compactMoney($pace['time_adjusted_expected']) . ', elapsed=' . number_format((float)$pace['elapsed_hours'], 1) . 'h', 'today < expected_now × ' . $this->thresholds['revenue_pace_ratio_watch'] . ' after ' . $this->thresholds['revenue_pace_min_elapsed_hours'] . 'h');
            }
        }

        foreach (['total_beds', 'occupied_beds', 'available_beds', 'occupancy_rate'] as $code) {
            if ($this->status($rows, $code) === 'pending') {
                $this->add($actions, 'beds_metric_pending', 'critical', 'data', 'Beds metric pending', 'مؤشر أسرة غير مكتمل', 'One or more bed summary metrics are still pending. Run the bed source diagnostic.', 'واحد أو أكثر من مؤشرات الأسرة ما زال غير مكتمل. شغّل تشخيص مصدر الأسرة.', 'Beds diagnostic', 'تشخيص الأسرة', 'public', 'beds-summary-source-check.php?hospital=' . rawurlencode($this->hospitalCode), 60, $code, $code . '=pending', 'all bed summary metrics must be ok');
                break;
            }
        }

        $this->add($actions, 'daily_revenue_review', 'routine', 'finance', 'Review daily revenue', 'مراجعة إيراد اليوم', 'Open the revenue dashboard to compare today, month, cash, insurance, MOH and net revenue.', 'افتح لوحة الإيرادات لمقارنة اليوم والشهر والنقدي والتأمين ووزارة الصحة والصافي.', 'Open revenue', 'فتح الإيرادات', 'page', 'revenue', 120, $todayRevenue !== null ? 'SAR ' . $this->compactMoney($todayRevenue) : 'Revenue', 'routine', 'daily leadership review');
        $this->add($actions, 'refresh_status_review', 'routine', 'system', 'Refresh Executive Summary', 'تحديث الملخص التنفيذي', 'Use the scheduled task status page to confirm automatic refresh is running and fresh.', 'استخدم صفحة حالة التحديث للتأكد أن التحديث التلقائي يعمل والبيانات حديثة.', 'Refresh status', 'حالة التحديث', 'public', 'executive-summary-refresh-status.php?hospital=' . rawurlencode($this->hospitalCode), 130, 'Auto', 'routine', 'scheduler should be healthy');
        $this->add($actions, 'top_specialties_review', 'routine', 'clinical', 'Review top specialties', 'مراجعة أعلى التخصصات', 'Review the highest revenue specialties and top doctor contribution from the clinical activity panel.', 'راجع أعلى التخصصات وإسهام أعلى طبيب من لوحة النشاط السريري.', 'Open doctors', 'فتح الأطباء', 'page', 'doctors', 140, 'Top 5', 'routine', 'daily clinical review');

        usort($actions, function ($a, $b) {
            return ((int)($a['sort'] ?? 999)) <=> ((int)($b['sort'] ?? 999));
        });

        return array_slice($actions, 0, 14);
    }

    /** @param array<string,array<string,mixed>> $rows */
    private function status(array $rows, string $code): string
    {
        $key = strtolower($code);
        if (!isset($rows[$key]) || !is_array($rows[$key])) { return 'missing'; }
        return strtolower((string)($rows[$key]['kpi_status'] ?? '')) ?: 'unknown';
    }

    /** @param array<int,array<string,mixed>> $actions */
    private function add(array &$actions, string $ruleId, string $severity, string $category, string $title, string $titleAr, string $text, string $textAr, string $label, string $labelAr, string $linkType, string $target, int $sort, string $metric, string $observed = '', string $threshold = ''): void
    {
        $action = [
            'rule_id' => $ruleId,
            'severity' => $severity,
            'category' => $category,
            'title' => $title,
            'title_ar' => $titleAr,
            'text' => $text,
            'text_ar' => $textAr,
            'link_label' => $label,
            'link_label_ar' => $labelAr,
            'link_type' => $linkType,
            'target' => $target,
            'sort' => $sort,
            'metric' => $metric,
            'observed' => $observed,
            'threshold' => $threshold,
        ];

        $actions[] = $this->withExplainability($action);
    }

    /**
     * Phase 10.5.2
     * Add executive-readable why/recommendation/context metadata to every rule.
     * These fields are deterministic and based only on the triggered rule,
     * observed value and threshold text already calculated by the engine.
     *
     * @param array<string,mixed> $a
     * @return array<string,mixed>
     */
    private function withExplainability(array $a): array
    {
        $rule = (string)($a['rule_id'] ?? '');
        $sev = (string)($a['severity'] ?? 'routine');
        $observed = (string)($a['observed'] ?? '');
        $threshold = (string)($a['threshold'] ?? '');

        $confidence = 'Confirmed';
        $confidenceAr = 'مؤكد';
        if ($sev === 'routine') { $confidence = 'Routine'; $confidenceAr = 'روتيني'; }
        elseif ($sev === 'opportunity') { $confidence = 'Opportunity'; $confidenceAr = 'فرصة'; }
        elseif ($sev === 'watch') { $confidence = 'Monitor'; $confidenceAr = 'متابعة'; }

        $why = 'This action was generated from the current executive summary values and the configured rule thresholds.';
        $whyAr = 'تم إنشاء هذا الإجراء من قيم الملخص التنفيذي الحالية وحدود القواعد المحددة.';
        $recommendation = 'Open the source dashboard, validate the number, and use the result in the leadership huddle.';
        $recommendationAr = 'افتح لوحة المصدر، تحقق من الرقم، واستخدم النتيجة في اجتماع القيادة.';
        $review = 'Source dashboard';
        $reviewAr = 'لوحة المصدر';

        if ($rule === 'summary_missing') {
            $confidence = 'Blocking'; $confidenceAr = 'مانع';
            $why = 'The Action Center cannot read DASH_EXEC_SUMMARY_KPI, so KPIs may be unavailable.';
            $whyAr = 'مركز الإجراءات لا يستطيع قراءة جدول الملخص التنفيذي، لذلك قد تكون المؤشرات غير متاحة.';
            $recommendation = 'Run the refresh job, then open Summary Check to confirm rows are present.';
            $recommendationAr = 'شغّل مهمة التحديث ثم افتح فحص الملخص للتأكد من وجود الصفوف.';
            $review = 'Summary Check'; $reviewAr = 'فحص الملخص';
        } elseif ($rule === 'summary_stale') {
            $confidence = 'Blocking'; $confidenceAr = 'مانع';
            $why = 'The last summary update is older than the allowed freshness window.';
            $whyAr = 'آخر تحديث للملخص أقدم من نافذة الحداثة المسموحة.';
            $recommendation = 'Check scheduler health and force refresh if the last run failed.';
            $recommendationAr = 'افحص حالة المجدول وشغّل تحديثًا إجباريًا إذا فشل آخر تشغيل.';
            $review = 'Refresh Status'; $reviewAr = 'حالة التحديث';
        } elseif ($rule === 'capacity_critical') {
            $why = 'Available beds or occupancy crossed the critical capacity threshold.';
            $whyAr = 'الأسرة المتاحة أو نسبة الإشغال تجاوزت حد السعة الحرج.';
            $recommendation = 'Review discharges, blocked beds and bed turnaround immediately.';
            $recommendationAr = 'راجع الخروج والأسرة المحجوزة ودورة تجهيز الأسرة فورًا.';
            $review = 'Beds Dashboard'; $reviewAr = 'لوحة الأسرة';
        } elseif ($rule === 'capacity_watch') {
            $why = 'Capacity is approaching the configured pressure zone.';
            $whyAr = 'السعة تقترب من منطقة الضغط المحددة.';
            $recommendation = 'Keep expected discharges and blocked beds under review before escalation.';
            $recommendationAr = 'تابع الخروج المتوقع والأسرة المحجوزة قبل التصعيد.';
            $review = 'Beds Dashboard'; $reviewAr = 'لوحة الأسرة';
        } elseif ($rule === 'capacity_opportunity') {
            $why = 'Occupancy is below the opportunity threshold, meaning capacity is available.';
            $whyAr = 'الإشغال أقل من حد الفرصة، ما يعني وجود سعة متاحة.';
            $recommendation = 'Review admission conversion, referral flow and OR scheduling opportunities.';
            $recommendationAr = 'راجع فرص تحويلات الدخول وتدفق الإحالات وجدولة العمليات.';
            $review = 'Visits Dashboard'; $reviewAr = 'لوحة الزيارات';
        } elseif ($rule === 'blocked_beds_watch') {
            $why = 'Blocked beds are above the configured review threshold.';
            $whyAr = 'عدد الأسرة المحجوزة أعلى من حد المراجعة المحدد.';
            $recommendation = 'Review block reasons and release plan by nursing/unit ownership.';
            $recommendationAr = 'راجع أسباب الحجز وخطة الإتاحة مع التمريض والوحدات.';
            $review = 'Beds Dashboard'; $reviewAr = 'لوحة الأسرة';
        } elseif ($rule === 'er_critical' || $rule === 'er_watch') {
            $why = 'ER volume reached a configured pressure threshold for the operational day.';
            $whyAr = 'حجم الطوارئ وصل إلى حد ضغط محدد خلال اليوم التشغيلي.';
            $recommendation = 'Review triage, bed requests, physician coverage and discharge readiness.';
            $recommendationAr = 'راجع الفرز وطلبات الأسرة وتغطية الأطباء وجاهزية الخروج.';
            $review = 'Visits Dashboard'; $reviewAr = 'لوحة الزيارات';
        } elseif ($rule === 'admission_discharge_gap') {
            $why = 'Admissions are outpacing discharges beyond the configured gap.';
            $whyAr = 'الدخول أعلى من الخروج بفارق يتجاوز الحد المحدد.';
            $recommendation = 'Use the discharge list and bed readiness view to reduce capacity pressure.';
            $recommendationAr = 'استخدم قائمة الخروج وجاهزية الأسرة لتقليل ضغط السعة.';
            $review = 'Visits Dashboard'; $reviewAr = 'لوحة الزيارات';
        } elseif ($rule === 'or_gap_watch') {
            $why = 'Scheduled operations exceed performed operations by the configured gap.';
            $whyAr = 'العمليات المجدولة أعلى من المنفذة بفارق يتجاوز الحد المحدد.';
            $recommendation = 'Review OR delays, cancellations and patient readiness before end of day.';
            $recommendationAr = 'راجع تأخيرات العمليات والإلغاءات وجاهزية المرضى قبل نهاية اليوم.';
            $review = 'Operations/Visits'; $reviewAr = 'العمليات/الزيارات';
        } elseif ($rule === 'revenue_zero_with_activity') {
            $why = 'Clinical activity exists but today revenue is zero, which may indicate finance date alignment or batch refresh delay.';
            $whyAr = 'يوجد نشاط سريري لكن إيراد اليوم صفر، وقد يكون السبب توافق التاريخ المالي أو تأخر تحديث Batch.';
            $recommendation = 'Open Finance Check and confirm revenue batch posting for the operational finance date.';
            $recommendationAr = 'افتح فحص المالي وتأكد من ترحيل Batch الإيرادات لتاريخ اليوم المالي التشغيلي.';
            $review = 'Finance Check'; $reviewAr = 'فحص المالي';
        } elseif ($rule === 'revenue_below_time_adjusted_pace') {
            $why = 'Revenue is below the time-adjusted watch floor after the configured operational grace period.';
            $whyAr = 'الإيراد أقل من الحد الزمني المتوقع بعد فترة السماح التشغيلية المحددة.';
            $recommendation = 'Check posting delay, cash/insurance/MOH split and current collections before escalation.';
            $recommendationAr = 'افحص تأخر الترحيل وتقسيم النقدي/التأمين/وزارة الصحة والتحصيل الحالي قبل التصعيد.';
            $review = 'Revenue Dashboard'; $reviewAr = 'لوحة الإيرادات';
        } elseif ($rule === 'beds_metric_pending') {
            $confidence = 'Blocking'; $confidenceAr = 'مانع';
            $why = 'One or more bed summary metrics are pending in the summary layer.';
            $whyAr = 'واحد أو أكثر من مؤشرات الأسرة ما زال Pending في طبقة الملخص.';
            $recommendation = 'Run beds source diagnostic and fix the exact bed source mapping.';
            $recommendationAr = 'شغّل تشخيص مصدر الأسرة واضبط خريطة مصدر الأسرة الفعلي.';
            $review = 'Beds Diagnostic'; $reviewAr = 'تشخيص الأسرة';
        } elseif ($rule === 'daily_revenue_review') {
            $why = 'Daily revenue review is part of the standard leadership rhythm even when no alert is active.';
            $whyAr = 'مراجعة إيراد اليوم جزء من إيقاع القيادة اليومي حتى بدون تنبيه.';
            $recommendation = 'Open Revenue to compare today, month, cash, insurance, MOH and net revenue.';
            $recommendationAr = 'افتح الإيرادات لمقارنة اليوم والشهر والنقدي والتأمين ووزارة الصحة والصافي.';
            $review = 'Revenue Dashboard'; $reviewAr = 'لوحة الإيرادات';
        } elseif ($rule === 'refresh_status_review') {
            $why = 'Fresh data depends on the scheduled refresh task staying healthy.';
            $whyAr = 'حداثة البيانات تعتمد على سلامة مهمة التحديث المجدولة.';
            $recommendation = 'Open Refresh Status and confirm latest scheduler run ended with exit=0.';
            $recommendationAr = 'افتح حالة التحديث وتأكد أن آخر تشغيل للمجدول انتهى بـ exit=0.';
            $review = 'Refresh Status'; $reviewAr = 'حالة التحديث';
        } elseif ($rule === 'top_specialties_review') {
            $why = 'Top specialty and doctor contribution should be reviewed daily for revenue and clinical mix.';
            $whyAr = 'يجب مراجعة أعلى التخصصات وإسهام الأطباء يوميًا لفهم الإيراد والمزيج السريري.';
            $recommendation = 'Open Doctors to review top revenue contributors and specialty concentration.';
            $recommendationAr = 'افتح الأطباء لمراجعة أعلى المساهمين في الإيراد وتركيز التخصصات.';
            $review = 'Doctors Dashboard'; $reviewAr = 'لوحة الأطباء';
        }

        $a['why'] = $why;
        $a['why_ar'] = $whyAr;
        $a['recommendation'] = $recommendation;
        $a['recommendation_ar'] = $recommendationAr;
        $a['confidence'] = $confidence;
        $a['confidence_ar'] = $confidenceAr;
        $a['review_source'] = $review;
        $a['review_source_ar'] = $reviewAr;
        $a['explain_observed'] = $observed;
        $a['explain_threshold'] = $threshold;
        return $a;
    }

    private function num($v)
    {
        if ($v === null || $v === '') { return null; }
        return (float)$v;
    }

    private function intEnv(string $key, int $default): int
    {
        $value = \env($key, $default);
        if ($value === null || $value === '') { return $default; }
        return (int)$value;
    }

    private function floatEnv(string $key, float $default): float
    {
        $value = \env($key, $default);
        if ($value === null || $value === '') { return $default; }
        return (float)$value;
    }

    /**
     * Phase 10.5.1
     * Build a fair revenue-pace comparison that respects the operational day clock.
     * It avoids early false WATCH alerts by requiring both:
     *  - configured clock hour has passed, and
     *  - enough operational hours have elapsed.
     *
     * @param array<string,mixed> $m
     * @param array<string,mixed> $operational
     * @return array<string,mixed>
     */
    public function revenuePaceCalibration(array $m, array $operational): array
    {
        $todayRevenue = (float)($m['today_total'] ?? 0);
        $monthRevenue = (float)($m['month_total'] ?? 0);
        $patientFlow = (float)($m['patient_flow'] ?? 0);

        $now = time();
        $windowStartRaw = (string)($operational['window_start'] ?? '');
        $windowStartTs = $windowStartRaw !== '' ? strtotime($windowStartRaw) : false;
        if ($windowStartTs === false) {
            $date = (string)($operational['operational_date'] ?? date('Y-m-d'));
            $hour = (int)\env('EXECUTIVE_OPERATIONAL_DAY_START_HOUR', 7);
            $windowStartTs = strtotime($date . ' ' . str_pad((string)$hour, 2, '0', STR_PAD_LEFT) . ':00:00');
        }
        if ($windowStartTs === false) { $windowStartTs = strtotime(date('Y-m-d') . ' 07:00:00'); }
        if ($windowStartTs === false) { $windowStartTs = $now; }

        $elapsedHours = max(0.0, min(24.0, ($now - $windowStartTs) / 3600.0));
        $clockHour = (int)date('G', $now);
        $date = (string)($operational['operational_date'] ?? date('Y-m-d', $windowStartTs));
        $day = 1;
        $dateTs = strtotime($date);
        if ($dateTs !== false) { $day = max(1, (int)date('j', $dateTs)); }

        $dailyPace = $day > 0 ? ($monthRevenue / $day) : 0.0;
        $elapsedRatio = $elapsedHours / 24.0;
        $floorRatio = max(0.0, min(1.0, (float)$this->thresholds['revenue_pace_time_weight_floor']));
        $timeWeight = min(1.0, max($floorRatio, $elapsedRatio));
        $timeAdjustedExpected = $dailyPace * $timeWeight;
        $watchFloor = $timeAdjustedExpected * (float)$this->thresholds['revenue_pace_ratio_watch'];

        $dueByClock = $clockHour >= (int)$this->thresholds['revenue_pace_after_hour'];
        $dueByElapsed = $elapsedHours >= (float)$this->thresholds['revenue_pace_min_elapsed_hours'];
        $dueByFlow = $patientFlow >= (float)$this->thresholds['revenue_pace_min_flow'];
        $checkDue = $dueByClock && $dueByElapsed && $dueByFlow && $dailyPace > 0;

        $skipReason = '';
        if (!$dueByClock) { $skipReason = 'clock_before_after_hour'; }
        elseif (!$dueByElapsed) { $skipReason = 'early_operational_day_grace'; }
        elseif (!$dueByFlow) { $skipReason = 'patient_flow_below_minimum'; }
        elseif ($dailyPace <= 0) { $skipReason = 'month_pace_unavailable'; }

        return [
            'now' => date('Y-m-d H:i:s', $now),
            'operational_date' => $date,
            'window_start' => date('Y-m-d H:i:s', $windowStartTs),
            'elapsed_hours' => $elapsedHours,
            'clock_hour' => $clockHour,
            'day_of_month' => $day,
            'month_total' => $monthRevenue,
            'daily_pace' => $dailyPace,
            'elapsed_ratio' => $elapsedRatio,
            'time_weight' => $timeWeight,
            'time_adjusted_expected' => $timeAdjustedExpected,
            'watch_floor' => $watchFloor,
            'today_total' => $todayRevenue,
            'patient_flow' => $patientFlow,
            'check_due' => $checkDue,
            'skip_reason' => $skipReason,
        ];
    }

    private function compactMoney($value): string
    {
        $value = (float)$value;
        $abs = abs($value);
        if ($abs >= 1000000000) { return number_format($value / 1000000000, 2) . 'B'; }
        if ($abs >= 1000000) { return number_format($value / 1000000, 2) . 'M'; }
        if ($abs >= 1000) { return number_format($value / 1000, 1) . 'K'; }
        return number_format($value, 0);
    }

    private function fmt($value): string
    {
        if ($value === null || $value === '') { return '—'; }
        return number_format((float)$value, 0);
    }
}
