<?php
declare(strict_types=1);

/**
 * /functional/post/manage.php
 *
 * Відповідає за:
 * - action=edit
 * - action=delete
 * - action=delete_attachment
 * - один центр логіки для root/admin/group-wrapper'ів
 *
 * Підтримує:
 * - редагування body
 * - видалення вибраних вкладень
 * - додавання нових вкладень
 * - sync poll (якщо UI передає poll_question + poll_options[])
 * - безпечне фізичне видалення файлів після commit
 * - сумісність зі звичайним CSRF і admin CSRF
 */

require_once dirname(__DIR__, 2) . '/includes/start.php';
require_once __DIR__ . '/_lib.php';

/* admin csrf сумісність (якщо файл існує) */
$__pmAdminCsrfFile = dirname(__DIR__, 2) . '/admin/_csrf.php';
if (is_file($__pmAdminCsrfFile)) {
    require_once $__pmAdminCsrfFile;
}

if (!function_exists('pm_redirect')) {
    function pm_redirect(string $to): never
    {
        header('Location: ' . $to);
        exit;
    }
}

if (!function_exists('pm_flash_set')) {
    function pm_flash_set(string $key, string $msg): void
    {
        $_SESSION['flash'][$key] = $msg;
    }
}

if (!function_exists('pm_is_post')) {
    function pm_is_post(): bool
    {
        return strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST';
    }
}

if (!function_exists('pm_safe_back_url')) {
    function pm_safe_back_url(string $raw): string
    {
        $raw = trim($raw);
        if ($raw === '') return '/';

        if (preg_match('~^https?://~i', $raw)) {
            $p = parse_url($raw);
            $path  = (string)($p['path'] ?? '/');
            $query = isset($p['query']) && $p['query'] !== '' ? ('?' . $p['query']) : '';
            $raw   = $path . $query;
        }

        if ($raw === '' || $raw[0] !== '/') return '/';
        if (str_starts_with($raw, '//')) return '/';
        if (strpos($raw, "\0") !== false) return '/';

        return $raw;
    }
}

if (!function_exists('pm_normalize_text')) {
    function pm_normalize_text(string $text): string
    {
        $text = str_replace(["\r\n", "\r"], "\n", $text);
        $text = preg_replace("/\n{3,}/", "\n\n", $text) ?? $text;
        return trim($text);
    }
}

if (!function_exists('pm_table_exists')) {
    function pm_table_exists(PDO $pdo, string $table): bool
    {
        if (!preg_match('/^[a-zA-Z0-9_]+$/', $table)) return false;

        $sql = "
            SELECT 1
            FROM information_schema.tables
            WHERE table_schema = DATABASE()
              AND table_name = ?
            LIMIT 1
        ";
        $st = $pdo->prepare($sql);
        $st->execute([$table]);

        return (bool)$st->fetchColumn();
    }
}

if (!function_exists('pm_table_columns')) {
    function pm_table_columns(PDO $pdo, string $table): array
    {
        if (!pm_table_exists($pdo, $table)) return [];
        $rows = $pdo->query("DESCRIBE `{$table}`")->fetchAll(PDO::FETCH_ASSOC) ?: [];

        $out = [];
        foreach ($rows as $r) {
            $f = strtolower((string)($r['Field'] ?? ''));
            if ($f !== '') $out[] = $f;
        }
        return $out;
    }
}

if (!function_exists('pm_has_column')) {
    function pm_has_column(PDO $pdo, string $table, string $column): bool
    {
        return in_array(strtolower($column), pm_table_columns($pdo, $table), true);
    }
}

if (!function_exists('pm_upload_root')) {
    function pm_upload_root(): string
    {
        return dirname(__DIR__, 2);
    }
}

if (!function_exists('pm_unlink_upload_webpath')) {
    function pm_unlink_upload_webpath(string $webPath): void
    {
        $webPath = trim($webPath);
        if ($webPath === '') return;

        if ($webPath[0] !== '/') {
            $webPath = '/' . $webPath;
        }

        if (!str_starts_with($webPath, '/upload/')) return;
        if (str_contains($webPath, '..')) return;

        $abs = pm_upload_root() . $webPath;
        $uploadRoot = realpath(pm_upload_root() . '/upload');
        $realDir = realpath(dirname($abs));

        if ($uploadRoot === false || $realDir === false) return;
        if (!str_starts_with($realDir, $uploadRoot)) return;

        if (is_file($abs)) {
            @unlink($abs);
        }
    }
}

