<?php

declare(strict_types=1);

require_once __DIR__ . '/db.php';

if (session_status() === PHP_SESSION_NONE) {
    session_start();
}

const REMEMBER_COOKIE_NAME = 'remember_login';
const API_TOKEN_COOKIE_NAME = 'api_token';

const REMEMBER_DAYS = 30;
const AUTH_TOKEN_HOURS = 12;

function isSecureRequest(): bool
{
    return (
        (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
        || (($_SERVER['SERVER_PORT'] ?? null) === '443')
    );
}

function hashPassword(string $password): string
{
    return password_hash($password, PASSWORD_ARGON2ID);
}

function setApiTokenCookie(string $token): void
{
    setcookie(API_TOKEN_COOKIE_NAME, $token, [
        'expires' => time() + 60 * 60 * AUTH_TOKEN_HOURS,
        'path' => '/',
        'secure' => isSecureRequest(),
        'httponly' => false,
        'samesite' => 'Lax',
    ]);

    $_COOKIE[API_TOKEN_COOKIE_NAME] = $token;
}

function deleteApiTokenCookie(): void
{
    setcookie(API_TOKEN_COOKIE_NAME, '', [
        'expires' => time() - 3600,
        'path' => '/',
        'secure' => isSecureRequest(),
        'httponly' => false,
        'samesite' => 'Lax',
    ]);

    unset($_COOKIE[API_TOKEN_COOKIE_NAME]);
}

function login(string $email, string $password, bool $remember = false): ?string
{
    $email = strtolower(trim($email));

    $stmt = db()->prepare('SELECT * FROM users WHERE email = ? LIMIT 1');
    $stmt->execute([$email]);
    $user = $stmt->fetch();

    if (!$user || !password_verify($password, $user['passwd'])) {
        return null;
    }

    session_regenerate_id(true);
    $_SESSION['user_id'] = (int)$user['id'];

    if ($remember) {
        createRememberToken((int)$user['id']);
    }

    $token = createAuthToken((int)$user['id']);
    setApiTokenCookie($token);

    return $token;
}

function logout(): void
{
    $token = getBearerToken();

    if ($token) {
        revokeAuthToken($token);
    }

    if (isset($_SESSION['user_id'])) {
        revokeUserAuthTokens((int)$_SESSION['user_id']);
    }

    deleteRememberCookie();
    deleteApiTokenCookie();

    $_SESSION = [];

    if (session_status() === PHP_SESSION_ACTIVE) {
        session_destroy();
    }
}

function currentUser(): ?array
{
    if (!isset($_SESSION['user_id'])) {
        tryRememberLogin();
    }

    if (isset($_SESSION['user_id'])) {
        $stmt = db()->prepare(
            'SELECT id, name, email, picture
             FROM users
             WHERE id = ?
             LIMIT 1'
        );

        $stmt->execute([$_SESSION['user_id']]);

        return $stmt->fetch() ?: null;
    }

    $token = getBearerToken();

    if ($token) {
        return authenticateApiToken($token);
    }

    return null;
}

function requireLogin(): array
{
    $user = currentUser();

    if (!$user) {
        header('Location: ?page=login');
        exit;
    }

    return $user;
}

function requireGuest(): void
{
    if (currentUser()) {
        header('Location: ?page=home');
        exit;
    }
}

function createRememberToken(int $userId): void
{
    $selector = bin2hex(random_bytes(16));
    $token = bin2hex(random_bytes(32));
    $tokenHash = hash('sha256', $token);

    $expires = date('Y-m-d H:i:s', time() + 60 * 60 * 24 * REMEMBER_DAYS);

    $stmt = db()->prepare(
        'INSERT INTO remember_tokens (user_id, selector, token_hash, expires_at)
         VALUES (?, ?, ?, ?)'
    );

    $stmt->execute([$userId, $selector, $tokenHash, $expires]);

    setcookie(REMEMBER_COOKIE_NAME, $selector . ':' . $token, [
        'expires' => time() + 60 * 60 * 24 * REMEMBER_DAYS,
        'path' => '/',
        'secure' => isSecureRequest(),
        'httponly' => true,
        'samesite' => 'Lax',
    ]);
}

function tryRememberLogin(): void
{
    if (empty($_COOKIE[REMEMBER_COOKIE_NAME])) {
        return;
    }

    $parts = explode(':', $_COOKIE[REMEMBER_COOKIE_NAME]);

    if (count($parts) !== 2) {
        deleteRememberCookie();
        return;
    }

    [$selector, $token] = $parts;
    $tokenHash = hash('sha256', $token);

    $stmt = db()->prepare(
        'SELECT *
         FROM remember_tokens
         WHERE selector = ?
           AND expires_at > NOW()
         LIMIT 1'
    );

    $stmt->execute([$selector]);
    $row = $stmt->fetch();

    if (!$row || !hash_equals($row['token_hash'], $tokenHash)) {
        deleteRememberCookie();
        return;
    }

    session_regenerate_id(true);
    $_SESSION['user_id'] = (int)$row['user_id'];

    db()->prepare('DELETE FROM remember_tokens WHERE id = ?')->execute([$row['id']]);

    createRememberToken((int)$row['user_id']);

    $apiToken = createAuthToken((int)$row['user_id']);
    setApiTokenCookie($apiToken);
}

function deleteRememberCookie(): void
{
    if (!empty($_COOKIE[REMEMBER_COOKIE_NAME])) {
        $parts = explode(':', $_COOKIE[REMEMBER_COOKIE_NAME]);

        if (count($parts) === 2) {
            db()->prepare(
                'DELETE FROM remember_tokens WHERE selector = ?'
            )->execute([$parts[0]]);
        }
    }

    setcookie(REMEMBER_COOKIE_NAME, '', [
        'expires' => time() - 3600,
        'path' => '/',
        'secure' => isSecureRequest(),
        'httponly' => true,
        'samesite' => 'Lax',
    ]);

    unset($_COOKIE[REMEMBER_COOKIE_NAME]);
}

function createAuthToken(int $userId): string
{
    $token = bin2hex(random_bytes(32));
    $tokenHash = hash('sha256', $token);

    $expires = date('Y-m-d H:i:s', time() + 60 * 60 * AUTH_TOKEN_HOURS);

    $stmt = db()->prepare(
        'INSERT INTO auth_tokens (user_id, token_hash, expires_at)
         VALUES (?, ?, ?)'
    );

    $stmt->execute([
        $userId,
        $tokenHash,
        $expires
    ]);

    return $token;
}

function authenticateApiToken(string $token): ?array
{
    $token = trim($token);

    if ($token === '') {
        return null;
    }

    $tokenHash = hash('sha256', $token);

    $stmt = db()->prepare(
        'SELECT users.id, users.name, users.email, users.picture
         FROM auth_tokens
         JOIN users ON users.id = auth_tokens.user_id
         WHERE auth_tokens.token_hash = ?
           AND auth_tokens.expires_at > NOW()
           AND auth_tokens.revoked_at IS NULL
         LIMIT 1'
    );

    $stmt->execute([$tokenHash]);
    $user = $stmt->fetch();

    if (!$user) {
        return null;
    }

    db()->prepare(
        'UPDATE auth_tokens
         SET last_used_at = NOW()
         WHERE token_hash = ?'
    )->execute([$tokenHash]);

    return $user;
}

function getBearerToken(): ?string
{
    $header = $_SERVER['HTTP_AUTHORIZATION']
        ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION']
        ?? '';

    if (preg_match('/Bearer\s+(.+)/i', $header, $matches)) {
        return trim($matches[1]);
    }

    if (!empty($_COOKIE[API_TOKEN_COOKIE_NAME])) {
        return trim($_COOKIE[API_TOKEN_COOKIE_NAME]);
    }

    return null;
}

function jsonAuthError(string $message, int $status): void
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');

    echo json_encode([
        'success' => false,
        'error' => $message
    ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

    exit;
}

function userHasRight(int $userId, string $rightName): bool
{
    try {
        $stmt = db()->prepare(
            'SELECT rights.id
             FROM user_rights
             INNER JOIN rights ON rights.id = user_rights.right_id
             WHERE user_rights.user_id = ?
               AND rights.name = ?
             LIMIT 1'
        );
        $stmt->execute([$userId, $rightName]);

        return (bool)$stmt->fetch();
    } catch (Throwable $e) {
        return false;
    }
}

function userIsAdmin(int $userId): bool
{
    return userHasRight($userId, 'Admin');
}

function requireUserRight(int $userId, string $rightName): void
{
    if (userIsAdmin($userId) || userHasRight($userId, $rightName)) {
        return;
    }

    jsonAuthError("Missing right: {$rightName}.", 403);
}

function projectExists(string $projectId): bool
{
    $stmt = db()->prepare('SELECT id FROM projects WHERE id = ? LIMIT 1');
    $stmt->execute([$projectId]);

    return (bool)$stmt->fetch();
}

function userCanAccessProject(int $userId, string $projectId): bool
{
    if (userIsAdmin($userId)) {
        return true;
    }

    $stmt = db()->prepare(
        'SELECT projects.id
         FROM projects
         LEFT JOIN user_access
            ON user_access.project_id = projects.id
           AND user_access.user_id = ?
         WHERE projects.id = ?
           AND (projects.owner = ? OR user_access.id IS NOT NULL)
         LIMIT 1'
    );
    $stmt->execute([$userId, $projectId, $userId]);

    return (bool)$stmt->fetch();
}

function requireProjectAccessForUser(int $userId, string $projectId): void
{
    if (!projectExists($projectId)) {
        jsonAuthError('Project not found.', 404);
    }

    if (!userCanAccessProject($userId, $projectId)) {
        jsonAuthError('No access to this project.', 403);
    }
}

function requireProjectRight(int $userId, string $projectId, string $rightName): void
{
    requireProjectAccessForUser($userId, $projectId);
    requireUserRight($userId, $rightName);
}

function requireApiAuth(): array
{
    $token = getBearerToken();

    if (!$token) {
        jsonAuthError('Missing API token.', 401);
    }

    $user = authenticateApiToken($token);

    if (!$user) {
        jsonAuthError('Invalid or expired API token.', 401);
    }

    return $user;
}

function revokeAuthToken(string $token): void
{
    $tokenHash = hash('sha256', $token);

    db()->prepare(
        'UPDATE auth_tokens
         SET revoked_at = NOW()
         WHERE token_hash = ?
           AND revoked_at IS NULL'
    )->execute([$tokenHash]);
}

function revokeUserAuthTokens(int $userId): void
{
    db()->prepare(
        'UPDATE auth_tokens
         SET revoked_at = NOW()
         WHERE user_id = ?
           AND revoked_at IS NULL'
    )->execute([$userId]);
}

function cleanupExpiredAuthData(): void
{
    db()->exec('DELETE FROM remember_tokens WHERE expires_at < NOW()');
    db()->exec('DELETE FROM auth_tokens WHERE expires_at < NOW() OR revoked_at IS NOT NULL');
}

function register(string $username, string $email, string $password): ?int
{
    $username = trim($username);
    $email = strtolower(trim($email));

    if ($username === '') {
        return null;
    }

    if ($email === '') {
        return null;
    }

    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        return null;
    }

    if ($password === '') {
        return null;
    }

    if (strlen($password) < 8) {
        return null;
    }

    $passwordHash = password_hash($password, PASSWORD_ARGON2ID);

    if ($passwordHash === false) {
        $passwordHash = password_hash($password, PASSWORD_DEFAULT);
    }

    try {
        $stmt = db()->prepare(
            'INSERT INTO users (name, email, passwd)
             VALUES (?, ?, ?)'
        );

        $stmt->execute([
            $username,
            $email,
            $passwordHash
        ]);

        return (int)db()->lastInsertId();

    } catch (PDOException $e) {
        return null;
    }
}
