<?php

declare(strict_types=1);

ob_start();
require_once __DIR__ . '/includes/bootstrap.php';

set_error_handler(static function (int $severity, string $message, string $file, int $line): bool {
    if (!(error_reporting() & $severity)) {
        return false;
    }
    throw new ErrorException($message, 0, $severity, $file, $line);
});

register_shutdown_function(static function (): void {
    $error = error_get_last();
    if ($error && in_array((int) ($error['type'] ?? 0), [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
        try {
            if (function_exists('log_message')) {
                log_message('send-fatal.log', sprintf('%s @ %s:%d', (string) ($error['message'] ?? 'Fatal error'), (string) ($error['file'] ?? __FILE__), (int) ($error['line'] ?? 0)));
            }
        } catch (Throwable $ignored) {
        }
        while (ob_get_level() > 0) {
            @ob_end_clean();
        }
        if (!headers_sent()) {
            http_response_code(500);
            header('Content-Type: application/json; charset=utf-8');
            header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
        }
        echo json_encode([
            'ok' => false,
            'message' => 'Ошибка сервера при обработке заявки. Попробуйте ещё раз.',
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
});

function send_json(array $payload, int $status = 200): void
{
    while (ob_get_level() > 0) {
        @ob_end_clean();
    }
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
    header('X-Content-Type-Options: nosniff');
    echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}

function booking_validation_error(string $field, string $message, int $status = 422): void
{
    send_json([
        'ok' => false,
        'message' => $message,
        'fields' => [$field => $message],
    ], $status);
}

function log_noncritical(string $message): void
{
    try {
        if (function_exists('log_message')) {
            log_message('send-noncritical.log', $message);
        }
    } catch (Throwable $ignored) {
    }
}

function format_booking_date_ru(string $ymd): string
{
    try {
        return (new DateTimeImmutable($ymd))->format('d.m.Y');
    } catch (Throwable $e) {
        return $ymd;
    }
}

function apply_template_vars(string $template, array $vars): string
{
    $result = preg_replace_callback('/\{(\w+)\}/', static function (array $m) use ($vars): string {
        return (string) ($vars[$m[1]] ?? '');
    }, $template);

    return is_string($result) ? $result : $template;
}

function safe_random_suffix(int $bytes = 3): string
{
    try {
        return substr(bin2hex(random_bytes($bytes)), 0, $bytes * 2);
    } catch (Throwable $e) {
        return substr(md5(uniqid((string) mt_rand(), true)), 0, $bytes * 2);
    }
}

function safe_country_for_ip(string $ip): string
{
    try {
        if (function_exists('geoip_lookup')) {
            $geo = geoip_lookup($ip, true);
            if (!empty($geo['country'])) {
                return (string) $geo['country'];
            }
            if (!empty($geo['country_name'])) {
                return (string) $geo['country_name'];
            }
            if (!empty($geo['label'])) {
                return (string) $geo['label'];
            }
        }
    } catch (Throwable $e) {
        log_noncritical('geoip_lookup failed: ' . $e->getMessage());
    }

    try {
        if (function_exists('detect_country_info')) {
            $info = detect_country_info();
            if (!empty($info['label'])) {
                return (string) $info['label'];
            }
            if (!empty($info['country'])) {
                return (string) $info['country'];
            }
        }
    } catch (Throwable $e) {
        log_noncritical('detect_country_info failed: ' . $e->getMessage());
    }

    return '—';
}

function safe_visits_by_ip(string $ip): int
{
    try {
        if (function_exists('visitor_visits_by_ip')) {
            return max(0, (int) visitor_visits_by_ip($ip));
        }
    } catch (Throwable $e) {
        log_noncritical('visitor_visits_by_ip failed: ' . $e->getMessage());
    }

    return 0;
}

function safe_create_lead_record(array $lead): bool
{
    try {
        if (function_exists('create_lead')) {
            create_lead($lead);
            return true;
        }
    } catch (Throwable $e) {
        log_noncritical('create_lead failed: ' . $e->getMessage());
    }

    try {
        $path = DATA_DIR . '/crm/leads.json';
        $data = function_exists('read_json_file') ? read_json_file($path, ['items' => []]) : ['items' => []];
        $items = is_array($data['items'] ?? null) ? $data['items'] : [];
        $lead['id'] = $lead['id'] ?? ('lead_' . date('Ymd_His') . '_' . safe_random_suffix(3));
        $lead['created_at'] = $lead['created_at'] ?? date('c');
        $lead['updated_at'] = date('c');
        array_unshift($items, $lead);
        $data['items'] = $items;
        if (function_exists('write_json_file')) {
            return (bool) write_json_file($path, $data);
        }
        return (bool) @file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
    } catch (Throwable $e) {
        try {
            if (function_exists('log_message')) {
                log_message('crm-error.log', 'Lead save fatal: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
            }
        } catch (Throwable $ignored) {
        }
    }

    return false;
}

function safe_track_booking_activity(string $pageSlug, string $house, string $checkin, string $checkout): void
{
    try {
        if (function_exists('track_activity')) {
            track_activity('booking_submit', [
                'page' => $pageSlug,
                'details' => trim($house . ' ' . $checkin . '—' . $checkout),
            ]);
        }
    } catch (Throwable $e) {
        log_noncritical('track_activity failed: ' . $e->getMessage());
    }
}

function same_origin_request(): bool
{
    $normalizeHost = static function (string $value): string {
        $value = strtolower(trim($value));
        $host = strtolower((string)(parse_url(strpos($value, '://') !== false ? $value : 'http://' . $value, PHP_URL_HOST) ?: $value));
        $host = rtrim($host, '.');
        return strpos($host, 'www.') === 0 ? substr($host, 4) : $host;
    };

    $allowedHosts = [];
    $requestHost = $normalizeHost((string)($_SERVER['HTTP_HOST'] ?? ''));
    if ($requestHost !== '') {
        $allowedHosts[$requestHost] = true;
    }
    if (defined('APP_URL') && APP_URL !== '') {
        $appHost = $normalizeHost((string)APP_URL);
        if ($appHost !== '') {
            $allowedHosts[$appHost] = true;
        }
    }
    if (!$allowedHosts) {
        return false;
    }

    $checkedHeader = false;
    foreach (['HTTP_ORIGIN', 'HTTP_REFERER'] as $key) {
        $value = trim((string)($_SERVER[$key] ?? ''));
        if ($value === '' || strtolower($value) === 'null') {
            continue;
        }
        $checkedHeader = true;
        $originHost = $normalizeHost($value);
        if ($originHost !== '' && isset($allowedHosts[$originHost])) {
            return true;
        }
    }

    // Some privacy modes, mobile browsers and blockers omit both headers.
    // CSRF and the signed, session-bound form token still protect the endpoint.
    return !$checkedHeader;
}

function telegram_result_from_response($responseBody, int $httpCode, string $error): array
{
    $decoded = is_string($responseBody) ? json_decode($responseBody, true) : null;
    $ok = is_array($decoded) ? (bool) ($decoded['ok'] ?? false) : false;
    $description = is_array($decoded) ? (string) ($decoded['description'] ?? '') : '';

    return [
        'ok' => $ok,
        'http_code' => $httpCode,
        'error' => $error,
        'description' => $description,
        'body' => is_string($responseBody) ? $responseBody : '',
    ];
}

function telegram_api_request(string $botToken, array $payload, bool $jsonPayload = true, bool $verifyPeer = true): array
{
    $url = 'https://api.telegram.org/bot' . $botToken . '/sendMessage';
    $timeout = defined('FORM_TELEGRAM_TIMEOUT') ? max(8, (int) FORM_TELEGRAM_TIMEOUT) : 15;
    $responseBody = false;
    $error = '';
    $httpCode = 0;

    if (function_exists('curl_init')) {
        $ch = curl_init($url);
        $body = $jsonPayload
            ? json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
            : http_build_query($payload);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $body,
            CURLOPT_HTTPHEADER => [$jsonPayload ? 'Content-Type: application/json' : 'Content-Type: application/x-www-form-urlencoded'],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => $timeout,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => $verifyPeer,
            CURLOPT_SSL_VERIFYHOST => $verifyPeer ? 2 : 0,
            CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
            CURLOPT_USERAGENT => 'feyaframe-com-booking-form/1.1',
        ]);
        $responseBody = curl_exec($ch);
        $error = (string) curl_error($ch);
        $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        return telegram_result_from_response($responseBody, $httpCode, $error);
    }

    $context = stream_context_create([
        'http' => [
            'method' => 'POST',
            'header' => ($jsonPayload ? "Content-Type: application/json
" : "Content-Type: application/x-www-form-urlencoded
") . "User-Agent: feyaframe-com-booking-form/1.1
",
            'content' => $jsonPayload ? json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : http_build_query($payload),
            'timeout' => $timeout,
            'ignore_errors' => true,
        ],
    ]);
    $responseBody = @file_get_contents($url, false, $context);
    if (isset($http_response_header) && is_array($http_response_header)) {
        foreach ($http_response_header as $headerLine) {
            if (preg_match('~^HTTP/\S+\s+(\d{3})~', $headerLine, $m)) {
                $httpCode = (int) $m[1];
                break;
            }
        }
    }
    if ($responseBody === false) {
        $error = 'file_get_contents failed';
    }

    return telegram_result_from_response($responseBody, $httpCode, $error);
}

function split_telegram_message(string $message, int $limit = 3900): array
{
    $lengthOf = static fn(string $value): int => function_exists('mb_strlen') ? mb_strlen($value, 'UTF-8') : strlen($value);
    $slice = static fn(string $value, int $offset, int $length): string => function_exists('mb_substr')
        ? mb_substr($value, $offset, $length, 'UTF-8')
        : substr($value, $offset, $length);

    if ($lengthOf($message) <= $limit) {
        return [$message];
    }

    $chunks = [];
    $current = '';
    foreach (preg_split('/\R/u', $message) ?: [$message] as $line) {
        $lineParts = [];
        if ($lengthOf($line) <= $limit) {
            $lineParts[] = $line;
        } else {
            for ($offset = 0, $lineLength = $lengthOf($line); $offset < $lineLength; $offset += $limit) {
                $lineParts[] = $slice($line, $offset, $limit);
            }
        }

        foreach ($lineParts as $part) {
            $candidate = $current === '' ? $part : $current . "\n" . $part;
            if ($lengthOf($candidate) > $limit && $current !== '') {
                $chunks[] = $current;
                $current = $part;
            } else {
                $current = $candidate;
            }
        }
    }
    if ($current !== '') {
        $chunks[] = $current;
    }

    return $chunks ?: [$message];
}

function send_telegram_message(string $botToken, string $chatId, string $message): array
{
    $lastResult = [
        'ok' => false,
        'http_code' => 0,
        'error' => 'not sent',
        'description' => '',
        'body' => '',
    ];

    foreach (split_telegram_message($message) as $index => $chunk) {
        $payload = [
            'chat_id' => $chatId,
            'text' => $chunk,
            'disable_web_page_preview' => true,
        ];

        $attempts = [
            [true, true],
            [false, true],
        ];

        foreach ($attempts as $attempt) {
            [$asJson, $verifyPeer] = $attempt;
            $lastResult = telegram_api_request($botToken, $payload, $asJson, $verifyPeer);
            if (!empty($lastResult['ok']) && (int)($lastResult['http_code'] ?? 0) < 400) {
                continue 2;
            }
            usleep(250000);
        }

        $lastResult['description'] = trim((string)($lastResult['description'] ?? '') . ' chunk=' . ($index + 1));
        return $lastResult;
    }

    return ['ok' => true, 'http_code' => 200, 'error' => '', 'description' => '', 'body' => ''];
}


function booking_security_ip(): string
{
    $cfIp = trim((string)($_SERVER['HTTP_CF_CONNECTING_IP'] ?? ''));
    if ($cfIp !== '' && filter_var($cfIp, FILTER_VALIDATE_IP)) {
        return $cfIp;
    }
    $remoteIp = trim((string)($_SERVER['REMOTE_ADDR'] ?? ''));
    if ($remoteIp !== '' && filter_var($remoteIp, FILTER_VALIDATE_IP)) {
        return $remoteIp;
    }
    return (string)client_ip();
}

function booking_security_path(): string
{
    return DATA_DIR . '/security/booking-submissions.json';
}

function booking_security_lock_path(): string
{
    return DATA_DIR . '/security/booking-submissions.lock';
}

function booking_security_prune(array $data): array
{
    $now = time();
    $attemptSince = $now - 600;
    $successSince = $now - 600;
    $duplicateSince = $now - 86400;

    foreach ((array)($data['attempts'] ?? []) as $key => $timestamps) {
        $kept = array_values(array_filter((array)$timestamps, static fn($ts): bool => (int)$ts >= $attemptSince));
        if ($kept) $data['attempts'][$key] = $kept;
        else unset($data['attempts'][$key]);
    }
    foreach ((array)($data['successes'] ?? []) as $key => $timestamps) {
        $kept = array_values(array_filter((array)$timestamps, static fn($ts): bool => (int)$ts >= $successSince));
        if ($kept) $data['successes'][$key] = $kept;
        else unset($data['successes'][$key]);
    }
    foreach (['fingerprints', 'tokens'] as $bucket) {
        foreach ((array)($data[$bucket] ?? []) as $key => $row) {
            $row = is_array($row) ? $row : ['time' => (int)$row, 'status' => 'sent'];
            $time = (int)($row['time'] ?? 0);
            $status = (string)($row['status'] ?? 'sent');
            if ($status === 'pending') {
                $maxAge = 120;
            } elseif ($bucket === 'fingerprints') {
                $maxAge = defined('FORM_DUPLICATE_WINDOW_SECONDS') ? max(60, (int)FORM_DUPLICATE_WINDOW_SECONDS) : 900;
            } else {
                $maxAge = 86400;
            }
            if ($time < ($now - $maxAge) || $time < $duplicateSince) {
                unset($data[$bucket][$key]);
            }
        }
    }

    return array_merge([
        'attempts' => [],
        'successes' => [],
        'fingerprints' => [],
        'tokens' => [],
    ], $data);
}

function booking_security_update(callable $callback)
{
    ensure_dir(dirname(booking_security_path()));
    $lock = @fopen(booking_security_lock_path(), 'c+');
    if (!is_resource($lock)) {
        throw new RuntimeException('Unable to open booking security lock');
    }
    if (!flock($lock, LOCK_EX)) {
        fclose($lock);
        throw new RuntimeException('Unable to lock booking security registry');
    }

    try {
        $data = booking_security_prune(read_json_file(booking_security_path(), []));
        $result = $callback($data);
        if (!write_json_file(booking_security_path(), $data)) {
            throw new RuntimeException('Unable to save booking security registry');
        }
        return $result;
    } finally {
        flock($lock, LOCK_UN);
        fclose($lock);
    }
}

function booking_record_attempt(string $ip): int
{
    $ipKey = hash('sha256', $ip);
    return (int)booking_security_update(static function (array &$data) use ($ipKey): int {
        $rows = array_values((array)($data['attempts'][$ipKey] ?? []));
        $rows[] = time();
        $data['attempts'][$ipKey] = array_slice($rows, -30);
        return count($data['attempts'][$ipKey]);
    });
}

function booking_reserve_submission(string $ip, string $fingerprint, string $formToken): array
{
    $ipKey = hash('sha256', $ip);
    $tokenKey = hash('sha256', $formToken);
    try {
        $reservation = bin2hex(random_bytes(16));
    } catch (Throwable $e) {
        $reservation = hash('sha256', uniqid((string)mt_rand(), true));
    }

    return (array)booking_security_update(static function (array &$data) use ($ipKey, $fingerprint, $tokenKey, $reservation): array {
        $now = time();
        if (isset($data['fingerprints'][$fingerprint]) || isset($data['tokens'][$tokenKey])) {
            return ['status' => 'duplicate', 'reservation' => ''];
        }
        $successes = array_values((array)($data['successes'][$ipKey] ?? []));
        $max = defined('FORM_MAX_REQUESTS_PER_10_MIN') ? max(2, (int)FORM_MAX_REQUESTS_PER_10_MIN) : 4;
        if (count($successes) >= $max) {
            return ['status' => 'rate_limited', 'reservation' => ''];
        }
        $row = ['time' => $now, 'status' => 'pending', 'reservation' => $reservation, 'ip_key' => $ipKey];
        $data['fingerprints'][$fingerprint] = $row;
        $data['tokens'][$tokenKey] = $row;
        return ['status' => 'reserved', 'reservation' => $reservation, 'token_key' => $tokenKey, 'ip_key' => $ipKey];
    });
}

function booking_finish_submission(string $fingerprint, string $formToken, string $reservation, string $ip, bool $success): void
{
    $tokenKey = hash('sha256', $formToken);
    $ipKey = hash('sha256', $ip);
    booking_security_update(static function (array &$data) use ($fingerprint, $tokenKey, $reservation, $ipKey, $success): bool {
        $fingerprintRow = is_array($data['fingerprints'][$fingerprint] ?? null) ? $data['fingerprints'][$fingerprint] : [];
        $tokenRow = is_array($data['tokens'][$tokenKey] ?? null) ? $data['tokens'][$tokenKey] : [];
        if (($fingerprintRow['reservation'] ?? '') !== $reservation || ($tokenRow['reservation'] ?? '') !== $reservation) {
            return false;
        }
        if (!$success) {
            unset($data['fingerprints'][$fingerprint], $data['tokens'][$tokenKey]);
            return true;
        }
        $now = time();
        $sentRow = ['time' => $now, 'status' => 'sent', 'reservation' => $reservation, 'ip_key' => $ipKey];
        $data['fingerprints'][$fingerprint] = $sentRow;
        $data['tokens'][$tokenKey] = $sentRow;
        $successes = array_values((array)($data['successes'][$ipKey] ?? []));
        $successes[] = $now;
        $data['successes'][$ipKey] = array_slice($successes, -20);
        return true;
    });
}

function text_length(string $value): int
{
    return function_exists('mb_strlen') ? mb_strlen($value, 'UTF-8') : strlen($value);
}

function valid_booking_name(string $name): bool
{
    if (text_length($name) < 2 || text_length($name) > 80 || preg_match('~https?://|www\\.|[<>]~iu', $name)) {
        return false;
    }
    preg_match_all('/\\p{L}/u', $name, $letters);
    return count($letters[0] ?? []) >= 2;
}

function normalized_phone(string $phone): string
{
    $digits = preg_replace('/\\D+/', '', $phone) ?? '';
    if (strlen($digits) === 11 && $digits[0] === '8') {
        $digits = '7' . substr($digits, 1);
    } elseif (strlen($digits) === 10) {
        $digits = '7' . $digits;
    }
    return $digits;
}

function valid_booking_phone(string $phone): bool
{
    $digits = normalized_phone($phone);
    if (strlen($digits) < 10 || strlen($digits) > 15) {
        return false;
    }
    return count(array_unique(str_split($digits))) > 1;
}

function strict_ymd(string $value): ?DateTimeImmutable
{
    $date = DateTimeImmutable::createFromFormat('!Y-m-d', $value);
    $errors = DateTimeImmutable::getLastErrors();
    if (!$date || ($errors !== false && ((int)$errors['warning_count'] > 0 || (int)$errors['error_count'] > 0)) || $date->format('Y-m-d') !== $value) {
        return null;
    }
    return $date;
}

function booking_response_payload(array $bookingSuccess, string $house, string $checkin, string $checkout, bool $duplicate = false): array
{
    $bookingPayload = [
        'house' => $house,
        'checkin' => $checkin,
        'checkout' => $checkout,
        'checkin_ru' => format_booking_date_ru($checkin),
        'checkout_ru' => format_booking_date_ru($checkout),
    ];
    $successText = apply_template_vars((string)($bookingSuccess['details_template'] ?? $bookingSuccess['text'] ?? ''), $bookingPayload);
    $redirectUrl = base_url('/booking-success.php') . '?' . http_build_query([
        'house' => $bookingPayload['house'],
        'checkin' => $bookingPayload['checkin'],
        'checkout' => $bookingPayload['checkout'],
    ], '', '&', PHP_QUERY_RFC3986);

    return [
        'ok' => true,
        'duplicate' => $duplicate,
        'message' => $duplicate ? 'Эта заявка уже отправлена. Повтор не создавался.' : (string)($bookingSuccess['text'] ?? 'Заявка отправлена. Скоро свяжемся с вами.'),
        'success_text' => $successText,
        'booking' => $bookingPayload,
        'redirect_url' => $redirectUrl,
        'return_url' => (string)($bookingSuccess['return_url'] ?? '/'),
    ];
}

try {
    if (request_method() !== 'POST') {
        send_json(['ok' => false, 'message' => 'Method not allowed'], 405);
    }

    if ((int)($_SERVER['CONTENT_LENGTH'] ?? 0) > 32768) {
        send_json(['ok' => false, 'message' => 'Слишком большой запрос.'], 413);
    }

    $csrfToken = is_string($_POST['csrf_token'] ?? null) ? (string)$_POST['csrf_token'] : '';
    if (!verify_csrf($csrfToken)) {
        send_json(['ok' => false, 'message' => 'Сессия формы устарела. Обновите страницу и попробуйте снова.'], 419);
    }
    if (!same_origin_request()) {
        send_json(['ok' => false, 'message' => 'Запрос отклонён системой безопасности.'], 403);
    }
    if (!empty($_POST['company'] ?? '') || !empty($_POST['fax_number'] ?? '')) {
        send_json(['ok' => true, 'duplicate' => true, 'filtered' => true, 'message' => 'Заявка принята.']);
    }

    $formIssuedAt = post_text('form_issued_at');
    $formToken = post_text('form_token');
    $formSignature = post_text('form_signature');
    $guardStatus = booking_form_guard_status($formIssuedAt, $formToken, $formSignature);
    if ($guardStatus === 'too_fast') {
        send_json(['ok' => false, 'message' => 'Форма отправлена слишком быстро. Проверьте данные и повторите через пару секунд.'], 429);
    }
    if ($guardStatus === 'expired') {
        send_json(['ok' => false, 'message' => 'Форма устарела. Обновите страницу и заполните её заново.'], 419);
    }
    if (!in_array($guardStatus, ['valid', 'used'], true)) {
        send_json(['ok' => false, 'message' => 'Не удалось проверить форму. Обновите страницу.'], 419);
    }
    if (post_text('js_enabled') !== '1') {
        send_json(['ok' => false, 'message' => 'Для отправки заявки включите JavaScript.'], 400);
    }
    $interactionStartedAt = (int)post_text('interaction_started_at', '0');
    if ($interactionStartedAt <= 0 || $interactionStartedAt > time() + 30) {
        send_json(['ok' => false, 'message' => 'Не удалось подтвердить заполнение формы. Обновите страницу.'], 400);
    }

    $ip = (string)client_ip();
    $securityIp = booking_security_ip();
    $maxAttempts = defined('FORM_MAX_ATTEMPTS_PER_10_MIN') ? max(6, (int)FORM_MAX_ATTEMPTS_PER_10_MIN) : 12;
    if (booking_record_attempt($securityIp) > $maxAttempts) {
        send_json(['ok' => false, 'message' => 'Слишком много попыток отправки. Попробуйте через 10 минут.'], 429);
    }

    $name = post_text('name');
    $phone = post_text('phone');
    $contactNick = post_text('contact_nick');
    $dates = post_text('dates');
    $checkin = post_text('checkin');
    $checkout = post_text('checkout');
    $adults = post_text('adults', '2');
    $children = post_text('children');
    $pet = post_text('pet', 'Нет');
    $comment = post_text('comment');
    $selectedRate = post_text('selected_rate');
    $estimatedTotal = post_text('estimated_total');
    $pageSlug = post_text('page_slug', 'home');
    $source = post_text('source', 'site');
    $house = post_text('house');
    $privacy = post_text('privacy');
    $settings = cms_settings();

    // Return all empty required fields in one response. This mirrors the visual
    // validation and also protects requests made without the normal JavaScript UI.
    $requiredErrors = [];
    if ($privacy !== '1') {
        $requiredErrors['privacy'] = 'Подтвердите согласие с политикой конфиденциальности и правилами проживания.';
    }
    $bookingFieldsConfig = (array)($settings['booking_form']['fields'] ?? []);
    $postedValues = [
        'name' => $name,
        'phone' => $phone,
        'contact_nick' => $contactNick,
        'house' => $house,
        'adults' => $adults,
        'children' => $children,
        'pet' => $pet,
        'comment' => $comment,
    ];
    foreach ($bookingFieldsConfig as $fieldKey => $fieldConfig) {
        if (empty($fieldConfig['enabled']) || empty($fieldConfig['required'])) {
            continue;
        }
        $fieldKey = (string)$fieldKey;
        if (in_array($fieldKey, ['checkin', 'checkout', 'calendar'], true)) {
            continue;
        }
        if (!array_key_exists($fieldKey, $postedValues) || trim((string)$postedValues[$fieldKey]) !== '') {
            continue;
        }
        $label = trim((string)($fieldConfig['label'] ?? $fieldKey));
        $requiredErrors[$fieldKey] = $label !== '' ? 'Заполните поле «' . $label . '».' : 'Заполните обязательное поле.';
    }
    $datesAreRequired = !empty($bookingFieldsConfig['calendar']['required'])
        || !empty($bookingFieldsConfig['checkin']['required'])
        || !empty($bookingFieldsConfig['checkout']['required']);
    if ($datesAreRequired && ($checkin === '' || $checkout === '')) {
        $requiredErrors['dates'] = 'Выберите даты заезда и выезда.';
    }
    if ($requiredErrors) {
        send_json([
            'ok' => false,
            'message' => count($requiredErrors) === 1 ? (string)reset($requiredErrors) : 'Заполните отмеченные обязательные поля.',
            'fields' => $requiredErrors,
        ], 422);
    }

    if ($privacy !== '1') {
        booking_validation_error('privacy', 'Подтвердите согласие с политикой конфиденциальности и правилами проживания.');
    }
    if (!valid_booking_name($name)) {
        booking_validation_error('name', 'Укажите корректное имя — минимум 2 буквы.');
    }
    if (!valid_booking_phone($phone)) {
        booking_validation_error('phone', 'Введите корректный номер телефона.');
    }
    if (text_length($contactNick) > 160 || text_length($children) > 200 || text_length($comment) > 1500 || text_length($selectedRate) > 200 || text_length($estimatedTotal) > 100) {
        send_json(['ok' => false, 'message' => 'Одно из полей содержит слишком много текста.'], 422);
    }
    if (!ctype_digit($adults) || (int)$adults < 1 || (int)$adults > 20) {
        booking_validation_error('adults', 'Укажите количество взрослых от 1 до 20.');
    }
    if (!in_array($pet, ['Нет', 'Да'], true)) {
        booking_validation_error('pet', 'Выберите корректное значение поля «Питомец».');
    }

    $allowedHouses = [];
    foreach ((array)(cms_collection('houses')['items'] ?? []) as $houseItem) {
        $title = trim((string)($houseItem['title'] ?? ''));
        if ($title !== '') $allowedHouses[] = $title;
    }
    if ($house === '' || !in_array($house, $allowedHouses, true)) {
        booking_validation_error('house', 'Выберите домик из списка.');
    }

    $from = strict_ymd($checkin);
    $to = strict_ymd($checkout);
    if (!$from || !$to) {
        booking_validation_error('dates', 'Выберите корректные даты заезда и выезда.');
    }
    $today = new DateTimeImmutable('today');
    if ($from < $today) {
        booking_validation_error('dates', 'Дата заезда не может быть в прошлом.');
    }
    if ($to <= $from) {
        booking_validation_error('dates', 'Дата выезда должна быть позже даты заезда.');
    }
    if ((int)$from->diff($to)->days > 60) {
        booking_validation_error('dates', 'Для бронирования более чем на 60 дней свяжитесь с администратором.');
    }

    $availability = cms_availability();
    $closed = array_fill_keys((array)($availability['global'] ?? []), true);
    $houseSlug = slugify($house);
    foreach ((array)(($availability['houses'][$houseSlug] ?? [])) as $date) {
        $closed[(string)$date] = true;
    }
    for ($d = $from; $d < $to; $d = $d->modify('+1 day')) {
        if (isset($closed[$d->format('Y-m-d')])) {
            booking_validation_error('dates', 'Одна или несколько выбранных дат уже закрыты в календаре.', 409);
        }
    }

    $brand = (string)($settings['brand']['name'] ?? (defined('SITE_NAME') ? SITE_NAME : 'Сайт'));
    $bookingSuccess = array_merge([
        'title' => 'Спасибо за заявку!',
        'text' => 'Мы скоро с вами свяжемся.',
        'details_template' => "Ваша бронь: {house}\nДаты: {checkin_ru} – {checkout_ru}\nМы свяжемся с вами.",
        'return_url' => '/',
    ], (array)($settings['booking_success'] ?? []));

    if ($guardStatus === 'used') {
        send_json(booking_response_payload($bookingSuccess, $house, $checkin, $checkout, true));
    }

    $fingerprintData = [
        normalized_phone($phone),
        function_exists('mb_strtolower') ? mb_strtolower($name, 'UTF-8') : strtolower($name),
        $house,
        $checkin,
        $checkout,
        $adults,
        $children,
        $pet,
        $comment,
    ];
    $fingerprint = hash('sha256', implode('|', array_map(static fn($v): string => trim((string)$v), $fingerprintData)));
    $reservation = booking_reserve_submission($securityIp, $fingerprint, $formToken);
    if (($reservation['status'] ?? '') === 'duplicate') {
        booking_form_guard_mark_used($formToken);
        send_json(booking_response_payload($bookingSuccess, $house, $checkin, $checkout, true));
    }
    if (($reservation['status'] ?? '') === 'rate_limited') {
        send_json(['ok' => false, 'message' => 'Слишком много заявок с этого устройства. Попробуйте через 10 минут.'], 429);
    }
    if (($reservation['status'] ?? '') !== 'reserved') {
        send_json(['ok' => false, 'message' => 'Не удалось защитить заявку от дублирования. Попробуйте ещё раз.'], 500);
    }
    $reservationId = (string)($reservation['reservation'] ?? '');

    $telegramSettings = (array)($settings['telegram_notifications'] ?? []);
    $botToken = trim((string)($telegramSettings['bot_token'] ?? (defined('TG_BOT_TOKEN') ? TG_BOT_TOKEN : '')));
    $chatId = trim((string)($telegramSettings['chat_id'] ?? (defined('TG_CHAT_ID') ? TG_CHAT_ID : '')));
    $checkinRu = format_booking_date_ru($checkin);
    $checkoutRu = format_booking_date_ru($checkout);
    $datesRu = $checkinRu . ' — ' . $checkoutRu;

    if ($botToken === '' || $chatId === '') {
        booking_finish_submission($fingerprint, $formToken, $reservationId, $securityIp, false);
        log_message('telegram-error.log', 'Missing Telegram settings: bot token or chat id is empty');
        send_json(['ok' => false, 'message' => 'Не настроена отправка заявок в Telegram. Обратитесь к администратору сайта.'], 500);
    }

    $lines = [
        '📩 Новая заявка с сайта ' . $brand,
        '',
        'Имя: ' . $name,
        'Телефон: ' . $phone,
        'Контакт в мессенджере: ' . ($contactNick !== '' ? $contactNick : '—'),
        'Даты: ' . $datesRu,
        'Домик: ' . $house,
        'Взрослые: ' . $adults,
        'Дети: ' . ($children !== '' ? $children : '—'),
        'Питомец: ' . $pet,
        'Комментарий: ' . ($comment !== '' ? $comment : '—'),
        'Тариф: ' . ($selectedRate !== '' ? $selectedRate : '—'),
        'Ориентировочная сумма: ' . ($estimatedTotal !== '' ? $estimatedTotal : '—'),
        '',
        'Страница: ' . $pageSlug,
        'Источник: ' . $source,
        'IP: ' . $ip,
        'User-Agent: ' . (string)($_SERVER['HTTP_USER_AGENT'] ?? '—'),
    ];

    $clientMessageEnabled = !array_key_exists('client_message_enabled', $telegramSettings)
        || !empty($telegramSettings['client_message_enabled']);
    $clientMessageTitle = trim((string)($telegramSettings['client_message_title'] ?? booking_client_message_default_title()));
    $clientMessageTemplate = (string)($telegramSettings['client_message_template'] ?? booking_client_message_default_template());
    $clientMessage = trim(apply_template_vars($clientMessageTemplate, [
        'name' => $name,
        'phone' => $phone,
        'contact' => ($contactNick !== '' ? $contactNick : '—'),
        'house' => $house,
        'checkin' => $checkin,
        'checkout' => $checkout,
        'checkin_ru' => $checkinRu,
        'checkout_ru' => $checkoutRu,
        'dates_ru' => $datesRu,
        'adults' => (string)$adults,
        'children' => ($children !== '' ? $children : '—'),
        'pet' => $pet,
        'comment' => ($comment !== '' ? $comment : '—'),
        'rate' => ($selectedRate !== '' ? $selectedRate : '—'),
        'total' => ($estimatedTotal !== '' ? $estimatedTotal : '—'),
        'brand' => $brand,
    ]));

    if ($clientMessageEnabled && $clientMessage !== '') {
        $lines[] = '';
        $lines[] = '────────────────────';
        $lines[] = ($clientMessageTitle !== '' ? $clientMessageTitle : booking_client_message_default_title());
        $lines[] = '';
        $lines[] = $clientMessage;
    }

    $telegramResult = send_telegram_message($botToken, $chatId, implode("\n", $lines));
    if (!$telegramResult['ok'] || (int)$telegramResult['http_code'] >= 400) {
        booking_finish_submission($fingerprint, $formToken, $reservationId, $securityIp, false);
        log_message(
            'telegram-error.log',
            'HTTP ' . $telegramResult['http_code'] . '; error=' . $telegramResult['error'] . '; telegram=' . $telegramResult['description'] . '; response=' . $telegramResult['body']
        );
        send_json(['ok' => false, 'message' => 'Не удалось отправить заявку. Проверьте настройки Telegram в админке и попробуйте ещё раз.'], 502);
    }

    booking_finish_submission($fingerprint, $formToken, $reservationId, $securityIp, true);
    booking_form_guard_mark_used($formToken);

    $leadSaved = safe_create_lead_record([
        'status' => 'new',
        'is_hidden' => false,
        'name' => $name,
        'phone' => $phone,
        'contact_nick' => $contactNick,
        'dates' => $datesRu,
        'checkin' => $checkin,
        'checkout' => $checkout,
        'house' => $house,
        'adults' => $adults,
        'children' => $children,
        'pet' => $pet,
        'comment' => $comment,
        'estimated_total' => $estimatedTotal,
        'page_slug' => $pageSlug,
        'source' => $source,
        'ip' => $ip,
        'country' => safe_country_for_ip($ip),
        'visits' => safe_visits_by_ip($ip),
        'user_agent' => (string)($_SERVER['HTTP_USER_AGENT'] ?? ''),
        'utm_source' => post_text('utm_source'),
        'utm_medium' => post_text('utm_medium'),
        'utm_campaign' => post_text('utm_campaign'),
        'utm_term' => post_text('utm_term'),
        'utm_content' => post_text('utm_content'),
        'gclid' => post_text('gclid'),
        'yclid' => post_text('yclid'),
        'fbclid' => post_text('fbclid'),
    ]);

    if (!$leadSaved) {
        log_noncritical(sprintf('Lead was not saved for booking %s / %s / %s - %s', $name, $phone, $checkin, $checkout));
    }
    safe_track_booking_activity($pageSlug, $house, $checkin, $checkout);

    $response = booking_response_payload($bookingSuccess, $house, $checkin, $checkout, false);
    $response['crm_saved'] = $leadSaved;
    send_json($response);
} catch (Throwable $e) {
    log_message('send-fatal.log', $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
    send_json(['ok' => false, 'message' => 'Ошибка сервера при обработке заявки. Попробуйте ещё раз.'], 500);
}