if (!function_exists('pm_cleanup_uploaded_rows')) {
    function pm_cleanup_uploaded_rows(array $rows): void
    {
        foreach ($rows as $row) {
            $abs = (string)($row['abs_path'] ?? '');
            if ($abs !== '' && is_file($abs)) {
                @unlink($abs);
            }
        }
    }
}

if (!function_exists('pm_safe_ext')) {
    function pm_safe_ext(string $name): string
    {
        $ext = strtolower((string)pathinfo($name, PATHINFO_EXTENSION));
        $ext = preg_replace('~[^a-z0-9]+~', '', $ext) ?? '';
        return $ext;
    }
}

if (!function_exists('pm_allowed_extensions')) {
    function pm_allowed_extensions(): array
    {
        return [
            'jpg','jpeg','png','gif','webp',
            'mp3','wav','ogg','m4a',
            'mp4','webm','mov',
            'pdf','txt','zip','rar','7z',
            'doc','docx','xls','xlsx','ppt','pptx'
        ];
    }
}

if (!function_exists('pm_allowed_mime')) {
    function pm_allowed_mime(string $mime): bool
    {
        $mime = strtolower(trim($mime));

        if (str_starts_with($mime, 'image/')) return true;
        if (str_starts_with($mime, 'audio/')) return true;
        if (str_starts_with($mime, 'video/')) return true;

        return in_array($mime, [
            'application/pdf',
            'text/plain',
            'application/zip',
            'application/x-zip-compressed',
            'application/x-rar-compressed',
            'application/vnd.rar',
            'application/x-7z-compressed',
            'application/msword',
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            'application/vnd.ms-excel',
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            'application/vnd.ms-powerpoint',
            'application/vnd.openxmlformats-officedocument.presentationml.presentation',
        ], true);
    }
}

if (!function_exists('pm_file_type_from_mime')) {
    function pm_file_type_from_mime(string $mime): string
    {
        $mime = strtolower(trim($mime));
        if (str_starts_with($mime, 'image/')) return 'image';
        if (str_starts_with($mime, 'audio/')) return 'audio';
        if (str_starts_with($mime, 'video/')) return 'video';
        return 'file';
    }
}

if (!function_exists('pm_normalize_files_array')) {
    function pm_normalize_files_array(?array $files): array
    {
        if (!$files || !isset($files['name'])) return [];

        if (!is_array($files['name'])) {
            return [$files];
        }

        $out = [];
        $count = count($files['name']);
        for ($i = 0; $i < $count; $i++) {
            $out[] = [
                'name'     => $files['name'][$i] ?? '',
                'type'     => $files['type'][$i] ?? '',
                'tmp_name' => $files['tmp_name'][$i] ?? '',
                'error'    => $files['error'][$i] ?? UPLOAD_ERR_NO_FILE,
                'size'     => $files['size'][$i] ?? 0,
            ];
        }

        return $out;
    }
}

