<?php
// =============================================================================
// General helpers — escaping, JSON IO, etc.
// =============================================================================

function h($s) {
    return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

// Auto-detect the panel's URL prefix from where the current request lives
// inside the panel root dir. Works for any mount point — document root,
// /Ibo_ultra/, /customer_a/foo/bar/, etc. Multiple instances of this panel
// can sit side-by-side under one domain without per-deployment config.
function panel_base_path() {
    static $cached = null;
    if ($cached !== null) return $cached;

    $panel_root = realpath(__DIR__ . '/..');                 // .../panel/
    $script_fn  = isset($_SERVER['SCRIPT_FILENAME']) ? realpath($_SERVER['SCRIPT_FILENAME']) : '';
    $script_url = isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : '';

    if ($panel_root && $script_fn && $script_url !== ''
        && strpos(str_replace('\\', '/', $script_fn), str_replace('\\', '/', $panel_root)) === 0) {
        $rel = str_replace('\\', '/', substr($script_fn, strlen($panel_root)));
        if ($rel !== '' && substr($script_url, -strlen($rel)) === $rel) {
            $cached = substr($script_url, 0, -strlen($rel));
            $cached = ($cached === '' ? '/' : rtrim($cached, '/') . '/');
            return $cached;
        }
    }
    // CLI or any oddity — return '/' as a safe default.
    $cached = '/';
    return $cached;
}

function panel_url($path = '') {
    $base = panel_base_path();
    return rtrim($base, '/') . '/' . ltrim((string)$path, '/');
}

function active_page($name) {
    $self = isset($_SERVER['SCRIPT_NAME']) ? basename($_SERVER['SCRIPT_NAME']) : '';
    if ($self === '') {
        return false;
    }
    return ($self === $name) || ($self === $name . '.php');
}

function client_ip() {
    // Trust order matters here — the throttle and request log use this value to
    // identify the caller. We never trust raw X-Forwarded-For because any HTTP
    // client can write it and bypass per-IP rate limits.
    //
    // 1. If we have proof we're behind Cloudflare (cf-ray is set on every CF
    //    edge response), trust the cf-connecting-ip Cloudflare injects.
    // 2. Otherwise fall back to REMOTE_ADDR, which is the actual TCP peer.
    //
    // Operators fronting the panel with a non-CF proxy can flip this default
    // by setting PANEL_TRUSTED_IP_HEADER in includes/config.php.
    $trusted_header = defined('PANEL_TRUSTED_IP_HEADER') ? PANEL_TRUSTED_IP_HEADER : '';
    if ($trusted_header !== '') {
        $key = 'HTTP_' . strtoupper(str_replace('-', '_', $trusted_header));
        if (!empty($_SERVER[$key])) {
            $parts = explode(',', (string)$_SERVER[$key]);
            return trim($parts[0]);
        }
    }
    if (!empty($_SERVER['HTTP_CF_RAY']) && !empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
        return trim((string)$_SERVER['HTTP_CF_CONNECTING_IP']);
    }
    return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
}

function user_agent() {
    return isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
}

function json_response($data, $status = 200) {
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    header('Cache-Control: no-store');
    echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    exit;
}

function json_error($msg, $status = 400, $extra = []) {
    $payload = ['error' => $msg];
    if (!empty($extra)) {
        $payload = array_merge($payload, $extra);
    }
    json_response($payload, $status);
}

function json_request_body() {
    // Multipart uploads (module install, restore, VPN bulk zip, slideshow image)
    // come through $_FILES / $_POST and intentionally exceed the 1 MiB cap below.
    // Skip the cap when the request is multipart — those endpoints enforce their
    // own per-feature size limits.
    $ct = $_SERVER['CONTENT_TYPE'] ?? '';
    if (stripos($ct, 'multipart/form-data') === 0) {
        return [];
    }
    // Cap JSON bodies at 1 MiB. Anything bigger is either a misuse or a DoS probe.
    $max = 1024 * 1024;
    if (isset($_SERVER['CONTENT_LENGTH']) && (int)$_SERVER['CONTENT_LENGTH'] > $max) {
        json_error('Request body too large.', 413);
    }
    $raw = file_get_contents('php://input', false, null, 0, $max + 1);
    if ($raw === '' || $raw === false) {
        return [];
    }
    if (strlen($raw) > $max) {
        json_error('Request body too large.', 413);
    }
    $obj = json_decode($raw, true);
    return is_array($obj) ? $obj : [];
}

function require_method($methods) {
    $methods = is_array($methods) ? $methods : [$methods];
    $m = isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET';
    if (!in_array($m, $methods, true)) {
        header('Allow: ' . implode(', ', $methods));
        json_error('method not allowed', 405);
    }
}

function random_hex($len) {
    return bin2hex(random_bytes((int)ceil($len / 2)));
}

function unix_now() {
    return time();
}

function ago($ts) {
    if (!$ts) {
        return 'never';
    }
    $diff = time() - (int)$ts;
    if ($diff < 60)    return $diff . 's ago';
    if ($diff < 3600)  return floor($diff / 60) . 'm ago';
    if ($diff < 86400) return floor($diff / 3600) . 'h ago';
    return floor($diff / 86400) . 'd ago';
}

function bool_text($v) {
    return $v ? 'true' : 'false';
}

function truthy($v) {
    if ($v === true || $v === 1 || $v === '1' || $v === 'true' || $v === 'on' || $v === 'yes') {
        return 1;
    }
    return 0;
}
