<?php
/**
 * Coraza Rules — admin page for runtime rule tuning.
 * Root-only. Firing-rules tab driven by coraza_rule_hits (populated by the
 * 5-min audit-log rollup cron).
 */

if (!defined('AUTH_USER') || AUTH_USER !== 'root') {
    header('Location: index.php?page=dashboard');
    exit;
}

require_once('/docker/whp/web/libs/coraza_rule_manager.php');
require_once('/docker/whp/web/libs/coraza_rule_state.php');
require_once('/docker/whp/web/libs/coraza_rule_families.php');

$m = new coraza_rule_manager();
$overrides = [];
foreach ($m->list_all_overrides() as $row) {
    $overrides[(int)$row['rule_id']] = $row;
}
$host_exceptions = $m->list_all_host_exceptions();
$window = ($_GET['window'] ?? '24h') === '7d' ? '7d' : '24h';
$global_mode = coraza_global_mode();

// coraza_rule_hits is guaranteed to exist by the manager's ensure_schema()
// (run in the constructor above). Guard prepare() anyway so a transient DB
// failure degrades to an empty table instead of a fatal that hangs the
// AJAX-loaded page on its loading spinner.
$db = new SQLite3('/docker/whp/sql/security.db');
$db->busyTimeout(10000);
$rows = [];
$stmt = $db->prepare('SELECT rule_id, hits, top_hosts, top_uris FROM coraza_rule_hits
                      WHERE window = :w ORDER BY hits DESC LIMIT 200');
if ($stmt !== false) {
    $stmt->bindValue(':w', $window, SQLITE3_TEXT);
    $res = $stmt->execute();
    while ($res !== false && ($r = $res->fetchArray(SQLITE3_ASSOC))) { $rows[] = $r; }
}
?>
<div class="d-flex justify-content-between align-items-center mb-3">
    <?php
        $mode_label = ['off' => 'OFF', 'detect_only' => 'detect-only', 'enforce' => 'enforce'][$global_mode] ?? $global_mode;
        $mode_class = ['off' => 'bg-secondary', 'detect_only' => 'bg-info text-dark', 'enforce' => 'bg-success'][$global_mode] ?? 'bg-secondary';
    ?>
    <div>
        <span class="small text-muted">Global WAF mode:</span>
        <span class="badge <?= htmlspecialchars($mode_class) ?>"><?= htmlspecialchars($mode_label) ?></span>
        <a href="?page=server-settings#waf" class="small ms-2">change</a>
    </div>
    <div class="btn-group" role="group">
        <a href="?page=coraza-rules&window=24h"
           class="btn btn-sm btn-<?= $window === '24h' ? 'primary' : 'outline-primary' ?>">Last 24h</a>
        <a href="?page=coraza-rules&window=7d"
           class="btn btn-sm btn-<?= $window === '7d' ? 'primary' : 'outline-primary' ?>">Last 7d</a>
    </div>
</div>

<ul class="nav nav-tabs mb-3" role="tablist">
    <li class="nav-item"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-firing" type="button">Firing rules</button></li>
    <li class="nav-item"><button class="nav-link"        data-bs-toggle="tab" data-bs-target="#tab-catalog" type="button">CRS catalog</button></li>
    <li class="nav-item"><button class="nav-link"        data-bs-toggle="tab" data-bs-target="#tab-activity" type="button">Activity</button></li>
</ul>
<div class="tab-content">
    <div class="tab-pane fade show active" id="tab-firing">

        <p class="text-muted small">
            Tunes the Coraza WAF without rebuilding the sidecar image.
            Source of truth: <code>security.db</code>. Reload via SIGHUP after every save (~10ms, no traffic blip).
        </p>

        <?php if (empty($rows)): ?>
            <div class="alert alert-info">No rule activity yet. Rollup runs every 5 minutes from <code>/var/log/coraza/audit.log</code>.</div>
        <?php else: ?>
        <table class="table table-sm table-hover align-middle">
            <thead>
                <tr>
                    <th>Rule ID</th>
                    <th class="text-end">Hits</th>
                    <th>Top hosts</th>
                    <th>Top URIs</th>
                    <th>State</th>
                    <th>Per-host</th>
                </tr>
            </thead>
            <tbody>
            <?php foreach ($rows as $r): $rid = (int)$r['rule_id']; ?>
                <?php
                    $state = $overrides[$rid]['state'] ?? 'default';
                    $hosts = json_decode($r['top_hosts'], true) ?: [];
                    $uris  = json_decode($r['top_uris'], true)  ?: [];
                ?>
                <tr data-rule-id="<?= $rid ?>">
                    <td>
                        <a href="#" class="coraza-rule-detail" data-rule-id="<?= $rid ?>" title="View rule details"><code><?= $rid ?></code></a>
                        <a href="#" class="coraza-view-matches small ms-2" data-rule-id="<?= $rid ?>">view</a>
                        <a href="#" class="coraza-ask-ai small ms-2" data-rule-id="<?= $rid ?>"
                           title="Explain with AI"><i class="fa fa-magic"></i> Ask AI</a>
                    </td>
                    <td class="text-end"><?= (int)$r['hits'] ?></td>
                    <td class="small">
                        <?php foreach (array_slice($hosts, 0, 3) as $h): ?>
                            <?= htmlspecialchars($h['host']) ?> <span class="text-muted">(<?= (int)$h['n'] ?>)</span><br>
                        <?php endforeach; ?>
                    </td>
                    <td class="small text-truncate" style="max-width:280px;">
                        <?php foreach (array_slice($uris, 0, 3) as $u): ?>
                            <code><?= htmlspecialchars($u['uri']) ?></code><br>
                        <?php endforeach; ?>
                    </td>
                    <td>
                        <?php $def = coraza_default_state($rid, $global_mode); ?>
                        <select class="form-select form-select-sm coraza-state-select" style="min-width: 8rem;" data-rule-id="<?= $rid ?>">
                            <option value="default" <?= $state === 'default' ? 'selected' : '' ?>>(default &rarr; <?= htmlspecialchars($def['label']) ?>)</option>
                            <option value="enforce" <?= $state === 'enforce' ? 'selected' : '' ?>>enforce</option>
                            <option value="detect"  <?= $state === 'detect'  ? 'selected' : '' ?>>detect</option>
                            <option value="off"     <?= $state === 'off'     ? 'selected' : '' ?>>off</option>
                        </select>
                        <?php if ($state === 'default'): ?>
                            <span class="badge <?= htmlspecialchars($def['class']) ?> mt-1 d-inline-block" title="<?= htmlspecialchars($def['reason']) ?>" style="cursor: help;"><?= htmlspecialchars($def['label']) ?></span>
                        <?php endif; ?>
                    </td>
                    <td>
                        <?php $n = count($host_exceptions[$rid] ?? []); ?>
                        <button class="btn btn-sm btn-outline-secondary coraza-host-edit" data-rule-id="<?= $rid ?>">
                            Hosts (<?= $n ?>)
                        </button>
                    </td>
                </tr>
            <?php endforeach; ?>
            </tbody>
        </table>
        <?php endif; ?>

    </div>

    <div class="tab-pane fade" id="tab-catalog">
        <?php
            $catalog = file_exists('/docker/coraza/rules-catalog.json')
                ? json_decode(file_get_contents('/docker/coraza/rules-catalog.json'), true)
                : [];
        ?>
        <p class="text-muted small">All <?= count($catalog) ?> rules in the bundled CRS catalog. Search by ID, message, or tag.</p>

        <div class="input-group mb-2">
            <span class="input-group-text"><i class="fa fa-magic"></i></span>
            <input type="text" class="form-control" id="catAiSearch"
                   placeholder="Ask AI: find rules about... (e.g. 'file upload payloads', 'SQL injection in JSON bodies')">
            <button class="btn btn-outline-primary" id="catAiSearchBtn" type="button">Search with AI</button>
            <button class="btn btn-outline-secondary" id="catAiSearchClear" type="button" style="display:none;">Clear</button>
        </div>
        <div id="catAiExplain" class="small text-muted mb-2" style="min-height: 1em;"></div>

        <input type="text" class="form-control mb-3" id="catSearch" placeholder="Or filter by ID, message, tag...">
        <table class="table table-sm">
            <thead><tr><th>Rule ID</th><th>Severity</th><th>Message</th><th>Tags</th><th>State</th></tr></thead>
            <tbody id="catBody">
            <?php foreach ($catalog as $rule): $rid = (int)$rule['id']; ?>
                <?php $state = $overrides[$rid]['state'] ?? 'default'; $def = coraza_default_state($rid, $global_mode); ?>
                <tr data-rule-id="<?= $rid ?>" data-search="<?= htmlspecialchars(strtolower("$rid {$rule['msg']} " . implode(' ', $rule['tags'] ?? []))) ?>">
                    <td><a href="#" class="coraza-rule-detail" data-rule-id="<?= $rid ?>" title="View rule details"><code><?= $rid ?></code></a></td>
                    <td><span class="badge bg-secondary"><?= htmlspecialchars($rule['severity'] ?? '') ?></span></td>
                    <td class="small"><?= htmlspecialchars($rule['msg'] ?? '') ?></td>
                    <td class="small text-muted"><?= htmlspecialchars(implode(', ', $rule['tags'] ?? [])) ?></td>
                    <td>
                        <select class="form-select form-select-sm coraza-state-select" style="min-width: 8rem;" data-rule-id="<?= $rid ?>">
                            <option value="default" <?= $state === 'default' ? 'selected' : '' ?>>(default &rarr; <?= htmlspecialchars($def['label']) ?>)</option>
                            <option value="enforce" <?= $state === 'enforce' ? 'selected' : '' ?>>enforce</option>
                            <option value="detect"  <?= $state === 'detect'  ? 'selected' : '' ?>>detect</option>
                            <option value="off"     <?= $state === 'off'     ? 'selected' : '' ?>>off</option>
                        </select>
                    </td>
                </tr>
            <?php endforeach; ?>
            </tbody>
        </table>
    </div>

    <div class="tab-pane fade" id="tab-activity">
        <p class="text-muted small">Most recent 100 changes.</p>
        <table class="table table-sm">
            <thead><tr><th>When</th><th>Actor</th><th>Rule</th><th>Action</th><th>Details</th></tr></thead>
            <tbody>
            <?php foreach ($m->recent_audit(100) as $a): ?>
                <tr>
                    <td class="small text-nowrap"><?= htmlspecialchars(date('Y-m-d H:i:s', (int)$a['ts'])) ?></td>
                    <td><?= htmlspecialchars((string)$a['actor']) ?></td>
                    <td><code><?= (int)$a['rule_id'] ?></code></td>
                    <td><?= htmlspecialchars((string)$a['action']) ?></td>
                    <td class="small text-muted"><?= htmlspecialchars(json_encode($a['payload'])) ?></td>
                </tr>
            <?php endforeach; ?>
            </tbody>
        </table>
    </div>
</div>

<input type="hidden" id="csrf-token" value="<?= htmlspecialchars($_SESSION['csrf_token'] ?? '') ?>">

<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.coraza-state-select').forEach(sel => {
    sel.addEventListener('change', async ev => {
        const ruleId = ev.target.dataset.ruleId;
        const state = ev.target.value;
        const params = new URLSearchParams({
            rule_id: ruleId,
            csrf_token: document.getElementById('csrf-token').value,
        });
        if (state === 'default') {
            params.set('action', 'clear_state');
        } else {
            params.set('action', 'set_state');
            params.set('state', state);
        }
        const r = await fetch('api/coraza-rules-save.php', { method: 'POST', body: params });
        const j = await r.json();
        if (j.ok) {
            ev.target.classList.add('is-valid');
            setTimeout(() => ev.target.classList.remove('is-valid'), 1500);
        } else {
            alert('Save failed: ' + j.error);
            window.location.reload();
        }
    });
});

