#![allow(clippy::unused_async)]

use async_stream::stream;
use axum::{
    body::{Body, Bytes},
    http::{
        HeaderMap, HeaderValue,
        header::{CACHE_CONTROL, CONNECTION, CONTENT_TYPE},
    },
};
use chrono::{DateTime, Utc};
use loco_rs::prelude::*;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::time::Instant;

use crate::{
    controllers::{admin::check_auth, site_settings},
    services::{abuse_guard, ai, analytics, worker_jobs},
};

#[derive(Clone, Debug, Deserialize)]
pub struct AskPayload {
    pub question: String,
}

#[derive(Clone, Debug, Serialize)]
pub struct AskResponse {
    pub question: String,
    pub answer: String,
    pub sources: Vec<ai::AiSource>,
    pub indexed_chunks: usize,
    pub last_indexed_at: Option<String>,
}

#[derive(Clone, Debug, Serialize)]
pub struct ReindexResponse {
    pub queued: bool,
    pub job: worker_jobs::WorkerJobRecord,
}

#[derive(Clone, Debug, Serialize)]
struct StreamStatusEvent {
    phase: String,
    message: String,
}

#[derive(Clone, Debug, Serialize)]
struct StreamDeltaEvent {
    delta: String,
}

#[derive(Clone, Debug, Serialize)]
struct StreamErrorEvent {
    message: String,
}

fn format_timestamp(value: Option<DateTime<Utc>>) -> Option<String> {
    value.map(|item| item.to_rfc3339())
}

fn trim_to_option(value: Option<String>) -> Option<String> {
    value.and_then(|item| {
        let trimmed = item.trim().to_string();
        if trimmed.is_empty() {
            None
        } else {
            Some(trimmed)
        }
    })
}

async fn current_provider_metadata(ctx: &AppContext) -> (Option<String>, Option<String>) {
    match site_settings::load_current(ctx).await {
        Ok(settings) => (
            trim_to_option(settings.ai_provider),
            trim_to_option(settings.ai_chat_model),
        ),
        Err(error) => {
            tracing::warn!("failed to load ai provider metadata for analytics: {error}");
            (None, None)
        }
    }
}

fn sse_bytes<T: Serialize>(event: &str, payload: &T) -> Bytes {
    let data = serde_json::to_string(payload)
        .unwrap_or_else(|_| "{\"message\":\"failed to serialize SSE payload\"}".to_string());

    Bytes::from(format!("event: {event}\ndata: {data}\n\n"))
}

fn take_next_sse_event(buffer: &mut String) -> Option<(Option<String>, String)> {
    let mut boundary = buffer.find("\n\n").map(|index| (index, 2));
    if let Some(index) = buffer.find("\r\n\r\n") {
        match boundary {
            Some((existing, _)) if existing <= index => {}
            _ => boundary = Some((index, 4)),
        }
    }

    let (index, separator_len) = boundary?;
    let raw = buffer[..index].to_string();
    buffer.drain(..index + separator_len);

    let normalized = raw.replace("\r\n", "\n");
    let mut event = None;
    let mut data_lines = Vec::new();

    for line in normalized.lines() {
        if let Some(value) = line.strip_prefix("event:") {
            event = Some(value.trim().to_string());
            continue;
        }

        if let Some(value) = line.strip_prefix("data:") {
            data_lines.push(value.trim_start().to_string());
        }
    }

    Some((event, data_lines.join("\n")))
}

fn extract_stream_delta(value: &Value) -> Option<String> {
    if let Some(delta) = value.get("delta").and_then(Value::as_str) {
        return Some(delta.to_string());
    }

    if let Some(content) = value
        .get("choices")
        .and_then(Value::as_array)
        .and_then(|choices| choices.first())
        .and_then(|choice| choice.get("delta"))
        .and_then(|delta| delta.get("content"))
    {
        if let Some(text) = content.as_str() {
            return Some(text.to_string());
        }

        if let Some(parts) = content.as_array() {
            let merged = parts
                .iter()
                .filter_map(|part| {
                    part.get("text")
                        .and_then(Value::as_str)
                        .or_else(|| part.as_str())
                })
                .collect::<Vec<_>>()
                .join("");

            if !merged.is_empty() {
                return Some(merged);
            }
        }
    }

    value
        .get("choices")
        .and_then(Value::as_array)
        .and_then(|choices| choices.first())
        .and_then(|choice| choice.get("text"))
        .and_then(Value::as_str)
        .map(ToString::to_string)
}

fn append_missing_suffix(accumulated: &mut String, full_text: &str) -> Option<String> {
    if full_text.is_empty() {
        return None;
    }

    if accumulated.is_empty() {
        accumulated.push_str(full_text);
        return Some(full_text.to_string());
    }

    if full_text.starts_with(accumulated.as_str()) {
        let suffix = full_text[accumulated.len()..].to_string();
        if !suffix.is_empty() {
            accumulated.push_str(&suffix);
            return Some(suffix);
        }
    }

    None
}

