use crate::app::AppState;
use crate::context::RequestContext;
use crate::error::WebError;
use crate::hermes_tools::ToolCallInput;
use crate::routes::knowledge_rag::{
    KnowledgeRagOpenReferenceRequest, KnowledgeRagQueryRequest, KnowledgeRagStatusQuery,
};
use axum::extract::{Extension, Json, Query, State};
use serde_json::{json, Value};

pub async fn status(
    state: &AppState,
    context: &RequestContext,
    input: &ToolCallInput,
) -> Result<Value, WebError> {
    let args = input.args.clone().unwrap_or_else(|| json!({}));
    let body = KnowledgeRagStatusQuery {
        workspace_id: input.effective_workspace_id().or_else(|| {
            args.get("workspaceId")
                .and_then(Value::as_str)
                .map(ToOwned::to_owned)
        }),
        root_uri: input.effective_root_uri().or_else(|| {
            args.get("rootUri")
                .and_then(Value::as_str)
                .map(ToOwned::to_owned)
        }),
    };
    let Json(payload) = crate::routes::knowledge_rag::status(
        State(state.clone()),
        Extension(context.clone()),
        Query(body),
    )
    .await?;
    Ok(payload)
}

pub async fn query(
    state: &AppState,
    context: &RequestContext,
    input: &ToolCallInput,
) -> Result<Value, WebError> {
    let mut args = input.args.clone().unwrap_or_else(|| json!({}));
    if args.get("workspaceId").is_none() {
        if let Some(workspace_id) = input.effective_workspace_id() {
            args["workspaceId"] = json!(workspace_id);
        }
    }
    if args.get("rootUri").is_none() {
        if let Some(root_uri) = input.effective_root_uri() {
            args["rootUri"] = json!(root_uri);
        }
    }
    let body = serde_json::from_value::<KnowledgeRagQueryRequest>(args).map_err(|error| {
        WebError::bad_request_code(
            "mnote_knowledge_rag_query_payload_invalid",
            format!("资料库问答参数无效: {error}"),
        )
        .with_context(context)
    })?;
    let Json(payload) = crate::routes::knowledge_rag::query_rag(
        State(state.clone()),
        Extension(context.clone()),
        Json(body),
    )
    .await?;
    Ok(compact_query_result_for_agent(payload))
}

pub async fn open_reference(
    state: &AppState,
    context: &RequestContext,
    input: &ToolCallInput,
) -> Result<Value, WebError> {
    let mut args = input.args.clone().unwrap_or_else(|| json!({}));
    if args.get("workspaceId").is_none() {
        if let Some(workspace_id) = input.effective_workspace_id() {
            args["workspaceId"] = json!(workspace_id);
        }
    }
    if args.get("rootUri").is_none() {
        if let Some(root_uri) = input.effective_root_uri() {
            args["rootUri"] = json!(root_uri);
        }
    }
    let body =
        serde_json::from_value::<KnowledgeRagOpenReferenceRequest>(args).map_err(|error| {
            WebError::bad_request_code(
                "mnote_knowledge_rag_open_reference_payload_invalid",
                format!("资料库引用打开参数无效: {error}"),
            )
            .with_context(context)
        })?;
    let Json(payload) = crate::routes::knowledge_rag::open_reference(
        State(state.clone()),
        Extension(context.clone()),
        Json(body),
    )
    .await?;
    Ok(payload)
}

