<?php
declare(strict_types=1);

/**
 * /admin/security/index.php
 * Адмін: Безпека
 *
 * Містить:
 * - лог входів
 * - IP/CIDR правила
 * - rate-limit логіну
 */

require_once __DIR__ . '/../../includes/start.php';
require_role(['admin']);

require_once __DIR__ . '/../_helpers.php';
require_once __DIR__ . '/../_sidebar.php';

require_once __DIR__ . '/../../includes/security.php';
require_once __DIR__ . '/../../includes/security_ip_rules.php';
require_once __DIR__ . '/../../includes/security_rate_limit.php';

$pdo = function_exists('db') ? db() : null;
$title = admin_tt('admin_security', 'Безпека', 'Безопасность', 'Security');

$meta_robots = 'noindex, nofollow, noarchive, nosnippet';
$meta_desc   = '';
$canonical   = function_exists('abs_url') ? abs_url('/admin/security/') : '/admin/security/';
if (!headers_sent()) {
    header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet', true);
}

if (!function_exists('h')) {
    function h(?string $s): string { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
}
if (!function_exists('short_ua')) {
    function short_ua(string $ua, int $max = 90): string {
        $ua = trim($ua);
        if ($ua === '') return '-';
        if (mb_strlen($ua, 'UTF-8') <= $max) return $ua;
        return mb_substr($ua, 0, $max - 1, 'UTF-8') . '…';
    }
}
if (!function_exists('vm_csrf_field')) {
    function vm_csrf_field(): void {
        if (function_exists('csrf_input')) {
            ob_start();
            $ret = csrf_input();
            $buf = (string)ob_get_clean();
            if (is_string($ret) && $ret !== '') { echo $ret; return; }
            if ($buf !== '') { echo $buf; return; }
        }
        if (session_status() !== PHP_SESSION_ACTIVE) {
            @session_start();
        }
        if (!isset($_SESSION['csrf']) || !is_string($_SESSION['csrf']) || $_SESSION['csrf'] === '') {
            $_SESSION['csrf'] = bin2hex(random_bytes(16));
        }
        echo '<input type="hidden" name="csrf" value="' . h((string)$_SESSION['csrf']) . '">';
    }
}
if (!function_exists('build_pagination_pages')) {
    function build_pagination_pages(int $current, int $total): array {
        if ($total <= 1) return [1];
        $pages = [1, 2, $current - 1, $current, $current + 1, max(1, $total - 1), $total];
        $pages = array_values(array_unique(array_filter($pages, fn($p) => $p >= 1 && $p <= $total)));
        sort($pages);
        return $pages;
    }
}

if (function_exists('csrf_init')) {
    csrf_init();
}

$flash = null;
if (isset($_GET['ok'])) {
    $ok = trim((string)$_GET['ok']);
    $msg = match ($ok) {
        'rate' => 'Налаштування rate-limit збережено.',
        default => 'Зміни збережено.',
    };
    $flash = ['type' => 'success', 'msg' => $msg];
}
if (isset($_GET['err'])) {
    $err = trim((string)$_GET['err']);
    $msg = match ($err) {
        'csrf' => 'Помилка CSRF.',
        'db' => 'Немає з’єднання з БД.',
        'save' => 'Не вдалося зберегти зміни.',
        default => 'Сталася помилка.',
    };
    $flash = ['type' => 'danger', 'msg' => $msg];
}

$loginRows = [];
$loginLogOk = false;
$loginTotal = 0;
$perPage = 10;
$page = max(1, (int)($_GET['page'] ?? 1));
$offset = ($page - 1) * $perPage;

if ($pdo instanceof PDO) {
    $loginLogOk = security_has_table($pdo, 'security_login_log');
    if ($loginLogOk) {
        try {
            $loginTotal = (int)$pdo->query('SELECT COUNT(*) FROM security_login_log')->fetchColumn();
        } catch (Throwable $e) {
            $loginTotal = 0;
        }
        try {
            $st = $pdo->prepare('SELECT l.id, l.created_at, l.user_id, l.email, l.ip, l.ua, l.ok, l.reason, l.country_code, l.city, u.username, u.email AS user_email
                                 FROM security_login_log l
                                 LEFT JOIN users u ON u.id = l.user_id
                                 ORDER BY l.id DESC
                                 LIMIT :lim OFFSET :off');
            $st->bindValue(':lim', $perPage, PDO::PARAM_INT);
            $st->bindValue(':off', $offset, PDO::PARAM_INT);
            $st->execute();
            $loginRows = $st->fetchAll(PDO::FETCH_ASSOC) ?: [];
        } catch (Throwable $e) {
            $loginRows = [];
        }
    }
}
$totalPages = max(1, (int)ceil(max(0, $loginTotal) / $perPage));
if ($page > $totalPages) $page = $totalPages;

$ipRulesOk = false;
$ipRules = [];
if ($pdo instanceof PDO) {
    $ipRulesOk = security_ip_rules_has_table($pdo);
    if ($ipRulesOk) {
        $ipRules = security_ip_rules_list($pdo, 500);
    }
}

$rateOk = false;
$rateSettings = security_rate_default_settings();
$appSettingsOk = false;
$rateTableOk = false;
if ($pdo instanceof PDO) {
    $appSettingsOk = security_app_settings_has_table($pdo);
    $rateTableOk = security_rate_limits_has_table($pdo);
    $rateOk = $appSettingsOk;
    if ($appSettingsOk) {
        $rateSettings = security_rate_get_settings($pdo);
    }
}

function render_ip_rules_rows(bool $ipRulesOk, array $ipRules): void {
    if (!$ipRulesOk): ?>
      <tr><td colspan="5" class="text-muted small">Таблиця security_ip_rules ще не створена.</td></tr>
    <?php elseif (!$ipRules): ?>
      <tr><td colspan="5" class="text-muted small">Список порожній.</td></tr>
    <?php else: ?>
      <?php foreach ($ipRules as $r): ?>
        <?php
          $id = (int)($r['id'] ?? 0);
          $rule = (string)($r['ip_or_cidr'] ?? '');
          $bl = (int)($r['block_login'] ?? 0) === 1;
          $br = (int)($r['block_register'] ?? 0) === 1;
          $reason = (string)($r['reason'] ?? '');
          $dt = (string)($r['created_at'] ?? '');
          $scope = $bl && $br ? 'Вхід + Реєстрація' : ($bl ? 'Тільки вхід' : 'Тільки реєстрація');
        ?>
        <tr>
          <td class="small mono"><?= h($rule) ?></td>
          <td class="small"><?= h($scope) ?></td>
          <td class="small"><?= h($reason !== '' ? $reason : '-') ?></td>
          <td class="small text-muted"><?= h($dt) ?></td>
          <td class="text-end">
            <form method="post" action="/functional/security/ip_rules.php" class="d-inline ip-delete-form" data-confirm="Видалити?">
              <input type="hidden" name="action" value="delete">
              <input type="hidden" name="ajax" value="1">
              <input type="hidden" name="id" value="<?= (int)$id ?>">
              <?php vm_csrf_field(); ?>
              <button class="btn btn-sm btn-outline-danger" type="submit">Видалити</button>
            </form>
          </td>
        </tr>
      <?php endforeach; ?>
    <?php endif;
}

if (isset($_GET['fragment']) && (string)$_GET['fragment'] === 'ip_rules') {
    if (!headers_sent()) {
        header('Content-Type: text/html; charset=UTF-8');
        header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
    }
    render_ip_rules_rows($ipRulesOk, $ipRules);
    exit;
}

require __DIR__ . '/../../includes/header.php';
?>

<style>
  .dev-badge{display:inline-flex;align-items:center;gap:.4rem;padding:.15rem .55rem;border-radius:999px;font-size:.78rem;font-weight:700;border:1px solid rgba(0,0,0,.12);background:rgba(13,110,253,.10)}
  .mini-muted{font-size:.85rem;color:rgba(0,0,0,.6)}
  .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}
  .badge-ok{display:inline-flex;align-items:center;gap:.35rem;padding:.15rem .5rem;border-radius:999px;font-weight:800;font-size:.75rem;background:rgba(25,135,84,.12);border:1px solid rgba(25,135,84,.25);color:#198754}
  .badge-fail{display:inline-flex;align-items:center;gap:.35rem;padding:.15rem .5rem;border-radius:999px;font-weight:800;font-size:.75rem;background:rgba(220,53,69,.10);border:1px solid rgba(220,53,69,.22);color:#dc3545}
  .td-ua{max-width:420px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .sec-box{border:1px solid rgba(0,0,0,.08);border-radius:12px;padding:12px;background:rgba(0,0,0,.02)}
  .sec-actions{display:flex;gap:.5rem;flex-wrap:wrap}
  .sec-pill{display:inline-flex;align-items:center;gap:.35rem;padding:.15rem .55rem;border-radius:999px;border:1px solid rgba(0,0,0,.12);background:#fff;font-weight:700;font-size:.78rem}
  .sec-pill-ok{border-color:rgba(25,135,84,.25);background:rgba(25,135,84,.08);color:#198754}
  .sec-pill-warn{border-color:rgba(255,193,7,.35);background:rgba(255,193,7,.12)}
  .pager{display:flex;gap:.35rem;flex-wrap:wrap;align-items:center}
  .pager a,.pager span{display:inline-flex;align-items:center;justify-content:center;min-width:34px;height:32px;padding:0 10px;border-radius:10px;border:1px solid rgba(0,0,0,.12);background:#fff;text-decoration:none;font-weight:700;font-size:.85rem;color:inherit}
  .pager .is-active{background:rgba(13,110,253,.10);border-color:rgba(13,110,253,.30);color:#0d6efd}
  .pager .is-dots{border-color:transparent;background:transparent;min-width:auto;padding:0 2px}
</style>

<div class="row g-3">
  <div class="col-lg-9">

    <div class="d-flex align-items-center justify-content-between mb-2">
      <div>
        <h1 class="h4 mb-0">Безпека</h1>
        <div class="mini-muted">
          <span class="dev-badge">🛡 Логи, IP-правила і логін rate-limit</span>
          <span class="ms-2">Антиспам винесено в окремий розділ меню.</span>
        </div>
      </div>
      <a class="btn btn-sm btn-outline-secondary" href="/admin/">Дашборд</a>
    </div>

    <?php if ($flash): ?>
      <div class="alert alert-<?= h((string)$flash['type']) ?>"><?= h((string)$flash['msg']) ?></div>
    <?php endif; ?>

    <div class="card card-app p-3 mb-3" id="login-log">
      <div class="d-flex align-items-center justify-content-between mb-2">
        <div class="fw-bold">Лог входів</div>
        <span class="dev-badge">🧾 Працює</span>
      </div>
      <div class="mini-muted mb-2">Успішні та невдалі спроби входу, IP, User-Agent, причина відмови.</div>
      <div class="table-responsive">
        <table class="table table-sm align-middle mb-0">
          <thead><tr><th>Час</th><th>Користувач</th><th>IP</th><th>Результат</th><th>Причина</th><th>UA</th></tr></thead>
          <tbody>
          <?php if (!$loginLogOk): ?>
            <tr><td colspan="6" class="text-muted small">Таблиця security_login_log ще не створена.</td></tr>
          <?php elseif (!$loginRows): ?>
            <tr><td colspan="6" class="text-muted small">Поки що записів немає.</td></tr>
          <?php else: ?>
            <?php foreach ($loginRows as $r): ?>
              <?php
                $ok = (int)($r['ok'] ?? 0) === 1;
                $uid = (int)($r['user_id'] ?? 0);
                $uname = trim((string)($r['username'] ?? ''));
                $uemail = trim((string)($r['user_email'] ?? ''));
                $fallbackEmail = trim((string)($r['email'] ?? ''));
                $userLabel = $uid > 0 && $uname !== '' ? ($uname . ($uemail !== '' ? ' (' . $uemail . ')' : '')) : ($fallbackEmail !== '' ? $fallbackEmail : '-');
              ?>
              <tr>
                <td class="small text-muted"><?= h((string)($r['created_at'] ?? '')) ?></td>
                <td class="small"><?php if ($uid > 0): ?><a href="/admin/users/edit.php?id=<?= $uid ?>"><?= h($userLabel) ?></a><?php else: ?><?= h($userLabel) ?><?php endif; ?></td>
                <td class="small mono"><?= h((string)($r['ip'] ?? '')) ?></td>
                <td class="small"><?php if ($ok): ?><span class="badge-ok">✅ Успіх</span><?php else: ?><span class="badge-fail">⛔ Помилка</span><?php endif; ?></td>
                <td class="small"><?= h((string)($r['reason'] ?? '-')) ?></td>
                <td class="small td-ua" title="<?= h((string)($r['ua'] ?? '')) ?>"><?= h(short_ua((string)($r['ua'] ?? ''))) ?></td>
              </tr>
            <?php endforeach; ?>
          <?php endif; ?>
          </tbody>
        </table>
      </div>
      <?php if ($loginLogOk && $loginTotal > 0 && $totalPages > 1): ?>
        <?php
          $pages = build_pagination_pages($page, $totalPages);
          $prev = max(1, $page - 1);
          $next = min($totalPages, $page + 1);
          $qs = $_GET; unset($qs['page']);
          $base = '/admin/security/';
          $base .= $qs ? ('?' . http_build_query($qs)) : '';
          $sep = str_contains($base, '?') ? '&' : '?';
        ?>
        <div class="d-flex align-items-center justify-content-between mt-3">
          <div class="mini-muted">Показано <?= (int)min($loginTotal, ($offset + 1)) ?>–<?= (int)min($loginTotal, ($offset + count($loginRows))) ?> з <?= (int)$loginTotal ?></div>
          <div class="pager">
            <a href="<?= h($base . $sep . 'page=' . $prev) ?>">Назад</a>
            <?php $lastPrinted = 0; foreach ($pages as $p): ?>
              <?php if ($lastPrinted > 0 && $p - $lastPrinted > 1): ?><span class="is-dots">…</span><?php endif; ?>
              <a class="<?= $p === $page ? 'is-active' : '' ?>" href="<?= h($base . $sep . 'page=' . $p) ?>"><?= (int)$p ?></a>
            <?php $lastPrinted = $p; endforeach; ?>
            <a href="<?= h($base . $sep . 'page=' . $next) ?>">Далі</a>
          </div>
        </div>
      <?php endif; ?>
    </div>

    <div class="card card-app p-3 mb-3" id="ip">
      <div class="d-flex align-items-center justify-content-between mb-2">
        <div class="fw-bold">Підозрілі / Заблоковані IP</div>
        <?php if ($ipRulesOk): ?><span class="sec-pill sec-pill-ok">✅ Працює</span><?php else: ?><span class="sec-pill sec-pill-warn">⚠️ Потрібна таблиця</span><?php endif; ?>
      </div>
      <div class="mini-muted mb-3">Додай IP або CIDR, які треба заборонити для входу та/або реєстрації.</div>
      <div id="ipAjaxMsg" class="alert d-none mb-3" role="alert"></div>
      <div class="sec-box mb-3">
        <form id="ipAddForm" method="post" action="/functional/security/ip_rules.php" class="row g-2">
          <input type="hidden" name="action" value="add"><input type="hidden" name="ajax" value="1"><?php vm_csrf_field(); ?>
          <div class="col-md-4"><label class="form-label small mb-1">IP або CIDR</label><input class="form-control form-control-sm mono" name="ip_or_cidr" placeholder="203.0.113.10 або 203.0.113.0/24" <?= $ipRulesOk ? '' : 'disabled' ?>></div>
          <div class="col-md-4"><label class="form-label small mb-1">Блокування</label><select class="form-select form-select-sm" name="scope" <?= $ipRulesOk ? '' : 'disabled' ?>><option value="both">Вхід + Реєстрація</option><option value="login">Тільки вхід</option><option value="register">Тільки реєстрація</option></select></div>
          <div class="col-md-4"><label class="form-label small mb-1">Причина</label><input class="form-control form-control-sm" name="reason" placeholder="Підозріла активність" <?= $ipRulesOk ? '' : 'disabled' ?>></div>
          <div class="col-12 sec-actions"><button class="btn btn-sm btn-primary" type="submit" <?= $ipRulesOk ? '' : 'disabled' ?>>Додати IP</button></div>
        </form>
      </div>
      <div class="table-responsive">
        <table class="table table-sm align-middle mb-0"><thead><tr><th>IP</th><th>Дія</th><th>Причина</th><th>Додано</th><th class="text-end">Дії</th></tr></thead><tbody id="ipRulesTbody"><?php render_ip_rules_rows($ipRulesOk, $ipRules); ?></tbody></table>
      </div>
    </div>

    <div class="card card-app p-3 mb-3" id="rate-limit">
      <div class="d-flex align-items-center justify-content-between mb-2">
        <div class="fw-bold">Обмеження спроб логіну</div>
        <?php if ($rateOk): ?><span class="sec-pill sec-pill-ok">✅ Працює</span><?php else: ?><span class="sec-pill sec-pill-warn">⚠️ Потрібна таблиця</span><?php endif; ?>
      </div>
      <div class="mini-muted mb-3">Налаштування кількості дозволених спроб, вікна часу та блокування для логіну.</div>
      <?php if (!$appSettingsOk): ?><div class="sec-box mb-3"><div class="fw-bold mb-1">Потрібна таблиця app_settings</div><div class="mini-muted">Без неї не збережуться налаштування rate-limit.</div></div><?php endif; ?>
      <?php if (!$rateTableOk): ?><div class="sec-box mb-3"><div class="fw-bold mb-1">Потрібна таблиця security_rate_limits</div><div class="mini-muted">Вона потрібна для підрахунку спроб і блокувань.</div></div><?php endif; ?>
      <form method="post" action="/functional/security/rate_limit.php">
        <?php vm_csrf_field(); ?>
        <div class="row g-2">
          <div class="col-md-4"><label class="form-label small mb-1">Спроб</label><input class="form-control form-control-sm" type="number" name="attempts" value="<?= (int)$rateSettings['attempts'] ?>" <?= $rateOk ? '' : 'disabled' ?>></div>
          <div class="col-md-4"><label class="form-label small mb-1">Вікно (хв)</label><input class="form-control form-control-sm" type="number" name="window_min" value="<?= (int)$rateSettings['window_min'] ?>" <?= $rateOk ? '' : 'disabled' ?>></div>
          <div class="col-md-4"><label class="form-label small mb-1">Блок (хв)</label><input class="form-control form-control-sm" type="number" name="lock_min" value="<?= (int)$rateSettings['lock_min'] ?>" <?= $rateOk ? '' : 'disabled' ?>></div>
          <div class="col-md-6"><label class="form-label small mb-1">Режим</label><select class="form-select form-select-sm" name="mode" <?= $rateOk ? '' : 'disabled' ?>><option value="ip" <?= ($rateSettings['mode'] === 'ip' ? 'selected' : '') ?>>По IP</option><option value="user" <?= ($rateSettings['mode'] === 'user' ? 'selected' : '') ?>>По акаунту</option><option value="both" <?= ($rateSettings['mode'] === 'both' ? 'selected' : '') ?>>Комбіновано</option></select></div>
          <div class="col-md-6 d-grid"><label class="form-label small mb-1">&nbsp;</label><button class="btn btn-sm btn-primary" <?= $rateOk ? '' : 'disabled' ?>>Зберегти</button></div>
        </div>
      </form>
    </div>

  </div>
  <div class="col-lg-3"><?php admin_sidebar('security'); ?></div>
</div>

<script>
(function () {
  const ipBox = document.getElementById('ip');
  if (!ipBox) return;
  const BASE = (function () {
    const p = window.location.pathname || '';
    const idx = p.indexOf('/admin/');
    return idx >= 0 ? p.slice(0, idx) : '';
  })();
  function absPath(path) {
    path = String(path || '').trim();
    if (!path) return '';
    if (/^https?:\/\//i.test(path)) return path;
    if (path.startsWith('/')) return BASE + path;
    return BASE + '/' + path.replace(/^\.\/+/, '').replace(/^\/+/, '');
  }
  const POST_URL = absPath('/functional/security/ip_rules.php');
  const FRAG_URL = absPath('/admin/security/?fragment=ip_rules');
  const msgEl = document.getElementById('ipAjaxMsg');
  const tbody = document.getElementById('ipRulesTbody');
  const addForm = document.getElementById('ipAddForm');
  if (addForm) addForm.action = POST_URL;
  function showMsg(ok, text) {
    if (!msgEl) return;
    msgEl.classList.remove('d-none', 'alert-success', 'alert-danger');
    msgEl.classList.add(ok ? 'alert-success' : 'alert-danger');
    msgEl.textContent = text || (ok ? 'OK' : 'Error');
  }
  function stripHtml(s) { return String(s || '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); }
  async function postForm(form) {
    const fd = new FormData(form);
    if (!fd.has('ajax')) fd.append('ajax', '1');
    let act = form.getAttribute('action') || form.action || POST_URL;
    act = absPath(act) || POST_URL;
    const res = await fetch(act, { method: 'POST', body: fd, headers: { 'X-Requested-With': 'fetch', 'Accept': 'application/json' }, credentials: 'same-origin', redirect: 'follow' });
    const text = await res.text();
    let data = null; try { data = JSON.parse(text); } catch (e) {}
    if (!data || typeof data.ok === 'undefined') {
      const snip = stripHtml(text).slice(0, 260);
      return { ok: false, msg: snip ? ('Bad response: ' + snip) : ('Bad response: HTTP ' + res.status) };
    }
    return data;
  }
  async function refreshRules() {
    if (!tbody) return;
    const res = await fetch(FRAG_URL + '&_=' + Date.now(), { headers: { 'X-Requested-With': 'fetch' }, credentials: 'same-origin', cache: 'no-store' });
    tbody.innerHTML = await res.text();
  }
  if (addForm) {
    addForm.addEventListener('submit', async function (e) {
      e.preventDefault();
      const btn = addForm.querySelector('button[type="submit"]');
      if (btn) btn.disabled = true;
      try {
        const data = await postForm(addForm);
        showMsg(!!data.ok, data.msg || '');
        if (data.ok) { addForm.reset(); await refreshRules(); }
      } catch (err) { showMsg(false, 'Network error'); }
      finally { if (btn) btn.disabled = false; }
    });
  }
  document.addEventListener('submit', async function (e) {
    const form = e.target;
    if (!form || !form.classList || !form.classList.contains('ip-delete-form')) return;
    e.preventDefault();
    const c = form.getAttribute('data-confirm') || '';
    if (c && !confirm(c)) return;
    form.action = POST_URL;
    const btn = form.querySelector('button[type="submit"]');
    if (btn) btn.disabled = true;
    try {
      const data = await postForm(form);
      showMsg(!!data.ok, data.msg || '');
      if (data.ok) await refreshRules();
    } catch (err) { showMsg(false, 'Network error'); }
    finally { if (btn) btn.disabled = false; }
  }, true);
})();
</script>

<?php require __DIR__ . '/../../includes/footer.php'; ?>
