<?php
require_once __DIR__ . "/../config.php";

function readJsonFile($file)
{
    if (!file_exists($file)) {
        return [];
    }

    $content = file_get_contents($file);
    if ($content === false || trim($content) === "") {
        return [];
    }

    $data = json_decode($content, true);
    return is_array($data) ? $data : [];
}

function writeJsonFile($file, $data)
{
    $dir = dirname($file);
    if (!is_dir($dir) && !mkdir($dir, 02775, true) && !is_dir($dir)) {
        return false;
    }

    @chmod($dir, 02775);

    if (file_exists($file) && !is_writable($file)) {
        @chmod($file, 0664);
    }

    $json = json_encode(
        $data,
        JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
    );

    // Atomic write using temp file + rename
    $tmpFile = $file . ".tmp";
    $result = file_put_contents($tmpFile, $json, LOCK_EX);

    if ($result === false) {
        return false;
    }

    @chmod($tmpFile, 0664);

    // Rename is atomic on same filesystem
    if (!rename($tmpFile, $file)) {
        unlink($tmpFile);
        return false;
    }

    @chmod($file, 0664);
    return true;
}

function sanitize($input)
{
    return trim(strip_tags((string) $input));
}

function escape($value)
{
    return htmlspecialchars((string) $value, ENT_QUOTES, "UTF-8");
}

/**
 * CSRF Protection
 */
function generateCsrfToken()
{
    if (empty($_SESSION["csrf_token"])) {
        $_SESSION["csrf_token"] = bin2hex(random_bytes(32));
    }
    return $_SESSION["csrf_token"];
}

function validateCsrfToken($token)
{
    if (empty($_SESSION["csrf_token"])) {
        return false;
    }
    return hash_equals($_SESSION["csrf_token"], $token);
}

function csrfField()
{
    return '<input type="hidden" name="csrf_token" value="' .
        escape(generateCsrfToken()) .
        '">';
}

/**
 * File-based rate limiting (per client IP) for shared hosting without Redis.
 * State lives under DATA_DIR/ratelimit/ (not web-accessible with default .htaccess).
 */
function getRateLimitClientIp(): string
{
    $ip = trim((string) ($_SERVER["REMOTE_ADDR"] ?? ""));
    return $ip !== "" ? $ip : "unknown";
}

function getRateLimitStatePath(string $name): ?string
{
    $name = preg_replace("/[^a-z0-9_-]/", "", strtolower($name));
    if ($name === "") {
        $name = "default";
    }
    $dir = rtrim(DATA_DIR, "/\\") . "/ratelimit";
    return $dir . "/" . $name . ".json";
}

/**
 * @return bool true if under limit; when $consume is true, increments counter on allow
 */
function rateLimitEvaluate(
    string $name,
    int $maxAttempts,
    int $windowSeconds,
    bool $consume,
): bool {
    if ($maxAttempts < 1 || $windowSeconds < 1) {
        return true;
    }

    $path = getRateLimitStatePath($name);
    if ($path === null) {
        return true;
    }

    $dir = dirname($path);
    if (
        !is_dir($dir) &&
        !@mkdir($dir, 02775, true) &&
        !is_dir($dir)
    ) {
        return true;
    }
    if (is_dir($dir)) {
        @chmod($dir, 02775);
    }

    $fh = fopen($path, "c+");
    if ($fh === false) {
        return true;
    }

    if (!flock($fh, LOCK_EX)) {
        fclose($fh);
        return true;
    }

    rewind($fh);
    $raw = stream_get_contents($fh);
    $data = [];
    if (is_string($raw) && trim($raw) !== "") {
        $decoded = json_decode($raw, true);
        if (is_array($decoded)) {
            $data = $decoded;
        }
    }

    $now = time();
    $ip = getRateLimitClientIp();

    foreach ($data as $key => $entry) {
        if (!is_array($entry)) {
            unset($data[$key]);
            continue;
        }
        $w = (int) ($entry["w"] ?? 0);
        if ($now - $w > $windowSeconds * 2) {
            unset($data[$key]);
        }
    }

    $entry = isset($data[$ip]) && is_array($data[$ip]) ? $data[$ip] : null;
    $windowStart = (int) ($entry["w"] ?? $now);
    $count = (int) ($entry["c"] ?? 0);

    if ($entry === null || $now - $windowStart > $windowSeconds) {
        $windowStart = $now;
        $count = 0;
    }

    if ($count >= $maxAttempts) {
        flock($fh, LOCK_UN);
        fclose($fh);
        return false;
    }

    if ($consume) {
        $count++;
        $data[$ip] = ["w" => $windowStart, "c" => $count];

        $payload = json_encode($data, JSON_UNESCAPED_UNICODE);
        if ($payload === false) {
            flock($fh, LOCK_UN);
            fclose($fh);
            return true;
        }

        ftruncate($fh, 0);
        rewind($fh);
        fwrite($fh, $payload);
        fflush($fh);
        @chmod($path, 0660);
    }

    flock($fh, LOCK_UN);
    fclose($fh);

    return true;
}

/**
 * @return bool true if under limit (and counter updated), false if blocked
 */
function rateLimitTryConsume(string $name, int $maxAttempts, int $windowSeconds): bool
{
    return rateLimitEvaluate($name, $maxAttempts, $windowSeconds, true);
}

function rateLimitClearIp(string $name, ?string $ip = null): void
{
    $path = getRateLimitStatePath($name);
    if ($path === null || !is_file($path)) {
        return;
    }

    $ip = $ip ?? getRateLimitClientIp();
    $fh = fopen($path, "c+");
    if ($fh === false) {
        return;
    }
    if (!flock($fh, LOCK_EX)) {
        fclose($fh);
        return;
    }

    rewind($fh);
    $raw = stream_get_contents($fh);
    $data = [];
    if (is_string($raw) && trim($raw) !== "") {
        $decoded = json_decode($raw, true);
        if (is_array($decoded)) {
            $data = $decoded;
        }
    }

    unset($data[$ip]);

    $payload = json_encode($data, JSON_UNESCAPED_UNICODE);
    if ($payload !== false) {
        ftruncate($fh, 0);
        rewind($fh);
        fwrite($fh, $payload);
        fflush($fh);
    }

    flock($fh, LOCK_UN);
    fclose($fh);
}

/** Max failed admin login attempts per IP within the window. */
function getAdminLoginRateLimitMax(): int
{
    return defined("ADMIN_LOGIN_RATE_LIMIT_MAX")
        ? max(1, (int) ADMIN_LOGIN_RATE_LIMIT_MAX)
        : 10;
}

function getAdminLoginRateLimitWindow(): int
{
    return defined("ADMIN_LOGIN_RATE_LIMIT_WINDOW")
        ? max(60, (int) ADMIN_LOGIN_RATE_LIMIT_WINDOW)
        : 900;
}

function isAdminLoginRateLimited(): bool
{
    $path = getRateLimitStatePath("admin_login");
    if ($path === null || !is_file($path)) {
        return false;
    }

    $fh = fopen($path, "r");
    if ($fh === false) {
        return false;
    }
    if (!flock($fh, LOCK_SH)) {
        fclose($fh);
        return false;
    }

    rewind($fh);
    $raw = stream_get_contents($fh);
    flock($fh, LOCK_UN);
    fclose($fh);

    $data = [];
    if (is_string($raw) && trim($raw) !== "") {
        $decoded = json_decode($raw, true);
        if (is_array($decoded)) {
            $data = $decoded;
        }
    }

    $ip = getRateLimitClientIp();
    $now = time();
    $window = getAdminLoginRateLimitWindow();
    $max = getAdminLoginRateLimitMax();

    if (!isset($data[$ip]) || !is_array($data[$ip])) {
        return false;
    }

    $windowStart = (int) ($data[$ip]["w"] ?? 0);
    $count = (int) ($data[$ip]["c"] ?? 0);
    if ($now - $windowStart > $window) {
        return false;
    }

    return $count >= $max;
}

function recordAdminLoginFailure(): void
{
    rateLimitTryConsume(
        "admin_login",
        getAdminLoginRateLimitMax(),
        getAdminLoginRateLimitWindow(),
    );
}

function clearAdminLoginRateLimitForCurrentIp(): void
{
    rateLimitClearIp("admin_login", getRateLimitClientIp());
}

function getCheckoutRateLimitMax(): int
{
    return defined("CHECKOUT_RATE_LIMIT_MAX")
        ? max(1, (int) CHECKOUT_RATE_LIMIT_MAX)
        : 30;
}

function getCheckoutRateLimitWindow(): int
{
    return defined("CHECKOUT_RATE_LIMIT_WINDOW")
        ? max(60, (int) CHECKOUT_RATE_LIMIT_WINDOW)
        : 3600;
}

function checkoutRateLimitWouldAllow(): bool
{
    return rateLimitEvaluate(
        "checkout",
        getCheckoutRateLimitMax(),
        getCheckoutRateLimitWindow(),
        false,
    );
}

function checkoutRateLimitTryConsume(): bool
{
    return rateLimitTryConsume(
        "checkout",
        getCheckoutRateLimitMax(),
        getCheckoutRateLimitWindow(),
    );
}

function getUploadFilename($filename)
{
    $filename = trim((string) $filename);
    if ($filename === "") {
        return "";
    }

    return basename($filename);
}

function getUploadPath($filename)
{
    $filename = getUploadFilename($filename);
    if ($filename === "") {
        return null;
    }

    return rtrim(UPLOADS_DIR, "/\\") . "/" . $filename;
}

function getUploadUrl($filename)
{
    $filename = getUploadFilename($filename);
    if ($filename === "") {
        return null;
    }

    return rtrim(UPLOADS_URL, "/\\") . "/" . rawurlencode($filename);
}

function setFlashMessage($key, $type, $message)
{
    $key = trim((string) $key);
    $type = trim((string) $type);
    $message = trim((string) $message);

    if ($key === "" || $type === "" || $message === "") {
        return;
    }

    $_SESSION["flash_messages"][$key] = [
        "type" => $type,
        "message" => $message,
    ];
}

function consumeFlashMessage($key)
{
    $key = trim((string) $key);
    if ($key === "") {
        return null;
    }

    $messages = $_SESSION["flash_messages"] ?? [];
    if (
        !is_array($messages) ||
        !isset($messages[$key]) ||
        !is_array($messages[$key])
    ) {
        return null;
    }

    $message = $messages[$key];
    unset($_SESSION["flash_messages"][$key]);

    $type = trim((string) ($message["type"] ?? ""));
    $text = trim((string) ($message["message"] ?? ""));
    if ($type === "" || $text === "") {
        return null;
    }

    return [
        "type" => $type,
        "message" => $text,
    ];
}

function normalizeAdminUsername($username)
{
    return trim((string) $username);
}

function normalizeAdminDescription($description)
{
    return trim((string) $description);
}

function normalizeAdminEmail($email)
{
    return strtolower(trim((string) $email));
}

function isValidAdminUsername($username)
{
    $username = normalizeAdminUsername($username);
    return preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]{2,49}$/', $username) === 1;
}

function isValidAdminDescription($description)
{
    $description = normalizeAdminDescription($description);
    if ($description === "") {
        return false;
    }

    $length = function_exists("mb_strlen")
        ? mb_strlen($description)
        : strlen($description);
    return $length <= 120;
}

function isValidAdminEmail($email)
{
    $email = normalizeAdminEmail($email);
    return $email !== "" && filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}

function getDefaultAdminDescription($username)
{
    return "Admin";
}