fn compact_query_result_for_agent(payload: Value) -> Value {
    let references = payload
        .get("references")
        .and_then(Value::as_array)
        .map(|items| {
            items
                .iter()
                .take(8)
                .map(compact_reference_for_agent)
                .collect::<Vec<_>>()
        })
        .unwrap_or_default();
    let payload_citations = payload
        .get("citations")
        .and_then(Value::as_array)
        .cloned()
        .unwrap_or_default();
    let citations = if payload_citations.is_empty() {
        citation_references_for_ui(&references)
            .into_iter()
            .map(compact_citation_for_agent)
            .collect::<Vec<_>>()
    } else {
        citation_values_for_ui(&payload_citations)
            .into_iter()
            .map(compact_citation_for_agent)
            .collect::<Vec<_>>()
    };
    let citation_markdowns = citations
        .iter()
        .filter_map(|citation| {
            citation
                .get("citationMarkdown")
                .and_then(Value::as_str)
                .filter(|value| !value.trim().is_empty())
                .map(ToOwned::to_owned)
        })
        .collect::<Vec<_>>();
    let ui_citations = citations.clone();
    json!({
        "ok": payload.get("ok").cloned().unwrap_or_else(|| json!(true)),
        "schema": "mnote.knowledge_rag.agent_query_result.v1",
        "provider": payload.get("provider").cloned().unwrap_or_else(|| json!("lightrag")),
        "answerGuidance": "Final answers should answer the user in plain text. Do not copy citationMarkdown, citationUrl, /documents, mnote://, or search-engine wrapped local links into the answer. MNote UI will render citations[] / references[].citationMarkdown after the answer as clickable source locators. Treat rawMetadata.keywords and entity/relation counts only as query analysis, not proof that a source contains those words. Use references[].quote/contentDiagnostics as evidence. If locatorDegraded is true, say the source location is degraded instead of inventing page or bbox. If contentDiagnostics.ocrTextExposed is false, say OCR text was not exposed in the returned reference instead of claiming OCR succeeded.",
        "references": references,
        "citations": citations,
        "citationMarkdowns": citation_markdowns,
        "uiCitations": ui_citations,
        "citationRendering": "MNote UI appends uiCitations/citations after the final answer; agent must not hand-write citation links.",
        "sourceScope": payload.get("sourceScope").cloned().unwrap_or(Value::Array(Vec::new())),
        "sourceScopeMode": payload.get("sourceScopeMode").cloned().unwrap_or_else(|| json!("post_filter_mapped_references")),
        "rawScopeFiltered": payload.get("rawScopeFiltered").cloned().unwrap_or(Value::Bool(false)),
        "rawMetadataMeaning": "provider metadata describes query processing/retrieval bookkeeping; it is not source text evidence unless the same text appears in references[].quote",
        "rawStatus": payload.pointer("/raw/status").cloned().unwrap_or(Value::Null),
        "rawMessage": payload.pointer("/raw/message").cloned().unwrap_or(Value::Null),
        "rawMetadata": payload.pointer("/raw/metadata").cloned().unwrap_or(Value::Null),
    })
}

fn compact_reference_for_agent(reference: &Value) -> Value {
    let quote = reference
        .get("displayQuote")
        .and_then(Value::as_str)
        .or_else(|| reference.get("quote").and_then(Value::as_str))
        .map(|value| value.chars().take(700).collect::<String>())
        .unwrap_or_default();
    let locator_evidence_text = reference
        .get("locatorEvidenceText")
        .and_then(Value::as_str)
        .map(|value| value.chars().take(700).collect::<String>())
        .unwrap_or_default();
    let quote_diagnostics = reference
        .get("contentDiagnostics")
        .cloned()
        .unwrap_or_else(|| quote_diagnostics(&quote));
    json!({
        "schema": reference.get("schema").cloned().unwrap_or_else(|| json!("mnote.knowledge_rag.reference.v1")),
        "provider": reference.get("provider").cloned().unwrap_or_else(|| json!("lightrag")),
        "citationId": reference.get("citationId").cloned().unwrap_or(Value::Null),
        "citationLabel": reference.get("citationLabel").cloned().unwrap_or(Value::Null),
        "filePath": reference.get("filePath").cloned().unwrap_or(Value::Null),
        "sourceRootRelativePath": reference.get("sourceRootRelativePath").cloned().unwrap_or(Value::Null),
        "chunkId": reference.get("chunkId").cloned().unwrap_or(Value::Null),
        "quote": quote,
        "displayQuote": reference.get("displayQuote").cloned().unwrap_or(Value::Null),
        "locatorEvidenceText": locator_evidence_text,
        "quoteSource": reference.get("quoteSource").cloned().unwrap_or(Value::Null),
        "locator": reference.get("locator").cloned().unwrap_or(Value::Null),
        "locatorPrecision": reference.get("locatorPrecision").cloned().unwrap_or(Value::Null),
        "locatorDegraded": reference.get("locatorDegraded").cloned().unwrap_or(Value::Bool(true)),
        "contentDiagnostics": quote_diagnostics,
        "citationDiagnostics": reference.get("citationDiagnostics").cloned().unwrap_or(Value::Null),
        "citationMarkdown": reference.get("citationMarkdown").cloned().unwrap_or(Value::Null),
        "citationUrl": reference.get("citationUrl").cloned().unwrap_or(Value::Null),
    })
}

