<?php

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\TopicLogMessage;
use mystic\forum\orm\User;
use mystic\forum\orm\UserPermissions;
use mystic\forum\utils\RequestUtils;
use mystic\forum\utils\StringUtils;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
use Twig\TwigFilter;
use Twig\TwigFunction;

header_remove("X-Powered-By");

const MYSTICBB_VERSION = "0.5.2";

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

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

const __ROOT__ = __DIR__;

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 msg_error(string $err, bool $skipLoginCheck = false): void {
    render("error_page.twig", [
        "message" => $err,
        "skipLoginCheck" => $skipLoginCheck,
    ]);
}

function msg_info(string $msg, bool $skipLoginCheck = false): void {
    render("info_page.twig", [
        "message" => $msg,
        "skipLoginCheck" => $skipLoginCheck,
    ]);
}

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 generatePasswordResetLink(Database &$db, User &$user): ?string {
    $token = $db->generateId(20);

    $user->passwordResetToken = $token;
    $user->passwordResetTokenCreated = new \DateTimeImmutable();

    if (!$db->update($user)) {
        return null;
    }

    return env("PUBLIC_URL") . "?_action=pwreset&token=" . urlencode($user->passwordResetToken) . "&sig=" . urlencode(base64_encode(hash("sha256", env("SECRET") . $user->passwordResetToken . $user->id . $user->passwordHash, true)));
}

function decodePasswordResetLink(Database &$db, string $token, string $signature): ?User {
    $user = new User();
    $user->passwordResetToken = $token;

    if (!$db->fetchWhere($user, "password_reset_token")) {
        return null;
    }

    $then = $user->passwordResetTokenCreated;
    $now = new \DateTimeImmutable();
    if (($now->getTimestamp() - $then->getTimestamp()) >= 3600) {
        return null;
    }

    $expectedSignature = base64_encode(hash("sha256", env("SECRET") . $user->passwordResetToken . $user->id . $user->passwordHash, true));
    if ($expectedSignature !== $signature) {
        return null;
    }

    return $user;
}

function getThemeAndLangInfo(): array {
    $availableThemes = [];
    $currentTheme = $_GET["theme"] ?? $_COOKIE["theme"] ?? env("MYSTIC_FORUM_THEME") ?? "default";
    foreach (scandir($dir = __ROOT__ . '/application/themes/') as $ent) {
        if ($ent[0] === "." || !is_dir($dir . "/" . $ent) || !is_file($theme_file = $dir . "/" . $ent . "/theme.json"))
            continue;
        $theme_info = json_decode(file_get_contents($theme_file));
        $availableThemes[$ent] = $theme_info;
    }

    $availableLangs = [
        "en" => "🇺🇸 English",
    ];
    foreach (i18n_get_available_locales() as $loc) {
        if (isset($availableLangs[$loc]))
            continue;
        $metadata = i18n_metadata($loc);
        $availableLangs[$loc] = $metadata["langName"] ?? $loc;
    }

    return [
        "availableThemes" => $availableThemes,
        "currentTheme" => $currentTheme,

        "availableLangs" => $availableLangs,
        "currentLang" => i18n_get_current_locale(),
    ];
}

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 render(string $page, array $context = []): void {
    $defaultTemplate = "bootstrap-3"; // bootstrap-3 is the default for backwards compatibility
    $templateDir = __ROOT__ . "/application/templates";

    [
        "availableThemes" => $availableThemes,
        "currentTheme" => $currentTheme,
    ] = getThemeAndLangInfo();

    $currentThemeInfo = $availableThemes[$currentTheme] ?? (object)[];
    $currentTemplate = $currentThemeInfo->template ?? $defaultTemplate;

    $availableTemplates = array_values(array_filter(scandir($templateDir), function($theme) use ($templateDir) {
        return $theme[0] !== "." && is_dir($templateDir . "/" . $theme);
    }));

    if (!in_array($currentTemplate, $availableTemplates))
        $currentTemplate = $defaultTemplate;

    $loader = new FilesystemLoader([
        $templateDir . "/" . $currentTemplate,
    ]);
    $twig = new Environment($loader, [
        // TODO Enable caching
    ]);

    $twig->addFunction(new TwigFunction("__", __(...), [ "is_safe" => ["html"] ]));
    $twig->addFunction(new TwigFunction("___", ___(...), [ "is_safe" => ["html"] ]));
    $twig->addFunction(new TwigFunction("renderPost", renderPost(...), [ "is_safe" => ["html"] ]));
    $twig->addFunction(new TwigFunction("renderPostSummary", renderPostSummary(...), [ "is_safe" => ["html"] ]));
    $twig->addFunction(new TwigFunction("permission", fn(string $name): int => constant(UserPermissions::class . "::" . $name)));
    $twig->addFunction(new TwigFunction("getAndClearFormError", RequestUtils::getAndClearFormError(...)));
    $twig->addFunction(new TwigFunction("lastFormField", function(string $formId, string $field): ?string {
        $lastFormId = "";
        $lastForm = RequestUtils::getLastForm($lastFormId) ?? null;
        if ($lastForm === null || $lastFormId !== $formId)
            return null;
        return $lastForm[$field] ?? null;
    }));

    $twig->addFilter(new TwigFilter("hash", fn(string $data, string $algo, bool $binary = false, array $options = []) => hash($algo, $data, $binary, $options)));
    $twig->addFilter(new TwigFilter("base64_encode", base64_encode(...)));
    $twig->addFilter(new TwigFilter("base64_decode", base64_decode(...)));

    echo $twig->render($page, [
        "g" => [
            "get" => &$_GET,
            "post" => &$_POST,
            "request" => &$_REQUEST,
            "session" => &$_SESSION,
            "cookie" => &$_COOKIE,
            "env" => &$_ENV,
            "server" => &$_SERVER,
            "globals" => $GLOBALS,
        ],
        "ctx" => &$context,

        "currentUser" => &$GLOBALS["currentUser"],
        ...getThemeAndLangInfo(),
    ]);
    RequestUtils::clearLastForm();
}

