<?php
declare(strict_types=1);

/**
 * /messages/send.php
 * Відповідає за:
 * - ЄДИНУ логіку надсилання приватного повідомлення
 * - створення треду ТІЛЬКИ в момент реального send
 * - текстові повідомлення
 * - повідомлення з вкладеннями
 * - redirect назад у діалог або JSON для AJAX
 * - email-сповіщення про нове приватне повідомлення (після commit)
 *
 * БЕЗПЕКА (посилено):
 * - whitelist дозволених вкладень
 * - заборона HTML/SVG/XML/JS та інших активних типів
 * - перевірка MIME через finfo
 * - ліміт кількості файлів у повідомленні
 * - ліміт сумарного розміру вкладень у повідомленні
 */

require_once __DIR__ . '/../includes/start.php';

$notificationsLib = __DIR__ . '/../functional/notifications/_lib.php';
if (is_file($notificationsLib)) {
    require_once $notificationsLib;
}

$notificationsEvents = __DIR__ . '/../functional/notifications/events.php';
if (is_file($notificationsEvents)) {
    require_once $notificationsEvents;
}

if (function_exists('require_login')) {
    require_login();
} else {
    $meTmp = function_exists('current_user') ? current_user() : null;
    if (!$meTmp) {
        header('Location: /login.php');
        exit;
    }
}

$pdo = db();
$me  = current_user();

$meId = (int)($me['id'] ?? 0);
if ($meId <= 0) {
    header('Location: /login.php');
    exit;
}

if (!function_exists('pm_is_ajax_request')) {
    function pm_is_ajax_request(): bool
    {
        $xrw = strtolower(trim((string)($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '')));
        if ($xrw === 'xmlhttprequest') return true;

        $accept = strtolower((string)($_SERVER['HTTP_ACCEPT'] ?? ''));
        return str_contains($accept, 'application/json');
    }
}

if (!function_exists('pm_json_out')) {
    function pm_json_out(array $data, int $code = 200): void
    {
        http_response_code($code);
        header('Content-Type: application/json; charset=utf-8');
        header('Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0');
        header('Pragma: no-cache');
        header('Expires: 0');
        echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }
}

if (!function_exists('pm_safe_local_url')) {
    function pm_safe_local_url(string $url, string $fallback = '/messages/'): string
    {
        $url = trim($url);
        if ($url === '') return $fallback;

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

        if ($url === '' || $url[0] !== '/' || str_starts_with($url, '//')) {
            return $fallback;
        }

        return $url;
    }
}

if (!function_exists('pm_redirect')) {
    function pm_redirect(string $url): void
    {
        header('Location: ' . $url, true, 303);
        exit;
    }
}

if (!function_exists('pm_flash_err')) {
    function pm_flash_err(string $msg): void
    {
        if (!isset($_SESSION)) return;
        if (!isset($_SESSION['_flash']) || !is_array($_SESSION['_flash'])) {
            $_SESSION['_flash'] = [];
        }
        $_SESSION['_flash']['err'] = $msg;

        if (!isset($_SESSION['flash']) || !is_array($_SESSION['flash'])) {
            $_SESSION['flash'] = [];
        }
        $_SESSION['flash']['err'] = $msg;
        $_SESSION['flash']['error'] = $msg;
    }
}