function getDefaultAdminEmail()
{
    $email = defined("ADMIN_EMAIL") ? normalizeAdminEmail(ADMIN_EMAIL) : "";
    return isValidAdminEmail($email) ? $email : "";
}

function getAdminAccounts()
{
    $data = readJsonFile(ADMINS_FILE);
    $records =
        isset($data["admins"]) && is_array($data["admins"])
            ? $data["admins"]
            : [];
    $accounts = [];

    foreach ($records as $username => $record) {
        $username = normalizeAdminUsername($username);
        if ($username === "") {
            continue;
        }

        if (is_string($record)) {
            $record = [
                "password_hash" => $record,
                "description" => getDefaultAdminDescription($username),
                "email" => getDefaultAdminEmail(),
            ];
        }

        if (!is_array($record)) {
            continue;
        }

        $hash = isset($record["password_hash"])
            ? (string) $record["password_hash"]
            : "";
        if ($hash === "") {
            continue;
        }

        $description = normalizeAdminDescription(
            $record["description"] ?? getDefaultAdminDescription($username),
        );
        if (!isValidAdminDescription($description)) {
            $description = getDefaultAdminDescription($username);
        }

        $email = normalizeAdminEmail(
            $record["email"] ?? getDefaultAdminEmail(),
        );
        if (!isValidAdminEmail($email)) {
            $email = getDefaultAdminEmail();
        }

        $accounts[$username] = [
            "password_hash" => $hash,
            "description" => $description,
            "email" => $email,
        ];
    }

    ksort($accounts);
    return $accounts;
}

function getAdminUsers()
{
    $users = [];
    foreach (getAdminAccounts() as $username => $record) {
        $users[$username] = $record["password_hash"];
    }
    return $users;
}

function saveAdminAccounts($accounts)
{
    $result = [];

    foreach ($accounts as $username => $record) {
        $username = normalizeAdminUsername($username);
        if ($username === "" || !is_array($record)) {
            continue;
        }

        $hash = isset($record["password_hash"])
            ? (string) $record["password_hash"]
            : "";
        if ($hash === "") {
            continue;
        }

        $description = normalizeAdminDescription(
            $record["description"] ?? getDefaultAdminDescription($username),
        );
        if (!isValidAdminDescription($description)) {
            $description = getDefaultAdminDescription($username);
        }

        $email = normalizeAdminEmail(
            $record["email"] ?? getDefaultAdminEmail(),
        );
        if (!isValidAdminEmail($email)) {
            $email = getDefaultAdminEmail();
        }

        $result[$username] = [
            "password_hash" => $hash,
            "description" => $description,
            "email" => $email,
        ];
    }

    ksort($result);
    return writeJsonFile(ADMINS_FILE, ["admins" => $result]);
}

function getDefaultCategories()
{
    return [["id" => "apparel", "label" => "Bekleidung"]];
}

function normalizeCategoryId($id)
{
    $id = trim((string) $id);
    if ($id === "") {
        return "";
    }

    if (function_exists("iconv")) {
        $converted = @iconv("UTF-8", "ASCII//TRANSLIT//IGNORE", $id);
        if (is_string($converted) && $converted !== "") {
            $id = $converted;
        }
    }

    $id = strtolower($id);
    $id = preg_replace("/[^a-z0-9]+/", "-", $id);
    return trim((string) $id, "-");
}

function normalizeCategoryLabel($label)
{
    return trim((string) $label);
}

function isValidCategoryLabel($label)
{
    $label = normalizeCategoryLabel($label);
    if ($label === "") {
        return false;
    }

    $length = function_exists("mb_strlen") ? mb_strlen($label) : strlen($label);
    return $length <= 80;
}

function normalizeCategories($categories)
{
    $normalized = [];
    if (!is_array($categories)) {
        $categories = [];
    }

    foreach ($categories as $category) {
        if (!is_array($category)) {
            continue;
        }

        $id = normalizeCategoryId($category["id"] ?? "");
        $label = normalizeCategoryLabel($category["label"] ?? "");
        if ($id === "" || !isValidCategoryLabel($label)) {
            continue;
        }

        $normalized[$id] = [
            "id" => $id,
            "label" => $label,
        ];
    }

    if (empty($normalized)) {
        foreach (getDefaultCategories() as $category) {
            $normalized[$category["id"]] = $category;
        }
    }

    uasort($normalized, function ($left, $right) {
        return strcasecmp($left["label"], $right["label"]);
    });

    return array_values($normalized);
}

function getCategories()
{
    $data = readJsonFile(CATEGORIES_FILE);
    return normalizeCategories($data["categories"] ?? []);
}

function saveCategories($categories)
{
    return writeJsonFile(CATEGORIES_FILE, [
        "categories" => normalizeCategories($categories),
    ]);
}

function getCategoryById($categoryId)
{
    $categoryId = normalizeCategoryId($categoryId);
    foreach (getCategories() as $category) {
        if ($category["id"] === $categoryId) {
            return $category;
        }
    }
    return null;
}

function getCategoryLabel($categoryId)
{
    $category = getCategoryById($categoryId);
    if ($category !== null) {
        return $category["label"];
    }
    return trim((string) $categoryId);
}

function getCategoryLabels($categoryIds)
{
    $labels = [];
    foreach (normalizeProductCategoryIds($categoryIds) as $categoryId) {
        $labels[] = getCategoryLabel($categoryId);
    }
    return $labels;
}

function getCategoryChipPalette($categoryId)
{
    $categoryId = normalizeCategoryId($categoryId);
    if ($categoryId === "") {
        return [
            "background" => "#ebe8df",
            "border" => "#d0c8b5",
            "text" => "#4b4b4b",
        ];
    }

    $hash = crc32($categoryId);
    if ($hash < 0) {
        $hash = $hash * -1;
    }

    // Spread hues across a distinct red -> blue range.
    $hueSteps = [0, 16, 32, 48, 66, 84, 104, 128, 152, 176, 200, 224];
    $hue = $hueSteps[$hash % count($hueSteps)];
    $saturation = 44 + (($hash >> 8) % 10);
    $lightness = 84 + (($hash >> 16) % 7);

    $background = "hsl(" . $hue . ", " . $saturation . "%, " . $lightness . "%)";
    $border = "hsl(" . $hue . ", " . ($saturation + 8) . "%, " . ($lightness - 16) . "%)";
    $text = "hsl(" . $hue . ", " . ($saturation + 18) . "%, 24%)";

    return [
        "background" => $background,
        "border" => $border,
        "text" => $text,
    ];
}

function generateCategoryIdFromLabel($label, $existingCategories = [])
{
    $baseId = normalizeCategoryId($label);
    if ($baseId === "") {
        $baseId = "category";
    }

    $used = [];
    foreach (normalizeCategories($existingCategories) as $category) {
        $used[$category["id"]] = true;
    }

    $candidate = $baseId;
    $counter = 2;
    while (isset($used[$candidate])) {
        $candidate = $baseId . "-" . $counter;
        $counter++;
    }

    return $candidate;
}

function isCategoryInUse($categoryId)
{
    foreach (getProducts() as $product) {
        if (productHasCategory($product, $categoryId)) {
            return true;
        }
    }
    return false;
}

function normalizeProductCategoryIds($categoryValue)
{
    if (is_array($categoryValue)) {
        $rawIds = $categoryValue;
    } elseif ($categoryValue === null || $categoryValue === "") {
        $rawIds = [];
    } else {
        $rawIds = [$categoryValue];
    }

    $normalized = [];
    foreach ($rawIds as $categoryId) {
        $categoryId = normalizeCategoryId($categoryId);
        if ($categoryId !== "") {
            $normalized[$categoryId] = $categoryId;
        }
    }

    return array_values($normalized);
}

function getProductCategoryIds($product)
{
    if (isset($product["categories"])) {
        return normalizeProductCategoryIds($product["categories"]);
    }
    return normalizeProductCategoryIds($product["category"] ?? []);
}

function productHasCategory($product, $categoryId)
{
    $categoryId = normalizeCategoryId($categoryId);
    if ($categoryId === "") {
        return false;
    }
    return in_array($categoryId, getProductCategoryIds($product), true);
}

function getProductSizes($product)
{
    if (isset($product["sizes"]) && is_array($product["sizes"])) {
        $sizes = $product["sizes"];
    } elseif (isset($product["sizes"]) && is_string($product["sizes"])) {
        $sizes = explode(",", $product["sizes"]);
    } else {
        $sizes = [];
    }

    $normalized = [];
    foreach ($sizes as $size) {
        $size = trim((string) $size);
        if ($size !== "") {
            $normalized[$size] = $size;
        }
    }

    return array_values($normalized);
}

function productUsesSizeStock($product)
{
    return !empty(getProductSizes($product));
}

function normalizeAvailabilityLabels($sizes, $labels)
{
    $result = [];
    if (!is_array($labels)) {
        $labels = [];
    }

    foreach ($sizes as $size) {
        $text = trim((string) ($labels[$size] ?? ""));
        $result[$size] = $text;
    }

    return $result;
}

function getAvailabilityLabel($product, $size)
{
    $labels =
        isset($product["availability_labels"]) &&
        is_array($product["availability_labels"])
            ? $product["availability_labels"]
            : [];
    return trim((string) ($labels[$size] ?? ""));
}

function normalizeProductRecord($product, $defaultCategoryId = "")
{
    if (!is_array($product)) {
        return null;
    }

    $productId = isset($product["id"]) ? (int) $product["id"] : 0;
    $name = trim((string) ($product["name"] ?? ""));
    if ($productId <= 0 || $name === "") {
        return null;
    }

    $sizes = getProductSizes($product);
    if (empty($sizes)) {
        $sizes = ["Standard"];
    }

    $categories = getProductCategoryIds($product);
    if (empty($categories) && $defaultCategoryId !== "") {
        $categories = [$defaultCategoryId];
    }

    $availabilityLabels = normalizeAvailabilityLabels(
        $sizes,
        isset($product["availability_labels"]) &&
        is_array($product["availability_labels"])
            ? $product["availability_labels"]
            : [],
    );

    return [
        "id" => $productId,
        "name" => $name,
        "description" => trim((string) ($product["description"] ?? "")),
        "image" => trim((string) ($product["image"] ?? "")),
        "categories" => $categories,
        "sizes" => implode(",", $sizes),
        "availability_labels" => $availabilityLabels,
    ];
}

function getProducts()
{
    $data = readJsonFile(PRODUCTS_FILE);
    $rawProducts =
        isset($data["products"]) && is_array($data["products"])
            ? $data["products"]
            : [];
    $categories = getCategories();
    $defaultCategoryId = !empty($categories) ? $categories[0]["id"] : "apparel";
    $products = [];

    foreach ($rawProducts as $product) {
        $normalized = normalizeProductRecord($product, $defaultCategoryId);
        if ($normalized !== null) {
            $products[] = $normalized;
        }
    }

    usort($products, function ($left, $right) {
        return strcasecmp($left["name"], $right["name"]);
    });

    return $products;
}

function getProductById($id)
{
    $id = (int) $id;
    foreach (getProducts() as $product) {
        if ((int) $product["id"] === $id) {
            return $product;
        }
    }
    return null;
}

function saveProducts($products)
{
    $categories = getCategories();
    $defaultCategoryId = !empty($categories) ? $categories[0]["id"] : "apparel";
    $normalized = [];

    foreach ($products as $product) {
        $record = normalizeProductRecord($product, $defaultCategoryId);
        if ($record !== null) {
            $normalized[] = $record;
        }
    }

    return writeJsonFile(PRODUCTS_FILE, [
        "products" => array_values($normalized),
    ]);
}