if (!function_exists('pm_handle_post_uploads')) {
    function pm_handle_post_uploads(?array $files, int $postId): array
    {
        $items = pm_normalize_files_array($files);
        if (!$items) return [];

        $root  = pm_upload_root();
        $year  = date('Y');
        $month = date('m');
        $finfo = new finfo(FILEINFO_MIME_TYPE);

        $out = [];

        foreach ($items as $file) {
            $err = (int)($file['error'] ?? UPLOAD_ERR_NO_FILE);
            if ($err === UPLOAD_ERR_NO_FILE) continue;
            if ($err !== UPLOAD_ERR_OK) {
                throw new RuntimeException('Upload error code: ' . $err);
            }

            $tmp  = (string)($file['tmp_name'] ?? '');
            $orig = (string)($file['name'] ?? '');
            $size = (int)($file['size'] ?? 0);

            if ($tmp === '' || !is_uploaded_file($tmp)) {
                throw new RuntimeException('Invalid uploaded file.');
            }

            if ($size <= 0) {
                throw new RuntimeException('Empty uploaded file.');
            }

            if ($size > 50 * 1024 * 1024) {
                throw new RuntimeException('File is too large.');
            }

            $mime = strtolower((string)$finfo->file($tmp));
            $ext  = pm_safe_ext($orig);

            if ($ext === '' || !in_array($ext, pm_allowed_extensions(), true)) {
                throw new RuntimeException('File extension is not allowed: ' . $orig);
            }

            if (!pm_allowed_mime($mime)) {
                throw new RuntimeException('File MIME is not allowed: ' . $mime);
            }

            if (str_starts_with($mime, 'image/')) {
                $img = @getimagesize($tmp);
                if ($img === false) {
                    throw new RuntimeException('Invalid image file: ' . $orig);
                }
            }

            $type = pm_file_type_from_mime($mime);
            $stored = $postId . '_' . bin2hex(random_bytes(10)) . '.' . $ext;
            $webPath = '/upload/' . $type . '/posts/' . $year . '/' . $month . '/' . $stored;
            $absPath = $root . $webPath;
            $dir = dirname($absPath);

            if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) {
                throw new RuntimeException('Failed to create upload directory.');
            }

            if (!move_uploaded_file($tmp, $absPath)) {
                throw new RuntimeException('Failed to move uploaded file.');
            }

            $out[] = [
                'file_type'     => $type,
                'web_path'      => $webPath,
                'abs_path'      => $absPath,
                'stored_name'   => $stored,
                'original_name' => $orig,
                'mime'          => $mime,
                'size_bytes'    => $size,
            ];
        }

        return $out;
    }
}

if (!function_exists('pm_post_row')) {
    function pm_post_row(PDO $pdo, int $postId): ?array
    {
        $st = $pdo->prepare('SELECT * FROM posts WHERE id = ? LIMIT 1');
        $st->execute([$postId]);
        $row = $st->fetch(PDO::FETCH_ASSOC);

        return $row ?: null;
    }
}

