<?php

use Gregwar\Captcha\CaptchaBuilder;
use mystic\forum\Database;
use mystic\forum\exceptions\DatabaseConnectionException;
use mystic\forum\Messaging;
use mystic\forum\orm\Attachment;
use mystic\forum\orm\Post;
use mystic\forum\orm\Topic;
use mystic\forum\orm\User;
use mystic\forum\orm\UserPermissions;
use mystic\forum\utils\FileUtils;
use mystic\forum\utils\RequestUtils;
use mystic\forum\utils\ValidationUtils;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;

header_remove("X-Powered-By");

if (($_SERVER["HTTP_USER_AGENT"] ?? "") === "") {
    http_response_code(403);
    exit;
}

// function exception_error_handler($errno, $errstr, $errfile, $errline ) {
//     throw new ErrorException(html_entity_decode($errstr), $errno, 0, $errfile, $errline);
// }
// set_error_handler("exception_error_handler");

define("REGISTRATION_ENABLED", isTrue(env("REGISTRATION_ENABLED") ?? ""));

session_name("fsid");
session_start();

const MAX_ATTACHMENT_SIZE = 0x200000;
const MAX_ATTACHMENT_COUNT = 4;
const THUMB_MAX_DIM = 100;
const CAPTCHA_PHRASE_LENGTH = 7;
const CAPTCHA_CHARSET = 'ABCDEFGHKLMNPQRTWXYZ234789abdefghkmnpqr';

$_rq_method = $_SERVER["REQUEST_METHOD"] ?? "GET";
$_action = $_GET["_action"] ?? null;

$GLOBALS["action"] = $_action;

function generateCaptchaText(): string {
    $phrase = "";
    for ($i = 0; $i < CAPTCHA_PHRASE_LENGTH; ++$i)
        $phrase .= CAPTCHA_CHARSET[random_int(0, strlen(CAPTCHA_CHARSET) - 1)];
    return $phrase;
}

function _view(string $name, array $params = []): void {
    $___NAME = $name;
    $___PARAMS = &$params;
    extract($params);
    echo "<!--{" . htmlentities($name) . "}-->\n";
    include __DIR__ . "/application/views/" . $___NAME . ".php";
    echo "<!--{/" . htmlentities($name) . "}-->\n";
}

function isTrue(string $str): bool {
    $str = strtolower($str);
    return in_array($str, ["yes","true","y","t","on","enabled","1","?1"]);
}

function reArrayFiles(&$file_post) {
    $file_ary = [];
    $file_count = count($file_post['name']);
    $file_keys = array_keys($file_post);

    for ($i=0; $i<$file_count; $i++) {
        if ($file_post["error"][$i] !== UPLOAD_ERR_OK)
            continue;
        foreach ($file_keys as $key) {
            $file_ary[$i][$key] = $file_post[$key][$i];
        }
    }

    return $file_ary;
}

function renderPost(string $contents): string {
    $contents = htmlentities($contents);
    $contents = nl2br($contents, false);
    $lines = explode("\n", $contents);
    $contents = "";
    $lineBuf = [];
    $inQuote = false;
    foreach ($lines as $ln) {
        if (str_starts_with($ln, "&gt; ")) {
            if (!$inQuote) {
                $contents .= implode("\n", $lineBuf);
                $lineBuf = [];
                $inQuote = true;
            }
            $lineBuf []= substr($ln, 5);
        } else {
            if ($inQuote) {
                $contents .= "<blockquote>\n" . implode("\n", $lineBuf) . "</blockquote>\n";
                $lineBuf = [];
                $inQuote = false;
                if (trim($ln) === "<br>")
                    continue;
            }
            $lineBuf []= $ln;
        }
    }
    if ($inQuote) {
        $contents .= "<blockquote>" . implode("\n", $lineBuf) . "</blockquote>";
    } else {
        $contents .= implode("\n", $lineBuf);
    }

    return $contents;
}

function env(string $key): ?string {
    $val = getenv($key);
    if ($val === false)
        return null;
    return $val;
}

require_once __DIR__ . "/vendor/autoload.php";

require_once __DIR__ . "/application/i18n.php";
i18n_locale("de");

$db = null;
try {
    $db = new Database(Database::getConnectionString("db", getenv("POSTGRES_USER"), getenv("POSTGRES_PASSWORD"), getenv("POSTGRES_DBNAME")));
} catch (DatabaseConnectionException $ex) {
    Messaging::error([
        Messaging::bold(__("Failed to connect to database!")),
        Messaging::italic($ex->getMessage()),
    ]);
    exit;
}