function getFaqFilePath(): string
{
    $dataDir = defined("DATA_DIR") ? DATA_DIR : dirname(__DIR__) . "/data/";
    $defaultPath = rtrim($dataDir, "/\\") . "/faq.json";

    if (!defined("FAQ_FILE") || !is_string(FAQ_FILE) || FAQ_FILE === "") {
        return $defaultPath;
    }

    $configuredPath = FAQ_FILE;
    $normalizedDataDir = str_replace("\\", "/", rtrim($dataDir, "/\\")) . "/";
    $normalizedConfigured = str_replace("\\", "/", $configuredPath);

    if (strpos($normalizedConfigured, $normalizedDataDir) !== 0) {
        return $defaultPath;
    }

    return $configuredPath;
}

function getFaqContent(): string
{
    $defaultContent =
        "# FAQ\n\nHier kann der FAQ-Inhalt im Admin-Bereich bearbeitet werden.";
    $data = readJsonFile(getFaqFilePath());

    if (!isset($data["content"]) || !is_string($data["content"])) {
        return $defaultContent;
    }

    return $data["content"];
}

function saveFaqContent(string $markdown): bool
{
    return writeJsonFile(getFaqFilePath(), ["content" => (string) $markdown]);
}

function renderFaqInlineMarkdown(string $text): string
{
    $escaped = escape($text);
    $escaped = preg_replace_callback(
        '/\[([^\]]+)\]\(([^)\s]+)\)/',
        function ($matches) {
            $label = $matches[1];
            $url = trim(html_entity_decode($matches[2], ENT_QUOTES, "UTF-8"));
            if ($url === "") {
                return $matches[0];
            }

            $scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
            if (!in_array($scheme, ["http", "https", "mailto"], true)) {
                return $matches[0];
            }

            return '<a href="' . escape($url) . '">' . $label . "</a>";
        },
        $escaped,
    );
    $escaped = preg_replace(
        "/\*\*(.+?)\*\*/s",
        '<strong>$1</strong>',
        $escaped,
    );
    $escaped = preg_replace(
        "/(?<!\*)\*(?!\s)(.+?)(?<!\s)\*(?!\*)/s",
        '<em>$1</em>',
        $escaped,
    );
    return $escaped;
}

function renderFaqMarkdown(string $markdown): string
{
    $normalized = str_replace(["\r\n", "\r"], "\n", $markdown);
    $lines = explode("\n", $normalized);
    $htmlParts = [];
    $paragraphLines = [];
    $listType = "";

    $flushParagraph = function () use (&$paragraphLines, &$htmlParts): void {
        if (empty($paragraphLines)) {
            return;
        }

        $rendered = [];
        foreach ($paragraphLines as $line) {
            $rendered[] = renderFaqInlineMarkdown($line);
        }

        $htmlParts[] = "<p>" . implode("<br>\n", $rendered) . "</p>";
        $paragraphLines = [];
    };

    $closeList = function () use (&$listType, &$htmlParts): void {
        if ($listType !== "") {
            $htmlParts[] = "</" . $listType . ">";
            $listType = "";
        }
    };

    foreach ($lines as $line) {
        $line = rtrim($line);
        $trimmed = trim($line);

        if ($trimmed === "") {
            $flushParagraph();
            $closeList();
            continue;
        }

        if (preg_match('/^(#{1,3})\s+(.+)$/', $trimmed, $matches) === 1) {
            $flushParagraph();
            $closeList();
            $level = strlen($matches[1]);
            $htmlParts[] =
                "<h" .
                $level .
                ">" .
                renderFaqInlineMarkdown($matches[2]) .
                "</h" .
                $level .
                ">";
            continue;
        }

        if (preg_match('/^\s*[-*]\s+(.+)$/', $line, $matches) === 1) {
            $flushParagraph();
            if ($listType !== "ul") {
                $closeList();
                $listType = "ul";
                $htmlParts[] = "<ul>";
            }
            $htmlParts[] =
                "<li>" . renderFaqInlineMarkdown($matches[1]) . "</li>";
            continue;
        }

        if (preg_match('/^\s*\d+\.\s+(.+)$/', $line, $matches) === 1) {
            $flushParagraph();
            if ($listType !== "ol") {
                $closeList();
                $listType = "ol";
                $htmlParts[] = "<ol>";
            }
            $htmlParts[] =
                "<li>" . renderFaqInlineMarkdown($matches[1]) . "</li>";
            continue;
        }

        $closeList();
        $paragraphLines[] = $trimmed;
    }

    $flushParagraph();
    $closeList();

    return empty($htmlParts)
        ? "<p>Keine FAQ-Inhalte vorhanden.</p>"
        : implode("\n", $htmlParts);
}

function getDefaultOrganizations()
{
    return [
        [
            "id" => "feuerwehr-freising",
            "label" => "Amt 32 - Feuerwehr Freising",
            "sort_order" => 10,
            "active" => true,
        ],
    ];
}

function normalizeOrganizationId($id)
{
    return normalizeCategoryId($id);
}

function normalizeOrganizationLabel($label)
{
    return trim((string) $label);
}

function isValidOrganizationLabel($label)
{
    $label = normalizeOrganizationLabel($label);
    if ($label === "") {
        return false;
    }

    $length = function_exists("mb_strlen") ? mb_strlen($label) : strlen($label);
    return $length <= 120;
}

function normalizeOrganizations($organizations)
{
    $normalized = [];
    if (!is_array($organizations)) {
        $organizations = [];
    }

    foreach ($organizations as $organization) {
        if (!is_array($organization)) {
            continue;
        }

        $id = normalizeOrganizationId($organization["id"] ?? "");
        $label = normalizeOrganizationLabel($organization["label"] ?? "");
        if ($id === "" || !isValidOrganizationLabel($label)) {
            continue;
        }

        $sortOrder = isset($organization["sort_order"])
            ? (int) $organization["sort_order"]
            : 0;
        $active =
            !isset($organization["active"]) || (bool) $organization["active"];

        $normalized[$id] = [
            "id" => $id,
            "label" => $label,
            "sort_order" => $sortOrder,
            "active" => $active,
        ];
    }

    if (empty($normalized)) {
        foreach (getDefaultOrganizations() as $organization) {
            $normalized[$organization["id"]] = $organization;
        }
    }

    uasort($normalized, function ($left, $right) {
        if ($left["sort_order"] === $right["sort_order"]) {
            return strcasecmp($left["label"], $right["label"]);
        }
        return $left["sort_order"] <=> $right["sort_order"];
    });

    return array_values($normalized);
}

function getOrganizations($onlyActive = false)
{
    $data = readJsonFile(ORGANIZATIONS_FILE);
    $organizations = normalizeOrganizations($data["organizations"] ?? []);

    if ($onlyActive) {
        $organizations = array_values(
            array_filter($organizations, function ($organization) {
                return !empty($organization["active"]);
            }),
        );
    }

    return $organizations;
}

function saveOrganizations($organizations)
{
    return writeJsonFile(ORGANIZATIONS_FILE, [
        "organizations" => normalizeOrganizations($organizations),
    ]);
}

function getOrganizationById($organizationId)
{
    $organizationId = normalizeOrganizationId($organizationId);
    foreach (getOrganizations(false) as $organization) {
        if ($organization["id"] === $organizationId) {
            return $organization;
        }
    }
    return null;
}

function generateOrganizationIdFromLabel($label, $existingOrganizations = [])
{
    $baseId = normalizeOrganizationId($label);
    if ($baseId === "") {
        $baseId = "organization";
    }

    $used = [];
    foreach (normalizeOrganizations($existingOrganizations) as $organization) {
        $used[$organization["id"]] = true;
    }

    $candidate = $baseId;
    $counter = 2;
    while (isset($used[$candidate])) {
        $candidate = $baseId . "-" . $counter;
        $counter++;
    }

    return $candidate;
}

function getDefaultSystemSettings()
{
    $startpageIntroText = "";
    if (defined("DISCLAIMER_LINES") && is_array(DISCLAIMER_LINES)) {
        $lines = array_filter(
            array_map(function ($line) {
                return trim((string) $line);
            }, DISCLAIMER_LINES),
            function ($line) {
                return $line !== "";
            },
        );
        $startpageIntroText = implode("\n", $lines);
    }

    return [
        "order_recipient_email" => defined("ORDER_RECIPIENT_EMAIL")
            ? ORDER_RECIPIENT_EMAIL
            : getDefaultAdminEmail(),
        "attach_order_pdf_to_admin_email" => defined(
            "ATTACH_ORDER_PDF_TO_ADMIN_EMAIL",
        )
            ? (bool) ATTACH_ORDER_PDF_TO_ADMIN_EMAIL
            : true,
        "startpage_intro_text" => $startpageIntroText,
    ];
}

function normalizeSystemSettings($settings)
{
    $defaults = getDefaultSystemSettings();
    if (!is_array($settings)) {
        $settings = [];
    }

    $recipientEmail = normalizeAdminEmail(
        $settings["order_recipient_email"] ??
            $defaults["order_recipient_email"],
    );
    if (!isValidAdminEmail($recipientEmail)) {
        $recipientEmail = $defaults["order_recipient_email"];
    }

    $startpageIntroText = trim(
        (string) ($settings["startpage_intro_text"] ?? $defaults["startpage_intro_text"]),
    );

    return [
        "order_recipient_email" => $recipientEmail,
        "attach_order_pdf_to_admin_email" => !empty(
            $settings["attach_order_pdf_to_admin_email"]
        ),
        "startpage_intro_text" => $startpageIntroText,
    ];
}

function getSystemSettings()
{
    $data = readJsonFile(SETTINGS_FILE);
    return normalizeSystemSettings($data["settings"] ?? []);
}

function saveSystemSettings($settings)
{
    return writeJsonFile(SETTINGS_FILE, [
        "settings" => normalizeSystemSettings($settings),
    ]);
}

function getOrderRecipientEmail()
{
    $settings = getSystemSettings();
    return $settings["order_recipient_email"];
}

function shouldAttachOrderPdfToAdminEmail()
{
    $settings = getSystemSettings();
    return !empty($settings["attach_order_pdf_to_admin_email"]);
}

function getStartpageIntroLines()
{
    $settings = getSystemSettings();
    $text = trim((string) ($settings["startpage_intro_text"] ?? ""));
    if ($text === "") {
        return [];
    }

    $lines = preg_split('/\R+/', $text) ?: [];
    $lines = array_values(
        array_filter(
            array_map(function ($line) {
                return trim((string) $line);
            }, $lines),
            function ($line) {
                return $line !== "";
            },
        ),
    );

    return $lines;
}

function backorderStatusEmpty(): string
{
    return "";
}

function backorderStatusToBeBackordered(): string
{
    return "to_be_backordered";
}

function backorderStatusOrdered(): string
{
    return "ordered";
}

/** Statuses persisted on order line items (load/save). */
function getBackorderStatusesForStorage(): array
{
    return [
        backorderStatusEmpty(),
        backorderStatusToBeBackordered(),
        backorderStatusOrdered(),
    ];
}

/** Statuses settable from order detail (mark for external reorder). */
function getBackorderStatusesForAdminMarking(): array
{
    return [backorderStatusEmpty(), backorderStatusToBeBackordered()];
}

function isValidBackorderStatusForStorage(string $status): bool
{
    return in_array($status, getBackorderStatusesForStorage(), true);
}

function isValidBackorderStatusForAdminMarking(string $status): bool
{
    return in_array($status, getBackorderStatusesForAdminMarking(), true);
}

