<?php
/**
 * IERuleScoringEngine — Phase 6: Auto Rule Scoring
 *
 * Reads REAL data from:
 *   ai_requests.ie_rule_id   → which rule fired
 *   ai_requests.ie_hit       → 1 = IE served, 0 = AI used
 *   ai_requests.latency_ms   → response time
 *   ai_requests.error_occurred → 1 = failure
 *   ai_requests.cost_usd     → actual cost (for AI fallback comparison)
 *
 * Scores each rule 0–100 using:
 *   score = (hit_rate×40) + (reliability×25) + (latency_gain×20) + (cost_saving×15)
 *
 * Classifies:
 *   ≥80 → STRONG | 60–79 → GOOD | 40–59 → WEAK | <40 → BAD
 *
 * Usage:
 *   php engine/IERuleScoringEngine.php [--days=30] [--json] [--write-db]
 *
 * DOES NOT modify rules. Read-only evaluation.
 */

require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/InferenceEngine.php';

class IERuleScoringEngine
{
    // ── Score weights (must sum to 100) ─────────────────────────
    const W_HIT_RATE     = 40;   // how often rule fires vs total IE hits
    const W_RELIABILITY  = 25;   // 1 - error_rate
    const W_LATENCY_GAIN = 20;   // vs AI fallback (baseline ~2500ms)
    const W_COST_SAVING  = 15;   // estimated USD saved per hit

    // ── Classification thresholds ───────────────────────────────
    const GRADE_STRONG = 80;
    const GRADE_GOOD   = 60;
    const GRADE_WEAK   = 40;

    // ── Baseline assumptions (used for normalization) ────────────
    const AI_BASELINE_MS       = 2500;    // avg AI call latency
    const AI_BASELINE_COST_USD = 0.0040;  // avg AI call cost
    const MAX_EXPECTED_HITS    = 100;     // P90 of actual hits — sqrt-normalized; recalibrate after 30 days

    // ─────────────────────────────────────────────────────────────
    // TASK 1 — Collect raw metrics per rule_id
    // ─────────────────────────────────────────────────────────────
    public static function collectMetrics(int $days = 30): array
    {
        $sql = "
            SELECT
                r.ie_rule_id                        AS rule_id,
                COUNT(*)                             AS total_hits,
                SUM(CASE WHEN r.error_occurred = 0 THEN 1 ELSE 0 END) AS success_hits,
                SUM(r.error_occurred)                AS error_hits,
                ROUND(AVG(r.latency_ms))             AS avg_latency_ms,
                ROUND(AVG(r.ie_confidence), 3)       AS avg_confidence,
                MAX(r.created_at)                    AS last_seen,
                -- Cost saved: each IE hit avoided an AI call
                ROUND(COUNT(*) * :ai_cost, 6)        AS cost_saved_usd_est
            FROM ai_requests r
            WHERE r.ie_rule_id IS NOT NULL
              AND r.ie_rule_id != ''
              AND r.ie_hit = 1
              AND r.created_at >= DATE_SUB(NOW(), INTERVAL :days DAY)
            GROUP BY r.ie_rule_id
            ORDER BY total_hits DESC
        ";

        try {
            $stmt = db()->prepare($sql);
            $stmt->execute([
                ':ai_cost' => self::AI_BASELINE_COST_USD,
                ':days'    => $days,
            ]);
            return $stmt->fetchAll(\PDO::FETCH_ASSOC);
        } catch (\Throwable $e) {
            error_log('[IERuleScoring] collectMetrics: ' . $e->getMessage());
            return [];
        }
    }