if (!function_exists('pm_is_group_manager')) {
    function pm_is_group_manager(PDO $pdo, int $groupId, int $userId): bool
    {
        if ($groupId <= 0 || $userId <= 0 || !pm_table_exists($pdo, 'group_members')) {
            return false;
        }

        $st = $pdo->prepare("
            SELECT 1
            FROM group_members
            WHERE group_id = ?
              AND user_id = ?
              AND status = 'active'
              AND role IN ('owner','admin')
            LIMIT 1
        ");
        $st->execute([$groupId, $userId]);

        return (bool)$st->fetchColumn();
    }
}

if (!function_exists('pm_user_can_manage_post')) {
    function pm_user_can_manage_post(PDO $pdo, array $me, array $post): bool
    {
        $uid = (int)($me['id'] ?? 0);
        $role = (string)($me['role'] ?? '');

        if ($uid <= 0) return false;
        if ($role === 'admin') return true;
        if ((int)($post['user_id'] ?? 0) === $uid) return true;

        $groupId = (int)($post['group_id'] ?? 0);
        if ($groupId > 0 && pm_is_group_manager($pdo, $groupId, $uid)) {
            return true;
        }

        return false;
    }
}

if (!function_exists('pm_path_column')) {
    function pm_path_column(PDO $pdo, string $table): ?string
    {
        $cols = pm_table_columns($pdo, $table);
        foreach (['path', 'rel_path'] as $c) {
            if (in_array($c, $cols, true)) return $c;
        }
        return null;
    }
}

if (!function_exists('pm_post_attachment_rows')) {
    function pm_post_attachment_rows(PDO $pdo, int $postId): array
    {
        if (!pm_table_exists($pdo, 'post_attachments')) return [];

        $st = $pdo->prepare('SELECT * FROM post_attachments WHERE post_id = ? ORDER BY id ASC');
        $st->execute([$postId]);

        return $st->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }
}

if (!function_exists('pm_post_attachment_paths')) {
    function pm_post_attachment_paths(PDO $pdo, int $postId): array
    {
        $rows = pm_post_attachment_rows($pdo, $postId);
        $out = [];

        foreach ($rows as $row) {
            $path = (string)($row['path'] ?? $row['rel_path'] ?? '');
            if ($path !== '') $out[] = $path;
        }

        return array_values(array_unique($out));
    }
}

if (!function_exists('pm_comment_attachment_paths_for_post')) {
    function pm_comment_attachment_paths_for_post(PDO $pdo, int $postId): array
    {
        if (!pm_table_exists($pdo, 'comments') || !pm_table_exists($pdo, 'comment_attachments')) {
            return [];
        }

        $pathCol = pm_path_column($pdo, 'comment_attachments');
        if ($pathCol === null) return [];

        $sql = "
            SELECT ca.`{$pathCol}` AS path_value
            FROM comment_attachments ca
            INNER JOIN comments c ON c.id = ca.comment_id
            WHERE c.post_id = ?
        ";
        $st = $pdo->prepare($sql);
        $st->execute([$postId]);

        $rows = $st->fetchAll(PDO::FETCH_ASSOC) ?: [];
        $out = [];

        foreach ($rows as $r) {
            $p = (string)($r['path_value'] ?? '');
            if ($p !== '') $out[] = $p;
        }

        return array_values(array_unique($out));
    }
}

if (!function_exists('pm_insert_post_attachment_dynamic')) {
    function pm_insert_post_attachment_dynamic(PDO $pdo, int $postId, array $row): void
    {
        if (!pm_table_exists($pdo, 'post_attachments')) {
            throw new RuntimeException('post_attachments table not found.');
        }

        $cols = pm_table_columns($pdo, 'post_attachments');
        $data = [];

        if (in_array('post_id', $cols, true))       $data['post_id'] = $postId;
        if (in_array('file_type', $cols, true))     $data['file_type'] = (string)$row['file_type'];
        if (in_array('path', $cols, true))          $data['path'] = (string)$row['web_path'];
        if (in_array('rel_path', $cols, true))      $data['rel_path'] = ltrim((string)$row['web_path'], '/');
        if (in_array('stored_name', $cols, true))   $data['stored_name'] = (string)$row['stored_name'];
        if (in_array('original_name', $cols, true)) $data['original_name'] = (string)$row['original_name'];
        if (in_array('mime', $cols, true))          $data['mime'] = (string)$row['mime'];
        if (in_array('size_bytes', $cols, true))    $data['size_bytes'] = (int)$row['size_bytes'];
        if (in_array('created_at', $cols, true))    $data['created_at'] = date('Y-m-d H:i:s');

        if (!isset($data['post_id'])) {
            throw new RuntimeException('post_attachments.post_id column is required.');
        }

        $fields = array_keys($data);
        $placeholders = implode(',', array_fill(0, count($fields), '?'));
        $fieldSql = '`' . implode('`,`', $fields) . '`';

        $sql = "INSERT INTO `post_attachments` ({$fieldSql}) VALUES ({$placeholders})";
        $st = $pdo->prepare($sql);
        $st->execute(array_values($data));
    }
}

if (!function_exists('pm_delete_post_attachment_rows_by_ids')) {
    function pm_delete_post_attachment_rows_by_ids(PDO $pdo, int $postId, array $ids): array
    {
        if (!$ids || !pm_table_exists($pdo, 'post_attachments')) return [];

        $ids = array_values(array_unique(array_map('intval', $ids)));
        $ids = array_values(array_filter($ids, static fn($v) => $v > 0));
        if (!$ids) return [];

        $pathCol = pm_path_column($pdo, 'post_attachments');
        if ($pathCol === null) $pathCol = 'path';

        $in = implode(',', array_fill(0, count($ids), '?'));

        $sqlSel = "SELECT id, `{$pathCol}` AS path_value FROM post_attachments WHERE post_id = ? AND id IN ({$in})";
        $stSel = $pdo->prepare($sqlSel);
        $stSel->execute(array_merge([$postId], $ids));
        $rows = $stSel->fetchAll(PDO::FETCH_ASSOC) ?: [];

        if ($rows) {
            $sqlDel = "DELETE FROM post_attachments WHERE post_id = ? AND id IN ({$in})";
            $stDel = $pdo->prepare($sqlDel);
            $stDel->execute(array_merge([$postId], $ids));
        }

        $paths = [];
        foreach ($rows as $r) {
            $p = (string)($r['path_value'] ?? '');
            if ($p !== '') $paths[] = $p;
        }

        return array_values(array_unique($paths));
    }
}

if (!function_exists('pm_sync_post_poll')) {
    function pm_sync_post_poll(PDO $pdo, int $postId, ?string $question, ?array $options): void
    {
        if (!pm_table_exists($pdo, 'post_polls') || !pm_table_exists($pdo, 'post_poll_options')) {
            return;
        }

        $question = trim((string)$question);
        $options = is_array($options) ? $options : [];
        $options = array_values(array_filter(array_map(static function ($v) {
            return trim((string)$v);
        }, $options), static fn($v) => $v !== ''));

        $stOld = $pdo->prepare('SELECT id FROM post_polls WHERE post_id = ?');
        $stOld->execute([$postId]);
        $oldPollIds = array_map('intval', $stOld->fetchAll(PDO::FETCH_COLUMN) ?: []);

        if ($oldPollIds) {
            $in = implode(',', array_fill(0, count($oldPollIds), '?'));
            $pdo->prepare("DELETE FROM post_poll_votes WHERE poll_id IN ({$in})")->execute($oldPollIds);
            $pdo->prepare("DELETE FROM post_poll_options WHERE poll_id IN ({$in})")->execute($oldPollIds);
            $pdo->prepare("DELETE FROM post_polls WHERE id IN ({$in})")->execute($oldPollIds);
        }

        if ($question === '' || count($options) < 2) {
            return;
        }

        $stPoll = $pdo->prepare('INSERT INTO post_polls (post_id, question, created_at) VALUES (?, ?, ?)');
        $stPoll->execute([$postId, $question, date('Y-m-d H:i:s')]);
        $pollId = (int)$pdo->lastInsertId();

        $stOpt = $pdo->prepare('INSERT INTO post_poll_options (poll_id, option_text, sort_order) VALUES (?, ?, ?)');
        $sort = 1;
        foreach ($options as $opt) {
            $stOpt->execute([$pollId, $opt, $sort]);
            $sort++;
        }
    }
}

if (!function_exists('pm_edit_post_core')) {
    function pm_edit_post_core(PDO $pdo, array $me, int $postId, string $body, array $deleteAttachmentIds, array $newUploads, ?string $pollQuestion, ?array $pollOptions): void
    {
        $post = pm_post_row($pdo, $postId);
        if (!$post) {
            throw new RuntimeException('Post not found.');
        }

        if (!pm_user_can_manage_post($pdo, $me, $post)) {
            throw new RuntimeException('Access denied.');
        }

        $body = pm_normalize_text($body);

        $existingAttachments = pm_post_attachment_rows($pdo, $postId);
        $remainingAfterDelete = $existingAttachments;

        if ($deleteAttachmentIds) {
            $deleteMap = array_fill_keys(array_map('intval', $deleteAttachmentIds), true);
            $remainingAfterDelete = array_values(array_filter($existingAttachments, static function ($row) use ($deleteMap) {
                return !isset($deleteMap[(int)($row['id'] ?? 0)]);
            }));
        }

        if ($body === '' && !$remainingAfterDelete && !$newUploads) {
            throw new RuntimeException('Post body or attachment required.');
        }

        $pathsToDeleteAfterCommit = [];

        try {
            if (!$pdo->inTransaction()) {
                $pdo->beginTransaction();
            }

            $st = $pdo->prepare('UPDATE posts SET body = ? WHERE id = ? LIMIT 1');
            $st->execute([$body, $postId]);

            if ($deleteAttachmentIds) {
                $pathsToDeleteAfterCommit = array_merge(
                    $pathsToDeleteAfterCommit,
                    pm_delete_post_attachment_rows_by_ids($pdo, $postId, $deleteAttachmentIds)
                );
            }

            foreach ($newUploads as $row) {
                pm_insert_post_attachment_dynamic($pdo, $postId, $row);
            }

            if ($pollQuestion !== null || $pollOptions !== null) {
                pm_sync_post_poll($pdo, $postId, $pollQuestion, $pollOptions);
            }

            $pdo->commit();

            foreach (array_values(array_unique($pathsToDeleteAfterCommit)) as $path) {
                pm_unlink_upload_webpath($path);
            }
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) {
                $pdo->rollBack();
            }
            pm_cleanup_uploaded_rows($newUploads);
            throw $e;
        }
    }
}