function normalizeOrderItem($item)
{
    if (!is_array($item)) {
        return null;
    }

    $productId = isset($item["product_id"]) ? (int) $item["product_id"] : 0;
    if ($productId <= 0) {
        return null;
    }

    $product = getProductById($productId);
    if ($product === null) {
        return null;
    }

    $size = trim((string) ($item["size"] ?? ""));
    $sizes = getProductSizes($product);
    if (!empty($sizes)) {
        if ($size === "" || !in_array($size, $sizes, true)) {
            return null;
        }
    } else {
        $size = "";
    }

    $backorderStatus = trim((string) ($item["backorder_status"] ?? ""));
    if (!isValidBackorderStatusForStorage($backorderStatus)) {
        $backorderStatus = backorderStatusEmpty();
    }

    return [
        "product_id" => $productId,
        "product_name" => $product["name"],
        "size" => $size,
        "availability_label" =>
            $size !== "" ? getAvailabilityLabel($product, $size) : "",
        "is_processed" => !empty($item["is_processed"]),
        "backorder_status" => $backorderStatus,
        "backordered_at" => trim((string) ($item["backordered_at"] ?? "")),
        "ordered_at" => trim((string) ($item["ordered_at"] ?? "")),
    ];
}

function normalizeOrderItems($items)
{
    $normalized = [];
    $seen = [];

    if (!is_array($items)) {
        return [];
    }

    foreach ($items as $item) {
        $record = normalizeOrderItem($item);
        if ($record === null) {
            continue;
        }

        $key = $record["product_id"] . "|" . $record["size"];
        if (isset($seen[$key])) {
            continue;
        }

        $seen[$key] = true;
        $normalized[] = $record;
    }

    return array_values($normalized);
}

function getOrders()
{
    $data = readJsonFile(ORDERS_FILE);
    $orders =
        isset($data["orders"]) && is_array($data["orders"])
            ? $data["orders"]
            : [];
    $normalized = [];

    foreach ($orders as $order) {
        $record = normalizeOrderRecord($order);
        if ($record !== null) {
            $normalized[] = $record;
        }
    }

    return $normalized;
}

function saveOrders($orders)
{
    $normalized = [];
    foreach ($orders as $order) {
        $record = normalizeOrderRecord($order);
        if ($record !== null) {
            $normalized[] = $record;
        }
    }

    $result = writeJsonFile(ORDERS_FILE, [
        "orders" => array_values($normalized),
    ]);

    if (!$result) {
        logError("Failed to save orders", [
            "file" => ORDERS_FILE,
            "order_count" => count($normalized),
        ]);
    }
    return (bool) $result;
}

function generateOrderId()
{
    $orders = getOrders();
    $year = date("Y");
    $prefix = defined("ORDER_PREFIX") ? ORDER_PREFIX : "ORD";
    $max = 0;
    $pattern = "/^" . preg_quote($prefix, "/") . '-\d{4}-(\d+)$/';

    foreach ($orders as $order) {
        if (preg_match($pattern, (string) $order["id"], $matches) === 1) {
            $number = (int) $matches[1];
            if ($number > $max) {
                $max = $number;
            }
        }
    }

    return sprintf("%s-%s-%03d", $prefix, $year, $max + 1);
}

function normalizeOrderRecord($order)
{
    if (!is_array($order)) {
        return null;
    }

    $id = trim((string) ($order["id"] ?? ""));
    $customerName = trim((string) ($order["customer_name"] ?? ""));
    $customerEmail = normalizeAdminEmail($order["customer_email"] ?? "");
    $organizationId = normalizeOrganizationId($order["organization_id"] ?? "");
    $organizationLabel = trim((string) ($order["organization_label"] ?? ""));
    $items = normalizeOrderItems($order["items"] ?? []);

    if (
        $id === "" ||
        $customerName === "" ||
        !isValidAdminEmail($customerEmail) ||
        $organizationId === "" ||
        $organizationLabel === "" ||
        empty($items)
    ) {
        return null;
    }

    $createdAt = trim((string) ($order["created_at"] ?? ""));
    if ($createdAt === "") {
        $createdAt = date("Y-m-d H:i:s");
    }

    $status = trim((string) ($order["status"] ?? "open"));
    $allowedStatuses = ["open", "partial", "processed", "cancelled"];
    if (!in_array($status, $allowedStatuses, true)) {
        $status = "open";
    }

    $normalized = [
        "id" => $id,
        "customer_name" => $customerName,
        "customer_email" => $customerEmail,
        "organization_id" => $organizationId,
        "organization_label" => $organizationLabel,
        "comment" => trim((string) ($order["comment"] ?? "")),
        "items" => $items,
        "status" => $status,
        "created_at" => $createdAt,
        "updated_at" => trim((string) ($order["updated_at"] ?? $createdAt)),
        "cancelled_at" => trim((string) ($order["cancelled_at"] ?? "")),
        "cancelled_by" => trim((string) ($order["cancelled_by"] ?? "")),
        "cancellation_reason" => trim(
            (string) ($order["cancellation_reason"] ?? ""),
        ),
        "admin_notified_at" => trim(
            (string) ($order["admin_notified_at"] ?? ""),
        ),
    ];

    return refreshOrderState($normalized);
}

function refreshOrderState($order)
{
    if (!is_array($order)) {
        return null;
    }

    if (($order["status"] ?? "") === "cancelled") {
        return $order;
    }

    $processedCount = 0;
    foreach ($order["items"] as $item) {
        if (!empty($item["is_processed"])) {
            $processedCount++;
        }
    }

    if ($processedCount <= 0) {
        $order["status"] = "open";
    } elseif ($processedCount >= count($order["items"])) {
        $order["status"] = "processed";
    } else {
        $order["status"] = "partial";
    }

    return $order;
}

function getOrderById($orderId)
{
    $orderId = trim((string) $orderId);
    if ($orderId === "") {
        return null;
    }

    foreach (getOrders() as $order) {
        if ($order["id"] === $orderId) {
            return $order;
        }
    }

    return null;
}

function buildAbsoluteUrl($path)
{
    $path = "/" . ltrim((string) $path, "/");
    $siteUrl = defined("SITE_URL") ? trim((string) SITE_URL) : "";

    if (strpos($siteUrl, "://") !== false) {
        return rtrim($siteUrl, "/") . $path;
    }

    $basePath = trim($siteUrl);
    if ($basePath !== "" && $basePath !== "/") {
        $path = "/" . trim($basePath, "/") . $path;
    }

    $canonical = defined("SITE_CANONICAL_ORIGIN")
        ? trim((string) SITE_CANONICAL_ORIGIN)
        : "";
    if (
        $canonical !== "" &&
        preg_match("#\Ahttps?://[^\s/]+#i", $canonical)
    ) {
        return rtrim($canonical, "/") . $path;
    }

    $scheme = isHttpsRequest() ? "https" : "http";
    $host = $_SERVER["HTTP_HOST"] ?? "";
    if ($host === "") {
        return $path;
    }

    return $scheme . "://" . $host . $path;
}

function isHttpsRequest(): bool
{
    if (
        !empty($_SERVER["HTTPS"]) &&
        strtolower((string) $_SERVER["HTTPS"]) !== "off"
    ) {
        return true;
    }
    if (
        !empty($_SERVER["HTTP_X_FORWARDED_PROTO"]) &&
        strtolower((string) $_SERVER["HTTP_X_FORWARDED_PROTO"]) === "https"
    ) {
        return true;
    }
    if (
        !empty($_SERVER["SERVER_PORT"]) &&
        (int) $_SERVER["SERVER_PORT"] === 443
    ) {
        return true;
    }
    return false;
}

function createOrder(
    $customerName,
    $customerEmail,
    $organizationId,
    $comment,
    $items,
) {
    $customerName = sanitize($customerName);
    $customerEmail = normalizeAdminEmail($customerEmail);
    $organizationId = normalizeOrganizationId($organizationId);
    $comment = trim((string) $comment);
    $items = normalizeOrderItems($items);

    if ($customerName === "") {
        return [
            "success" => false,
            "message" => "Bitte geben Sie einen Namen ein.",
        ];
    }
    if (!isValidAdminEmail($customerEmail)) {
        return [
            "success" => false,
            "message" => "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
        ];
    }
    if (empty($items)) {
        return [
            "success" => false,
            "message" =>
                "Der Warenkorb ist leer oder enthält ungültige Positionen.",
        ];
    }

    $organization = getOrganizationById($organizationId);
    if ($organization === null || empty($organization["active"])) {
        return [
            "success" => false,
            "message" => "Bitte wählen Sie eine gültige Organisation aus.",
        ];
    }

    $now = date("Y-m-d H:i:s");

    $order = [
        "id" => generateOrderId(),
        "customer_name" => $customerName,
        "customer_email" => $customerEmail,
        "organization_id" => $organization["id"],
        "organization_label" => $organization["label"],
        "comment" => $comment,
        "items" => $items,
        "status" => "open",
        "created_at" => $now,
        "updated_at" => $now,
        "cancelled_at" => "",
        "cancelled_by" => "",
        "cancellation_reason" => "",
        "admin_notified_at" => "",
    ];

    $orders = getOrders();
    $orders[] = $order;
    if (!saveOrders($orders)) {
        return [
            "success" => false,
            "message" =>
                "Die Bestellung konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.",
        ];
    }

    logAccess("Order created in createOrder", [
        "order_id" => $order["id"],
        "customer" => $customerEmail,
        "organization" => $organization["label"],
    ]);

    $result = sendConfirmedOrderAdminNotification($order);
    if ($result) {
        markOrderAdminNotified($order["id"]);
        $order = getOrderById($order["id"]);
    }
    sendOrderCreatedCustomerEmail($order);

    return ["success" => true, "order" => $order];
}

function markOrderAdminNotified($orderId)
{
    $orders = getOrders();
    foreach ($orders as &$order) {
        if ($order["id"] !== $orderId) {
            continue;
        }

        $order["admin_notified_at"] = date("Y-m-d H:i:s");
        $order["updated_at"] = date("Y-m-d H:i:s");
        break;
    }
    unset($order);

    saveOrders($orders);
}

function toggleOrderItemProcessed($orderId, $itemIndex)
{
    $orders = getOrders();
    $now = date("Y-m-d H:i:s");

    foreach ($orders as &$order) {
        if ($order["id"] !== $orderId) {
            continue;
        }

        if ($order["status"] === "cancelled") {
            return [
                "success" => false,
                "message" =>
                    "Stornierte Bestellungen können nicht mehr bearbeitet werden.",
            ];
        }
        if (!isset($order["items"][$itemIndex])) {
            return [
                "success" => false,
                "message" => "Position nicht gefunden.",
            ];
        }

        $order["items"][$itemIndex]["is_processed"] = empty(
            $order["items"][$itemIndex]["is_processed"]
        );
        $order["updated_at"] = $now;
        $order = refreshOrderState($order);
        saveOrders($orders);

        return ["success" => true, "order" => $order];
    }
    unset($order);

    return ["success" => false, "message" => "Bestellung nicht gefunden."];
}