document.getElementById('catSearch')?.addEventListener('input', ev => {
    const q = ev.target.value.toLowerCase();
    document.querySelectorAll('#catBody tr').forEach(tr => {
        tr.style.display = (q === '' || tr.dataset.search.includes(q)) ? '' : 'none';
    });
});
}); // end DOMContentLoaded
</script>

<div class="modal fade" id="coraza-ai-modal" tabindex="-1">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Ask AI &mdash; rule <code id="aiRuleId"></code></h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
                <div id="aiLoading" class="text-center py-4">
                    <div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>
                    <p class="small text-muted mt-2 mb-0">Consulting AI (uses the AI Dashboard's model)...</p>
                </div>
                <div id="aiError" class="alert alert-danger" style="display:none;"></div>
                <div id="aiResult" style="display:none;">
                    <div class="mb-2">
                        <span class="badge" id="aiVerdictBadge"></span>
                        <small class="text-muted ms-2" id="aiContext"></small>
                    </div>
                    <p id="aiExplanation"></p>
                    <hr>
                    <h6 class="small text-uppercase text-muted">Suggested action</h6>
                    <div id="aiSuggestionBody"></div>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
                <button type="button" class="btn btn-success" id="aiApplyBtn" style="display:none;">Apply suggestion</button>
            </div>
        </div>
    </div>
</div>

<script>
document.addEventListener('DOMContentLoaded', () => {
const aiModalEl = document.getElementById('coraza-ai-modal');
const getAiModal = () => bootstrap.Modal.getOrCreateInstance(aiModalEl);
aiModalEl.querySelectorAll('[data-bs-dismiss="modal"]').forEach(b => {
    b.addEventListener('click', ev => { ev.preventDefault(); getAiModal().hide(); });
});
let aiCurrentSuggestion = null;

function aiResetView() {
    document.getElementById('aiLoading').style.display = '';
    document.getElementById('aiError').style.display = 'none';
    document.getElementById('aiResult').style.display = 'none';
    document.getElementById('aiApplyBtn').style.display = 'none';
    aiCurrentSuggestion = null;
}
function aiShowError(msg) {
    document.getElementById('aiLoading').style.display = 'none';
    const e = document.getElementById('aiError');
    e.textContent = msg;
    e.style.display = '';
}
function aiClearChildren(el) { while (el.firstChild) el.removeChild(el.firstChild); }

function aiRenderSuggestion(s) {
    const body = document.getElementById('aiSuggestionBody');
    aiClearChildren(body);
    if (!s) { body.textContent = '(no suggestion)'; return; }
    if (s.type === 'add_host_exception') {
        const p = document.createElement('p');
        p.appendChild(document.createTextNode('Add a host exception for '));
        const c1 = document.createElement('code'); c1.textContent = s.hostname; p.appendChild(c1);
        p.appendChild(document.createTextNode(' on rule '));
        const c2 = document.createElement('code'); c2.textContent = String(s.rule_id); p.appendChild(c2);
        p.appendChild(document.createTextNode('.'));
        body.appendChild(p);
        const r = document.createElement('p');
        r.className = 'small text-muted mb-0';
        r.textContent = s.reason || '';
        body.appendChild(r);
        aiCurrentSuggestion = s;
        document.getElementById('aiApplyBtn').style.display = '';
    } else {
        const p = document.createElement('p');
        p.textContent = (s.type === 'no_action')
            ? 'No change recommended — the block looks correct.'
            : 'Investigate further before changing rule state.';
        body.appendChild(p);
        const r = document.createElement('p');
        r.className = 'small text-muted mb-0';
        r.textContent = s.reason || '';
        body.appendChild(r);
    }
}

document.querySelectorAll('.coraza-ask-ai').forEach(a => {
    a.addEventListener('click', async ev => {
        ev.preventDefault();
        const rid = a.dataset.ruleId;
        const host = a.dataset.host || '';
        document.getElementById('aiRuleId').textContent = rid;
        aiResetView();
        getAiModal().show();
        const body = new URLSearchParams({
            rule_id: rid, host: host,
            csrf_token: document.getElementById('csrf-token').value,
        });
        let j;
        try {
            const r = await fetch('api/coraza-ai-explain.php', { method: 'POST', body });
            j = await r.json();
        } catch (e) {
            aiShowError('Network error: ' + e.message);
            return;
        }
        if (!j.ok) { aiShowError(j.error || 'AI request failed.'); return; }

        document.getElementById('aiLoading').style.display = 'none';
        document.getElementById('aiResult').style.display = '';
        const badge = document.getElementById('aiVerdictBadge');
        const verdictMap = {
            false_positive: ['False positive likely', 'bg-warning text-dark'],
            suspicious:     ['Suspicious', 'bg-info text-dark'],
            real_attack:    ['Real attack', 'bg-danger'],
        };
        const [label, cls] = verdictMap[j.verdict] || [j.verdict, 'bg-secondary'];
        badge.className = 'badge ' + cls;
        badge.textContent = label;
        const ctx = j.context || {};
        document.getElementById('aiContext').textContent =
            `${ctx.method || ''} ${ctx.host || ''}${ctx.uri || ''} · ${j.model || ''}`;
        document.getElementById('aiExplanation').textContent = j.explanation || '';
        aiRenderSuggestion(j.suggestion);
    });
});

document.getElementById('aiApplyBtn').addEventListener('click', async () => {
    if (!aiCurrentSuggestion || aiCurrentSuggestion.type !== 'add_host_exception') return;
    const btn = document.getElementById('aiApplyBtn');
    btn.disabled = true; btn.textContent = 'Applying...';
    const body = new URLSearchParams({
        action: 'add_host',
        rule_id: aiCurrentSuggestion.rule_id,
        hostname: aiCurrentSuggestion.hostname,
        note: 'AI-suggested: ' + (aiCurrentSuggestion.reason || '').slice(0, 200),
        csrf_token: document.getElementById('csrf-token').value,
    });
    const r = await fetch('api/coraza-rules-save.php', { method: 'POST', body });
    const j = await r.json();
    if (!j.ok) {
        btn.disabled = false; btn.textContent = 'Apply suggestion';
        alert('Apply failed: ' + j.error);
        return;
    }
    btn.className = 'btn btn-success';
    btn.textContent = '✓ Applied — reloading...';
    setTimeout(() => window.location.reload(), 600);
});

// --- Catalog AI search ---
const aiSearchInput = document.getElementById('catAiSearch');
const aiSearchBtn   = document.getElementById('catAiSearchBtn');
const aiSearchClear = document.getElementById('catAiSearchClear');
const aiSearchExp   = document.getElementById('catAiExplain');
const catSearchEl   = document.getElementById('catSearch');

function catShowAll() {
    document.querySelectorAll('#catBody tr').forEach(tr => { tr.style.display = ''; });
}
function catShowOnlyIds(ids) {
    const set = new Set(ids.map(String));
    document.querySelectorAll('#catBody tr').forEach(tr => {
        tr.style.display = set.has(tr.dataset.ruleId) ? '' : 'none';
    });
}

async function runAiSearch() {
    const q = aiSearchInput.value.trim();
    if (!q) return;
    aiSearchBtn.disabled = true;
    const originalLabel = aiSearchBtn.textContent;
    aiSearchBtn.textContent = 'Searching...';
    aiSearchExp.textContent = '';
    const body = new URLSearchParams({
        query: q,
        csrf_token: document.getElementById('csrf-token').value,
    });
    let j;
    try {
        const r = await fetch('api/coraza-ai-search.php', { method: 'POST', body });
        j = await r.json();
    } catch (e) {
        aiSearchExp.textContent = 'Network error: ' + e.message;
        aiSearchBtn.disabled = false; aiSearchBtn.textContent = originalLabel;
        return;
    }
    aiSearchBtn.disabled = false; aiSearchBtn.textContent = originalLabel;
    if (!j.ok) { aiSearchExp.textContent = j.error || 'AI search failed.'; return; }
    if (!j.ids || j.ids.length === 0) {
        aiSearchExp.textContent = j.explanation || 'No rules matched.';
        return;
    }
    catShowOnlyIds(j.ids);
    catSearchEl.value = '';
    aiSearchClear.style.display = '';
    aiSearchExp.textContent = `${j.ids.length} rule(s): ${j.explanation || ''} · ${j.model || ''}`;
}

aiSearchBtn?.addEventListener('click', runAiSearch);
aiSearchInput?.addEventListener('keydown', ev => {
    if (ev.key === 'Enter') { ev.preventDefault(); runAiSearch(); }
});
aiSearchClear?.addEventListener('click', () => {
    aiSearchInput.value = '';
    aiSearchExp.textContent = '';
    aiSearchClear.style.display = 'none';
    catShowAll();
});
}); // end DOMContentLoaded
</script>

<div class="modal fade" id="hostExceptionModal" tabindex="-1">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Host exceptions for rule <code id="hxRuleId"></code></h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
                <p class="small text-muted">
                    Hostnames listed here are exempt from the rule. Useful for
                    keeping a rule enforced server-wide while letting it pass
                    for specific sites (e.g. Mastodon federation on HPR).
                </p>
                <div class="input-group mb-3">
                    <input type="text" class="form-control" id="hxHostInput" placeholder="hostname" autocomplete="off">
                    <button class="btn btn-primary" id="hxAddBtn">Add</button>
                </div>
                <ul class="list-group" id="hxHostList"></ul>
            </div>
        </div>
    </div>
</div>

<script>
document.addEventListener('DOMContentLoaded', () => {
// Use getOrCreateInstance — bootstrap5-native.js (loaded in the page footer)
// runs its own DOMContentLoaded handler that auto-instantiates a Modal for
// every .modal element. Calling `new bootstrap.Modal(...)` would create a
// second instance that replaces ours in Bootstrap's data map, leaving the
// close button (which looks up via the data map) out of sync.
const hxModalEl = document.getElementById('hostExceptionModal');
const getHxModal = () => bootstrap.Modal.getOrCreateInstance(hxModalEl);
// Belt-and-suspenders: bind close button explicitly. Bootstrap's data-bs-dismiss
// delegation can land on the wrong instance when bootstrap5-native's auto-init
// races with our own — symptom: clicking X does nothing.
hxModalEl.querySelectorAll('[data-bs-dismiss="modal"]').forEach(b => {
    b.addEventListener('click', ev => { ev.preventDefault(); getHxModal().hide(); });
});
let hxCurrentRule = null;

async function hxLoadHosts(ruleId) {
    const r = await fetch('api/coraza-rules-host-list.php?rule_id=' + encodeURIComponent(ruleId));
    const list = await r.json();
    const ul = document.getElementById('hxHostList');
    ul.innerHTML = '';
    for (const h of list) {
        const li = document.createElement('li');
        li.className = 'list-group-item d-flex justify-content-between align-items-center';
        const code = document.createElement('code');
        code.textContent = h.hostname;
        const btn = document.createElement('button');
        btn.className = 'btn btn-sm btn-outline-danger hx-remove';
        btn.dataset.host = h.hostname;
        btn.textContent = 'Remove';
        li.appendChild(code);
        li.appendChild(btn);
        ul.appendChild(li);
    }
    ul.querySelectorAll('.hx-remove').forEach(b => {
        b.addEventListener('click', () => hxRemove(b.dataset.host));
    });
}

async function hxAdd(host) {
    const body = new URLSearchParams({
        action: 'add_host', rule_id: hxCurrentRule, hostname: host,
        csrf_token: document.getElementById('csrf-token').value,
    });
    const r = await fetch('api/coraza-rules-save.php', { method: 'POST', body });
    const j = await r.json();
    if (!j.ok) { alert(j.error); return; }
    await hxLoadHosts(hxCurrentRule);
    document.getElementById('hxHostInput').value = '';
}

async function hxRemove(host) {
    const body = new URLSearchParams({
        action: 'remove_host', rule_id: hxCurrentRule, hostname: host,
        csrf_token: document.getElementById('csrf-token').value,
    });
    const r = await fetch('api/coraza-rules-save.php', { method: 'POST', body });
    const j = await r.json();
    if (!j.ok) { alert(j.error); return; }
    await hxLoadHosts(hxCurrentRule);
}

document.querySelectorAll('.coraza-host-edit').forEach(btn => {
    btn.addEventListener('click', async () => {
        hxCurrentRule = btn.dataset.ruleId;
        document.getElementById('hxRuleId').textContent = hxCurrentRule;
        document.getElementById('hxHostInput').value = '';
        await hxLoadHosts(hxCurrentRule);
        getHxModal().show();
    });
});
document.getElementById('hxAddBtn').addEventListener('click', () => {
    const v = document.getElementById('hxHostInput').value.trim();
    if (v) hxAdd(v);
});
document.getElementById('hxHostInput').addEventListener('keydown', ev => {
    if (ev.key === 'Enter') {
        ev.preventDefault();
        const v = ev.target.value.trim();
        if (v) hxAdd(v);
    }
});
}); // end DOMContentLoaded
</script>

<div class="offcanvas offcanvas-end" tabindex="-1" id="matchesPanel" style="width: 600px;">
    <div class="offcanvas-header">
        <h5 class="offcanvas-title">Recent matches: <code id="mpRuleId"></code></h5>
        <button class="btn-close" data-bs-dismiss="offcanvas"></button>
    </div>
    <div class="offcanvas-body small" id="mpBody">Loading...</div>
</div>

<script>
document.addEventListener('DOMContentLoaded', () => {
// See modal block above for why we use getOrCreateInstance instead of `new`.
const matchesPanelEl = document.getElementById('matchesPanel');
const getMatchesPanel = () => bootstrap.Offcanvas.getOrCreateInstance(matchesPanelEl);
// Belt-and-suspenders: bind close-button explicitly. Bootstrap's data-bs-dismiss
// delegation can land on the wrong instance when bootstrap5-native's
// auto-init races with our own.
matchesPanelEl.querySelectorAll('[data-bs-dismiss="offcanvas"]').forEach(b => {
    b.addEventListener('click', ev => { ev.preventDefault(); getMatchesPanel().hide(); });
});
document.querySelectorAll('.coraza-view-matches').forEach(a => {
    a.addEventListener('click', async ev => {
        ev.preventDefault();
        const rid = a.dataset.ruleId;
        document.getElementById('mpRuleId').textContent = rid;
        document.getElementById('mpBody').textContent = 'Loading...';
        getMatchesPanel().show();
        const r = await fetch('api/coraza-rules-recent-matches.php?rule_id=' + encodeURIComponent(rid));
        const list = await r.json();
        const body = document.getElementById('mpBody');
        if (list.length === 0) {
            body.textContent = 'No recent matches.';
            return;
        }
        body.innerHTML = '';
        for (const m of list) {
            const div = document.createElement('div');
            div.className = 'border-bottom py-2';
            const meta = document.createElement('div');
            meta.className = 'text-muted';
            meta.textContent = `${m.ts} — ${m.client} → ${m.host}`;
            const req = document.createElement('div');
            const code = document.createElement('code');
            code.textContent = `${m.method} ${m.uri}`;
            req.appendChild(code);
            div.appendChild(meta);
            div.appendChild(req);
            body.appendChild(div);
        }
    });
});
}); // end DOMContentLoaded
</script>

<?php
// Build a compact lookup table for the rule-details modal. Includes catalog
// entries (for CRS rules) and synthesizes minimal records for engine-built-ins
// + any other firing-tab rules that aren't in the catalog.
$rule_meta_map = [];
$rule_meta_builder = function (int $rid, array $base) use ($global_mode): array {
    $fam = coraza_rule_family_for($rid);
    return $base + [
        'id'      => $rid,
        'origin'  => coraza_rule_origin($rid),
        'default' => coraza_default_state($rid, $global_mode),
        'doc_url' => coraza_rule_doc_url($rid),
        'family'  => $fam === null ? null : [
            'key'         => $fam['key'] ?? '',
            'name'        => $fam['name'] ?? '',
            'description' => $fam['description'] ?? '',
            'common_fp'   => $fam['common_fp'] ?? '',
            'tuning'      => $fam['tuning'] ?? '',
            'file'        => $fam['file'] ?? null,
        ],
    ];
};
foreach ($catalog as $r) {
    $rid = (int)$r['id'];
    $rule_meta_map[$rid] = $rule_meta_builder($rid, [
        'msg'      => (string)($r['msg'] ?? ''),
        'severity' => (string)($r['severity'] ?? ''),
        'tags'     => (array)($r['tags'] ?? []),
    ]);
}
// Add any firing-tab rules not in catalog (engine rules, custom overrides).
foreach ($rows as $r_row) {
    $rid = (int)$r_row['rule_id'];
    if (!isset($rule_meta_map[$rid])) {
        $rule_meta_map[$rid] = $rule_meta_builder($rid, [
            'msg'      => '(not in CRS catalog — see audit-log entries for live message)',
            'severity' => '',
            'tags'     => [],
        ]);
    }
}
?>

<div class="modal fade" id="coraza-rule-detail-modal" tabindex="-1">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Rule <code id="rdRuleId"></code> &mdash; <span id="rdOrigin" class="text-muted small"></span></h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
                <p class="lead" id="rdMsg"></p>

                <dl class="row mb-3">
                    <dt class="col-sm-3">Severity</dt>
                    <dd class="col-sm-9"><span class="badge bg-secondary" id="rdSeverity"></span></dd>

                    <dt class="col-sm-3">Default behavior</dt>
                    <dd class="col-sm-9">
                        <span class="badge" id="rdDefaultLabel"></span>
                        <div class="small text-muted mt-1" id="rdDefaultReason"></div>
                    </dd>

                    <dt class="col-sm-3">Current override</dt>
                    <dd class="col-sm-9" id="rdCurrent"></dd>

                    <dt class="col-sm-3">Tags</dt>
                    <dd class="col-sm-9 small text-muted" id="rdTags"></dd>
                </dl>

                <div id="rdFamilyCard" class="card mb-3" style="display:none;">
                    <div class="card-body">
                        <h6 class="card-title mb-1"><span id="rdFamilyName"></span> <small class="text-muted ms-2" id="rdFamilyKey"></small></h6>
                        <p class="small mb-2" id="rdFamilyDesc"></p>
                        <p class="small mb-1"><strong>Common false-positives:</strong> <span id="rdFamilyFp"></span></p>
                        <p class="small mb-0"><strong>Tuning tip:</strong> <span id="rdFamilyTuning"></span></p>
                    </div>
                </div>

                <div class="small">
                    <a href="#" id="rdDocLink" target="_blank" rel="noopener" style="display:none;"><i class="fa fa-external-link"></i> Open reference docs</a>
                    <span class="mx-2 text-muted" id="rdDocSep" style="display:none;">·</span>
                    <a href="#" id="rdAskAiLink"><i class="fa fa-magic"></i> Ask AI about this rule</a>
                    <span class="mx-2 text-muted">·</span>
                    <a href="#" id="rdViewMatchesLink">View recent matches</a>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
            </div>
        </div>
    </div>
</div>

<script>
window.CORAZA_RULE_META = <?= json_encode($rule_meta_map, JSON_UNESCAPED_SLASHES) ?>;
window.CORAZA_OVERRIDES = <?= json_encode(array_map(function($o) { return $o['state']; }, $overrides), JSON_UNESCAPED_SLASHES) ?>;

document.addEventListener('DOMContentLoaded', () => {
const rdModalEl = document.getElementById('coraza-rule-detail-modal');
const getRdModal = () => bootstrap.Modal.getOrCreateInstance(rdModalEl);
rdModalEl.querySelectorAll('[data-bs-dismiss="modal"]').forEach(b => {
    b.addEventListener('click', ev => { ev.preventDefault(); getRdModal().hide(); });
});

function openRuleDetail(rid) {
    const meta = window.CORAZA_RULE_META[rid] || {
        id: rid, msg: '(unknown rule)', severity: '', tags: [],
        origin: 'unknown',
        default: {label: '?', class: 'bg-secondary', reason: 'Not enough info to resolve default behavior.'},
    };
    document.getElementById('rdRuleId').textContent = rid;
    document.getElementById('rdOrigin').textContent = meta.origin || '';
    document.getElementById('rdMsg').textContent = meta.msg || '';
    const sev = document.getElementById('rdSeverity');
    sev.textContent = meta.severity || '(none)';

    const lab = document.getElementById('rdDefaultLabel');
    lab.className = 'badge ' + (meta.default.class || 'bg-secondary');
    lab.textContent = meta.default.label || '?';
    document.getElementById('rdDefaultReason').textContent = meta.default.reason || '';

    const curEl = document.getElementById('rdCurrent');
    const override = window.CORAZA_OVERRIDES[rid];
    if (override && override !== 'default') {
        curEl.textContent = '';
        const b = document.createElement('span');
        b.className = 'badge bg-primary';
        b.textContent = override;
        curEl.appendChild(b);
        const note = document.createElement('span');
        note.className = 'small text-muted ms-2';
        note.textContent = '(server-wide override pinned via DB)';
        curEl.appendChild(note);
    } else {
        curEl.textContent = '(none — using default)';
    }

    const tagsEl = document.getElementById('rdTags');
    tagsEl.textContent = (meta.tags && meta.tags.length) ? meta.tags.join(', ') : '(none)';

    // Family reference card.
    const famCard = document.getElementById('rdFamilyCard');
    if (meta.family && (meta.family.description || meta.family.name)) {
        document.getElementById('rdFamilyName').textContent = meta.family.name || '';
        document.getElementById('rdFamilyKey').textContent = meta.family.key ? ('family ' + meta.family.key) : '';
        document.getElementById('rdFamilyDesc').textContent = meta.family.description || '';
        document.getElementById('rdFamilyFp').textContent = meta.family.common_fp || '(none)';
        document.getElementById('rdFamilyTuning').textContent = meta.family.tuning || '';
        famCard.style.display = '';
    } else {
        famCard.style.display = 'none';
    }

    // External documentation link.
    const docLink = document.getElementById('rdDocLink');
    const docSep  = document.getElementById('rdDocSep');
    if (meta.doc_url) {
        docLink.href = meta.doc_url;
        docLink.style.display = '';
        docSep.style.display = '';
    } else {
        docLink.style.display = 'none';
        docSep.style.display = 'none';
    }

    // Wire the side-links to the existing flows.
    const askLink = document.getElementById('rdAskAiLink');
    askLink.onclick = (ev) => {
        ev.preventDefault();
        getRdModal().hide();
        // Trigger the existing Ask-AI flow by clicking a matching link if one
        // is on the page; otherwise just open the AI modal manually with the
        // first known host for this rule (none in catalog tab — graceful fall).
        const askTrigger = document.querySelector('.coraza-ask-ai[data-rule-id="' + rid + '"]');
        if (askTrigger) askTrigger.click();
        else alert('Ask AI is available from the Firing rules tab (needs recent audit-log data).');
    };
    const viewLink = document.getElementById('rdViewMatchesLink');
    viewLink.onclick = (ev) => {
        ev.preventDefault();
        getRdModal().hide();
        const viewTrigger = document.querySelector('.coraza-view-matches[data-rule-id="' + rid + '"]');
        if (viewTrigger) viewTrigger.click();
        else alert('View matches is available from the Firing rules tab.');
    };

    getRdModal().show();
}

document.querySelectorAll('.coraza-rule-detail').forEach(a => {
    a.addEventListener('click', ev => {
        ev.preventDefault();
        openRuleDetail(a.dataset.ruleId);
    });
});
}); // end DOMContentLoaded
</script>
