<?php
/**
 * JWT encoding / decoding (HS256) and password helpers.
 * Pure PHP, no Composer dependency required.
 *
 * Compatible with the original Python backend's JWTs, so existing
 * `bcrypt` ($2b$ ...) password hashes verify correctly through
 * PHP's `password_verify()`.
 */

function b64url_encode(string $bytes): string {
    return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
}

function b64url_decode(string $b64url): string {
    $b64 = strtr($b64url, '-_', '+/');
    $pad = strlen($b64) % 4;
    if ($pad) $b64 .= str_repeat('=', 4 - $pad);
    return base64_decode($b64);
}

function jwt_encode(array $payload, string $secret): string {
    $header = ['typ' => 'JWT', 'alg' => 'HS256'];
    $h = b64url_encode(json_encode($header,  JSON_UNESCAPED_SLASHES));
    $p = b64url_encode(json_encode($payload, JSON_UNESCAPED_SLASHES));
    $sig = hash_hmac('sha256', "$h.$p", $secret, true);
    return "$h.$p." . b64url_encode($sig);
}

/** Returns the decoded payload or null on any failure. */
function jwt_decode_safe(string $token, string $secret): ?array {
    $parts = explode('.', $token);
    if (count($parts) !== 3) return null;
    [$h, $p, $sig] = $parts;
    $expected = b64url_encode(hash_hmac('sha256', "$h.$p", $secret, true));
    if (!hash_equals($expected, $sig)) return null;

    $payload = json_decode(b64url_decode($p), true);
    if (!is_array($payload)) return null;

    if (isset($payload['exp']) && time() >= (int)$payload['exp']) return null;
    return $payload;
}

/** Build a token compatible with the existing frontend. */
function make_access_token(array $user): string {
    $payload = [
        'sub'      => $user['id'],
        'username' => $user['username'],
        'role'     => $user['role'],
        'exp'      => time() + JWT_TTL_SECONDS,
        'type'     => 'access',
    ];
    return jwt_encode($payload, JWT_SECRET);
}

/** Extract the bearer token from the request, or null. */
function extract_token(): ?string {
    // 1) Authorization header (preferred)
    $hdr = '';
    if (function_exists('getallheaders')) {
        $h = getallheaders();
        foreach ($h as $k => $v) {
            if (strcasecmp($k, 'Authorization') === 0) { $hdr = $v; break; }
        }
    }
    if ($hdr === '' && isset($_SERVER['HTTP_AUTHORIZATION']))      $hdr = $_SERVER['HTTP_AUTHORIZATION'];
    if ($hdr === '' && isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) $hdr = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];

    if ($hdr !== '' && stripos($hdr, 'Bearer ') === 0) {
        return trim(substr($hdr, 7));
    }
    // 2) Cookie fallback (if you ever switch to cookie auth)
    if (!empty($_COOKIE['access_token'])) return $_COOKIE['access_token'];
    return null;
}

/** Returns the user record (no password_hash) or terminates with 401. */
function require_auth(): array {
    $token = extract_token();
    if (!$token) json_error(401, 'Not authenticated');

    $payload = jwt_decode_safe($token, JWT_SECRET);
    if (!$payload || empty($payload['sub'])) json_error(401, 'Invalid or expired token');

    // Use SELECT * so we never crash on missing columns (old DB without hybrid fields).
    // shape_user() will strip password_hash and add computed fields.
    $stmt = db()->prepare('SELECT * FROM users WHERE id = ?');
    $stmt->execute([$payload['sub']]);
    $user = $stmt->fetch();
    if (!$user) json_error(401, 'User not found');
    return $user;
}

function require_admin(): array {
    $user = require_auth();
    if (($user['role'] ?? '') !== 'admin') json_error(403, 'Admin access required');
    return $user;
}

/** Add computed fields the frontend expects. */
function shape_user(array $u): array {
    $days = days_remaining($u['expiry_at'] ?? null);
    $u['days_remaining'] = $days;
    $u['is_expired']     = $days === 0;
    unset($u['password_hash']);
    return $u;
}

function days_remaining(?string $expiry_iso): ?int {
    if (!$expiry_iso) return null;
    try {
        $dt   = new DateTime($expiry_iso);
        $now  = new DateTime('now', new DateTimeZone('UTC'));
        $secs = $dt->getTimestamp() - $now->getTimestamp();
        if ($secs <= 0) return 0;
        return max(1, (int)ceil($secs / 86400));
    } catch (Throwable $e) {
        return null;
    }
}