    // ─────────────────────────────────────────────────────────────
    // TASK 2 — Score formula
    // ─────────────────────────────────────────────────────────────
    public static function scoreRule(array $m, int $global_ie_hits): array
    {
        $hits         = (int)  $m['total_hits'];
        $success      = (int)  $m['success_hits'];
        $errors       = (int)  $m['error_hits'];
        $avg_lat      = (float)$m['avg_latency_ms'];
        $cost_saved   = (float)$m['cost_saved_usd_est'];

        // ── Component 1: Hit rate (40 pts) ───────────────────────
        // CALIBRATED: sqrt normalization compresses bimodal distribution.
        // Linear /500 skewed 90% WEAK. sqrt /sqrt(P90) gives ~15/29/56% split.
        // Recalibrate MAX_EXPECTED_HITS to P90 of actual hits after 30 days.
        $hit_rate_norm  = min(1.0, sqrt($hits) / sqrt(max(1, self::MAX_EXPECTED_HITS)));
        $hit_score      = round($hit_rate_norm * self::W_HIT_RATE, 2);

        // ── Component 2: Reliability = 1 - error_rate (25 pts) ──
        $error_rate     = $hits > 0 ? $errors / $hits : 0;
        $reliability    = 1.0 - $error_rate;
        $rel_score      = round($reliability * self::W_RELIABILITY, 2);

        // ── Component 3: Latency gain (20 pts) ──────────────────
        // IE latency vs AI baseline. Lower = better.
        // If IE answers in 50ms vs AI in 2500ms → 98% faster = full score
        $lat_gain       = max(0, (self::AI_BASELINE_MS - $avg_lat) / self::AI_BASELINE_MS);
        $lat_gain       = max(0, min(1.0, $lat_gain));
        $lat_score      = round($lat_gain * self::W_LATENCY_GAIN, 2);

        // ── Component 4: Cost saving (15 pts) ───────────────────
        // Normalized: $1 saved per period = full score
        $cost_norm      = min(1.0, $cost_saved / 1.0);
        $cost_score     = round($cost_norm * self::W_COST_SAVING, 2);

        $total_score    = round($hit_score + $rel_score + $lat_score + $cost_score, 2);

        return [
            'components' => [
                'hit_rate'    => ['norm' => round($hit_rate_norm, 3), 'points' => $hit_score,   'weight' => self::W_HIT_RATE],
                'reliability' => ['norm' => round($reliability, 3),   'points' => $rel_score,   'weight' => self::W_RELIABILITY],
                'latency_gain'=> ['norm' => round($lat_gain, 3),      'points' => $lat_score,   'weight' => self::W_LATENCY_GAIN],
                'cost_saving' => ['norm' => round($cost_norm, 3),     'points' => $cost_score,  'weight' => self::W_COST_SAVING],
            ],
            'score'       => $total_score,
            'error_rate'  => round($error_rate * 100, 1),
        ];
    }

    // ─────────────────────────────────────────────────────────────
    // TASK 3 — Classify
    // ─────────────────────────────────────────────────────────────
    public static function classify(float $score): string
    {
        if ($score >= self::GRADE_STRONG) return 'STRONG';
        if ($score >= self::GRADE_GOOD)   return 'GOOD';
        if ($score >= self::GRADE_WEAK)   return 'WEAK';
        return 'BAD';
    }

    // ─────────────────────────────────────────────────────────────
    // TASK 4 — Determine action per grade
    // ─────────────────────────────────────────────────────────────
    public static function recommendAction(string $grade, array $m): string
    {
        return match($grade) {
            'STRONG' => 'KEEP — high-value rule, monitor for drift',
            'GOOD'   => 'MONITOR — healthy, re-evaluate in 30 days',
            'WEAK'   => 'REVIEW — low hit rate or high latency; consider refining pattern',
            'BAD'    => 'CANDIDATE FOR REMOVAL — not contributing value',
            default  => 'UNSCORED',
        };
    }