fn chunk_text(value: &str, chunk_size: usize) -> Vec<String> {
    let chars = value.chars().collect::<Vec<_>>();
    chars
        .chunks(chunk_size.max(1))
        .map(|chunk| chunk.iter().collect::<String>())
        .filter(|chunk| !chunk.is_empty())
        .collect::<Vec<_>>()
}

fn build_ask_response(prepared: &ai::PreparedAiAnswer, answer: String) -> AskResponse {
    AskResponse {
        question: prepared.question.clone(),
        answer,
        sources: prepared.sources.clone(),
        indexed_chunks: prepared.indexed_chunks,
        last_indexed_at: format_timestamp(prepared.last_indexed_at),
    }
}

#[debug_handler]
pub async fn ask(
    State(ctx): State<AppContext>,
    headers: HeaderMap,
    Json(payload): Json<AskPayload>,
) -> Result<Response> {
    let started_at = Instant::now();
    let question = payload.question.trim().to_string();
    let (provider, chat_model) = current_provider_metadata(&ctx).await;
    abuse_guard::enforce_public_scope(
        "ai_ask",
        abuse_guard::detect_client_ip(&headers).as_deref(),
        Some(&question),
    )?;

    match ai::answer_question(&ctx, &payload.question).await {
        Ok(result) => {
            analytics::record_ai_question_event(
                &ctx,
                &question,
                &headers,
                true,
                "sync",
                provider,
                chat_model,
                Some(result.sources.len()),
                started_at.elapsed().as_millis() as i64,
            )
            .await;

            format::json(AskResponse {
                question,
                answer: result.answer,
                sources: result.sources,
                indexed_chunks: result.indexed_chunks,
                last_indexed_at: format_timestamp(result.last_indexed_at),
            })
        }
        Err(error) => {
            analytics::record_ai_question_event(
                &ctx,
                &question,
                &headers,
                false,
                "sync",
                provider,
                chat_model,
                None,
                started_at.elapsed().as_millis() as i64,
            )
            .await;
            Err(error)
        }
    }
}