fn compact_citation_for_agent(citation: &Value) -> Value {
    let display_quote = citation
        .get("displayQuote")
        .and_then(Value::as_str)
        .or_else(|| citation.get("quote").and_then(Value::as_str))
        .map(|value| value.chars().take(420).collect::<String>())
        .unwrap_or_default();
    let locator_evidence_text = citation
        .get("locatorEvidenceText")
        .and_then(Value::as_str)
        .map(|value| value.chars().take(420).collect::<String>())
        .unwrap_or_default();
    json!({
        "schema": citation.get("schema").cloned().unwrap_or_else(|| json!("mnote.knowledge_rag.citation.v1")),
        "provider": citation.get("provider").cloned().unwrap_or_else(|| json!("lightrag")),
        "citationId": citation.get("citationId").cloned().unwrap_or(Value::Null),
        "citationLabel": citation.get("citationLabel").cloned().unwrap_or(Value::Null),
        "sourceId": citation.get("sourceId").cloned().unwrap_or(Value::Null),
        "sourcePath": citation.get("sourcePath").cloned().unwrap_or(Value::Null),
        "sourceRootRelativePath": citation.get("sourceRootRelativePath").cloned().unwrap_or(Value::Null),
        "filePath": citation.get("filePath").or_else(|| citation.get("lightRagFilePath")).cloned().unwrap_or(Value::Null),
        "chunkId": citation.get("chunkId").or_else(|| citation.get("lightRagChunkId")).cloned().unwrap_or(Value::Null),
        "blockId": citation.get("blockId").cloned().unwrap_or(Value::Null),
        "headingPath": citation.get("headingPath").cloned().unwrap_or_else(|| json!([])),
        "quote": display_quote,
        "displayQuote": citation.get("displayQuote").cloned().unwrap_or(Value::Null),
        "locatorEvidenceText": locator_evidence_text,
        "searchQuery": citation.get("searchQuery").cloned().unwrap_or(Value::Null),
        "locatorPrecision": citation.get("locatorPrecision").cloned().unwrap_or(Value::Null),
        "locatorDegraded": citation.get("locatorDegraded").cloned().unwrap_or(Value::Bool(true)),
        "citationMarkdown": citation.get("citationMarkdown").cloned().unwrap_or(Value::Null),
        "citationUrl": citation.get("citationUrl").cloned().unwrap_or(Value::Null),
        "diagnostics": citation.get("diagnostics").or_else(|| citation.get("citationDiagnostics")).cloned().unwrap_or(Value::Null),
    })
}

fn citation_references_for_ui(references: &[Value]) -> Vec<&Value> {
    citation_values_for_ui(references)
}

fn citation_values_for_ui(values: &[Value]) -> Vec<&Value> {
    let has_precise = values.iter().any(|reference| {
        reference
            .get("citationMarkdown")
            .and_then(Value::as_str)
            .is_some_and(|value| !value.trim().is_empty())
            && reference.get("locatorDegraded").and_then(Value::as_bool) != Some(true)
    });
    values
        .iter()
        .filter(|reference| {
            reference
                .get("citationMarkdown")
                .and_then(Value::as_str)
                .is_some_and(|value| !value.trim().is_empty())
                && (!has_precise
                    || reference.get("locatorDegraded").and_then(Value::as_bool) != Some(true))
        })
        .collect()
}

fn quote_diagnostics(quote: &str) -> Value {
    let meaningful_lines = quote
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty())
        .filter(|line| !line.starts_with('#'))
        .filter(|line| !is_markdown_image_line(line))
        .collect::<Vec<_>>();
    json!({
        "quoteEmpty": quote.trim().is_empty(),
        "quoteOnlyImagePlaceholder": !quote.trim().is_empty() && meaningful_lines.is_empty(),
        "ocrTextExposed": !meaningful_lines.is_empty(),
    })
}