function cancelOrder($orderId, $adminUsername, $reason = "")
{
    $orders = getOrders();
    $now = date("Y-m-d H:i:s");
    $adminUsername = normalizeAdminUsername($adminUsername);
    $reason = trim((string) $reason);

    foreach ($orders as &$order) {
        if ($order["id"] !== $orderId) {
            continue;
        }

        if ($order["status"] === "cancelled") {
            return [
                "success" => false,
                "message" => "Die Bestellung ist bereits storniert.",
            ];
        }

        $order["status"] = "cancelled";
        $order["cancelled_at"] = $now;
        $order["cancelled_by"] = $adminUsername;
        $order["cancellation_reason"] = $reason;
        $order["updated_at"] = $now;
        foreach ($order["items"] as &$item) {
            $item["backorder_status"] = "";
            $item["backordered_at"] = "";
            $item["ordered_at"] = "";
        }
        unset($item);
        saveOrders($orders);

        return ["success" => true, "order" => $order];
    }
    unset($order);

    return ["success" => false, "message" => "Bestellung nicht gefunden."];
}

function uncancelOrder($orderId)
{
    $orders = getOrders();
    $now = date("Y-m-d H:i:s");

    foreach ($orders as &$order) {
        if ($order["id"] !== $orderId) {
            continue;
        }

        if ($order["status"] !== "cancelled") {
            return [
                "success" => false,
                "message" => "Die Bestellung ist nicht storniert.",
            ];
        }

        $order["cancelled_at"] = "";
        $order["cancelled_by"] = "";
        $order["cancellation_reason"] = "";
        $order["status"] = "open";
        $order["updated_at"] = $now;
        $order = refreshOrderState($order);
        saveOrders($orders);

        return ["success" => true, "order" => $order];
    }
    unset($order);

    return ["success" => false, "message" => "Bestellung nicht gefunden."];
}

function orderItemCanBeManaged($order)
{
    if (($order["status"] ?? "") === "cancelled") {
        return [
            "success" => false,
            "message" =>
                "Stornierte Bestellungen können nicht mehr bearbeitet werden.",
        ];
    }

    return ["success" => true];
}

function setOrderItemBackorderStatus($orderId, $itemIndex, $status)
{
    if (!isValidBackorderStatusForAdminMarking($status)) {
        return ["success" => false, "message" => "Ungültiger Nachbestellstatus."];
    }

    $orders = getOrders();
    $now = date("Y-m-d H:i:s");

    foreach ($orders as &$order) {
        if ($order["id"] !== $orderId) {
            continue;
        }

        $guard = orderItemCanBeManaged($order);
        if (!$guard["success"]) {
            return $guard;
        }

        if (!isset($order["items"][$itemIndex])) {
            return [
                "success" => false,
                "message" => "Position nicht gefunden.",
            ];
        }

        if ($status === backorderStatusToBeBackordered()) {
            if (!empty($order["items"][$itemIndex]["is_processed"])) {
                return [
                    "success" => false,
                    "message" =>
                        "Bearbeitete Positionen können nicht als Nachbestellung markiert werden.",
                ];
            }
            $order["items"][$itemIndex]["backorder_status"] =
                backorderStatusToBeBackordered();
            $order["items"][$itemIndex]["backordered_at"] = $now;
            $order["items"][$itemIndex]["ordered_at"] = "";
        } else {
            $order["items"][$itemIndex]["backorder_status"] = backorderStatusEmpty();
            $order["items"][$itemIndex]["backordered_at"] = "";
            $order["items"][$itemIndex]["ordered_at"] = "";
        }

        $order["updated_at"] = $now;
        saveOrders($orders);

        return ["success" => true, "order" => $order];
    }
    unset($order);

    return ["success" => false, "message" => "Bestellung nicht gefunden."];
}

function toggleOrderItemBackorder($orderId, $itemIndex)
{
    $orders = getOrders();

    foreach ($orders as $order) {
        if ($order["id"] !== $orderId) {
            continue;
        }

        if (!isset($order["items"][$itemIndex])) {
            return [
                "success" => false,
                "message" => "Position nicht gefunden.",
            ];
        }

        $current = (string) ($order["items"][$itemIndex]["backorder_status"] ?? "");
        $newStatus = $current === "to_be_backordered" ? "" : "to_be_backordered";

        return setOrderItemBackorderStatus($orderId, $itemIndex, $newStatus);
    }

    return ["success" => false, "message" => "Bestellung nicht gefunden."];
}

function getManualBackorders()
{
    $data = readJsonFile(MANUAL_BACKORDERS_FILE);
    $entries =
        isset($data["entries"]) && is_array($data["entries"])
            ? $data["entries"]
            : [];
    $normalized = [];

    foreach ($entries as $entry) {
        if (!is_array($entry)) {
            continue;
        }

        $id = trim((string) ($entry["id"] ?? ""));
        $productId = (int) ($entry["product_id"] ?? 0);
        $productName = trim((string) ($entry["product_name"] ?? ""));
        $size = trim((string) ($entry["size"] ?? ""));
        $status = trim((string) ($entry["backorder_status"] ?? ""));
        if ($id === "" || $productId <= 0 || $productName === "" || $status === "") {
            continue;
        }

        if (!in_array($status, ["to_be_backordered", "ordered"], true)) {
            continue;
        }

        $normalized[] = [
            "id" => $id,
            "product_id" => $productId,
            "product_name" => $productName,
            "size" => $size,
            "backorder_status" => $status,
            "backordered_at" => trim((string) ($entry["backordered_at"] ?? "")),
            "ordered_at" => trim((string) ($entry["ordered_at"] ?? "")),
            "created_at" => trim((string) ($entry["created_at"] ?? "")),
        ];
    }

    return $normalized;
}

function saveManualBackorders($entries)
{
    return writeJsonFile(MANUAL_BACKORDERS_FILE, [
        "entries" => array_values($entries),
    ]);
}

function generateManualBackorderId()
{
    return "mb-" . date("YmdHis") . "-" . bin2hex(random_bytes(4));
}

function addManualBackorderItems($productId, $size, $quantity)
{
    $productId = (int) $productId;
    $size = trim((string) $size);
    $quantity = (int) $quantity;

    if ($productId <= 0 || $quantity <= 0) {
        return [
            "success" => false,
            "message" => "Ungültige Menge oder Artikel.",
        ];
    }

    if ($quantity > 100) {
        return [
            "success" => false,
            "message" => "Maximal 100 Positionen auf einmal.",
        ];
    }

    $product = getProductById($productId);
    if ($product === null) {
        return ["success" => false, "message" => "Artikel nicht gefunden."];
    }

    $allowedSizes = getProductSizes($product);
    if (empty($allowedSizes)) {
        $allowedSizes = ["Standard"];
    }

    if (!in_array($size, $allowedSizes, true)) {
        return ["success" => false, "message" => "Ungültige Größe für diesen Artikel."];
    }

    $entries = getManualBackorders();
    $now = date("Y-m-d H:i:s");

    for ($i = 0; $i < $quantity; $i++) {
        $entries[] = [
            "id" => generateManualBackorderId(),
            "product_id" => $productId,
            "product_name" => $product["name"],
            "size" => $size,
            "backorder_status" => "to_be_backordered",
            "backordered_at" => $now,
            "ordered_at" => "",
            "created_at" => $now,
        ];
    }

    if (!saveManualBackorders($entries)) {
        return [
            "success" => false,
            "message" => "Nachbestellungen konnten nicht gespeichert werden.",
        ];
    }

    return ["success" => true, "added" => $quantity];
}

function collectBackorderItemRefs()
{
    $refs = [];

    foreach (getManualBackorders() as $entry) {
        $refs[] = [
            "order_id" => "",
            "item_index" => 0,
            "manual_id" => $entry["id"],
            "is_manual" => true,
            "product_id" => $entry["product_id"],
            "product_name" => $entry["product_name"],
            "size" => $entry["size"],
            "backorder_status" => $entry["backorder_status"],
            "created_at" => $entry["created_at"] !== "" ? $entry["created_at"] : $entry["backordered_at"],
            "sort_at" =>
                $entry["backorder_status"] === "ordered"
                    ? ($entry["ordered_at"] !== ""
                        ? $entry["ordered_at"]
                        : $entry["created_at"])
                    : ($entry["backordered_at"] !== ""
                        ? $entry["backordered_at"]
                        : $entry["created_at"]),
        ];
    }

    foreach (getOrders() as $order) {
        if (($order["status"] ?? "") === "cancelled") {
            continue;
        }

        foreach ($order["items"] as $itemIndex => $item) {
            $status = (string) ($item["backorder_status"] ?? "");
            if ($status === "") {
                continue;
            }

            $refs[] = [
                "order_id" => $order["id"],
                "item_index" => (int) $itemIndex,
                "product_id" => (int) $item["product_id"],
                "product_name" => $item["product_name"],
                "size" => $item["size"],
                "backorder_status" => $status,
                "created_at" => $order["created_at"],
                "sort_at" =>
                    $status === "ordered"
                        ? ($item["ordered_at"] !== ""
                            ? $item["ordered_at"]
                            : $order["created_at"])
                        : ($item["backordered_at"] !== ""
                            ? $item["backordered_at"]
                            : $order["created_at"]),
            ];
        }
    }

    return $refs;
}

function compareBackorderRefs(array $left, array $right): int
{
    $cmp = strcmp((string) ($left["sort_at"] ?? ""), (string) ($right["sort_at"] ?? ""));
    if ($cmp !== 0) {
        return $cmp;
    }
    $cmp = strcmp((string) ($left["order_id"] ?? ""), (string) ($right["order_id"] ?? ""));
    if ($cmp !== 0) {
        return $cmp;
    }
    return strcmp((string) ($left["manual_id"] ?? ""), (string) ($right["manual_id"] ?? ""));
}

function getBackorderGroups()
{
    $refs = collectBackorderItemRefs();
    $groups = [];

    foreach ($refs as $ref) {
        $key = $ref["product_id"] . "|" . $ref["size"];
        if (!isset($groups[$key])) {
            $groups[$key] = [
                "product_id" => $ref["product_id"],
                "product_name" => $ref["product_name"],
                "size" => $ref["size"],
                "to_be_backordered" => 0,
                "ordered" => 0,
                "items_to_be_backordered" => [],
                "items_ordered" => [],
            ];
        }

        if ($ref["backorder_status"] === "to_be_backordered") {
            $groups[$key]["to_be_backordered"]++;
            $groups[$key]["items_to_be_backordered"][] = $ref;
        } elseif ($ref["backorder_status"] === "ordered") {
            $groups[$key]["ordered"]++;
            $groups[$key]["items_ordered"][] = $ref;
        }
    }

    foreach ($groups as &$group) {
        usort($group["items_to_be_backordered"], "compareBackorderRefs");
        usort($group["items_ordered"], "compareBackorderRefs");
    }
    unset($group);

    $result = array_values($groups);
    usort($result, function ($left, $right) {
        $cmp = strcmp($left["product_name"], $right["product_name"]);
        if ($cmp !== 0) {
            return $cmp;
        }
        return strcmp($left["size"], $right["size"]);
    });

    return $result;
}