if (!function_exists('pm_delete_post_core')) {
    function pm_delete_post_core(PDO $pdo, array $me, int $postId): void
    {
        $post = pm_post_row($pdo, $postId);
        if (!$post) {
            throw new RuntimeException('Post not found.');
        }

        if (!pm_user_can_manage_post($pdo, $me, $post)) {
            throw new RuntimeException('Access denied.');
        }

        $paths = array_merge(
            pm_post_attachment_paths($pdo, $postId),
            pm_comment_attachment_paths_for_post($pdo, $postId)
        );
        $paths = array_values(array_unique($paths));

        try {
            if (!$pdo->inTransaction()) {
                $pdo->beginTransaction();
            }

            $st = $pdo->prepare('DELETE FROM posts WHERE id = ? LIMIT 1');
            $st->execute([$postId]);

            $pdo->commit();

            foreach ($paths as $path) {
                pm_unlink_upload_webpath($path);
            }
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) {
                $pdo->rollBack();
            }
            throw $e;
        }
    }
}

if (!function_exists('pm_delete_post_attachment_core')) {
    function pm_delete_post_attachment_core(PDO $pdo, array $me, int $attachmentId): void
    {
        if ($attachmentId <= 0 || !pm_table_exists($pdo, 'post_attachments')) {
            throw new RuntimeException('Attachment not found.');
        }

        $pathCol = pm_path_column($pdo, 'post_attachments') ?? 'path';

        $sql = "
            SELECT pa.id AS attachment_id, pa.`{$pathCol}` AS path_value, p.*
            FROM post_attachments pa
            INNER JOIN posts p ON p.id = pa.post_id
            WHERE pa.id = ?
            LIMIT 1
        ";
        $st = $pdo->prepare($sql);
        $st->execute([$attachmentId]);
        $row = $st->fetch(PDO::FETCH_ASSOC);

        if (!$row) {
            throw new RuntimeException('Attachment not found.');
        }

        if (!pm_user_can_manage_post($pdo, $me, $row)) {
            throw new RuntimeException('Access denied.');
        }

        $path = (string)($row['path_value'] ?? '');

        try {
            if (!$pdo->inTransaction()) {
                $pdo->beginTransaction();
            }

            $del = $pdo->prepare('DELETE FROM post_attachments WHERE id = ? LIMIT 1');
            $del->execute([$attachmentId]);

            $pdo->commit();

            if ($path !== '') {
                pm_unlink_upload_webpath($path);
            }
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) {
                $pdo->rollBack();
            }
            throw $e;
        }
    }
}

