//! Tolerant extractors that pull workbench panes out of saved artifacts.
//!
//! Each extractor takes a `serde_json::Value` (a tools/list snapshot, a
//! cassette, a run report, a security report, an evidence pack) and returns the
//! normalized rows the HTML renderer shows. They are deliberately forgiving:
//! a missing field yields an empty pane, never an error, so the workbench opens
//! on whatever the user happens to have.

use serde_json::Value;

use super::model::{CapItem, EvidenceMeta, ResultRow, ResultsSummary, SecItem, TraceEntry};

/// Pull tools, resources, or prompts from a snapshot under the given key.
pub fn capabilities(snapshot: &Value, key: &str) -> Vec<CapItem> {
    let array = snapshot
        .get(key)
        .or_else(|| snapshot.pointer(&format!("/result/{key}")))
        .and_then(Value::as_array)
        .cloned()
        .unwrap_or_default();
    array
        .iter()
        .filter_map(|item| {
            let name = item.get("name").and_then(Value::as_str)?.to_string();
            let description = item
                .get("description")
                .and_then(Value::as_str)
                .unwrap_or("")
                .to_string();
            Some(CapItem { name, description })
        })
        .collect()
}

/// Pull a JSON-RPC trace from a cassette, tolerant of several shapes.
pub fn trace(cassette: &Value) -> Vec<TraceEntry> {
    let entries = first_array(
        cassette,
        &["interactions", "entries", "messages", "exchanges"],
    );
    let mut out = Vec::new();
    for entry in entries {
        // A request/response pair, or a single directional message.
        if let Some(request) = entry.get("request") {
            out.push(TraceEntry {
                direction: "request".to_string(),
                method: method_of(request),
                summary: preview(request),
            });
            if let Some(response) = entry.get("response") {
                out.push(TraceEntry {
                    direction: "response".to_string(),
                    method: method_of(request),
                    summary: preview(response),
                });
            }
        } else {
            out.push(TraceEntry {
                direction: entry
                    .get("direction")
                    .and_then(Value::as_str)
                    .unwrap_or("message")
                    .to_string(),
                method: method_of(entry),
                summary: preview(entry),
            });
        }
    }
    out
}

/// Pull the run results summary and per-test rows from a run report.
pub fn results(report: &Value) -> Option<ResultsSummary> {
    let summary = report.get("summary")?;
    let count = |k: &str| summary.get(k).and_then(Value::as_u64).unwrap_or(0) as usize;
    let rows = report
        .get("tests")
        .and_then(Value::as_array)
        .map(|tests| {
            tests
                .iter()
                .filter_map(|t| {
                    let name = t.get("name").and_then(Value::as_str)?.to_string();
                    let status = t
                        .get("status")
                        .and_then(Value::as_str)
                        .map(str::to_string)
                        .unwrap_or_else(|| match t.get("passed").and_then(Value::as_bool) {
                            Some(true) => "passed".to_string(),
                            Some(false) => "failed".to_string(),
                            None => "unknown".to_string(),
                        });
                    Some(ResultRow { name, status })
                })
                .collect()
        })
        .unwrap_or_default();
    Some(ResultsSummary {
        total: count("total"),
        passed: count("passed"),
        failed: count("failed"),
        skipped: count("skipped"),
        rows,
    })
}

/// Pull security findings from a security report.
pub fn security(report: &Value) -> Vec<SecItem> {
    let findings = first_array(report, &["findings", "results"]);
    findings
        .iter()
        .filter_map(|f| {
            let id = f
                .get("id")
                .or_else(|| f.get("rule_id"))
                .or_else(|| f.get("ruleId"))
                .and_then(Value::as_str)
                .unwrap_or("")
                .to_string();
            let severity = f
                .get("severity")
                .and_then(Value::as_str)
                .unwrap_or("info")
                .to_string();
            let title = f
                .get("title")
                .or_else(|| f.get("message"))
                .and_then(value_text)
                .unwrap_or_default();
            if id.is_empty() && title.is_empty() {
                return None;
            }
            Some(SecItem {
                id,
                severity,
                title,
            })
        })
        .collect()
}

/// Pull evidence metadata (schema, digest, sections) from an evidence pack.
pub fn evidence(pack: &Value) -> Option<EvidenceMeta> {
    let schema = pack
        .get("schema_version")
        .and_then(Value::as_str)?
        .to_string();
    let digest = pack
        .get("digest")
        .and_then(Value::as_str)
        .unwrap_or("")
        .to_string();
    let generated_at = pack
        .get("generated_at")
        .and_then(Value::as_str)
        .unwrap_or("")
        .to_string();
    let sections = pack
        .get("sections")
        .and_then(Value::as_object)
        .map(|m| m.keys().cloned().collect())
        .unwrap_or_default();
    Some(EvidenceMeta {
        schema,
        digest,
        generated_at,
        sections,
    })
}

/// The first present array among candidate keys.
fn first_array<'a>(value: &'a Value, keys: &[&str]) -> Vec<&'a Value> {
    for key in keys {
        if let Some(arr) = value.get(key).and_then(Value::as_array) {
            return arr.iter().collect();
        }
    }
    Vec::new()
}

/// The JSON-RPC method of a message, when present.
fn method_of(message: &Value) -> String {
    message
        .get("method")
        .and_then(Value::as_str)
        .unwrap_or("")
        .to_string()
}

/// A short one-line preview of a JSON value.
fn preview(value: &Value) -> String {
    let s = value.to_string();
    // Clip on a char boundary, not a byte offset: recorded content whose 161st
    // byte lands mid-multibyte-char would panic a byte slice.
    if s.chars().count() > 160 {
        let clipped: String = s.chars().take(160).collect();
        format!("{clipped}...")
    } else {
        s
    }
}

/// Text from a string-or-object message field.
fn value_text(value: &Value) -> Option<String> {
    match value {
        Value::String(s) => Some(s.clone()),
        Value::Object(o) => o.get("text").and_then(Value::as_str).map(str::to_string),
        _ => None,
    }
}

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

    #[test]
    fn preview_clips_multibyte_without_panicking() {
        // 200 multibyte chars: a byte slice at 160 would land mid-char and panic.
        let long = serde_json::json!("é".repeat(200));
        let out = preview(&long);
        assert!(out.ends_with("..."));
    }
}