function applyBackorderBulkUpdate($productId, $size, $fromStatus, $toStatus, $quantity)
{
    $productId = (int) $productId;
    $size = trim((string) $size);
    $quantity = (int) $quantity;

    if ($productId <= 0 || $quantity <= 0) {
        return [
            "success" => false,
            "message" => "Ungültige Menge oder Artikel.",
        ];
    }

    $refs = collectBackorderItemRefs();
    $candidates = [];

    foreach ($refs as $ref) {
        if (
            $ref["product_id"] !== $productId ||
            $ref["size"] !== $size ||
            $ref["backorder_status"] !== $fromStatus
        ) {
            continue;
        }
        $candidates[] = $ref;
    }

    usort($candidates, "compareBackorderRefs");

    if ($quantity > count($candidates)) {
        return [
            "success" => false,
            "message" =>
                "Nur " .
                count($candidates) .
                " Position(en) verfügbar, " .
                $quantity .
                " angefordert.",
        ];
    }

    $targets = array_slice($candidates, 0, $quantity);
    $orders = getOrders();
    $manualEntries = getManualBackorders();
    $manualChanged = false;
    $now = date("Y-m-d H:i:s");
    $updated = 0;

    foreach ($targets as $target) {
        if (!empty($target["is_manual"])) {
            $manualId = (string) ($target["manual_id"] ?? "");
            foreach ($manualEntries as &$entry) {
                if ($entry["id"] !== $manualId) {
                    continue;
                }

                if ($toStatus === "ordered") {
                    $entry["backorder_status"] = "ordered";
                    $entry["ordered_at"] = $now;
                } else {
                    $entry["backorder_status"] = "";
                    $entry["ordered_at"] = "";
                }

                $manualChanged = true;
                $updated++;
                break;
            }
            unset($entry);
            continue;
        }

        foreach ($orders as &$order) {
            if ($order["id"] !== $target["order_id"]) {
                continue;
            }

            $itemIndex = $target["item_index"];
            if (!isset($order["items"][$itemIndex])) {
                continue 2;
            }

            if ($toStatus === backorderStatusOrdered()) {
                $order["items"][$itemIndex]["backorder_status"] = backorderStatusOrdered();
                $order["items"][$itemIndex]["ordered_at"] = $now;
            } else {
                $order["items"][$itemIndex]["backorder_status"] = backorderStatusEmpty();
                $order["items"][$itemIndex]["ordered_at"] = "";
            }

            $order["updated_at"] = $now;
            $updated++;
            break;
        }
        unset($order);
    }

    if ($updated === 0) {
        return [
            "success" => false,
            "message" => "Keine Positionen aktualisiert.",
        ];
    }

    if ($manualChanged) {
        $manualEntries = array_values(
            array_filter($manualEntries, function ($entry) {
                return ($entry["backorder_status"] ?? "") !== "";
            }),
        );
        if (!saveManualBackorders($manualEntries)) {
            return [
                "success" => false,
                "message" => "Manuelle Nachbestellungen konnten nicht gespeichert werden.",
            ];
        }
    }

    saveOrders($orders);

    return ["success" => true, "updated" => $updated];
}

function markBackorderItemsOrdered($productId, $size, $quantity)
{
    return applyBackorderBulkUpdate(
        $productId,
        $size,
        "to_be_backordered",
        "ordered",
        $quantity,
    );
}

function markBackorderItemsDelivered($productId, $size, $quantity)
{
    return applyBackorderBulkUpdate($productId, $size, "ordered", "", $quantity);
}

function orderHasBackorder($order)
{
    if (!is_array($order["items"] ?? null)) {
        return false;
    }

    foreach ($order["items"] as $item) {
        if (($item["backorder_status"] ?? "") !== "") {
            return true;
        }
    }

    return false;
}

function getOrderOpenBackorderSummary($order)
{
    $statuses = [];
    if (!is_array($order["items"] ?? null)) {
        return ["label" => "", "class" => ""];
    }

    foreach ($order["items"] as $item) {
        if (!empty($item["is_processed"])) {
            continue;
        }
        $status = trim((string) ($item["backorder_status"] ?? ""));
        if ($status !== "") {
            $statuses[$status] = true;
        }
    }

    if ($statuses === []) {
        return ["label" => "", "class" => ""];
    }
    if (count($statuses) > 1) {
        return ["label" => "Gemischt", "class" => "status-backorder"];
    }

    $status = array_key_first($statuses);
    return [
        "label" => getBackorderStatusLabel($status),
        "class" => getBackorderStatusClass($status),
    ];
}

function getBackorderStatusLabel($status)
{
    switch ((string) $status) {
        case "to_be_backordered":
            return "Nachzubestellen";
        case "ordered":
            return "Wartet auf Lieferung";
        default:
            return "-";
    }
}

function getBackorderStatusClass($status)
{
    switch ((string) $status) {
        case "to_be_backordered":
            return "status-backorder";
        case "ordered":
            return "status-backorder-waiting";
        default:
            return "";
    }
}

function getOrderStatusLabel($order)
{
    if (($order["status"] ?? "") === "cancelled") {
        return "Storniert";
    }
    if (($order["status"] ?? "") === "processed") {
        return "Bearbeitet";
    }
    if (($order["status"] ?? "") === "partial") {
        return "Teilweise bearbeitet";
    }
    return "Offen";
}

function getOrderStatusClass($order)
{
    if (($order["status"] ?? "") === "cancelled") {
        return "status-cancelled";
    }
    if (($order["status"] ?? "") === "processed") {
        return "status-processed";
    }
    if (($order["status"] ?? "") === "partial") {
        return "status-partial";
    }
    return "status-open";
}

function formatDate($dateString)
{
    $dateString = trim((string) $dateString);
    if ($dateString === "") {
        return "-";
    }

    try {
        $date = new DateTimeImmutable($dateString);
        return $date->format("d.m.Y H:i");
    } catch (Exception $exception) {
        return $dateString;
    }
}

function getCart()
{
    $cart = $_SESSION["cart"] ?? [];
    if (!is_array($cart)) {
        $cart = [];
    }

    $normalized = [];

    foreach ($cart as $item) {
        $productId = isset($item["product_id"]) ? (int) $item["product_id"] : 0;
        $size = trim((string) ($item["size"] ?? ""));
        $product = getProductById($productId);
        if ($product === null) {
            continue;
        }

        $sizes = getProductSizes($product);
        if (!empty($sizes)) {
            if ($size === "" || !in_array($size, $sizes, true)) {
                continue;
            }
        } else {
            $size = "";
        }

        if (isset($normalized[$productId])) {
            unset($normalized[$productId]);
        }

        $normalized[$productId] = [
            "product_id" => $productId,
            "size" => $size,
        ];
    }

    $_SESSION["cart"] = array_values($normalized);
    return $_SESSION["cart"];
}

function addCartItem($productId, $size = "")
{
    $productId = (int) $productId;
    $size = trim((string) $size);
    $product = getProductById($productId);
    if ($product === null) {
        return [
            "success" => false,
            "status" => "error",
        ];
    }

    $sizes = getProductSizes($product);
    if (!empty($sizes)) {
        if ($size === "" || !in_array($size, $sizes, true)) {
            return [
                "success" => false,
                "status" => "error",
            ];
        }
    } else {
        $size = "";
    }

    $cart = getCart();
    foreach ($cart as $index => $item) {
        if ((int) $item["product_id"] !== $productId) {
            continue;
        }

        $existingSize = trim((string) ($item["size"] ?? ""));
        if ($existingSize === $size) {
            return [
                "success" => true,
                "status" => "unchanged",
                "size" => $size,
            ];
        }

        $cart[$index]["size"] = $size;
        $_SESSION["cart"] = array_values($cart);

        return [
            "success" => true,
            "status" => "replaced",
            "size" => $size,
            "previous_size" => $existingSize,
        ];
    }

    $cart[] = [
        "product_id" => $productId,
        "size" => $size,
    ];

    $_SESSION["cart"] = array_values($cart);
    return [
        "success" => true,
        "status" => "added",
        "size" => $size,
    ];
}

function removeCartItem($productId, $size)
{
    $productId = (int) $productId;
    $size = trim((string) $size);
    $cart = getCart();
    $filtered = [];

    foreach ($cart as $item) {
        $itemProductId = (int) ($item["product_id"] ?? 0);
        $itemSize = trim((string) ($item["size"] ?? ""));
        if ($itemProductId === $productId && $itemSize === $size) {
            continue;
        }
        $filtered[] = $item;
    }

    $_SESSION["cart"] = array_values($filtered);
}

function clearCart()
{
    $_SESSION["cart"] = [];
}

function getCartItemsDetailed()
{
    $items = [];
    foreach (getCart() as $cartItem) {
        $product = getProductById($cartItem["product_id"]);
        if ($product === null) {
            continue;
        }

        $size = trim((string) ($cartItem["size"] ?? ""));
        $items[] = [
            "product" => $product,
            "size" => $size,
            "availability_label" =>
                $size !== "" ? getAvailabilityLabel($product, $size) : "",
        ];
    }

    return $items;
}

function buildOrderItemsFromCart()
{
    $items = [];
    foreach (getCart() as $cartItem) {
        $product = getProductById($cartItem["product_id"]);
        if ($product === null) {
            continue;
        }

        $size = trim((string) ($cartItem["size"] ?? ""));
        $items[] = [
            "product_id" => $product["id"],
            "size" => $size,
            "is_processed" => false,
        ];
    }

    return normalizeOrderItems($items);
}

function buildOrderItemsHtml($order)
{
    $parts = [];
    foreach ($order["items"] as $item) {
        $label = "<strong>" . escape($item["product_name"]) . "</strong>";
        if ($item["size"] !== "") {
            $label .= " - Größe: " . escape($item["size"]);
        }
        if (!empty($item["availability_label"])) {
            $label .=
                "<br><small>" .
                nl2br(escape($item["availability_label"])) .
                "</small>";
        }
        $parts[] =
            '<li style="margin: 0 0 0.75rem 0; padding: 0.9rem 1rem; background: #f8f4e7; border: 1px solid #d7ceb5; border-radius: 18px; color: #111111;">' .
            $label .
            "</li>";
    }

    return '<ul style="list-style: none; margin: 0; padding: 0;">' .
        implode("", $parts) .
        "</ul>";
}

function buildOrderSummaryHtml($order, $title, $introHtml, $extraHtml = "")
{
    $itemsHtml = buildOrderItemsHtml($order);

    return '
    <html>
    <head>
        <meta charset="UTF-8">
    </head>
    <body style="font-family: \'Freising Sans\', Arial, sans-serif; line-height: 1.6; color: #111111; background: #f4efe1; padding: 1.5rem;">
        <div style="max-width: 720px; margin: 0 auto; background: #ffffff; padding: 2rem; border-radius: 28px; border: 1px solid #d7ceb5; box-shadow: 0 18px 40px rgba(16, 16, 16, 0.08);">
            <p style="margin: 0 0 0.5rem; font-size: 0.8rem; letter-spacing: 0.08em; text-transform: uppercase; color: #5e5a4d;">' .
        escape(SITE_DEPARTMENT_NAME) .
        '</p>
            <h1 style="margin: 0; font-size: 1.75rem; line-height: 1.2; color: #111111;">' .
        escape(SITE_NAME) .
        '</h1>
            <p style="margin: 0.25rem 0 1.5rem; font-size: 1rem; color: #5e5a4d;">' .
        escape(SITE_SERVICE_HEADER) .
        '</p>
            <div style="width: 80px; height: 6px; border-radius: 999px; background: #ffd71c; margin-bottom: 1.5rem;"></div>
            <h2 style="color: #111111; margin-top: 0;">' .
        escape($title) .
        '</h2>
            ' .
        $introHtml .
        '
            <div style="background: #f8f4e7; border: 2px solid #ffd71c; padding: 1.5rem; margin: 1.5rem 0; border-radius: 20px;">
                <h3 style="margin-top: 0;">Bestellnummer</h3>
                <p style="margin: 0; color: #111111; font-family: monospace; font-size: 1.05rem;">' .
        escape($order["id"]) .
        '</p>
            </div>
            <p><strong>Name:</strong> ' .
        escape($order["customer_name"]) .
        '</p>
            <p><strong>E-Mail:</strong> ' .
        escape($order["customer_email"]) .
        '</p>
            <p><strong>Organisation:</strong> ' .
        escape($order["organization_label"]) .
        '</p>
            <p><strong>Erstellt am:</strong> ' .
        escape(formatDate($order["created_at"])) .
        '</p>
            <h3>Bestellte Artikel</h3>
            <div style="background: #fffdf6; border: 1px solid #d7ceb5; border-left: 6px solid #ffd71c; border-radius: 20px; padding: 1rem;">' .
        $itemsHtml .
        '</div>
            <p><strong>Kommentar:</strong><br>' .
        ($order["comment"] !== ""
            ? nl2br(escape($order["comment"]))
            : "Kein Kommentar") .
        '</p>
            ' .
        $extraHtml .
        '
            <p style="margin: 2rem 0 0; color: #5e5a4d; font-size: 0.9rem;">' .
        escape(SITE_NAME) .
        " | " .
        escape(SITE_DEPARTMENT_NAME) .
        "<br>" .
        escape(SITE_ADDRESS_LINE) .
        '</p>
        </div>
    </body>
    </html>';
}

