<?php
/**
 * IERuleLifecycleEngine — Phase 7: Rule Lifecycle Management
 *
 * State machine:  ACTIVE → MONITORED → DEPRECATED → PENDING_REMOVAL
 *                                                  ↓ (manual approval only)
 *                                               REMOVED
 *
 * Thresholds (score-based, not grade-based):
 *   score ≥ 70  → ACTIVE       (healthy, keep as-is)
 *   score 50–69 → MONITORED    (watch for 30 days before deciding)
 *   score 30–49 → DEPRECATED   (lower confidence by 0.05, flag for review)
 *   score < 30  → PENDING_REMOVAL (requires human sign-off)
 *
 * Safety rules:
 *   - NEVER auto-deletes rules from InferenceEngine
 *   - PENDING_REMOVAL requires manual approval via approveRemoval()
 *   - Core rules (pay/emergency) are shielded from automatic DEPRECATED
 *   - All state changes are logged with reason + timestamp
 *
 * Usage:
 *   php engine/IERuleLifecycleEngine.php [--dry-run] [--json] [--days=30]
 */

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

class IERuleLifecycleEngine
{
    // ── State thresholds ─────────────────────────────────────
    const ACTIVE_MIN      = 70;     // score ≥ 70 → ACTIVE
    const MONITORED_MIN   = 50;     // score 50–69 → MONITORED
    const DEPRECATED_MIN  = 30;     // score 30–49 → DEPRECATED
    // score < 30 → PENDING_REMOVAL

    // ── Monitoring rules ─────────────────────────────────────
    const MONITOR_DAYS          = 30;   // days in MONITORED before escalating (score-only)
    const AGING_DAYS            = 60;   // days in MONITORED + declining trend → auto-DEPRECATED
    const CONSECUTIVE_WEAK_LIMIT= 3;    // periods below threshold → escalate
    const CONF_REDUCTION        = 0.05; // confidence reduction for DEPRECATED rules
    const CONF_FLOOR            = 0.60; // never reduce confidence below this

    // ── Core sectors: protected from auto-deprecation ────────
    const CORE_SECTORS = ['pay', 'emergency', 'pharmacy'];
    const CORE_RULE_PREFIXES = ['emergency_', 'sdpay_', 'critical_'];

    // ─────────────────────────────────────────────────────────
    // Determine lifecycle state from score
    // ─────────────────────────────────────────────────────────
    public static function scoreToState(float $score): string
    {
        if ($score >= self::ACTIVE_MIN)    return 'ACTIVE';
        if ($score >= self::MONITORED_MIN) return 'MONITORED';
        if ($score >= self::DEPRECATED_MIN) return 'DEPRECATED';
        return 'PENDING_REMOVAL';
    }

    // ─────────────────────────────────────────────────────────
    // Is this a core/protected rule?
    // ─────────────────────────────────────────────────────────
    public static function isCoreRule(string $rule_id, string $sector): bool
    {
        if (in_array($sector, self::CORE_SECTORS)) return true;
        foreach (self::CORE_RULE_PREFIXES as $prefix) {
            if (str_starts_with($rule_id, $prefix)) return true;
        }
        return false;
    }

    // ─────────────────────────────────────────────────────────
    // Load all known rules from InferenceEngine source
    // ─────────────────────────────────────────────────────────
    public static function loadIERules(): array
    {
        $content = file_get_contents(__DIR__ . '/InferenceEngine.php');
        $rules   = [];
        preg_match_all(
            "/'id'=>'([^']+)'[^;]*'sector'=>'([^']+)'[^;]*(?:'c'|'conf')=>([\d.]+)/",
            $content,
            $m,
            PREG_SET_ORDER
        );
        foreach ($m as $row) {
            $rules[$row[1]] = [
                'id'     => $row[1],
                'sector' => $row[2],
                'conf'   => (float)$row[3],
                'is_core'=> self::isCoreRule($row[1], $row[2]),
            ];
        }
        return $rules;
    }

