<?php
require_once 'vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

class NetworkException extends Exception {}
class BackendException extends Exception {}
class AuthException extends Exception {}

class Server {
    private string $url;
    private bool $keepalive;
    private float $timeout;

    public function __construct(string $url, bool $keepalive, float $timeout = 0.5) {
        $this->url = $url;
        $this->keepalive = $keepalive;
        $this->timeout = $timeout;
    }

    // Server requests
    private static function request(mixed $context, string $url, bool $json = true): mixed {
        $response = file_get_contents($url, false, $context);
        if ($response === false)
            throw new NetworkException("Failed to request $url");
        if (!$json)
            return $response;
        try {
            $output = json_decode($response, flags: JSON_THROW_ON_ERROR);
        } catch (JsonException $e) {
            throw new NetworkException("Failed to decode response from $url: $response");
        }
        return $output;
    }

    /** @param array<string> $headers */
    private function build_context(string $method, array $headers = [], mixed $content = null): mixed {
        if ($content !== null)
            array_push($headers, 'Content-Type: application/json');
        if ($this->keepalive)
            array_push($headers, 'Connection: keep-alive');
        $http = [
            'method' => $method,
            'header' => $headers,
            'timeout' => $this->timeout,
            'content' => $content,
            'ignore_errors' => true,
        ];
        return stream_context_create(['http' => $http]);
    }

    /** @param array<string> $headers */
    public function get(string $path, array $headers = [], bool $json = true): mixed {
        return self::request($this->build_context('GET', $headers, null), $this->url . $path, $json);
    }

    /** @param array<string> $headers */
    public function post(string $path, mixed $body, array $headers = [], bool $json = true): mixed {
        return self::request($this->build_context('POST', $headers, json_encode($body)), $this->url . $path, $json);
    }
}

class Auth {
    private static ?self $singleton = null;
    private \Server $server;
    private string $auth_key;

    private function __construct() {
        $this->server = new Server(getenv("AUTH"), false);
        $this->auth_key = $this->server->get('/public-key', json: false);
    }

    public static function instance(): self {
        if (self::$singleton === null)
            self::$singleton = new Auth();
        return self::$singleton;
    }

    // JWT decoding
    private function decode(string $token): mixed {
        try {
            $data = JWT::decode($token, new Key($this->auth_key, 'RS256'));
        } catch (UnexpectedValueException $e) {
            throw new AuthException('Invalid JWT token');
        }
        return $data;
    }

    public function username(string $token): ?string {
        return $this->decode($token)->username;
    }

    // API calls
    public function login(string $username, string $password): string {
        $response = $this->server->post('/login', [
            'username' => $username,
            'password' => $password
        ]);
        if (!$response->success)
            throw new AuthException($response->error);
        else
            return $response->token;
    }

    public function register(string $username, string $password): void {
        $response = $this->server->post('/register', [
            'username' => $username,
            'password' => $password
        ]);
        if (!$response->success)
            throw new AuthException($response->error);
    }
}

class Backend {
    private array $auth;
    private string $user;
    private \Server $server;

    public function __construct(mixed $token) {
        if ($token == null || !is_string($token))
            throw new AuthException('You are not logged in');
        $this->server = new Server(getenv("BACKEND"), true);
        $this->auth = ["Authorization: Bearer " . $token];
        $this->user = Auth::instance()->username($token);
    }

    public function create(): string {
        return bin2hex(random_bytes(16));
    }

    public static function check(string $id): bool {
        return preg_match('/^[0-9a-f]{32}$/', $id) === 1;
    }

    // API calls
    public function get(string $note): stdClass {
        if (!Backend::check($note))
            throw new BackendException('Invalid note ID');
        $response = $this->server->get("/$note", $this->auth);
        if (!$response->success)
            throw new BackendException($response->error);
        return $response->note;
    }

    public function set(string $note, string $title, string $content): void {
        if (!Backend::check($note))
            throw new BackendException('Invalid note ID');
        $body = [ 'title' => $title, 'content' => $content ];
        $response = $this->server->post("/$note", $body, $this->auth);
        if (!$response->success)
            throw new BackendException($response->error);
    }
}
