use std::cmp::Ordering;
use std::collections::HashSet;
use std::time::Duration;

use reqwest::blocking::Client;
use serde_json::Value;
use sha2::{Digest, Sha256};

pub struct JitContextStreamer {
    vocab_size: u32,
    max_context_tokens: usize,
    client: Client,
    api_url: Option<String>,
    api_key: Option<String>,
}

impl JitContextStreamer {
    pub fn new(
        vocab_size: u32,
        max_context_tokens: usize,
        api_url: Option<String>,
        api_key: Option<String>,
    ) -> Self {
        let client = Client::builder()
            .timeout(Duration::from_secs(6))
            .build()
            .unwrap_or_else(|_| Client::new());
        Self {
            vocab_size: vocab_size.max(1),
            max_context_tokens: max_context_tokens.max(1),
            client,
            api_url,
            api_key,
        }
    }

    pub fn fetch_and_pack_context(&self, user_prompt: &str) -> Vec<u32> {
        let docs = self.collect_context(user_prompt);
        let ranked = self.rank_lines(user_prompt, &docs);
        let mut packed = Vec::with_capacity(self.max_context_tokens);

        for line in ranked {
            for tok in tokenize(&line) {
                let digest = Sha256::digest(tok.as_bytes());
                let id = u64::from_le_bytes([
                    digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[6],
                    digest[7],
                ]) % self.vocab_size as u64;
                packed.push(id as u32);
                if packed.len() >= self.max_context_tokens {
                    break;
                }
            }
            if packed.len() >= self.max_context_tokens {
                break;
            }
        }

        if packed.is_empty() {
            vec![0]
        } else {
            packed
        }
    }

    fn collect_context(&self, query: &str) -> Vec<String> {
        let subqueries = decompose_query(query, 4);
        let mut docs = Vec::new();

        if let Some(api_url) = &self.api_url {
            for sq in subqueries {
                if let Ok(mut fetched) = self.fetch_live_docs(api_url, &sq) {
                    docs.append(&mut fetched);
                }
            }
        }

        if docs.is_empty() {
            return vec![
                format!(
                    "# Offline Context\nNo live API response for query: {query}\nUsing local fallback retrieval surface."
                ),
                format!(
                    "## Notes\nEnable --context-api-url (or AXIOM_CONTEXT_API_URL) to inject live markdown context for: {query}"
                ),
            ];
        }

        docs
    }

    fn fetch_live_docs(&self, base_url: &str, query: &str) -> Result<Vec<String>, reqwest::Error> {
        let encoded = urlencoding::encode(query);
        let target = if base_url.contains("{query}") {
            base_url.replace("{query}", &encoded)
        } else if base_url.contains('?') {
            format!("{base_url}&q={encoded}")
        } else {
            format!("{base_url}?q={encoded}")
        };

        let mut request = self.client.get(target).header(
            "accept",
            "application/json,text/markdown;q=0.9,text/plain;q=0.9",
        );
        if let Some(api_key) = &self.api_key {
            request = request.bearer_auth(api_key);
        }

        let response = request.send()?.error_for_status()?;
        let body = response.text()?;

        let parsed = parse_documents(&body);
        if parsed.is_empty() {
            Ok(vec![body])
        } else {
            Ok(parsed)
        }
    }

    fn rank_lines(&self, query: &str, docs: &[String]) -> Vec<String> {
        let query_terms = tokenize(query);
        let query_set: HashSet<&str> = query_terms.iter().map(String::as_str).collect();
        let mut seen = HashSet::new();
        let mut scored: Vec<(f32, String)> = Vec::new();

        for doc in docs {
            for raw_line in doc.lines() {
                let line = raw_line.trim().to_lowercase();
                if line.is_empty() || !seen.insert(line.clone()) {
                    continue;
                }
                let line_terms = tokenize(&line);
                if line_terms.is_empty() {
                    continue;
                }
                let overlap = line_terms
                    .iter()
                    .filter(|term| query_set.contains(term.as_str()))
                    .count() as f32;
                let score = overlap / (line_terms.len() as f32);
                if score > 0.0 {
                    scored.push((score, line));
                }
            }
        }

        scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));
        scored.into_iter().map(|(_, line)| line).collect()
    }
}

fn tokenize(text: &str) -> Vec<String> {
    text.split(|c: char| !c.is_alphanumeric() && c != '_')
        .filter(|s| !s.is_empty())
        .map(|s| s.to_lowercase())
        .collect()
}

fn decompose_query(query: &str, max_subqueries: usize) -> Vec<String> {
    let mut out = vec![query.to_string()];
    for term in tokenize(query)
        .into_iter()
        .filter(|t| t.len() > 3)
        .take(max_subqueries.saturating_sub(1))
    {
        out.push(term);
    }
    out
}

fn parse_documents(body: &str) -> Vec<String> {
    fn collect_strings(value: &Value, out: &mut Vec<String>) {
        match value {
            Value::String(s) => {
                let trimmed = s.trim();
                if !trimmed.is_empty() {
                    out.push(trimmed.to_string());
                }
            }
            Value::Array(values) => {
                for item in values {
                    collect_strings(item, out);
                }
            }
            Value::Object(map) => {
                for value in map.values() {
                    collect_strings(value, out);
                }
            }
            _ => {}
        }
    }

    let mut docs = Vec::new();
    if let Ok(json) = serde_json::from_str::<Value>(body) {
        collect_strings(&json, &mut docs);
    }
    docs
}