if (!function_exists('pm_table_exists')) {
    function pm_table_exists(PDO $pdo, string $table): bool
    {
        try {
            $st = $pdo->prepare("
                SELECT 1
                FROM information_schema.tables
                WHERE table_schema = DATABASE()
                  AND table_name = ?
                LIMIT 1
            ");
            $st->execute([$table]);
            return (bool)$st->fetchColumn();
        } catch (Throwable $e) {
            return false;
        }
    }
}

if (!function_exists('pm_csrf_ok')) {
    function pm_csrf_ok(): bool
    {
        $token = trim((string)($_POST['csrf'] ?? ''));
        if ($token === '') return false;

        if (function_exists('csrf_check')) {
            try {
                if (csrf_check($token)) return true;
            } catch (Throwable $e) {}
        }

        if (function_exists('csrf_token')) {
            try {
                $cur = (string)csrf_token();
                if ($cur !== '' && hash_equals($cur, $token)) return true;
            } catch (Throwable $e) {}
        }

        return false;
    }
}

if (!function_exists('pm_user_by_username')) {
    function pm_user_by_username(PDO $pdo, string $username): ?array
    {
        $username = trim($username);
        if ($username === '') return null;

        $st = $pdo->prepare("SELECT id, username FROM users WHERE username = ? LIMIT 1");
        $st->execute([$username]);
        $row = $st->fetch(PDO::FETCH_ASSOC);
        return $row ?: null;
    }
}

if (!function_exists('pm_thread_by_id')) {
    function pm_thread_by_id(PDO $pdo, int $threadId): ?array
    {
        $st = $pdo->prepare("SELECT * FROM message_threads WHERE id = ? LIMIT 1");
        $st->execute([$threadId]);
        $row = $st->fetch(PDO::FETCH_ASSOC);
        return $row ?: null;
    }
}

if (!function_exists('pm_find_thread')) {
    function pm_find_thread(PDO $pdo, int $meId, int $peerId): ?array
    {
        $a = min($meId, $peerId);
        $b = max($meId, $peerId);

        $st = $pdo->prepare("SELECT * FROM message_threads WHERE user1_id = ? AND user2_id = ? LIMIT 1");
        $st->execute([$a, $b]);
        $row = $st->fetch(PDO::FETCH_ASSOC);
        return $row ?: null;
    }
}

if (!function_exists('pm_find_or_create_thread')) {
    function pm_find_or_create_thread(PDO $pdo, int $meId, int $peerId): array
    {
        $row = pm_find_thread($pdo, $meId, $peerId);
        if ($row) return $row;

        $a = min($meId, $peerId);
        $b = max($meId, $peerId);

        $st = $pdo->prepare("
            INSERT INTO message_threads (user1_id, user2_id, created_at, updated_at)
            VALUES (?, ?, NOW(), NOW())
            ON DUPLICATE KEY UPDATE
              id = LAST_INSERT_ID(id),
              updated_at = updated_at
        ");
        $st->execute([$a, $b]);

        $threadId = (int)$pdo->lastInsertId();
        if ($threadId <= 0) {
            $row = pm_find_thread($pdo, $meId, $peerId);
            if ($row) return $row;
            throw new RuntimeException('Не вдалося створити діалог.');
        }

        $thread = pm_thread_by_id($pdo, $threadId);
        if (!$thread) {
            throw new RuntimeException('Не вдалося завантажити діалог.');
        }

        return $thread;
    }
}

if (!function_exists('pm_set_sender_read_at')) {
    function pm_set_sender_read_at(PDO $pdo, array $thread, int $meId): void
    {
        $isU1 = ((int)($thread['user1_id'] ?? 0) === $meId);
        $col = $isU1 ? 'user1_last_read_at' : 'user2_last_read_at';

        $st = $pdo->prepare("UPDATE message_threads SET {$col} = NOW() WHERE id = ?");
        $st->execute([(int)$thread['id']]);
    }
}

if (!function_exists('pm_ensure_dir')) {
    function pm_ensure_dir(string $dir): void
    {
        if (!is_dir($dir)) {
            if (!mkdir($dir, 0775, true) && !is_dir($dir)) {
                throw new RuntimeException('Не вдалося створити папку.');
            }
        }
    }
}

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

if (!function_exists('pm_dangerous_upload_exts')) {
    function pm_dangerous_upload_exts(): array
    {
        return [
            'php','phtml','php3','php4','php5','php7','php8','phar',
            'cgi','pl','py','rb','sh','bat','cmd','exe','com','dll','msi','apk',
            'js','mjs','cjs','jar','hta',
            'html','htm','xhtml','shtml','mhtml',
            'svg','svgz','xml','xsl','xslt',
            'htaccess','ini','env','conf','yml','yaml','json',
        ];
    }
}

if (!function_exists('pm_is_dangerous_ext')) {
    function pm_is_dangerous_ext(string $name): bool
    {
        $name = strtolower(trim($name));
        $ext = strtolower((string)pathinfo($name, PATHINFO_EXTENSION));
        return $ext !== '' && in_array($ext, pm_dangerous_upload_exts(), true);
    }
}

if (!function_exists('pm_is_allowed_ext')) {
    function pm_is_allowed_ext(string $name): bool
    {
        $name = strtolower(trim($name));
        $ext = strtolower((string)pathinfo($name, PATHINFO_EXTENSION));
        return $ext !== '' && in_array($ext, pm_allowed_upload_exts(), true);
    }
}

if (!function_exists('pm_guess_mime')) {
    function pm_guess_mime(string $tmp): string
    {
        if (function_exists('finfo_open')) {
            $f = finfo_open(FILEINFO_MIME_TYPE);
            if ($f) {
                $m = finfo_file($f, $tmp) ?: '';
                finfo_close($f);
                return strtolower(trim((string)$m));
            }
        }
        return '';
    }
}

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

        $badExact = [
            'text/html',
            'application/xhtml+xml',
            'image/svg+xml',
            'text/xml',
            'application/xml',
            'application/javascript',
            'text/javascript',
            'application/x-javascript',
            'text/x-php',
            'application/x-httpd-php',
        ];

        if (in_array($mime, $badExact, true)) {
            return true;
        }

        return str_starts_with($mime, 'application/x-php');
    }
}