fn is_markdown_image_line(line: &str) -> bool {
    line.starts_with("![") && line.contains("](") && line.ends_with(')')
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn compact_query_result_marks_post_filter_scope_without_raw_chunks() {
        let payload = json!({
            "ok": true,
            "provider": "lightrag",
            "sourceScope": ["docs/a.md"],
            "sourceScopeMode": "post_filter_mapped_references",
            "rawScopeFiltered": false,
            "raw": {
                "status": "success",
                "chunks": [{"content": "raw chunk must not be exposed to agent"}],
                "metadata": {"count": 1}
            },
            "references": [{
                "sourceRootRelativePath": "docs/a.md",
                "quote": "scoped quote",
                "locatorDegraded": true,
                "citationMarkdown": "[来源定位降级：docs/a.md](/documents/local-md:docs~2Fa.md)"
            }, {
                "sourceRootRelativePath": "docs/b.md",
                "quote": "precise quote",
                "locatorDegraded": false,
                "citationMarkdown": "[docs/b.md · p.2](/documents/local-md:docs~2Fb.md?page=2)"
            }]
        });

        let compact = compact_query_result_for_agent(payload);
        assert_eq!(
            compact["sourceScopeMode"].as_str(),
            Some("post_filter_mapped_references")
        );
        assert_eq!(compact["rawScopeFiltered"].as_bool(), Some(false));
        assert!(compact.get("raw").is_none());
        assert!(compact.get("chunks").is_none());
        assert_eq!(
            compact["rawMetadataMeaning"].as_str(),
            Some("provider metadata describes query processing/retrieval bookkeeping; it is not source text evidence unless the same text appears in references[].quote")
        );
        assert!(compact["answerGuidance"]
            .as_str()
            .unwrap_or_default()
            .contains("Do not copy citationMarkdown"));
        assert_eq!(
            compact["uiCitations"][0]["citationMarkdown"].as_str(),
            Some("[docs/b.md · p.2](/documents/local-md:docs~2Fb.md?page=2)")
        );
        assert_eq!(compact["uiCitations"].as_array().unwrap().len(), 1);
        assert_eq!(
            compact["references"][0]["sourceRootRelativePath"].as_str(),
            Some("docs/a.md")
        );
    }

    #[test]
    fn compact_query_result_prefers_structured_citations() {
        let payload = json!({
            "references": [{
                "sourceRootRelativePath": "docs/a.docx",
                "displayQuote": "raw reference should not be source card",
                "locatorDegraded": false,
                "citationMarkdown": "[docs/a.docx](/documents/a)"
            }],
            "citations": [{
                "schema": "mnote.knowledge_rag.citation.v1",
                "citationId": "c0de",
                "citationLabel": "[c0de]",
                "sourceRootRelativePath": "docs/a.docx",
                "headingPath": ["保护基", "硅基保护"],
                "displayQuote": "吡咯烷,5 h,90%",
                "locatorEvidenceText": "如果叔丁基用 TFA 裂解, 吡咯烷将不会去除 BOC 亚乙基。",
                "locatorPrecision": "paragraph",
                "locatorDegraded": false,
                "citationMarkdown": "[docs/a.docx](/documents/a)"
            }]
        });

        let compact = compact_query_result_for_agent(payload);

        assert_eq!(compact["citations"][0]["citationId"], "c0de");
        assert_eq!(compact["citations"][0]["headingPath"][0], "保护基");
        assert_eq!(compact["citations"][0]["displayQuote"], "吡咯烷,5 h,90%");
        assert!(compact["citations"][0].get("rawQuote").is_none());
        assert_eq!(
            compact["uiCitations"][0]["citationMarkdown"].as_str(),
            Some("[docs/a.docx](/documents/a)")
        );
    }

    #[test]
    fn compact_reference_marks_image_placeholder_as_not_ocr_text() {
        let payload = json!({
            "references": [{
                "sourceRootRelativePath": "docs/image.png",
                "quote": "# image.png\n\n![image.png](<image.png>)",
                "quoteSource": "chunk",
                "locatorDegraded": true,
                "citationMarkdown": "[来源定位降级：image.png](/documents/local-md:docs~2FPage.md)"
            }]
        });

        let compact = compact_query_result_for_agent(payload);
        let diagnostics = &compact["references"][0]["contentDiagnostics"];
        assert_eq!(
            diagnostics["quoteOnlyImagePlaceholder"].as_bool(),
            Some(true)
        );
        assert_eq!(diagnostics["ocrTextExposed"].as_bool(), Some(false));
        assert_eq!(
            compact["references"][0]["quoteSource"].as_str(),
            Some("chunk")
        );
    }
}