    // ─────────────────────────────────────────────────────────
    // Initialize lifecycle records for new rules
    // ─────────────────────────────────────────────────────────
    public static function initializeRules(array $ie_rules, bool $dry_run = false): int
    {
        $sql_check  = "SELECT COUNT(*) FROM ie_rule_lifecycle WHERE rule_id = ?";
        $sql_insert = "INSERT INTO ie_rule_lifecycle
            (rule_id, sector, state, score, confidence_orig, confidence_cur, is_core_rule)
            VALUES (?, ?, 'ACTIVE', 0, ?, ?, ?)
            ON DUPLICATE KEY UPDATE sector=VALUES(sector), is_core_rule=VALUES(is_core_rule)";

        $inserted = 0;
        try {
            $pdo = db();
            foreach ($ie_rules as $r) {
                $count = $pdo->prepare($sql_check);
                $count->execute([$r['id']]);
                if ($count->fetchColumn() == 0) {
                    if (!$dry_run) {
                        $pdo->prepare($sql_insert)->execute([
                            $r['id'], $r['sector'], $r['conf'], $r['conf'], (int)$r['is_core']
                        ]);
                    }
                    $inserted++;
                }
            }
        } catch (\Throwable $e) {
            error_log('[IELifecycle] initializeRules: ' . $e->getMessage());
        }
        return $inserted;
    }