if (!function_exists('pm_extract_delete_attachment_ids')) {
    function pm_extract_delete_attachment_ids(array $src): array
    {
        $out = [];

        foreach (['delete_attachment_ids', 'remove_attachment_ids', 'attachment_delete_ids'] as $key) {
            if (!isset($src[$key])) continue;

            $value = $src[$key];
            if (!is_array($value)) $value = [$value];

            foreach ($value as $v) {
                if ($v !== '' && is_numeric((string)$v)) {
                    $out[] = (int)$v;
                }
            }
        }

        return array_values(array_unique(array_filter($out, static fn($v) => $v > 0)));
    }
}

if (!function_exists('pm_extract_csrf_token')) {
    function pm_extract_csrf_token(): string
    {
        if (isset($_POST['csrf_token']) && is_scalar($_POST['csrf_token'])) {
            return (string)$_POST['csrf_token'];
        }
        if (isset($_POST['csrf']) && is_scalar($_POST['csrf'])) {
            return (string)$_POST['csrf'];
        }
        return '';
    }
}

if (!function_exists('pm_verify_csrf_request')) {
    function pm_verify_csrf_request(): bool
    {
        $sent = pm_extract_csrf_token();

        if ($sent === '') {
            return false;
        }

        /* 1. Звичайний csrf */
        if (function_exists('csrf_check') && csrf_check($sent)) {
            return true;
        }

        /* 2. Адмінський csrf через helper */
        if (function_exists('admin_csrf_verify') && admin_csrf_verify()) {
            return true;
        }

        /* 3. Fallback напряму через session admin_csrf */
        $adminReal = (string)($_SESSION['admin_csrf'] ?? '');
        if ($adminReal !== '' && hash_equals($adminReal, $sent)) {
            return true;
        }

        return false;
    }
}