    // ─────────────────────────────────────────────────────────────
    // TASK 6 — Detect never-fired + always-overridden rules
    // ─────────────────────────────────────────────────────────────
    public static function detectDeadRules(array $scored_ids, int $days = 30): array
    {
        // All rule IDs from InferenceEngine source
        $ie_content = file_get_contents(__DIR__ . '/InferenceEngine.php');
        preg_match_all("/'id'=>'([^']+)'/", $ie_content, $matches);
        $all_rule_ids = array_unique($matches[1]);

        // Never fired = in IE but not in scored_ids
        $never_fired = array_diff($all_rule_ids, $scored_ids);

        // Always overridden: fired but always lost to a higher-conf rule
        // Proxy: check ai_requests where ie_hit=0 but ie_rule_id is set
        $overridden = [];
        try {
            $sql = "
                SELECT ie_rule_id, COUNT(*) as times_overridden
                FROM ai_requests
                WHERE ie_rule_id IS NOT NULL
                  AND ie_hit = 0
                  AND ie_rule_id != ''
                  AND created_at >= DATE_SUB(NOW(), INTERVAL :days DAY)
                GROUP BY ie_rule_id
                HAVING times_overridden >= 3
                ORDER BY times_overridden DESC
                LIMIT 20
            ";
            $stmt = db()->prepare($sql);
            $stmt->execute([':days' => $days]);
            $overridden = $stmt->fetchAll(\PDO::FETCH_ASSOC);
        } catch (\Throwable $e) {
            error_log('[IERuleScoring] detectDeadRules: ' . $e->getMessage());
        }

        return [
            'never_fired'       => array_values($never_fired),
            'never_fired_count' => count($never_fired),
            'always_overridden' => $overridden,
        ];
    }

    // ─────────────────────────────────────────────────────────────
    // Optionally persist scores to ie_rule_scores table
    // ─────────────────────────────────────────────────────────────
    public static function writeScores(array $scored_rules): void
    {
        $sql = "
            INSERT INTO ie_rule_scores
                (rule_id, sector, total_hits, success_hits, error_hits,
                 avg_latency_ms, avg_confidence, cost_saved_usd, score, grade, last_seen)
            VALUES (?,?,?,?,?,?,?,?,?,?,?)
            ON DUPLICATE KEY UPDATE
                total_hits     = VALUES(total_hits),
                success_hits   = VALUES(success_hits),
                error_hits     = VALUES(error_hits),
                avg_latency_ms = VALUES(avg_latency_ms),
                avg_confidence = VALUES(avg_confidence),
                cost_saved_usd = VALUES(cost_saved_usd),
                score          = VALUES(score),
                grade          = VALUES(grade),
                last_seen      = VALUES(last_seen),
                scored_at      = NOW()
        ";
        try {
            $pdo = db();
            $stmt = $pdo->prepare($sql);
            foreach ($scored_rules as $r) {
                $stmt->execute([
                    $r['rule_id'], $r['sector'], $r['total_hits'],
                    $r['success_hits'], $r['error_hits'],
                    $r['avg_latency_ms'], $r['avg_confidence'],
                    $r['cost_saved_usd_est'], $r['score'], $r['grade'],
                    $r['last_seen'],
                ]);
            }
        } catch (\Throwable $e) {
            error_log('[IERuleScoring] writeScores: ' . $e->getMessage());
        }
    }

    // ─────────────────────────────────────────────────────────────
    // MAIN: Full scoring run
    // ─────────────────────────────────────────────────────────────
    public static function run(int $days = 30, bool $write_db = false): array
    {
        // 1. Collect raw metrics
        $metrics = self::collectMetrics($days);

        if (empty($metrics)) {
            return [
                'status'  => 'NO_DATA',
                'message' => 'No rule_id data in ai_requests. Ensure Gateway logs ie_rule_id.',
            ];
        }

        $global_ie_hits = array_sum(array_column($metrics, 'total_hits'));

        // 2–3. Score + classify each rule
        $scored = [];
        foreach ($metrics as $m) {
            $scoring = self::scoreRule($m, $global_ie_hits);
            $grade   = self::classify($scoring['score']);
            $action  = self::recommendAction($grade, $m);

            $scored[] = array_merge($m, [
                'score'       => $scoring['score'],
                'grade'       => $grade,
                'action'      => $action,
                'error_rate'  => $scoring['error_rate'],
                'components'  => $scoring['components'],
            ]);
        }

        // Sort by score desc
        usort($scored, fn($a,$b) => $b['score'] <=> $a['score']);

        // 6. Detect dead/overridden
        $scored_ids = array_column($scored, 'rule_id');
        $dead       = self::detectDeadRules($scored_ids, $days);

        // 5. Persist if requested
        if ($write_db) self::writeScores($scored);

        return [
            'status'           => 'OK',
            'period_days'      => $days,
            'rules_scored'     => count($scored),
            'global_ie_hits'   => $global_ie_hits,
            'scored'           => $scored,
            'never_fired'      => $dead,
            'grade_summary'    => array_count_values(array_column($scored, 'grade')),
        ];
    }

