//! LLM classifier fallback for documents whose rule-based confidence is low.
//!
//! Activated only when:
//!   1. `ANTHROPIC_API_KEY` environment variable is set.
//!   2. The rule-based classifier returned confidence < `REVIEW_THRESHOLD` (0.75).
//!
//! The LLM is asked to choose one of the 10 knowledge stores and return a JSON
//! object with its choice and a one-sentence reason. The response is parsed and,
//! if confidence ≥ 0.5, applied as the document's classification.
//!
//! # Privacy note
//! Only the document title and the first 600 characters of the normalized text
//! excerpt are sent to the API — not the full document content. Set
//! `P3AK_VAULT_LLM_EXCERPT_CHARS` to override the excerpt length (max 2000).

use anyhow::{bail, Result};
use vault_core::types::{Classification, StoreId};

/// Maximum excerpt length sent to the LLM (characters of normalized text).
const DEFAULT_EXCERPT_CHARS: usize = 600;
/// Minimum LLM-reported confidence required to accept its classification.
const MIN_LLM_CONFIDENCE: f64 = 0.50;
/// Anthropic API endpoint for Messages.
const ANTHROPIC_API_URL: &str = "https://api.anthropic.com/v1/messages";
/// Model to use — haiku is fast and cheap for classification.
const MODEL: &str = "claude-haiku-4-5";
/// API version header value.
const ANTHROPIC_VERSION: &str = "2023-06-01";

/// Attempt LLM classification for a document.
///
/// Returns `Ok(Some(Classification))` if the LLM returned a valid result with
/// confidence ≥ `MIN_LLM_CONFIDENCE`.
/// Returns `Ok(None)` if the API key is not set or the LLM declined to classify.
/// Returns `Err` only on network / JSON parse failures (callers should log and
/// fall back to the rule-based result rather than hard-failing).
pub fn llm_classify(title: &str, normalized_text: &str) -> Result<Option<Classification>> {
    let api_key = match std::env::var("ANTHROPIC_API_KEY") {
        Ok(k) if !k.trim().is_empty() => k,
        _ => return Ok(None), // not configured — silently skip
    };

    let excerpt_chars = std::env::var("P3AK_VAULT_LLM_EXCERPT_CHARS")
        .ok()
        .and_then(|s| s.parse::<usize>().ok())
        .unwrap_or(DEFAULT_EXCERPT_CHARS)
        .min(2000);

    let excerpt: String = normalized_text.chars().take(excerpt_chars).collect();

    let prompt = build_prompt(title, &excerpt);
    let response_json = call_anthropic(&api_key, &prompt)?;
    parse_response(&response_json)
}

// ─── prompt ───────────────────────────────────────────────────────────────────

fn build_prompt(title: &str, excerpt: &str) -> String {
    format!(
        r#"Classify the following document into exactly ONE knowledge store.

Available stores (use the two-letter code):
  HC  Healthcare — clinical, medical, insurance, HIPAA
  LG  Legal — contracts, compliance, regulation, litigation
  FN  Finance — accounting, budgets, revenue, investment
  OP  Operations — logistics, facilities, supply chain, process
  HR  Human Resources — hiring, compensation, benefits, culture
  TC  Technology — software, infrastructure, security, architecture
  MS  Marketing/Sales — campaigns, pipeline, brand, customer
  IN  Insurance — policy terms, coverage, premiums, claims
  GV  Government/Regulatory — public sector, government, regulatory filings
  ED  Education/Research — research, academic, training, curriculum

Document title: {title}

Document excerpt:
{excerpt}

Respond with ONLY a JSON object (no prose, no markdown):
{{"store": "XX", "confidence": 0.0-1.0, "reason": "one sentence explaining why"}}"#,
        title = title,
        excerpt = excerpt,
    )
}

// ─── API call ─────────────────────────────────────────────────────────────────

fn call_anthropic(api_key: &str, prompt: &str) -> Result<serde_json::Value> {
    let body = serde_json::json!({
        "model": MODEL,
        "max_tokens": 256,
        "messages": [{"role": "user", "content": prompt}]
    });

    let response = ureq::post(ANTHROPIC_API_URL)
        .set("x-api-key", api_key)
        .set("anthropic-version", ANTHROPIC_VERSION)
        .set("content-type", "application/json")
        .send_json(&body);

    let response = match response {
        Ok(r) => r,
        Err(ureq::Error::Status(code, resp)) => {
            let body = resp.into_string().unwrap_or_default();
            bail!("Anthropic API returned HTTP {}: {}", code, body);
        }
        Err(e) => bail!("Anthropic API network error: {}", e),
    };

    let value: serde_json::Value = response.into_json()?;
    Ok(value)
}

// ─── response parsing ─────────────────────────────────────────────────────────

