<?php

declare(strict_types=1);

// ── Date / Time ─────────────────────────────────────────────────────

/**
 * Return current datetime in Y-m-d H:i:s format.
 */
function now(): string
{
    return date('Y-m-d H:i:s');
}

/**
 * Return current date (Y-m-d).
 */
function today(): string
{
    return date('Y-m-d');
}

/**
 * Human‑readable "time ago" string.
 *
 * @param string $datetime  A parseable date/time string.
 * @return string e.g. "3 minutes ago", "2 weeks ago"
 */
function timeAgo(string $datetime): string
{
    $timestamp = strtotime($datetime);
    $diff = time() - $timestamp;

    if ($diff < 60) {
        return 'just now';
    }
    $intervals = [
        31536000 => 'year',
        2592000  => 'month',
        604800   => 'week',
        86400    => 'day',
        3600     => 'hour',
        60       => 'minute',
    ];
    foreach ($intervals as $seconds => $label) {
        $count = intdiv($diff, $seconds);
        if ($count > 0) {
            return $count . ' ' . $label . ($count > 1 ? 's' : '') . ' ago';
        }
    }
    return 'just now';
}

/**
 * Format a datetime into a friendly string.
 *
 * @param string $datetime
 * @param string $format   Default 'M j, Y, g:i a' (e.g. "Mar 15, 2025, 2:30 pm")
 */
function formatDate(string $datetime, string $format = 'M j, Y, g:i a'): string
{
    return date($format, strtotime($datetime));
}

// ── String helpers ──────────────────────────────────────────────────

/** Generate a URL‑safe slug from a string. */
function slug(string $text): string
{
    $text = strtolower($text);
    $text = preg_replace('/[^a-z0-9]+/', '-', $text);
    return trim($text, '-');
}

/** Truncate a string to a given length, appending "…" if cut. */
function truncate(string $text, int $length = 100, string $ellipsis = '…'): string
{
    if (mb_strlen($text) <= $length) {
        return $text;
    }
    return mb_substr($text, 0, $length) . $ellipsis;
}

/** Convert newlines to <br> tags (safe for display). */
function nl2brSafe(string $text): string
{
    return nl2br(htmlspecialchars($text));
}

/** Strip all HTML tags and optionally truncate. */
function plain(string $text, int $maxLength = 0): string
{
    $text = strip_tags($text);
    if ($maxLength > 0) {
        $text = truncate($text, $maxLength);
    }
    return $text;
}

// ── Array helpers ───────────────────────────────────────────────────

/** Return the first element of an array, or null. */
function first(array $array): mixed
{
    return !empty($array) ? reset($array) : null;
}

/** Return the last element of an array, or null. */
function last(array $array): mixed
{
    return !empty($array) ? end($array) : null;
}

// ── URL / Redirect ──────────────────────────────────────────────────

/** Safely redirect to another URL (no further script execution). */
function redirect(string $path): void
{
    $base = defined('BASE_URL') ? rtrim(BASE_URL, '/') : '';
    $path = '/' . ltrim($path, '/');
    header('Location: ' . $base . $path);
    exit;
}

/** Get a value from $_GET with a default. */
function get(string $key, mixed $default = null): mixed
{
    return $_GET[$key] ?? $default;
}

/** Get a value from $_POST with a default. */
function post(string $key, mixed $default = null): mixed
{
    return $_POST[$key] ?? $default;
}

// ── Encryption / Hashing ────────────────────────────────────────────

