use axum::{
    body::Bytes,
    extract::{DefaultBodyLimit, MatchedPath, Request, State},
    http::{header, StatusCode},
    middleware::{self, Next},
    response::{IntoResponse, Response},
    routing::{get, post},
    Json, Router,
};
use metrics_exporter_prometheus::PrometheusBuilder;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::sync::{Arc, LazyLock};
use std::time::Instant;

mod classifier;
use classifier::ToxicityClassifier;

#[derive(Deserialize, Serialize, Debug)]
struct CommentPayload {
    nickname: String,
    message: String,
}

// Генератор нагрузки для спидтеста (64 MB на один запрос).
//
// Байты генерируются линейным конгруэнтным генератором (LCG), а не просто
// заполняются нулями: нулевой (или вообще любой сильно сжимаемый) блоб при
// передаче через gzip/brotli в HTTP-стеке "похудеет" в десятки раз, и клиент
// измерит скорость сжатия, а не скорость сети. LCG даёт дешёвый (без крипто-
// рандома и внешних зависимостей) псевдослучайный поток байт, который не
// сжимается, поэтому размер тела на проводе честно равен заявленному объёму.
// Генерируется один раз при первом обращении (LazyLock) и переиспользуется
// для всех последующих запросов (клонирование Bytes — это инкремент
// refcount, а не копия буфера, так что параллельные запросы дёшевы).
//
// 64 MB, а не 25 — клиент теперь качает окно фиксированной ДЛИТЕЛЬНОСТИ
// несколькими параллельными потоками и переоткрывает поток, как только он
// исчерпал блоб. Больший блоб просто снижает частоту переоткрытий на очень
// быстрых каналах, не меняя семантику теста.
static RANDOM_PAYLOAD: LazyLock<Bytes> = LazyLock::new(|| {
    let mut data = vec![0u8; 64 * 1024 * 1024];
    let mut state: u32 = 123456789;
    for byte in data.iter_mut() {
        state = state.wrapping_mul(1103515245).wrapping_add(12345);
        *byte = (state >> 16) as u8;
    }
    Bytes::from(data)
});

/// Общее состояние Axum-приложения, разделяемое между всеми запросами через `Arc`.
///
/// Хранит уже загруженный и скомпилированный классификатор токсичности —
/// модель грузится один раз при старте (см. `main`), а не на каждый запрос,
/// т.к. загрузка ONNX-графа и токенизатора — дорогая операция.
struct AppState {
    ai_classifier: ToxicityClassifier,
}