    // ─────────────────────────────────────────────────────────
    // TASK 2 — Main lifecycle update logic
    // ─────────────────────────────────────────────────────────
    public static function updateLifecycle(array $scored_rules, bool $dry_run = false): array
    {
        $transitions = [];
        $now         = date('Y-m-d H:i:s');

        try {
            $pdo = db();

            $sql_get = "SELECT * FROM ie_rule_lifecycle WHERE rule_id = ? LIMIT 1";
            $sql_upd = "UPDATE ie_rule_lifecycle SET
                state = ?, prev_state = ?, state_reason = ?, score = ?,
                confidence_cur = ?,
                consecutive_weak_periods = ?,
                monitoring_started = ?,
                deprecated_at = ?,
                removal_requested_at = ?,
                updated_at = NOW()
                WHERE rule_id = ?";

            foreach ($scored_rules as $scored) {
                $rule_id = $scored['rule_id'];
                $sector  = $scored['sector'];
                $score   = $scored['score'];
                $is_core = self::isCoreRule($rule_id, $sector);

                // Get current lifecycle record
                $stmt = $pdo->prepare($sql_get);
                $stmt->execute([$rule_id]);
                $current = $stmt->fetch(\PDO::FETCH_ASSOC);

                if (!$current) continue;  // not initialized

                $old_state = $current['state'];
                $new_state = self::scoreToState($score);

                // ── SAFETY SHIELD: core rules cannot go below MONITORED ──
                if ($is_core && in_array($new_state, ['DEPRECATED', 'PENDING_REMOVAL'])) {
                    $new_state = 'MONITORED';
                }

                // ── Already PENDING_REMOVAL or REMOVED: don't change ─────
                if (in_array($old_state, ['PENDING_REMOVAL', 'REMOVED'])) {
                    continue;
                }

                // ── Track consecutive weak periods ───────────────────────
                $weak_periods = (int)$current['consecutive_weak_periods'];
                $is_weak      = $score < self::ACTIVE_MIN;
                if ($is_weak) {
                    $weak_periods++;
                } else {
                    $weak_periods = 0;  // reset on recovery
                }

                // ── Compute new confidence (DEPRECATED: reduce by 0.05) ──
                $conf_orig = (float)($current['confidence_orig'] ?? 0.75);
                $conf_cur  = (float)($current['confidence_cur']  ?? $conf_orig);
                $new_conf  = $conf_cur;

                if ($new_state === 'DEPRECATED' && $old_state !== 'DEPRECATED') {
                    // First entry to DEPRECATED: reduce confidence
                    $new_conf = max(self::CONF_FLOOR, round($conf_cur - self::CONF_REDUCTION, 3));
                } elseif ($new_state === 'ACTIVE' && $old_state === 'DEPRECATED') {
                    // Recovery: restore to original confidence
                    $new_conf = $conf_orig;
                }

                // ── Monitoring timer ─────────────────────────────────────
                $monitoring_started = $current['monitoring_started'];
                if ($new_state === 'MONITORED' && empty($monitoring_started)) {
                    $monitoring_started = $now;
                } elseif ($new_state !== 'MONITORED') {
                    $monitoring_started = null;  // reset if leaving MONITORED
                }

                // ── Check: has rule been in MONITORED too long? ──────────
                $aged_out = false;
                if ($old_state === 'MONITORED' && !empty($monitoring_started)) {
                    $days_monitored = (int)((time() - strtotime($monitoring_started)) / 86400);

                    // Rule A: 30 days + score still below MONITORED_MIN → DEPRECATED
                    if ($days_monitored >= self::MONITOR_DAYS
                        && $new_state === 'MONITORED'
                        && $score < self::MONITORED_MIN) {
                        $new_state = 'DEPRECATED';
                    }

                    // Rule B: AGING — 60 days + declining trend + delta ≤ -5 → DEPRECATED
                    // Applies even if score is still in MONITORED band (50–69)
                    // Does NOT affect ACTIVE rules (guard: old_state === 'MONITORED')
                    // Does NOT bypass core-rule shield (shield applied before this block)
                    if (!$is_core
                        && $days_monitored >= self::AGING_DAYS
                        && $new_state === 'MONITORED') {
                        $trend = self::computeTrend($rule_id, 4);
                        $delta = self::computeScoreDelta($rule_id, 4);
                        if ($trend === 'DECLINING' && $delta <= -5) {
                            $new_state = 'DEPRECATED';
                            $aged_out  = true;
                        }
                    }
                }

                // ── Build reason string ──────────────────────────────────
                $reason = self::buildReason($old_state, $new_state, $score, $weak_periods, $is_core, $aged_out);

                // ── Timestamps for state entries ─────────────────────────
                $deprecated_at = $current['deprecated_at'];
                $removal_req   = $current['removal_requested_at'];
                if ($new_state === 'DEPRECATED' && $old_state !== 'DEPRECATED') {
                    $deprecated_at = $now;
                }
                if ($new_state === 'PENDING_REMOVAL' && $old_state !== 'PENDING_REMOVAL') {
                    $removal_req = $now;
                }

                // ── Record transition ────────────────────────────────────
                if ($old_state !== $new_state || $score != $current['score']) {
                    $transitions[] = [
                        'rule_id'    => $rule_id,
                        'sector'     => $sector,
                        'old_state'  => $old_state,
                        'new_state'  => $new_state,
                        'score'      => $score,
                        'conf_before'=> $conf_cur,
                        'conf_after' => $new_conf,
                        'reason'     => $reason,
                        'is_core'    => $is_core,
                        'weak_periods'=> $weak_periods,
                    ];

                    if (!$dry_run) {
                        $pdo->prepare($sql_upd)->execute([
                            $new_state, $old_state, $reason, $score,
                            $new_conf, $weak_periods,
                            $monitoring_started, $deprecated_at, $removal_req,
                            $rule_id
                        ]);
                    }
                }
            }
        } catch (\Throwable $e) {
            error_log('[IELifecycle] updateLifecycle: ' . $e->getMessage());
        }

        return $transitions;
    }