fn parse_response(api_response: &serde_json::Value) -> Result<Option<Classification>> {
    // Extract the text content from the Messages API response
    let content_text = api_response
        .get("content")
        .and_then(|c| c.as_array())
        .and_then(|arr| arr.first())
        .and_then(|block| block.get("text"))
        .and_then(|t| t.as_str())
        .unwrap_or("")
        .trim();

    if content_text.is_empty() {
        return Ok(None);
    }

    // Strip markdown fences if the model ignores our "no markdown" instruction
    let json_str = strip_markdown_fences(content_text);

    let parsed: serde_json::Value = serde_json::from_str(json_str.trim())
        .map_err(|e| anyhow::anyhow!("LLM returned non-JSON: {:?} — error: {}", content_text, e))?;

    let store_code = parsed
        .get("store")
        .and_then(|s| s.as_str())
        .unwrap_or("")
        .trim()
        .to_uppercase();

    let confidence = parsed
        .get("confidence")
        .and_then(|c| c.as_f64())
        .unwrap_or(0.0);

    let reason = parsed
        .get("reason")
        .and_then(|r| r.as_str())
        .unwrap_or("")
        .to_string();

    if confidence < MIN_LLM_CONFIDENCE {
        return Ok(None); // LLM itself is uncertain — skip
    }

    let store_id = match StoreId::from_str(&store_code) {
        Some(s) => s,
        None => {
            bail!(
                "LLM returned unknown store code '{}' (confidence {:.2})",
                store_code,
                confidence
            );
        }
    };

    Ok(Some(Classification {
        store_id,
        category: "LLM-classified".to_string(),
        subcategory: reason,
        confidence: confidence as f32,
        tags: vec![],
        secondary: None,
        rule_based: false,
    }))
}

fn strip_markdown_fences(s: &str) -> &str {
    let s = s.trim();
    let s = s.strip_prefix("```json").unwrap_or(s);
    let s = s.strip_prefix("```").unwrap_or(s);
    let s = s.strip_suffix("```").unwrap_or(s);
    s.trim()
}

// ─── unit tests ───────────────────────────────────────────────────────────────

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

    #[test]
    fn test_parse_valid_response() {
        let api_resp = serde_json::json!({
            "content": [{"type": "text", "text": r#"{"store":"LG","confidence":0.92,"reason":"Contract language and compliance terms dominate."}"#}]
        });
        let result = parse_response(&api_resp).unwrap();
        let cls = result.unwrap();
        assert_eq!(cls.store_id, StoreId::Lg);
        assert!((cls.confidence - 0.92).abs() < 0.01);
        assert!(!cls.rule_based);
    }

    #[test]
    fn test_parse_markdown_fenced_response() {
        let api_resp = serde_json::json!({
            "content": [{"type": "text", "text": "```json\n{\"store\":\"HR\",\"confidence\":0.85,\"reason\":\"Hiring and compensation.\"}\n```"}]
        });
        let result = parse_response(&api_resp).unwrap();
        let cls = result.unwrap();
        assert_eq!(cls.store_id, StoreId::Hr);
    }

    #[test]
    fn test_parse_low_confidence_returns_none() {
        let api_resp = serde_json::json!({
            "content": [{"type": "text", "text": r#"{"store":"TC","confidence":0.3,"reason":"Ambiguous."}"#}]
        });
        let result = parse_response(&api_resp).unwrap();
        assert!(result.is_none());
    }

    #[test]
    fn test_parse_unknown_store_errors() {
        let api_resp = serde_json::json!({
            "content": [{"type": "text", "text": r#"{"store":"XX","confidence":0.9,"reason":"Unknown."}"#}]
        });
        assert!(parse_response(&api_resp).is_err());
    }

    #[test]
    fn test_no_api_key_returns_none() {
        // Temporarily unset the key so the function returns None cleanly
        let original = std::env::var("ANTHROPIC_API_KEY").ok();
        std::env::remove_var("ANTHROPIC_API_KEY");
        let result = llm_classify("Test doc", "Some content").unwrap();
        assert!(result.is_none());
        if let Some(key) = original {
            std::env::set_var("ANTHROPIC_API_KEY", key);
        }
    }

    #[test]
    fn test_build_prompt_contains_title() {
        let prompt = build_prompt("Privacy Policy v2", "This policy describes...");
        assert!(prompt.contains("Privacy Policy v2"));
        assert!(prompt.contains("This policy describes"));
    }

    #[test]
    fn test_strip_markdown_fences() {
        assert_eq!(
            strip_markdown_fences("```json\n{\"a\":1}\n```"),
            "{\"a\":1}"
        );
        assert_eq!(strip_markdown_fences("{\"a\":1}"), "{\"a\":1}");
    }
}