function sendOrderCreatedCustomerEmail($order)
{
    $subject = SITE_SERVICE_NAME . ": Ihre Bestellung - " . $order["id"];
    $intro =
        "<p>Guten Tag " .
        escape($order["customer_name"]) .
        ",</p><p>Ihre Bestellung wurde erfasst und an " .
        escape(SITE_DEPARTMENT_NAME) .
        " weitergeleitet.</p>";
    $message = buildOrderSummaryHtml($order, "Bestellung eingegangen", $intro);

    return sendEmail($order["customer_email"], $subject, $message);
}

function sendConfirmedOrderAdminNotification($order)
{
    $recipient = getOrderRecipientEmail();
    if (!isValidAdminEmail($recipient)) {
        return false;
    }

    $subject = SITE_SERVICE_NAME . ": Neue Bestellung - " . $order["id"];
    $intro =
        "<p>Eine neue PSA-Bestellung ist eingegangen und muss bearbeitet werden.</p>";
    $message = buildOrderSummaryHtml($order, "Neue PSA-Bestellung", $intro);

    $attachments = [];
    if (shouldAttachOrderPdfToAdminEmail()) {
        $attachments[] = [
            "filename" => "bestellung-" . strtolower($order["id"]) . ".pdf",
            "content_type" => "application/pdf",
            "content" => renderOrderPdf($order),
        ];
    }

    return sendEmail($recipient, $subject, $message, true, $attachments);
}

function sendEmail($to, $subject, $message, $isHtml = true, $attachments = [])
{
    $headers = [];
    $headers[] = "From: " . FROM_NAME . " <" . FROM_EMAIL . ">";
    $headers[] = "Reply-To: " . FROM_EMAIL;
    $headers[] = "X-Mailer: PHP/" . phpversion();

    if (empty($attachments)) {
        if ($isHtml) {
            $headers[] = "MIME-Version: 1.0";
            $headers[] = "Content-Type: text/html; charset=UTF-8";
        } else {
            $headers[] = "Content-Type: text/plain; charset=UTF-8";
        }

        return mail($to, $subject, $message, implode("\r\n", $headers));
    }

    $boundary = "=_Boundary_" . bin2hex(random_bytes(8));
    $headers[] = "MIME-Version: 1.0";
    $headers[] = 'Content-Type: multipart/mixed; boundary="' . $boundary . '"';

    $body = [];
    $body[] = "--" . $boundary;
    $body[] =
        "Content-Type: " .
        ($isHtml ? "text/html" : "text/plain") .
        "; charset=UTF-8";
    $body[] = "Content-Transfer-Encoding: 8bit";
    $body[] = "";
    $body[] = $message;

    foreach ($attachments as $attachment) {
        if (!is_array($attachment)) {
            continue;
        }

        $filename = trim(
            (string) ($attachment["filename"] ?? "attachment.bin"),
        );
        $contentType = trim(
            (string) ($attachment["content_type"] ??
                "application/octet-stream"),
        );
        $content = isset($attachment["content"])
            ? (string) $attachment["content"]
            : "";

        $body[] = "--" . $boundary;
        $body[] =
            "Content-Type: " . $contentType . '; name="' . $filename . '"';
        $body[] = "Content-Transfer-Encoding: base64";
        $body[] =
            'Content-Disposition: attachment; filename="' . $filename . '"';
        $body[] = "";
        $body[] = chunk_split(base64_encode($content));
    }

    $body[] = "--" . $boundary . "--";
    $body[] = "";

    return mail(
        $to,
        $subject,
        implode("\r\n", $body),
        implode("\r\n", $headers),
    );
}

function pdfEncodeWinAnsi($text)
{
    $text = str_replace("\r", "", (string) $text);

    if (!function_exists("iconv")) {
        return preg_replace('/[^\x09\x0A\x20-\x7E]/', "?", $text);
    }

    $chars = preg_split("//u", $text, -1, PREG_SPLIT_NO_EMPTY);
    if (!is_array($chars)) {
        $fallback = @iconv("UTF-8", "Windows-1252//TRANSLIT//IGNORE", $text);
        return is_string($fallback) ? $fallback : $text;
    }

    $result = "";
    foreach ($chars as $char) {
        $converted = @iconv("UTF-8", "Windows-1252", $char);
        if ($converted !== false) {
            $result .= $converted;
            continue;
        }

        $fallback = @iconv("UTF-8", "Windows-1252//TRANSLIT", $char);
        if ($fallback !== false && $fallback !== "") {
            $result .= $fallback;
            continue;
        }

        $result .= "?";
    }

    return $result;
}

function pdfEscapeText($text)
{
    $text = str_replace("\\", "\\\\", $text);
    $text = str_replace("(", "\(", $text);
    $text = str_replace(")", "\)", $text);
    return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', "", $text);
}

function pdfWrapAnsiText($text, $maxChars)
{
    $lines = [];
    $paragraphs = explode("\n", str_replace("\r", "", (string) $text));

    foreach ($paragraphs as $paragraph) {
        $normalized = trim(preg_replace("/\s+/", " ", $paragraph));
        if ($normalized === "") {
            $lines[] = "";
            continue;
        }

        $words = explode(" ", $normalized);
        $current = "";

        foreach ($words as $word) {
            if ($word === "") {
                continue;
            }

            if (strlen($word) > $maxChars) {
                if ($current !== "") {
                    $lines[] = $current;
                    $current = "";
                }

                $parts = str_split($word, $maxChars);
                $lastIndex = count($parts) - 1;
                for ($i = 0; $i < $lastIndex; $i++) {
                    $lines[] = $parts[$i];
                }
                $current = $parts[$lastIndex];
                continue;
            }

            $candidate = $current === "" ? $word : $current . " " . $word;
            if (strlen($candidate) <= $maxChars) {
                $current = $candidate;
            } else {
                if ($current !== "") {
                    $lines[] = $current;
                }
                $current = $word;
            }
        }

        if ($current !== "") {
            $lines[] = $current;
        }
    }

    if (empty($lines)) {
        $lines[] = "";
    }

    return $lines;
}

function prepareOrderForDocument(array $order): array
{
    $items = [];
    foreach ($order["items"] ?? [] as $item) {
        if (!is_array($item)) {
            continue;
        }

        $items[] = [
            "product_name" => trim((string) ($item["product_name"] ?? "")),
            "size" => trim((string) ($item["size"] ?? "")),
            "availability_label" => trim(
                (string) ($item["availability_label"] ?? ""),
            ),
        ];
    }

    return [
        "id" => trim((string) ($order["id"] ?? "")),
        "created_at" => trim((string) ($order["created_at"] ?? "")),
        "customer_name" => trim((string) ($order["customer_name"] ?? "")),
        "customer_email" => trim((string) ($order["customer_email"] ?? "")),
        "organization_label" => trim(
            (string) ($order["organization_label"] ?? ""),
        ),
        "comment" => (string) ($order["comment"] ?? ""),
        "items" => $items,
    ];
}

function renderOrderPdf(array $order): string
{
    return generateOrderPdf(prepareOrderForDocument($order));
}

function streamOrderPdf(
    array $order,
    string $filename,
    bool $inline = true,
): void {
    $pdf = renderOrderPdf($order);
    $safeFilename = preg_replace('/[^A-Za-z0-9._-]+/', "-", $filename) ?: "bestellung.pdf";
    $disposition = $inline ? "inline" : "attachment";

    header("Content-Type: application/pdf");
    header(
        'Content-Disposition: ' .
            $disposition .
            '; filename="' .
            $safeFilename .
            '"',
    );
    header("Content-Length: " . strlen($pdf));
    echo $pdf;
    exit();
}