if (!function_exists('pm_clean_original_filename')) {
    function pm_clean_original_filename(string $name): string
    {
        $name = preg_replace('/[\x00-\x1F\x7F]+/u', '', $name);
        $name = str_replace(["\\", '/', "\r", "\n", "\t"], '_', (string)$name);
        $name = trim((string)$name, " ._-");
        return $name !== '' ? $name : 'file';
    }
}

if (!function_exists('pm_upload_accept_attr')) {
    function pm_upload_accept_attr(): string
    {
        return '.jpg,.jpeg,.png,.gif,.webp,.mp3,.ogg,.wav,.m4a,.aac,.flac,.mp4,.webm,.mov,.m4v,.pdf,.txt,.rtf,.csv,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.odt,.ods,.odp,.zip,.rar,.7z';
    }
}

/* ===== guards ===== */

if (!pm_table_exists($pdo, 'message_threads') || !pm_table_exists($pdo, 'message_messages')) {
    $err = 'Схема приватних повідомлень не знайдена.';
    if (pm_is_ajax_request()) {
        pm_json_out(['ok' => false, 'err' => 'schema'], 500);
    }
    pm_flash_err($err);
    pm_redirect('/messages/');
}

$body = trim((string)($_POST['body'] ?? ($_POST['message'] ?? ($_POST['text'] ?? ''))));

$threadId = (int)($_POST['thread_id'] ?? ($_POST['dialog_id'] ?? ($_POST['chat_id'] ?? 0)));
$toUserId = (int)($_POST['to_user_id'] ?? ($_POST['user_id'] ?? ($_POST['to_id'] ?? 0)));
$toUsername = trim((string)($_POST['to_username'] ?? ($_POST['username'] ?? '')));

$back = trim((string)($_POST['back'] ?? ''));
if ($back === '' && isset($_SERVER['HTTP_REFERER'])) {
    $back = (string)$_SERVER['HTTP_REFERER'];
}
$back = pm_safe_local_url($back, '/messages/');

$files = $_FILES['files'] ?? null;
$hasFiles = is_array($files)
    && isset($files['name'])
    && is_array($files['name'])
    && count(array_filter($files['name'], static fn($x) => trim((string)$x) !== '')) > 0;

if (!pm_csrf_ok()) {
    if (pm_is_ajax_request()) {
        pm_json_out(['ok' => false, 'err' => 'csrf'], 403);
    }
    pm_flash_err('Помилка безпеки (CSRF). Оновіть сторінку і спробуйте ще раз.');
    pm_redirect($back);
}

$pmRestrictionState = function_exists('ar_get_state') ? ar_get_state($pdo, $meId) : [];
if (is_array($pmRestrictionState) && function_exists('ar_is_action_blocked') && ar_is_action_blocked($pmRestrictionState, 'messages')) {
    $pmBlockMsg = function_exists('ar_action_error')
        ? ar_action_error($pmRestrictionState, 'messages')
        : 'Надсилання приватних повідомлень тимчасово заборонено.';

    if (pm_is_ajax_request()) {
        pm_json_out(['ok' => false, 'err' => 'restricted', 'message' => $pmBlockMsg], 403);
    }
    pm_flash_err($pmBlockMsg);
    pm_redirect($back);
}

if ($body === '' && !$hasFiles) {
    if (pm_is_ajax_request()) {
        pm_json_out(['ok' => false, 'err' => 'empty'], 400);
    }
    pm_flash_err('Порожнє повідомлення.');
    pm_redirect($back);
}

