use crate::app::AppState;
use crate::context::RequestContext;
use crate::error::WebError;
use crate::routes::local_folder_source;
use axum::extract::{Extension, State};
use axum::http::{HeaderMap, HeaderValue, StatusCode};
use axum::Json;
use core_protocol::{
    EvidenceLocator, EvidenceOpenRequest, EvidenceReadRequest, EvidenceSearchRequest,
};
use serde_json::{json, Value};

pub async fn search(
    State(_state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Json(_body): Json<EvidenceSearchRequest>,
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
    let mut headers = HeaderMap::new();
    headers.insert("content-type", HeaderValue::from_static("application/json"));
    Ok((
        StatusCode::GONE,
        headers,
        Json(json!({
            "ok": false,
            "code": "mnote_evidence_search_retired",
            "message": "旧 LiteParse/evidence 搜索已退役；PDF、Office、图片 OCR 与资料索引统一走 /api/knowledge-rag/query",
            "requestId": context.trace.request_id,
            "traceId": context.trace.trace_id,
        })),
    ))
}

pub(crate) fn citation_markdown_for_locator(locator: &EvidenceLocator) -> String {
    let label = citation_label_for_locator(locator);
    let url = citation_url_for_locator(locator);
    format!(
        "[{}]({})",
        markdown_link_label_escape(&label),
        url.replace(')', "%29")
    )
}

fn citation_label_for_locator(locator: &EvidenceLocator) -> String {
    let source_path = locator
        .resource_path
        .as_deref()
        .filter(|value| !value.trim().is_empty())
        .unwrap_or(locator.owner_document_path.as_str());
    let name = source_path
        .rsplit('/')
        .find(|part| !part.trim().is_empty())
        .unwrap_or(source_path)
        .trim();
    let mut parts = vec![if name.is_empty() {
        "证据".to_string()
    } else {
        name.to_string()
    }];
    if let Some(page) = locator.page {
        parts.push(format!("p.{page}"));
    }
    if let Some(section) = locator
        .section_path
        .last()
        .filter(|value| !value.trim().is_empty())
    {
        parts.push(section.to_string());
    }
    parts.join(" · ")
}

pub(crate) fn citation_url_for_locator(locator: &EvidenceLocator) -> String {
    let owner_document_id = locator.owner_document_id.trim();
    let mut url = if owner_document_id.is_empty() {
        let existing = locator.open_action.url.trim();
        if existing.is_empty() {
            "/".to_string()
        } else {
            existing.to_string()
        }
    } else {
        format!("/documents/{owner_document_id}")
    };
    append_query_param(&mut url, "sourceKind", "local_folder");
    append_query_param(&mut url, "rootUri", locator.root_uri.trim());
    if let Some(resource_path) = locator
        .resource_path
        .as_deref()
        .filter(|value| !value.trim().is_empty())
    {
        append_query_param(
            &mut url,
            "resourceTab",
            &format!(
                "resource:file:{}:{}",
                locator.root_uri.trim(),
                resource_path
            ),
        );
        append_query_param(&mut url, "resourcePath", resource_path);
    }
    if let Some(page) = locator.page {
        append_query_param(&mut url, "page", &page.to_string());
    }
    if let Some(bbox) = &locator.bbox {
        append_query_param(
            &mut url,
            "bbox",
            &format!("{},{},{},{}", bbox.x0, bbox.y0, bbox.x1, bbox.y1),
        );
    }
    if let Some(block_id) = locator.block_id.as_deref() {
        append_query_param(&mut url, "blockId", block_id);
    }
    if let Some(source_map_path) = locator.source_map_path.as_deref() {
        append_query_param(&mut url, "sourceMapPath", source_map_path);
    }
    append_open_action_param(&mut url, &locator.open_action.params, "paragraphOrdinal");
    append_open_action_param(&mut url, &locator.open_action.params, "paraIdStart");
    append_open_action_param(&mut url, &locator.open_action.params, "paraIdEnd");
    append_open_action_param(&mut url, &locator.open_action.params, "textFingerprint");
    if let Some(query) = locator
        .open_action
        .params
        .get("query")
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        append_query_param(&mut url, "evidenceText", query);
    }
    if let Some(search_query) = locator
        .open_action
        .params
        .get("searchQuery")
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        append_query_param(&mut url, "searchQuery", search_query);
    }
    if let Some(line_range) = &locator.line_range {
        append_query_param(
            &mut url,
            "lineRange",
            &format!("{}-{}", line_range.start, line_range.end),
        );
    }
    if let Some(char_range) = &locator.char_range {
        append_query_param(
            &mut url,
            "charRange",
            &format!("{}-{}", char_range.start, char_range.end),
        );
    }
    url
}