function generateOrderPdf($order)
{
    $pageWidth = 595;
    $pageHeight = 842;
    $leftMargin = 45;
    $topY = 800;
    $bottomY = 60;
    $lineHeight = 14;
    $itemLineHeight = 16;

    $pages = [];
    $pageContent = "";
    $y = $topY;
    $pageNumber = 0;

    $encodeText = function ($text) {
        return pdfEncodeWinAnsi((string) $text);
    };

    $orderId = $encodeText($order["id"]);
    $createdAt = $encodeText(formatDate($order["created_at"]));
    $customerName = $encodeText($order["customer_name"]);
    $customerEmail = $encodeText($order["customer_email"]);
    $organization = $encodeText($order["organization_label"]);
    $commentRaw = (string) $order["comment"];

    $writeText = function ($x, $y, $encodedText, $fontSize = 12) use (&$pageContent) {
        $pageContent .=
            "BT\n/F1 " .
            $fontSize .
            " Tf\n1 0 0 1 " .
            number_format($x, 2, ".", "") .
            " " .
            number_format($y, 2, ".", "") .
            " Tm\n(" .
            pdfEscapeText($encodedText) .
            ") Tj\nET\n";
    };

    $startPage = function () use (
        &$pages,
        &$pageContent,
        &$y,
        &$pageNumber,
        $topY,
        $leftMargin,
        $lineHeight,
        $orderId,
        $createdAt,
        $writeText,
        $encodeText
    ) {
        if ($pageContent !== "") {
            $pages[] = $pageContent;
        }

        $pageNumber++;
        $pageContent = "";
        $y = $topY;

        $headerLine = "Bestellung: " . $orderId;
        if ($pageNumber > 1) {
            $headerLine .= " | Seite " . $encodeText((string) $pageNumber);
        }
        $writeText($leftMargin, $y, $headerLine, 14);
        $y -= 19;
        $writeText($leftMargin, $y, "Erstellt am: " . $createdAt, 11);
        $y -= 20;
    };

    $ensureSpace = function ($requiredHeight) use (&$y, $bottomY, $startPage) {
        if ($y - $requiredHeight < $bottomY) {
            $startPage();
        }
    };

    $writeWrapped = function (
        $encodedText,
        $maxChars,
        $fontSize = 11,
        $x = null,
        $spacing = null
    ) use (&$y, $lineHeight, $leftMargin, $writeText, $ensureSpace) {
        $targetX = $x === null ? $leftMargin : $x;
        $step = $spacing === null ? $lineHeight : $spacing;
        $lines = pdfWrapAnsiText($encodedText, $maxChars);
        foreach ($lines as $line) {
            $ensureSpace($step);
            $writeText($targetX, $y, $line, $fontSize);
            $y -= $step;
        }
    };

    $writeSectionTitle = function ($titleText, $fontSize = 13) use (
        &$y,
        $leftMargin,
        $writeText,
        $ensureSpace,
        $encodeText
    ) {
        $ensureSpace(24);
        $writeText($leftMargin, $y, $encodeText($titleText), $fontSize);
        $y -= 18;
    };

    $startPage();

    $writeSectionTitle("Gehört zu");
    $writeWrapped("Name: " . $customerName, 80);
    $writeWrapped("Organisation: " . $organization, 80);
    $writeWrapped("E-Mail: " . $customerEmail, 80);
    $y -= 6;

    $writeSectionTitle("Artikelliste", 15);

    $itemNumber = 1;
    if (empty($order["items"])) {
        $writeWrapped($encodeText("Keine Artikel"), 80, 12);
    } else {
        foreach ($order["items"] as $item) {
            $itemName = $encodeText($item["product_name"]);
            $sizeLabel = $encodeText($item["size"]);
            $hintLabel = $encodeText(
                preg_replace("/\s+/", " ", (string) $item["availability_label"]),
            );

            $writeWrapped(
                $encodeText($itemNumber . ". ") . $itemName,
                68,
                14,
                null,
                $itemLineHeight,
            );

            if ($sizeLabel !== "") {
                $writeWrapped(
                    $encodeText("   Größe: ") . $sizeLabel,
                    66,
                    12,
                    null,
                    $itemLineHeight,
                );
            }

            if ($hintLabel !== "") {
                $writeWrapped(
                    $encodeText("   Hinweis: ") . $hintLabel,
                    66,
                    12,
                    null,
                    $itemLineHeight,
                );
            }

            $ensureSpace(40);
            $y -= 40;
            $itemNumber++;
        }
    }

    $y -= 4;
    $writeSectionTitle("Kommentar");

    if (trim($commentRaw) === "") {
        $writeWrapped($encodeText("Kein Kommentar"), 80);
    } else {
        foreach (preg_split('/\r\n|\r|\n/', $commentRaw) as $commentLine) {
            $writeWrapped($encodeText($commentLine), 82);
        }
    }

    $y -= 6;
    $writeSectionTitle("Lagerbearbeitung");

    $warehouseLines = [
        "Ausgegeben am / durch: __________________________________________________",
        "[ ] Vollständig ausgegeben",
        "[ ] Teilweise ausgegeben",
    ];

    foreach ($warehouseLines as $line) {
        $writeWrapped($encodeText($line), 80);
    }

    if ($pageContent !== "") {
        $pages[] = $pageContent;
    }

    if (empty($pages)) {
        $pages[] = "";
    }

    $fontObjectNumber = 3;
    $objects = [];
    $objects[1] = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
    $objects[
        $fontObjectNumber
    ] = "3 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\nendobj\n";

    $kids = [];
    $nextObjectNumber = 4;
    foreach ($pages as $pageStream) {
        $pageObjectNumber = $nextObjectNumber++;
        $contentObjectNumber = $nextObjectNumber++;

        $kids[] = $pageObjectNumber . " 0 R";
        $objects[$pageObjectNumber] =
            $pageObjectNumber .
            " 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 " .
            $pageWidth .
            " " .
            $pageHeight .
            "] /Contents " .
            $contentObjectNumber .
            " 0 R /Resources << /Font << /F1 " .
            $fontObjectNumber .
            " 0 R >> >> >>\nendobj\n";

        $objects[$contentObjectNumber] =
            $contentObjectNumber .
            " 0 obj\n<< /Length " .
            strlen($pageStream) .
            " >>\nstream\n" .
            $pageStream .
            "\nendstream\nendobj\n";
    }

    $objects[2] =
        "2 0 obj\n<< /Type /Pages /Kids [" .
        implode(" ", $kids) .
        "] /Count " .
        count($kids) .
        " >>\nendobj\n";

    ksort($objects);

    $pdf = "%PDF-1.4\n";
    $offsets = [0];
    foreach ($objects as $number => $objectContent) {
        $offsets[$number] = strlen($pdf);
        $pdf .= $objectContent;
    }

    $lastObjectNumber = (int) max(array_keys($objects));
    $xrefOffset = strlen($pdf);
    $pdf .= "xref\n0 " . ($lastObjectNumber + 1) . "\n";
    $pdf .= "0000000000 65535 f \n";

    for ($i = 1; $i <= $lastObjectNumber; $i++) {
        if (isset($offsets[$i])) {
            $pdf .= sprintf("%010d 00000 n \n", $offsets[$i]);
        } else {
            $pdf .= "0000000000 00000 f \n";
        }
    }

    $pdf .= "trailer\n<< /Size " . ($lastObjectNumber + 1) . " /Root 1 0 R >>\n";
    $pdf .= "startxref\n" . $xrefOffset . "\n%%EOF";

    return $pdf;
}

/**
 * Input Validation Class
 */
class Validator
{
    private $errors = [];
    private $data = [];

    public function __construct($data)
    {
        $this->data = $data;
    }

    public function required($field, $label = null)
    {
        $value = $this->get($field);
        if (empty(trim($value ?? ""))) {
            $this->errors[] = ($label ?? $field) . " ist erforderlich.";
        }
        return $this;
    }

    public function email($field, $label = null)
    {
        $value = $this->get($field);
        if (!empty($value) && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
            $this->errors[] =
                ($label ?? $field) . " muss eine gültige E-Mail-Adresse sein.";
        }
        return $this;
    }

    public function minLength($field, $min, $label = null)
    {
        $value = $this->get($field);
        if (!empty($value) && strlen($value) < $min) {
            $this->errors[] =
                ($label ?? $field) .
                " muss mindestens " .
                $min .
                " Zeichen lang sein.";
        }
        return $this;
    }

    public function maxLength($field, $max, $label = null)
    {
        $value = $this->get($field);
        if (!empty($value) && strlen($value) > $max) {
            $this->errors[] =
                ($label ?? $field) .
                " darf maximal " .
                $max .
                " Zeichen lang sein.";
        }
        return $this;
    }

    public function inArray($field, $allowed, $label = null)
    {
        $value = $this->get($field);
        if (!empty($value) && !in_array($value, $allowed, true)) {
            $this->errors[] = ($label ?? $field) . " ist ungültig.";
        }
        return $this;
    }

    public function get($field)
    {
        return $this->data[$field] ?? null;
    }

    public function addError(string $message): self
    {
        $message = trim($message);
        if ($message !== "") {
            $this->errors[] = $message;
        }
        return $this;
    }

    public function getErrors()
    {
        return $this->errors;
    }

    public function isValid()
    {
        return empty($this->errors);
    }
}

/**
 * Error Logging Functions
 */
if (!defined("LOG_DIR")) {
    define("LOG_DIR", DATA_DIR . "logs/");
}
if (!defined("ERROR_LOG_FILE")) {
    define("ERROR_LOG_FILE", LOG_DIR . "error.log");
}
if (!defined("ACCESS_LOG_FILE")) {
    define("ACCESS_LOG_FILE", LOG_DIR . "access.log");
}
if (!defined("LOG_MAX_BYTES")) {
    define("LOG_MAX_BYTES", 1048576);
}
if (!defined("LOG_KEEP_FILES")) {
    define("LOG_KEEP_FILES", 5);
}
if (!defined("LOG_MAX_AGE_SECONDS")) {
    define("LOG_MAX_AGE_SECONDS", 2592000);
}

function initLogging()
{
    if (!is_dir(LOG_DIR)) {
        mkdir(LOG_DIR, 02775, true);
        @chmod(LOG_DIR, 02775);
    }
}

function rotateLogFileIfNeeded(string $logFile): void
{
    $maxBytes = max(1024, (int) LOG_MAX_BYTES);
    $keepFiles = max(1, (int) LOG_KEEP_FILES);
    $maxAgeSeconds = max(0, (int) LOG_MAX_AGE_SECONDS);
    $now = time();
    $rotatedFiles = glob($logFile . ".*") ?: [];

    foreach ($rotatedFiles as $rotatedFile) {
        if (preg_match('/\.(\d+)$/', $rotatedFile, $matches) !== 1) {
            continue;
        }

        $index = (int) $matches[1];
        $isTooOld =
            $maxAgeSeconds > 0 &&
            is_file($rotatedFile) &&
            ($now - (int) filemtime($rotatedFile)) > $maxAgeSeconds;

        if ($index > $keepFiles || $isTooOld) {
            @unlink($rotatedFile);
        }
    }

    if (!is_file($logFile) || (filesize($logFile) ?: 0) < $maxBytes) {
        return;
    }

    for ($i = $keepFiles; $i >= 1; $i--) {
        $source = $i === 1 ? $logFile : $logFile . "." . ($i - 1);
        $target = $logFile . "." . $i;
        if (!is_file($source)) {
            continue;
        }
        if ($i === $keepFiles && is_file($target)) {
            @unlink($target);
        }
        @rename($source, $target);
    }
}

function logError($message, $context = [], $level = "ERROR")
{
    initLogging();
    rotateLogFileIfNeeded(ERROR_LOG_FILE);

    $entry = [
        "timestamp" => date("Y-m-d H:i:s.u"),
        "level" => $level,
        "message" => $message,
        "context" => $context,
        "ip" => $_SERVER["REMOTE_ADDR"] ?? "unknown",
        "user_agent" => $_SERVER["HTTP_USER_AGENT"] ?? "unknown",
        "request_uri" => $_SERVER["REQUEST_URI"] ?? "unknown",
        "session_id" => session_id()
            ? substr(session_id(), 0, 8) . "..."
            : "none",
    ];

    $logLine = json_encode($entry, JSON_UNESCAPED_UNICODE) . PHP_EOL;

    file_put_contents(ERROR_LOG_FILE, $logLine, FILE_APPEND | LOCK_EX);

    if (defined("DEVELOPMENT_MODE") && DEVELOPMENT_MODE) {
        error_log(
            "PSA Order: " . $message . " | Context: " . json_encode($context),
        );
    }
}

function logAccess($message, $context = [])
{
    initLogging();
    rotateLogFileIfNeeded(ACCESS_LOG_FILE);

    $entry = [
        "timestamp" => date("Y-m-d H:i:s.u"),
        "message" => $message,
        "context" => $context,
        "ip" => $_SERVER["REMOTE_ADDR"] ?? "unknown",
        "request_method" => $_SERVER["REQUEST_METHOD"] ?? "unknown",
        "request_uri" => $_SERVER["REQUEST_URI"] ?? "unknown",
    ];

    $logLine = json_encode($entry, JSON_UNESCAPED_UNICODE) . PHP_EOL;

    file_put_contents(ACCESS_LOG_FILE, $logLine, FILE_APPEND | LOCK_EX);
}

function handleException($exception)
{
    logError(
        "Uncaught exception: " . $exception->getMessage(),
        [
            "file" => $exception->getFile(),
            "line" => $exception->getLine(),
            "trace" => $exception->getTraceAsString(),
        ],
        "CRITICAL",
    );

    if (defined("DEVELOPMENT_MODE") && DEVELOPMENT_MODE) {
        echo "<h1>Error</h1><pre>" .
            escape($exception->getMessage()) .
            "</pre>";
    } else {
        echo "<h1>Es ist ein Fehler aufgetreten</h1><p>Bitte versuchen Sie es später erneut.</p>";
    }
    exit();
}

set_exception_handler("handleException");

function handleError($errno, $errstr, $errfile, $errline)
{
    if (!(error_reporting() & $errno)) {
        return false;
    }

    logError(
        "PHP Error: " . $errstr,
        [
            "type" => $errno,
            "file" => $errfile,
            "line" => $errline,
        ],
        "WARNING",
    );

    return true;
}

set_error_handler("handleError");