try {
    $thread = null;
    $peerId = 0;

    if ($threadId > 0) {
        $thread = pm_thread_by_id($pdo, $threadId);

        if (!$thread) {
            if (pm_is_ajax_request()) {
                pm_json_out(['ok' => false, 'err' => 'thread_not_found'], 404);
            }
            pm_flash_err('Діалог не знайдено.');
            pm_redirect('/messages/');
        }

        $u1 = (int)($thread['user1_id'] ?? 0);
        $u2 = (int)($thread['user2_id'] ?? 0);

        if ($u1 !== $meId && $u2 !== $meId) {
            if (pm_is_ajax_request()) {
                pm_json_out(['ok' => false, 'err' => 'forbidden'], 403);
            }
            pm_flash_err('Немає доступу до цього діалогу.');
            pm_redirect('/messages/');
        }

        $peerId = ($u1 === $meId) ? $u2 : $u1;
    } else {
        if ($toUserId <= 0 && $toUsername !== '') {
            $peer = pm_user_by_username($pdo, $toUsername);
            $toUserId = (int)($peer['id'] ?? 0);
        }

        if ($toUserId <= 0) {
            if (pm_is_ajax_request()) {
                pm_json_out(['ok' => false, 'err' => 'bad_to'], 400);
            }
            pm_flash_err('Не вдалося визначити отримувача.');
            pm_redirect($back);
        }

        if ($toUserId === $meId) {
            if (pm_is_ajax_request()) {
                pm_json_out(['ok' => false, 'err' => 'self'], 400);
            }
            pm_flash_err('Не можна писати самому собі.');
            pm_redirect($back);
        }

        $peerId = $toUserId;
    }

    $projectRoot = dirname(__DIR__);

    $pdo->beginTransaction();

    if (!$thread) {
        $thread = pm_find_or_create_thread($pdo, $meId, $peerId);
        $threadId = (int)($thread['id'] ?? 0);
    }

    if ($threadId <= 0) {
        throw new RuntimeException('Не вдалося визначити діалог.');
    }

    $thread = pm_thread_by_id($pdo, $threadId) ?: $thread;

    if ((int)($thread['is_closed'] ?? 0) === 1) {
        throw new RuntimeException('Розмову завершено. Надсилання повідомлень заборонено.');
    }

    $st = $pdo->prepare("
        INSERT INTO message_messages (
            thread_id,
            sender_id,
            msg_type,
            body,
            has_attachments,
            created_at
        ) VALUES (?, ?, 'user', ?, ?, NOW())
    ");
    $st->execute([$threadId, $meId, $body, $hasFiles ? 1 : 0]);

    $messageId = (int)$pdo->lastInsertId();
    if ($messageId <= 0) {
        throw new RuntimeException('Не вдалося зберегти повідомлення.');
    }

    $savedAny = false;

    if ($hasFiles) {
        $maxBytesPerFile = 100 * 1024 * 1024;
        $maxFilesPerMessage = 10;
        $maxBytesPerMessage = 200 * 1024 * 1024;

        $usernameSafe = preg_replace('/[^a-zA-Z0-9_\-]+/', '_', (string)($me['username'] ?? ('u' . $meId)));
        $usernameSafe = trim((string)$usernameSafe, '_');
        if ($usernameSafe === '') $usernameSafe = 'u' . $meId;

        $baseRel = 'upload/messages/' . $usernameSafe;
        $ym = date('Y') . '/' . date('m');
        $targetRelDir = $baseRel . '/' . $ym;
        $targetAbsDir = $projectRoot . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $targetRelDir);

        pm_ensure_dir($projectRoot . DIRECTORY_SEPARATOR . 'upload');
        pm_ensure_dir($projectRoot . DIRECTORY_SEPARATOR . 'upload' . DIRECTORY_SEPARATOR . 'messages');
        pm_ensure_dir($projectRoot . DIRECTORY_SEPARATOR . 'upload' . DIRECTORY_SEPARATOR . 'messages' . DIRECTORY_SEPARATOR . $usernameSafe);
        pm_ensure_dir($targetAbsDir);

        $nonEmptyIndexes = [];
        $totalBytes = 0;
        $rawCount = count($files['name']);
        for ($i = 0; $i < $rawCount; $i++) {
            $orig = (string)($files['name'][$i] ?? '');
            $err  = (int)($files['error'][$i] ?? UPLOAD_ERR_NO_FILE);
            if ($orig === '' || $err === UPLOAD_ERR_NO_FILE) {
                continue;
            }
            $nonEmptyIndexes[] = $i;
            $totalBytes += max(0, (int)($files['size'][$i] ?? 0));
        }

        if (count($nonEmptyIndexes) > $maxFilesPerMessage) {
            throw new RuntimeException('Можна прикріпити не більше 10 файлів за одне повідомлення.');
        }

        if ($totalBytes > $maxBytesPerMessage) {
            throw new RuntimeException('Сумарний розмір вкладень завеликий. Ліміт 200 MB на повідомлення.');
        }

        foreach ($nonEmptyIndexes as $i) {
            $orig = pm_clean_original_filename((string)($files['name'][$i] ?? 'file'));
            $tmp  = (string)($files['tmp_name'][$i] ?? '');
            $err  = (int)($files['error'][$i] ?? UPLOAD_ERR_NO_FILE);
            $size = (int)($files['size'][$i] ?? 0);

            if ($err !== UPLOAD_ERR_OK || $tmp === '') {
                throw new RuntimeException('Помилка завантаження файлу.');
            }

            if (!is_uploaded_file($tmp)) {
                throw new RuntimeException('Некоректне джерело файлу.');
            }

            if ($size <= 0) {
                throw new RuntimeException('Порожній файл заборонено.');
            }

            if ($size > $maxBytesPerFile) {
                throw new RuntimeException('Файл завеликий. Ліміт 100 MB.');
            }

            if (pm_is_dangerous_ext($orig)) {
                throw new RuntimeException('Цей тип файлу заборонений.');
            }

            if (!pm_is_allowed_ext($orig)) {
                throw new RuntimeException('Цей тип файлу не дозволений.');
            }

            $detectedMime = pm_guess_mime($tmp);
            if (pm_is_dangerous_mime($detectedMime)) {
                throw new RuntimeException('Небезпечний тип вмісту файлу заборонений.');
            }

            $ext = strtolower((string)pathinfo($orig, PATHINFO_EXTENSION));
            $ext = preg_replace('/[^a-z0-9]+/', '', $ext);
            $ext = $ext !== '' ? ('.' . $ext) : '';

            $stored = date('Ymd_His') . '_' . bin2hex(random_bytes(6)) . $ext;
            $abs = $targetAbsDir . DIRECTORY_SEPARATOR . $stored;

            if (!move_uploaded_file($tmp, $abs)) {
                throw new RuntimeException('Помилка завантаження файлу.');
            }

            $mime = pm_guess_mime($abs);
            if (pm_is_dangerous_mime($mime)) {
                @unlink($abs);
                throw new RuntimeException('Небезпечний тип вмісту файлу заборонений.');
            }
            if ($mime === '') {
                $mime = 'application/octet-stream';
            }

            $relPath = $targetRelDir . '/' . $stored;

            $pdo->prepare("
                INSERT INTO message_attachments
                    (message_id, rel_path, stored_name, original_name, mime, size_bytes, created_at)
                VALUES (?, ?, ?, ?, ?, ?, NOW())
            ")->execute([$messageId, $relPath, $stored, $orig, $mime, $size]);

            $savedAny = true;
        }
    }

    if ($hasFiles && !$savedAny) {
        $pdo->prepare("UPDATE message_messages SET has_attachments = 0 WHERE id = ?")
            ->execute([$messageId]);
    }

    $pdo->prepare("
        UPDATE message_threads
        SET last_message_id = ?,
            last_message_at = NOW(),
            updated_at = NOW()
        WHERE id = ?
    ")->execute([$messageId, $threadId]);

    pm_set_sender_read_at($pdo, $thread, $meId);

    $pdo->commit();

    try {
        if (function_exists('ntf_dispatch_private_message')) {
            ntf_dispatch_private_message($pdo, $messageId, $threadId, $meId, $peerId);
        }
    } catch (Throwable $e) {
        error_log('[messages/send.php notify] ' . $e->getMessage());
    }

    $redirectUrl = '/messages/dialog.php?t=' . $threadId;

    if (pm_is_ajax_request()) {
        pm_json_out([
            'ok' => true,
            'id' => $messageId,
            'thread_id' => $threadId,
            'redirect' => $redirectUrl,
        ]);
    }

    pm_redirect($redirectUrl);

} catch (Throwable $e) {

    try {
        if ($pdo->inTransaction()) {
            $pdo->rollBack();
        }
    } catch (Throwable $e2) {}

    if (pm_is_ajax_request()) {
        pm_json_out([
            'ok' => false,
            'err' => 'server',
            'msg' => $e->getMessage(),
        ], 500);
    }

    pm_flash_err($e->getMessage() !== '' ? $e->getMessage() : 'Помилка при надсиланні повідомлення.');
    pm_redirect($back);
}