$GLOBALS["db"] = &$db;

$db->ensureTable(User::class);
$db->ensureTable(Topic::class);
$db->ensureTable(Post::class);
$db->ensureTable(Attachment::class);

$superuser = new User();
$superuser->id = "SUPERUSER";
if (!$db->fetch($superuser)) {
    $superUserPassword = base64_encode(random_bytes(12));

    $superuser->name = "superuser";
    $superuser->email = "";
    $superuser->passwordHash = password_hash($superUserPassword, PASSWORD_DEFAULT);
    $superuser->displayName = "SuperUser";
    $superuser->created = new \DateTimeImmutable();
    $superuser->permissionMask = PHP_INT_MAX;
    $superuser->passwordResetRequired = false;
    $superuser->activated = true;
    $superuser->activationToken = "";

    $db->insert($superuser);

    Messaging::info([
        Messaging::bold("Superuser account created"),
        [
            "Username" => $superuser->name,
            "Password" => $superUserPassword,
        ],
        "Please note that the password can only be shown this time, so please note it down!",
    ]);
    exit;
}

$currentUser = RequestUtils::getAuthorizedUser($db);
$GLOBALS["currentUser"] = &$currentUser;

// initialization finished

if ($_action === "auth") {
    if ($currentUser) {
        header("Location: " . ($_GET["next"] ?? "."));
        exit;
    }

    if (RequestUtils::isRequestMethod("POST")) {
        $username = RequestUtils::getRequiredField("username");
        $password = RequestUtils::getRequiredField("password");

        $user = new User();
        $user->name = $username;
        if (!$db->fetchWhere($user, "name") || !password_verify($password, $user->passwordHash)) {
            RequestUtils::triggerFormError(__("Username or password incorrect!"));
        }
        
        if (!$user->activated) {
            RequestUtils::triggerFormError(__("Please activate your user account first!"));
        }

        RequestUtils::setAuthorizedUser($user);
        header("Location: " . ($_GET["next"] ?? "."));
    } else {
        _view("template_start", ["_title" => __("Log in")]);
        _view("template_navigation_start");
        _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
        _view("template_navigation_end");
        _view("form_login");
        _view("template_end");
    }
} elseif ($_action === "register") {
    if ($currentUser) {
        header("Location: " . ($_GET["next"] ?? "."));
        exit;
    }

    if (!REGISTRATION_ENABLED) {
        http_response_code(403);
        Messaging::error(__("Public registration disabled"));
        exit;
    }

    if (RequestUtils::isRequestMethod("POST")) {
        $doNotFill = $_POST["username"] ?? null;
        if (!empty($doNotFill)) {
            sleep(10);
            http_response_code(204);
            exit;
        }
        $username = RequestUtils::getRequiredField("df82a9bc21");
        $password = RequestUtils::getRequiredField("password");
        $passwordRetype = RequestUtils::getRequiredField("password_retype");
        $email = trim(RequestUtils::getRequiredField("email"));
        $displayName = RequestUtils::getRequiredField("display_name");
        $captcha = RequestUtils::getRequiredField("captcha");

        if ($captcha !== ($_SESSION["captchaPhrase"] ?? null)) {
            RequestUtils::triggerFormError(__("Incorrect CAPTCHA text!"));
        }
        
        // usernames are always lowercase
        $username = strtolower($username);

        if ($password !== $passwordRetype) {
            RequestUtils::triggerFormError(__("Passwords do not match!"));
        }

        if (strlen($password) < 8) {
            RequestUtils::triggerFormError(__("Password too short! Your password must consist of 8 or more characters"));
        }

        if (!ValidationUtils::isUsernameValid($username)) {
            RequestUtils::triggerFormError(__("Username has an invalid format"));
        }
        
        if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
            RequestUtils::triggerFormError(__("Invalid email address"));
        }

        $user = new User();
        $user->name = $username;
        $user->email = $email;

        if ($db->fetchWhere($user, "name")) {
            RequestUtils::triggerFormError(__("This username is already taken!"));
        }

        if ($db->fetchWhere($user, "email")) {
            RequestUtils::triggerFormError(__("This email address is already in use!"));
        }

        // re-create user so we don't forget to clear properties set by the above queries

        $user = new User();
        $user->id = $db->generateId();
        $user->displayName = $displayName;
        $user->name = $username;
        $user->email = $email;
        $user->passwordHash = password_hash($password, PASSWORD_DEFAULT);
        $user->permissionMask = UserPermissions::GROUP_USER;
        $user->passwordResetRequired = false;
        $user->activated = false;
        $user->activationToken = $db->generateId(12);
        $user->created = new \DateTimeImmutable();

        Transport::fromDsn(env("MAILER_DSN"))->send(
            (new Email())
                ->from(env("MAILER_FROM"))
                ->to(new Address($email, $displayName))
                ->text(__(
                    "Welcome to %forum_title%, %user_display_name%!\n" .
                    "\n" .
                    "Please activate your account by clicking the link below:\n" .
                    "%activation_link%\n" .
                    "\n" .
                    "Kind regards,\n" .
                    "%forum_copyright%",
                    params: [
                        "forum_title" => (env("MYSTIC_FORUM_TITLE") ?? "Forum"),
                        "user_display_name" => $displayName,
                        "activation_link" => env("PUBLIC_URL") . "?_action=verifyemail&token=" . urlencode($user->activationToken) . "&sig=" . urlencode(base64_encode(hash("sha256", env("SECRET") . $user->activationToken . $user->id, true))),
                        "forum_copyright" => (env("MYSTIC_FORUM_COPYRIGHT") ?? env("MYSTIC_FORUM_TITLE") ?? "Forum")
                    ]
                ))
                ->subject("Please activate your account")
        );

        $db->insert($user);

        Messaging::info(
            Messaging::html(nl2br(htmlentities(__("Your account has been created!\nPlease check your emails for an activation link!"), true)))
        );
    } else {
        _view("template_start", ["_title" => __("Register")]);
        _view("template_navigation_start");
        _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
        _view("template_navigation_end");
        _view("form_register");
        _view("template_end");
    }
} elseif ($_action === "verifyemail") {
    RequestUtils::ensureRequestMethod("GET");
    $token = $_GET["token"] ?? throw new Exception("Missing token");
    $sig = $_GET["sig"] ?? throw new Exception("Missing signature");

    $user = new User();
    $user->activated = false;
    $user->activationToken = $token;

    if (!$db->fetchWhere($user, [ "activated", "activation_token" ])) {
        http_response_code(400);
        Messaging::error(__("Invalid token"));
        exit;
    }

    $expectedSignature = base64_encode(hash("sha256", env("SECRET") . $user->activationToken . $user->id, true));
    
    if ($expectedSignature !== $sig) {
        http_response_code(400);
        Messaging::error(__("Invalid signature."));
        exit;
    }

    $user->activated = true;
    $user->activationToken = "";

    if (!$db->update($user)) {
        http_response_code(400);
        Messaging::error(__("Failed to update user"));
        exit;
    }

    Messaging::info([
        Messaging::html(nl2br(__(
                "Your account has been activated!\nPlease click %link%here%/link% to log in!",
                [
                    "link" => '<a href="?_action=auth">',
                    "/link" => '</a>',
                ]
        )), true),
    ]);
} elseif ($_action === "logout") {
    RequestUtils::unsetAuthorizedUser();
    header("Location: " . ($_GET["next"] ?? "."));
} elseif ($_action === "viewtopic") {
    $topicId = $_GET["topic"] ?? throw new Exception("Missing topic id");
    $topic = new Topic();
    $topic->id = $topicId;
    if (!$db->fetch($topic)) {
        http_response_code(404);
        Messaging::error("No topic exists with this id");
        exit;
    }

    if (RequestUtils::isRequestMethod("POST")) {
        if (!$currentUser) {
            http_response_code(403);
            Messaging::error("You need to be logged in to add new posts!");
            exit;
        }

        $attachments = reArrayFiles($_FILES["files"]);

        if (count($attachments) > MAX_ATTACHMENT_COUNT)
            RequestUtils::triggerFormError(__("Too many attachments"));

        // check all attachments before saving one
        foreach ($attachments as $att) {
            if ($att["size"] > MAX_ATTACHMENT_SIZE) {
                RequestUtils::triggerFormError(__("Individual file size exceeded"));
            }
        }

        $message = trim(RequestUtils::getRequiredField("message"));

        if (strlen($message) < 1 || strlen($message) > 0x8000) {
            RequestUtils::triggerFormError(__("Message too short or too long!"));
        }

        $post = new Post();
        $post->id = $db->generateId();
        $post->authorId = $currentUser->id;
        $post->topicId = $topicId;
        $post->content = $message;
        $post->postDate = new DateTimeImmutable();
        $post->deleted = false;
        $post->edited = false;

        $db->insert($post);

        foreach ($attachments as $att) {
            [
                "name" => $name,
                "type" => $type,
                "tmp_name" => $tmpName,
            ] = $att;
            $attachment = new Attachment();
            $attachment->id = $db->generateId();
            $attachment->name = $name;
            $attachment->mimeType = $type;
            $attachment->postId = $post->id;
            $attachment->contents = file_get_contents($tmpName);
            
            $db->insert($attachment);
        }

        header("Location: ?_action=viewtopic&topic=" . urlencode($topicId) . "#form");
    } else {
        $posts = $db->fetchCustom(Post::class, 'WHERE topic_id = $1 ORDER BY post_date', [ $topicId ]);
        $userCache = [];

        $topicAuthor = null;
        if ($topic->createdBy !== null) {
            $topicAuthor = new User();
            $topicAuthor->id = $topic->createdBy;
            if (!$db->fetch($topicAuthor)) {
                $topicAuthor = null;
            }
        }

        _view("template_start", ["_title" => $topic->title]);
        _view("template_navigation_start");
        _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
        _view("template_navigation_end");
        _view("view_topic_start", ["topic" => $topic, "topicAuthor" => $topicAuthor]);

        /** @var Post $post */
        foreach ($posts as $post) {
            /** @var ?User $topicAuthor */
            $topicAuthor = null;
            if ($post->authorId !== null && !isset($userCache[$post->authorId])) {
                $usr = new User();
                $usr->id = $post->authorId;
                if ($db->fetch($usr))
                    $userCache[$post->authorId] = &$usr;
            }
            if (isset($userCache[$post->authorId]))
                $topicAuthor = &$userCache[$post->authorId];

            $attachments = $db->fetchCustom(Attachment::class, 'WHERE post_id = $1', [ $post->id ]);

            _view("view_post", [
                "post" => $post,
                "postAuthor" => $topicAuthor,
                "attachments" => $attachments,
            ]);
        }

        _view("view_topic_end");

        if ($currentUser) {
            _view("form_addpost");
        }

        _view("template_end");
    }

} elseif ($_action === "newtopic") {
    if (!$currentUser) {
        http_response_code(403);
        Messaging::error("You need to be logged in to create new topics!");
        exit;
    }

    if (RequestUtils::isRequestMethod("POST")) {
        $title = trim(RequestUtils::getRequiredField("title"));
        $message = trim(RequestUtils::getRequiredField("message"));

        $attachments = reArrayFiles($_FILES["files"]);

        if (count($attachments) > MAX_ATTACHMENT_COUNT)
            RequestUtils::triggerFormError(__("Too many attachments"));

        // check all attachments before saving one
        foreach ($attachments as $att) {
            if ($att["size"] > MAX_ATTACHMENT_SIZE) {
                RequestUtils::triggerFormError(__("Individual file size exceeded"));
            }
        }

        if (strlen($title) < 1 || strlen($title) > 255) {
            RequestUtils::triggerFormError(__("Title too short or too long!"));
        }

        if (strlen($message) < 1 || strlen($message) > 0x8000) {
            RequestUtils::triggerFormError(__("Message too short or too long!"));
        }

        $topic = new Topic();
        $topic->createdBy = $currentUser->id;
        $topic->id = $db->generateId();
        $topic->title = $title;
        $topic->creationDate = new DateTimeImmutable();

        $db->insert($topic);

        $post = new Post();
        $post->id = $db->generateId();
        $post->authorId = $currentUser->id;
        $post->topicId = $topic->id;
        $post->content = $message;
        $post->postDate = $topic->creationDate;
        $post->deleted = false;
        $post->edited = false;

        $db->insert($post);

        foreach ($attachments as $att) {
            [
                "name" => $name,
                "type" => $type,
                "tmp_name" => $tmpName,
            ] = $att;
            $attachment = new Attachment();
            $attachment->id = $db->generateId();
            $attachment->name = $name;
            $attachment->mimeType = $type;
            $attachment->postId = $post->id;
            $attachment->contents = file_get_contents($tmpName);
            
            $db->insert($attachment);
        }

        header("Location: ?_action=viewtopic&topic=" . urlencode($topic->id));
    } else {
        _view("template_start", ["_title" => __("New topic")]);
        _view("template_navigation_start");
        _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
        _view("template_navigation_end");
        _view("form_newtopic");
        _view("template_end");
    }
} elseif ($_action === "lookupuser") {
    RequestUtils::ensureRequestMethod("GET");
    $userHandle = $_GET["handle"] ?? throw new Exception("Missing handle");

    $user = new User();
    $user->name = $userHandle;

    if (!$db->fetchWhere($user, "name")) {
        http_response_code(404);
        Messaging::error(__("No user with name @%user_handle%", [ "user_handle" => $userHandle ]));
        exit;
    }

    header("Location: ./?_action=viewuser&user=" . urlencode($user->id));
} elseif ($_action === "viewuser") {
    $userId = $_GET["user"] ?? throw new Exception("Missing user id");
    $user = new User();
    $user->id = $userId;
    if (!$db->fetch($user)) {
        http_response_code(404);
        Messaging::error(__("No user exists with this id"));
        exit;
    }

    $lastNameChangeTooRecent = false;
    $isOwnProfile = $user->id === $currentUser?->id;
    if ($isOwnProfile && $user->nameLastChanged !== null) {
        $diff = $user->nameLastChanged->diff(new DateTime());
        $diffSeconds = (new DateTime())->setTimestamp(0)->add($diff)->getTimestamp();
        $diffDays = $diffSeconds / 60.0 / 60.0 / 24.0 / 30.0;
        $lastNameChangeTooRecent = $diffDays <= 30;
    }

    if (RequestUtils::isRequestMethod("POST")) {
        $displayName = RequestUtils::getRequiredField("display_name");
        $pfpAction = RequestUtils::getRequiredField("pfp_action");

        $userName = $_POST["name"] ?? $user->name;

        $user->displayName = $displayName;

        $userName = strtolower($userName);

        if ($userName !== $user->name) {
            if ($lastNameChangeTooRecent) {
                RequestUtils::triggerFormError(__("You can only change your username every 30 days!"));
            } else {
                if (!ValidationUtils::isUsernameValid($userName))
                    RequestUtils::triggerFormError(__("Invalid username!"));
                if (!ValidationUtils::isUsernameAvailable($db, $userName))
                    RequestUtils::triggerFormError(__("This username is already taken!"));
                $user->name = $userName;
                $user->nameLastChanged = new DateTimeImmutable();
            }
        }

        switch ($pfpAction) {
            case "keep":
                // Do nothing
                break;
            case "remove":
                $user->profilePicture = null;
                break;
            case "replace": {
                if (!isset($_FILES["pfp"]) || $_FILES["pfp"]["error"] !== UPLOAD_ERR_OK) {
                    RequestUtils::triggerFormError(__("Please upload an image to change your profile picture"));
                }
                $im = @imagecreatefromjpeg($_FILES["pfp"]["tmp_name"]);
                if ($im === false)
                    $im = @imagecreatefrompng($_FILES["pfp"]["tmp_name"]);
                if ($im === false)
                    RequestUtils::triggerFormError(__("Please upload a valid PNG or JPEG file"));
                /** @var \GdImage $im */
                $thumb = imagecreatetruecolor(64, 64);
                imagecopyresampled($thumb, $im, 0, 0, 0, 0, 64, 64, imagesx($im), imagesy($im));
                imagedestroy($im);
                $stream = fopen("php://memory", "w+");
                imagejpeg($thumb, $stream, 50);
                rewind($stream);
                imagedestroy($thumb);
                $user->profilePicture = stream_get_contents($stream);
                fclose($stream);
            } break;
            default:
                RequestUtils::triggerFormError("Invalid value for pfp_action");
                break;
        }

        if (!$db->update($user))
            RequestUtils::triggerFormError(__("Failed to save changes", context: "Update profile"));

        header("Location: $_SERVER[REQUEST_URI]");
    } else {
        $posts = $db->fetchCustom(Post::class, 'WHERE author_id = $1 ORDER BY post_date DESC', [ $userId ]);
        $topics = [];
        foreach ($posts as $post) {
            if (isset($topics[$post->topicId]))
                continue;
            $topic = new Topic();
            $topic->id = $post->topicId;
            if (!$db->fetch($topic))
                continue;
            $topics[$post->topicId] = $topic;
        }
        _view("template_start", ["_title" => $user->displayName]);
        _view("template_navigation_start");
        _view("template_navigation", ["user" => $currentUser]);
        _view("template_navigation_end");
        _view("view_user", [
            "user" => $user,
            "posts" => $posts,
            "topics" => $topics,
            "lastNameChangeTooRecent" => $lastNameChangeTooRecent,
        ]);
        _view("template_end");
    }
} elseif ($_action === "attachment") {
    if (!$currentUser) {
        http_response_code(403);
        Messaging::error(__("You must be logged in to view attachments"));
        exit;
    }

    $attId = $_GET["attachment"] ?? throw new Exception("Missing attachment id");
    $attachment = new Attachment();
    $attachment->id = $attId;
    if (!$db->fetch($attachment)) {
        http_response_code(404);
        Messaging::error(__("No attachment exists with this id"));
        exit;
    }

    $name = preg_replace('/[\r\n\t\/]/', '_', $attachment->name);
    
    $extension = pathinfo($attachment->name, PATHINFO_EXTENSION);

    $mime = FileUtils::getMimeTypeForExtension($extension);
    switch ($mime) {
        case "text/html":
        case "text/css":
        case "text/javascript":
        case "text/xml":
        case "application/css":
        case "application/javascript":
        case "application/xml":
            $mime = "text/plain";
            break;
    }
    header("Content-Type: " . $mime);
    header("Content-Length: " . strlen($attachment->contents));
    header("Cache-Control: no-cache");
    header("Content-Disposition: inline; filename=\"" . $name . "\"");
    echo $attachment->contents;
} elseif ($_action === "profilepicture") {
    $userId = $_GET["user"] ?? throw new Exception("Missing user id");
    $user = new User();
    $user->id = $userId;
    if (!$db->fetch($user)) {
        http_response_code(404);
        Messaging::error(__("No user exists with this id"));
        exit;
    }
    
    $ifNoneMatch = $_SERVER["HTTP_IF_NONE_MATCH"] ?? null;
    if ($ifNoneMatch !== null)
        $ifNoneMatch = trim($ifNoneMatch, '"');
    
    if ($user->profilePicture === null) {
        $fallback = __DIR__ . "/application/assets/user-fallback.jpg";
        $etag = md5("\0");
        header("Content-Type: image/jpeg");
        header("Content-Length: " . filesize($fallback));
        header("Cache-Control: no-cache");
        header("ETag: \"" . $etag . "\"");
        if ($ifNoneMatch === $etag)
            http_response_code(304);
        else
            readfile($fallback);
    } else {
        $etag = md5($user->profilePicture);
        header("Content-Type: image/jpeg");
        header("Content-Length: " . strlen($user->profilePicture));
        header("Cache-Control: no-cache");
        header("ETag: \"" . $etag . "\"");
        if ($ifNoneMatch === $etag)
            http_response_code(304);
        else
            echo $user->profilePicture;
    }
} elseif ($_action === "thumb") {

    $attId = $_GET["attachment"] ?? throw new Exception("Missing attachment id");
    $attachment = new Attachment();
    $attachment->id = $attId;
    if (!$db->fetch($attachment)) {
        http_response_code(404);
        Messaging::error(__("No attachment exists with this id"));
        exit;
    }

    if (!str_starts_with($attachment->mimeType, "image/")) {
        http_response_code(400);
        Messaging::error(__("Attachment is not an image"));
        exit;
    }

    $contentHash = hash("sha256", $attachment->contents);
    
    $cacheId = bin2hex($attachment->id);
    $cacheDir = sys_get_temp_dir() . "/mystic/forum/0/cache/thumbs/" . substr($cacheId, 0, 2) . "/" . substr($cacheId, 0, 8) . "/";
    if (!is_dir($cacheDir))
        mkdir($cacheDir, recursive: true);

    $cacheFileData = $cacheDir . $cacheId . ".data";
    $cacheFileInfo = $cacheDir . $cacheId . ".info";

    if (is_file($cacheFileData) && is_file($cacheFileInfo)) {
        $info = json_decode(file_get_contents($cacheFileInfo));
        if ($info->contentHash === $contentHash) {
            header("Content-Type: image/jpeg");
            header("Cache-Control: max-age=86400");
            header("X-Debug-Content: $cacheFileData");
            readfile($cacheFileData);
            exit;
        }
    }

    $im = imagecreatefromstring($attachment->contents);
    $w = imagesx($im);
    $h = imagesy($im);
    $r = $w / floatval($h);

    if ($w > $h) {
        $nw = THUMB_MAX_DIM;
        $nh = floor($nw / $r);
    } else {
        $nh = THUMB_MAX_DIM;
        $nw = floor($r * $nh);
    }

    $thumb = imagecreatetruecolor($nw, $nh);
    imagecopyresampled($thumb, $im, 0, 0, 0, 0, $nw, $nh, $w, $h);
    imagedestroy($im);
    
    header("Content-Type: image/jpeg");
    header("Cache-Control: max-age=86400");
    imagejpeg($thumb, $cacheFileData, 40);
    imagedestroy($thumb);
    file_put_contents($cacheFileInfo, json_encode([
        "format" => 1,
        "contentHash" => $contentHash,
        "created" => time(),
    ], JSON_UNESCAPED_SLASHES));
    readfile($cacheFileData);
} elseif ($_action === "deletepost") {
    RequestUtils::ensureRequestMethod("POST");

    if (!$currentUser) {
        http_response_code(403);
        Messaging::error("You need to be logged in to delete posts!");
        exit;
    }

    $postId = RequestUtils::getRequiredField("post");

    $post = new Post();
    $post->id = $postId;

    if (!$db->fetch($post) || $post->deleted) {
        http_response_code(404);
        Messaging::error("No post exists with this id");
        exit;
    }

    $topicAuthor = new User();
    $topicAuthor->id = $post->authorId;

    if (!$db->fetch($topicAuthor))
        $topicAuthor = null;

    $canEdit = ($currentUser->id === $topicAuthor?->id && $topicAuthor?->hasPermission(UserPermissions::DELETE_OWN_POST))
              || ($currentUser->hasPermission(UserPermissions::DELETE_OTHER_POST));

    if (!$canEdit) {
        http_response_code(403);
        Messaging::error("You don't have permission to delete this post");
        exit;
    }

    $attachments = $db->fetchCustom(Attachment::class, 'WHERE post_id = $1', [ $post->id ]);

    $confirm = $_POST["confirm"] ?? null;
    if ($confirm !== null) {
        $expectedConfirm = base64_encode(hash("sha256", "confirm" . $post->id, true));
        if ($confirm !== $expectedConfirm) {
            http_response_code(400);
            Messaging::error("Invalid confirmation");
            exit;
        }

        $post->deleted = true;
        $post->content = "";

        if (!$db->update($post)) {
            http_response_code(500);
            Messaging::error("Failed to delete post");
            exit;
        }
        
        foreach ($attachments as $attachment) {
            if (!$db->delete($attachment)) {
                http_response_code(500);
                Messaging::error("Failed to delete attachment");
                exit;
            }
        }

        header("Location: ?_action=viewtopic&topic=" . urlencode($post->topicId));
    } else {
        _view("template_start", ["_title" => __("Delete post")]);
        _view("template_navigation_start");
        _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
        _view("template_navigation_end");
        _view("form_delete_post_confirm", [
            "post" => $post,
            "postAuthor" => $topicAuthor,
            "attachments" => $attachments,
        ]);
        _view("template_end");
    }
} elseif ($_action === "updatepost") {
    RequestUtils::ensureRequestMethod("POST");

    if (!$currentUser) {
        http_response_code(403);
        Messaging::error("You need to be logged in to update posts!");
        exit;
    }

    $postId = RequestUtils::getRequiredField("post");
    $message = RequestUtils::getRequiredField("message");

    $post = new Post();
    $post->id = $postId;

    if (!$db->fetch($post) || $post->deleted) {
        http_response_code(404);
        Messaging::error("No post exists with this id");
        exit;
    }

    $topicAuthor = new User();
    $topicAuthor->id = $post->authorId;

    if (!$db->fetch($topicAuthor))
        $topicAuthor = null;

    $canEdit = ($currentUser->id === $topicAuthor?->id && $topicAuthor?->hasPermission(UserPermissions::EDIT_OWN_POST))
              || ($currentUser->hasPermission(UserPermissions::EDIT_OTHER_POST));

    if (!$canEdit) {
        http_response_code(403);
        Messaging::error("You don't have permission to edit this post");
        exit;
    }

    $confirm = $_POST["confirm"] ?? null;

    $post->content = $message;
    $post->edited = true;

    if (!$db->update($post)) {
        http_response_code(500);
        Messaging::error("Failed to update post");
        exit;
    }

    header("Location: ?_action=viewtopic&topic=" . urlencode($post->topicId) . "#post-" . urlencode($postId));
} elseif ($_action === "deletetopic") {
    RequestUtils::ensureRequestMethod("POST");

    if (!$currentUser) {
        http_response_code(403);
        Messaging::error("You need to be logged in to delete topics!");
        exit;
    }

    $topicId = RequestUtils::getRequiredField("topic");

    $topic = new Topic();
    $topic->id = $topicId;

    if (!$db->fetch($topic)) {
        http_response_code(404);
        Messaging::error("No topic exists with this id");
        exit;
    }

    $topicAuthor = new User();
    $topicAuthor->id = $topic->createdBy;

    if (!$db->fetch($topicAuthor))
        $topicAuthor = null;

    $canEdit = ($currentUser->id === $topicAuthor?->id && $topicAuthor?->hasPermission(UserPermissions::DELETE_OWN_TOPIC))
              || ($currentUser->hasPermission(UserPermissions::DELETE_OTHER_TOPIC));

    if (!$canEdit) {
        http_response_code(403);
        Messaging::error("You don't have permission to delete this topic");
        exit;
    }

    $confirm = $_POST["confirm"] ?? null;
    if ($confirm !== null) {
        $expectedConfirm = base64_encode(hash("sha256", "confirm" . $topic->id, true));
        if ($confirm !== $expectedConfirm) {
            http_response_code(400);
            Messaging::error("Invalid confirmation");
            exit;
        }

        if (!$db->delete($topic)) {
            http_response_code(500);
            Messaging::error("Failed to delete topic");
            exit;
        }

        header("Location: .");
    } else {
        _view("template_start", ["_title" => "Delete topic"]);
        _view("template_navigation_start");
        _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
        _view("template_navigation_end");
        _view("form_delete_topic_confirm", [
            "topic" => $topic,
            "topicAuthor" => $topicAuthor,
        ]);
        _view("template_end");
    }
} elseif ($_action === "updatetopic") {
    RequestUtils::ensureRequestMethod("POST");

    if (!$currentUser) {
        http_response_code(403);
        Messaging::error("You need to be logged in to update topics!");
        exit;
    }

    $topicId = RequestUtils::getRequiredField("topic");
    $title = RequestUtils::getRequiredField("title");

    $topic = new Topic();
    $topic->id = $topicId;

    if (!$db->fetch($topic)) {
        http_response_code(404);
        Messaging::error("No topic exists with this id");
        exit;
    }

    $topicAuthor = new User();
    $topicAuthor->id = $topic->createdBy;

    if (!$db->fetch($topicAuthor))
        $topicAuthor = null;

    $canEdit = ($currentUser->id === $topicAuthor?->id && $topicAuthor?->hasPermission(UserPermissions::EDIT_OWN_TOPIC))
            || ($currentUser->hasPermission(UserPermissions::EDIT_OTHER_TOPIC));

    if (!$canEdit) {
        http_response_code(403);
        Messaging::error("You don't have permission to update this topic");
        exit;
    }

    $topic->title = $title;

    if (!$db->update($topic)) {
        http_response_code(500);
        Messaging::error("Failed to update topic");
        exit;
    }

    header("Location: ./?_action=viewtopic&topic=" . urlencode($topicId));
} elseif ($_action === "captcha") {
    $phrase = generateCaptchaText();
    $builder = new CaptchaBuilder($phrase);
    $builder->build(192, 48);
    $_SESSION["captchaPhrase"] = $phrase;
    header("Content-Type: image/jpeg");
    header("Pragma: no-cache");
    header("Cache-Control: no-cache");
    $builder->save(null, 40);
} elseif ($_action === null) {
    _view("template_start");
    _view("template_navigation_start");
    _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
    _view("template_navigation_end");
    _view("view_topics", ["topics" => $db->fetchCustom(Topic::class, "ORDER BY creation_date DESC")]);
    _view("template_end");
} else {
    http_response_code(404);
    Messaging::error("Invalid or unknown action $_action");
}