    // ─────────────────────────────────────────────────────────────
    // Print formatted report
    // ─────────────────────────────────────────────────────────────
    public static function printReport(array $result): void
    {
        $sep = str_repeat('═', 72);

        if (($result['status'] ?? '') === 'NO_DATA') {
            echo "\n{$sep}\nIE RULE SCORING ENGINE — NO DATA\n{$sep}\n";
            echo $result['message'] . "\n\n";
            echo "Required: ai_requests.ie_rule_id populated by Gateway.\n";
            echo "Migration: ALTER TABLE ai_requests ADD COLUMN ie_rule_id VARCHAR(80);\n\n";
            return;
        }

        $scored = $result['scored'];
        $by_grade = ['STRONG'=>[],'GOOD'=>[],'WEAK'=>[],'BAD'=>[]];
        foreach ($scored as $r) $by_grade[$r['grade']][] = $r;

        echo "\n{$sep}\n";
        echo "  IE RULE SCORING ENGINE — PHASE 6 REPORT\n";
        echo "{$sep}\n";
        printf("  Period:        Last %d days\n", $result['period_days']);
        printf("  Rules scored:  %d\n", $result['rules_scored']);
        printf("  Total IE hits: %d\n", $result['global_ie_hits']);
        $gs = $result['grade_summary'];
        printf("  Grades:        STRONG=%d  GOOD=%d  WEAK=%d  BAD=%d\n\n",
            $gs['STRONG']??0, $gs['GOOD']??0, $gs['WEAK']??0, $gs['BAD']??0);

        // Score formula explanation
        echo "  Score formula:\n";
        echo "    score = (hit_rate×40) + (reliability×25) + (latency_gain×20) + (cost_saving×15)\n\n";

        // ── Top 10 STRONG ────────────────────────────────────────
        echo "  " . str_repeat('─', 70) . "\n";
        echo "  TOP 10 STRONG RULES (score ≥ 80)\n";
        echo "  " . str_repeat('─', 70) . "\n";
        self::printRuleTable(array_slice($by_grade['STRONG'], 0, 10));

        // ── Top 10 WEAK ──────────────────────────────────────────
        $weak_sorted = $by_grade['WEAK'];
        usort($weak_sorted, fn($a,$b) => $a['score'] <=> $b['score']);  // worst first
        echo "\n  " . str_repeat('─', 70) . "\n";
        echo "  TOP 10 WEAK RULES (score 40–59) — need review\n";
        echo "  " . str_repeat('─', 70) . "\n";
        self::printRuleTable(array_slice($weak_sorted, 0, 10));

        // ── Top 5 BAD ────────────────────────────────────────────
        echo "\n  " . str_repeat('─', 70) . "\n";
        echo "  TOP 5 BAD RULES (score < 40) — candidates for removal\n";
        echo "  " . str_repeat('─', 70) . "\n";
        self::printRuleTable(array_slice($by_grade['BAD'], 0, 5));

        // ── Task 6: Never fired / always overridden ──────────────
        $dead = $result['never_fired'];
        echo "\n  " . str_repeat('─', 70) . "\n";
        echo "  TASK 6 — DEAD RULE DETECTION\n";
        echo "  " . str_repeat('─', 70) . "\n";
        printf("  Rules never fired in %d days: %d\n", $result['period_days'], $dead['never_fired_count']);
        if (!empty($dead['never_fired'])) {
            foreach (array_slice($dead['never_fired'], 0, 10) as $rid) {
                echo "    ⚪ {$rid}\n";
            }
            if ($dead['never_fired_count'] > 10) {
                echo "    ... and " . ($dead['never_fired_count'] - 10) . " more\n";
            }
        }
        if (!empty($dead['always_overridden'])) {
            echo "\n  Rules always overridden (fired but IE lost):\n";
            foreach ($dead['always_overridden'] as $r) {
                printf("    🔻 %s — overridden %d times\n", $r['ie_rule_id'], $r['times_overridden']);
            }
        }

        // ── Actions summary ──────────────────────────────────────
        echo "\n  " . str_repeat('─', 70) . "\n";
        echo "  RECOMMENDED ACTIONS\n";
        echo "  " . str_repeat('─', 70) . "\n";
        $actions = [
            'STRONG' => ['icon'=>'✅','action'=>'KEEP — high-value, monitor for drift'],
            'GOOD'   => ['icon'=>'🔵','action'=>'MONITOR — re-evaluate in 30 days'],
            'WEAK'   => ['icon'=>'⚠️','action'=>'REVIEW — refine pattern or lower confidence'],
            'BAD'    => ['icon'=>'❌','action'=>'CANDIDATE FOR REMOVAL — submit for peer review'],
        ];
        foreach ($actions as $grade => $a) {
            $count = count($by_grade[$grade]);
            printf("  %s %-8s (%2d rules) — %s\n", $a['icon'], $grade, $count, $a['action']);
        }
        echo "\n  ⚠️  NO rules were modified. Evaluation only.\n";
        echo "{$sep}\n\n";
    }