/** Simple AES‑256‑CBC encryption (for database storage). */
function encrypt(string $plaintext, string $key): string
{
    $iv = random_bytes(16);
    $ciphertext = openssl_encrypt($plaintext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
    return base64_encode($iv . $ciphertext);
}

/** Decrypt a value encrypted with encrypt(). */
function decrypt(string $encrypted, string $key): string
{
    $data = base64_decode($encrypted);
    $iv = substr($data, 0, 16);
    $ciphertext = substr($data, 16);
    return openssl_decrypt($ciphertext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
}

// ── Validation ──────────────────────────────────────────────────────

/** Validate an email address. */
function isEmail(string $email): bool
{
    return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}

/** Validate a URL. */
function isUrl(string $url): bool
{
    return filter_var($url, FILTER_VALIDATE_URL) !== false;
}

/** Check if a value is a valid JSON string. */
function isJson(string $string): bool
{
    json_decode($string);
    return json_last_error() === JSON_ERROR_NONE;
}

// ── Database shortcuts ──────────────────────────────────────────────
//
// A small, stable façade over the DB layer for code written BY app authors:
// Designer expressions (Default value / AutoUpdate value), Events code, module
// and SPA pages. Author code should never have to name internal classes, so
// `\PpPhp\Core\DB::FetchOne(...)` becomes `dbValue(...)`. PHP function names are
// case-insensitive, so DBValue()/dbvalue() reach the same helper — write
// whichever reads best.
//
// Always pass values as PLACEHOLDERS (`?` here, `:1` in dbLookup) so they are
// bound, never concatenated into the SQL.

/** True when the database layer is loaded and usable. */
function dbReady(): bool
{
    return class_exists('\PpPhp\Core\DB');
}

/**
 * Scalar lookup with PHPRunner-style numbered placeholders.
 * dbLookup('SELECT name FROM customers WHERE id = :1', $id)
 */
function dbLookup(string $sql, mixed ...$args): mixed
{
    if (!dbReady()) return null;
    try { return \PpPhp\Core\DB::DBLookup($sql, ...$args); } catch (\Throwable $e) { return null; }
}

/**
 * First column of the first row, using `?` placeholders.
 * dbValue('SELECT name FROM customers WHERE id = ?', [$id])
 */
function dbValue(string $sql, array $params = [], mixed $default = null): mixed
{
    if (!dbReady()) return $default;
    try {
        $v = \PpPhp\Core\DB::FetchOne($sql, $params);
        return $v === null ? $default : $v;
    } catch (\Throwable $e) { return $default; }
}

/**
 * First matching row as an associative array, or null.
 * dbRow('SELECT * FROM customers WHERE id = ?', [$id])
 */
function dbRow(string $sql, array $params = []): ?array
{
    if (!dbReady()) return null;
    try {
        $rows = \PpPhp\Core\DB::FetchAll($sql, $params);
        return (is_array($rows) && $rows !== []) ? (array) $rows[0] : null;
    } catch (\Throwable $e) { return null; }
}

/**
 * Every matching row (empty array when there are none or on error).
 * dbAll('SELECT id, name FROM customers WHERE active = ?', [1])
 */
function dbAll(string $sql, array $params = []): array
{
    if (!dbReady()) return [];
    try {
        $rows = \PpPhp\Core\DB::FetchAll($sql, $params);
        return is_array($rows) ? $rows : [];
    } catch (\Throwable $e) { return []; }
}

/** Row count for a query — dbCount('SELECT 1 FROM orders WHERE customer_id = ?', [$id]) */
function dbCount(string $sql, array $params = []): int
{
    return count(dbAll($sql, $params));
}

/**
 * Run a write (INSERT / UPDATE / DELETE) and return affected rows, or -1 on error.
 * Reads should use dbValue / dbRow / dbAll.
 */
function dbExec(string $sql, array $params = []): int
{
    if (!dbReady()) return -1;
    try {
        $res = \PpPhp\Core\DB::Query($sql, $params);
        return $res === false ? -1 : $res->rowCount();
    } catch (\Throwable $e) { return -1; }
}

/**
 * Render a notification template: replace {field} tokens with values from $row.
 * Used by generated Notification Rules (BuildWriter::compileNotifRule). Unknown
 * tokens become empty. Values are inserted raw — the notification bell escapes
 * them on render.
 */
if (!function_exists('pp_notif_render')) {
    function pp_notif_render(string $template, array $row): string
    {
        if (strpos($template, '{') === false) return $template;
        return (string) preg_replace_callback('/\{([a-zA-Z0-9_]+)\}/', static function (array $m) use ($row): string {
            return array_key_exists($m[1], $row) ? (string) $row[$m[1]] : '';
        }, $template);
    }
}