fn append_open_action_param(url: &mut String, params: &Value, key: &str) {
    let Some(value) = params.get(key) else {
        return;
    };
    if let Some(text) = value.as_str() {
        append_query_param(url, key, text);
    } else if value.is_number() || value.is_boolean() {
        append_query_param(url, key, &value.to_string());
    }
}

fn append_query_param(url: &mut String, key: &str, value: &str) {
    let value = value.trim();
    if value.is_empty() {
        return;
    }
    let separator = if url.contains('?') { '&' } else { '?' };
    url.push(separator);
    url.push_str(&encode_query_component(key));
    url.push('=');
    url.push_str(&encode_query_component(value));
}

fn encode_query_component(value: &str) -> String {
    let mut encoded = String::with_capacity(value.len());
    for byte in value.as_bytes() {
        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~') {
            encoded.push(*byte as char);
        } else {
            encoded.push_str(&format!("%{byte:02X}"));
        }
    }
    encoded
}

fn markdown_link_label_escape(value: &str) -> String {
    value
        .replace('\\', "\\\\")
        .replace('[', "\\[")
        .replace(']', "\\]")
}

pub async fn read(
    State(_state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Json(_body): Json<EvidenceReadRequest>,
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
    let mut headers = HeaderMap::new();
    headers.insert("content-type", HeaderValue::from_static("application/json"));
    Ok((
        StatusCode::GONE,
        headers,
        Json(json!({
            "ok": false,
            "code": "mnote_evidence_read_retired",
            "message": "旧 evidence 读回已退役；Agent 引文打开请使用 /api/knowledge-rag/open-reference 或 citationUrl",
            "requestId": context.trace.request_id,
            "traceId": context.trace.trace_id,
        })),
    ))
}

pub async fn open(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Json(body): Json<EvidenceOpenRequest>,
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
    let response = open_payload(&state, &context, body).await?;
    let mut headers = HeaderMap::new();
    headers.insert("content-type", HeaderValue::from_static("application/json"));
    Ok((StatusCode::OK, headers, Json(response)))
}

pub(crate) async fn open_payload(
    state: &AppState,
    context: &RequestContext,
    body: EvidenceOpenRequest,
) -> Result<Value, WebError> {
    let mut locator = body.locator;
    let citation_url = citation_url_for_locator(&locator);
    locator.open_action.url = citation_url.clone();
    let root_uri = locator.root_uri.trim().to_string();
    if root_uri.is_empty() {
        return Err(
            WebError::bad_request_code("evidence_root_required", "证据打开缺少 rootUri")
                .with_context(context),
        );
    }
    let _ = local_folder_source::ensure_local_workspace_read_access_with_state(
        state, context, &root_uri,
    )
    .map_err(|error| error.with_context(context))?;
    let response = json!({
        "ok": true,
        "locator": locator.clone(),
        "openAction": locator.open_action,
        "citationUrl": citation_url,
        "citationLabel": citation_label_for_locator(&locator),
        "citationMarkdown": citation_markdown_for_locator(&locator),
    });
    Ok(response)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::app::{build_app, AppConfig, AppState};
    use axum::body::{to_bytes, Body};
    use axum::http::{Request, StatusCode};
    use core_protocol::{EvidenceOpenAction, EvidenceResourceKind};
    use tower::util::ServiceExt;

    #[tokio::test]
    async fn evidence_search_route_returns_retired_guard() {
        let app = test_app();
        let response = app
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/evidence/search")
                    .header("content-type", "application/json")
                    .header("x-mnote-actor-id", "user_test")
                    .header("x-mnote-actor-type", "user")
                    .body(Body::from(
                        json!({
                            "query": "SqliteRouteToken",
                            "scope": {
                                "workspaceId": "local-ws-evidence-route",
                                "rootUri": "file:///workspace",
                                "targetDocumentId": "local-md:missing.md",
                                "includeResources": true,
                                "includeOcr": true
                            },
                            "mode": "hybrid",
                            "topK": 5
                        })
                        .to_string(),
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::GONE);
        let body = to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("json");
        assert_eq!(payload["ok"], false);
        assert_eq!(
            payload["code"].as_str(),
            Some("mnote_evidence_search_retired")
        );
    }

    #[tokio::test]
    async fn evidence_read_route_returns_retired_guard() {
        let app = test_app();
        let locator = sample_locator("file:///workspace");
        let response = app
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/evidence/read")
                    .header("content-type", "application/json")
                    .header("x-mnote-actor-id", "user_test")
                    .header("x-mnote-actor-type", "user")
                    .body(Body::from(
                        json!({
                            "locator": locator
                        })
                        .to_string(),
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::GONE);
        let body = to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("json");
        assert_eq!(payload["ok"], false);
        assert_eq!(
            payload["code"].as_str(),
            Some("mnote_evidence_read_retired")
        );
    }

    #[test]
    fn evidence_citation_url_keeps_locator_bridge_params() {
        let mut locator = sample_locator("file:///workspace");
        locator.page = Some(3);
        locator.block_id = Some("para-7".into());
        locator.resource_path = Some("docs/Page.assets/spec.pdf".into());
        locator.source_map_path = Some("docs/Page.assets/spec.source-map.json".into());

        let url = citation_url_for_locator(&locator);

        assert!(url.starts_with("/documents/local-md:docs~2FPage.md?"));
        assert!(url.contains("sourceKind=local_folder"));
        assert!(url.contains("rootUri=file%3A%2F%2F%2Fworkspace"));
        assert!(url.contains("resourcePath=docs%2FPage.assets%2Fspec.pdf"));
        assert!(url.contains("page=3"));
        assert!(url.contains("blockId=para-7"));
    }

    fn sample_locator(root_uri: &str) -> EvidenceLocator {
        EvidenceLocator::new(
            root_uri,
            "local-md:docs~2FPage.md",
            "docs/Page.md",
            EvidenceResourceKind::Pdf,
            EvidenceOpenAction {
                action_type: "mnote.open_resource_locator".into(),
                url: "/documents/local-md:docs~2FPage.md".into(),
                params: json!({"resourcePath":"docs/Page.assets/spec.pdf"}),
            },
        )
    }

    fn test_app() -> axum::Router {
        build_app(AppState::new(AppConfig {
            service_name: "mnote-web".into(),
            service_version: "0.1.0".into(),
            bind_addr: "127.0.0.1:0".into(),
            public_bind_addr: "127.0.0.1:3000".into(),
            legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
            enable_legacy_next_compat: true,
            enable_debug_shell_routes: false,
            enable_editor_actor: true,
            hermes_base_path: "/api/hermes".into(),
            compat_next_base_path: "/api/compat/next".into(),
            convex_url: None,
            convex_admin_key: None,
            allow_dev_fixtures: true,
            query_fixtures_json: None,
            mutation_fixtures_json: None,
            dev_user_id: "dev-user".into(),
            dev_user_name: "开发用户".into(),
            dev_user_email: "dev@mnote.local".into(),
        }))
    }
}