    private static function printRuleTable(array $rules): void
    {
        if (empty($rules)) { echo "  (none)\n"; return; }
        printf("  %-30s %5s  %5s  %7s  %5s  %8s  %s\n",
            'Rule ID', 'Score', 'Hits', 'AvgLat', 'Err%', 'Saved$', 'Action');
        echo "  " . str_repeat('─', 80) . "\n";
        foreach ($rules as $r) {
            $icon = ['STRONG'=>'✅','GOOD'=>'🔵','WEAK'=>'⚠️','BAD'=>'❌'][$r['grade']] ?? '?';
            printf("  %s %-27s %5.1f  %5d  %5dms  %4.1f%%  $%7.4f  %s\n",
                $icon,
                mb_substr($r['rule_id'], 0, 27),
                $r['score'],
                $r['total_hits'],
                $r['avg_latency_ms'],
                $r['error_rate'],
                $r['cost_saved_usd_est'],
                mb_substr($r['action'], 0, 30)
            );
        }
    }
}

// ═════════════════════════════════════════════════════════════════
// CLI RUNNER
// ═════════════════════════════════════════════════════════════════
if (php_sapi_name() === 'cli') {
    $opts    = getopt('', ['days:', 'json', 'write-db', 'help']);
    if (isset($opts['help'])) {
        echo "Usage: php engine/IERuleScoringEngine.php [--days=30] [--json] [--write-db]\n";
        echo "  --days=N     Analyze last N days (default: 30)\n";
        echo "  --json       Output JSON instead of formatted report\n";
        echo "  --write-db   Persist scores to ie_rule_scores table\n";
        exit(0);
    }
    $days    = (int)($opts['days'] ?? 30);
    $as_json = isset($opts['json']);
    $write   = isset($opts['write-db']);

    $result = IERuleScoringEngine::run($days, $write);

    if ($as_json) {
        echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    } else {
        IERuleScoringEngine::printReport($result);
    }
}