#[tokio::main]
async fn main() {
    println!("🧠 [SYSTEM] Booting AI Neural Link (Pure Rust Engine)...");
    let classifier = ToxicityClassifier::new("models/model.onnx", "models/tokenizer.json")
        .expect("CRITICAL: Failed to load Neural Link model");

    let state = Arc::new(AppState {
        ai_classifier: classifier,
    });

    // Для центрального Prometheus (см. netrunner-data/docker-compose.observability.yml,
    // scrape по сети — этот порт нужно будет зафайрволить на его IP так же,
    // как /metrics на прокси-нодах).
    let recorder_handle = PrometheusBuilder::new()
        .install_recorder()
        .expect("Failed to install Prometheus recorder");

    let app = Router::new()
        .route("/api/comments", post(create_comment))
        .route("/api/speedtest/ping", get(speedtest_ping))
        .route("/api/speedtest/download", get(speedtest_download))
        .route("/api/speedtest/upload", post(speedtest_upload))
        .layer(DefaultBodyLimit::max(100 * 1024 * 1024))
        .with_state(state)
        .route(
            "/metrics",
            get(move || std::future::ready(recorder_handle.render())),
        )
        .route_layer(middleware::from_fn(track_metrics));

    println!("🚀 [READY] Netrunner Gateway started on 0.0.0.0:3001");
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

/// Принимает запись гостевой книги с фронта, модерирует её ИИ-классификатором
/// токсичности и, если всё чисто, форвардит как есть в PocketBase.
///
/// Никнейм проверяется моделью всегда и без исключений (это то, что видно
/// рядом с каждым сообщением, и с него чаще всего пытаются материться).
/// Текст сообщения дополнительно проходит через гвард-рейлы (см. ниже) перед
/// тем, как попасть на инференс.
async fn create_comment(
    State(state): State<Arc<AppState>>,
    Json(payload): Json<CommentPayload>,
) -> impl IntoResponse {
    let message = payload.message.trim().to_string();
    let nickname = payload.nickname.trim().to_string();

    println!("📩 [INCOMING] '{}': '{}'", nickname, message);

    // --- ГВАРД-РЕЙЛЫ (Только для текста сообщения) ---
    // Модель — небольшой BERT-классификатор, обученный на относительно длинных
    // фразах, и на коротких/безобидных сообщениях склонна давать false positive
    // (например, "ок", "супер", "спасибо, работает" иногда цепляют высокий
    // скор токсичности просто из-за нехватки контекста). Чтобы не отклонять
    // легитимные отзывы, часть сообщений вообще не отправляется на инференс:
    //   - whitelist явно безобидных слов (в т.ч. частые в отзывах про VPN);
    //   - совсем короткие сообщения (< 4 символов) — на них модель нестабильна;
    //   - сообщения без единой буквы (эмодзи, знаки препинания, цифры) — там
    //     токсичности в текстовом смысле в принципе быть не может.
    // Это осознанный компромисс: немного снижает строгость модерации ради
    // пользовательского опыта и экономии инференса на заведомо безопасном тексте.
    let whitelist = [
        "vpn",
        "service",
        "experience",
        "use",
        "working",
        "fast",
        "thanks",
        "привет",
        "спасибо",
        "работает",
        "test",
        "тест",
    ];
    let msg_lower = message.to_lowercase();
    let msg_whitelisted = whitelist.iter().any(|&word| msg_lower.contains(word));
    let msg_has_alphabetic = message.chars().any(|c| c.is_alphabetic());

    let msg_safe_by_guard = msg_whitelisted || message.len() < 4 || !msg_has_alphabetic;

    // Запускаем проверку.
    // tract-onnx выполняет инференс синхронно и CPU-bound (никаких await внутри),
    // поэтому вызов напрямую в async-функции застолбил бы поток из пула Tokio
    // на всё время работы модели, не давая ему обслуживать другие запросы.
    // spawn_blocking переносит эту работу в отдельный пул потоков, специально
    // предназначенный для блокирующих операций, освобождая async-раннтайм.
    let is_toxic = tokio::task::spawn_blocking(move || {
        // 1. ВСЕГДА проверяем никнейм
        let is_nick_toxic = state.ai_classifier.is_toxic(&nickname);

        // 2. Проверяем сообщение, если оно не попало под гвард-рейлы
        let is_msg_toxic = if msg_safe_by_guard {
            println!(
                "🛡️ [GUARD RAILS] Message text bypassed (whitelisted, short, or non-alphabetic)."
            );
            false
        } else {
            state.ai_classifier.is_toxic(&message)
        };

        is_nick_toxic || is_msg_toxic
    })
    .await
    .unwrap_or(false);

    if is_toxic {
        println!(
            "🚫 [REJECTED] Blocked by AI: nick='{}', msg='{}'",
            payload.nickname, payload.message
        );
        return (StatusCode::NOT_ACCEPTABLE, "Behavioral policy violation.");
    }

    println!("✅ [PASSED] Forwarding to PocketBase...");

    let pb_url = format!(
        "{}/api/collections/comments/records",
        std::env::var("PB_INTERNAL_URL")
            .unwrap_or_else(|_| "http://netrunner-cms:8090".to_string())
    );

    let client = Client::new();
    match client.post(&pb_url).json(&payload).send().await {
        Ok(res) => {
            if res.status().is_success() {
                println!("🎉 [SUCCESS] Saved to PocketBase");
                (StatusCode::OK, "Delivered.")
            } else {
                println!("❌ [ERROR] PocketBase rejected: {}", res.status());
                (StatusCode::BAD_REQUEST, "Uplink rejected.")
            }
        }
        Err(e) => {
            println!("💥 [CRITICAL] PocketBase unreachable: {}", e);
            (StatusCode::SERVICE_UNAVAILABLE, "Uplink offline.")
        }
    }
}

// Без no-store браузер/промежуточный кэш мог бы отдать закэшированный "pong"
// вместо реального round-trip'а — и клиентский спидтест мерил бы кэш, а не сеть.
async fn speedtest_ping() -> impl IntoResponse {
    (
        [
            (
                header::CACHE_CONTROL,
                header::HeaderValue::from_static("no-store, no-cache, must-revalidate, max-age=0"),
            ),
            (header::PRAGMA, header::HeaderValue::from_static("no-cache")),
        ],
        "pong",
    )
}

async fn speedtest_download() -> impl IntoResponse {
    println!("📊 [SPEEDTEST] Download requested");
    let body = RANDOM_PAYLOAD.clone();
    let len = body.len();
    (
        [
            (
                header::CONTENT_TYPE,
                header::HeaderValue::from_static("application/octet-stream"),
            ),
            (header::CONTENT_LENGTH, header::HeaderValue::from(len)),
            (
                header::CACHE_CONTROL,
                header::HeaderValue::from_static("no-store, no-cache, must-revalidate, max-age=0"),
            ),
            (header::PRAGMA, header::HeaderValue::from_static("no-cache")),
        ],
        body,
    )
}

async fn speedtest_upload(body: Bytes) -> impl IntoResponse {
    println!("📊 [SPEEDTEST] Upload received: {} bytes", body.len());
    (StatusCode::OK, format!("Received {} bytes", body.len()))
}

/// Тот же паттерн, что `api.rs::track_metrics` в netrunner-backend —
/// латентность/число запросов по маршруту+методу+статусу.
async fn track_metrics(req: Request, next: Next) -> Response {
    let start = Instant::now();
    let path = req
        .extensions()
        .get::<MatchedPath>()
        .map(|m| m.as_str().to_owned())
        .unwrap_or_else(|| req.uri().path().to_owned());
    let method = req.method().to_string();

    let response = next.run(req).await;

    let latency = start.elapsed().as_secs_f64();
    let status = response.status().as_u16().to_string();

    let labels = [("method", method), ("path", path), ("status", status)];
    metrics::counter!("http_requests_total", &labels).increment(1);
    metrics::histogram!("http_request_duration_seconds", &labels).record(latency);

    response
}