    // ─────────────────────────────────────────────────────────
    // TASK 3 — Record score history
    // ─────────────────────────────────────────────────────────
    public static function recordHistory(array $scored_rules, int $period_days = 7, bool $dry_run = false): void
    {
        $sql = "INSERT INTO ie_rule_score_history
            (rule_id, score, grade, total_hits, error_rate, avg_latency_ms, period_days)
            VALUES (?, ?, ?, ?, ?, ?, ?)";
        try {
            $pdo  = db();
            $stmt = $pdo->prepare($sql);
            foreach ($scored_rules as $r) {
                if (!$dry_run) {
                    $stmt->execute([
                        $r['rule_id'],
                        $r['score'],
                        $r['grade'],
                        $r['total_hits'],
                        $r['error_rate'],
                        $r['avg_latency_ms'],
                        $period_days,
                    ]);
                }
            }
        } catch (\Throwable $e) {
            error_log('[IELifecycle] recordHistory: ' . $e->getMessage());
        }
    }

    // ─────────────────────────────────────────────────────────
    // TASK 3 — Compute trend from history
    // ─────────────────────────────────────────────────────────
    public static function computeTrend(string $rule_id, int $periods = 4): string
    {
        $sql = "SELECT score, recorded_at FROM ie_rule_score_history
                WHERE rule_id = ?
                ORDER BY recorded_at DESC
                LIMIT ?";
        try {
            $rows = db()->prepare($sql);
            $rows->execute([$rule_id, $periods]);
            $scores = array_column($rows->fetchAll(\PDO::FETCH_ASSOC), 'score');

            if (count($scores) < 2) return 'INSUFFICIENT_DATA';

            $scores = array_reverse($scores);  // oldest first
            $first  = (float)$scores[0];
            $last   = (float)end($scores);
            $delta  = $last - $first;

            if ($delta >  5) return 'IMPROVING';
            if ($delta < -5) return 'DECLINING';
            return 'STABLE';

        } catch (\Throwable $e) {
            return 'ERROR';
        }
    }

    // ─────────────────────────────────────────────────────────
    // Phase 7.1: Score history retrieval (used by aging + delta)
    // ─────────────────────────────────────────────────────────
    private static function getLastScores(string $rule_id, int $periods = 4): array
    {
        $sql = "SELECT score FROM ie_rule_score_history
                WHERE rule_id = ?
                ORDER BY recorded_at DESC
                LIMIT ?";
        try {
            $stmt = db()->prepare($sql);
            $stmt->execute([$rule_id, $periods]);
            $rows = array_column($stmt->fetchAll(\PDO::FETCH_ASSOC), 'score');
            return array_reverse(array_map('floatval', $rows));  // oldest first
        } catch (\Throwable $e) {
            return [];
        }
    }

    // Phase 7.1: Numeric score delta (last − first over N periods)
    private static function computeScoreDelta(string $rule_id, int $periods = 4): float
    {
        $scores = self::getLastScores($rule_id, $periods);
        if (count($scores) < 2) return 0.0;
        return (float)end($scores) - (float)reset($scores);
    }