#[debug_handler]
pub async fn ask_stream(
    State(ctx): State<AppContext>,
    headers: HeaderMap,
    Json(payload): Json<AskPayload>,
) -> Result<Response> {
    let request_headers = headers.clone();
    let question = payload.question.trim().to_string();
    let (fallback_provider, fallback_chat_model) = current_provider_metadata(&ctx).await;
    abuse_guard::enforce_public_scope(
        "ai_stream",
        abuse_guard::detect_client_ip(&headers).as_deref(),
        Some(&question),
    )?;

    let stream = stream! {
        let started_at = Instant::now();
        yield Ok::<Bytes, std::io::Error>(sse_bytes("status", &StreamStatusEvent {
            phase: "retrieving".to_string(),
            message: "正在检索知识库上下文...".to_string(),
        }));

        let prepared = match ai::prepare_answer(&ctx, &payload.question).await {
            Ok(prepared) => prepared,
            Err(error) => {
                analytics::record_ai_question_event(
                    &ctx,
                    &question,
                    &request_headers,
                    false,
                    "stream",
                    fallback_provider.clone(),
                    fallback_chat_model.clone(),
                    None,
                    started_at.elapsed().as_millis() as i64,
                )
                .await;
                yield Ok(sse_bytes("error", &StreamErrorEvent {
                    message: error.to_string(),
                }));
                return;
            }
        };

        let mut accumulated_answer = String::new();
        let active_provider = prepared
            .provider_request
            .as_ref()
            .map(|request| request.provider.clone())
            .or_else(|| fallback_provider.clone());
        let active_chat_model = prepared
            .provider_request
            .as_ref()
            .map(|request| request.chat_model.clone())
            .or_else(|| fallback_chat_model.clone());

        if let Some(answer) = prepared.immediate_answer.as_deref() {
            yield Ok(sse_bytes("status", &StreamStatusEvent {
                phase: "answering".to_string(),
                message: "已完成检索，正在输出检索结论...".to_string(),
            }));

            for chunk in chunk_text(answer, 48) {
                accumulated_answer.push_str(&chunk);
                yield Ok(sse_bytes("delta", &StreamDeltaEvent { delta: chunk }));
            }
        } else if let Some(provider_request) = prepared.provider_request.as_ref() {
            yield Ok(sse_bytes("status", &StreamStatusEvent {
                phase: "streaming".to_string(),
                message: "已命中相关资料，正在流式生成回答...".to_string(),
            }));

            let client = reqwest::Client::new();
            let response = client
                .post(ai::build_provider_url(provider_request))
                .bearer_auth(&provider_request.api_key)
                .header("Accept", "text/event-stream, application/json")
                .json(&ai::build_provider_payload(provider_request, true))
                .send()
                .await;

            let mut response = match response {
                Ok(response) => response,
                Err(error) => {
                    analytics::record_ai_question_event(
                        &ctx,
                        &question,
                        &request_headers,
                        false,
                        "stream",
                        active_provider.clone(),
                        active_chat_model.clone(),
                        Some(prepared.sources.len()),
                        started_at.elapsed().as_millis() as i64,
                    )
                    .await;
                    yield Ok(sse_bytes("error", &StreamErrorEvent {
                        message: format!("AI request failed: {error}"),
                    }));
                    return;
                }
            };

            if !response.status().is_success() {
                let status = response.status();
                let body = response.text().await.unwrap_or_default();
                analytics::record_ai_question_event(
                    &ctx,
                    &question,
                    &request_headers,
                    false,
                    "stream",
                    active_provider.clone(),
                    active_chat_model.clone(),
                    Some(prepared.sources.len()),
                    started_at.elapsed().as_millis() as i64,
                )
                .await;
                yield Ok(sse_bytes("error", &StreamErrorEvent {
                    message: format!("AI provider returned {status}: {body}"),
                }));
                return;
            }

            let mut sse_buffer = String::new();
            let mut last_full_answer = None;

            loop {
                let next_chunk = response.chunk().await;
                let Some(chunk) = (match next_chunk {
                    Ok(chunk) => chunk,
                    Err(error) => {
                        analytics::record_ai_question_event(
                            &ctx,
                            &question,
                            &request_headers,
                            false,
                            "stream",
                            active_provider.clone(),
                            active_chat_model.clone(),
                            Some(prepared.sources.len()),
                            started_at.elapsed().as_millis() as i64,
                        )
                        .await;
                        yield Ok(sse_bytes("error", &StreamErrorEvent {
                            message: format!("AI stream read failed: {error}"),
                        }));
                        return;
                    }
                }) else {
                    break;
                };

                sse_buffer.push_str(&String::from_utf8_lossy(&chunk));

                while let Some((_event_name, data)) = take_next_sse_event(&mut sse_buffer) {
                    let trimmed = data.trim();
                    if trimmed.is_empty() {
                        continue;
                    }

                    if trimmed == "[DONE]" {
                        continue;
                    }

                    let parsed = match serde_json::from_str::<Value>(trimmed) {
                        Ok(parsed) => parsed,
                        Err(_) => continue,
                    };

                    if let Some(full_text) = ai::extract_provider_text(&parsed) {
                        last_full_answer = Some(full_text);
                    }

                    if let Some(delta) = extract_stream_delta(&parsed) {
                        accumulated_answer.push_str(&delta);
                        yield Ok(sse_bytes("delta", &StreamDeltaEvent { delta }));
                    }
                }
            }

            let leftover = sse_buffer.trim();
            if !leftover.is_empty() && leftover != "[DONE]" {
                if let Ok(parsed) = serde_json::from_str::<Value>(leftover) {
                    if let Some(full_text) = ai::extract_provider_text(&parsed) {
                        last_full_answer = Some(full_text);
                    }

                    if let Some(delta) = extract_stream_delta(&parsed) {
                        accumulated_answer.push_str(&delta);
                        yield Ok(sse_bytes("delta", &StreamDeltaEvent { delta }));
                    }
                }
            }

            if let Some(full_text) = last_full_answer {
                if let Some(suffix) = append_missing_suffix(&mut accumulated_answer, &full_text) {
                    yield Ok(sse_bytes("delta", &StreamDeltaEvent { delta: suffix }));
                }
            }

            if accumulated_answer.is_empty() {
                analytics::record_ai_question_event(
                    &ctx,
                    &question,
                    &request_headers,
                    false,
                    "stream",
                    active_provider.clone(),
                    active_chat_model.clone(),
                    Some(prepared.sources.len()),
                    started_at.elapsed().as_millis() as i64,
                )
                .await;
                yield Ok(sse_bytes("error", &StreamErrorEvent {
                    message: "AI chat response did not contain readable content".to_string(),
                }));
                return;
            }
        }

        analytics::record_ai_question_event(
            &ctx,
            &question,
            &request_headers,
            true,
            "stream",
            active_provider,
            active_chat_model,
            Some(prepared.sources.len()),
            started_at.elapsed().as_millis() as i64,
        )
        .await;

        let final_payload = build_ask_response(&prepared, accumulated_answer);
        yield Ok(sse_bytes("complete", &final_payload));
    };

    let mut response = Response::new(Body::from_stream(stream));
    response.headers_mut().insert(
        CONTENT_TYPE,
        HeaderValue::from_static("text/event-stream; charset=utf-8"),
    );
    response
        .headers_mut()
        .insert(CACHE_CONTROL, HeaderValue::from_static("no-cache"));
    response
        .headers_mut()
        .insert(CONNECTION, HeaderValue::from_static("keep-alive"));

    Ok(response)
}

#[debug_handler]
pub async fn reindex(headers: HeaderMap, State(ctx): State<AppContext>) -> Result<Response> {
    let actor = check_auth(&headers)?;
    let job = worker_jobs::queue_ai_reindex_job(
        &ctx,
        Some(actor.username.clone()),
        Some(actor.source.clone()),
        None,
        Some("manual".to_string()),
    )
    .await?;

    format::json(ReindexResponse { queued: true, job })
}

pub fn routes() -> Routes {
    Routes::new()
        .prefix("api/ai/")
        .add("/ask", post(ask))
        .add("/ask/stream", post(ask_stream))
        .add("/reindex", post(reindex))
}