const TAGS_REGEX = '/\[(b|i|u|s|\\^|_|spoiler)\](.+?)\[\/\1\]/s';

function expandTags(string $contents): string {
    return preg_replace_callback(TAGS_REGEX, function(array $m) {
        $randomId = "target-" . bin2hex(random_bytes(8));
        $inner = $m[2];
        $tag = [
            "b" => ["<strong>", "</strong>" ],
            "i" => ["<em>", "</em>" ],
            "s" => ["<del>", "</del>" ],
            "^" => ["<sup>", "</sup>" ],
            "_" => ["<sub>", "</sub>" ],
            "spoiler" => [ "<!--##SPOILER_START##--><div class=\"panel panel-default\"><div class=\"panel-heading\"><div class=\"panel-title\"><a role=\"button\" data-toggle=\"collapse\" href=\"#$randomId\"><i class=\"fa fa-eye\"></i> Spoiler</a></div></div><div id=\"$randomId\" class=\"panel-collapse collapse\"><div class=\"panel-body\">", "</div></div></div><!--##SPOILER_END##-->" ],
        ][$m[1]] ?? $m[1];
        if (preg_match(TAGS_REGEX, $inner))
            $inner = expandTags($inner);
        if (!is_array($tag)) $tag = [ "<$tag>", "</$tag>" ];
        $tagsBefore = $tag[0];
        $tagsAfter = $tag[1];
        return "{$tagsBefore}{$inner}{$tagsAfter}";
    }, $contents);
}

function renderPostSummary(string $contents): string {
    $contents = renderPost($contents);
    // remove spoiler contents so they don't appear in summaries
    $contents = preg_replace('/<!--##SPOILER_START##-->(.*?)<!--##SPOILER_END##-->/s', '[ ' . __("Spoiler") . ' ]', $contents);
    return htmlentities(html_entity_decode(StringUtils::truncate(strip_tags($contents), 100)));
}

function renderPost(string $contents): string {
    $contents = preg_replace('~\R~u', "\n", $contents);
    $contents = trim($contents);
    $contents = preg_replace('/\n{3,}/', "\n\n", $contents);
    $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);
    }
    $contents = expandTags($contents);

    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";

if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["lang"])) {
    parse_str($_SERVER["QUERY_STRING"], $query);
    if (empty($query["lang"])) {
        setcookie("lang", "", 100);
    } else {
        setcookie("lang", $query["lang"], time()+60*60*24*30);
    }
    unset($query["lang"]);
    $path = strtok($_SERVER["REQUEST_URI"], "?");
    header("Location: $path?" . http_build_query($query));
    exit;
}

$user_locale = env("LOCALE") ?? $_COOKIE["lang"] ?? locale_accept_from_http($_SERVER["HTTP_ACCEPT_LANGUAGE"] ?? "");
$chosen_locale = locale_lookup(i18n_get_available_locales(), $user_locale, true, "en");
i18n_locale($user_locale);

$db = null;
try {
    $db = new Database(Database::getConnectionString("db", getenv("POSTGRES_USER"), getenv("POSTGRES_PASSWORD"), getenv("POSTGRES_DBNAME")));
} catch (DatabaseConnectionException $ex) {
    msg_error(
        __("Failed to connect to database:\n%details%", [
            "details" => $ex->getMessage(),
        ]),
        true
    );
    exit;
}

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

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

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

    $suEmail = env("MYSTIC_FORUM_SUPERUSER_EMAIL");
    if ($suEmail === null) {
        http_response_code(500);
        Messaging::error("No superuser email defined. Please set the 'MYSTIC_FORUM_SUPERUSER_EMAIL' environment variable accordingly");
        exit;
    }

    $superuser->name = "superuser";
    $superuser->email = $suEmail;
    $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

function invalid_action(string $_action): never {
    http_response_code(404);
    msg_error(__("Invalid or unknown action \"%action%\"", [
        "action" => $_action,
    ]));
    exit;
}

if ($_action !== null && !preg_match('/^[a-z][a-z0-9_]*$/', $_action))
    invalid_action($_action);

$_action ??= "_default";

$actionDir = __DIR__ . "/application/actions/$_action";

if (!is_dir($actionDir))
    invalid_action($_action);

$actionMethod = strtolower(preg_replace('/[^A-Za-z]/', '', $_rq_method));

if (is_file($commonFile = $actionDir . "/_common.php"))
    include $commonFile;

if (is_file($anyFile = $actionDir . "/_any.php"))
    include $anyFile;
elseif (is_file($methodFile = $actionDir . "/$actionMethod.php"))
    include $methodFile;
else {
    http_response_code(405);
    Messaging::error("Invalid request method " . RequestUtils::getRequestMethod());
}