    // ─────────────────────────────────────────────────────────
    // TASK 4 — Manual removal approval (human gate)
    // ─────────────────────────────────────────────────────────
    public static function requestRemoval(string $rule_id, string $requested_by, string $notes = ''): bool
    {
        try {
            $pdo = db();
            // Log the request
            $pdo->prepare("INSERT INTO ie_rule_removal_log
                (rule_id, sector, score_at_request, requested_by, notes)
                SELECT rule_id, sector, score, ?, ?
                FROM ie_rule_lifecycle WHERE rule_id = ?")
                ->execute([$requested_by, $notes, $rule_id]);

            // Move to PENDING_REMOVAL (NOT REMOVED — human must approve)
            $pdo->prepare("UPDATE ie_rule_lifecycle
                SET state = 'PENDING_REMOVAL',
                    state_reason = CONCAT('Removal requested by: ', ?),
                    removal_requested_at = NOW()
                WHERE rule_id = ? AND state != 'REMOVED'")
                ->execute([$requested_by, $rule_id]);
            return true;
        } catch (\Throwable $e) {
            error_log('[IELifecycle] requestRemoval: ' . $e->getMessage());
            return false;
        }
    }

    public static function approveRemoval(string $rule_id, string $approved_by): array
    {
        // TASK 4: Manual approval gate — NEVER auto-removes
        try {
            $pdo = db();
            // Mark approved in log
            $pdo->prepare("UPDATE ie_rule_removal_log SET
                approved_by = ?, approved_at = NOW()
                WHERE rule_id = ? AND approved_at IS NULL AND rejected_at IS NULL")
                ->execute([$approved_by, $rule_id]);
            // Move lifecycle to REMOVED
            $pdo->prepare("UPDATE ie_rule_lifecycle SET
                state = 'REMOVED',
                prev_state = 'PENDING_REMOVAL',
                state_reason = CONCAT('Removal approved by: ', ?),
                removal_approved_by = ?,
                removal_approved_at = NOW(),
                updated_at = NOW()
                WHERE rule_id = ? AND state = 'PENDING_REMOVAL'")
                ->execute([$approved_by, $approved_by, $rule_id]);

            return [
                'ok'      => true,
                'rule_id' => $rule_id,
                'action'  => 'REMOVED — delete $R[] block from InferenceEngine manually',
                'warning' => 'This only marks the record. Remove the PHP rule manually and deploy.',
            ];
        } catch (\Throwable $e) {
            return ['ok' => false, 'error' => $e->getMessage()];
        }
    }

    // ─────────────────────────────────────────────────────────
    // Internal: build human-readable reason string
    // ─────────────────────────────────────────────────────────
    private static function buildReason(
        string $old, string $new, float $score, int $weak, bool $is_core, bool $aged_out = false
    ): string {
        if ($old === $new) return "Score updated: {$score}";
        if ($is_core && $new === 'MONITORED') return "Core rule shielded from deprecation (score={$score})";
        if ($aged_out) return "aging: MONITORED ≥60 days with declining trend (score={$score})";
        return match("{$old}→{$new}") {
            'ACTIVE→MONITORED'           => "Score dropped to {$score} (below 70 threshold)",
            'MONITORED→ACTIVE'           => "Score recovered to {$score} (above 70 threshold)",
            'MONITORED→DEPRECATED'       => "Monitoring period expired or score={$score} (<50); weak_periods={$weak}",
            'DEPRECATED→MONITORED'       => "Score improved to {$score}, exiting DEPRECATED",
            'DEPRECATED→ACTIVE'          => "Score fully recovered to {$score}",
            'DEPRECATED→PENDING_REMOVAL' => "Score={$score} below 30 threshold after DEPRECATED",
            'MONITORED→PENDING_REMOVAL'  => "Score={$score} below 30 threshold",
            'ACTIVE→PENDING_REMOVAL'     => "Score={$score} critically low; weak_periods={$weak}",
            default                      => "{$old}→{$new}: score={$score}",
        };
    }

    // ─────────────────────────────────────────────────────────
    // TASK 5 — Full lifecycle run + report
    // ─────────────────────────────────────────────────────────
    public static function run(int $days = 30, bool $dry_run = false): array
    {
        echo $dry_run ? "[DRY RUN] " : "";
        echo "Loading IE rules...\n";
        $ie_rules = self::loadIERules();

        echo "Initializing lifecycle records for " . count($ie_rules) . " rules...\n";
        $initialized = self::initializeRules($ie_rules, $dry_run);

        echo "Running scorer...\n";
        $scoring_result = IERuleScoringEngine::run($days, false);

        if ($scoring_result['status'] !== 'OK') {
            return ['status' => 'NO_SCORING_DATA', 'message' => $scoring_result['message'] ?? ''];
        }

        $scored = $scoring_result['scored'];

        echo "Updating lifecycle states...\n";
        $transitions = self::updateLifecycle($scored, $dry_run);

        echo "Recording score history...\n";
        self::recordHistory($scored, $days, $dry_run);

        // Build state summary from DB (or transitions if dry-run)
        $state_summary = ['ACTIVE'=>0,'MONITORED'=>0,'DEPRECATED'=>0,'PENDING_REMOVAL'=>0,'REMOVED'=>0];
        if (!$dry_run) {
            try {
                $rows = db()->query("SELECT state, COUNT(*) as n FROM ie_rule_lifecycle GROUP BY state");
                foreach ($rows->fetchAll(\PDO::FETCH_ASSOC) as $r) {
                    $state_summary[$r['state']] = (int)$r['n'];
                }
            } catch (\Throwable $e) {}
        } else {
            // In dry-run, simulate from transitions
            $simulated = [];
            foreach ($scored as $s) {
                $st = self::scoreToState($s['score']);
                if (self::isCoreRule($s['rule_id'], $s['sector']) &&
                    in_array($st, ['DEPRECATED','PENDING_REMOVAL'])) $st = 'MONITORED';
                $simulated[$st] = ($simulated[$st] ?? 0) + 1;
            }
            foreach ($simulated as $k => $v) $state_summary[$k] = $v;
        }

        return [
            'status'        => 'OK',
            'dry_run'       => $dry_run,
            'period_days'   => $days,
            'ie_rules_total'=> count($ie_rules),
            'rules_initialized' => $initialized,
            'rules_scored'  => count($scored),
            'transitions'   => $transitions,
            'state_summary' => $state_summary,
            'scored'        => $scored,
            'never_fired'   => $scoring_result['never_fired'],
        ];
    }

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

        echo "\n{$sep}\n";
        echo "  IE RULE LIFECYCLE ENGINE — PHASE 7 REPORT\n";
        echo "{$sep}\n";
        if ($result['dry_run'] ?? false) echo "  ⚠️  DRY RUN — no DB changes made\n\n";

        printf("  IE rules total:     %d\n", $result['ie_rules_total']);
        printf("  Rules scored:       %d\n", $result['rules_scored']);
        printf("  New initializations:%d\n", $result['rules_initialized']);
        printf("  State transitions:  %d\n", count($result['transitions']));
        echo "\n";

        // State summary
        $states = $result['state_summary'];
        $total  = array_sum($states);
        $icons  = [
            'ACTIVE'          => '✅',
            'MONITORED'       => '🔵',
            'DEPRECATED'      => '⚠️',
            'PENDING_REMOVAL' => '🔴',
            'REMOVED'         => '⬛',
        ];
        echo "  {$dash}\n";
        echo "  LIFECYCLE STATE DISTRIBUTION\n";
        echo "  {$dash}\n";
        foreach ($states as $state => $n) {
            $pct = $total > 0 ? round(100*$n/$total, 1) : 0;
            $bar = str_repeat('█', max(0, (int)($pct/2)));
            printf("  %s %-18s %4d (%5.1f%%)  %s\n", $icons[$state]??'?', $state, $n, $pct, $bar);
        }

        // Transitions
        if (!empty($result['transitions'])) {
            echo "\n  {$dash}\n";
            echo "  STATE TRANSITIONS THIS RUN\n";
            echo "  {$dash}\n";
            $trans_by_type = [];
            foreach ($result['transitions'] as $t) {
                $key = "{$t['old_state']} → {$t['new_state']}";
                $trans_by_type[$key][] = $t;
            }
            foreach ($trans_by_type as $type => $rules) {
                printf("  %s  (%d rules)\n", $type, count($rules));
                foreach (array_slice($rules, 0, 5) as $r) {
                    $core_tag = $r['is_core'] ? ' [CORE]' : '';
                    $conf_tag = $r['conf_before'] != $r['conf_after']
                        ? sprintf(" | conf %.2f→%.2f", $r['conf_before'], $r['conf_after']) : '';
                    printf("    %-30s score=%.1f%s%s\n",
                        $r['rule_id'], $r['score'], $conf_tag, $core_tag);
                }
                if (count($rules) > 5) printf("    ... and %d more\n", count($rules)-5);
            }
        }

        // Pending removals
        $pending = array_filter($result['scored'] ?? [],
            fn($s) => self::scoreToState($s['score']) === 'PENDING_REMOVAL');
        if (!empty($pending)) {
            echo "\n  {$dash}\n";
            echo "  PENDING REMOVAL (manual approval required)\n";
            echo "  {$dash}\n";
            foreach ($pending as $r) {
                printf("  🔴 %-32s score=%.1f  sector=%s\n",
                    $r['rule_id'], $r['score'], $r['sector']);
                echo "     Action: call approveRemoval('{$r['rule_id']}', 'admin_name')\n";
                echo "     Then:   manually delete \$R[] block from InferenceEngine.php\n";
            }
        }

        // Never fired
        $nf = $result['never_fired']['never_fired_count'] ?? 0;
        if ($nf > 0) {
            echo "\n  {$dash}\n";
            echo "  NEVER FIRED ({$nf} rules) — consider after 90 days of data\n";
            echo "  {$dash}\n";
            foreach (array_slice($result['never_fired']['never_fired'] ?? [], 0, 8) as $rid) {
                echo "  ⚪ {$rid}\n";
            }
            if ($nf > 8) echo "  ... and " . ($nf-8) . " more\n";
        }

        // Actions guide
        echo "\n  {$sep}\n";
        echo "  AUTOMATED ACTIONS TAKEN\n";
        echo "  {$sep}\n";
        echo "  ✅ ACTIVE          → No action. Monitoring score drift.\n";
        echo "  🔵 MONITORED       → 30-day watch. Will DEPRECATE if score stays < 70.\n";
        echo "  ⚠️  DEPRECATED      → Confidence reduced by " . (self::CONF_REDUCTION * 100) . "pp.\n";
        echo "                       Rule still fires. AI enriches via hybrid path.\n";
        echo "  🔴 PENDING_REMOVAL → Flagged for human review. NOT deleted.\n";
        echo "                       Requires: approveRemoval(rule_id, admin_name)\n";
        echo "  ⬛ REMOVED         → Marked only. Must delete \$R[] in IE manually.\n";
        echo "\n  ⚠️  NO RULES WERE AUTO-DELETED. All removals require manual approval.\n";
        echo "{$sep}\n\n";
    }
}

// ═════════════════════════════════════════════════════════════════
// CLI RUNNER
// ═════════════════════════════════════════════════════════════════
if (php_sapi_name() === 'cli') {
    $opts    = getopt('', ['days:', 'dry-run', 'json', 'approve:', 'help']);

    if (isset($opts['help'])) {
        echo "Usage: php engine/IERuleLifecycleEngine.php [options]\n";
        echo "  --days=N          Analysis window (default: 30)\n";
        echo "  --dry-run         Simulate without DB writes\n";
        echo "  --json            JSON output\n";
        echo "  --approve=RULE_ID Approve a PENDING_REMOVAL rule (requires ADMIN confirmation)\n";
        exit(0);
    }

    if (isset($opts['approve'])) {
        $rule_id = $opts['approve'];
        echo "⚠️  You are approving REMOVAL of rule: {$rule_id}\n";
        echo "This cannot be undone via this tool. Type 'CONFIRM' to proceed: ";
        $confirm = trim(fgets(STDIN));
        if ($confirm !== 'CONFIRM') { echo "Aborted.\n"; exit(1); }
        $result = IERuleLifecycleEngine::approveRemoval($rule_id, get_current_user());
        echo json_encode($result, JSON_PRETTY_PRINT) . "\n";
        exit(0);
    }

    $days    = (int)($opts['days'] ?? 30);
    $dry_run = isset($opts['dry-run']);
    $as_json = isset($opts['json']);

    $result = IERuleLifecycleEngine::run($days, $dry_run);

    if ($as_json) {
        unset($result['scored']);  // too large for CLI json output
        echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    } else {
        IERuleLifecycleEngine::printReport($result);
    }
}