if (!function_exists('pm_handle_request')) {
    function pm_handle_request(array $opts = []): void
    {
        header('X-Robots-Tag: noindex, nofollow, noarchive', true);
        header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');

        require_login();

        $pdo = isset($GLOBALS['pdo']) && $GLOBALS['pdo'] instanceof PDO ? $GLOBALS['pdo'] : db();
        $me  = isset($GLOBALS['me']) && is_array($GLOBALS['me']) ? $GLOBALS['me'] : current_user();

        $back = pm_safe_back_url((string)(
            $_POST['back'] ??
            $_GET['back'] ??
            ($_SERVER['HTTP_REFERER'] ?? '/')
        ));

        if (!pm_is_post()) {
            http_response_code(405);
            echo 'Method Not Allowed';
            exit;
        }

        if (!pm_verify_csrf_request()) {
            pm_flash_set('err', 'Bad CSRF token.');
            pm_redirect($back);
        }

        $defaultAction = (string)($opts['default_action'] ?? '');
        $action = (string)($_POST['action'] ?? $_GET['action'] ?? $defaultAction);
        $action = trim($action);

        $postId = (int)($_POST['post_id'] ?? $_POST['id'] ?? $_GET['post_id'] ?? $_GET['id'] ?? 0);
        $attachmentId = (int)($_POST['attachment_id'] ?? $_POST['aid'] ?? $_GET['attachment_id'] ?? $_GET['aid'] ?? 0);

        try {
            if ($action === 'delete') {
                pm_delete_post_core($pdo, $me, $postId);
                pm_flash_set('ok', 'Post deleted.');
                pm_redirect($back);
            }

            if ($action === 'delete_attachment') {
                pm_delete_post_attachment_core($pdo, $me, $attachmentId);
                pm_flash_set('ok', 'Attachment deleted.');
                pm_redirect($back);
            }

            if ($action === 'edit') {
                $body = (string)($_POST['body'] ?? '');
                $deleteIds = pm_extract_delete_attachment_ids($_POST);

                $uploadField = null;
                foreach (['attachments', 'files', 'post_files'] as $f) {
                    if (isset($_FILES[$f])) {
                        $uploadField = $f;
                        break;
                    }
                }

                $newUploads = $uploadField !== null ? pm_handle_post_uploads($_FILES[$uploadField], $postId) : [];

                $pollQuestion = array_key_exists('poll_question', $_POST) ? (string)$_POST['poll_question'] : null;
                $pollOptions = array_key_exists('poll_options', $_POST) ? (array)$_POST['poll_options'] : null;

                pm_edit_post_core($pdo, $me, $postId, $body, $deleteIds, $newUploads, $pollQuestion, $pollOptions);
                pm_flash_set('ok', 'Post updated.');
                pm_redirect($back);
            }

            throw new RuntimeException('Unknown action.');
        } catch (Throwable $e) {
            pm_flash_set('err', $e->getMessage());
            pm_redirect($back);
        }
    }
}

$__pmDirect = realpath((string)($_SERVER['SCRIPT_FILENAME'] ?? ''));
if ($__pmDirect !== false && $__pmDirect === __FILE__) {
    pm_handle_request();
}